Forráskód Böngészése

Merge remote-tracking branch 'origin/starter' into stock

yourname 4 hónapja
szülő
commit
f327057a13

+ 31 - 0
.roo/rules/10-entity.md

@@ -1,5 +1,29 @@
 # 数据库实体规范
 
+## 常见不规范问题
+  1. nullable 字段用了? 而不是!, 类型没加 null
+    错误示例:
+      ```ts
+      @Column({
+        name: 'description',
+        type: 'text',
+        nullable: true,
+        comment: '容器配置描述'
+      })
+      description?: string;
+      ```
+    正确示例:
+      ```ts
+      @Column({
+        name: 'description',
+        type: 'text',
+        nullable: true,
+        comment: '容器配置描述'
+      })
+      description!: string | null;
+      ```
+
+
 ## 1. 实体基础结构
 
 ```typescript
@@ -171,6 +195,7 @@ z.coerce.boolean().openapi({
 1. **创建Schema**:
    - 不包含`id`字段(由数据库自动生成)
    - 所有必填字段必须显式定义,不得为`optional()`
+   - 如字段为选填项(包括数据库允许为null的字段),必须显式添加`optional()`
    - 必须使用适当的coerce方法处理非字符串类型
 
 ```typescript
@@ -194,6 +219,12 @@ export const CreateEntityDto = z.object({
   expireDate: z.coerce.date().openapi({
     description: '过期日期',
     example: '2024-12-31T23:59:59Z'
+  }),
+  // 选填字段示例
+  // nullable字段必须显式添加optional()
+  description: z.string().max(500).nullable().optional().openapi({
+    description: '商品描述(选填)',
+    example: '这是一个可选的商品描述信息'
   })
 });
 ```

+ 18 - 0
.roo/rules/11-admin-frontend.md

@@ -0,0 +1,18 @@
+# 管理后台前端页面开发流程规范
+
+## 适用场景
+
+管理后台页面开发,包括列表页、详情页、表单页等后台功能页面的实现流程。
+
+## 开发流程
+
+### 1. **创建页面组件**
+   - 位置: `src/client/admin/pages/[EntityName].tsx`
+
+### 2. **注册路由配置**
+   - 位置: `src/client/admin/routes.tsx`
+     ```
+
+### 3. **添加菜单配置**
+   - 位置: `src/client/admin/menu.tsx`
+

+ 375 - 0
.roo/rules/11-custom-crud.md

