ソースを参照

✨ feat(admin): 新增完整的部门管理功能

- 在管理菜单中添加【部门管理】入口,支持部门树形结构管理
- 创建Departments页面组件,实现部门的新增、编辑、删除功能
- 添加部门相关API路由配置,支持RESTful接口调用
- 实现Department实体类,包含部门基本信息、层级关系、负责人等字段
- 新增UserDepartment关联实体,支持用户与部门的多对多关系
- 集成数据权限服务,实现基于部门的数据访问控制
- 创建部门服务类,提供部门树查询、成员管理等业务逻辑
- 添加权限管理相关实体,支持细粒度的数据权限控制
yourname 8 ヶ月 前
コミット
62dba355d5

+ 7 - 0
src/client/admin/menu.tsx

@@ -168,6 +168,13 @@ export const useMenu = () => {
           icon: <HistoryOutlined />,
           icon: <HistoryOutlined />,
           path: '/admin/logs',
           path: '/admin/logs',
           permission: 'log:view'
           permission: 'log:view'
+        },
+        {
+          key: 'departments',
+          label: '部门管理',
+          icon: <TeamOutlined />,
+          path: '/admin/departments',
+          permission: 'department:manage'
         }
         }
       ]
       ]
     },
     },

+ 312 - 0
src/client/admin/pages/Departments.tsx

@@ -0,0 +1,312 @@
+import React, { useState } from 'react';
+import { Table, Button, Modal, Form, Input, TreeSelect, Switch, message, Space, Popconfirm } from 'antd';
+import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { departmentsClient } from '@/client/api';
+
+const Departments: React.FC = () => {
+  const [form] = Form.useForm();
+  const [modalVisible, setModalVisible] = useState(false);
+  const [editingRecord, setEditingRecord] = useState<any>(null);
+  const queryClient = useQueryClient();
+
+  // 查询部门列表
+  const { data: departmentsData, isLoading } = useQuery({
+    queryKey: ['departments'],
+    queryFn: async () => {
+      const response = await departmentsClient.$get({ query: { page: 1, pageSize: 100 } });
+      if (response.status !== 200) throw new Error('获取部门列表失败');
+      return response.json();
+    }
+  });
+
+  // 创建部门
+  const createMutation = useMutation({
+    mutationFn: async (data: any) => {
+      const response = await departmentsClient.$post({ json: data });
+      return response.json();
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['departments'] });
+      message.success('部门创建成功');
+      setModalVisible(false);
+      form.resetFields();
+    },
+    onError: (error) => {
+      message.error(error instanceof Error ? error.message : '创建部门失败');
+    }
+  });
+
+  // 更新部门
+  const updateMutation = useMutation({
+    mutationFn: async ({ id, data }: { id: number; data: any }) => {
+      const response = await departmentsClient[':id'].$put({ 
+        param: { id }, 
+        json: data 
+      });
+      return response.json();
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['departments'] });
+      message.success('部门更新成功');
+      setModalVisible(false);
+      form.resetFields();
+    },
+    onError: (error) => {
+      message.error(error instanceof Error ? error.message : '更新部门失败');
+    }
+  });
+
+  // 删除部门
+  const deleteMutation = useMutation({
+    mutationFn: async (id: number) => {
+      await departmentsClient[':id'].$delete({ param: { id } });
+    },
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['departments'] });
+      message.success('部门删除成功');
+    },
+    onError: (error) => {
+      message.error(error instanceof Error ? error.message : '删除部门失败');
+    }
+  });
+
+  // 构建部门树形数据
+  const buildDepartmentTree = (departments: any[]) => {
+    const tree: any[] = [];
+    const map = new Map<number, any>();
+
+    // 创建节点映射
+    departments.forEach(dept => {
+      map.set(dept.id, {
+        title: dept.name,
+        value: dept.id,
+        key: dept.id.toString(),
+        children: []
+      });
+    });
+
+    // 构建树形结构
+    departments.forEach(dept => {
+      const node = map.get(dept.id)!;
+      if (dept.parentId && map.has(dept.parentId)) {
+        const parent = map.get(dept.parentId)!;
+        if (!parent.children) parent.children = [];
+        parent.children.push(node);
+      } else {
+        tree.push(node);
+      }
+    });
+
+    return tree;
+  };
+
+  // 表格列配置
+  const columns = [
+    {
+      title: '部门名称',
+      dataIndex: 'name',
+      key: 'name',
+      fixed: 'left' as const,
+    },
+    {
+      title: '部门编码',
+      dataIndex: 'code',
+      key: 'code',
+    },
+    {
+      title: '上级部门',
+      dataIndex: 'parentId',
+      key: 'parentId',
+      render: (parentId: number) => {
+        if (!parentId) return '-';
+        const parent = departmentsData?.data.find((d: any) => d.id === parentId);
+        return parent?.name || '-';
+      },
+    },
+    {
+      title: '排序',
+      dataIndex: 'sortOrder',
+      key: 'sortOrder',
+      width: 80,
+    },
+    {
+      title: '状态',
+      dataIndex: 'isActive',
+      key: 'isActive',
+      render: (isActive: number) => (
+        <Switch checked={isActive === 1} disabled />
+      ),
+      width: 80,
+    },
+    {
+      title: '描述',
+      dataIndex: 'description',
+      key: 'description',
+      ellipsis: true,
+    },
+    {
+      title: '操作',
+      key: 'action',
+      fixed: 'right' as const,
+      width: 120,
+      render: (_: any, record: any) => (
+        <Space>
+          <Button
+            type="link"
+            icon={<EditOutlined />}
+            onClick={() => handleEdit(record)}
+          />
+          <Popconfirm
+            title="确定要删除这个部门吗?"
+            onConfirm={() => deleteMutation.mutate(record.id)}
+            okText="确定"
+            cancelText="取消"
+          >
+            <Button
+              type="link"
+              danger
+              icon={<DeleteOutlined />}
+              loading={deleteMutation.isPending}
+            />
+          </Popconfirm>
+        </Space>
+      ),
+    },
+  ];
+
+  const handleAdd = () => {
+    setEditingRecord(null);
+    form.resetFields();
+    setModalVisible(true);
+  };
+
+  const handleEdit = (record: any) => {
+    setEditingRecord(record);
+    form.setFieldsValue({
+      ...record,
+      isActive: record.isActive === 1
+    });
+    setModalVisible(true);
+  };
+
+  const handleSubmit = async (values: any) => {
+    const data = {
+      ...values,
+      isActive: values.isActive ? 1 : 0
+    };
+
+    if (editingRecord) {
+      updateMutation.mutate({ id: editingRecord.id, data });
+    } else {
+      createMutation.mutate(data);
+    }
+  };
+
+  const departmentTree = departmentsData ? buildDepartmentTree(departmentsData.data) : [];
+
+  return (
+    <div className="p-6">
+      <div className="mb-4 flex justify-between items-center">
+        <h2 className="text-xl font-bold">部门管理</h2>
+        <Button
+          type="primary"
+          icon={<PlusOutlined />}
+          onClick={handleAdd}
+        >
+          新增部门
+        </Button>
+      </div>
+
+      <Table
+        columns={columns}
+        dataSource={departmentsData?.data || []}
+        loading={isLoading}
+        rowKey="id"
+        scroll={{ x: 1000 }}
+        pagination={{
+          total: departmentsData?.pagination.total || 0,
+          pageSize: 10,
+          showSizeChanger: true,
+          showQuickJumper: true,
+          showTotal: (total: number) => `共 ${total} 条记录`,
+        }}
+      />
+
+      <Modal
+        title={editingRecord ? '编辑部门' : '新增部门'}
+        open={modalVisible}
+        onCancel={() => setModalVisible(false)}
+        onOk={() => form.submit()}
+        width={600}
+      >
+        <Form
+          form={form}
+          layout="vertical"
+          onFinish={handleSubmit}
+        >
+          <Form.Item
+            label="部门名称"
+            name="name"
+            rules={[{ required: true, message: '请输入部门名称' }]}
+          >
+            <Input placeholder="请输入部门名称" />
+          </Form.Item>
+
+          <Form.Item
+            label="部门编码"
+            name="code"
+            rules={[{ required: true, message: '请输入部门编码' }]}
+          >
+            <Input placeholder="请输入部门编码" />
+          </Form.Item>
+
+          <Form.Item
+            label="上级部门"
+            name="parentId"
+          >
+            <TreeSelect
+              treeData={departmentTree}
+              placeholder="请选择上级部门"
+              allowClear
+              treeDefaultExpandAll
+            />
+          </Form.Item>
+
+          <Form.Item
+            label="负责人"
+            name="managerId"
+          >
+            <Input placeholder="请输入负责人ID" type="number" />
+          </Form.Item>
+
+          <Form.Item
+            label="描述"
+            name="description"
+          >
+            <Input.TextArea rows={3} placeholder="请输入部门描述" />
+          </Form.Item>
+
+          <Form.Item
+            label="排序"
+            name="sortOrder"
+            initialValue={0}
+          >
+            <Input type="number" placeholder="请输入排序值" />
+          </Form.Item>
+
+          <Form.Item
+            label="状态"
+            name="isActive"
+            valuePropName="checked"
+            initialValue={true}
+          >
+            <Switch checkedChildren="启用" unCheckedChildren="禁用" />
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};
+
+export default Departments;

