---
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 '@d8d/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 '@d8d/server/modules/your-module/your-entity.schema';
import { parseWithAwait } from '@/server/utils/parseWithAwait';
import { ErrorSchema } from '@/server/utils/errorHandler';
import { AppDataSource } from '@/server/data-source';
import { YourEntityService } from '@d8d/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);
    }
    
    // 使用 parseWithAwait 处理响应数据
    const validatedResult = await parseWithAwait(YourEntitySchema, result);
    return c.json(validatedResult, 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 '@d8d/server/modules/your-module/your-entity.service';
import { YourEntitySchema } from '@d8d/server/modules/your-module/your-entity.schema';
import { parseWithAwait } from '@/server/utils/parseWithAwait';
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);
    
    // 使用 parseWithAwait 处理数据格式
    const validatedData = await parseWithAwait(z.array(YourEntitySchema), data);
    
    if (format === 'csv') {
      const csv = convertToCSV(validatedData);
      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 '@d8d/server/modules/your-module/your-entity.entity';
import { YourEntitySchema, CreateYourEntityDto, UpdateYourEntityDto } from '@d8d/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('/batch', batchDeleteRoute)        // 批量操作路由
  .route('/:id/status', statusUpdateRoute)  // 状态更新路由
  .route('/export', exportRoute)          // 导出路由
  .route('/', yourEntityRoutes);               // 基础CRUD路由必需放最后，不然自定义路由会覆盖掉基础CRUD路由

// 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`
- **自定义方法**：避免在路径中使用动词，用名词+参数表示

## 扩展路由 Schema 文件规范

### Schema 文件位置
所有扩展路由的 Zod Schema 定义必须遵循以下文件位置规范：

```
src/server/modules/[模块名]/
├── [实体名].entity.ts      # 实体定义
├── [实体名].schema.ts      # 实体Schema定义（已存在）
└── schemas/                # 扩展路由专用Schema目录（新增）
    ├── batch/              # 批量操作Schema
    │   └── delete.schema.ts
    ├── [id]/               # 单条记录操作Schema
    │   ├── status.schema.ts
    │   ├── toggle.schema.ts
    │   └── audit.schema.ts
    ├── export.schema.ts    # 导出操作Schema
    ├── import.schema.ts    # 导入操作Schema
    └── stats.schema.ts     # 统计操作Schema
```

### Schema 文件命名规范
- **文件名**：`[操作名].schema.ts`
- **导出**：必须包含完整的请求/响应Schema定义
- **引用**：在扩展路由文件中直接引用对应的Schema文件

### Schema 文件示例

#### 批量删除Schema - `schemas/batch/delete.schema.ts`
```typescript
import { z } from '@hono/zod-openapi';

// 请求Schema
export const BatchDeleteRequestSchema = z.object({
  ids: z.array(z.number().int().positive()).openapi({
    description: '要删除的ID列表',
    example: [1, 2, 3]
  })
});

// 响应Schema
export const BatchDeleteResponseSchema = z.object({
  deletedCount: z.number().openapi({
    example: 3,
    description: '删除的记录数'
  })
});

// 类型定义
export type BatchDeleteRequest = z.infer<typeof BatchDeleteRequestSchema>;
export type BatchDeleteResponse = z.infer<typeof BatchDeleteResponseSchema>;
```

#### 状态更新Schema - `schemas/[id]/status.schema.ts`
```typescript
import { z } from '@hono/zod-openapi';
import { YourEntitySchema } from '../../your-entity.schema';

// 路径参数Schema
export const StatusUpdateParamsSchema = z.object({
  id: z.string().openapi({
    param: { name: 'id', in: 'path' },
    example: '1',
    description: '记录ID'
  })
});

// 请求体Schema
export const StatusUpdateBodySchema = z.object({
  status: z.number().openapi({
    example: 1,
    description: '新状态值'
  })
});

// 响应Schema（复用实体Schema）
export const StatusUpdateResponseSchema = YourEntitySchema;

// 类型定义
export type StatusUpdateParams = z.infer<typeof StatusUpdateParamsSchema>;
export type StatusUpdateBody = z.infer<typeof StatusUpdateBodySchema>;
export type StatusUpdateResponse = z.infer<typeof StatusUpdateResponseSchema>;
```

### 在扩展路由中的使用方式

#### 引用Schema文件
```typescript
// src/server/api/your-entity/batch/delete.ts
import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
import { BatchDeleteRequestSchema, BatchDeleteResponseSchema } from '@d8d/server/modules/your-module/schemas/batch/delete.schema';
// ...其他导入
```

### 最佳实践
1. **Schema复用**：尽量复用实体已有的Schema定义
2. **类型安全**：所有Schema必须包含完整的OpenAPI元数据
3. **模块化**：每个扩展路由对应独立的Schema文件
4. **命名一致**：Schema文件名与路由功能保持一致
5. **导出规范**：同时导出Schema和对应的TypeScript类型

## 注意事项

1. **模块化设计**：每个扩展功能单独一个文件，保持代码清晰
2. **路径一致性**：扩展路由的路径要与聚合时的路径匹配
3. **类型安全**：为所有自定义路由定义完整的OpenAPI schema
4. **错误处理**：统一使用标准错误响应格式
5. **权限控制**：为敏感操作添加适当的中间件
6. **性能考虑**：批量操作要考虑事务处理和性能优化
7. **Schema管理**：所有Schema必须放在指定的schemas目录下
8. **版本兼容**：Schema变更要保持向后兼容性
9. **数据验证**：所有查询类路由必须使用 `parseWithAwait` 处理响应数据，确保类型安全

## parseWithAwait 使用规范

### 概述
`parseWithAwait` 是通用CRUD模块提供的数据验证工具，用于确保返回数据的类型安全，支持异步验证和转换。

### 使用场景
所有涉及数据查询和返回的扩展路由都应使用 `parseWithAwait` 处理响应数据。

### 基本用法
```typescript
import { parseWithAwait } from '@/server/utils/parseWithAwait';

// 验证单个实体
const validatedEntity = await parseWithAwait(YourEntitySchema, entityData);

// 验证实体数组
const validatedEntities = await parseWithAwait(z.array(YourEntitySchema), entitiesData);
```

### 集成示例
```typescript
// 在扩展路由中使用
const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
  try {
    const data = await yourService.getList();
    
    // 使用 parseWithAwait 确保数据格式正确
    const validatedData = await parseWithAwait(z.array(YourEntitySchema), data);
    
    return c.json({
      data: validatedData,
      pagination: { total, current: page, pageSize }
    }, 200);
  } catch (error) {
    return c.json({ code: 500, message: '获取数据失败' }, 500);
  }
});
```

### 优势
- **类型安全**：确保返回数据完全符合Zod schema定义
- **异步支持**：支持异步验证和转换操作
- **错误处理**：提供详细的验证错误信息
- **性能优化**：避免运行时类型错误
- **向后兼容**：与现有代码完全兼容

### 最佳实践
1. **所有查询路由**：GET请求返回数据前必须使用 `parseWithAwait`
2. **列表查询**：使用 `z.array(EntitySchema)` 格式验证数组
3. **单条查询**：直接使用实体Schema验证单个对象
4. **错误处理**：捕获并适当处理验证错误

## 验证步骤

1. 创建独立的扩展路由文件
2. 实现各路由的业务逻辑
3. 在 index.ts 中聚合所有路由
4. 测试所有API端点
5. 验证RPC客户端能正确识别所有路由
6. 更新前端API客户端（如需要）