Browse Source

📝 docs(commands): add generic crud extend route development guide

- document the development process for extending generic CRUD routes
- provide examples for batch delete, status update, and data export routes
- explain file structure, route aggregation, and common extension scenarios
- include naming conventions and validation steps for custom routes
yourname 4 months ago
parent
commit
f8c2891455
1 changed files with 340 additions and 0 deletions
  1. 340 0
      .roo/commands/generic-crud-extend.md

+ 340 - 0
.roo/commands/generic-crud-extend.md

@@ -0,0 +1,340 @@
+---
+description: "通用curd扩展路由开发指令"
+---
+
+# 通用CRUD扩展路由开发指令
+
+本指令基于通用CRUD规范,指导如何为已存在的通用CRUD路由添加自定义扩展路由,采用模块化方式保持代码清晰。
+
+## 适用场景
+
+当通用CRUD提供的标准路由(GET /, POST /, GET /{id}, PUT /{id}, DELETE /{id})无法满足业务需求时,需要添加自定义业务路由。
+
+## 开发流程
+
+### 1. **定位现有通用CRUD路由文件**
+找到对应的通用CRUD路由文件,通常位于:
+- `src/server/api/[实体名]/index.ts`
+
+### 2. **创建扩展路由文件**
+为每个扩展功能创建单独的路由文件:
+
+```
+src/server/api/your-entity/
+├── index.ts            # 聚合路由(已存在)
+├── batch/              # 新增 - 批量操作
+│   └── delete.ts       # 批量删除
+├── [id]/               # 新增 - 单条记录扩展操作
+│   ├── status.ts       # 状态更新
+│   ├── toggle.ts       # 状态切换
+│   └── audit.ts        # 审核操作
+├── export.ts           # 新增 - 数据导出
+├── import.ts           # 新增 - 数据导入
+├── stats.ts            # 新增 - 统计信息
+└── upload.ts           # 新增 - 文件上传
+```
+
+### 3. **创建独立扩展路由文件**
+
+#### 3.1 批量删除路由 - `batch/delete.ts`
+```typescript
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+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: 'delete',
+  path: '/',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': {
+          schema: z.object({
+            ids: z.array(z.number().int().positive()).openapi({
+              description: '要删除的ID列表',
+              example: [1, 2, 3]
+            })
+          })
+        }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '批量删除成功',
+      content: {
+        'application/json': {
+          schema: z.object({
+            deletedCount: z.number().openapi({ example: 3, description: '删除的记录数' })
+          })
+        }
+      }
+    },
+    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 { ids } = await c.req.json();
+    const service = new YourEntityService(AppDataSource);
+    
+    let deletedCount = 0;
+    for (const id of ids) {
+      const result = await service.delete(id);
+      if (result) deletedCount++;
+    }
+    
+    return c.json({ deletedCount }, 200);
+  } catch (error) {
+    return c.json({ code: 500, message: '批量删除失败' }, 500);
+  }
+});
+
+export default app;
+```
+
+#### 3.2 状态更新路由 - `[id]/status.ts`
+```typescript
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { YourEntitySchema } from '@/server/modules/your-module/your-entity.schema';
+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: 'patch',
+  path: '/',
+  middleware: [authMiddleware],
+  request: {
+    params: z.object({
+      id: z.string().openapi({
+        param: { name: 'id', in: 'path' },
+        example: '1',
+        description: '记录ID'
+      })
+    }),
+    body: {
+      content: {
+        'application/json': {
+          schema: z.object({
+            status: z.number().openapi({ example: 1, description: '新状态值' })
+          })
+        }
+      }
+    }
+  },
+  responses: {
+    200: { description: '状态更新成功', content: { 'application/json': { schema: YourEntitySchema } } },
+    404: { description: '记录不存在', content: { 'application/json': { schema: ErrorSchema } } },
+    500: { description: '服务器错误', content: { 'application/json': { schema: ErrorSchema } } }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const { id } = c.req.valid('param');
+    const { status } = await c.req.json();
+    const service = new YourEntityService(AppDataSource);
+    
+    const result = await service.update(Number(id), { status });
+    if (!result) {
+      return c.json({ code: 404, message: '记录不存在' }, 404);
+    }
+    
+    return c.json(result, 200);
+  } catch (error) {
+    return c.json({ code: 500, message: '状态更新失败' }, 500);
+  }
+});
+
+export default app;
+```
+
+#### 3.3 数据导出路由 - `export.ts`
+```typescript
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+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: 'get',
+  path: '/',
+  middleware: [authMiddleware],
+  request: {
+    query: z.object({
+      format: z.enum(['csv', 'xlsx']).default('csv').openapi({
+        description: '导出格式',
+        example: 'csv'
+      }),
+      keyword: z.string().optional().openapi({
+        description: '搜索关键词',
+        example: '测试'
+      }),
+      filters: z.string().optional().openapi({
+        description: '筛选条件(JSON字符串)',
+        example: '{"status":1}'
+      })
+    })
+  },
+  responses: {
+    200: {
+      description: '导出文件',
+      content: {
+        'text/csv': { schema: z.string() },
+        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': { schema: z.any() }
+      }
+    }
+  }
+});
+
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const { format, keyword, filters } = c.req.valid('query');
+    const service = new YourEntityService(AppDataSource);
+    
+    let filterObj = {};
+    if (filters) {
+      try {
+        filterObj = JSON.parse(filters);
+      } catch (e) {
+        return c.json({ code: 400, message: '筛选条件格式错误' }, 400);
+      }
+    }
+    
+    const [data] = await service.getList(1, 1000, keyword, undefined, filterObj);
+    
+    if (format === 'csv') {
+      const csv = convertToCSV(data);
+      return new Response(csv, {
+        headers: {
+          'Content-Type': 'text/csv',
+          'Content-Disposition': 'attachment; filename="export.csv"'
+        }
+      });
+    }
+    
+    return c.json({ code: 400, message: '不支持的导出格式' }, 400);
+  } catch (error) {
+    return c.json({ code: 500, message: '导出失败' }, 500);
+  }
+});
+
+export default app;
+
+function convertToCSV(data: any[]): string {
+  if (!data || data.length === 0) return '';
+  
+  const headers = Object.keys(data[0]);
+  const csvHeaders = headers.join(',');
+  const csvRows = data.map(row => 
+    headers.map(header => {
+      const value = row[header];
+      return typeof value === 'string' && value.includes(',') ? `"${value}"` : value;
+    }).join(',')
+  );
+  
+  return [csvHeaders, ...csvRows].join('\n');
+}
+```
+
+### 4. **聚合所有路由**
+在 `index.ts` 中聚合基础CRUD路由和所有扩展路由:
+
+```typescript
+// src/server/api/your-entity/index.ts
+import { OpenAPIHono } from '@hono/zod-openapi';
+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.schema';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+// 导入基础路由和各扩展路由
+import batchDeleteRoute from './batch/delete';
+import statusUpdateRoute from './[id]/status';
+import exportRoute from './export';
+
+// 1. 创建基础CRUD路由
+const yourEntityRoutes = createCrudRoutes({
+  entity: YourEntity,
+  createSchema: CreateYourEntityDto,
+  updateSchema: UpdateYourEntityDto,
+  getSchema: YourEntitySchema,
+  listSchema: YourEntitySchema,
+  searchFields: ['name', 'description'],
+  middleware: [authMiddleware]
+});
+
+// 2. 聚合所有路由(保持链式)
+const app = new OpenAPIHono()
+  .route('/', yourEntityRoutes)              // 基础CRUD路由
+  .route('/batch', batchDeleteRoute)        // 批量操作路由
+  .route('/:id/status', statusUpdateRoute)  // 状态更新路由
+  .route('/export', exportRoute);           // 导出路由
+
+// 3. 导出聚合后的路由
+export default app;
+```
+
+
+## 常见扩展场景
+
+### 1. **批量操作**
+- 批量删除:`DELETE /your-entity/batch`
+- 批量更新状态:`PATCH /your-entity/batch/status`
+- 批量导入:`POST /your-entity/import`
+
+### 2. **数据统计**
+- 获取统计信息:`GET /your-entity/stats`
+- 获取图表数据:`GET /your-entity/chart-data`
+
+### 3. **文件相关**
+- 上传文件:`POST /your-entity/upload`
+- 下载文件:`GET /your-entity/download/{id}`
+- 导出数据:`GET /your-entity/export`
+
+### 4. **状态管理**
+- 状态切换:`PATCH /your-entity/{id}/toggle-status`
+- 审核操作:`POST /your-entity/{id}/audit`
+
+### 5. **关联操作**
+- 获取关联数据:`GET /your-entity/{id}/related-data`
+- 更新关联关系:`PUT /your-entity/{id}/relations`
+
+## 命名规范
+
+- **路径命名**:使用RESTful风格,动词用HTTP方法表示
+- **批量操作**:使用复数名词,如 `/batch`, `/import`, `/export`
+- **状态变更**:使用 PATCH 方法,路径中体现操作,如 `/status`, `/toggle`
+- **自定义方法**:避免在路径中使用动词,用名词+参数表示
+
+## 注意事项
+
+1. **模块化设计**:每个扩展功能单独一个文件,保持代码清晰
+2. **路径一致性**:扩展路由的路径要与聚合时的路径匹配
+3. **类型安全**:为所有自定义路由定义完整的OpenAPI schema
+4. **错误处理**:统一使用标准错误响应格式
+5. **权限控制**:为敏感操作添加适当的中间件
+6. **性能考虑**:批量操作要考虑事务处理和性能优化
+
+## 验证步骤
+
+1. 创建独立的扩展路由文件
+2. 实现各路由的业务逻辑
+3. 在 index.ts 中聚合所有路由
+4. 测试所有API端点
+5. 验证RPC客户端能正确识别所有路由
+6. 更新前端API客户端(如需要)