+ 6 - 0
src/client/admin/routes.tsx

@@ -19,6 +19,7 @@ import ContactsPage from './pages/Contacts';
 import LogsPage from './pages/Logs';
 import LogsPage from './pages/Logs';
 import OrderRecordsPage from './pages/OrderRecords';
 import OrderRecordsPage from './pages/OrderRecords';
 import FollowUpRecordsPage from './pages/FollowUpRecords';
 import FollowUpRecordsPage from './pages/FollowUpRecords';
+import DepartmentsPage from './pages/Departments';
 import { LoginPage } from './pages/Login';
 import { LoginPage } from './pages/Login';
 
 
 export const router = createBrowserRouter([
 export const router = createBrowserRouter([
@@ -117,6 +118,11 @@ export const router = createBrowserRouter([
         element: <FollowUpRecordsPage />,
         element: <FollowUpRecordsPage />,
         errorElement: <ErrorPage />
         errorElement: <ErrorPage />
       },
       },
+      {
+        path: 'departments',
+        element: <DepartmentsPage />,
+        errorElement: <ErrorPage />
+      },
       {
       {
         path: '*',
         path: '*',
         element: <NotFoundPage />,
         element: <NotFoundPage />,

+ 9 - 1
src/client/api.ts

@@ -3,7 +3,7 @@ import { hc } from 'hono/client'
 import type {
 import type {
   AuthRoutes, UserRoutes, RoleRoutes,
   AuthRoutes, UserRoutes, RoleRoutes,
   AreaRoutes, ClientRoutes, ExpenseRoutes, FileRoutes, HetongRoutes, HetongRenewRoutes, LinkmanRoutes, LogfileRoutes,
   AreaRoutes, ClientRoutes, ExpenseRoutes, FileRoutes, HetongRoutes, HetongRenewRoutes, LinkmanRoutes, LogfileRoutes,
-  OrderRecordRoutes, FollowUpRecordRoutes
+  OrderRecordRoutes, FollowUpRecordRoutes, DepartmentRoutes, UserDepartmentRoutes
 } from '@/server/api';
 } from '@/server/api';
 
 
 // 创建 axios 适配器
 // 创建 axios 适配器
@@ -112,3 +112,11 @@ export const orderRecordClient = hc<OrderRecordRoutes>('/', {
 export const followUpRecordClient = hc<FollowUpRecordRoutes>('/', {
 export const followUpRecordClient = hc<FollowUpRecordRoutes>('/', {
   fetch: axiosFetch,
   fetch: axiosFetch,
 }).api.v1['follow-up-records'];
 }).api.v1['follow-up-records'];
+
+export const departmentsClient = hc<DepartmentRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1.departments;
+
+export const userDepartmentsClient = hc<UserDepartmentRoutes>('/', {
+  fetch: axiosFetch,
+}).api.v1['user-departments'];

+ 6 - 0
src/server/api.ts

@@ -14,6 +14,8 @@ import linkmanRoutes from './api/contacts/index'
 import logfileRoutes from './api/logs/index'
 import logfileRoutes from './api/logs/index'
 import orderRecordRoutes from './api/order-records/index'
 import orderRecordRoutes from './api/order-records/index'
 import followUpRecordRoutes from './api/follow-up-records/index'
 import followUpRecordRoutes from './api/follow-up-records/index'
+import departmentsRoute from './api/departments/index'
+import userDepartmentsRoute from './api/user-departments/index'
 import { AuthContext } from './types/context'
 import { AuthContext } from './types/context'
 import { AppDataSource } from './data-source'
 import { AppDataSource } from './data-source'
 
 
@@ -75,6 +77,8 @@ const linkmanApiRoutes = api.route('/api/v1/contacts', linkmanRoutes)
 const logfileApiRoutes = api.route('/api/v1/logs', logfileRoutes)
 const logfileApiRoutes = api.route('/api/v1/logs', logfileRoutes)
 const orderRecordApiRoutes = api.route('/api/v1/order-records', orderRecordRoutes)
 const orderRecordApiRoutes = api.route('/api/v1/order-records', orderRecordRoutes)
 const followUpRecordApiRoutes = api.route('/api/v1/follow-up-records', followUpRecordRoutes)
 const followUpRecordApiRoutes = api.route('/api/v1/follow-up-records', followUpRecordRoutes)
+const departmentsApiRoutes = api.route('/api/v1/departments', departmentsRoute)
+const userDepartmentsApiRoutes = api.route('/api/v1/user-departments', userDepartmentsRoute)
 
 
 export type AuthRoutes = typeof authRoutes
 export type AuthRoutes = typeof authRoutes
 export type UserRoutes = typeof userRoutes
 export type UserRoutes = typeof userRoutes
@@ -90,5 +94,7 @@ export type LinkmanRoutes = typeof linkmanApiRoutes
 export type LogfileRoutes = typeof logfileApiRoutes
 export type LogfileRoutes = typeof logfileApiRoutes
 export type OrderRecordRoutes = typeof orderRecordApiRoutes
 export type OrderRecordRoutes = typeof orderRecordApiRoutes
 export type FollowUpRecordRoutes = typeof followUpRecordApiRoutes
 export type FollowUpRecordRoutes = typeof followUpRecordApiRoutes
+export type DepartmentRoutes = typeof departmentsApiRoutes
+export type UserDepartmentRoutes = typeof userDepartmentsApiRoutes
 
 
 export default api
 export default api

+ 16 - 0
src/server/api/departments/index.ts

@@ -0,0 +1,16 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { Department, DepartmentSchema, CreateDepartmentDto, UpdateDepartmentDto } from '@/server/modules/departments/department.entity';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const departmentRoutes = createCrudRoutes({
+  entity: Department,
+  createSchema: CreateDepartmentDto,
+  updateSchema: UpdateDepartmentDto,
+  getSchema: DepartmentSchema,
+  listSchema: DepartmentSchema,
+  searchFields: ['name', 'code', 'description'],
+  relations: ['parent', 'manager', 'children'],
+  middleware: [authMiddleware]
+});
+
+export default departmentRoutes;

+ 15 - 0
src/server/api/user-departments/index.ts

@@ -0,0 +1,15 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { UserDepartment, UserDepartmentSchema, CreateUserDepartmentDto, UpdateUserDepartmentDto } from '@/server/modules/departments/department.entity';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const userDepartmentRoutes = createCrudRoutes({
+  entity: UserDepartment,
+  createSchema: CreateUserDepartmentDto,
+  updateSchema: UpdateUserDepartmentDto,
+  getSchema: UserDepartmentSchema,
+  listSchema: UserDepartmentSchema,
+  relations: ['user', 'department'],
+  middleware: [authMiddleware]
+});
+
+export default userDepartmentRoutes;

+ 3 - 1
src/server/data-source.ts

@@ -16,6 +16,8 @@ import { Linkman } from "./modules/contacts/linkman.entity"
 import { Logfile } from "./modules/logs/logfile.entity"
 import { Logfile } from "./modules/logs/logfile.entity"
 import { OrderRecord } from "./modules/orders/order-record.entity"
 import { OrderRecord } from "./modules/orders/order-record.entity"
 import { FollowUpRecord } from "./modules/follow-ups/follow-up-record.entity"
 import { FollowUpRecord } from "./modules/follow-ups/follow-up-record.entity"
+import { Department, UserDepartment } from "./modules/departments/department.entity"
+import { Permission, RolePermission } from "./modules/permissions/permission.entity"
 
 
 export const AppDataSource = new DataSource({
 export const AppDataSource = new DataSource({
   type: "mysql",
   type: "mysql",
@@ -27,7 +29,7 @@ export const AppDataSource = new DataSource({
   entities: [
   entities: [
     User, Role,
     User, Role,
     AreaData, Client, Expense, File, Hetong, HetongRenew, Linkman, Logfile,
     AreaData, Client, Expense, File, Hetong, HetongRenew, Linkman, Logfile,
-    OrderRecord, FollowUpRecord
+    OrderRecord, FollowUpRecord, Department, UserDepartment, Permission, RolePermission
   ],
   ],
   migrations: [],
   migrations: [],
   synchronize: process.env.DB_SYNCHRONIZE !== "false",
   synchronize: process.env.DB_SYNCHRONIZE !== "false",

+ 140 - 0
src/server/modules/departments/department.entity.ts

@@ -0,0 +1,140 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn, ManyToMany, JoinTable } from 'typeorm';
+import { z } from '@hono/zod-openapi';
+import { UserEntity } from '@/server/modules/users/user.entity';
+
+export enum DataScopeType {
+  PERSONAL = 'personal',
+  DEPARTMENT = 'department',
+  SUB_DEPARTMENT = 'sub_department',
+  COMPANY = 'company',
+  CUSTOM = 'custom'
+}
+
+@Entity('department')
+export class Department {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'name', type: 'varchar', length: 100, comment: '部门名称' })
+  name!: string;
+
+  @Column({ name: 'code', type: 'varchar', length: 50, unique: true, comment: '部门编码' })
+  code!: string;
+
+  @Column({ name: 'description', type: 'text', nullable: true, comment: '部门描述' })
+  description?: string;
+
+  @Column({ name: 'sort_order', type: 'int', default: 0, comment: '排序' })
+  sortOrder!: number;
+
+  @Column({ name: 'is_active', type: 'tinyint', default: 1, comment: '是否启用(0:禁用,1:启用)' })
+  isActive!: number;
+
+  @ManyToOne(() => Department, dept => dept.children)
+  @JoinColumn({ name: 'parent_id' })
+  parent?: Department;
+
+  @OneToMany(() => Department, dept => dept.parent)
+  children!: Department[];
+
+  @ManyToOne(() => UserEntity)
+  @JoinColumn({ name: 'manager_id' })
+  manager?: UserEntity;
+
+  @Column({ name: 'manager_id', type: 'int', nullable: true })
+  managerId?: number;
+
+  @Column({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
+  createdAt!: Date;
+
+  @Column({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', onUpdate: 'CURRENT_TIMESTAMP' })
+  updatedAt!: Date;
+}
+
+@Entity('user_department')
+export class UserDepartment {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'user_id', type: 'int', unsigned: true })
+  userId!: number;
+
+  @Column({ name: 'department_id', type: 'int', unsigned: true })
+  departmentId!: number;
+
+  @Column({ name: 'position', type: 'varchar', length: 100, nullable: true, comment: '职位' })
+  position?: string;
+
+  @Column({ name: 'is_primary', type: 'tinyint', default: 1, comment: '是否主部门(0:兼职,1:主部门)' })
+  isPrimary!: number;
+
+  @Column({ name: 'join_date', type: 'date', nullable: true, comment: '入职日期' })
+  joinDate?: Date;
+
+  @Column({ name: 'leave_date', type: 'date', nullable: true, comment: '离职日期' })
+  leaveDate?: Date;
+
+  @Column({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
+  createdAt!: Date;
+}
+
+// Zod schemas
+export const DepartmentSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '部门ID' }),
+  name: z.string().max(100).openapi({ description: '部门名称', example: '销售部' }),
+  code: z.string().max(50).openapi({ description: '部门编码', example: 'SALES' }),
+  description: z.string().max(500).nullable().openapi({ description: '部门描述', example: '负责销售业务' }),
+  sortOrder: z.number().int().default(0).openapi({ description: '排序', example: 0 }),
+  isActive: z.number().int().min(0).max(1).default(1).openapi({ description: '是否启用', example: 1 }),
+  managerId: z.number().int().positive().nullable().openapi({ description: '部门负责人ID', example: 1 }),
+  parentId: z.number().int().positive().nullable().openapi({ description: '父部门ID', example: 1 }),
+  createdAt: z.date().openapi({ description: '创建时间' }),
+  updatedAt: z.date().openapi({ description: '更新时间' })
+});
+
+export const CreateDepartmentDto = z.object({
+  name: z.string().max(100).openapi({ description: '部门名称', example: '销售部' }),
+  code: z.string().max(50).openapi({ description: '部门编码', example: 'SALES' }),
+  description: z.string().max(500).optional().openapi({ description: '部门描述', example: '负责销售业务' }),
+  sortOrder: z.number().int().optional().default(0).openapi({ description: '排序', example: 0 }),
+  isActive: z.number().int().min(0).max(1).optional().default(1).openapi({ description: '是否启用', example: 1 }),
+  managerId: z.number().int().positive().optional().openapi({ description: '部门负责人ID', example: 1 }),
+  parentId: z.number().int().positive().optional().openapi({ description: '父部门ID', example: 1 })
+});
+
+export const UpdateDepartmentDto = z.object({
+  name: z.string().max(100).optional().openapi({ description: '部门名称', example: '销售部' }),
+  code: z.string().max(50).optional().openapi({ description: '部门编码', example: 'SALES' }),
+  description: z.string().max(500).optional().openapi({ description: '部门描述', example: '负责销售业务' }),
+  sortOrder: z.number().int().optional().openapi({ description: '排序', example: 0 }),
+  isActive: z.number().int().min(0).max(1).optional().openapi({ description: '是否启用', example: 1 }),
+  managerId: z.number().int().positive().optional().openapi({ description: '部门负责人ID', example: 1 }),
+  parentId: z.number().int().positive().optional().openapi({ description: '父部门ID', example: 1 })
+});
+
+export const UserDepartmentSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '关联ID' }),
+  userId: z.number().int().positive().openapi({ description: '用户ID', example: 1 }),
+  departmentId: z.number().int().positive().openapi({ description: '部门ID', example: 1 }),
+  position: z.string().max(100).optional().openapi({ description: '职位', example: '销售经理' }),
+  isPrimary: z.number().int().min(0).max(1).default(1).openapi({ description: '是否主部门', example: 1 }),
+  joinDate: z.coerce.date().optional().openapi({ description: '入职日期', example: '2024-01-01' }),
+  leaveDate: z.coerce.date().optional().openapi({ description: '离职日期', example: '2024-12-31' }),
+  createdAt: z.date().openapi({ description: '创建时间' })
+});
+
+export const CreateUserDepartmentDto = z.object({
+  userId: z.number().int().positive().openapi({ description: '用户ID', example: 1 }),
+  departmentId: z.number().int().positive().openapi({ description: '部门ID', example: 1 }),
+  position: z.string().max(100).optional().openapi({ description: '职位', example: '销售经理' }),
+  isPrimary: z.number().int().min(0).max(1).optional().default(1).openapi({ description: '是否主部门', example: 1 }),
+  joinDate: z.coerce.date().optional().openapi({ description: '入职日期', example: '2024-01-01' }),
+  leaveDate: z.coerce.date().optional().openapi({ description: '离职日期', example: '2024-12-31' })
+});
+
+export const UpdateUserDepartmentDto = z.object({
+  position: z.string().max(100).optional().openapi({ description: '职位', example: '销售经理' }),
+  isPrimary: z.number().int().min(0).max(1).optional().openapi({ description: '是否主部门', example: 1 }),
+  joinDate: z.coerce.date().optional().openapi({ description: '入职日期', example: '2024-01-01' }),
+  leaveDate: z.coerce.date().optional().openapi({ description: '离职日期', example: '2024-12-31' })
+});

+ 191 - 0
src/server/modules/departments/department.service.ts

@@ -0,0 +1,191 @@
+import { DataSource, Repository, SelectQueryBuilder } from 'typeorm';
+import { Department, UserDepartment } from './department.entity';
+import { UserEntity } from '@/server/modules/users/user.entity';
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+
+export class DepartmentService extends GenericCrudService<Department> {
+  private userDepartmentRepository: Repository<UserDepartment>;
+
+  constructor(dataSource: DataSource) {
+    super(dataSource, Department);
+    this.userDepartmentRepository = dataSource.getRepository(UserDepartment);
+  }
+
+  /**
+   * 获取部门树结构
+   */
+  async getDepartmentTree(): Promise<Department[]> {
+    const queryBuilder = this.repository.createQueryBuilder('dept')
+      .leftJoinAndSelect('dept.children', 'children')
+      .leftJoinAndSelect('dept.manager', 'manager')
+      .where('dept.parent IS NULL')
+      .andWhere('dept.isActive = 1')
+      .orderBy('dept.sortOrder', 'ASC');
+
+    return queryBuilder.getMany();
+  }
+
+  /**
+   * 获取部门及其所有子部门
+   */
+  async getDepartmentWithChildren(departmentId: number): Promise<number[]> {
+    const department = await this.repository.findOne({
+      where: { id: departmentId }
+    });
+    
+    if (!department) {
+      throw new Error('部门不存在');
+    }
+
+    const allIds: number[] = [departmentId];
+    
+    // 获取所有子部门
+    const getChildren = async (parentId: number) => {
+      const children = await this.repository.find({ 
+        where: { parent: { id: parentId } } 
+      });
+      
+      for (const child of children) {
+        allIds.push(child.id);
+        await getChildren(child.id);
+      }
+    };
+
+    await getChildren(departmentId);
+    return [...new Set(allIds)];
+  }
+
+  /**
+   * 获取用户的部门ID列表(包括子部门)
+   */
+  async getUserDepartmentIds(userId: number, includeSubDepartments: boolean = false): Promise<number[]> {
+    const userDepartments = await this.userDepartmentRepository.find({
+      where: { userId, isPrimary: 1 },
+      relations: ['department']
+    });
+
+    const departmentIds: number[] = [];
+    for (const userDept of userDepartments) {
+      departmentIds.push(userDept.departmentId);
+      
+      if (includeSubDepartments) {
+        const subDepartmentIds = await this.getDepartmentWithChildren(userDept.departmentId);
+        departmentIds.push(...subDepartmentIds);
+      }
+    }
+
+    return [...new Set(departmentIds)];
+  }
+
+  /**
+   * 获取用户的主部门
+   */
+  async getUserPrimaryDepartment(userId: number): Promise<Department | null> {
+    const userDepartment = await this.userDepartmentRepository.findOne({
+      where: { userId, isPrimary: 1 },
+      relations: ['department']
+    });
+
+    return userDepartment?.department || null;
+  }
+
+  /**
+   * 为用户分配部门
+   */
+  async assignUserToDepartment(
+    userId: number,
+    departmentId: number,
+    isPrimary: boolean = true,
+    position?: string
+  ): Promise<UserDepartment> {
+    // 如果设置为主部门,先将其他部门设为非主部门
+    if (isPrimary) {
+      await this.userDepartmentRepository.update(
+        { userId, isPrimary: 1 },
+        { isPrimary: 0 }
+      );
+    }
+
+    const userDepartment = this.userDepartmentRepository.create({
+      userId,
+      departmentId,
+      isPrimary: isPrimary ? 1 : 0,
+      position,
+      joinDate: new Date()
+    });
+
+    return this.userDepartmentRepository.save(userDepartment);
+  }
+
+  /**
+   * 获取部门成员
+   */
+  async getDepartmentMembers(departmentId: number): Promise<UserEntity[]> {
+    const userDepartments = await this.userDepartmentRepository.find({
+      where: { departmentId },
+      relations: ['user']
+    });
+
+    return userDepartments.map(ud => ud.user);
+  }
+
+  /**
+   * 检查部门是否存在循环引用
+   */
+  private async hasCircularReference(departmentId: number, parentId?: number): Promise<boolean> {
+    if (!parentId || departmentId === parentId) {
+      return true;
+    }
+
+    let currentParent = parentId;
+    while (currentParent) {
+      if (currentParent === departmentId) {
+        return true;
+      }
+      const parent = await this.repository.findOne({ where: { id: currentParent } });
+      currentParent = parent?.parent?.id;
+    }
+
+    return false;
+  }
+
+  /**
+   * 更新部门
+   */
+  async updateDepartment(id: number, data: Partial<Department>): Promise<Department> {
+    // 检查循环引用
+    if (data.parent && await this.hasCircularReference(id, data.parent.id)) {
+      throw new Error('不能设置上级部门为当前部门或其子部门');
+    }
+
+    const department = await this.repository.findOne({ where: { id }, relations: ['parent'] });
+    if (!department) {
+      throw new Error('部门不存在');
+    }
+
+    Object.assign(department, data);
+    return this.repository.save(department);
+  }
+
+  /**
+   * 删除部门及其子部门
+   */
+  async deleteDepartment(id: number): Promise<boolean> {
+    // 检查是否有用户在此部门
+    const userCount = await this.userDepartmentRepository.count({
+      where: { departmentId: id }
+    });
+
+    if (userCount > 0) {
+      throw new Error('该部门下还有用户,不能删除');
+    }
+
+    // 递归删除子部门
+    const children = await this.repository.find({ where: { parent: { id } } });
+    for (const child of children) {
+      await this.deleteDepartment(child.id);
+    }
+
+    return this.delete(id);
+  }
+}

+ 240 - 0
src/server/modules/permissions/data-permission.service.ts

@@ -0,0 +1,240 @@
+import { DataSource, Repository, SelectQueryBuilder } from 'typeorm';
+import { UserEntity } from '@/server/modules/users/user.entity';
+import { DepartmentService } from '@/server/modules/departments/department.service';
+import { DataScopeType } from '@/server/modules/departments/department.entity';
+
+export interface DataPermissionConfig {
+  entity: string;
+  userIdField: string;
+  departmentIdField?: string;
+  responsibleUserIdField?: string;
+}
+
+export class DataPermissionService {
+  private userRepository: Repository<UserEntity>;
+
+  constructor(
+    private dataSource: DataSource,
+    private departmentService: DepartmentService
+  ) {
+    this.userRepository = this.dataSource.getRepository(UserEntity);
+  }
+
+  /**
+   * 应用数据权限过滤
+   */
+  async applyDataScope(
+    queryBuilder: SelectQueryBuilder<any>,
+    entity: string,
+    user: UserEntity,
+    action: string,
+    config: DataPermissionConfig
+  ): Promise<SelectQueryBuilder<any>> {
+    // 超级管理员不受限制
+    if (await this.isSuperAdmin(user)) {
+      return queryBuilder;
+    }
+
+    // 获取用户的数据范围类型
+    const dataScopeType = user.dataScopeType;
+    const userId = user.id;
+
+    switch (dataScopeType) {
+      case DataScopeType.PERSONAL:
+        return this.applyPersonalScope(queryBuilder, entity, userId, config);
+        
+      case DataScopeType.DEPARTMENT:
+        return this.applyDepartmentScope(queryBuilder, entity, userId, config, false);
+        
+      case DataScopeType.SUB_DEPARTMENT:
+        return this.applyDepartmentScope(queryBuilder, entity, userId, config, true);
+        
+      case DataScopeType.COMPANY:
+        return queryBuilder; // 全公司数据,不加限制
+        
+      case DataScopeType.CUSTOM:
+        return this.applyCustomScope(queryBuilder, entity, userId, config);
+        
+      default:
+        return this.applyPersonalScope(queryBuilder, entity, userId, config);
+    }
+  }
+
+  /**
+   * 检查用户是否为超级管理员
+   */
+  private async isSuperAdmin(user: UserEntity): Promise<boolean> {
+    const userWithRoles = await this.userRepository.findOne({
+      where: { id: user.id },
+      relations: ['roles']
+    });
+
+    if (!userWithRoles?.roles) return false;
+
+    return userWithRoles.roles.some(role => role.name === 'super_admin');
+  }
+
+  /**
+   * 应用个人数据范围
+   */
+  private applyPersonalScope(
+    queryBuilder: SelectQueryBuilder<any>,
+    entity: string,
+    userId: number,
+    config: DataPermissionConfig
+  ): SelectQueryBuilder<any> {
+    const { userIdField, responsibleUserIdField } = config;
+
+    if (responsibleUserIdField) {
+      // 如果实体有负责人字段,使用负责人字段
+      return queryBuilder.andWhere(
+        `${entity}.${responsibleUserIdField} = :userId`,
+        { userId }
+      );
+    } else if (userIdField) {
+      // 否则使用用户ID字段
+      return queryBuilder.andWhere(
+        `${entity}.${userIdField} = :userId`,
+        { userId }
+      );
+    }
+
+    return queryBuilder;
+  }
+
+  /**
+   * 应用部门数据范围
+   */
+  private async applyDepartmentScope(
+    queryBuilder: SelectQueryBuilder<any>,
+    entity: string,
+    userId: number,
+    config: DataPermissionConfig,
+    includeSubDepartments: boolean
+  ): Promise<SelectQueryBuilder<any>> {
+    const { departmentIdField, responsibleUserIdField } = config;
+
+    if (!departmentIdField && !responsibleUserIdField) {
+      // 如果没有部门字段,退回到个人权限
+      return this.applyPersonalScope(queryBuilder, entity, userId, config);
+    }
+
+    // 获取用户的部门ID列表
+    const departmentIds = await this.departmentService.getUserDepartmentIds(
+      userId,
+      includeSubDepartments
+    );
+
+    if (departmentIds.length === 0) {
+      // 如果用户没有部门,退回到个人权限
+      return this.applyPersonalScope(queryBuilder, entity, userId, config);
+    }
+
+    if (departmentIdField) {
+      // 使用部门ID过滤
+      return queryBuilder.andWhere(
+        `${entity}.${departmentIdField} IN (:...departmentIds)`,
+        { departmentIds }
+      );
+    } else if (responsibleUserIdField) {
+      // 获取部门内的用户ID列表
+      const userIds = await this.getUsersInDepartments(departmentIds);
+      return queryBuilder.andWhere(
+        `${entity}.${responsibleUserIdField} IN (:...userIds)`,
+        { userIds }
+      );
+    }
+
+    return queryBuilder;
+  }
+
+  /**
+   * 应用自定义数据范围
+   */
+  private async applyCustomScope(
+    queryBuilder: SelectQueryBuilder<any>,
+    entity: string,
+    userId: number,
+    config: DataPermissionConfig
+  ): Promise<SelectQueryBuilder<any>> {
+    // 这里可以实现更复杂的自定义逻辑
+    // 例如:根据用户的特定配置来决定数据范围
+    return this.applyPersonalScope(queryBuilder, entity, userId, config);
+  }
+
+  /**
+   * 获取部门内的用户ID列表
+   */
+  private async getUsersInDepartments(departmentIds: number[]): Promise<number[]> {
+    const departments = await Promise.all(
+      departmentIds.map(deptId => 
+        this.departmentService.getDepartmentMembers(deptId)
+      )
+    );
+
+    const userIds = departments
+      .flat()
+      .map(user => user.id);
+
+    return [...new Set(userIds)];
+  }
+
+  /**
+   * 检查用户对特定数据是否有权限
+   */
+  async checkDataPermission(
+    user: UserEntity,
+    entity: string,
+    dataId: number,
+    action: string,
+    config: DataPermissionConfig
+  ): Promise<boolean> {
+    // 超级管理员有所有权限
+    if (await this.isSuperAdmin(user)) {
+      return true;
+    }
+
+    const queryBuilder = this.dataSource
+      .getRepository(entity)
+      .createQueryBuilder(entity);
+
+    await this.applyDataScope(queryBuilder, entity, user, action, config);
+
+    const count = await queryBuilder
+      .andWhere(`${entity}.id = :dataId`, { dataId })
+      .getCount();
+
+    return count > 0;
+  }
+
+  /**
+   * 获取用户可访问的部门列表
+   */
+  async getAccessibleDepartments(user: UserEntity): Promise<number[]> {
+    if (await this.isSuperAdmin(user) || user.dataScopeType === DataScopeType.COMPANY) {
+      return []; // 空数组表示所有部门
+    }
+
+    return this.departmentService.getUserDepartmentIds(
+      user.id,
+      user.dataScopeType === DataScopeType.SUB_DEPARTMENT
+    );
+  }
+
+  /**
+   * 获取用户可访问的用户列表
+   */
+  async getAccessibleUsers(user: UserEntity): Promise<number[]> {
+    if (await this.isSuperAdmin(user) || user.dataScopeType === DataScopeType.COMPANY) {
+      return []; // 空数组表示所有用户
+    }
+
+    const departmentIds = await this.getAccessibleDepartments(user);
+    if (departmentIds.length === 0) {
+      return [user.id]; // 个人权限
+    }
+
+    const userIds = await this.getUsersInDepartments(departmentIds);
+    return userIds;
+  }
+}

+ 254 - 0
src/server/modules/permissions/permission.entity.ts

@@ -0,0 +1,254 @@
+import { Entity, PrimaryGeneratedColumn, Column, OneToMany, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { z } from '@hono/zod-openapi';
+
+export enum PermissionType {
+  MODULE = 'module',      // 模块权限
+  OPERATION = 'operation', // 操作权限
+  DATA = 'data'          // 数据权限
+}
+
+export enum DataScopeType {
+  PERSONAL = 'personal',      // 仅个人数据
+  DEPARTMENT = 'department',  // 部门数据
+  SUB_DEPARTMENT = 'sub_department', // 部门及下级数据
+  COMPANY = 'company',        // 全公司数据
+  CUSTOM = 'custom'          // 自定义范围
+}
+
+@Entity('permission')
+export class Permission {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'code', type: 'varchar', length: 100, unique: true, comment: '权限编码' })
+  code!: string;
+
+  @Column({ name: 'name', type: 'varchar', length: 100, comment: '权限名称' })
+  name!: string;
+
+  @Column({ name: 'type', type: 'enum', enum: PermissionType, comment: '权限类型' })
+  type!: PermissionType;
+
+  @Column({ name: 'module', type: 'varchar', length: 50, comment: '所属模块' })
+  module!: string;
+
+  @Column({ name: 'action', type: 'varchar', length: 50, comment: '操作类型' })
+  action!: string;
+
+  @Column({ name: 'description', type: 'text', nullable: true, comment: '权限描述' })
+  description?: string;
+
+  @Column({ name: 'sort_order', type: 'int', default: 0, comment: '排序' })
+  sortOrder!: number;
+
+  @Column({ name: 'is_active', type: 'tinyint', default: 1, comment: '是否启用(0:禁用,1:启用)' })
+  isActive!: number;
+
+  @Column({ name: 'parent_id', type: 'int', nullable: true, comment: '父权限ID' })
+  parentId?: number;
+
+  @Column({ name: 'config', type: 'json', nullable: true, comment: '权限配置' })
+  config?: any;
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at', type: 'timestamp' })
+  updatedAt!: Date;
+}
+
+@Entity('role_permission')
+export class RolePermission {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'role_id', type: 'int', unsigned: true, comment: '角色ID' })
+  roleId!: number;
+
+  @Column({ name: 'permission_id', type: 'int', unsigned: true, comment: '权限ID' })
+  permissionId!: number;
+
+  @Column({ name: 'data_scope_type', type: 'enum', enum: DataScopeType, default: DataScopeType.PERSONAL, comment: '数据范围类型' })
+  dataScopeType!: DataScopeType;
+
+  @Column({ name: 'custom_departments', type: 'json', nullable: true, comment: '自定义部门范围' })
+  customDepartments?: number[];
+
+  @CreateDateColumn({ name: 'created_at', type: 'timestamp' })
+  createdAt!: Date;
+}
+
+// 预定义权限列表
+export const PERMISSIONS = {
+  // 系统管理权限
+  SYSTEM: {
+    USER: {
+      CREATE: 'system:user:create',
+      UPDATE: 'system:user:update',
+      DELETE: 'system:user:delete',
+      VIEW: {
+        OWN: 'system:user:view:own',
+        DEPARTMENT: 'system:user:view:department',
+        SUB_DEPARTMENT: 'system:user:view:sub_department',
+        ALL: 'system:user:view:all'
+      }
+    },
+    ROLE: {
+      CREATE: 'system:role:create',
+      UPDATE: 'system:role:update',
+      DELETE: 'system:role:delete',
+      VIEW: 'system:role:view'
+    },
+    DEPARTMENT: {
+      CREATE: 'system:department:create',
+      UPDATE: 'system:department:update',
+      DELETE: 'system:department:delete',
+      VIEW: 'system:department:view'
+    }
+  },
+  
+  // 客户管理权限
+  CLIENT: {
+    CREATE: 'client:create',
+    UPDATE: 'client:update',
+    DELETE: 'client:delete',
+    VIEW: {
+      OWN: 'client:view:own',
+      DEPARTMENT: 'client:view:department',
+      SUB_DEPARTMENT: 'client:view:sub_department',
+      ALL: 'client:view:all'
+    },
+    ASSIGN: 'client:assign',
+    TRANSFER: 'client:transfer'
+  },
+
+  // 合同管理权限
+  CONTRACT: {
+    CREATE: 'contract:create',
+    UPDATE: 'contract:update',
+    DELETE: 'contract:delete',
+    VIEW: {
+      OWN: 'contract:view:own',
+      DEPARTMENT: 'contract:view:department',
+      SUB_DEPARTMENT: 'contract:view:sub_department',
+      ALL: 'contract:view:all'
+    },
+    APPROVE: 'contract:approve',
+    RENEW: 'contract:renew'
+  },
+
+  // 跟进记录权限
+  FOLLOW_UP: {
+    CREATE: 'follow_up:create',
+    UPDATE: 'follow_up:update',
+    DELETE: 'follow_up:delete',
+    VIEW: {
+      OWN: 'follow_up:view:own',
+      DEPARTMENT: 'follow_up:view:department',
+      SUB_DEPARTMENT: 'follow_up:view:sub_department',
+      ALL: 'follow_up:view:all'
+    }
+  },
+
+  // 订单管理权限
+  ORDER: {
+    CREATE: 'order:create',
+    UPDATE: 'order:update',
+    DELETE: 'order:delete',
+    VIEW: {
+      OWN: 'order:view:own',
+      DEPARTMENT: 'order:view:department',
+      SUB_DEPARTMENT: 'order:view:sub_department',
+      ALL: 'order:view:all'
+    }
+  },
+
+  // 费用管理权限
+  EXPENSE: {
+    CREATE: 'expense:create',
+    UPDATE: 'expense:update',
+    DELETE: 'expense:delete',
+    VIEW: {
+      OWN: 'expense:view:own',
+      DEPARTMENT: 'expense:view:department',
+      SUB_DEPARTMENT: 'expense:view:sub_department',
+      ALL: 'expense:view:all'
+    }
+  },
+
+  // 文件管理权限
+  FILE: {
+    UPLOAD: 'file:upload',
+    DELETE: 'file:delete',
+    VIEW: {
+      OWN: 'file:view:own',
+      DEPARTMENT: 'file:view:department',
+      SUB_DEPARTMENT: 'file:view:sub_department',
+      ALL: 'file:view:all'
+    }
+  }
+};
+
+// Zod schemas
+export const PermissionSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '权限ID' }),
+  code: z.string().max(100).openapi({ description: '权限编码', example: 'client:create' }),
+  name: z.string().max(100).openapi({ description: '权限名称', example: '创建客户' }),
+  type: z.enum([PermissionType.MODULE, PermissionType.OPERATION, PermissionType.DATA]).openapi({ description: '权限类型', example: PermissionType.OPERATION }),
+  module: z.string().max(50).openapi({ description: '所属模块', example: 'client' }),
+  action: z.string().max(50).openapi({ description: '操作类型', example: 'create' }),
+  description: z.string().max(500).optional().openapi({ description: '权限描述', example: '允许创建新客户' }),
+  sortOrder: z.number().int().default(0).openapi({ description: '排序', example: 0 }),
+  isActive: z.number().int().min(0).max(1).default(1).openapi({ description: '是否启用', example: 1 }),
+  parentId: z.number().int().positive().optional().openapi({ description: '父权限ID', example: 1 }),
+  config: z.any().optional().openapi({ description: '权限配置' }),
+  createdAt: z.date().openapi({ description: '创建时间' }),
+  updatedAt: z.date().openapi({ description: '更新时间' })
+});
+
+export const CreatePermissionDto = z.object({
+  code: z.string().max(100).openapi({ description: '权限编码', example: 'client:create' }),
+  name: z.string().max(100).openapi({ description: '权限名称', example: '创建客户' }),
+  type: z.enum([PermissionType.MODULE, PermissionType.OPERATION, PermissionType.DATA]).openapi({ description: '权限类型', example: PermissionType.OPERATION }),
+  module: z.string().max(50).openapi({ description: '所属模块', example: 'client' }),
+  action: z.string().max(50).openapi({ description: '操作类型', example: 'create' }),
+  description: z.string().max(500).optional().openapi({ description: '权限描述', example: '允许创建新客户' }),
+  sortOrder: z.number().int().optional().default(0).openapi({ description: '排序', example: 0 }),
+  isActive: z.number().int().min(0).max(1).optional().default(1).openapi({ description: '是否启用', example: 1 }),
+  parentId: z.number().int().positive().optional().openapi({ description: '父权限ID', example: 1 }),
+  config: z.any().optional().openapi({ description: '权限配置' })
+});
+
+export const UpdatePermissionDto = z.object({
+  code: z.string().max(100).optional().openapi({ description: '权限编码', example: 'client:create' }),
+  name: z.string().max(100).optional().openapi({ description: '权限名称', example: '创建客户' }),
+  type: z.enum([PermissionType.MODULE, PermissionType.OPERATION, PermissionType.DATA]).optional().openapi({ description: '权限类型', example: PermissionType.OPERATION }),
+  module: z.string().max(50).optional().openapi({ description: '所属模块', example: 'client' }),
+  action: z.string().max(50).optional().openapi({ description: '操作类型', example: 'create' }),
+  description: z.string().max(500).optional().openapi({ description: '权限描述', example: '允许创建新客户' }),
+  sortOrder: z.number().int().optional().openapi({ description: '排序', example: 0 }),
+  isActive: z.number().int().min(0).max(1).optional().openapi({ description: '是否启用', example: 1 }),
+  parentId: z.number().int().positive().optional().openapi({ description: '父权限ID', example: 1 }),
+  config: z.any().optional().openapi({ description: '权限配置' })
+});
+
+export const RolePermissionSchema = z.object({
+  id: z.number().int().positive().openapi({ description: '关联ID' }),
+  roleId: z.number().int().positive().openapi({ description: '角色ID', example: 1 }),
+  permissionId: z.number().int().positive().openapi({ description: '权限ID', example: 1 }),
+  dataScopeType: z.enum([DataScopeType.PERSONAL, DataScopeType.DEPARTMENT, DataScopeType.SUB_DEPARTMENT, DataScopeType.COMPANY, DataScopeType.CUSTOM]).default(DataScopeType.PERSONAL).openapi({ description: '数据范围类型', example: DataScopeType.PERSONAL }),
+  customDepartments: z.array(z.number().int().positive()).optional().openapi({ description: '自定义部门范围', example: [1, 2, 3] }),
+  createdAt: z.date().openapi({ description: '创建时间' })
+});
+
+export const CreateRolePermissionDto = z.object({
+  roleId: z.number().int().positive().openapi({ description: '角色ID', example: 1 }),
+  permissionId: z.number().int().positive().openapi({ description: '权限ID', example: 1 }),
+  dataScopeType: z.enum([DataScopeType.PERSONAL, DataScopeType.DEPARTMENT, DataScopeType.SUB_DEPARTMENT, DataScopeType.COMPANY, DataScopeType.CUSTOM]).optional().default(DataScopeType.PERSONAL).openapi({ description: '数据范围类型', example: DataScopeType.PERSONAL }),
+  customDepartments: z.array(z.number().int().positive()).optional().openapi({ description: '自定义部门范围', example: [1, 2, 3] })
+});
+
+export const UpdateRolePermissionDto = z.object({
+  dataScopeType: z.enum([DataScopeType.PERSONAL, DataScopeType.DEPARTMENT, DataScopeType.SUB_DEPARTMENT, DataScopeType.COMPANY, DataScopeType.CUSTOM]).optional().openapi({ description: '数据范围类型', example: DataScopeType.PERSONAL }),
+  customDepartments: z.array(z.number().int().positive()).optional().openapi({ description: '自定义部门范围', example: [1, 2, 3] })
+});

