|
@@ -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`中添加菜单配置
|