Parcourir la source

✨ feat(crud): 增强通用CRUD功能

- 添加用户跟踪功能,支持自动记录创建者和更新者信息
- 增加自定义筛选参数支持,支持精确匹配、范围查询和IN查询等
- 优化关联查询,支持嵌套关联关系(如['contract.client'])
- 为列表和详情接口添加relations参数,支持关联数据查询

♻️ refactor(crud): 优化代码结构与类型定义

- 重构getList方法参数顺序,使接口更清晰
- 改进排序处理逻辑,移除类型断言
- 优化错误处理,移除重复的Zod错误检查代码
- 完善类型定义,添加UserTrackingOptions接口定义
yourname il y a 4 mois
Parent
commit
c228c247da
2 fichiers modifiés avec 149 ajouts et 50 suppressions
  1. 37 26
      src/server/utils/generic-crud.routes.ts
  2. 112 24
      src/server/utils/generic-crud.service.ts

+ 37 - 26
src/server/utils/generic-crud.routes.ts

@@ -13,13 +13,13 @@ export function createCrudRoutes<
   GetSchema extends z.ZodSchema = z.ZodSchema,
   ListSchema extends z.ZodSchema = z.ZodSchema
 >(options: CrudOptions<T, CreateSchema, UpdateSchema, GetSchema, ListSchema>) {
-  const { entity, createSchema, updateSchema, getSchema, listSchema, searchFields, middleware = [] } = options;
+  const { entity, createSchema, updateSchema, getSchema, listSchema, searchFields, relations, middleware = [], userTracking } = options;
   
   // 创建CRUD服务实例
   // 抽象类不能直接实例化,需要创建具体实现类
   class ConcreteCrudService extends GenericCrudService<T> {
     constructor() {
-      super(AppDataSource, entity);
+      super(AppDataSource, entity, { userTracking });
     }
   }
   const crudService = new ConcreteCrudService();
@@ -53,6 +53,11 @@ export function createCrudRoutes<
         sortOrder: z.enum(['ASC', 'DESC']).optional().default('DESC').openapi({
           example: 'DESC',
           description: '排序方向'
+        }),
+        // 增强的筛选参数
+        filters: z.string().optional().openapi({
+          example: '{"status": 1, "createdAt": {"gte": "2024-01-01", "lte": "2024-12-31"}}',
+          description: '筛选条件(JSON字符串),支持精确匹配、范围查询、IN查询等'
         })
       })
     },
@@ -215,16 +220,25 @@ export function createCrudRoutes<
   const routes = app
     .openapi(listRoute, async (c) => {
       try {
-        const { page, pageSize, keyword, sortBy, sortOrder } = c.req.valid('query');
+        const query = c.req.valid('query') as any;
+        const { page, pageSize, keyword, sortBy, sortOrder, filters } = query;
         
         // 构建排序对象
-        // 使用Record和类型断言解决泛型索引写入问题
-        const order: Partial<Record<keyof T, 'ASC' | 'DESC'>> = {};
+        const order: any = {};
         if (sortBy) {
-          (order as Record<string, 'ASC' | 'DESC'>)[sortBy] = sortOrder || 'DESC';
+          order[sortBy] = sortOrder || 'DESC';
         } else {
-          // 默认按id降序排序
-          (order as Record<string, 'ASC' | 'DESC'>)['id'] = 'DESC';
+          order['id'] = 'DESC';
+        }
+        
+        // 解析筛选条件
+        let parsedFilters: any = undefined;
+        if (filters) {
+          try {
+            parsedFilters = JSON.parse(filters);
+          } catch (e) {
+            return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
+          }
         }
         
         const [data, total] = await crudService.getList(
@@ -232,13 +246,14 @@ export function createCrudRoutes<
           pageSize,
           keyword,
           searchFields,
-          undefined, // where条件
-          [], // relations
-          order
+          undefined,
+          relations || [],
+          order,
+          parsedFilters
         );
         
         return c.json({
-          data: data as any[],
+          data,
           pagination: { total, current: page, pageSize }
         }, 200);
       } catch (error) {
@@ -251,10 +266,11 @@ export function createCrudRoutes<
         }, 500);
       }
     })