+ 10 - 1
src/server/modules/users/user.entity.ts

@@ -1,8 +1,9 @@
-import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
+import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable, CreateDateColumn, UpdateDateColumn, OneToMany, ManyToOne, JoinColumn } from 'typeorm';
 import { Role, RoleSchema } from './role.entity';
 import { Role, RoleSchema } from './role.entity';
 import { z } from '@hono/zod-openapi';
 import { z } from '@hono/zod-openapi';
 import { DeleteStatus, DisabledStatus } from '@/share/types';
 import { DeleteStatus, DisabledStatus } from '@/share/types';
 import { File } from '@/server/modules/files/file.entity';
 import { File } from '@/server/modules/files/file.entity';
+import { DataScopeType } from '@/server/modules/departments/department.entity';
 
 
 @Entity({ name: 'users' })
 @Entity({ name: 'users' })
 export class UserEntity {
 export class UserEntity {
@@ -43,6 +44,12 @@ export class UserEntity {
   @OneToMany(() => File, file => file.uploadUser)
   @OneToMany(() => File, file => file.uploadUser)
   uploadFiles!: File[];
   uploadFiles!: File[];
 
 
+  @Column({ name: 'default_department_id', type: 'int', nullable: true, comment: '默认部门ID' })
+  defaultDepartmentId?: number;
+
+  @Column({ name: 'data_scope_type', type: 'enum', enum: DataScopeType, default: DataScopeType.PERSONAL, comment: '数据范围类型' })
+  dataScopeType!: DataScopeType;
+
   @CreateDateColumn({ name: 'created_at', type: 'timestamp' })
   @CreateDateColumn({ name: 'created_at', type: 'timestamp' })
   createdAt!: Date;
   createdAt!: Date;
 
 
@@ -98,6 +105,8 @@ export const UserSchema = z.object({
     ],
     ],
     description: '用户角色列表'
     description: '用户角色列表'
   }),
   }),
