Parcourir la source

✨ feat(shared-crud): 创建通用CRUD工具包

- 初始化共享CRUD工具包项目结构
- 实现GenericCrudService基础服务类,支持分页、搜索、筛选等功能
- 开发ConcreteCrudService具体服务实现
- 创建通用CRUD路由生成器createCrudRoutes
- 配置package.json及TypeScript项目设置
- 添加单元测试配置文件vitest.config.ts
yourname il y a 4 semaines
Parent
commit
b4999bee6f

+ 49 - 0
packages/shared-crud/package.json

@@ -0,0 +1,49 @@
+{
+  "name": "@d8d/shared-crud",
+  "version": "1.0.0",
+  "type": "module",
+  "description": "D8D Shared CRUD Utilities",
+  "main": "src/index.ts",
+  "types": "src/index.ts",
+  "exports": {
+    ".": {
+      "import": "./src/index.ts",
+      "require": "./src/index.ts",
+      "types": "./src/index.ts"
+    },
+    "./services": {
+      "import": "./src/services/index.ts",
+      "require": "./src/services/index.ts",
+      "types": "./src/services/index.ts"
+    },
+    "./routes": {
+      "import": "./src/routes/index.ts",
+      "require": "./src/routes/index.ts",
+      "types": "./src/routes/index.ts"
+    }
+  },
+  "scripts": {
+    "build": "tsc",
+    "dev": "tsc --watch",
+    "typecheck": "tsc --noEmit",
+    "test": "vitest",
+    "test:unit": "vitest run tests/unit",
+    "test:coverage": "vitest --coverage",
+    "test:typecheck": "tsc --noEmit"
+  },
+  "dependencies": {
+    "@d8d/shared-types": "workspace:*",
+    "@d8d/shared-utils": "workspace:*",
+    "typeorm": "^0.3.20",
+    "@hono/zod-openapi": "1.0.2",
+    "zod": "^4.1.12"
+  },
+  "devDependencies": {
+    "typescript": "^5.8.3",
+    "vitest": "^3.2.4",
+    "@vitest/coverage-v8": "^3.2.4"
+  },
+  "files": [
+    "src"
+  ]
+}

+ 5 - 0
packages/shared-crud/src/index.ts

@@ -0,0 +1,5 @@
+// 导出服务模块
+export * from './services';
+
+// 导出路由模块
+export * from './routes';

+ 463 - 0
packages/shared-crud/src/routes/generic-crud.routes.ts