-    .openapi(createRouteDef, async (c) => {
+    .openapi(createRouteDef, async (c: any) => {
       try {
         const data = c.req.valid('json');
-        const result = await crudService.create(data);
+        const user = c.get('user');
+        const result = await crudService.create(data, user?.id);
         return c.json(result, 201);
       } catch (error) {
         if (error instanceof z.ZodError) {
@@ -266,10 +282,10 @@ export function createCrudRoutes<
         }, 500);
       }
     })
-    .openapi(getRouteDef, async (c) => {
+    .openapi(getRouteDef, async (c: any) => {
       try {
         const { id } = c.req.valid('param');
-        const result = await crudService.getById(id);
+        const result = await crudService.getById(id, relations || []);
         
         if (!result) {
           return c.json({ code: 404, message: '资源不存在' }, 404);
@@ -286,11 +302,12 @@ export function createCrudRoutes<
         }, 500);
       }
     })
-    .openapi(updateRouteDef, async (c) => {
+    .openapi(updateRouteDef, async (c: any) => {
       try {
         const { id } = c.req.valid('param');
         const data = c.req.valid('json');
-        const result = await crudService.update(id, data);
+        const user = c.get('user');
+        const result = await crudService.update(id, data, user?.id);
         
         if (!result) {
           return c.json({ code: 404, message: '资源不存在' }, 404);
@@ -298,9 +315,6 @@ export function createCrudRoutes<
         
         return c.json(result, 200);
       } catch (error) {
-        if (error instanceof z.ZodError) {
-          return c.json({ code: 400, message: '参数验证失败', errors: error.errors }, 400);
-        }
         if (error instanceof z.ZodError) {
           return c.json({ code: 400, message: '参数验证失败', errors: error.errors }, 400);
         }
@@ -310,7 +324,7 @@ export function createCrudRoutes<
         }, 500);
       }
     })
-    .openapi(deleteRouteDef, async (c) => {
+    .openapi(deleteRouteDef, async (c: any) => {
       try {
         const { id } = c.req.valid('param');
         const success = await crudService.delete(id);
@@ -319,11 +333,8 @@ export function createCrudRoutes<
           return c.json({ code: 404, message: '资源不存在' }, 404);
         }
         
-        return c.body(null, 204) as unknown as Response;
+        return c.body(null, 204);
       } catch (error) {
-        if (error instanceof z.ZodError) {
-          return c.json({ code: 400, message: '参数验证失败', errors: error.errors }, 400);
-        }
         if (error instanceof z.ZodError) {
           return c.json({ code: 400, message: '参数验证失败', errors: error.errors }, 400);
         }

+ 112 - 24
src/server/utils/generic-crud.service.ts

@@ -3,20 +3,22 @@ import { z } from '@hono/zod-openapi';
 
 export abstract class GenericCrudService<T extends ObjectLiteral> {
   protected repository: Repository<T>;
-  
+  private userTrackingOptions?: UserTrackingOptions;
+
   constructor(
     protected dataSource: DataSource,
-    protected entity: new () => T
+    protected entity: new () => T,
+    options?: {
+      userTracking?: UserTrackingOptions;
+    }
   ) {
     this.repository = this.dataSource.getRepository(entity);
+    this.userTrackingOptions = options?.userTracking;
   }
 
   /**
    * 获取分页列表
    */
-  /**
-   * 获取分页列表,支持高级查询
-   */
   async getList(
     page: number = 1,
     pageSize: number = 10,
@@ -24,25 +26,35 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
     searchFields?: string[],
     where?: Partial<T>,
     relations: string[] = [],
-    order: { [P in keyof T]?: 'ASC' | 'DESC' } = {}
+    order: { [P in keyof T]?: 'ASC' | 'DESC' } = {},
+    filters?: {
+      [key: string]: any;
+    }
   ): Promise<[T[], number]> {
     const skip = (page - 1) * pageSize;
     const query = this.repository.createQueryBuilder('entity');
-    
-    // 关联查询
+
+    // 添加关联关系(支持嵌套关联,如 ['contract.client'])
     if (relations.length > 0) {
-      relations.forEach(relation => {
-        query.leftJoinAndSelect(`entity.${relation}`, relation);
+      relations.forEach((relation, relationIndex) => {
+        const parts = relation.split('.');
+        let currentAlias = 'entity';
+        
+        parts.forEach((part, index) => {
+          const newAlias = index === 0 ? part : `${currentAlias}_${relationIndex}`;
+          query.leftJoinAndSelect(`${currentAlias}.${part}`, newAlias);
+          currentAlias = newAlias;
+        });
       });
     }
-    
+
     // 关键词搜索
     if (keyword && searchFields && searchFields.length > 0) {
       query.andWhere(searchFields.map(field => `entity.${field} LIKE :keyword`).join(' OR '), {
         keyword: `%${keyword}%`
       });
     }
-    
+
     // 条件查询
     if (where) {
       Object.entries(where).forEach(([key, value]) => {
@@ -51,42 +63,104 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
         }
       });
     }
-    
+
+    // 扩展筛选条件
+    if (filters) {
+      Object.entries(filters).forEach(([key, value]) => {
+        if (value !== undefined && value !== null && value !== '') {
+          const fieldName = key.startsWith('_') ? key.substring(1) : key;
+          
+          // 支持不同类型的筛选
+          if (Array.isArray(value)) {
+            // 数组类型:IN查询
+            if (value.length > 0) {
+              query.andWhere(`entity.${fieldName} IN (:...${key})`, { [key]: value });
+            }
+          } else if (typeof value === 'string' && value.includes('%')) {
+            // 模糊匹配
+            query.andWhere(`entity.${fieldName} LIKE :${key}`, { [key]: value });
+          } else if (typeof value === 'object' && value !== null) {
+            // 范围查询
+            if ('gte' in value) {
+              query.andWhere(`entity.${fieldName} >= :${key}_gte`, { [`${key}_gte`]: value.gte });
+            }
+            if ('gt' in value) {
+              query.andWhere(`entity.${fieldName} > :${key}_gt`, { [`${key}_gt`]: value.gt });
+            }
+            if ('lte' in value) {
+              query.andWhere(`entity.${fieldName} <= :${key}_lte`, { [`${key}_lte`]: value.lte });
+            }
+            if ('lt' in value) {
+              query.andWhere(`entity.${fieldName} < :${key}_lt`, { [`${key}_lt`]: value.lt });
+            }
+            if ('between' in value && Array.isArray(value.between) && value.between.length === 2) {
+              query.andWhere(`entity.${fieldName} BETWEEN :${key}_start AND :${key}_end`, {
+                [`${key}_start`]: value.between[0],
+                [`${key}_end`]: value.between[1]
+              });
+            }
+          } else {
+            // 精确匹配
+            query.andWhere(`entity.${fieldName} = :${key}`, { [key]: value });
+          }
+        }
+      });
+    }
+
     // 排序
     Object.entries(order).forEach(([key, direction]) => {
       query.orderBy(`entity.${key}`, direction);
     });
-    
+
     return query.skip(skip).take(pageSize).getManyAndCount();
   }
 
   /**
-   * 高级查询方法
+   * 根据ID获取单个实体
    */
-  createQueryBuilder(alias: string = 'entity') {
-    return this.repository.createQueryBuilder(alias);
+  async getById(id: number, relations: string[] = []): Promise<T | null> {
+    return this.repository.findOne({
+      where: { id } as any,
+      relations
+    });
   }
 
   /**
-   * 根据ID获取单个实体
+   * 设置用户跟踪字段
    */
-  async getById(id: number): Promise<T | null> {
-    return this.repository.findOneBy({ id } as any);
+  private setUserFields(data: any, userId?: string | number, isCreate: boolean = true): void {
+    if (!this.userTrackingOptions || !userId) {
+      return;
+    }
+
+    const { createdByField = 'createdBy', updatedByField = 'updatedBy' } = this.userTrackingOptions;
+
+    if (isCreate && createdByField) {
+      data[createdByField] = userId;
+    }
+
+    if (updatedByField) {
+      data[updatedByField] = userId;
+    }
   }
 
   /**
    * 创建实体
    */
-  async create(data: DeepPartial<T>): Promise<T> {
-    const entity = this.repository.create(data as DeepPartial<T>);
+  async create(data: DeepPartial<T>, userId?: string | number): Promise<T> {
+    const entityData = { ...data };
+    this.setUserFields(entityData, userId, true);
+    const entity = this.repository.create(entityData as DeepPartial<T>);
     return this.repository.save(entity);
   }
 
   /**
    * 更新实体
    */
-  async update(id: number, data: Partial<T>): Promise<T | null> {
-    await this.repository.update(id, data);
+  async update(id: number, data: Partial<T>, userId?: string | number): Promise<T | null> {
+    const updateData = { ...data };
+    this.setUserFields(updateData, userId, false);
+    await this.repository.update(id, updateData);
     return this.getById(id);
   }
 
@@ -97,6 +171,18 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
     const result = await this.repository.delete(id);
     return result.affected === 1;
   }
+
+  /**
+   * 高级查询方法
+   */
+  createQueryBuilder(alias: string = 'entity') {
+    return this.repository.createQueryBuilder(alias);
+  }
+}
+
+export interface UserTrackingOptions {
+  createdByField?: string;
+  updatedByField?: string;
 }
 
 export type CrudOptions<
@@ -112,5 +198,7 @@ export type CrudOptions<
   getSchema: GetSchema;
   listSchema: ListSchema;
   searchFields?: string[];
+  relations?: string[];
   middleware?: any[];
+  userTracking?: UserTrackingOptions;
 };