+  defaultDepartmentId: z.number().int().positive().nullable().openapi({ description: '默认部门ID', example: 1 }),
+  dataScopeType: z.enum([DataScopeType.PERSONAL, DataScopeType.DEPARTMENT, DataScopeType.SUB_DEPARTMENT, DataScopeType.COMPANY, DataScopeType.CUSTOM]).default(DataScopeType.PERSONAL).openapi({ description: '数据范围类型', example: DataScopeType.PERSONAL }),
   createdAt: z.date().openapi({ description: '创建时间' }),
   createdAt: z.date().openapi({ description: '创建时间' }),
   updatedAt: z.date().openapi({ description: '更新时间' })
   updatedAt: z.date().openapi({ description: '更新时间' })
 });
 });

+ 66 - 4
src/server/utils/generic-crud.routes.ts

@@ -5,6 +5,8 @@ import { ErrorSchema } from './errorHandler';
 import { AuthContext } from '../types/context';
 import { AuthContext } from '../types/context';
 import { ObjectLiteral } from 'typeorm';
 import { ObjectLiteral } from 'typeorm';
 import { AppDataSource } from '../data-source';
 import { AppDataSource } from '../data-source';
+import { DataPermissionService } from '@/server/modules/permissions/data-permission.service';
+import { DepartmentService } from '@/server/modules/departments/department.service';
 
 
 export function createCrudRoutes<
 export function createCrudRoutes<
   T extends ObjectLiteral,
   T extends ObjectLiteral,