@@ -0,0 +1,375 @@
+# 自定义复杂CRUD开发流程规范
+
+## 适用场景
+
+适用于包含复杂业务逻辑的实体,需要手动实现服务方法和路由处理的场景,如:
+- 复杂业务规则
+- 多表关联操作
+- 特殊权限控制
+- 非标准数据处理
+
+## 开发流程
+
+### 1. **创建实体**
+   - 位置: `src/server/modules/[模块名]/[实体名].entity.ts`
+   - 定义实体类和Zod Schema
+   - 示例:
+     ```typescript
+     import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
+     import { z } from '@hono/zod-openapi';
+     
+     @Entity('your_entity')
+     export class YourEntity {
+       @PrimaryGeneratedColumn({ unsigned: true })
+       id!: number;
+       
+       @Column({ name: 'name', type: 'varchar', length: 255 })
+       name!: string;
+       
+       // 其他业务字段...
+     }
+     
+     // Zod Schema定义
+     export const YourEntitySchema = z.object({
+       id: z.number().int().positive().openapi({ description: '实体ID' }),
+       name: z.string().max(255).openapi({ description: '名称', example: '示例名称' })
+       // 其他字段Schema...
+     });
+     
+     export const CreateYourEntityDto = z.object({
+       name: z.string().max(255).openapi({ description: '名称', example: '示例名称' })
+       // 其他创建字段...
+     });
+     
+     export const UpdateYourEntityDto = z.object({
+       name: z.string().max(255).optional().openapi({ description: '名称', example: '示例名称' })
+       // 其他更新字段...
+     });
+     ```
+
+### 2. **注册实体到数据源**
+   - 在`src/server/data-source.ts`中添加实体导入和注册:
+     ```typescript
+     // 实体类导入
+     import { YourEntity } from "./modules/[模块名]/[实体名].entity"
+     
+     export const AppDataSource = new DataSource({
+       // ...其他配置
+       entities: [
+         User, Role, YourEntity // 添加新实体到数组
+       ],
+       // ...其他配置
+     });
+     ```
+
+### 3. **创建自定义Service**
+   - 位置: `src/server/modules/[模块名]/[实体名].service.ts`
+   - 实现自定义业务逻辑和数据访问
+   - 示例:
+     ```typescript
+     import { DataSource, Repository } from 'typeorm';
+     import { YourEntity } from './your-entity.entity';
+     import { CreateYourEntityDto, UpdateYourEntityDto } from './your-entity.entity';
+     import { AppError } from '@/server/utils/errorHandler';
+     
+     export class YourEntityService {
+       private repository: Repository<YourEntity>;
+       
+       constructor(dataSource: DataSource) {
+         this.repository = dataSource.getRepository(YourEntity);
+       }
+       
+       /**
+        * 获取实体列表(带复杂过滤条件)
+        */
+       async findAll(filters: any): Promise<[YourEntity[], number]> {
+         const query = this.repository.createQueryBuilder('entity');
+         
+         // 添加复杂业务过滤逻辑
+         if (filters.status) {
+           query.andWhere('entity.status = :status', { status: filters.status });
+         }
+         
+         // 多表关联查询示例
+         if (filters.includeRelated) {
+           query.leftJoinAndSelect('entity.relatedEntity', 'related');
+         }
+         
+         const [items, total] = await query.getManyAndCount();
+         return [items, total];
+       }
+       
+       /**
+        * 根据ID获取单个实体
+        */
+       async findById(id: number): Promise<YourEntity> {
+         const entity = await this.repository.findOneBy({ id });
+         if (!entity) {
+           throw new AppError('实体不存在', 404);
+         }
+         return entity;
+       }
+       
+       /**
+        * 创建实体(带业务规则验证)
+        */
+       async create(data: CreateYourEntityDto): Promise<YourEntity> {
+         // 业务规则验证示例
+         const existing = await this.repository.findOneBy({ name: data.name });
+         if (existing) {
+           throw new AppError('名称已存在', 400);
+         }
+         
+         const entity = this.repository.create(data);
+         return this.repository.save(entity);
+       }
+       
+       /**
+        * 更新实体(带业务逻辑处理)
+        */
+       async update(id: number, data: UpdateYourEntityDto): Promise<YourEntity> {
+         const entity = await this.findById(id);
+         
+         // 业务逻辑处理示例
+         if (data.name && data.name !== entity.name) {
+           // 记录名称变更日志等业务操作
+         }
+         
+         Object.assign(entity, data);
+         return this.repository.save(entity);
+       }
+       
+       /**
+        * 删除实体(带权限检查)
+        */
+       async delete(id: number, userId: number): Promise<boolean> {
+         const entity = await this.findById(id);
+         
+         // 权限检查示例
+         if (entity.createdBy !== userId) {
+           throw new AppError('没有删除权限', 403);
+         }
+         
+         await this.repository.remove(entity);
+         return true;
+       }
+       
+       /**
+        * 自定义业务方法示例
+        */
+       async customBusinessOperation(params: any): Promise<any> {
+         // 实现复杂业务逻辑
+         // 可能包含事务、多表操作等
+       }
+     }
+     ```
+
+### 4. **创建自定义API路由**
+   - 目录结构:
+     ```
+     src/server/api/[实体名]/
+     ├── get.ts       # 列表查询
+     ├── post.ts      # 创建实体
+     ├── [id]/
+     │   ├── get.ts   # 获取单个实体
+     │   ├── put.ts   # 更新实体
+     │   └── delete.ts # 删除实体
+     └── index.ts     # 路由聚合
+     ```
+   
+   - **列表查询路由示例** (get.ts):
+     ```typescript
+     import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+     import { z } from 'zod';
+     import { YourEntitySchema } from '@/server/modules/your-module/your-entity.entity';
+     import { ErrorSchema } from '@/server/utils/errorHandler';
+     import { AppDataSource } from '@/server/data-source';
+     import { YourEntityService } from '@/server/modules/your-module/your-entity.service';
+     import { AuthContext } from '@/server/types/context';
+     import { authMiddleware } from '@/server/middleware/auth.middleware';
+     
+     // 查询参数Schema
+     const ListQuery = z.object({
+       page: z.coerce.number().int().positive().default(1).openapi({
+         description: '页码',
+         example: 1
+       }),
+       pageSize: z.coerce.number().int().positive().default(10).openapi({
+         description: '每页条数',
+         example: 10
+       }),
+       status: z.coerce.number().optional().openapi({
+         description: '状态过滤',
+         example: 1
+       })
+     });
+     
+     // 响应Schema
+     const ListResponse = z.object({
+       data: z.array(YourEntitySchema),
+       pagination: z.object({
+         total: z.number().openapi({ example: 100, description: '总记录数' }),
+         current: z.number().openapi({ example: 1, description: '当前页码' }),
+         pageSize: z.number().openapi({ example: 10, description: '每页数量' })
+       })
+     });
+     
+     // 路由定义
+     const routeDef = createRoute({
+       method: 'get',
+       path: '/',
+       middleware: [authMiddleware],
+       request: {
+         query: ListQuery
+       },
+       responses: {
+         200: {
+           description: '成功获取实体列表',
+           content: { 'application/json': { schema: ListResponse } }
+         },
+         400: {
+           description: '请求参数错误',
+           content: { 'application/json': { schema: ErrorSchema } }
+         },
+         500: {
+           description: '服务器错误',
+           content: { 'application/json': { schema: ErrorSchema } }
+         }
+       }
+     });
+     
+     // 路由实现
+     const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+       try {
+         const query = c.req.valid('query');
+         const service = new YourEntityService(AppDataSource);
+         
+         const [data, total] = await service.findAll({
+           status: query.status,
+           // 其他过滤条件
+         });
+         
+         return c.json({
+           data,
+           pagination: {
+             total,
+             current: query.page,
+             pageSize: query.pageSize
+           }
+         }, 200);
+       } catch (error) {
+         const { code = 500, message = '获取列表失败' } = error as Error & { code?: number };
+         return c.json({ code, message }, code);
+       }
+     });
+     
+     export default app;
+     ```
+     
+   - **创建实体路由示例** (post.ts):
+     ```typescript
+     import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+     import { CreateYourEntityDto, YourEntitySchema } from '@/server/modules/your-module/your-entity.entity';
+     import { ErrorSchema } from '@/server/utils/errorHandler';
+     import { AppDataSource } from '@/server/data-source';
+     import { YourEntityService } from '@/server/modules/your-module/your-entity.service';
+     import { AuthContext } from '@/server/types/context';
+     import { authMiddleware } from '@/server/middleware/auth.middleware';
+     
+     // 路由定义
+     const routeDef = createRoute({
+       method: 'post',
+       path: '/',
+       middleware: [authMiddleware],
+       request: {
+         body: {
+           content: {
+             'application/json': { schema: CreateYourEntityDto }
+           }
+         }
+       },
+       responses: {
+         200: {
+           description: '成功创建实体',
+           content: { 'application/json': { schema: YourEntitySchema } }
+         },
+         400: {
+           description: '请求参数错误',
+           content: { 'application/json': { schema: ErrorSchema } }
+         },
+         500: {
+           description: '服务器错误',
+           content: { 'application/json': { schema: ErrorSchema } }
+         }
+       }
+     });
+     
+     // 路由实现
+     const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+       try {
+         const data = await c.req.json();
+         const service = new YourEntityService(AppDataSource);
+         const result = await service.create(data);
+         return c.json(result, 200);
+       } catch (error) {
+         const { code = 500, message = '创建实体失败' } = error as Error & { code?: number };
+         return c.json({ code, message }, code);
+       }
+     });
+     
+     export default app;
+     ```
+     
+   - **路由聚合示例** (index.ts):
+     ```typescript
+     import { OpenAPIHono } from '@hono/zod-openapi';
+     import listRoute from './get';
+     import createRoute from './post';
+     import getByIdRoute from './[id]/get';
+     import updateRoute from './[id]/put';
+     import deleteRoute from './[id]/delete';
+     
+     const app = new OpenAPIHono()
+       .route('/', listRoute)
+       .route('/', createRoute)
+       .route('/', getByIdRoute)
+       .route('/', updateRoute)
+       .route('/', deleteRoute);
+     
+     export default app;
+     ```
+
+### 5. **注册路由**
+   - 在`src/server/api.ts`中添加路由注册:
+     ```typescript
+     import yourEntityRoutes from '@/server/api/your-entity/index';
+     
+     // 注册路由
+     api.route('/api/v1/your-entities', yourEntityRoutes);
+     ```
+
+### 6. **创建客户端API**
+   - 在`src/client/api.ts`中添加客户端定义:
+     ```typescript
+     import { hc } from 'hono/client';
+     import { YourEntityRoutes } from '@/server/api';
+     
+     export const yourEntityClient = hc<YourEntityRoutes>('/api/v1', {
+       fetch: axiosFetch,
+     }).api.v1['your-entities'];
+     ```
+
+### 7. **前端调用**
+    - 在页面组件(如`Users.tsx`)中:
+      - 使用`InferResponseType`提取响应类型
+      - 使用`InferRequestType`提取请求类型
+      - 示例:
+        ```typescript
+        type EntityResponse = InferResponseType<typeof entityClient.$get, 200>;
+        type CreateRequest = InferRequestType<typeof entityClient.$post>['json'];
+        ```
+
+### 8. **注册管理后台路由和菜单**
+    - **注册路由**:在`src/client/admin/routes.tsx`中添加路由配置
+      
+    - **注册菜单**:在`src/client/admin/menu.tsx`中添加菜单配置

