|
|
@@ -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客户端(如需要)
|