Ver código fonte

反哺packages

yourname 1 semana atrás
pai
commit
a77dfee2e9
35 arquivos alterados com 2581 adições e 87 exclusões
  1. 1 0
      packages/auth-module/package.json
  2. 2 2
      packages/file-module/src/entities/file.entity.ts
  3. 1 0
      packages/file-module/tests/integration/file.routes.integration.test.ts
  4. 3 1
      packages/file-module/tests/unit/file.service.test.ts
  5. 5 0
      packages/geo-areas/package.json
  6. 3 3
      packages/geo-areas/src/api/admin/areas/index.ts
  7. 96 8
      packages/geo-areas/src/api/areas/index.ts
  8. 3 2
      packages/geo-areas/src/modules/areas/area.entity.ts
  9. 24 0
      packages/geo-areas/src/modules/areas/area.service.ts
  10. 2 0
      packages/geo-areas/src/schemas/index.ts
  11. 6 3
      packages/shared-crud/package.json
  12. 261 34
      packages/shared-crud/src/routes/generic-crud.routes.ts
  13. 8 2
      packages/shared-crud/src/services/concrete-crud.service.ts
  14. 250 8
      packages/shared-crud/src/services/generic-crud.service.ts
  15. 2 1
      packages/shared-crud/src/services/index.ts
  16. 127 0
      packages/shared-crud/src/types/data-permission.types.ts
  17. 586 0
      packages/shared-crud/tests/integration/data-permission.integration.test.ts
  18. 371 0
      packages/shared-crud/tests/integration/schema-type-conversion.integration.test.ts
  19. 503 0
      packages/shared-crud/tests/integration/tenant-isolation.integration.test.ts
  20. 7 1
      packages/shared-crud/vitest.config.ts
  21. 3 0
      packages/shared-test-util/src/index.ts
  22. 190 0
      packages/shared-test-util/src/test-data-factory.ts
  23. 2 0
      packages/shared-types/src/index.ts
  24. 2 1
      packages/shared-utils/src/utils/jwt.util.ts
  25. 94 0
      packages/shared-utils/src/utils/redis.util.ts
  26. 1 1
      packages/shared-utils/tests/unit/data-source.test.ts
  27. 1 1
      packages/shared-utils/tests/unit/jwt.util.test.ts
  28. 4 2
      packages/user-module/src/entities/user.entity.ts
  29. 1 1
      packages/user-module/src/routes/user.routes.ts
  30. 1 1
      packages/user-module/src/services/role.service.ts
  31. 5 5
      packages/user-module/tests/integration/role.integration.test.ts
  32. 2 1
      packages/user-module/tests/integration/user.integration.test.ts
  33. 6 1
      packages/user-module/tests/integration/user.routes.integration.test.ts
  34. 4 4
      packages/user-module/tests/unit/role.service.test.ts
  35. 4 4
      packages/user-module/tests/unit/user.service.test.ts

+ 1 - 0
packages/auth-module/package.json

@@ -22,6 +22,7 @@
       "require": "./src/schemas/index.ts"
     },
     "./schemas/*": {
+      "types": "./src/schemas/*",
       "import": "./src/schemas/*",
       "require": "./src/schemas/*"
     },

+ 2 - 2
packages/file-module/src/entities/file.entity.ts

@@ -1,5 +1,5 @@
 import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
-import { UserEntity } from '@d8d/user-module';
+import type { UserEntity } from '@d8d/user-module';
 import process from 'node:process';
 import { MinioService } from '../services/minio.service';
 
@@ -57,7 +57,7 @@ export class File {
   @Column({ name: 'upload_user_id', type: 'int', unsigned: true })
   uploadUserId!: number;
 
-  @ManyToOne(() => UserEntity)
+  @ManyToOne('UserEntity')
   @JoinColumn({ name: 'upload_user_id', referencedColumnName: 'id' })
   uploadUser!: UserEntity;
 

+ 1 - 0
packages/file-module/tests/integration/file.routes.integration.test.ts

@@ -211,6 +211,7 @@ describe('文件路由API集成测试 (使用hono/testing)', () => {
         }
       });
 
