Browse Source

📝 docs(crud): update generic crud documentation

- update getById method parameters in 12-generic-crud.md
- add relations configuration option in route creation
- update route descriptions to mention relation support
- add new rules for crud filtering and user tracking

📝 docs(crud): add crud filtering specification

- define filters parameter format and usage
- document supported filtering operations
- provide api usage examples
- add frontend and backend implementation guidelines
- include best practices and common questions

📝 docs(crud): add user tracking specification

- define automatic operator ID field handling
- document configuration options for user tracking
- provide entity class definition requirements
- include multiple configuration examples
- explain working principles and best practices
yourname 4 months ago
parent
commit
8e668be22c
3 changed files with 407 additions and 5 deletions
  1. 15 5
      .roo/rules/12-generic-crud.md
  2. 207 0
      .roo/rules/14-crud-filtering.md
  3. 185 0
      .roo/rules/15-user-tracking.md

+ 15 - 5
.roo/rules/12-generic-crud.md

@@ -32,7 +32,7 @@ export class YourEntityService extends GenericCrudService<YourEntity> {
 | 方法 | 描述 | 参数 | 返回值 |
 |------|------|------|--------|
 | `getList` | 获取分页列表 | `page`, `pageSize`, `keyword`, `searchFields`, `where`, `relations`, `order` | `[T[], number]` |
-| `getById` | 根据ID获取单个实体 | `id: number` | `T \| null` |
+| `getById` | 根据ID获取单个实体 | `id: number`, `relations?: string[]` | `T \| null` |
 | `create` | 创建实体 | `data: DeepPartial<T>` | `T` |
 | `update` | 更新实体 | `id: number`, `data: Partial<T>` | `T \| null` |
 | `delete` | 删除实体 | `id: number` | `boolean` |
@@ -73,6 +73,7 @@ const yourEntityRoutes = createCrudRoutes({
   getSchema: YourEntitySchema,
   listSchema: YourEntitySchema,
   searchFields: ['name', 'description'], // 可选,指定搜索字段
+  relations: ['relatedEntity'], // 可选,指定关联查询关系(支持嵌套关联,如 ['relatedEntity.client'])
   middleware: [authMiddleware] // 可选,添加中间件
 });
 
@@ -89,6 +90,7 @@ export default yourEntityRoutes;
 | `getSchema` | `z.ZodSchema` | 获取单个实体的响应 schema | 是 |
 | `listSchema` | `z.ZodSchema` | 获取列表的响应 schema | 是 |
 | `searchFields` | `string[]` | 搜索字段数组,用于关键词搜索 | 否 |
+| `relations` | `string[]` | 关联查询配置,指定需要关联查询的关系 | 否 |
 | `middleware` | `any[]` | 应用于所有CRUD路由的中间件数组 | 否 |
 
 ### 2.3 生成的路由
@@ -97,9 +99,9 @@ export default yourEntityRoutes;
 
 | 方法 | 路径 | 描述 |
 |------|------|------|
-| GET | `/` | 获取实体列表(支持分页、搜索、排序) |
+| GET | `/` | 获取实体列表(支持分页、搜索、排序、关联查询) |
 | POST | `/` | 创建新实体 |
-| GET | `/{id}` | 获取单个实体详情 |
+| GET | `/{id}` | 获取单个实体详情(支持关联查询) |
 | PUT | `/{id}` | 更新实体 |
 | DELETE | `/{id}` | 删除实体 |
 
@@ -129,8 +131,9 @@ api.route('/api/v1/your-entities', yourEntityRoutes);
 
 ```typescript
 // your-entity.entity.ts
-import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany } from 'typeorm';
 import { z } from '@hono/zod-openapi';
+import { RelatedEntity } from './related-entity.entity';
 
 @Entity('your_entity')
 export class YourEntity {
@@ -140,6 +143,9 @@ export class YourEntity {
   @Column({ name: 'name', type: 'varchar', length: 255 })
   name!: string;
   
+  @ManyToOne(() => RelatedEntity, related => related.yourEntities)
+  relatedEntity!: RelatedEntity;
+  
   // 其他字段...
 }
 
@@ -147,16 +153,19 @@ export class YourEntity {
 export const YourEntitySchema = z.object({
   id: z.number().int().positive().openapi({ description: '实体ID' }),
   name: z.string().max(255).openapi({ description: '名称', example: '示例名称' }),
+  relatedEntity: RelatedEntitySchema, // 关联实体schema
   // 其他字段schema...
 });
 
 export const CreateYourEntityDto = z.object({
   name: z.string().max(255).openapi({ description: '名称', example: '示例名称' }),
+  relatedEntityId: z.number().int().positive().openapi({ description: '关联实体ID', example: 1 }),
   // 其他创建字段schema...
 });
 
 export const UpdateYourEntityDto = z.object({
   name: z.string().max(255).optional().openapi({ description: '名称', example: '示例名称' }),
+  relatedEntityId: z.number().int().positive().optional().openapi({ description: '关联实体ID', example: 1 }),
   // 其他更新字段schema...
 });
 ```
@@ -249,4 +258,5 @@ export class YourEntityService extends GenericCrudService<YourEntity> {
 4. **数据验证**:确保Zod schema包含完整的验证规则和OpenAPI元数据
 5. **搜索优化**:合理设置`searchFields`,避免在大表的文本字段上进行模糊搜索
 6. **分页处理**:所有列表接口必须支持分页,避免返回大量数据
-7. **事务管理**:复杂操作应使用事务确保数据一致性
+7. **关联查询**:使用`relations`配置时,避免过度关联导致性能问题
+8. **事务管理**:复杂操作应使用事务确保数据一致性

+ 207 - 0
.roo/rules/14-crud-filtering.md

@@ -0,0 +1,207 @@
+# 通用CRUD筛选规范
+
+## 概述
+
+本规范定义了通用CRUD模块的增强筛选功能,支持多种查询模式和灵活的筛选条件。
+
+## 新增功能
+
+### 1. 筛选参数支持
+- **参数名**: `filters`
+- **类型**: JSON字符串
+- **位置**: 查询参数
+- **可选**: 是
+
+### 2. 支持的筛选操作
+
+| 操作类型 | 语法格式 | 示例 | 说明 |
+|----------|----------|------|------|
+| 精确匹配 | `"字段名": 值` | `{"status": 1}` | 等于指定值 |
+| 模糊匹配 | `"字段名": "%值%"` | `{"name": "%张%"}` | LIKE查询,包含指定值 |
+| 大于 | `"字段名": {"gt": 值}` | `{"age": {"gt": 18}}` | 大于指定值 |
+| 大于等于 | `"字段名": {"gte": 值}` | `{"age": {"gte": 18}}` | 大于等于指定值 |
+| 小于 | `"字段名": {"lt": 值}` | `{"score": {"lt": 60}}` | 小于指定值 |
+| 小于等于 | `"字段名": {"lte": 值}` | `{"score": {"lte": 60}}` | 小于等于指定值 |
+| 范围查询 | `"字段名": {"between": [最小值, 最大值]}` | `{"createdAt": {"between": ["2024-01-01", "2024-12-31"]}}` | 在指定范围内 |
+| IN查询 | `"字段名": [值1, 值2, ...]` | `{"status": [1, 2, 3]}` | 值在指定数组中 |
+| NULL匹配 | `"字段名": null` | `{"deletedAt": null}` | 字段为NULL |
+
+### 3. 组合查询
+支持多个条件同时筛选,所有条件为AND关系:
+
+```json
+{
+  "status": 1,
+  "name": "%张%",
+  "createdAt": {"gte": "2024-01-01"}
+}
+```
+
+## API使用规范
+
+### 请求格式
+```http
+GET /api/v1/entities?filters={"status":1,"name":"%张%","createdAt":{"gte":"2024-01-01"}}
+```
+
+### 响应格式
+```json
+{
+  "data": [...],
+  "pagination": {
+    "total": 100,
+    "current": 1,
+    "pageSize": 10
+  }
+}
+```
+
+### 错误处理
+- 格式错误:返回400状态码,message为"筛选条件格式错误"
+- 字段不存在:安全忽略,不影响查询
+- 类型不匹配:安全处理,返回空结果
+
+## 前端集成规范
+
+### React Hook示例
+```typescript
+const useEntityList = () => {
+  const [data, setData] = useState([]);
+  
+  const fetchData = async (filters = {}) => {
+    const response = await client.$get({
+      query: {
+        page: 1,
+        pageSize: 10,
+        filters: JSON.stringify(filters)
+      }
+    });
+    // 处理响应...
+  };
+  
+  return { data, fetchData };
+};
+```
+
+### ProTable集成
+```typescript
+const columns = [
+  {
+    title: '状态',
+    dataIndex: 'status',
+    valueType: 'select',
+    valueEnum: { 0: '禁用', 1: '启用' },
+  },
+  {
+    title: '创建时间',
+    dataIndex: 'createdAt',
+    valueType: 'dateRange',
+  },
+];
+
+const handleRequest = async (params) => {
+  const filters = {};
+  
+  if (params.status !== undefined) filters.status = params.status;
+  if (params.createdAt?.length === 2) {
+    filters.createdAt = { between: params.createdAt };
+  }
+  
+  return client.$get({
+    query: {
+      page: params.current,
+      pageSize: params.pageSize,
+      keyword: params.keyword,
+      filters: JSON.stringify(filters)
+    }
+  });
+};
+```
+
+## 后端实现规范
+
+### GenericCrudService使用
+```typescript
+// 直接使用(无需修改)
+const service = new GenericCrudService(dataSource, UserEntity);
+const [data, total] = await service.getList(
+  page, pageSize, keyword, searchFields, where, relations, order, filters
+);
+
+// 自定义实现
+export class UserService extends GenericCrudService<User> {
+  async getList(
+    page: number = 1,
+    pageSize: number = 10,
+    keyword?: string,
+    searchFields?: string[],
+    where?: Partial<User>,
+    relations: string[] = [],
+    order: any = {},
+    filters?: any
+  ) {
+    // 添加业务特定的筛选
+    const customFilters = {
+      ...filters,
+      isDeleted: 0
+    };
+    
+    return super.getList(page, pageSize, keyword, searchFields, where, relations, order, customFilters);
+  }
+}
+```
+
+## 最佳实践
+
+### 1. 性能优化
+- 为常用筛选字段添加数据库索引
+- 避免对大字段进行模糊查询
+- 合理使用分页参数
+
+### 2. 安全考虑
+- 避免传递敏感字段到filters
+- 验证用户权限相关的筛选条件
+- 限制查询结果集大小
+
+### 3. 类型安全
+```typescript
+// 推荐:定义筛选类型
+interface UserFilters {
+  status?: number;
+  name?: string;
+  age?: { gte?: number; lte?: number };
+  createdAt?: { between?: [string, string] };
+}
+
+// 使用时
+const filters: UserFilters = { status: 1 };
+```
+
+### 4. 错误处理
+```typescript
+try {
+  const filters = JSON.parse(query.filters);
+  // 验证filters结构...
+} catch (error) {
+  return { code: 400, message: '筛选条件格式错误' };
+}
+```
+
+## 常见问题
+
+### Q: 如何忽略大小写的模糊匹配?
+A: 使用标准SQL LIKE语法,数据库层面处理大小写
+
+### Q: 是否支持OR条件?
+A: 当前版本仅支持AND关系,OR条件需要自定义实现
+
+### Q: 如何处理空字符串?
+A: 空字符串会被自动忽略,不会加入筛选条件
+
+### Q: 是否支持嵌套对象筛选?
+A: 支持,使用点表示法:`{"related.field": "value"}`
+
+## 版本兼容
+- 完全向后兼容,不影响现有API调用
+- 新增参数为可选,不传递时保持原有行为
+- 错误时返回标准化错误响应

+ 185 - 0
.roo/rules/15-user-tracking.md

@@ -0,0 +1,185 @@
+# 通用CRUD操作人ID字段配置规范
+
+## 概述
+
+本规范定义了如何在通用CRUD模块中自动处理操作人ID字段,确保创建和更新操作时自动记录操作用户信息。
+
+## 新增功能
+
+通用CRUD模块已支持自动注入操作人ID字段,包含以下功能:
+
+1. **创建时自动记录创建人ID**
+2. **更新时自动记录更新人ID**
+3. **灵活配置字段名称**
+4. **向后兼容现有实现**
+
+## 配置方式
+
+### 1. 在CrudOptions中配置用户跟踪
+
+```typescript
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { YourEntity } from '@/server/modules/your-module/your-entity.entity';
+import { YourEntitySchema, CreateYourEntityDto, UpdateYourEntityDto } from '@/server/modules/your-module/your-entity.entity';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const yourEntityRoutes = createCrudRoutes({
+  entity: YourEntity,
+  createSchema: CreateYourEntityDto,
+  updateSchema: UpdateYourEntityDto,
+  getSchema: YourEntitySchema,
+  listSchema: YourEntitySchema,
+  middleware: [authMiddleware],
+  // 用户跟踪配置
+  userTracking: {
+    createdByField: 'createdBy',    // 创建人ID字段名,默认 'createdBy'
+    updatedByField: 'updatedBy'     // 更新人ID字段名,默认 'updatedBy'
+  }
+});
+```
+
+### 2. 实体类定义要求
+
+实体必须包含与配置对应的字段:
+
+```typescript
+@Entity('your_entities')
+export class YourEntity {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  // 业务字段...
+  
+  @Column({ name: 'created_by', type: 'int', nullable: true, comment: '创建用户ID' })
+  createdBy?: number;
+  
+  @Column({ name: 'updated_by', type: 'int', nullable: true, comment: '更新用户ID' })
+  updatedBy?: number;
+  
+  @CreateDateColumn({ name: 'created_at' })
+  createdAt!: Date;
+  
+  @UpdateDateColumn({ name: 'updated_at' })
+  updatedAt!: Date;
+}
+```
+
+### 3. 字段命名约定
+
+| 配置字段名 | 推荐数据库字段名 | 类型 | 说明 |
+|------------|------------------|------|------|
+| createdByField | created_by | int/string | 创建人用户ID |
+| updatedByField | updated_by | int/string | 更新人用户ID |
+
+## 配置示例
+
+### 示例1:使用默认字段名
+
+```typescript
+const routes = createCrudRoutes({
+  entity: MyEntity,
+  createSchema: CreateMyEntityDto,
+  updateSchema: UpdateMyEntityDto,
+  getSchema: MyEntitySchema,
+  listSchema: MyEntitySchema,
+  userTracking: {} // 使用默认字段名 createdBy 和 updatedBy
+});
+```
+
+### 示例2:自定义字段名
+
+```typescript
+const routes = createCrudRoutes({
+  entity: MyEntity,
+  createSchema: CreateMyEntityDto,
+  updateSchema: UpdateMyEntityDto,
+  getSchema: MyEntitySchema,
+  listSchema: MyEntitySchema,
+  userTracking: {
+    createdByField: 'created_user_id',
+    updatedByField: 'modified_user_id'
+  }
+});
+```
+
+### 示例3:只记录创建人
+
+```typescript
+const routes = createCrudRoutes({
+  entity: MyEntity,
+  createSchema: CreateMyEntityDto,
+  updateSchema: UpdateMyEntityDto,
+  getSchema: MyEntitySchema,
+  listSchema: MyEntitySchema,
+  userTracking: {
+    createdByField: 'created_by',
+    updatedByField: undefined // 不记录更新人
+  }
+});
+```
+
+### 示例4:兼容现有字段名
+
+```typescript
+const routes = createCrudRoutes({
+  entity: Linkman,
+  createSchema: CreateLinkmanDto,
+  updateSchema: UpdateLinkmanDto,
+  getSchema: LinkmanSchema,
+  listSchema: LinkmanSchema,
+  userTracking: {
+    createdByField: 'createdUserId', // 匹配现有字段
+    updatedByField: 'updatedUserId'  // 匹配现有字段
+  }
+});
+```
+
+## 工作原理
+
+1. **创建操作**:
+   - 当用户创建新记录时,系统自动从认证上下文中提取用户ID
+   - 将用户ID注入到配置的创建人字段中
+   - 如果未配置createdByField,则跳过此步骤
+
+2. **更新操作**:
+   - 当用户更新记录时,系统自动从认证上下文中提取用户ID
+   - 将用户ID注入到配置的更新人字段中
+   - 如果未配置updatedByField,则跳过此步骤
+
+3. **向后兼容**:
+   - 未配置userTracking时,保持原有行为不变
+   - 所有现有代码无需修改即可继续运行
+
+## 注意事项
+
+1. **认证要求**:必须在路由中添加`authMiddleware`才能获取用户信息
+2. **字段类型**:用户ID字段应支持存储用户ID的数据类型(通常为int或varchar)
+3. **数据库字段**:确保实体类中定义的数据库字段名与配置一致
+4. **空值处理**:如果用户未登录,相关字段将保持为null
+
+## 最佳实践
+
+1. **统一命名**:在项目中统一使用相同的字段命名约定
+2. **索引优化**:为操作人ID字段添加数据库索引以提高查询性能
+3. **关联查询**:可以通过relations配置加载操作人详情
+4. **审计功能**:结合时间戳字段实现完整的操作审计
+
+## 常见问题
+
+### Q: 如何支持字符串类型的用户ID?
+A: 实体类字段类型设置为varchar即可,系统会自动处理字符串类型
+
+### Q: 可以配置不同的用户ID字段吗?
+A: 可以,通过createdByField和updatedByField分别配置
+
+### Q: 会影响现有实体吗?
+A: 不会,只有配置了userTracking的实体才会启用此功能
+
+### Q: 如何获取操作人详情?
+A: 在relations中配置用户关联关系即可:
+```typescript
+const routes = createCrudRoutes({
+  entity: YourEntity,
+  relations: ['createdByUser', 'updatedByUser'],
+  // ...其他配置
+});