generic-crud-standards.md 7.8 KB

通用 CRUD 规范

版本信息

版本 日期 描述 作者
1.0 2025-10-15 创建通用 CRUD 规范文档 Winston

概述

通用 CRUD(Create, Read, Update, Delete)系统是本项目的核心基础设施,提供类型安全、可扩展的数据操作服务。基于 TypeORM 和 Hono 框架构建,支持自动生成 OpenAPI 文档、关联查询、用户跟踪等高级功能。

设计原则

核心原则

  • 类型安全: 基于 TypeScript 的完整类型支持
  • 可扩展性: 支持自定义扩展和业务逻辑注入
  • 一致性: 统一的 API 设计和错误处理
  • 性能优化: 支持分页、缓存、关联查询优化
  • 安全性: 内置用户跟踪和权限控制

架构模式

  • 服务层: GenericCrudService 提供核心 CRUD 操作
  • 路由层: createCrudRoutes 自动生成 RESTful API 路由
  • 实体层: TypeORM 实体定义数据模型
  • 验证层: Zod schema 提供输入验证

核心组件

1. GenericCrudService

位置: packages/server/src/utils/generic-crud.service.ts

核心功能:

  • 分页列表查询(支持搜索、排序、复杂筛选)
  • 单个实体查询(支持关联关系)
  • 创建、更新、删除操作
  • 用户跟踪(创建人、更新人)
  • 关联字段处理

主要方法:

// 分页查询
async getList(
  page: number = 1,
  pageSize: number = 10,
  keyword?: string,
  searchFields?: string[],
  where?: Partial<T>,
  relations: string[] = [],
  order: { [P in keyof T]?: 'ASC' | 'DESC' } = {},
  filters?: { [key: string]: any }
): Promise<[T[], number]>

// 根据ID查询
async getById(id: number, relations: string[] = []): Promise<T | null>

// 创建实体
async create(data: DeepPartial<T>, userId?: string | number): Promise<T>

// 更新实体
async update(id: number, data: Partial<T>, userId?: string | number): Promise<T | null>

// 删除实体
async delete(id: number): Promise<boolean>

2. ConcreteCrudService

位置: packages/server/src/utils/concrete-crud.service.ts

用途: 简化 GenericCrudService 的实例化,自动注入数据源。

使用示例:

const userService = new ConcreteCrudService(User, {
  userTracking: { createdByField: 'createdBy', updatedByField: 'updatedBy' }
});

3. createCrudRoutes

位置: packages/server/src/utils/generic-crud.routes.ts

功能: 自动生成完整的 CRUD 路由,包括:

  • GET / - 分页列表
  • GET /{id} - 单个实体详情
  • POST / - 创建实体
  • PUT /{id} - 更新实体
  • DELETE /{id} - 删除实体

支持特性:

  • 自动 OpenAPI 文档生成
  • 输入验证(Zod schema)
  • 错误处理统一
  • 中间件支持
  • 只读模式

使用指南

1. 创建实体类

// user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';

@Entity()
export class User {
  /** 用户ID */
  @PrimaryGeneratedColumn()
  id: number;

  /** 用户名,唯一标识 */
  @Column({ unique: true, comment: '用户名,唯一标识' })
  username: string;

  /** 邮箱地址 */
  @Column({ nullable: true, comment: '邮箱地址' })
  email: string | null;

  /** 创建时间 */
  @CreateDateColumn({ comment: '创建时间' })
  createdAt: Date;

  /** 更新时间 */
  @UpdateDateColumn({ comment: '更新时间' })
  updatedAt: Date;
}

2. 创建 Zod Schema

// user.schema.ts
import { z } from '@hono/zod-openapi';

export const UserCreateSchema = z.object({
  username: z.string().min(3).max(50),
  email: z.string().email().optional().nullable(),
});

export const UserUpdateSchema = UserCreateSchema.partial();

export const UserGetSchema = z.object({
  id: z.number(),
  username: z.string(),
  email: z.string().email().nullable(),
  createdAt: z.string().datetime(),
  updatedAt: z.string().datetime(),
});