+
       expect(response.status).toBe(200);
       if (response.status === 200) {
         const responseData = await response.json();

+ 3 - 1
packages/file-module/tests/unit/file.service.test.ts

@@ -11,7 +11,8 @@ vi.mock('@d8d/shared-utils', () => ({
   logger: {
     error: vi.fn(),
     db: vi.fn()
-  }
+  },
+  ErrorSchema: {}
 }));
 vi.mock('uuid', () => ({
   v4: () => 'test-uuid-123'
@@ -23,6 +24,7 @@ describe('FileService', () => {
   beforeEach(() => {
     mockDataSource = {
       getRepository: vi.fn(() => ({
+        findOne: vi.fn(),
         findOneBy: vi.fn(),
         save: vi.fn()
       }))

+ 5 - 0
packages/geo-areas/package.json

@@ -35,6 +35,11 @@
       "types": "./src/api/admin/areas/index.ts",
       "import": "./src/api/admin/areas/index.ts",
       "require": "./src/api/admin/areas/index.ts"
+    },
+    "./schemas": {
+      "types": "./src/schemas/index.ts",
+      "import": "./src/schemas/index.ts",
+      "require": "./src/schemas/index.ts"
     }
   },
   "scripts": {

+ 3 - 3
packages/geo-areas/src/api/admin/areas/index.ts

@@ -21,8 +21,8 @@ const areaRoutes = createCrudRoutes({
   relations: ['parent', 'children'],
   middleware: [authMiddleware]
 })
-
-export default new OpenAPIHono()
+const app = new OpenAPIHono()
   // 合并树形结构路由
   .route('/', treeRoutes)
-  .route('/', areaRoutes);
+  .route('/', areaRoutes);
+export default app;

+ 96 - 8
packages/geo-areas/src/api/areas/index.ts

@@ -48,6 +48,22 @@ const getDistrictsSchema = z.object({
   })
 });
 
+// 街道查询参数Schema
+const getTownsSchema = z.object({
+  districtId: z.coerce.number<number>().int().positive('区县ID必须为正整数').openapi({
+    example: 3401,
+    description: '区县ID'
+  }),
+  page: z.coerce.number<number>().int().min(1).default(1).openapi({
+    example: 1,
+    description: '页码'
+  }),
+  pageSize: z.coerce.number<number>().int().min(1).max(100).default(50).openapi({
+    example: 50,
+    description: '每页数量'
+  })
+});
+
 // 省市区响应Schema
 const areaResponseSchema = z.object({
   id: z.number(),
@@ -102,6 +118,21 @@ const districtsResponseSchema = z.object({
   message: z.string()
 });
 
+// 街道列表响应Schema
+const townsResponseSchema = z.object({
+  success: z.boolean(),
+  data: z.object({
+    towns: z.array(areaResponseSchema),
+    pagination: z.object({
+      page: z.number(),
+      pageSize: z.number(),
+      total: z.number(),
+      totalPages: z.number()
+    })
+  }),
+  message: z.string()
+});
+
 // 错误响应Schema
 const errorSchema = z.object({
   code: z.number(),
@@ -183,14 +214,39 @@ const getDistrictsRoute = createRoute({
   }
 });
 
+// 创建街道查询路由
+const getTownsRoute = createRoute({
+  method: 'get',
+  path: '/towns',
+  request: {
+    query: getTownsSchema
+  },
+  responses: {
+    200: {
+      description: '获取街道列表成功',
+      content: {
+        'application/json': { schema: townsResponseSchema }
+      }
+    },
+    400: {
+      description: '参数错误',
+      content: { 'application/json': { schema: errorSchema } }
+    },
+    500: {
+      description: '获取街道列表失败',
+      content: { 'application/json': { schema: errorSchema } }
+    }
+  }
+});
+
 const app = new OpenAPIHono()
   .openapi(getProvincesRoute, async (c) => {
     try {
       const { page, pageSize } = c.req.valid('query');
       const areaService = new AreaService(AppDataSource);
 
-      // 获取所有省份数据
-      const provinces = await areaService.getAreaTreeByLevel(AreaLevel.PROVINCE);
+      // 使用高效查询方法获取省份数据
+      const provinces = await areaService.getAreasByLevelAndParent(AreaLevel.PROVINCE);
 
       // 分页
       const startIndex = (page - 1) * pageSize;
@@ -223,9 +279,8 @@ const app = new OpenAPIHono()
       const { provinceId, page, pageSize } = c.req.valid('query');
       const areaService = new AreaService(AppDataSource);
 
-      // 获取指定省份下的所有城市
-      const allCities = await areaService.getAreaTreeByLevel(AreaLevel.CITY);
-      const cities = allCities.filter(city => city.parentId === provinceId);
+      // 使用高效查询方法获取指定省份下的城市数据
+      const cities = await areaService.getAreasByLevelAndParent(AreaLevel.CITY, provinceId);
 
       // 分页
       const startIndex = (page - 1) * pageSize;
@@ -258,9 +313,8 @@ const app = new OpenAPIHono()
       const { cityId, page, pageSize } = c.req.valid('query');
       const areaService = new AreaService(AppDataSource);
 
-      // 获取指定城市下的所有区县
-      const allDistricts = await areaService.getAreaTreeByLevel(AreaLevel.DISTRICT);
-      const districts = allDistricts.filter(district => district.parentId === cityId);
+      // 使用高效查询方法获取指定城市下的区县数据
+      const districts = await areaService.getAreasByLevelAndParent(AreaLevel.DISTRICT, cityId);
 
       // 分页
       const startIndex = (page - 1) * pageSize;
@@ -287,6 +341,40 @@ const app = new OpenAPIHono()
         message: error instanceof Error ? error.message : '获取区县列表失败'
       }, 500);
     }
+  })
+  .openapi(getTownsRoute, async (c) => {
+    try {
+      const { districtId, page, pageSize } = c.req.valid('query');
+      const areaService = new AreaService(AppDataSource);
+
+      // 使用高效查询方法获取指定区县下的街道数据
+      const towns = await areaService.getAreasByLevelAndParent(AreaLevel.TOWN, districtId);
+
+      // 分页
+      const startIndex = (page - 1) * pageSize;
+      const endIndex = startIndex + pageSize;
+      const paginatedTowns = towns.slice(startIndex, endIndex);
+
+      return c.json({
+        success: true,
+        data: {
+          towns: paginatedTowns,
+          pagination: {
+            page,
+            pageSize,
+            total: towns.length,
+            totalPages: Math.ceil(towns.length / pageSize)
+          }
+        },
+        message: '获取街道列表成功'
+      }, 200);
+    } catch (error) {
+      console.error('获取街道列表失败:', error);
+      return c.json({
+        code: 500,
+        message: error instanceof Error ? error.message : '获取街道列表失败'
+      }, 500);
+    }
   });
 
 export const areaRoutes = app;

+ 3 - 2
packages/geo-areas/src/modules/areas/area.entity.ts

@@ -4,7 +4,8 @@ import { DeleteStatus, DisabledStatus } from '@d8d/shared-types';
 export enum AreaLevel {
   PROVINCE = 1, // 省/直辖市
   CITY = 2,     // 市
-  DISTRICT = 3  // 区/县
+  DISTRICT = 3, // 区/县
+  TOWN = 4      // 街道/乡镇
 }
 
 @Entity({ name: 'areas' })
@@ -22,7 +23,7 @@ export class AreaEntity {
     name: 'level',
     type: 'enum',
     enum: AreaLevel,
-    comment: '层级: 1:省/直辖市, 2:市, 3:区/县'
+    comment: '层级: 1:省/直辖市, 2:市, 3:区/县, 4:街道/乡镇'
   })
   level!: AreaLevel;
 

+ 24 - 0
packages/geo-areas/src/modules/areas/area.service.ts

@@ -160,4 +160,28 @@ export class AreaService {
       }
     });
   }
+
+  /**
+   * 根据层级和父级ID获取区域列表(高效查询)
+   */
+  async getAreasByLevelAndParent(level: AreaLevel, parentId?: number): Promise<AreaEntity[]> {
+    const where: any = {
+      level,
+      isDeleted: 0,
+      isDisabled: DisabledStatus.ENABLED
+    };
+
+    // 如果指定了父级ID,则添加父级条件
+    if (parentId !== undefined) {
+      where.parentId = parentId;
+    }
+
+    return this.areaRepository.find({
+      where,
+      order: {
+        id: 'ASC',
+        name: 'ASC'
+      }
+    });
+  }
 }

+ 2 - 0
packages/geo-areas/src/schemas/index.ts

@@ -0,0 +1,2 @@
+export * from '../modules/areas/area.schema';
+export { AreaLevel } from '../modules/areas/area.entity';

+ 6 - 3
packages/shared-crud/package.json

@@ -29,16 +29,19 @@
     "test:typecheck": "tsc --noEmit"
   },
   "dependencies": {
+    "@asteasolutions/zod-to-openapi": "^8.1.0",
     "@d8d/shared-types": "workspace:*",
     "@d8d/shared-utils": "workspace:*",
-    "typeorm": "^0.3.20",
+    "@d8d/shared-test-util": "workspace:*",
     "@hono/zod-openapi": "1.0.2",
+    "hono": "^4.8.5",
+    "typeorm": "^0.3.20",
     "zod": "^4.1.12"
   },
   "devDependencies": {
+    "@vitest/coverage-v8": "^3.2.4",
     "typescript": "^5.8.3",
-    "vitest": "^3.2.4",
-    "@vitest/coverage-v8": "^3.2.4"
+    "vitest": "^3.2.4"
   },
   "files": [
     "src"

+ 261 - 34
packages/shared-crud/src/routes/generic-crud.routes.ts

@@ -1,11 +1,13 @@
 import { createRoute, OpenAPIHono, extendZodWithOpenApi } from '@hono/zod-openapi';
-import { z } from '@hono/zod-openapi';
+import { z } from 'zod';
+import type { ZodError } from 'zod';
 import { ObjectLiteral } from 'typeorm';
 import { CrudOptions } from '../services/generic-crud.service';
 import { ErrorSchema } from '@d8d/shared-utils';
 import { AuthContext } from '@d8d/shared-types';
 import { parseWithAwait } from '@d8d/shared-utils';
 import { ConcreteCrudService } from '../services/concrete-crud.service';
+import { PermissionError } from '../types/data-permission.types';
 
 extendZodWithOpenApi(z)
 
@@ -18,7 +20,7 @@ export function createCrudRoutes<
 >(
   options: CrudOptions<T, CreateSchema, UpdateSchema, GetSchema, ListSchema>
 ) {
-  const { entity, createSchema, updateSchema, getSchema, listSchema, searchFields, relations, middleware = [], userTracking, relationFields, readOnly = false } = options;
+  const { entity, createSchema, updateSchema, getSchema, listSchema, searchFields, relations, middleware = [], userTracking, relationFields, readOnly = false, dataPermission, defaultFilters, tenantOptions } = options;
 
   // 创建路由实例
   const app = new OpenAPIHono<AuthContext>();
@@ -30,11 +32,11 @@ export function createCrudRoutes<
     middleware,
     request: {
       query: z.object({
-        page: z.coerce.number<number>().int().positive().default(1).openapi({
+        page: z.coerce.number<number>().int().positive('页码必须是正整数').default(1).openapi({
           example: 1,
           description: '页码,从1开始'
         }),
-        pageSize: z.coerce.number<number>().int().positive().default(10).openapi({
+        pageSize: z.coerce.number<number>().int().positive('每页数量必须是正整数').default(10).openapi({
           example: 10,
           description: '每页数量'
         }),
@@ -81,6 +83,10 @@ export function createCrudRoutes<
         description: '认证失败',
         content: { 'application/json': { schema: ErrorSchema } }
       },
+      403: {
+        description: '权限不足',
+        content: { 'application/json': { schema: ErrorSchema } }
+      },
       500: {
         description: '服务器错误',
         content: { 'application/json': { schema: ErrorSchema } }
@@ -113,6 +119,10 @@ export function createCrudRoutes<
         description: '认证失败',
         content: { 'application/json': { schema: ErrorSchema } }
       },
+      403: {
+        description: '权限不足',
+        content: { 'application/json': { schema: ErrorSchema } }
+      },
       500: {
         description: '服务器错误',
         content: { 'application/json': { schema: ErrorSchema } }
@@ -242,6 +252,7 @@ export function createCrudRoutes<
         try {
           const query = c.req.valid('query') as any;
           const { page, pageSize, keyword, sortBy, sortOrder, filters } = query;
+          const user = c.get('user');
 
           // 构建排序对象
           const order: any = {};
@@ -252,19 +263,36 @@ export function createCrudRoutes<
           }
 
           // 解析筛选条件
-          let parsedFilters: any = undefined;
+          let parsedFilters: any = { ...defaultFilters };
           if (filters) {
             try {
-              parsedFilters = JSON.parse(filters);
+              const userFilters = JSON.parse(filters);
+              // 合并默认过滤条件和用户传入的过滤条件
+              parsedFilters = { ...parsedFilters, ...userFilters };
             } catch (e) {
               return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
             }
           }
           const crudService = new ConcreteCrudService(entity, {
             userTracking: userTracking,
-            relationFields: relationFields
+            relationFields: relationFields,
+            dataPermission: dataPermission,
+            tenantOptions: tenantOptions
           });
 
+          // 设置租户上下文
+          const tenantId = c.get('tenantId');
+
+          // 优先从tenantId上下文获取,如果没有则从用户对象中提取
+          let finalTenantId = tenantId;
+          if (finalTenantId === undefined && user?.tenantId !== undefined) {
+            finalTenantId = user.tenantId;
+          }
+
+          if (finalTenantId !== undefined) {
+            crudService.setTenantContext(finalTenantId);
+          }
+
           const [data, total] = await crudService.getList(
             page,
             pageSize,
@@ -273,17 +301,32 @@ export function createCrudRoutes<
             undefined,
             relations || [],
             order,
-            parsedFilters
+            parsedFilters,
+            user?.id
           );
 
-          return c.json({
-            // data: z.array(listSchema).parse(data),
-            data: await parseWithAwait(z.array(listSchema), data),
-            pagination: { total, current: page, pageSize }
-          }, 200);
+
+          try {
+            const validatedData = await parseWithAwait(z.array(listSchema), data);
+            return c.json({
+              data: validatedData,
+              pagination: { total, current: page, pageSize }
+            }, 200);
+          } catch (validationError) {
+            if (validationError instanceof z.ZodError) {
+              const zodError = validationError as ZodError;
+              return c.json({
+                code: 400,
+                message: '参数验证失败',
+                errors: (zodError as any).errors || validationError.message
+              }, 400);
+            }
+            throw validationError;
+          }
         } catch (error) {
           if (error instanceof z.ZodError) {
-            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+            const zodError = error as ZodError;
+            return c.json({ code: 400, message: '参数验证失败', errors: (zodError as any).errors || error.message }, 400);
           }
           return c.json({
             code: 500,
@@ -299,14 +342,46 @@ export function createCrudRoutes<
 
           const crudService = new ConcreteCrudService(entity, {
             userTracking: userTracking,
-            relationFields: relationFields
+            relationFields: relationFields,
+            dataPermission: dataPermission,
+            tenantOptions: tenantOptions
           });
+
+          // 设置租户上下文
+          const tenantId = c.get('tenantId');
+
+          // 优先从tenantId上下文获取,如果没有则从用户对象中提取
+          let finalTenantId = tenantId;
+          if (finalTenantId === undefined && user?.tenantId !== undefined) {
+            finalTenantId = user.tenantId;
+          }
+
+          if (finalTenantId !== undefined) {
+            crudService.setTenantContext(finalTenantId);
+          }
           const result = await crudService.create(data, user?.id);
-          return c.json(result, 201);
+          // 重新获取包含关联关系的数据
+          const fullResult = await crudService.getById(result.id, relations || [], user?.id);
+          return c.json(await parseWithAwait(getSchema, fullResult), 201);
         } catch (error) {
           if (error instanceof z.ZodError) {
-            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+            const zodError = error as ZodError;
+            return c.json({ code: 400, message: '参数验证失败', errors: (zodError as any).errors || error.message }, 400);
+          }
+
+          // 处理数据库唯一约束错误
+          if (error instanceof Error && error.message.includes('duplicate key value violates unique constraint')) {
+            return c.json({ code: 400, message: '数据已存在,请检查唯一性约束' }, 400);
+          }
+
+          // 处理权限错误,返回403状态码
+          if (error instanceof Error && error.message.includes('无权')) {
+            return c.json({
+              code: 403,
+              message: error.message
+            }, 403);
           }
+
           return c.json({
             code: 500,
             message: error instanceof Error ? error.message : '创建资源失败'
@@ -317,22 +392,53 @@ export function createCrudRoutes<
       .openapi(getRouteDef, async (c: any) => {
         try {
           const { id } = c.req.valid('param');
+          const user = c.get('user');
 
           const crudService = new ConcreteCrudService(entity, {
             userTracking: userTracking,
-            relationFields: relationFields
+            relationFields: relationFields,
+            dataPermission: dataPermission,
+            tenantOptions: tenantOptions
           });
-          const result = await crudService.getById(id, relations || []);
+
+          // 设置租户上下文
+          const tenantId = c.get('tenantId');
+
+          // 优先从tenantId上下文获取,如果没有则从用户对象中提取
+          let finalTenantId = tenantId;
+          if (finalTenantId === undefined && user?.tenantId !== undefined) {
+            finalTenantId = user.tenantId;
+          }
+
+          if (finalTenantId !== undefined) {
+            crudService.setTenantContext(finalTenantId);
+          }
+          const result = await crudService.getById(id, relations || [], user?.id);
 
           if (!result) {
             return c.json({ code: 404, message: '资源不存在' }, 404);
           }
 
+          // 应用默认过滤条件
+          if (defaultFilters && Object.keys(defaultFilters).length > 0) {
+            const shouldFilter = Object.entries(defaultFilters).some(([key, value]) => {
+              return result[key as keyof T] !== value;
+            });
+            if (shouldFilter) {
+              return c.json({ code: 404, message: '资源不存在' }, 404);
+            }
+          }
+
           // return c.json(await getSchema.parseAsync(result), 200);
           return c.json(await parseWithAwait(getSchema, result), 200);
         } catch (error) {
           if (error instanceof z.ZodError) {
-            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+            const zodError = error as ZodError;
+            return c.json({ code: 400, message: '参数验证失败', errors: (zodError as any).errors || error.message }, 400);
+          }
+          if (error instanceof PermissionError) {
+            // GET操作中,权限错误应该返回404而不是403
+            return c.json({ code: 404, message: '资源不存在' }, 404);
           }
           return c.json({
             code: 500,
@@ -349,19 +455,46 @@ export function createCrudRoutes<
 
           const crudService = new ConcreteCrudService(entity, {
             userTracking: userTracking,
-            relationFields: relationFields
+            relationFields: relationFields,
+            dataPermission: dataPermission,
+            tenantOptions: tenantOptions
           });
+
+          // 设置租户上下文
+          const tenantId = c.get('tenantId');
+
+          // 优先从tenantId上下文获取,如果没有则从用户对象中提取
+          let finalTenantId = tenantId;
+          if (finalTenantId === undefined && user?.tenantId !== undefined) {
+            finalTenantId = user.tenantId;
+          }
+
+          if (finalTenantId !== undefined) {
+            crudService.setTenantContext(finalTenantId);
+          }
           const result = await crudService.update(id, data, user?.id);
 
           if (!result) {
             return c.json({ code: 404, message: '资源不存在' }, 404);
           }
 
-          return c.json(result, 200);
+          // 重新获取包含关联关系的数据
+          const fullResult = await crudService.getById(id, relations || [], user?.id);
+          return c.json(await parseWithAwait(getSchema, fullResult), 200);
         } catch (error) {
           if (error instanceof z.ZodError) {
-            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+            const zodError = error as ZodError;
+            return c.json({ code: 400, message: '参数验证失败', errors: (zodError as any).errors || error.message }, 400);
+          }
+
+          // 处理权限错误,返回403状态码
+          if (error instanceof Error && error.message.includes('无权')) {
+            return c.json({
+              code: 403,
+              message: error.message
+            }, 403);
           }
+
           return c.json({
             code: 500,
             message: error instanceof Error ? error.message : '更新资源失败'
@@ -371,12 +504,28 @@ export function createCrudRoutes<
       .openapi(deleteRouteDef, async (c: any) => {
         try {
           const { id } = c.req.valid('param');
+          const user = c.get('user');
 
           const crudService = new ConcreteCrudService(entity, {
             userTracking: userTracking,
-            relationFields: relationFields
+            relationFields: relationFields,
+            dataPermission: dataPermission,
+            tenantOptions: tenantOptions
           });
-          const success = await crudService.delete(id);
+
+          // 设置租户上下文
+          const tenantId = c.get('tenantId');
+
+          // 优先从tenantId上下文获取,如果没有则从用户对象中提取
+          let finalTenantId = tenantId;
+          if (finalTenantId === undefined && user?.tenantId !== undefined) {
+            finalTenantId = user.tenantId;
+          }
+
+          if (finalTenantId !== undefined) {
+            crudService.setTenantContext(finalTenantId);
+          }
+          const success = await crudService.delete(id, user?.id);
 
           if (!success) {
             return c.json({ code: 404, message: '资源不存在' }, 404);
@@ -385,8 +534,18 @@ export function createCrudRoutes<
           return c.body(null, 204);
         } catch (error) {
           if (error instanceof z.ZodError) {
-            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+            const zodError = error as ZodError;
+            return c.json({ code: 400, message: '参数验证失败', errors: (zodError as any).errors || error.message }, 400);
+          }
+
+          // 处理权限错误,返回403状态码
+          if (error instanceof Error && error.message.includes('无权')) {
+            return c.json({
+              code: 403,
+              message: error.message
+            }, 403);
           }
+
           return c.json({
             code: 500,
             message: error instanceof Error ? error.message : '删除资源失败'
@@ -402,6 +561,7 @@ export function createCrudRoutes<
         try {
           const query = c.req.valid('query') as any;
           const { page, pageSize, keyword, sortBy, sortOrder, filters } = query;
+          const user = c.get('user');
 
           // 构建排序对象
           const order: any = {};
@@ -412,19 +572,36 @@ export function createCrudRoutes<
           }
 
           // 解析筛选条件
-          let parsedFilters: any = undefined;
+          let parsedFilters: any = { ...defaultFilters };
           if (filters) {
             try {
-              parsedFilters = JSON.parse(filters);
+              const userFilters = JSON.parse(filters);
+              // 合并默认过滤条件和用户传入的过滤条件
+              parsedFilters = { ...parsedFilters, ...userFilters };
             } catch (e) {
               return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
             }
           }
           const crudService = new ConcreteCrudService(entity, {
             userTracking: userTracking,
-            relationFields: relationFields
+            relationFields: relationFields,
+            dataPermission: dataPermission,
+            tenantOptions: tenantOptions
           });
 
+          // 设置租户上下文
+          const tenantId = c.get('tenantId');
+
+          // 优先从tenantId上下文获取,如果没有则从用户对象中提取
+          let finalTenantId = tenantId;
+          if (finalTenantId === undefined && user?.tenantId !== undefined) {
+            finalTenantId = user.tenantId;
+          }
+
+          if (finalTenantId !== undefined) {
+            crudService.setTenantContext(finalTenantId);
+          }
+
           const [data, total] = await crudService.getList(
             page,
             pageSize,
@@ -433,7 +610,8 @@ export function createCrudRoutes<
             undefined,
             relations || [],
             order,
-            parsedFilters
+            parsedFilters,
+            user?.id
           );
 
           return c.json({
@@ -442,8 +620,18 @@ export function createCrudRoutes<
           }, 200);
         } catch (error) {
           if (error instanceof z.ZodError) {
-            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+            const zodError = error as ZodError;
+            return c.json({ code: 400, message: '参数验证失败', errors: (zodError as any).errors || error.message }, 400);
+          }
+
+          // 处理权限错误,返回403状态码
+          if (error instanceof Error && error.message.includes('无权')) {
+            return c.json({
+              code: 403,
+              message: error.message
+            }, 403);
           }
+
           return c.json({
             code: 500,
             message: error instanceof Error ? error.message : '获取列表失败'
@@ -454,22 +642,61 @@ export function createCrudRoutes<
       .openapi(getRouteDef, async (c: any) => {
         try {
           const { id } = c.req.valid('param');
+          const user = c.get('user');
 
           const crudService = new ConcreteCrudService(entity, {
             userTracking: userTracking,
-            relationFields: relationFields
+            relationFields: relationFields,
+            dataPermission: dataPermission,
+            tenantOptions: tenantOptions
           });
-          const result = await crudService.getById(id, relations || []);
+
+          // 设置租户上下文
+          const tenantId = c.get('tenantId');
+
+          // 优先从tenantId上下文获取,如果没有则从用户对象中提取
+          let finalTenantId = tenantId;
+          if (finalTenantId === undefined && user?.tenantId !== undefined) {
+            finalTenantId = user.tenantId;
+          }
+
+          if (finalTenantId !== undefined) {
+            crudService.setTenantContext(finalTenantId);
+          }
+          const result = await crudService.getById(id, relations || [], user?.id);
 
           if (!result) {
             return c.json({ code: 404, message: '资源不存在' }, 404);
           }
 
+          // 应用默认过滤条件
+          if (defaultFilters && Object.keys(defaultFilters).length > 0) {
+            const shouldFilter = Object.entries(defaultFilters).some(([key, value]) => {
+              return result[key as keyof T] !== value;
+            });
+            if (shouldFilter) {
+              return c.json({ code: 404, message: '资源不存在' }, 404);
+            }
+          }
+
           return c.json(await parseWithAwait(getSchema, result), 200);
         } catch (error) {
           if (error instanceof z.ZodError) {
-            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+            const zodError = error as ZodError;
+            return c.json({ code: 400, message: '参数验证失败', errors: (zodError as any).errors || error.message }, 400);
           }
+          if (error instanceof PermissionError) {
+            return c.json({ code: 403, message: error.message }, 403);
+          }
+
+          // 处理权限错误,返回403状态码
+          if (error instanceof Error && error.message.includes('无权')) {
+            return c.json({
+              code: 403,
+              message: error.message
+            }, 403);
+          }
+
           return c.json({
             code: 500,
             message: error instanceof Error ? error.message : '获取资源失败'

+ 8 - 2
packages/shared-crud/src/services/concrete-crud.service.ts

@@ -1,12 +1,18 @@
 import { ObjectLiteral } from 'typeorm';
-import { GenericCrudService, UserTrackingOptions, RelationFieldOptions } from './generic-crud.service';
+import { GenericCrudService, UserTrackingOptions, RelationFieldOptions, TenantOptions } from './generic-crud.service';
+import { DataPermissionOptions } from '../types/data-permission.types';
 import { AppDataSource } from '@d8d/shared-utils';
 
 // 创建具体CRUD服务类
 export class ConcreteCrudService<T extends ObjectLiteral> extends GenericCrudService<T> {
   constructor(
     entity: new () => T,
-    options?: { userTracking?: UserTrackingOptions; relationFields?: RelationFieldOptions }
+    options?: {
+      userTracking?: UserTrackingOptions;
+      relationFields?: RelationFieldOptions;
+      dataPermission?: DataPermissionOptions;
+      tenantOptions?: TenantOptions;
+    }
   ) {
     super(AppDataSource, entity, options);
   }

+ 250 - 8
packages/shared-crud/src/services/generic-crud.service.ts

@@ -1,9 +1,12 @@
 import { DataSource, Repository, ObjectLiteral, DeepPartial, In } from 'typeorm';
 import { z } from '@hono/zod-openapi';
+import { DataPermissionOptions, validateDataPermissionOptions, PermissionError } from '../types/data-permission.types';
 
 export abstract class GenericCrudService<T extends ObjectLiteral> {
-  protected repository: Repository<T>;
+  public repository: Repository<T>;
   private userTrackingOptions?: UserTrackingOptions;
+  private dataPermissionOptions?: DataPermissionOptions;
+  private tenantOptions?: TenantOptions;
 
   protected relationFields?: RelationFieldOptions;
 
@@ -13,11 +16,22 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
     options?: {
       userTracking?: UserTrackingOptions;
       relationFields?: RelationFieldOptions;
+      dataPermission?: DataPermissionOptions;
+      tenantOptions?: TenantOptions;
     }
   ) {
     this.repository = this.dataSource.getRepository(entity);
     this.userTrackingOptions = options?.userTracking;
     this.relationFields = options?.relationFields;
+
+    // 验证并设置数据权限配置
+    if (options?.dataPermission) {
+      validateDataPermissionOptions(options.dataPermission);
+      this.dataPermissionOptions = options.dataPermission;
+    }
+
+    // 设置租户选项
+    this.tenantOptions = options?.tenantOptions;
   }
 
   /**
@@ -33,11 +47,27 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
     order: { [P in keyof T]?: 'ASC' | 'DESC' } = {},
     filters?: {
       [key: string]: any;
-    }
+    },
+    userId?: string | number
   ): Promise<[T[], number]> {
     const skip = (page - 1) * pageSize;
     const query = this.repository.createQueryBuilder('entity');
 
+    // 添加数据权限过滤
+    if (this.dataPermissionOptions?.enabled && userId) {
+      const userIdField = this.dataPermissionOptions.userIdField;
+      query.andWhere(`entity.${userIdField} = :userId`, { userId });
+    }
+
+    // 添加租户隔离过滤
+    if (this.tenantOptions?.enabled) {
+      const tenantIdField = this.tenantOptions.tenantIdField || 'tenantId';
+      const tenantId = await this.extractTenantId(userId);
+      if (tenantId !== undefined && tenantId !== null) {
+        query.andWhere(`entity.${tenantIdField} = :tenantId`, { tenantId });
+      }
+    }
+
     // 添加关联关系(支持嵌套关联,如 ['contract.client'])
     // 使用一致的别名生成策略,确保搜索时能正确引用关联字段
     if (relations.length > 0) {
@@ -155,11 +185,129 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
   /**
    * 根据ID获取单个实体
    */
-  async getById(id: number, relations: string[] = []): Promise<T | null> {
-    return this.repository.findOne({
+  async getById(id: number, relations: string[] = [], userId?: string | number): Promise<T | null> {
+    const entity = await this.repository.findOne({
       where: { id } as any,
       relations
     });
+
+    if (!entity) {
+      return null;
+    }
+
+    // 租户隔离验证 - 先于数据权限验证
+    if (this.tenantOptions?.enabled) {
+      const tenantIdField = this.tenantOptions.tenantIdField || 'tenantId';
+      const tenantId = await this.extractTenantId(userId);
+      if (tenantId !== undefined && tenantId !== null) {
+        const entityTenantId = (entity as any)[tenantIdField];
+        if (entityTenantId !== tenantId) {
+          return null; // 不属于当前租户,返回null
+        }
+      }
+    }
+
+    // 数据权限验证
+    if (this.dataPermissionOptions?.enabled && userId) {
+      const hasPermission = await this.checkPermission(entity, userId);
+      if (!hasPermission) {
+        throw new PermissionError('没有权限访问该资源');
+      }
+    }
+
+    return entity;
+  }
+
+  /**
+   * 检查用户对实体的权限
+   */
+  private async checkPermission(entity: any, userId: string | number): Promise<boolean> {
+    const options = this.dataPermissionOptions;
+    if (!options?.enabled) return true;
+
+    // 管理员权限覆盖检查
+    if (options.adminOverride?.enabled && options.adminOverride.adminRole) {
+      // 这里需要从认证系统获取用户角色信息
+      // 暂时假设管理员可以访问所有数据
+      // 实际实现中需要集成用户角色检查
+      const isAdmin = await this.checkAdminRole(userId, options.adminOverride.adminRole);
+      if (isAdmin) {
+        return true;
+      }
+    }
+
+    // 自定义权限验证器
+    if (options.customValidator) {
+      return await options.customValidator(userId, entity);
+    }
+
+    // 基础权限验证:用户ID字段匹配
+    const userIdField = options.userIdField;
+    const entityUserId = entity[userIdField];
+    return entityUserId === userId;
+  }
+
+  /**
+   * 检查用户是否为管理员
+   * TODO: 需要集成实际的用户角色检查
+   */
+  private async checkAdminRole(userId: string | number, adminRole: string): Promise<boolean> {
+    // 这里需要从认证系统获取用户角色信息
+    // 暂时返回false,实际实现中需要集成用户角色检查
+    return false;
+  }
+
+  /**
+   * 提取租户ID
+   * 从用户对象或认证上下文中提取租户ID
+   */
+  private async extractTenantId(userId?: string | number): Promise<string | number | undefined> {
+    // 首先检查是否有存储的租户上下文
+    if ((this as any)._tenantId !== undefined) {
+      console.debug('从存储的租户上下文中获取租户ID:', (this as any)._tenantId);
+      return (this as any)._tenantId;
+    }
+
+    // 如果租户选项启用了从上下文自动提取,则从上下文获取
+    if (this.tenantOptions?.autoExtractFromContext) {
+      // 这里需要从Hono上下文中获取租户ID
+      // 在实际实现中,认证中间件应该设置租户上下文
+      // 暂时返回undefined,实际实现中需要认证中间件设置tenantId
+      console.debug('autoExtractFromContext为true,但未实现从Hono上下文获取租户ID');
+      return undefined;
+    }
+
+    // 如果用户对象包含租户ID字段,则从用户对象中提取
+    // 这里需要实际的用户对象,暂时返回undefined
+    console.debug('没有找到租户ID,返回undefined');
+    return undefined;
+  }
+
+  /**
+   * 设置租户上下文
+   * 用于从外部传递租户ID
+   */
+  setTenantContext(tenantId: string | number): void {
+    // 存储租户上下文
+    (this as any)._tenantId = tenantId;
+    console.debug('设置租户上下文:', tenantId);
+  }
+
+  /**
+   * 设置租户字段
+   */
+  private async setTenantFields(data: any, userId?: string | number): Promise<void> {
+    if (!this.tenantOptions?.enabled) {
+      return;
+    }
+
+    const tenantIdField = this.tenantOptions.tenantIdField || 'tenantId';
+    const tenantId = await this.extractTenantId(userId);
+
+    // 只有在数据中不存在租户ID字段时才设置
+    if (tenantId !== undefined && tenantId !== null && !data[tenantIdField]) {
+      data[tenantIdField] = tenantId;
+    }
   }
 
   /**
@@ -177,7 +325,8 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
     } = this.userTrackingOptions;
 
     // 设置创建人
-    if (isCreate && createdByField) {
+    // 只有在数据中不存在该字段时才设置,避免覆盖管理员传入的用户ID
+    if (isCreate && createdByField && !data[createdByField]) {
       data[createdByField] = userId;
     }
 
@@ -187,7 +336,8 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
     }
 
     // 设置关联的用户ID(如userId字段)
-    if (isCreate && userIdField) {
+    // 只有在数据中不存在该字段时才设置,避免覆盖管理员传入的用户ID
+    if (isCreate && userIdField && !data[userIdField]) {
       data[userIdField] = userId;
     }
   }
@@ -220,8 +370,20 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
    * 创建实体
    */
   async create(data: DeepPartial<T>, userId?: string | number): Promise<T> {
+    // 权限验证:防止用户创建不属于自己的数据
+    if (this.dataPermissionOptions?.enabled && userId) {
+      const userIdField = this.dataPermissionOptions.userIdField;
+
+      // 如果数据中已经包含用户ID字段,验证是否与当前用户匹配
+      const dataObj = data as any;
+      if (dataObj[userIdField] && dataObj[userIdField] !== userId) {
+        throw new Error('无权创建该资源');
+      }
+    }
+
     const entityData = { ...data };
     this.setUserFields(entityData, userId, true);
+    await this.setTenantFields(entityData, userId);
 
     // 分离关联字段数据
     const relationData: any = {};
@@ -246,6 +408,32 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
    * 更新实体
    */
   async update(id: number, data: Partial<T>, userId?: string | number): Promise<T | null> {
+    // 权限验证
+    if (this.dataPermissionOptions?.enabled && userId) {
+      const entity = await this.getById(id);
+      if (!entity) return null;
+
+      const hasPermission = await this.checkPermission(entity, userId);
+      if (!hasPermission) {
+        throw new Error('无权更新该资源');
+      }
+    }
+
+    // 租户隔离验证
+    if (this.tenantOptions?.enabled) {
+      const entity = await this.getById(id);
+      if (!entity) return null;
+
+      const tenantIdField = this.tenantOptions.tenantIdField || 'tenantId';
+      const tenantId = await this.extractTenantId(userId);
+      if (tenantId !== undefined && tenantId !== null) {
+        const entityTenantId = (entity as any)[tenantIdField];
+        if (entityTenantId !== tenantId) {
+          return null; // 不属于当前租户,返回null
+        }
+      }
+    }
+
     const updateData = { ...data };
     this.setUserFields(updateData, userId, false);
 
@@ -276,7 +464,34 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
   /**
    * 删除实体
    */
-  async delete(id: number): Promise<boolean> {
+  async delete(id: number, userId?: string | number): Promise<boolean> {
+    // 权限验证
+    if (this.dataPermissionOptions?.enabled && userId) {
+      const entity = await this.getById(id);
+      if (!entity) return false;
+
+      const hasPermission = await this.checkPermission(entity, userId);
+      if (!hasPermission) {
+        throw new Error('无权删除该资源');
+      }
+    }
+
+    // 租户隔离验证
+    if (this.tenantOptions?.enabled) {
+      const entity = await this.getById(id);
+      if (!entity) return false;
+
+      const tenantIdField = this.tenantOptions.tenantIdField || 'tenantId';
+      const tenantId = await this.extractTenantId(userId);
+      if (tenantId !== undefined && tenantId !== null) {
+        const entityTenantId = (entity as any)[tenantIdField];
+        if (entityTenantId !== tenantId) {
+          return false; // 不属于当前租户,返回false
+        }
+      }
+    }
+
+    // 执行删除
     const result = await this.repository.delete(id);
     return result.affected === 1;
   }
@@ -321,4 +536,31 @@ export type CrudOptions<
   userTracking?: UserTrackingOptions;
   relationFields?: RelationFieldOptions;
   readOnly?: boolean;
-};
+  /**
+   * 数据权限控制配置
+   */
+  dataPermission?: DataPermissionOptions;
+  /**
+   * 默认过滤条件,会在所有查询中应用
+   */
+  defaultFilters?: Partial<T>;
+  /**
+   * 租户隔离配置
+   */
+  tenantOptions?: TenantOptions;
+};
+
+export interface TenantOptions {
+  /**
+   * 租户ID字段名,默认为 'tenantId'
+   */
+  tenantIdField?: string;
+  /**
+   * 是否启用租户隔离
+   */
+  enabled?: boolean;
+  /**
+   * 是否自动从认证上下文提取租户ID
+   */
+  autoExtractFromContext?: boolean;
+}

+ 2 - 1
packages/shared-crud/src/services/index.ts

@@ -1,3 +1,4 @@
 export { GenericCrudService } from './generic-crud.service';
 export { ConcreteCrudService } from './concrete-crud.service';
-export type { UserTrackingOptions, RelationFieldOptions, CrudOptions } from './generic-crud.service';
+export type { UserTrackingOptions, RelationFieldOptions, CrudOptions } from './generic-crud.service';
+export type { DataPermissionOptions, PermissionError, PermissionResult } from '../types/data-permission.types';

+ 127 - 0
packages/shared-crud/src/types/data-permission.types.ts

@@ -0,0 +1,127 @@
+/**
+ * 数据权限控制配置选项
+ */
+export interface DataPermissionOptions {
+  /**
+   * 是否启用数据权限控制
+   * @default false
+   */
+  enabled: boolean;
+
+  /**
+   * 用户ID字段名,用于权限过滤
+   * 例如: 'userId', 'createdBy'
+   */
+  userIdField: string;
+
+  /**
+   * 管理员权限覆盖配置
+   */
+  adminOverride?: {
+    /**
+     * 是否启用管理员权限覆盖
+     * @default false
+     */
+    enabled: boolean;
+
+    /**
+     * 管理员角色标识
+     * @default 'admin'
+     */
+    adminRole?: string;
+
+    /**
+     * 管理员用户ID字段(可选,用于管理员用户ID检查)
+     */
+    userIdField?: string;
+  };
+
+  /**
+   * 多租户数据隔离配置
+   */
+  multiTenant?: {
+    /**
+     * 是否启用多租户数据隔离
+     * @default false
+     */
+    enabled: boolean;
+
+    /**
+     * 租户ID字段名
+     */
+    tenantIdField: string;
+  };
+
+  /**
+   * 自定义权限验证函数
+   * @param userId 当前用户ID
+   * @param entity 目标实体
+   * @returns 是否具有权限
+   */
+  customValidator?: (userId: string | number, entity: any) => Promise<boolean>;
+}
+
+/**
+ * 权限验证错误
+ */
+export class PermissionError extends Error {
+  constructor(message: string) {
+    super(message);
+    this.name = 'PermissionError';
+  }
+}
+
+/**
+ * 权限验证结果
+ */
+export interface PermissionResult {
+  /**
+   * 是否具有权限
+   */
+  hasPermission: boolean;
+
+  /**
+   * 错误信息(如果没有权限)
+   */
+  error?: string;
+
+  /**
+   * 是否被管理员权限覆盖
+   */
+  adminOverride?: boolean;
+}
+
+/**
+ * 验证数据权限配置
+ * @param options 数据权限配置
+ * @throws {Error} 如果配置无效
+ */
+export function validateDataPermissionOptions(options: DataPermissionOptions): void {
+  if (!options.enabled) {
+    return;
+  }
+
+  // 验证必填字段
+  if (!options.userIdField) {
+    throw new Error('启用数据权限控制时必须指定 userIdField');
+  }
+
+  // 验证管理员权限覆盖配置
+  if (options.adminOverride?.enabled) {
+    if (!options.adminOverride.adminRole) {
+      throw new Error('启用管理员权限覆盖时必须指定 adminRole');
+    }
+  }
+
+  // 验证多租户配置
+  if (options.multiTenant?.enabled) {
+    if (!options.multiTenant.tenantIdField) {
+      throw new Error('启用多租户数据隔离时必须指定 tenantIdField');
+    }
+  }
+
+  // 验证自定义验证器
+  if (options.customValidator && typeof options.customValidator !== 'function') {
+    throw new Error('customValidator 必须是一个函数');
+  }
+}

+ 586 - 0
packages/shared-crud/tests/integration/data-permission.integration.test.ts

@@ -0,0 +1,586 @@
+import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
+import { testClient } from 'hono/testing';
+import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities } from '@d8d/shared-test-util';
+import { JWTUtil } from '@d8d/shared-utils';
+import { z } from '@hono/zod-openapi';
+import { createCrudRoutes } from '../../src/routes/generic-crud.routes';
+import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
+
+// 测试用户实体
+@Entity()
+class TestUser {
+  @PrimaryGeneratedColumn()
+  id!: number;
+
+  @Column('varchar')
+  username!: string;
+
+  @Column('varchar')
+  password!: string;
+
+  @Column('varchar')
+  nickname!: string;
+
+  @Column('varchar')
+  registrationSource!: string;
+}
+
+// 测试实体类
+@Entity()
+class TestEntity {
+  @PrimaryGeneratedColumn()
+  id!: number;
+
+  @Column('varchar')
+  name!: string;
+
+  @Column('int')
+  userId!: number;
+
+  @Column('int', { nullable: true })
+  createdBy?: number;
+
+  @Column('int', { nullable: true })
+  updatedBy?: number;
+}
+
+// 定义测试实体的Schema
+const createTestSchema = z.object({
+  name: z.string().min(1, '名称不能为空'),
+  userId: z.number().optional()
+});
+
+const updateTestSchema = z.object({
+  name: z.string().min(1, '名称不能为空').optional()
+});
+
+const getTestSchema = z.object({
+  id: z.number(),
+  name: z.string(),
+  userId: z.number(),
+  createdBy: z.number().nullable().optional(),
+  updatedBy: z.number().nullable().optional()
+});
+
+const listTestSchema = z.object({
+  id: z.number(),
+  name: z.string(),
+  userId: z.number(),
+  createdBy: z.number().nullable().optional(),
+  updatedBy: z.number().nullable().optional()
+});
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooksWithEntities([TestUser, TestEntity])
+
+describe('共享CRUD数据权限控制集成测试', () => {
+  let client: any;
+  let testToken1: string;
+  let testToken2: string;
+  let testUser1: TestUser;
+  let testUser2: TestUser;
+  let mockAuthMiddleware: any;
+
+  beforeEach(async () => {
+    // 获取数据源
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+
+    // 创建测试用户1
+    const userRepository = dataSource.getRepository(TestUser);
+    testUser1 = userRepository.create({
+      username: `test_user_1_${Date.now()}`,
+      password: 'test_password',
+      nickname: '测试用户1',
+      registrationSource: 'web'
+    });
+    await userRepository.save(testUser1);
+
+    // 创建测试用户2
+    testUser2 = userRepository.create({
+      username: `test_user_2_${Date.now()}`,
+      password: 'test_password',
+      nickname: '测试用户2',
+      registrationSource: 'web'
+    });
+    await userRepository.save(testUser2);
+
+    // 生成测试用户的token
+    testToken1 = JWTUtil.generateToken({
+      id: testUser1.id,
+      username: testUser1.username,
+      roles: [{name:'user'}]
+    });
+
+    testToken2 = JWTUtil.generateToken({
+      id: testUser2.id,
+      username: testUser2.username,
+      roles: [{name:'user'}]
+    });
+
+    // 创建模拟认证中间件
+    mockAuthMiddleware = async (c: any, next: any) => {
+      const authHeader = c.req.header('Authorization');
+      if (authHeader && authHeader.startsWith('Bearer ')) {
+        const token = authHeader.substring(7);
+        try {
+          // 简单模拟用户解析
+          if (token === testToken1) {
+            c.set('user', { id: testUser1.id, username: testUser1.username });
+          } else if (token === testToken2) {
+            c.set('user', { id: testUser2.id, username: testUser2.username });
+          }
+        } catch (error) {
+          // token解析失败
+        }
+      } else {
+        // 没有认证信息,返回401
+        return c.json({ code: 401, message: '认证失败' }, 401);
+      }
+      await next();
+    };
+
+    // 创建测试路由 - 启用数据权限控制
+    const testRoutes = createCrudRoutes({
+      entity: TestEntity,
+      createSchema: createTestSchema,
+      updateSchema: updateTestSchema,
+      getSchema: getTestSchema,
+      listSchema: listTestSchema,
+      middleware: [mockAuthMiddleware],
+      dataPermission: {
+        enabled: true,
+        userIdField: 'userId'
+      }
+    });
+
+    client = testClient(testRoutes);
+  });
+
+  describe('GET / - 列表查询权限过滤', () => {
+    it('应该只返回当前用户的数据', async () => {
+      // 创建测试数据
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const testRepository = dataSource.getRepository(TestEntity);
+
+      // 为用户1创建数据
+      const user1Data1 = testRepository.create({
+        name: '用户1的数据1',
+        userId: testUser1.id
+      });
+      await testRepository.save(user1Data1);
+
+      const user1Data2 = testRepository.create({
+        name: '用户1的数据2',
+        userId: testUser1.id
+      });
+      await testRepository.save(user1Data2);
+
+      // 为用户2创建数据
+      const user2Data = testRepository.create({
+        name: '用户2的数据',
+        userId: testUser2.id
+      });
+      await testRepository.save(user2Data);
+
+      // 用户1查询列表
+      const response = await client.index.$get({
+        query: {
+          page: 1,
+          pageSize: 10
+        }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      console.debug('列表查询响应状态:', response.status);
+
+      if (response.status !== 200) {
+        const errorData = await response.json();
+        console.debug('列表查询错误信息:', errorData);
+      }
+
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data).toHaveProperty('data');
+        expect(Array.isArray(data.data)).toBe(true);
+        expect(data.data).toHaveLength(2); // 应该只返回用户1的2条数据
+
+        // 验证所有返回的数据都属于用户1
+        data.data.forEach((item: any) => {
+          expect(item.userId).toBe(testUser1.id);
+        });
+      }
+    });
+
+    it('应该拒绝未认证用户的访问', async () => {
+      const response = await client.index.$get({
+        query: {
+          page: 1,
+          pageSize: 10
+        }
+      });
+      expect(response.status).toBe(401);
+    });
+  });
+
+  describe('POST / - 创建操作权限验证', () => {
+    it('应该成功创建属于当前用户的数据', async () => {
+      const createData = {
+        name: '测试创建数据',
+        userId: testUser1.id // 用户ID与当前用户匹配
+      };
+
+      const response = await client.index.$post({
+        json: createData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      console.debug('创建数据响应状态:', response.status);
+      expect(response.status).toBe(201);
+
+      if (response.status === 201) {
+        const data = await response.json();
+        expect(data).toHaveProperty('id');
+        expect(data.name).toBe(createData.name);
+        expect(data.userId).toBe(testUser1.id);
+      }
+    });
+
+    it('应该拒绝创建不属于当前用户的数据', async () => {
+      const createData = {
+        name: '测试创建数据',
+        userId: testUser2.id // 用户ID与当前用户不匹配
+      };
+
+      const response = await client.index.$post({
+        json: createData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      console.debug('创建无权数据响应状态:', response.status);
+      expect(response.status).toBe(403); // 权限验证失败返回403 Forbidden
+
+      if (response.status === 403) {
+        const data = await response.json();
+        expect(data.message).toContain('无权');
+      }
+    });
+  });
+
+  describe('GET /:id - 获取详情权限验证', () => {
+    it('应该成功获取属于当前用户的数据详情', async () => {
+      // 先创建测试数据
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const testRepository = dataSource.getRepository(TestEntity);
+      const testData = testRepository.create({
+        name: '测试数据详情',
+        userId: testUser1.id
+      });
+      await testRepository.save(testData);
+
+      const response = await client[':id'].$get({
+        param: { id: testData.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      console.debug('获取详情响应状态:', response.status);
+
+      if (response.status !== 200) {
+        const errorData = await response.json();
+        console.debug('获取详情错误信息:', errorData);
+      }
+
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data.id).toBe(testData.id);
+        expect(data.name).toBe(testData.name);
+        expect(data.userId).toBe(testUser1.id);
+      }
+    });
+
+    it('应该拒绝获取不属于当前用户的数据详情', async () => {
+      // 先创建属于用户2的数据
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const testRepository = dataSource.getRepository(TestEntity);
+      const testData = testRepository.create({
+        name: '用户2的数据',
+        userId: testUser2.id
+      });
+      await testRepository.save(testData);
+
+      // 用户1尝试获取用户2的数据
+      const response = await client[':id'].$get({
+        param: { id: testData.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      console.debug('获取无权详情响应状态:', response.status);
+      expect(response.status).toBe(403); // 权限验证失败返回403
+    });
+
+    it('应该处理不存在的资源', async () => {
+      const response = await client[':id'].$get({
+        param: { id: 999999 }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      expect(response.status).toBe(404);
+    });
+  });
+
+  describe('PUT /:id - 更新操作权限验证', () => {
+    it('应该成功更新属于当前用户的数据', async () => {
+      // 先创建测试数据
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const testRepository = dataSource.getRepository(TestEntity);
+      const testData = testRepository.create({
+        name: '原始数据',
+        userId: testUser1.id
+      });
+      await testRepository.save(testData);
+
+      const updateData = {
+        name: '更新后的数据'
+      };
+
+      const response = await client[':id'].$put({
+        param: { id: testData.id },
+        json: updateData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      console.debug('更新数据响应状态:', response.status);
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data.name).toBe(updateData.name);
+        expect(data.userId).toBe(testUser1.id);
+      }
+    });
+
+    it('应该拒绝更新不属于当前用户的数据', async () => {
+      // 先创建属于用户2的数据
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const testRepository = dataSource.getRepository(TestEntity);
+      const testData = testRepository.create({
+        name: '用户2的数据',
+        userId: testUser2.id
+      });
+      await testRepository.save(testData);
+
+      const updateData = {
+        name: '尝试更新的数据'
+      };
+
+      // 用户1尝试更新用户2的数据
+      const response = await client[':id'].$put({
+        param: { id: testData.id },
+        json: updateData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      console.debug('更新无权数据响应状态:', response.status);
+      expect(response.status).toBe(403); // 权限验证失败返回403 Forbidden
+
+      if (response.status === 403) {
+        const data = await response.json();
+        expect(data.message).toContain('无权');
+      }
+    });
+  });
+
+  describe('DELETE /:id - 删除操作权限验证', () => {
+    it('应该成功删除属于当前用户的数据', async () => {
+      // 先创建测试数据
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const testRepository = dataSource.getRepository(TestEntity);
+      const testData = testRepository.create({
+        name: '待删除数据',
+        userId: testUser1.id
+      });
+      await testRepository.save(testData);
+
+      const response = await client[':id'].$delete({
+        param: { id: testData.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      console.debug('删除数据响应状态:', response.status);
+      expect(response.status).toBe(204);
+
+      // 验证数据确实被删除
+      const deletedData = await testRepository.findOne({
+        where: { id: testData.id }
+      });
+      expect(deletedData).toBeNull();
+    });
+
+    it('应该拒绝删除不属于当前用户的数据', async () => {
+      // 先创建属于用户2的数据
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const testRepository = dataSource.getRepository(TestEntity);
+      const testData = testRepository.create({
+        name: '用户2的数据',
+        userId: testUser2.id
+      });
+      await testRepository.save(testData);
+
+      // 用户1尝试删除用户2的数据
+      const response = await client[':id'].$delete({
+        param: { id: testData.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      console.debug('删除无权数据响应状态:', response.status);
+      expect(response.status).toBe(403); // 权限验证失败返回403 Forbidden
+
+      if (response.status === 403) {
+        const data = await response.json();
+        expect(data.message).toContain('无权');
+      }
+
+      // 验证数据没有被删除
+      const existingData = await testRepository.findOne({
+        where: { id: testData.id }
+      });
+      expect(existingData).not.toBeNull();
+    });
+  });
+
+  describe('禁用数据权限控制的情况', () => {
+    it('当数据权限控制禁用时应该允许跨用户访问', async () => {
+      // 创建禁用数据权限控制的路由
+      const noPermissionRoutes = createCrudRoutes({
+        entity: TestEntity,
+        createSchema: createTestSchema,
+        updateSchema: updateTestSchema,
+        getSchema: getTestSchema,
+        listSchema: listTestSchema,
+        middleware: [mockAuthMiddleware],
+        dataPermission: {
+          enabled: false, // 禁用权限控制
+          userIdField: 'userId'
+        }
+      });
+
+      const noPermissionClient = testClient(noPermissionRoutes);
+
+      // 创建属于用户2的数据
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const testRepository = dataSource.getRepository(TestEntity);
+      const testData = testRepository.create({
+        name: '用户2的数据',
+        userId: testUser2.id
+      });
+      await testRepository.save(testData);
+
+      // 用户1应该能够访问用户2的数据(权限控制已禁用)
+      const response = await noPermissionClient[':id'].$get({
+        param: { id: testData.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      console.debug('禁用权限控制时的响应状态:', response.status);
+
+      if (response.status !== 200) {
+        try {
+          const errorData = await response.json();
+          console.debug('禁用权限控制时的错误信息:', errorData);
+        } catch (e) {
+          const text = await response.text();
+          console.debug('禁用权限控制时的响应文本:', text);
+        }
+      }
+
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data.id).toBe(testData.id);
+        expect(data.userId).toBe(testUser2.id);
+      }
+    });
+
+    it('当不传递dataPermission配置时应该允许跨用户访问', async () => {
+      // 创建不传递数据权限控制的路由
+      const noPermissionRoutes = createCrudRoutes({
+        entity: TestEntity,
+        createSchema: createTestSchema,
+        updateSchema: updateTestSchema,
+        getSchema: getTestSchema,
+        listSchema: listTestSchema,
+        middleware: [mockAuthMiddleware]
+        // 不传递 dataPermission 配置
+      });
+
+      const noPermissionClient = testClient(noPermissionRoutes);
+
+      // 创建属于用户2的数据
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const testRepository = dataSource.getRepository(TestEntity);
+      const testData = testRepository.create({
+        name: '用户2的数据(无权限配置)',
+        userId: testUser2.id
+      });
+      await testRepository.save(testData);
+
+      // 用户1应该能够访问用户2的数据(没有权限控制配置)
+      console.debug('测试数据ID(无权限配置):', testData.id);
+
+      const response = await noPermissionClient[':id'].$get({
+        param: { id: testData.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      console.debug('无权限配置时的响应状态:', response.status);
+
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data.id).toBe(testData.id);
+        expect(data.userId).toBe(testUser2.id);
+      }
+    });
+  });
+});

+ 371 - 0
packages/shared-crud/tests/integration/schema-type-conversion.integration.test.ts

@@ -0,0 +1,371 @@
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import { testClient } from 'hono/testing';
+import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities } from '@d8d/shared-test-util';
+import { JWTUtil } from '@d8d/shared-utils';
+import { z } from '@hono/zod-openapi';
+import { createCrudRoutes } from '../../src/routes/generic-crud.routes';
+import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
+
+// 测试实体类 - 包含decimal和bigint字段
+@Entity()
+class TestNumberEntity {
+  @PrimaryGeneratedColumn()
+  id!: number;
+
+  @Column('varchar')
+  name!: string;
+
+  @Column({ type: 'decimal', precision: 10, scale: 2, default: 0.00 })
+  price!: number;
+
+  @Column({ type: 'bigint', unsigned: true, default: 0 })
+  quantity!: number;
+
+  @Column({ type: 'decimal', precision: 15, scale: 4, nullable: true })
+  discount?: number;
+
+  @Column('int')
+  userId!: number;
+}
+
+// 定义测试实体的Schema
+const createTestSchema = z.object({
+  name: z.string().min(1, '名称不能为空'),
+  price: z.coerce.number().positive('价格必须为正数'),
+  quantity: z.coerce.number().int().min(0, '数量必须为非负整数'),
+  discount: z.coerce.number().min(0).max(1).optional(),
+  userId: z.coerce.number().optional()
+});
+
+const getTestSchema = z.object({
+  id: z.coerce.number(),
+  name: z.string(),
+  price: z.coerce.number(),
+  quantity: z.coerce.number(),
+  discount: z.coerce.number().nullable().optional(),
+  userId: z.coerce.number()
+});
+
+const listTestSchema = z.object({
+  id: z.coerce.number(),
+  name: z.string(),
+  price: z.coerce.number(),
+  quantity: z.coerce.number(),
+  discount: z.coerce.number().nullable().optional(),
+  userId: z.coerce.number()
+});
+
+const updateTestSchema = z.object({
+  name: z.string().min(1, '名称不能为空').optional(),
+  price: z.coerce.number().positive('价格必须为正数').optional(),
+  quantity: z.coerce.number().int().min(0, '数量必须为非负整数').optional(),
+  discount: z.coerce.number().min(0).max(1).optional()
+});
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooksWithEntities([TestNumberEntity])
+
+describe('共享CRUD Schema类型转换集成测试', () => {
+
+  let client: any;
+  let testToken: string;
+  const testUser = { id: 1, username: 'testuser' };
+
+  beforeEach(async () => {
+    // 创建测试路由
+    const app = createCrudRoutes({
+      entity: TestNumberEntity,
+      createSchema: createTestSchema,
+      updateSchema: updateTestSchema,
+      getSchema: getTestSchema,
+      listSchema: listTestSchema,
+      dataPermission: {
+        enabled: true,
+        userIdField: 'userId'
+      }
+    });
+
+    client = testClient(app);
+    testToken = JWTUtil.generateToken(testUser);
+  });
+
+  // setupIntegrationDatabaseHooksWithEntities 已经自动处理了数据库清理
+
+  describe('创建操作 - 数字类型转换', () => {
+    it('应该正确处理decimal和bigint字段的创建和返回', async () => {
+      // 使用 z.coerce.number() 后,schema验证应该成功
+      const createData = {
+        name: '测试商品',
+        price: 99.99,
+        quantity: 1000,
+        discount: 0.1,
+        userId: testUser.id
+      };
+
+      const response = await client['/'].$post({
+        json: createData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      console.debug('创建商品响应状态:', response.status);
+      expect(response.status).toBe(201);
+
+      const result = await response.json();
+      console.debug('创建商品返回结果:', result);
+
+      // 验证返回的数据类型
+      expect(typeof result.price).toBe('number');
+      expect(result.price).toBe(99.99);
+      expect(typeof result.quantity).toBe('number');
+      expect(result.quantity).toBe(1000);
+      expect(typeof result.discount).toBe('number');
+      expect(result.discount).toBe(0.1);
+    });
+
+    it('应该处理没有discount字段的创建', async () => {
+      const createData = {
+        name: '测试商品无折扣',
+        price: 50.00,
+        quantity: 500,
+        userId: testUser.id
+      };
+
+      const response = await client['/'].$post({
+        json: createData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(201);
+
+      const result = await response.json();
+
+      // 验证返回的数据类型
+      expect(typeof result.price).toBe('number');
+      expect(result.price).toBe(50.00);
+      expect(typeof result.quantity).toBe('number');
+      expect(result.quantity).toBe(500);
+      expect(result.discount).toBeNull(); // 没有discount字段应该返回null
+    });
+  });
+
+  describe('更新操作 - 数字类型转换', () => {
+    let createdId: number;
+
+    beforeEach(async () => {
+      // 先创建一个测试数据
+      const createData = {
+        name: '原始商品',
+        price: 100.00,
+        quantity: 100,
+        discount: 0.2,
+        userId: testUser.id
+      };
+
+      const response = await client['/'].$post({
+        json: createData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      const result = await response.json();
+      createdId = result.id;
+    });
+
+    it('应该正确处理decimal和bigint字段的更新和返回', async () => {
+      const updateData = {
+        name: '更新后的商品',
+        price: 88.88,
+        quantity: 888,
+        discount: 0.15
+      };
+
+      const response = await client['/:id'].$put({
+        param: { id: createdId },
+        json: updateData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      console.debug('更新商品响应状态:', response.status);
+      expect(response.status).toBe(200);
+
+      const result = await response.json();
+      console.debug('更新商品返回结果:', result);
+
+      // 验证返回的数据类型
+      expect(result.id).toBe(createdId);
+      expect(result.name).toBe(updateData.name);
+
+      // 关键验证:更新后的price字段应该是数字类型
+      expect(typeof result.price).toBe('number');
+      expect(result.price).toBe(88.88);
+
+      // 关键验证:更新后的quantity字段应该是数字类型
+      expect(typeof result.quantity).toBe('number');
+      expect(result.quantity).toBe(888);
+
+      // 关键验证:更新后的discount字段应该是数字类型
+      expect(typeof result.discount).toBe('number');
+      expect(result.discount).toBe(0.15);
+    });
+
+    it('应该处理部分字段的更新', async () => {
+      const updateData = {
+        price: 66.66
+      };
+
+      const response = await client['/:id'].$put({
+        param: { id: createdId },
+        json: updateData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+
+      const result = await response.json();
+
+      // 验证只有price字段被更新,其他字段保持不变
+      expect(result.name).toBe('原始商品');
+      expect(typeof result.price).toBe('number');
+      expect(result.price).toBe(66.66);
+      expect(typeof result.quantity).toBe('number');
+      expect(result.quantity).toBe(100); // 原始值
+      expect(typeof result.discount).toBe('number');
+      expect(result.discount).toBe(0.2); // 原始值
+    });
+  });
+
+  describe('获取操作 - 数字类型转换', () => {
+    let createdId: number;
+
+    beforeEach(async () => {
+      // 先创建一个测试数据
+      const createData = {
+        name: '查询测试商品',
+        price: 123.45,
+        quantity: 999,
+        discount: 0.05,
+        userId: testUser.id
+      };
+
+      const response = await client['/'].$post({
+        json: createData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      const result = await response.json();
+      createdId = result.id;
+    });
+
+    it('应该正确处理decimal和bigint字段的查询返回', async () => {
+      const response = await client['/:id'].$get({
+        param: { id: createdId }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      console.debug('获取商品响应状态:', response.status);
+      expect(response.status).toBe(200);
+
+      const result = await response.json();
+      console.debug('获取商品返回结果:', result);
+
+      // 验证返回的数据类型
+      expect(result.id).toBe(createdId);
+      expect(result.name).toBe('查询测试商品');
+
+      // 关键验证:price字段应该是数字类型
+      expect(typeof result.price).toBe('number');
+      expect(result.price).toBe(123.45);
+
+      // 关键验证:quantity字段应该是数字类型
+      expect(typeof result.quantity).toBe('number');
+      expect(result.quantity).toBe(999);
+
+      // 关键验证:discount字段应该是数字类型
+      expect(typeof result.discount).toBe('number');
+      expect(result.discount).toBe(0.05);
+    });
+  });
+
+  describe('列表查询 - 数字类型转换', () => {
+    beforeEach(async () => {
+      // 创建多个测试数据
+      const testData = [
+        { name: '商品1', price: 10.50, quantity: 100, discount: 0.1, userId: testUser.id },
+        { name: '商品2', price: 20.75, quantity: 200, discount: 0.2, userId: testUser.id },
+        { name: '商品3', price: 30.99, quantity: 300, userId: testUser.id } // 没有discount
+      ];
+
+      for (const data of testData) {
+        await client['/'].$post({
+          json: data
+        }, {
+          headers: {
+            'Authorization': `Bearer ${testToken}`
+          }
+        });
+      }
+    });
+
+    it('应该正确处理列表查询中的数字类型转换', async () => {
+      const response = await client['/'].$get({
+        query: { page: 1, pageSize: 10 }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      console.debug('列表查询响应状态:', response.status);
+      expect(response.status).toBe(200);
+
+      const result = await response.json();
+      console.debug('列表查询返回结果:', result);
+
+      expect(result).toHaveProperty('data');
+      expect(result).toHaveProperty('pagination');
+      expect(Array.isArray(result.data)).toBe(true);
+
+      // 验证每个商品的数字字段类型
+      result.data.forEach((item: any) => {
+        expect(typeof item.price).toBe('number');
+        expect(typeof item.quantity).toBe('number');
+
+        // discount字段可能为数字或null
+        if (item.discount !== null) {
+          expect(typeof item.discount).toBe('number');
+        }
+      });
+
+      // 验证具体数值
+      const item1 = result.data.find((item: any) => item.name === '商品1');
+      expect(item1).toBeDefined();
+      expect(item1.price).toBe(10.50);
+      expect(item1.quantity).toBe(100);
+      expect(item1.discount).toBe(0.1);
+
+      const item3 = result.data.find((item: any) => item.name === '商品3');
+      expect(item3).toBeDefined();
+      expect(item3.discount).toBeNull();
+    });
+  });
+});

+ 503 - 0
packages/shared-crud/tests/integration/tenant-isolation.integration.test.ts

@@ -0,0 +1,503 @@
+import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
+import { testClient } from 'hono/testing';
+import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities } from '@d8d/shared-test-util';
+import { JWTUtil } from '@d8d/shared-utils';
+import { z } from '@hono/zod-openapi';
+import { createCrudRoutes } from '../../src/routes/generic-crud.routes';
+import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
+
+// 测试实体类
+@Entity()
+class TestEntity {
+  @PrimaryGeneratedColumn()
+  id!: number;
+
+  @Column('varchar')
+  name!: string;
+
+  @Column('int')
+  tenantId!: number;
+}
+
+// 定义测试实体的Schema
+const createTestSchema = z.object({
+  name: z.string().min(1, '名称不能为空'),
+  tenantId: z.number().optional()
+});
+
+const updateTestSchema = z.object({
+  name: z.string().min(1, '名称不能为空').optional()
+});
+
+const getTestSchema = z.object({
+  id: z.number(),
+  name: z.string(),
+  tenantId: z.number()
+});
+
+const listTestSchema = z.object({
+  id: z.number(),
+  name: z.string(),
+  tenantId: z.number()
+});
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooksWithEntities([TestEntity])
+
+describe('共享CRUD租户隔离集成测试', () => {
+  let client: any;
+  let testToken1: string;
+  let testToken2: string;
+  let mockAuthMiddleware: any;
+
+  beforeEach(async () => {
+    // 获取数据源
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+
+    // 生成测试用户的token
+    testToken1 = JWTUtil.generateToken({
+      id: 1,
+      username: 'tenant1_user',
+      roles: [{name:'user'}],
+      tenantId: 1
+    });
+
+    testToken2 = JWTUtil.generateToken({
+      id: 2,
+      username: 'tenant2_user',
+      roles: [{name:'user'}],
+      tenantId: 2
+    });
+
+    // 创建模拟认证中间件
+    mockAuthMiddleware = async (c: any, next: any) => {
+      const authHeader = c.req.header('Authorization');
+      if (authHeader && authHeader.startsWith('Bearer ')) {
+        const token = authHeader.substring(7);
+        try {
+          const payload = JWTUtil.verifyToken(token);
+          // 根据token确定租户ID
+          let tenantId: number | undefined;
+          if (token === testToken1) {
+            tenantId = 1;
+          } else if (token === testToken2) {
+            tenantId = 2;
+          }
+
+          // 确保用户对象包含tenantId
+          const userWithTenant = { ...payload, tenantId };
+          c.set('user', userWithTenant);
+          // 设置租户上下文
+          c.set('tenantId', tenantId);
+        } catch (error) {
+          // token解析失败
+        }
+      } else {
+        // 没有认证信息,返回401
+        return c.json({ code: 401, message: '认证失败' }, 401);
+      }
+      await next();
+    };
+
+    // 创建测试路由 - 启用租户隔离
+    const testRoutes = createCrudRoutes({
+      entity: TestEntity,
+      createSchema: createTestSchema,
+      updateSchema: updateTestSchema,
+      getSchema: getTestSchema,
+      listSchema: listTestSchema,
+      middleware: [mockAuthMiddleware],
+      tenantOptions: {
+        enabled: true,
+        tenantIdField: 'tenantId',
+        autoExtractFromContext: true
+      }
+    });
+
+    client = testClient(testRoutes);
+  });
+
+  describe('GET / - 列表查询租户隔离', () => {
+    it('应该只返回当前租户的数据', async () => {
+      // 创建测试数据
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const testRepository = dataSource.getRepository(TestEntity);
+
+      // 为租户1创建数据
+      const tenant1Data1 = testRepository.create({
+        name: '租户1的数据1',
+        tenantId: 1
+      });
+      await testRepository.save(tenant1Data1);
+
+      const tenant1Data2 = testRepository.create({
+        name: '租户1的数据2',
+        tenantId: 1
+      });
+      await testRepository.save(tenant1Data2);
+
+      // 为租户2创建数据
+      const tenant2Data = testRepository.create({
+        name: '租户2的数据',
+        tenantId: 2
+      });
+      await testRepository.save(tenant2Data);
+
+      // 租户1用户查询列表
+      const response = await client.index.$get({
+        query: {
+          page: 1,
+          pageSize: 10
+        }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      console.debug('租户隔离列表查询响应状态:', response.status);
+
+      if (response.status !== 200) {
+        const errorData = await response.json();
+        console.debug('租户隔离列表查询错误信息:', errorData);
+      }
+
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data).toHaveProperty('data');
+        expect(Array.isArray(data.data)).toBe(true);
+        expect(data.data).toHaveLength(2); // 应该只返回租户1的2条数据
+
+        // 验证所有返回的数据都属于租户1
+        data.data.forEach((item: any) => {
+          expect(item.tenantId).toBe(1);
+        });
+      }
+    });
+
+    it('应该拒绝未认证用户的访问', async () => {
+      const response = await client.index.$get({
+        query: {
+          page: 1,
+          pageSize: 10
+        }
+      });
+      expect(response.status).toBe(401);
+    });
+  });
+
+  describe('POST / - 创建操作租户验证', () => {
+    it('应该成功创建属于当前租户的数据', async () => {
+      const createData = {
+        name: '测试创建数据'
+      };
+
+      const response = await client.index.$post({
+        json: createData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      console.debug('租户隔离创建数据响应状态:', response.status);
+
+      expect(response.status).toBe(201);
+
+      if (response.status === 201) {
+        const data = await response.json();
+        expect(data).toHaveProperty('id');
+        expect(data.name).toBe(createData.name);
+        expect(data.tenantId).toBe(1); // 应该自动设置为租户1
+      }
+    });
+  });
+
+  describe('GET /:id - 获取详情租户验证', () => {
+    it('应该成功获取属于当前租户的数据详情', async () => {
+      // 先创建测试数据
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const testRepository = dataSource.getRepository(TestEntity);
+      const testData = testRepository.create({
+        name: '测试数据详情',
+        tenantId: 1
+      });
+      await testRepository.save(testData);
+
+      const response = await client[':id'].$get({
+        param: { id: testData.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      console.debug('租户隔离获取详情响应状态:', response.status);
+
+      if (response.status !== 200) {
+        const errorData = await response.json();
+        console.debug('租户隔离获取详情错误信息:', errorData);
+      }
+
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data.id).toBe(testData.id);
+        expect(data.name).toBe(testData.name);
+        expect(data.tenantId).toBe(1);
+      }
+    });
+
+    it('应该拒绝获取不属于当前租户的数据详情', async () => {
+      // 先创建属于租户2的数据
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const testRepository = dataSource.getRepository(TestEntity);
+      const testData = testRepository.create({
+        name: '租户2的数据',
+        tenantId: 2
+      });
+      await testRepository.save(testData);
+
+      // 租户1用户尝试获取租户2的数据
+      const response = await client[':id'].$get({
+        param: { id: testData.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      console.debug('租户隔离获取无权详情响应状态:', response.status);
+      expect(response.status).toBe(404); // 应该返回404而不是403
+    });
+  });
+
+  describe('PUT /:id - 更新操作租户验证', () => {
+    it('应该成功更新属于当前租户的数据', async () => {
+      // 先创建测试数据
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const testRepository = dataSource.getRepository(TestEntity);
+      const testData = testRepository.create({
+        name: '原始数据',
+        tenantId: 1
+      });
+      await testRepository.save(testData);
+
+      const updateData = {
+        name: '更新后的数据'
+      };
+
+      const response = await client[':id'].$put({
+        param: { id: testData.id },
+        json: updateData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      console.debug('租户隔离更新数据响应状态:', response.status);
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data.name).toBe(updateData.name);
+        expect(data.tenantId).toBe(1);
+      }
+    });
+
+    it('应该拒绝更新不属于当前租户的数据', async () => {
+      // 先创建属于租户2的数据
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const testRepository = dataSource.getRepository(TestEntity);
+      const testData = testRepository.create({
+        name: '租户2的数据',
+        tenantId: 2
+      });
+      await testRepository.save(testData);
+
+      const updateData = {
+        name: '尝试更新的数据'
+      };
+
+      // 租户1用户尝试更新租户2的数据
+      const response = await client[':id'].$put({
+        param: { id: testData.id },
+        json: updateData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      console.debug('租户隔离更新无权数据响应状态:', response.status);
+      expect(response.status).toBe(404); // 应该返回404而不是403
+    });
+  });
+
+  describe('DELETE /:id - 删除操作租户验证', () => {
+    it('应该成功删除属于当前租户的数据', async () => {
+      // 先创建测试数据
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const testRepository = dataSource.getRepository(TestEntity);
+      const testData = testRepository.create({
+        name: '待删除数据',
+        tenantId: 1
+      });
+      await testRepository.save(testData);
+
+      const response = await client[':id'].$delete({
+        param: { id: testData.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      console.debug('租户隔离删除数据响应状态:', response.status);
+      expect(response.status).toBe(204);
+
+      // 验证数据确实被删除
+      const deletedData = await testRepository.findOne({
+        where: { id: testData.id }
+      });
+      expect(deletedData).toBeNull();
+    });
+
+    it('应该拒绝删除不属于当前租户的数据', async () => {
+      // 先创建属于租户2的数据
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const testRepository = dataSource.getRepository(TestEntity);
+      const testData = testRepository.create({
+        name: '租户2的数据',
+        tenantId: 2
+      });
+      await testRepository.save(testData);
+
+      // 租户1用户尝试删除租户2的数据
+      const response = await client[':id'].$delete({
+        param: { id: testData.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      console.debug('租户隔离删除无权数据响应状态:', response.status);
+      expect(response.status).toBe(404); // 应该返回404而不是403
+
+      // 验证数据没有被删除
+      const existingData = await testRepository.findOne({
+        where: { id: testData.id }
+      });
+      expect(existingData).not.toBeNull();
+    });
+  });
+
+  describe('禁用租户隔离的情况', () => {
+    it('当租户隔离禁用时应该允许跨租户访问', async () => {
+      // 创建禁用租户隔离的路由
+      const noTenantRoutes = createCrudRoutes({
+        entity: TestEntity,
+        createSchema: createTestSchema,
+        updateSchema: updateTestSchema,
+        getSchema: getTestSchema,
+        listSchema: listTestSchema,
+        middleware: [mockAuthMiddleware],
+        tenantOptions: {
+          enabled: false, // 禁用租户隔离
+          tenantIdField: 'tenantId',
+          autoExtractFromContext: true
+        }
+      });
+
+      const noTenantClient = testClient(noTenantRoutes);
+
+      // 创建属于租户2的数据
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const testRepository = dataSource.getRepository(TestEntity);
+      const testData = testRepository.create({
+        name: '租户2的数据',
+        tenantId: 2
+      });
+      await testRepository.save(testData);
+
+      // 租户1用户应该能够访问租户2的数据(租户隔离已禁用)
+      const response = await noTenantClient[':id'].$get({
+        param: { id: testData.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      console.debug('禁用租户隔离时的响应状态:', response.status);
+
+      if (response.status !== 200) {
+        try {
+          const errorData = await response.json();
+          console.debug('禁用租户隔离时的错误信息:', errorData);
+        } catch (e) {
+          const text = await response.text();
+          console.debug('禁用租户隔离时的响应文本:', text);
+        }
+      }
+
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data.id).toBe(testData.id);
+        expect(data.tenantId).toBe(2);
+      }
+    });
+
+    it('当不传递tenantOptions配置时应该允许跨租户访问', async () => {
+      // 创建不传递租户隔离配置的路由
+      const noTenantRoutes = createCrudRoutes({
+        entity: TestEntity,
+        createSchema: createTestSchema,
+        updateSchema: updateTestSchema,
+        getSchema: getTestSchema,
+        listSchema: listTestSchema,
+        middleware: [mockAuthMiddleware]
+        // 不传递 tenantOptions 配置
+      });
+
+      const noTenantClient = testClient(noTenantRoutes);
+
+      // 创建属于租户2的数据
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const testRepository = dataSource.getRepository(TestEntity);
+      const testData = testRepository.create({
+        name: '租户2的数据(无租户配置)',
+        tenantId: 2
+      });
+      await testRepository.save(testData);
+
+      // 租户1用户应该能够访问租户2的数据(没有租户隔离配置)
+      console.debug('测试数据ID(无租户配置):', testData.id);
+
+      const response = await noTenantClient[':id'].$get({
+        param: { id: testData.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken1}`
+        }
+      });
+
+      console.debug('无租户配置时的响应状态:', response.status);
+
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data.id).toBe(testData.id);
+        expect(data.tenantId).toBe(2);
+      }
+    });
+  });
+});

+ 7 - 1
packages/shared-crud/vitest.config.ts

@@ -9,6 +9,12 @@ export default defineConfig({
       provider: 'v8',
       reporter: ['text', 'json', 'html'],
       exclude: ['node_modules/', 'tests/']
-    }
+    },
+    // 关闭文件并行测试以避免数据库连接冲突
+    fileParallelism: false,
+    // 设置最大工作线程数为1,确保测试顺序执行
+    maxWorkers: 1,
+    // 设置最小工作线程数为1
+    minWorkers: 1
   }
 });

+ 3 - 0
packages/shared-test-util/src/index.ts

@@ -4,5 +4,8 @@ export * from './integration-test-db';
 // 导出集成测试断言工具
 export * from './integration-test-utils';
 
+// 导出测试数据工厂
+export * from './test-data-factory';
+
 // 导出类型定义
 export type { EntityTarget, ObjectLiteral } from 'typeorm';

+ 190 - 0
packages/shared-test-util/src/test-data-factory.ts

@@ -0,0 +1,190 @@
+import { IntegrationTestDatabase } from './integration-test-db';
+import { EntityTarget, ObjectLiteral } from 'typeorm';
+
+/**
+ * 测试数据工厂
+ * 用于创建多租户测试数据,减少重复代码
+ */
+export class TestDataFactory {
+  /**
+   * 创建测试用户
+   */
+  static async createTestUser(overrides: Partial<any> = {}): Promise<any> {
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+    const userRepository = dataSource.getRepository('UserEntityMt');
+
+    const defaultUser = {
+      username: `test_user_${Date.now()}`,
+      password: 'test_password',
+      nickname: '测试用户',
+      registrationSource: 'web',
+      tenantId: 1
+    };
+
+    const userData = { ...defaultUser, ...overrides };
+    const user = userRepository.create(userData);
+    return await userRepository.save(user);
+  }
+
+  /**
+   * 创建测试地区数据
+   */
+  static async createTestArea(overrides: Partial<any> = {}): Promise<any> {
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+    const areaRepository = dataSource.getRepository('AreaEntityMt');
+
+    const defaultArea = {
+      name: '测试地区',
+      code: '110000',
+      level: 1,
+      parentId: null,
+      tenantId: 1
+    };
+
+    const areaData = { ...defaultArea, ...overrides };
+    const area = areaRepository.create(areaData);
+    return await areaRepository.save(area);
+  }
+
+  /**
+   * 创建测试省份数据
+   */
+  static async createTestProvince(overrides: Partial<any> = {}): Promise<any> {
+    return await this.createTestArea({
+      name: '北京市',
+      code: '110000',
+      level: 1, // PROVINCE
+      parentId: null,
+      ...overrides
+    });
+  }
+
+  /**
+   * 创建测试城市数据
+   */
+  static async createTestCity(provinceId: number, overrides: Partial<any> = {}): Promise<any> {
+    return await this.createTestArea({
+      name: '北京市',
+      code: '110100',
+      level: 2, // CITY
+      parentId: provinceId,
+      ...overrides
+    });
+  }
+
+  /**
+   * 创建测试区县数据
+   */
+  static async createTestDistrict(cityId: number, overrides: Partial<any> = {}): Promise<any> {
+    return await this.createTestArea({
+      name: '朝阳区',
+      code: '110105',
+      level: 3, // DISTRICT
+      parentId: cityId,
+      ...overrides
+    });
+  }
+
+  /**
+   * 创建测试配送地址数据
+   */
+  static async createTestDeliveryAddress(
+    userId: number,
+    provinceId: number,
+    cityId: number,
+    districtId: number,
+    overrides: Partial<any> = {}
+  ): Promise<any> {
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+    const deliveryAddressRepository = dataSource.getRepository('DeliveryAddressMt');
+
+    const defaultAddress = {
+      userId,
+      name: '测试收货人',
+      phone: '13800138000',
+      address: '测试详细地址',
+      receiverProvince: provinceId,
+      receiverCity: cityId,
+      receiverDistrict: districtId,
+      receiverTown: 1,
+      state: 1,
+      isDefault: 0,
+      createdBy: userId,
+      tenantId: 1
+    };
+
+    const addressData = { ...defaultAddress, ...overrides };
+    const address = deliveryAddressRepository.create(addressData);
+    return await deliveryAddressRepository.save(address);
+  }
+
+  /**
+   * 创建完整的测试数据集合
+   */
+  static async createTestDataSet(tenantId: number = 1): Promise<{
+    user: any;
+    otherUser: any;
+    province: any;
+    city: any;
+    district: any;
+  }> {
+    // 创建测试用户
+    const user = await this.createTestUser({ tenantId });
+    const otherUser = await this.createTestUser({
+      username: `other_user_${Date.now()}`,
+      nickname: '其他用户',
+      tenantId
+    });
+
+    // 创建测试地区数据
+    const province = await this.createTestProvince({ tenantId });
+    const city = await this.createTestCity(province.id, { tenantId });
+    const district = await this.createTestDistrict(city.id, { tenantId });
+
+    return {
+      user,
+      otherUser,
+      province,
+      city,
+      district
+    };
+  }
+
+  /**
+   * 通用实体创建方法
+   */
+  static async createEntity<T extends ObjectLiteral>(
+    entity: EntityTarget<T>,
+    data: Partial<T>
+  ): Promise<T> {
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+    const repository = dataSource.getRepository(entity);
+    const entityInstance = repository.create(data as T);
+    return await repository.save(entityInstance);
+  }
+
+  /**
+   * 批量创建实体
+   */
+  static async createEntities<T extends ObjectLiteral>(
+    entity: EntityTarget<T>,
+    dataArray: Partial<T>[]
+  ): Promise<T[]> {
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+    const repository = dataSource.getRepository(entity);
+    const entities = dataArray.map(data => repository.create(data as T));
+    return await repository.save(entities);
+  }
+
+  /**
+   * 清理测试数据
+   */
+  static async cleanupTestData(entities: EntityTarget<any>[]): Promise<void> {
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+
+    for (const entity of entities) {
+      const repository = dataSource.getRepository(entity);
+      await repository.clear();
+    }
+  }
+}

+ 2 - 0
packages/shared-types/src/index.ts

@@ -86,6 +86,7 @@ export interface JWTPayload {
   username: string;
   roles?: string[];
   openid?: string;
+  tenantId?: number; // 租户ID,用于多租户场景
 }
 
 // Hono 认证上下文类型
@@ -93,5 +94,6 @@ export type AuthContext = {
   Variables: {
     user: any; // 用户类型将在具体模块中定义
     token: string;
+    tenantId?: number; // 租户ID,用于多租户场景
   }
 };

+ 2 - 1
packages/shared-utils/src/utils/jwt.util.ts

@@ -18,7 +18,7 @@ export class JWTUtil {
    * @param expiresIn 过期时间
    * @returns JWT token
    */
-  static generateToken(user: { id: number; username: string; roles?: { name: string }[]; openid?: string }, additionalPayload: Partial<JWTPayload> = {}, expiresIn?: string): string {
+  static generateToken(user: { id: number; username: string; roles?: { name: string }[]; openid?: string; tenantId?: number }, additionalPayload: Partial<JWTPayload> = {}, expiresIn?: string): string {
     if (!user.id || !user.username) {
       throw new Error('用户ID和用户名不能为空');
     }
@@ -28,6 +28,7 @@ export class JWTUtil {
       username: user.username,
       roles: user.roles?.map(role => role.name) || [],
       openid: user.openid || undefined,
+      tenantId: user.tenantId || undefined,
       ...additionalPayload
     };
 

+ 94 - 0
packages/shared-utils/src/utils/redis.util.ts

@@ -59,6 +59,100 @@ class RedisUtil {
     const sessionKey = await this.getSessionKey(userId);
     return !!sessionKey;
   }
+
+  /**
+   * 设置系统配置缓存
+   */
+  async setSystemConfig(tenantId: number, configKey: string, configValue: string, ttlSeconds: number = 3600): Promise<void> {
+    const client = await this.connect();
+    const key = `system_config:${tenantId}:${configKey}`;
+    await client.set(key, configValue, {
+      EX: ttlSeconds // 默认1小时过期
+    });
+  }
+
+  /**
+   * 获取系统配置缓存
+   */
+  async getSystemConfig(tenantId: number, configKey: string): Promise<string | null> {
+    const client = await this.connect();
+    const key = `system_config:${tenantId}:${configKey}`;
+    return await client.get(key);
+  }
+
+  /**
+   * 删除系统配置缓存
+   */
+  async deleteSystemConfig(tenantId: number, configKey: string): Promise<void> {
+    const client = await this.connect();
+    const key = `system_config:${tenantId}:${configKey}`;
+    await client.del(key);
+  }
+
+  /**
+   * 批量获取系统配置缓存
+   */
+  async getSystemConfigs(tenantId: number, configKeys: string[]): Promise<Record<string, string | null>> {
+    const client = await this.connect();
+    const keys = configKeys.map(key => `system_config:${tenantId}:${key}`);
+    const values = await client.mGet(keys);
+
+    const result: Record<string, string | null> = {};
+    configKeys.forEach((key, index) => {
+      result[key] = values[index];
+    });
+
+    return result;
+  }
+
+  /**
+   * 设置空值缓存(防止缓存穿透)
+   */
+  async setNullSystemConfig(tenantId: number, configKey: string, ttlSeconds: number = 300): Promise<void> {
+    const client = await this.connect();
+    const key = `system_config:${tenantId}:${configKey}`;
+    await client.set(key, '__NULL__', {
+      EX: ttlSeconds // 默认5分钟过期
+    });
+  }
+
+  /**
+   * 检查是否为空值缓存
+   */
+  isNullValue(value: string | null): boolean {
+    return value === '__NULL__';
+  }
+
+  /**
+   * 清除租户的所有系统配置缓存
+   */
+  async clearTenantSystemConfigs(tenantId: number): Promise<void> {
+    const client = await this.connect();
+    const pattern = `system_config:${tenantId}:*`;
+
+    // 使用SCAN命令遍历匹配的键并删除
+    let cursor = 0;
+    do {
+      const result = await client.scan(cursor, {
+        MATCH: pattern,
+        COUNT: 100
+      });
+
+      cursor = result.cursor;
+      const keys = result.keys;
+
+      if (keys.length > 0) {
+        await client.del(keys);
+      }
+    } while (cursor !== 0);
+  }
+
+  /**
+   * 格式化系统配置缓存键
+   */
+  formatSystemConfigKey(tenantId: number, configKey: string): string {
+    return `system_config:${tenantId}:${configKey}`;
+  }
 }
 
 export const redisUtil = RedisUtil.getInstance();

+ 1 - 1
packages/shared-utils/tests/unit/data-source.test.ts

@@ -36,7 +36,7 @@ describe('data-source', () => {
 
       expect(dataSource).toBeDefined();
       expect(dataSource.options.type).toBe('postgres');
-      expect(dataSource.options.url).toBe('postgresql://test:test@localhost:5432/test');
+      expect((dataSource.options as any).url).toBe('postgresql://test:test@localhost:5432/test');
       expect(dataSource.options.synchronize).toBe(true);
       expect(dataSource.options.dropSchema).toBe(true);
       expect(dataSource.options.logging).toBe(false);

+ 1 - 1
packages/shared-utils/tests/unit/jwt.util.test.ts

@@ -25,7 +25,7 @@ describe('JWTUtil', () => {
     });
 
     it('应该使用额外的payload数据', () => {
-      const additionalPayload = { customField: 'customValue' };
+      const additionalPayload = { customField: 'customValue' } as any;
       const token = JWTUtil.generateToken(mockUser, additionalPayload);
 
       const decoded = JWTUtil.decodeToken(token);

+ 4 - 2
packages/user-module/src/entities/user.entity.ts

@@ -29,7 +29,7 @@ export class UserEntity {
   @Column({ name: 'avatar_file_id', type: 'int', unsigned: true, nullable: true, comment: '头像文件ID' })
   avatarFileId!: number | null;
 
-  @ManyToOne(() => File, { nullable: true })
+  @ManyToOne('File', { nullable: true })
   @JoinColumn({ name: 'avatar_file_id', referencedColumnName: 'id' })
   avatarFile!: File | null;
 
@@ -61,4 +61,6 @@ export class UserEntity {
   constructor(partial?: Partial<UserEntity>) {
     Object.assign(this, partial);
   }
-}
+}
+
+export { UserEntity as User };

+ 1 - 1
packages/user-module/src/routes/user.routes.ts

@@ -13,7 +13,7 @@ const userCrudRoutes = createCrudRoutes({
   getSchema: UserSchema,
   listSchema: UserSchema,
   searchFields: ['username', 'nickname', 'phone', 'email'],
-  relations: ['roles'],
+  relations: ['roles', 'avatarFile'],
   middleware: [authMiddleware],
   readOnly: true // 创建/更新/删除使用自定义路由
 });

+ 1 - 1
packages/user-module/src/services/role.service.ts

@@ -12,7 +12,7 @@ export class RoleService extends GenericCrudService<Role> {
     return this.repository.findOneBy({ name });
   }
 
-  async checkPermission(roleId: number, permission: string): Promise<boolean> {
+  async hasPermission(roleId: number, permission: string): Promise<boolean> {
     const role = await this.getById(roleId);
     if (!role) return false;
     return role.permissions.includes(permission);

+ 5 - 5
packages/user-module/tests/integration/role.integration.test.ts

@@ -158,16 +158,16 @@ describe('Role Integration Tests', () => {
       const role = await roleService.create(roleData);
 
       // Check existing permissions
-      expect(await roleService.checkPermission(role.id, 'user:create')).toBe(true);
-      expect(await roleService.checkPermission(role.id, 'user:read')).toBe(true);
-      expect(await roleService.checkPermission(role.id, 'user:update')).toBe(true);
+      expect(await roleService.hasPermission(role.id, 'user:create')).toBe(true);
+      expect(await roleService.hasPermission(role.id, 'user:read')).toBe(true);
+      expect(await roleService.hasPermission(role.id, 'user:update')).toBe(true);
 
       // Check non-existing permission
-      expect(await roleService.checkPermission(role.id, 'user:delete')).toBe(false);
+      expect(await roleService.hasPermission(role.id, 'user:delete')).toBe(false);
     });
 
     it('should return false for non-existent role', async () => {
-      const hasPermission = await roleService.checkPermission(999, 'user:create');
+      const hasPermission = await roleService.hasPermission(999, 'user:create');
       expect(hasPermission).toBe(false);
     });
   });

+ 2 - 1
packages/user-module/tests/integration/user.integration.test.ts

@@ -5,6 +5,7 @@ import { RoleService } from '../../src/services/role.service';
 import { UserEntity } from '../../src/entities/user.entity';
 import { Role } from '../../src/entities/role.entity';
 import { AppDataSource, initializeDataSource } from '@d8d/shared-utils';
+import { File } from '@d8d/file-module';
 
 // 确保测试环境变量被设置
 process.env.NODE_ENV = 'test';
@@ -16,7 +17,7 @@ describe('User Integration Tests', () => {
 
   beforeAll(() => {
     // 使用预先配置的数据源
-    initializeDataSource([UserEntity, Role])
+    initializeDataSource([UserEntity, Role, File])
     dataSource = AppDataSource;
   })
 

+ 6 - 1
packages/user-module/tests/integration/user.routes.integration.test.ts

@@ -13,9 +13,10 @@ import { Role } from '../../src/entities/role.entity';
 import { TestDataFactory } from '../utils/integration-test-db';
 import { AuthService } from '@d8d/auth-module';
 import { UserService } from '../../src/services/user.service';
+import { File } from '@d8d/file-module';
 
 // 设置集成测试钩子
-setupIntegrationDatabaseHooksWithEntities([UserEntity, Role])
+setupIntegrationDatabaseHooksWithEntities([UserEntity, Role, File])
 
 describe('用户路由API集成测试 (使用hono/testing)', () => {
   let client: ReturnType<typeof testClient<typeof userRoutes>>;
@@ -199,6 +200,10 @@ describe('用户路由API集成测试 (使用hono/testing)', () => {
         }
       });
 
+      if (response.status !== 200) {
+        const errorData = await response.json();
+        console.debug('获取用户列表失败:', errorData);
+      }
       expect(response.status).toBe(200);
       if (response.status === 200) {
         const responseData = await response.json();

+ 4 - 4
packages/user-module/tests/unit/role.service.test.ts

@@ -52,7 +52,7 @@ describe('RoleService', () => {
     });
   });
 
-  describe('checkPermission', () => {
+  describe('hasPermission', () => {
     it('should return true when role has permission', async () => {
       const mockRole = {
         id: 1,
@@ -62,7 +62,7 @@ describe('RoleService', () => {
 
       vi.mocked(mockRepository.findOne).mockResolvedValue(mockRole);
 
-      const result = await roleService.checkPermission(1, 'user:create');
+      const result = await roleService.hasPermission(1, 'user:create');
 
       expect(result).toBe(true);
     });
@@ -76,7 +76,7 @@ describe('RoleService', () => {
 
       vi.mocked(mockRepository.findOne).mockResolvedValue(mockRole);
 
-      const result = await roleService.checkPermission(1, 'user:update');
+      const result = await roleService.hasPermission(1, 'user:update');
 
       expect(result).toBe(false);
     });
@@ -84,7 +84,7 @@ describe('RoleService', () => {
     it('should return false when role not found', async () => {
       vi.mocked(mockRepository.findOne).mockResolvedValue(null);
 
-      const result = await roleService.checkPermission(999, 'user:create');
+      const result = await roleService.hasPermission(999, 'user:create');
 
       expect(result).toBe(false);
     });

+ 4 - 4
packages/user-module/tests/unit/user.service.test.ts

@@ -112,7 +112,7 @@ describe('UserService', () => {
 
       expect(mockUserRepository.findOne).toHaveBeenCalledWith({
         where: { id: 1 },
-        relations: ['roles']
+        relations: ['roles', 'avatarFile']
       });
       expect(result).toEqual(mockUser);
     });
@@ -150,7 +150,7 @@ describe('UserService', () => {
 
       expect(mockUserRepository.findOne).toHaveBeenCalledWith({
         where: { username: 'testuser' },
-        relations: ['roles']
+        relations: ['roles', 'avatarFile']
       });
       expect(result).toEqual(mockUser);
     });
@@ -264,7 +264,7 @@ describe('UserService', () => {
       const result = await userService.getUsers();
 
       expect(mockUserRepository.find).toHaveBeenCalledWith({
-        relations: ['roles']
+        relations: ['roles', 'avatarFile']
       });
       expect(result).toEqual(mockUsers);
     });
@@ -285,7 +285,7 @@ describe('UserService', () => {
 
       expect(mockUserRepository.findOne).toHaveBeenCalledWith({
         where: [{ username: 'testuser' }, { email: 'testuser' }],
-        relations: ['roles']
+        relations: ['roles', 'avatarFile']
       });
       expect(result).toEqual(mockUser);
     });