@@ -13,13 +15,19 @@ export function createCrudRoutes<
   GetSchema extends z.ZodSchema = z.ZodSchema,
   GetSchema extends z.ZodSchema = z.ZodSchema,
   ListSchema extends z.ZodSchema = z.ZodSchema
   ListSchema extends z.ZodSchema = z.ZodSchema
 >(options: CrudOptions<T, CreateSchema, UpdateSchema, GetSchema, ListSchema>) {
 >(options: CrudOptions<T, CreateSchema, UpdateSchema, GetSchema, ListSchema>) {
-  const { entity, createSchema, updateSchema, getSchema, listSchema, searchFields, relations, middleware = [], userTracking } = options;
+  const { entity, createSchema, updateSchema, getSchema, listSchema, searchFields, relations, middleware = [], userTracking, dataPermission } = options;
+  
+  // 创建部门服务和数据权限服务
+  const departmentService = new DepartmentService(AppDataSource);
+  const dataPermissionService = dataPermission ? new DataPermissionService(AppDataSource, departmentService) : undefined;
   
   
   // 创建CRUD服务实例
   // 创建CRUD服务实例
-  // 抽象类不能直接实例化,需要创建具体实现类
   class ConcreteCrudService extends GenericCrudService<T> {
   class ConcreteCrudService extends GenericCrudService<T> {
     constructor() {
     constructor() {
-      super(AppDataSource, entity, { userTracking });
+      super(AppDataSource, entity, { 
+        userTracking,
+        dataPermission: dataPermission ? { service: dataPermissionService!, config: dataPermission } : undefined
+      });
     }
     }
   }
   }
   const crudService = new ConcreteCrudService();
   const crudService = new ConcreteCrudService();
@@ -241,6 +249,9 @@ export function createCrudRoutes<
           }
           }
         }
         }
         
         