+ 29 - 445
.roo/rules/11-entity-creation.md

@@ -16,456 +16,40 @@
 
 ## 标准通用CRUD开发流程
 
-### 1. **创建实体**
-   - 位置: `src/server/modules/[模块名]/[实体名].entity.ts`
-   - 参考已有实体文件如`user.entity.ts`
-   - 注意: 必须包含Zod Schema定义
-
-### 2. **注册实体到数据源**
-   - 在`src/server/data-source.ts`中添加实体导入和注册:
-     ```typescript
-     // 实体类导入
-     import { YourEntity } from "./modules/[模块名]/[实体名].entity"
-     
-     export const AppDataSource = new DataSource({
-       // ...其他配置
-       entities: [
-         User, Role, YourEntity // 添加新实体到数组
-       ],
-       // ...其他配置
-     });
-     ```
-
-### 3. **创建Service**
-   - 位置: `src/server/modules/[模块名]/[实体名].service.ts`
-   - 继承`GenericCrudService`
-   - 通过构造函数注入DataSource
-   ```typescript
-   import { GenericCrudService } from '@/server/utils/generic-crud.service';
-   import { DataSource } from 'typeorm';
-   import { YourEntity } from './your-entity.entity';
-   
-   export class YourEntityService extends GenericCrudService<YourEntity> {
-     constructor(dataSource: DataSource) {
-       super(dataSource, YourEntity);
-     }
-   }
-   ```
-
-### 4. **创建API路由**
-   - 使用`createCrudRoutes`快速生成CRUD路由:
-     ```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,
-       searchFields: ['name', 'description'], // 可选,指定搜索字段
-       middleware: [authMiddleware] // 可选,添加中间件
-     });
-     
-     export default yourEntityRoutes;
-     ```
-
-### 5. **注册路由**
-   - 在`src/server/api.ts`中添加路由注册:
-     ```typescript
-     import yourEntityRoutes from '@/server/api/your-entity/index';
-     
-     // 注册路由
-     api.route('/api/v1/your-entities', yourEntityRoutes);
-     ```
-
-### 6. **创建客户端API**
-   - 在`src/client/api.ts`中添加客户端定义:
-     ```typescript
-     import { hc } from 'hono/client';
-     import { YourEntityRoutes } from '@/server/api';
-     
-     export const yourEntityClient = hc<YourEntityRoutes>('/api/v1', {
-       fetch: axiosFetch,
-     }).api.v1['your-entities'];
-     ```
-
-6. **前端调用**
-   - 在页面组件(如`pages_users.tsx`)中:
-     - 使用`InferResponseType`提取响应类型
-     - 使用`InferRequestType`提取请求类型
-     - 示例:
-       ```typescript
-       type EntityResponse = InferResponseType<typeof entityClient.$get, 200>;
-       type CreateRequest = InferRequestType<typeof entityClient.$post>['json'];
-       ```
-
+适用于简单数据模型,无复杂业务逻辑,仅需基础CRUD操作的场景。采用`GenericCrudService`和`createCrudRoutes`快速生成接口。
+
+### 开发步骤概要
+
+1. **创建实体**:在`src/server/modules/[模块名]/[实体名].entity.ts`定义实体类和Zod Schema
+2. **注册实体**:在`src/server/data-source.ts`中注册新实体
+3. **创建Service**:继承`GenericCrudService`实现基础CRUD操作
+4. **创建API路由**:使用`createCrudRoutes`快速生成CRUD路由
+5. **注册路由**:在`src/server/api.ts`中注册路由
+6. **创建客户端API**:在`src/client/api.ts`中定义客户端调用方法
+7. **前端调用**:在页面组件中使用类型化API调用
+8. **注册路由和菜单**:
+   - 管理后台:在`src/client/admin/routes.tsx`中添加路由配置,在`src/client/admin/menu.tsx`中添加菜单配置
+   - 前台:在`src/client/home/routes.tsx`中添加路由配置
+
+详细流程请参见[标准通用CRUD开发流程规范](./11-standard-crud.md)
 ## 自定义复杂CRUD开发流程
 