export const UserListSchema = UserGetSchema;

3. 注册 CRUD 路由

// api/users/index.ts
import { createCrudRoutes } from '../../utils/generic-crud.routes';
import { User } from './user.entity';
import {
  UserCreateSchema,
  UserUpdateSchema,
  UserGetSchema,
  UserListSchema
} from './user.schema';

export const userRoutes = createCrudRoutes({
  entity: User,
  createSchema: UserCreateSchema,
  updateSchema: UserUpdateSchema,
  getSchema: UserGetSchema,
  listSchema: UserListSchema,
  searchFields: ['username', 'email'],
  relations: ['roles'],
  middleware: [authMiddleware],
  userTracking: {
    createdByField: 'createdBy',
    updatedByField: 'updatedBy'
  }
});

4. 在 API 中注册路由

// api/index.ts
import { userRoutes } from './users';

const app = new OpenAPIHono();
app.route('/users', userRoutes);

高级功能

关联字段处理

支持多对多、一对多等关联关系的自动处理:

const roleRoutes = createCrudRoutes({
  entity: Role,
  // ... schemas
  relationFields: {
    permissions: {
      relationName: 'permissions',
      targetEntity: Permission
    }
  }
});

复杂筛选

支持多种筛选方式:

// 精确匹配
filters: '{"status": 1}'

// 范围查询
filters: '{"createdAt": {"gte": "2024-01-01", "lte": "2024-12-31"}}'

// IN 查询
filters: '{"status": [1, 2, 3]}'

// 模糊匹配
filters: '{"name": "%keyword%"}'

关联字段搜索

支持嵌套关联字段的搜索:

// 搜索用户关联的角色名称
searchFields: ['username', 'roles.name']

// 搜索嵌套关联
searchFields: ['contract.client.name']

最佳实践

1. 实体设计

  • 使用 TypeORM 装饰器定义字段
  • 为所有字段添加 comment 配置,说明字段用途
  • 包含 createdAtupdatedAt 时间戳
  • 考虑软删除需求

2. Schema 设计

  • 创建和更新使用不同的 schema
  • 响应 schema 应包含完整字段
  • 使用 .optional().nullable() 明确字段可选性

3. 性能优化

  • 合理设置分页大小(默认10条)
  • 为常用查询字段添加索引
  • 使用关联查询时注意 N+1 问题
  • 考虑缓存策略

4. 安全性

  • 使用用户跟踪记录操作人
  • 实现适当的权限控制
  • 验证所有输入数据
  • 避免敏感信息暴露

扩展和自定义

自定义业务逻辑

class CustomUserService extends ConcreteCrudService<User> {
  async createWithValidation(data: UserCreateDto): Promise<User> {
    // 自定义验证逻辑
    await this.validateUniqueUsername(data.username);

    // 调用父类方法
    return super.create(data);
  }

  private async validateUniqueUsername(username: string): Promise<void> {
    const existing = await this.repository.findOne({ where: { username } });
    if (existing) {
      throw new Error('用户名已存在');
    }
  }
}

自定义路由

const customRoutes = new OpenAPIHono();

// 使用通用 CRUD 路由
customRoutes.route('/', createCrudRoutes(crudOptions));

// 添加自定义路由
customRoutes.post('/bulk-create', async (c) => {
  // 自定义批量创建逻辑
});

常见问题解答

Q: 如何处理复杂的业务逻辑?

A: 继承 ConcreteCrudService 并添加自定义方法,或创建独立的业务服务类。

Q: 如何实现软删除?

A: 在实体中添加 deletedAt 字段,重写 delete 方法实现软删除逻辑。

Q: 如何优化关联查询性能?

A: 使用 relations 参数指定需要的关联关系,避免不必要的关联加载。

Q: 如何处理文件上传等非标准操作?

A: 创建独立的文件管理服务,或扩展 CRUD 服务添加文件处理逻辑。

相关文档


文档状态: 正式版 下次评审: 2025-11-15