+        // 获取当前用户信息
+        const user = c.get('user');
+        
         const [data, total] = await crudService.getList(
         const [data, total] = await crudService.getList(
           page,
           page,
           pageSize,
           pageSize,
@@ -249,7 +260,8 @@ export function createCrudRoutes<
           undefined,
           undefined,
           relations || [],
           relations || [],
           order,
           order,
-          parsedFilters
+          parsedFilters,
+          user
         );
         );
         
         
         return c.json({
         return c.json({
@@ -285,6 +297,23 @@ export function createCrudRoutes<
     .openapi(getRouteDef, async (c: any) => {
     .openapi(getRouteDef, async (c: any) => {
       try {
       try {
         const { id } = c.req.valid('param');
         const { id } = c.req.valid('param');
+        const user = c.get('user');
+        
+        // 如果配置了数据权限,检查用户是否有权限访问该资源
+        if (dataPermissionService && dataPermissionConfig && user) {
+          const hasPermission = await dataPermissionService.checkDataPermission(
+            user,
+            entity.name,
+            id,
+            'view',
+            dataPermissionConfig
+          );
+          
+          if (!hasPermission) {
+            return c.json({ code: 403, message: '没有权限访问该资源' }, 403);
+          }
+        }
+        
         const result = await crudService.getById(id, relations || []);
         const result = await crudService.getById(id, relations || []);
         
         
         if (!result) {
         if (!result) {
@@ -307,6 +336,22 @@ export function createCrudRoutes<
         const { id } = c.req.valid('param');
         const { id } = c.req.valid('param');
         const data = c.req.valid('json');
         const data = c.req.valid('json');
         const user = c.get('user');
         const user = c.get('user');
+        
+        // 如果配置了数据权限,检查用户是否有权限更新该资源
+        if (dataPermissionService && dataPermissionConfig && user) {
+          const hasPermission = await dataPermissionService.checkDataPermission(
+            user,
+            entity.name,
+            id,
+            'update',
+            dataPermissionConfig
+          );
+          
+          if (!hasPermission) {
+            return c.json({ code: 403, message: '没有权限更新该资源' }, 403);
+          }
+        }
+        
         const result = await crudService.update(id, data, user?.id);
         const result = await crudService.update(id, data, user?.id);
         
         
         if (!result) {
         if (!result) {
@@ -327,6 +372,23 @@ export function createCrudRoutes<
     .openapi(deleteRouteDef, async (c: any) => {
     .openapi(deleteRouteDef, async (c: any) => {
       try {
       try {
         const { id } = c.req.valid('param');
         const { id } = c.req.valid('param');
+        const user = c.get('user');
+        
+        // 如果配置了数据权限,检查用户是否有权限删除该资源
+        if (dataPermissionService && dataPermissionConfig && user) {
+          const hasPermission = await dataPermissionService.checkDataPermission(
+            user,
+            entity.name,
+            id,
+            'delete',
+            dataPermissionConfig
+          );
+          
+          if (!hasPermission) {
+            return c.json({ code: 403, message: '没有权限删除该资源' }, 403);
+          }
+        }
+        
         const success = await crudService.delete(id);
         const success = await crudService.delete(id);
         
         
         if (!success) {
         if (!success) {

+ 32 - 1
src/server/utils/generic-crud.service.ts

@@ -1,19 +1,30 @@
 import { DataSource, Repository, ObjectLiteral, DeepPartial } from 'typeorm';
 import { DataSource, Repository, ObjectLiteral, DeepPartial } from 'typeorm';
 import { z } from '@hono/zod-openapi';
 import { z } from '@hono/zod-openapi';
+import { UserEntity } from '@/server/modules/users/user.entity';
+import { DataPermissionService } from '@/server/modules/permissions/data-permission.service';
+import { DataPermissionConfig } from '@/server/modules/permissions/data-permission.service';
 
 
 export abstract class GenericCrudService<T extends ObjectLiteral> {
 export abstract class GenericCrudService<T extends ObjectLiteral> {
   protected repository: Repository<T>;
   protected repository: Repository<T>;
   private userTrackingOptions?: UserTrackingOptions;
   private userTrackingOptions?: UserTrackingOptions;
+  private dataPermissionService?: DataPermissionService;
+  private dataPermissionConfig?: DataPermissionConfig;
 
 
   constructor(
   constructor(
     protected dataSource: DataSource,
     protected dataSource: DataSource,
     protected entity: new () => T,
     protected entity: new () => T,
     options?: {
     options?: {
       userTracking?: UserTrackingOptions;
       userTracking?: UserTrackingOptions;
+      dataPermission?: {
+        service: DataPermissionService;
+        config: DataPermissionConfig;
+      };
     }
     }
   ) {
   ) {
     this.repository = this.dataSource.getRepository(entity);
     this.repository = this.dataSource.getRepository(entity);
     this.userTrackingOptions = options?.userTracking;
     this.userTrackingOptions = options?.userTracking;
+    this.dataPermissionService = options?.dataPermission?.service;
+    this.dataPermissionConfig = options?.dataPermission?.config;
   }
   }
 
 
   /**
   /**
@@ -29,11 +40,23 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
     order: { [P in keyof T]?: 'ASC' | 'DESC' } = {},
     order: { [P in keyof T]?: 'ASC' | 'DESC' } = {},
     filters?: {
     filters?: {
       [key: string]: any;
       [key: string]: any;
-    }
+    },
+    user?: UserEntity
   ): Promise<[T[], number]> {
   ): Promise<[T[], number]> {
     const skip = (page - 1) * pageSize;
     const skip = (page - 1) * pageSize;
     const query = this.repository.createQueryBuilder('entity');
     const query = this.repository.createQueryBuilder('entity');
 
 
+    // 应用数据权限
+    if (user && this.dataPermissionService && this.dataPermissionConfig) {
+      await this.dataPermissionService.applyDataScope(
+        query,
+        this.entity.name,
+        user,
+        'view',
+        this.dataPermissionConfig
+      );
+    }
+
     // 添加关联关系(支持嵌套关联,如 ['contract.client'])
     // 添加关联关系(支持嵌套关联,如 ['contract.client'])
     if (relations.length > 0) {
     if (relations.length > 0) {
       relations.forEach((relation, relationIndex) => {
       relations.forEach((relation, relationIndex) => {
@@ -185,6 +208,13 @@ export interface UserTrackingOptions {
   updatedByField?: string;
   updatedByField?: string;
 }
 }
 
 
+export interface DataPermissionConfig {
+  entity: string;
+  userIdField: string;
+  departmentIdField?: string;
+  responsibleUserIdField?: string;
+}
+
 export type CrudOptions<
 export type CrudOptions<
   T extends ObjectLiteral,
   T extends ObjectLiteral,
   CreateSchema extends z.ZodSchema = z.ZodSchema,
   CreateSchema extends z.ZodSchema = z.ZodSchema,
@@ -201,4 +231,5 @@ export type CrudOptions<
   relations?: string[];
   relations?: string[];
   middleware?: any[];
   middleware?: any[];
   userTracking?: UserTrackingOptions;
   userTracking?: UserTrackingOptions;
+  dataPermission?: DataPermissionConfig;
 };
 };