-当实体需要复杂业务逻辑或非标准CRUD操作时,采用以下完整流程:
-
-### 1. **创建实体**
-   - 位置: `src/server/modules/[模块名]/[实体名].entity.ts`
-   - 定义实体类和Zod Schema
-   - 示例:
-     ```typescript
-     import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
-     import { z } from '@hono/zod-openapi';
-     
-     @Entity('your_entity')
-     export class YourEntity {
-       @PrimaryGeneratedColumn({ unsigned: true })
-       id!: number;
-       
-       @Column({ name: 'name', type: 'varchar', length: 255 })
-       name!: string;
-       
-       // 其他业务字段...
-     }
-     
-     // Zod Schema定义
-     export const YourEntitySchema = z.object({
-       id: z.number().int().positive().openapi({ description: '实体ID' }),
-       name: z.string().max(255).openapi({ description: '名称', example: '示例名称' })
-       // 其他字段Schema...
-     });
-     
-     export const CreateYourEntityDto = z.object({
-       name: z.string().max(255).openapi({ description: '名称', example: '示例名称' })
-       // 其他创建字段...
-     });
-     
-     export const UpdateYourEntityDto = z.object({
-       name: z.string().max(255).optional().openapi({ description: '名称', example: '示例名称' })
-       // 其他更新字段...
-     });
-     ```
-
-### 2. **注册实体到数据源**
-   - 在`src/server/data-source.ts`中添加实体导入和注册:
-     ```typescript
-     // 实体类导入
-     import { YourEntity } from "./modules/[模块名]/[实体名].entity"
-     
-     export const AppDataSource = new DataSource({
-       // ...其他配置
-       entities: [
-         User, Role, YourEntity // 添加新实体到数组
-       ],
-       // ...其他配置
-     });
-     ```
-
-### 3. **创建自定义Service**
-   - 位置: `src/server/modules/[模块名]/[实体名].service.ts`
-   - 实现自定义业务逻辑和数据访问
-   - 示例:
-     ```typescript
-     import { DataSource, Repository } from 'typeorm';
-     import { YourEntity } from './your-entity.entity';
-     import { CreateYourEntityDto, UpdateYourEntityDto } from './your-entity.entity';
-     import { AppError } from '@/server/utils/errorHandler';
-     
-     export class YourEntityService {
-       private repository: Repository<YourEntity>;
-       
-       constructor(dataSource: DataSource) {
-         this.repository = dataSource.getRepository(YourEntity);
-       }
-       
-       /**
-        * 获取实体列表(带复杂过滤条件)
-        */
-       async findAll(filters: any): Promise<[YourEntity[], number]> {
-         const query = this.repository.createQueryBuilder('entity');
-         
-         // 添加复杂业务过滤逻辑
-         if (filters.status) {
-           query.andWhere('entity.status = :status', { status: filters.status });
-         }
-         
-         // 多表关联查询示例
-         if (filters.includeRelated) {
-           query.leftJoinAndSelect('entity.relatedEntity', 'related');
-         }
-         
-         const [items, total] = await query.getManyAndCount();
-         return [items, total];
-       }
-       
-       /**
-        * 根据ID获取单个实体
-        */
-       async findById(id: number): Promise<YourEntity> {
-         const entity = await this.repository.findOneBy({ id });
-         if (!entity) {
-           throw new AppError('实体不存在', 404);
-         }
-         return entity;
-       }
-       
-       /**
-        * 创建实体(带业务规则验证)
-        */
-       async create(data: CreateYourEntityDto): Promise<YourEntity> {
-         // 业务规则验证示例
-         const existing = await this.repository.findOneBy({ name: data.name });
-         if (existing) {
-           throw new AppError('名称已存在', 400);
-         }
-         
-         const entity = this.repository.create(data);
-         return this.repository.save(entity);
-       }
-       
-       /**
-        * 更新实体(带业务逻辑处理)
-        */
-       async update(id: number, data: UpdateYourEntityDto): Promise<YourEntity> {
-         const entity = await this.findById(id);
-         
-         // 业务逻辑处理示例
-         if (data.name && data.name !== entity.name) {
-           // 记录名称变更日志等业务操作
-         }
-         
-         Object.assign(entity, data);
-         return this.repository.save(entity);
-       }
-       
-       /**
-        * 删除实体(带权限检查)
-        */
-       async delete(id: number, userId: number): Promise<boolean> {
-         const entity = await this.findById(id);
-         
-         // 权限检查示例
-         if (entity.createdBy !== userId) {
-           throw new AppError('没有删除权限', 403);
-         }
-         
-         await this.repository.remove(entity);
-         return true;
-       }
-       
-       /**
-        * 自定义业务方法示例
-        */
-       async customBusinessOperation(params: any): Promise<any> {
-         // 实现复杂业务逻辑
-         // 可能包含事务、多表操作等
-       }
-     }
-     ```
-
-### 4. **创建自定义API路由**
-   - 目录结构:
-     ```
-     src/server/api/[实体名]/
-     ├── get.ts       # 列表查询
-     ├── post.ts      # 创建实体
-     ├── [id]/
-     │   ├── get.ts   # 获取单个实体
-     │   ├── put.ts   # 更新实体
-     │   └── delete.ts # 删除实体
-     └── index.ts     # 路由聚合
-     ```
-   
-   - **列表查询路由示例** (get.ts):
-     ```typescript
-     import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
-     import { z } from 'zod';
-     import { YourEntitySchema } from '@/server/modules/your-module/your-entity.entity';
-     import { ErrorSchema } from '@/server/utils/errorHandler';
-     import { AppDataSource } from '@/server/data-source';
-     import { YourEntityService } from '@/server/modules/your-module/your-entity.service';
-     import { AuthContext } from '@/server/types/context';
-     import { authMiddleware } from '@/server/middleware/auth.middleware';
-     
-     // 查询参数Schema
-     const ListQuery = z.object({
-       page: z.coerce.number().int().positive().default(1).openapi({
-         description: '页码',
-         example: 1
-       }),
-       pageSize: z.coerce.number().int().positive().default(10).openapi({
-         description: '每页条数',
-         example: 10
-       }),
-       status: z.coerce.number().optional().openapi({
-         description: '状态过滤',
-         example: 1
-       })
-     });
-     
-     // 响应Schema
-     const ListResponse = z.object({
-       data: z.array(YourEntitySchema),
-       pagination: z.object({
-         total: z.number().openapi({ example: 100, description: '总记录数' }),
-         current: z.number().openapi({ example: 1, description: '当前页码' }),
-         pageSize: z.number().openapi({ example: 10, description: '每页数量' })
-       })
-     });
-     
-     // 路由定义
-     const routeDef = createRoute({
-       method: 'get',
-       path: '/',
-       middleware: [authMiddleware],
-       request: {
-         query: ListQuery
-       },
-       responses: {
-         200: {
-           description: '成功获取实体列表',
-           content: { 'application/json': { schema: ListResponse } }
-         },
-         400: {
-           description: '请求参数错误',
-           content: { 'application/json': { schema: ErrorSchema } }
-         },
-         500: {
-           description: '服务器错误',
-           content: { 'application/json': { schema: ErrorSchema } }
-         }
-       }
-     });
-     
-     // 路由实现
-     const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
-       try {
-         const query = c.req.valid('query');
-         const service = new YourEntityService(AppDataSource);
-         
-         const [data, total] = await service.findAll({
-           status: query.status,
-           // 其他过滤条件
-         });
-         
-         return c.json({
-           data,
-           pagination: {
-             total,
-             current: query.page,
-             pageSize: query.pageSize
-           }
-         }, 200);
-       } catch (error) {
-         const { code = 500, message = '获取列表失败' } = error as Error & { code?: number };
-         return c.json({ code, message }, code);
-       }
-     });
-     
-     export default app;
-     ```
-     
-   - **创建实体路由示例** (post.ts):
-     ```typescript
-     import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
-     import { CreateYourEntityDto, YourEntitySchema } from '@/server/modules/your-module/your-entity.entity';
-     import { ErrorSchema } from '@/server/utils/errorHandler';
-     import { AppDataSource } from '@/server/data-source';
-     import { YourEntityService } from '@/server/modules/your-module/your-entity.service';
-     import { AuthContext } from '@/server/types/context';
-     import { authMiddleware } from '@/server/middleware/auth.middleware';
-     
-     // 路由定义
-     const routeDef = createRoute({
-       method: 'post',
-       path: '/',
-       middleware: [authMiddleware],
-       request: {
-         body: {
-           content: {
-             'application/json': { schema: CreateYourEntityDto }
-           }
-         }
-       },
-       responses: {
-         200: {
-           description: '成功创建实体',
-           content: { 'application/json': { schema: YourEntitySchema } }
-         },
-         400: {
-           description: '请求参数错误',
-           content: { 'application/json': { schema: ErrorSchema } }
-         },
-         500: {
-           description: '服务器错误',
-           content: { 'application/json': { schema: ErrorSchema } }
-         }
-       }
-     });
-     
-     // 路由实现
-     const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
-       try {
-         const data = await c.req.json();
-         const service = new YourEntityService(AppDataSource);
-         const result = await service.create(data);
-         return c.json(result, 200);
-       } catch (error) {
-         const { code = 500, message = '创建实体失败' } = error as Error & { code?: number };
-         return c.json({ code, message }, code);
-       }
-     });
-     
-     export default app;
-     ```
-     
-   - **路由聚合示例** (index.ts):
-     ```typescript
-     import { OpenAPIHono } from '@hono/zod-openapi';
-     import listRoute from './get';
-     import createRoute from './post';
-     import getByIdRoute from './[id]/get';
-     import updateRoute from './[id]/put';
-     import deleteRoute from './[id]/delete';
-     
-     const app = new OpenAPIHono()
-       .route('/', listRoute)
-       .route('/', createRoute)
-       .route('/', getByIdRoute)
-       .route('/', updateRoute)
-       .route('/', deleteRoute);
-     
-     export default app;
-     ```
-
-### 5. **注册路由**
-   - 在`src/server/api.ts`中添加路由注册:
-     ```typescript
-     import yourEntityRoutes from '@/server/api/your-entity/index';
-     
-     // 注册路由
-     api.route('/api/v1/your-entities', yourEntityRoutes);
-     ```
+适用于包含复杂业务逻辑的实体,需要手动实现服务方法和路由处理的场景,如复杂业务规则、多表关联操作、特殊权限控制等。
 