@@ -0,0 +1,463 @@
+import { createRoute, OpenAPIHono, extendZodWithOpenApi } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { CrudOptions } from '../services/generic-crud.service';
+import { ErrorSchema } from '@d8d/shared-utils';
+import { AuthContext } from '@d8d/shared-types';
+import { ObjectLiteral } from 'typeorm';
+import { parseWithAwait } from '@d8d/shared-utils';
+import { ConcreteCrudService } from '../services/concrete-crud.service';
+
+extendZodWithOpenApi(z)
+
+export function createCrudRoutes<
+  T extends ObjectLiteral,
+  CreateSchema extends z.ZodSchema = z.ZodSchema,
+  UpdateSchema extends z.ZodSchema = z.ZodSchema,
+  GetSchema extends z.ZodSchema = z.ZodSchema,
+  ListSchema extends z.ZodSchema = z.ZodSchema
+>(
+  dataSource: DataSource,
+  options: CrudOptions<T, CreateSchema, UpdateSchema, GetSchema, ListSchema>
+) {
+  const { entity, createSchema, updateSchema, getSchema, listSchema, searchFields, relations, middleware = [], userTracking, relationFields, readOnly = false } = options;
+
+  // 创建路由实例
+  const app = new OpenAPIHono<AuthContext>();
+
+  // 分页查询路由
+  const listRoute = createRoute({
+    method: 'get',
+    path: '/',
+    middleware,
+    request: {
+      query: z.object({
+        page: z.coerce.number<number>().int().positive().default(1).openapi({
+          example: 1,
+          description: '页码,从1开始'
+        }),
+        pageSize: z.coerce.number<number>().int().positive().default(10).openapi({
+          example: 10,
+          description: '每页数量'
+        }),
+        keyword: z.string().optional().openapi({
+          example: '搜索关键词',
+          description: '搜索关键词'
+        }),
+        sortBy: z.string().optional().openapi({
+          example: 'createdAt',
+          description: '排序字段'
+        }),
+        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查询等'
+        })
+      })
+    },
+    responses: {
+      200: {
+        description: '成功获取列表',
+        content: {
+          'application/json': {
+            schema: z.object({
+              data: z.array(listSchema),
+              pagination: z.object({
+                total: z.number().openapi({ example: 100, description: '总记录数' }),
+                current: z.number().openapi({ example: 1, description: '当前页码' }),
+                pageSize: z.number().openapi({ example: 10, description: '每页数量' })
+              })
+            })
+          }
+        }
+      },
+      400: {
+        description: '参数错误',
+        content: { 'application/json': { schema: ErrorSchema } }
+      },
+      500: {
+        description: '服务器错误',
+        content: { 'application/json': { schema: ErrorSchema } }
+      }
+    }
+  });
+
+  // 创建资源路由
+  const createRouteDef = createRoute({
+    method: 'post',
+    path: '/',
+    middleware,
+    request: {
+      body: {
+        content: {
+          'application/json': { schema: createSchema }
+        }
+      }
+    },
+    responses: {
+      201: {
+        description: '创建成功',
+        content: { 'application/json': { schema: getSchema } }
+      },
+      400: {
+        description: '输入数据无效',
+        content: { 'application/json': { schema: ErrorSchema } }
+      },
+      500: {
+        description: '服务器错误',
+        content: { 'application/json': { schema: ErrorSchema } }
+      }
+    }
+  });
+
+  // 获取单个资源路由
+  const getRouteDef = createRoute({
+    method: 'get',
+    path: '/{id}',
+    middleware,
+    request: {
+      params: z.object({
+        id: z.coerce.number<number>().openapi({
+          param: { name: 'id', in: 'path' },
+          example: 1,
+          description: '资源ID'
+        })
+      })
+    },
+    responses: {
+      200: {
+        description: '成功获取详情',
+        content: { 'application/json': { schema: getSchema } }
+      },
+      400: {
+        description: '资源不存在',
+        content: { 'application/json': { schema: ErrorSchema } }
+      },
+      404: {
+        description: '参数验证失败',
+        content: { 'application/json': { schema: ErrorSchema } }
+      },
+      500: {
+        description: '服务器错误',
+        content: { 'application/json': { schema: ErrorSchema } }
+      }
+    }
+  });
+
+  // 更新资源路由
+  const updateRouteDef = createRoute({
+    method: 'put',
+    path: '/{id}',
+    middleware,
+    request: {
+      params: z.object({
+        id: z.coerce.number<number>().openapi({
+          param: { name: 'id', in: 'path' },
+          example: 1,
+          description: '资源ID'
+        })
+      }),
+      body: {
+        content: {
+          'application/json': { schema: updateSchema }
+        }
+      }
+    },
+    responses: {
+      200: {
+        description: '更新成功',
+        content: { 'application/json': { schema: getSchema } }
+      },
+      400: {
+        description: '无效输入',
+        content: { 'application/json': { schema: ErrorSchema } }
+      },
+      404: {
+        description: '资源不存在',
+        content: { 'application/json': { schema: ErrorSchema } }
+      },
+      500: {
+        description: '服务器错误',
+        content: { 'application/json': { schema: ErrorSchema } }
+      }
+    }
+  });
+
+  // 删除资源路由
+  const deleteRouteDef = createRoute({
+    method: 'delete',
+    path: '/{id}',
+    middleware,
+    request: {
+      params: z.object({
+        id: z.coerce.number<number>().openapi({
+          param: { name: 'id', in: 'path' },
+          example: 1,
+          description: '资源ID'
+        })
+      })
+    },
+    responses: {
+      204: { description: '删除成功' },
+      404: {
+        description: '资源不存在',
+        content: { 'application/json': { schema: ErrorSchema } }
+      },
+      500: {
+        description: '服务器错误',
+        content: { 'application/json': { schema: ErrorSchema } }
+      }
+    }
+  });
+
+  // 注册路由处理函数
+
+  // 只读模式下只注册 GET 路由
+  if (!readOnly) {
+    // 完整 CRUD 路由
+    const routes = app
+      .openapi(listRoute, async (c) => {
+        try {
+          const query = c.req.valid('query') as any;
+          const { page, pageSize, keyword, sortBy, sortOrder, filters } = query;
+
+          // 构建排序对象
+          const order: any = {};
+          if (sortBy) {
+            order[sortBy] = sortOrder || 'DESC';
+          } else {
+            order['id'] = 'DESC';
+          }
+
+          // 解析筛选条件
+          let parsedFilters: any = undefined;
+          if (filters) {
+            try {
+              parsedFilters = JSON.parse(filters);
+            } catch (e) {
+              return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
+            }
+          }
+          const crudService = new ConcreteCrudService(dataSource, entity, {
+            userTracking: userTracking,
+            relationFields: relationFields
+          });
+
+          const [data, total] = await crudService.getList(
+            page,
+            pageSize,
+            keyword,
+            searchFields,
+            undefined,
+            relations || [],
+            order,
+            parsedFilters
+          );
+
+          return c.json({
+            // data: z.array(listSchema).parse(data),
+            data: await parseWithAwait(z.array(listSchema), data),
+            pagination: { total, current: page, pageSize }
+          }, 200);
+        } catch (error) {
+          if (error instanceof z.ZodError) {
+            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+          }
+          return c.json({
+            code: 500,
+            message: error instanceof Error ? error.message : '获取列表失败'
+          }, 500);
+        }
+      })
+      // @ts-ignore
+      .openapi(createRouteDef, async (c: any) => {
+        try {
+          const data = c.req.valid('json');
+          const user = c.get('user');
+
+          const crudService = new ConcreteCrudService(dataSource, entity, {
+            userTracking: userTracking,
+            relationFields: relationFields
+          });
+          const result = await crudService.create(data, user?.id);
+          return c.json(result, 201);
+        } catch (error) {
+          if (error instanceof z.ZodError) {
+            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+          }
+          return c.json({
+            code: 500,
+            message: error instanceof Error ? error.message : '创建资源失败'
+          }, 500);
+        }
+      })
+      // @ts-ignore
+      .openapi(getRouteDef, async (c: any) => {
+        try {
+          const { id } = c.req.valid('param');
+
+          const crudService = new ConcreteCrudService(dataSource, entity, {
+            userTracking: userTracking,
+            relationFields: relationFields
+          });
+          const result = await crudService.getById(id, relations || []);
+
+          if (!result) {
+            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);
+          }
+          return c.json({
+            code: 500,
+            message: error instanceof Error ? error.message : '获取资源失败'
+          }, 500);
+        }
+      })
+      // @ts-ignore
+      .openapi(updateRouteDef, async (c: any) => {
+        try {
+          const { id } = c.req.valid('param');
+          const data = c.req.valid('json');
+          const user = c.get('user');
+
+          const crudService = new ConcreteCrudService(dataSource, entity, {
+            userTracking: userTracking,
+            relationFields: relationFields
+          });
+          const result = await crudService.update(id, data, user?.id);
+
+          if (!result) {
+            return c.json({ code: 404, message: '资源不存在' }, 404);
+          }
+
+          return c.json(result, 200);
+        } catch (error) {
+          if (error instanceof z.ZodError) {
+            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+          }
+          return c.json({
+            code: 500,
+            message: error instanceof Error ? error.message : '更新资源失败'
+          }, 500);
+        }
+      })
+      .openapi(deleteRouteDef, async (c: any) => {
+        try {
+          const { id } = c.req.valid('param');
+
+          const crudService = new ConcreteCrudService(dataSource, entity, {
+            userTracking: userTracking,
+            relationFields: relationFields
+          });
+          const success = await crudService.delete(id);
+
+          if (!success) {
+            return c.json({ code: 404, message: '资源不存在' }, 404);
+          }
+
+          return c.body(null, 204);
+        } catch (error) {
+          if (error instanceof z.ZodError) {
+            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+          }
+          return c.json({
+            code: 500,
+            message: error instanceof Error ? error.message : '删除资源失败'
+          }, 500);
+        }
+      });
+
+    return routes;
+  } else {
+    // 只读模式,只注册 GET 路由
+    const routes = app
+      .openapi(listRoute, async (c) => {
+        try {
+          const query = c.req.valid('query') as any;
+          const { page, pageSize, keyword, sortBy, sortOrder, filters } = query;
+
+          // 构建排序对象
+          const order: any = {};
+          if (sortBy) {
+            order[sortBy] = sortOrder || 'DESC';
+          } else {
+            order['id'] = 'DESC';
+          }
+
+          // 解析筛选条件
+          let parsedFilters: any = undefined;
+          if (filters) {
+            try {
+              parsedFilters = JSON.parse(filters);
+            } catch (e) {
+              return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
+            }
+          }
+          const crudService = new ConcreteCrudService(dataSource, entity, {
+            userTracking: userTracking,
+            relationFields: relationFields
+          });
+
+          const [data, total] = await crudService.getList(
+            page,
+            pageSize,
+            keyword,
+            searchFields,
+            undefined,
+            relations || [],
+            order,
+            parsedFilters
+          );
+
+          return c.json({
+            data: await parseWithAwait(z.array(listSchema), data),
+            pagination: { total, current: page, pageSize }
+          }, 200);
+        } catch (error) {
+          if (error instanceof z.ZodError) {
+            return c.json({ code: 400, message: '参数验证失败', errors: JSON.parse(error.message) }, 400);
+          }
+          return c.json({
+            code: 500,
+            message: error instanceof Error ? error.message : '获取列表失败'
+          }, 500);
+        }
+      })
+      // @ts-ignore
+      .openapi(getRouteDef, async (c: any) => {
+        try {
+          const { id } = c.req.valid('param');
+
+          const crudService = new ConcreteCrudService(dataSource, entity, {
+            userTracking: userTracking,
+            relationFields: relationFields
+          });
+          const result = await crudService.getById(id, relations || []);
+
+          if (!result) {
+            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);
+          }
+          return c.json({
+            code: 500,
+            message: error instanceof Error ? error.message : '获取资源失败'
+          }, 500);
+        }
+      });
+    return routes;
+  }
+
+}