-### 6. **创建客户端API**
-   - 在`src/client/api.ts`中添加客户端定义:
-     ```typescript
-     import { hc } from 'hono/client';
-     import { YourEntityRoutes } from '@/server/api';
-     
-     export const yourEntityClient = hc<YourEntityRoutes>('/api/v1', {
-       fetch: axiosFetch,
-     }).api.v1['your-entities'];
-     ```
+### 开发步骤概要
 
-### 7. **前端调用**
-   - 在页面组件(如`pages_users.tsx`)中:
-     - 使用`InferResponseType`提取响应类型
-     - 使用`InferRequestType`提取请求类型
-     - 示例:
-       ```typescript
-       type EntityResponse = InferResponseType<typeof entityClient.$get, 200>;
-       type CreateRequest = InferRequestType<typeof entityClient.$post>['json'];
-       ```
+1. **创建实体**:在`src/server/modules/[模块名]/[实体名].entity.ts`定义实体类和Zod Schema
+2. **注册实体**:在`src/server/data-source.ts`中注册新实体
+3. **创建自定义Service**:实现包含复杂业务逻辑的数据访问方法
+4. **创建自定义API路由**:手动实现CRUD路由及处理逻辑
+5. **注册路由**:在`src/server/api.ts`中注册路由
+6. **创建客户端API**:在`src/client/api.ts`中定义客户端调用方法
+7. **前端调用**:在页面组件中使用类型化API调用
+8. **注册路由和菜单**:
+   - 管理后台:在`src/client/admin/routes.tsx`中添加路由配置,在`src/client/admin/menu.tsx`中添加菜单配置
+   - 前台:在`src/client/home/routes.tsx`中添加路由配置
 
+详细流程请参见[自定义复杂CRUD开发流程规范](./11-custom-crud.md)
 ## 注意事项
 
 1. 实体Schema必须在实体文件中定义,路由中直接引用,不要重复定义

+ 33 - 0
.roo/rules/11-home-frontend.md

@@ -0,0 +1,33 @@
+# 前台Home页面开发流程规范 (Tailwind CSS版)
+
+## 适用场景
+
+前台用户界面开发,包括首页、登录页、注册页、会员中心等面向普通用户的页面实现流程。采用原生Tailwind CSS进行样式开发。不使用antd
+
+## 目录结构
+
+```
+src/client/home/
+├── index.tsx           # 入口文件
+├── routes.tsx          # 路由配置
+├── components/         # 共享组件
+│   ├── ErrorPage.tsx   # 错误页面
+│   ├── NotFoundPage.tsx # 404页面
+│   └── ProtectedRoute.tsx # 路由保护组件
+├── hooks/              # 自定义hooks
+│   └── AuthProvider.tsx # 认证上下文
+├── layouts/            # 布局组件
+│   └── MainLayout.tsx  # 主布局
+└── pages/              # 页面组件
+    ├── HomePage.tsx    # 首页
+    ├── LoginPage.tsx   # 登录页
+    ├── RegisterPage.tsx # 注册页
+    └── MemberPage.tsx  # 会员中心
+```
+
+### 1. **创建页面组件**
+   - 位置: `src/client/home/pages/[PageName].tsx`
+
+### 2. **注册路由配置**
+   - 位置: `src/client/home/routes.tsx`
+     ```

+ 100 - 0
.roo/rules/11-standard-crud.md

@@ -0,0 +1,100 @@
+# 标准通用CRUD开发流程规范
+
+## 适用场景
+
+适用于简单数据模型,无复杂业务逻辑,仅需基础CRUD操作的场景。采用`GenericCrudService`和`createCrudRoutes`快速生成接口。
+
+## 开发流程
+
+### 1. **创建实体**
+   - 位置: `src/server/modules/[模块名]/[实体名].entity.ts`
+   - 参考已有实体文件如`user.entity.ts`
+   - 注意: 必须包含Zod Schema定义
+
+### 2. **注册实体到数据源**
+   - 在`src/server/data-source.ts`中添加实体导入和注册:
+     ```typescript
+     // 实体类导入
+     import { YourEntity } from "./modules/[模块名]/[实体名].entity"
+     
+     export const AppDataSource = new DataSource({
+       // ...其他配置
+       entities: [
+         User, Role, YourEntity // 添加新实体到数组
+       ],
+       // ...其他配置
+     });
+     ```
+
+### 3. **创建Service**
+   - 位置: `src/server/modules/[模块名]/[实体名].service.ts`
+   - 继承`GenericCrudService`
+   - 通过构造函数注入DataSource
+   ```typescript
+   import { GenericCrudService } from '@/server/utils/generic-crud.service';
+   import { DataSource } from 'typeorm';
+   import { YourEntity } from './your-entity.entity';
+   
+   export class YourEntityService extends GenericCrudService<YourEntity> {
+     constructor(dataSource: DataSource) {
+       super(dataSource, YourEntity);
+     }
+   }
+   ```
+
+### 4. **创建API路由**
+   - 使用`createCrudRoutes`快速生成CRUD路由:
+     ```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,
+       searchFields: ['name', 'description'], // 可选,指定搜索字段
+       middleware: [authMiddleware] // 可选,添加中间件
+     });
+     
+     export default yourEntityRoutes;
+     ```
+
+### 5. **注册路由**
+   - 在`src/server/api.ts`中添加路由注册:
+     ```typescript
+     import yourEntityRoutes from '@/server/api/your-entity/index';
+     
+     // 注册路由
+     api.route('/api/v1/your-entities', yourEntityRoutes);
+     ```
+
+### 6. **创建客户端API**
+   - 在`src/client/api.ts`中添加客户端定义:
+     ```typescript
+     import { hc } from 'hono/client';
+     import { YourEntityRoutes } from '@/server/api';
+     
+     export const yourEntityClient = hc<YourEntityRoutes>('/api/v1', {
+       fetch: axiosFetch,
+     }).api.v1['your-entities'];
+     ```
+
+### 7. **前端调用**
+   - 在页面组件(如`Users.tsx`)中:
+     - 使用`InferResponseType`提取响应类型
+     - 使用`InferRequestType`提取请求类型
+     - 示例:
+       ```typescript
+       type EntityResponse = InferResponseType<typeof entityClient.$get, 200>;
+       type CreateRequest = InferRequestType<typeof entityClient.$post>['json'];
+       ```
+
+### 8. **注册管理后台路由和菜单**
+    - **注册路由**:在`src/client/admin/routes.tsx`中添加路由配置
+      
+    - **注册菜单**:在`src/client/admin/menu.tsx`中添加菜单配置
+

+ 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'],
+  // ...其他配置
+});

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

@@ -19,7 +19,7 @@ export const AppDataSource = new DataSource({
   password: process.env.DB_PASSWORD || "",
   database: process.env.DB_DATABASE || "d8dai",
   entities: [
-    User, Role, ClassroomData, DateNotes, StockData, StockXunlianCodes, SubmissionRecords
+    User, Role, ClassroomData, DateNotes, StockData, StockXunlianCodes, SubmissionRecords, 
   ],
   migrations: [],
   synchronize: process.env.DB_SYNCHRONIZE !== "false",

+ 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;
 };

+ 11 - 1
vite.config.ts

@@ -24,7 +24,17 @@ export default defineConfig({
       '@hono/node-server', 'jsonwebtoken', 'minio',
       'node-fetch', 'node-cron',
       '@alicloud/dysmsapi20170525', '@alicloud/openapi-client',
-      '@alicloud/tea-util'
+      '@alicloud/tea-util',
+      'react',
+      'react-dom',
+      'hono',
+      '@heroicons/react',
+      '@hono/node-server',
+      '@hono/react-renderer',
+      '@hono/swagger-ui',
+      '@hono/vite-dev-server',
+      '@hono/zod-openapi',
+      '@hono/zod-validator',
     ]
   },
   server:{