+ 1 - 0
packages/shared-crud/src/routes/index.ts

@@ -0,0 +1 @@
+export { createCrudRoutes } from './generic-crud.routes';

+ 13 - 0
packages/shared-crud/src/services/concrete-crud.service.ts

@@ -0,0 +1,13 @@
+import { DataSource, ObjectLiteral } from 'typeorm';
+import { GenericCrudService, UserTrackingOptions, RelationFieldOptions } from './generic-crud.service';
+
+// 创建具体CRUD服务类
+export class ConcreteCrudService<T extends ObjectLiteral> extends GenericCrudService<T> {
+  constructor(
+    dataSource: DataSource,
+    entity: new () => T,
+    options?: { userTracking?: UserTrackingOptions; relationFields?: RelationFieldOptions }
+  ) {
+    super(dataSource, entity, options);
+  }
+}

+ 324 - 0
packages/shared-crud/src/services/generic-crud.service.ts

@@ -0,0 +1,324 @@
+import { DataSource, Repository, ObjectLiteral, DeepPartial, In } from 'typeorm';
+import { z } from '@hono/zod-openapi';
+
+export abstract class GenericCrudService<T extends ObjectLiteral> {
+  protected repository: Repository<T>;
+  private userTrackingOptions?: UserTrackingOptions;
+
+  protected relationFields?: RelationFieldOptions;
+
+  constructor(
+    protected dataSource: DataSource,
+    protected entity: new () => T,
+    options?: {
+      userTracking?: UserTrackingOptions;
+      relationFields?: RelationFieldOptions;
+    }
+  ) {
+    this.repository = this.dataSource.getRepository(entity);
+    this.userTrackingOptions = options?.userTracking;
+    this.relationFields = options?.relationFields;
+  }
+
+  /**
+   * 获取分页列表
+   */
+  async getList(
+    page: number = 1,
+    pageSize: number = 10,
+    keyword?: string,
+    searchFields?: string[],
+    where?: Partial<T>,
+    relations: string[] = [],
+    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) => {
+        const parts = relation.split('.');
+        let currentAlias = 'entity';
+
+        parts.forEach((part, index) => {
+          // 生成一致的别名:对于嵌套关联,使用下划线连接路径
+          const newAlias = index === 0 ? part : parts.slice(0, index + 1).join('_');
+          query.leftJoinAndSelect(`${currentAlias}.${part}`, newAlias);
+          currentAlias = newAlias;
+        });
+      });
+    }
+
+    // 关键词搜索 - 支持关联字段搜索(格式:relation.field 或 relation.nestedRelation.field)
+    if (keyword && searchFields && searchFields.length > 0) {
+      const searchConditions: string[] = [];
+      const searchParams: Record<string, string> = { keyword: `%${keyword}%` };
+
+      searchFields.forEach((field) => {
+        // 检查是否为关联字段(包含点号)
+        if (field.includes('.')) {
+          const parts = field.split('.');
+          const alias = parts.slice(0, -1).join('_'); // 使用下划线连接关系路径作为别名
+          const fieldName = parts[parts.length - 1];
+
+          searchConditions.push(`${alias}.${fieldName} LIKE :keyword`);
+        } else {
+          // 普通字段搜索
+          searchConditions.push(`entity.${field} LIKE :keyword`);
+        }
+      });
+
+      if (searchConditions.length > 0) {
+        query.andWhere(`(${searchConditions.join(' OR ')})`, searchParams);
+      }
+    }
+
+    // 条件查询
+    if (where) {
+      Object.entries(where).forEach(([key, value]) => {
+        if (value !== undefined && value !== null) {
+          query.andWhere(`entity.${key} = :${key}`, { [key]: value });
+        }
+      });
+    }
+
+    // 扩展筛选条件
+    if (filters) {
+      Object.entries(filters).forEach(([key, value]) => {
+        if (value !== undefined && value !== null && value !== '') {
+          const fieldName = key.startsWith('_') ? key.substring(1) : key;
+
+          // 检查是否为关联字段(包含点号)
+          let tableAlias: string = 'entity';
+          let actualFieldName: string = fieldName;
+
+          if (fieldName.includes('.')) {
+            const parts = fieldName.split('.');
+            tableAlias = parts.slice(0, -1).join('_') || 'entity'; // 使用下划线连接关系路径作为别名
+            actualFieldName = parts[parts.length - 1] || fieldName;
+          }
+
+          // 支持不同类型的筛选
+          if (Array.isArray(value)) {
+            // 数组类型:IN查询
+            if (value.length > 0) {
+              query.andWhere(`${tableAlias}.${actualFieldName} IN (:...${key})`, { [key]: value });
+            }
+          } else if (typeof value === 'string' && value.includes('%')) {
+            // 模糊匹配
+            query.andWhere(`${tableAlias}.${actualFieldName} LIKE :${key}`, { [key]: value });
+          } else if (typeof value === 'object' && value !== null) {
+            // 范围查询
+            if ('gte' in value) {
+              query.andWhere(`${tableAlias}.${actualFieldName} >= :${key}_gte`, { [`${key}_gte`]: value.gte });
+            }
+            if ('gt' in value) {
+              query.andWhere(`${tableAlias}.${actualFieldName} > :${key}_gt`, { [`${key}_gt`]: value.gt });
+            }
+            if ('lte' in value) {
+              query.andWhere(`${tableAlias}.${actualFieldName} <= :${key}_lte`, { [`${key}_lte`]: value.lte });
+            }
+            if ('lt' in value) {
+              query.andWhere(`${tableAlias}.${actualFieldName} < :${key}_lt`, { [`${key}_lt`]: value.lt });
+            }
+            if ('between' in value && Array.isArray(value.between) && value.between.length === 2) {
+              query.andWhere(`${tableAlias}.${actualFieldName} BETWEEN :${key}_start AND :${key}_end`, {
+                [`${key}_start`]: value.between[0],
+                [`${key}_end`]: value.between[1]
+              });
+            }
+          } else {
+            // 精确匹配
+            query.andWhere(`${tableAlias}.${actualFieldName} = :${key}`, { [key]: value });
+          }
+        }
+      });
+    }
+
+    // 排序
+    Object.entries(order).forEach(([key, direction]) => {
+      query.orderBy(`entity.${key}`, direction);
+    });
+
+    const finalQuery = query.skip(skip).take(pageSize);
+
+    // console.log(finalQuery.getSql())
+
+    return finalQuery.getManyAndCount();
+  }
+
+  /**
+   * 根据ID获取单个实体
+   */
+  async getById(id: number, relations: string[] = []): Promise<T | null> {
+    return this.repository.findOne({
+      where: { id } as any,
+      relations
+    });
+  }
+
+  /**
+   * 设置用户跟踪字段
+   */
+  private setUserFields(data: any, userId?: string | number, isCreate: boolean = true): void {
+    if (!this.userTrackingOptions || !userId) {
+      return;
+    }
+
+    const {
+      createdByField = 'createdBy',
+      updatedByField = 'updatedBy',
+      userIdField = 'userId'
+    } = this.userTrackingOptions;
+
+    // 设置创建人
+    if (isCreate && createdByField) {
+      data[createdByField] = userId;
+    }
+
+    // 设置更新人
+    if (updatedByField) {
+      data[updatedByField] = userId;
+    }
+
+    // 设置关联的用户ID(如userId字段)
+    if (isCreate && userIdField) {
+      data[userIdField] = userId;
+    }
+  }
+
+  /**
+   * 处理关联字段
+   */
+  private async handleRelationFields(data: any, entity: T, _isUpdate: boolean = false): Promise<void> {
+    if (!this.relationFields) return;
+
+    for (const [fieldName, config] of Object.entries(this.relationFields)) {
+      if (data[fieldName] !== undefined) {
+        const ids = data[fieldName];
+        const relationRepository = this.dataSource.getRepository(config.targetEntity);
+
+        if (ids && Array.isArray(ids) && ids.length > 0) {
+          const relatedEntities = await relationRepository.findBy({ id: In(ids) });
+          (entity as any)[config.relationName] = relatedEntities;
+        } else {
+          (entity as any)[config.relationName] = [];
+        }
+
+        // 清理原始数据中的关联字段
+        delete data[fieldName];
+      }
+    }
+  }
+
+  /**
+   * 创建实体
+   */
+  async create(data: DeepPartial<T>, userId?: string | number): Promise<T> {
+    const entityData = { ...data };
+    this.setUserFields(entityData, userId, true);
+
+    // 分离关联字段数据
+    const relationData: any = {};
+    if (this.relationFields) {
+      for (const fieldName of Object.keys(this.relationFields)) {
+        if (fieldName in entityData) {
+          relationData[fieldName] = (entityData as any)[fieldName];
+          delete (entityData as any)[fieldName];
+        }
+      }
+    }
+
+    const entity = this.repository.create(entityData as DeepPartial<T>);
+
+    // 处理关联字段
+    await this.handleRelationFields(relationData, entity);
+
+    return this.repository.save(entity);
+  }
+
+  /**
+   * 更新实体
+   */
+  async update(id: number, data: Partial<T>, userId?: string | number): Promise<T | null> {
+    const updateData = { ...data };
+    this.setUserFields(updateData, userId, false);
+
+    // 分离关联字段数据
+    const relationData: any = {};
+    if (this.relationFields) {
+      for (const fieldName of Object.keys(this.relationFields)) {
+        if (fieldName in updateData) {
+          relationData[fieldName] = (updateData as any)[fieldName];
+          delete (updateData as any)[fieldName];
+        }
+      }
+    }
+
+    // 先更新基础字段
+    await this.repository.update(id, updateData);
+
+    // 获取完整实体并处理关联字段
+    const entity = await this.getById(id);
+    if (!entity) return null;
+
+    // 处理关联字段
+    await this.handleRelationFields(relationData, entity, true);
+
+    return this.repository.save(entity);
+  }
+
+  /**
+   * 删除实体
+   */
+  async delete(id: number): Promise<boolean> {
+    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;
+  userIdField?: string;
+}
+
+export interface RelationFieldOptions {
+  [fieldName: string]: {
+    relationName: string;
+    targetEntity: new () => any;
+    joinTableName?: string;
+  };
+}
+
+export type CrudOptions<
+  T extends ObjectLiteral,
+  CreateSchema extends z.ZodSchema = z.ZodSchema,
+  UpdateSchema extends z.ZodSchema = z.ZodSchema,
+  GetSchema extends z.ZodSchema = z.ZodSchema,
+  ListSchema extends z.ZodSchema = z.ZodSchema
+> = {
+  entity: new () => T;
+  createSchema: CreateSchema;
+  updateSchema: UpdateSchema;
+  getSchema: GetSchema;
+  listSchema: ListSchema;
+  searchFields?: string[];
+  relations?: string[];
+  middleware?: any[];
+  userTracking?: UserTrackingOptions;
+  relationFields?: RelationFieldOptions;
+  readOnly?: boolean;
+};

+ 3 - 0
packages/shared-crud/src/services/index.ts

@@ -0,0 +1,3 @@
+export { GenericCrudService } from './generic-crud.service';
+export { ConcreteCrudService } from './concrete-crud.service';
+export type { UserTrackingOptions, RelationFieldOptions, CrudOptions } from './generic-crud.service';

+ 16 - 0
packages/shared-crud/tsconfig.json

@@ -0,0 +1,16 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "composite": true,
+    "rootDir": "src",
+    "outDir": "dist"
+  },
+  "include": [
+    "src/**/*"
+  ],
+  "exclude": [
+    "node_modules",
+    "dist",
+    "tests"
+  ]
+}

+ 14 - 0
packages/shared-crud/vitest.config.ts

@@ -0,0 +1,14 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+  test: {
+    globals: true,
+    environment: 'node',
+    include: ['tests/**/*.test.ts'],
+    coverage: {
+      provider: 'v8',
+      reporter: ['text', 'json', 'html'],
+      exclude: ['node_modules/', 'tests/']
+    }
+  }
+});