# 通用CRUD实现规范

## 概述

通用CRUD模块提供标准化的增删改查功能实现，通过泛型机制支持快速创建实体的RESTful API接口。本规范定义了使用通用CRUD服务和路由的标准流程和最佳实践。

## 1. 通用CRUD服务 (GenericCrudService)

### 1.1 基础用法

通用CRUD服务提供基础的数据操作方法，所有实体服务类应继承此类：

```typescript
import { GenericCrudService } from '@/server/utils/generic-crud.service';
import { DataSource } from 'typeorm';
import { YourEntity } from '@/server/modules/your-module/your-entity.entity';

export class YourEntityService extends GenericCrudService<YourEntity> {
  constructor(dataSource: DataSource) {
    super(dataSource, YourEntity);
  }
  
  // 可以重写或扩展基础方法
  async customMethod() {
    // 自定义业务逻辑
  }
}
```

### 1.2 核心方法

| 方法 | 描述 | 参数 | 返回值 |
|------|------|------|--------|
| `getList` | 获取分页列表 | `page`, `pageSize`, `keyword`, `searchFields`, `where`, `relations`, `order` | `[T[], number]` |
| `getById` | 根据ID获取单个实体 | `id: number`, `relations?: string[]` | `T \| null` |
| `create` | 创建实体 | `data: DeepPartial<T>` | `T` |
| `update` | 更新实体 | `id: number`, `data: Partial<T>` | `T \| null` |
| `delete` | 删除实体 | `id: number` | `boolean` |
| `createQueryBuilder` | 创建查询构建器 | `alias?: string` | `QueryBuilder<T>` |

### 1.3 构造函数注入

必须通过构造函数注入`DataSource`，禁止直接使用全局实例：

```typescript
// 正确示例
constructor(private dataSource: DataSource) {
  super(dataSource, YourEntity);
}

// 错误示例
constructor() {
  super(AppDataSource, YourEntity); // 禁止使用全局AppDataSource
}
```

## 2. 通用CRUD路由 (createCrudRoutes)

### 2.1 基础用法

通过`createCrudRoutes`函数快速创建标准CRUD路由：

```typescript
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';

const yourEntityRoutes = createCrudRoutes({
  entity: YourEntity,
  createSchema: CreateYourEntityDto,
  updateSchema: UpdateYourEntityDto,
  getSchema: YourEntitySchema,
  listSchema: YourEntitySchema,
  searchFields: ['name', 'description'], // 可选，指定搜索字段
  relations: ['relatedEntity'], // 可选，指定关联查询关系（支持嵌套关联，如 ['relatedEntity.client']）
  middleware: [authMiddleware] // 可选，添加中间件
});

export default yourEntityRoutes;
```

### 2.2 配置选项 (CrudOptions)

| 参数 | 类型 | 描述 | 是否必需 |
|------|------|------|----------|
| `entity` | `new () => T` | 实体类构造函数 | 是 |
| `createSchema` | `z.ZodSchema` | 创建实体的Zod验证 schema | 是 |
| `updateSchema` | `z.ZodSchema` | 更新实体的Zod验证 schema | 是 |
| `getSchema` | `z.ZodSchema` | 获取单个实体的响应 schema | 是 |
| `listSchema` | `z.ZodSchema` | 获取列表的响应 schema | 是 |
| `searchFields` | `string[]` | 搜索字段数组，用于关键词搜索 | 否 |
| `relations` | `string[]` | 关联查询配置，指定需要关联查询的关系 | 否 |
| `middleware` | `any[]` | 应用于所有CRUD路由的中间件数组 | 否 |
| `relationFields` | `RelationFieldOptions` | 多对多关联字段配置，支持通过ID数组操作关联关系 | 否 |
| `readOnly` | `boolean` | 只读模式，只生成GET路由，默认false | 否 |

### 2.3 生成的路由

调用`createCrudRoutes`会自动生成以下标准RESTful路由：

| 方法 | 路径 | 描述 |
|------|------|------|
| GET | `/` | 获取实体列表（支持分页、搜索、排序、关联查询、关联字段筛选） |
| POST | `/` | 创建新实体 |
| GET | `/{id}` | 获取单个实体详情（支持关联查询） |
| PUT | `/{id}` | 更新实体 |
| DELETE | `/{id}` | 删除实体 |

#### 2.3.1 只读模式 (readOnly)

通过设置 `readOnly: true` 可以创建只读路由，仅包含读取操作：

| 方法 | 路径 | 描述 |
|------|------|------|
| GET | `/` | 获取实体列表（支持分页、搜索、排序、关联查询、关联字段筛选） |
| GET | `/{id}` | 获取单个实体详情（支持关联查询） |

**使用示例：**
```typescript
const readOnlyRoutes = createCrudRoutes({
  entity: Advertisement,
  createSchema: CreateAdvertisementDto,
  updateSchema: UpdateAdvertisementDto,
  getSchema: AdvertisementSchema,
  listSchema: AdvertisementSchema,
  readOnly: true, // 启用只读模式
  searchFields: ['title', 'description'],
  relations: ['imageFile'],
});
```

**适用场景：**
- 公开API接口（如游客访问的广告列表）
- 只读数据展示
- 需要限制修改操作的接口

### 2.4 路由注册

生成的路由需要在API入口文件中注册：

```typescript
// src/server/api.ts
import yourEntityRoutes from '@/server/api/your-entity/index';

// 注册路由
api.route('/api/v1/your-entities', yourEntityRoutes);
```

## 3. 实体类要求

使用通用CRUD模块的实体类必须满足以下要求：

1. 必须包含主键`id`字段，类型为数字
2. 必须定义配套的Zod schema：
   - 实体完整schema（用于响应）
   - 创建DTO schema（用于创建请求验证）
   - 更新DTO schema（用于更新请求验证）

示例：

```typescript
// your-entity.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany } from 'typeorm';
import { RelatedEntity } from './related-entity.entity';

@Entity('your_entity')
export class YourEntity {
  @PrimaryGeneratedColumn({ unsigned: true })
  id!: number;

  @Column({ name: 'name', type: 'varchar', length: 255 })
  name!: string;
  
  @ManyToOne(() => RelatedEntity, related => related.yourEntities)
  relatedEntity!: RelatedEntity;
  
  // 其他字段...
}

// your-entity.schema.ts
import { z } from '@hono/zod-openapi';
// Zod schemas
export const YourEntitySchema = z.object({
  id: z.number().int().positive().openapi({ description: '实体ID' }),
  name: z.string().max(255).openapi({ description: '名称', example: '示例名称' }),
  relatedEntity: RelatedEntitySchema, // 关联实体schema
  // 其他字段schema...
});

export const CreateYourEntityDto = z.object({
  name: z.string().max(255).openapi({ description: '名称', example: '示例名称' }),
  relatedEntityId: z.number().int().positive().openapi({ description: '关联实体ID', example: 1 }),
  // 其他创建字段schema...
});

export const UpdateYourEntityDto = z.object({
  name: z.string().max(255).optional().openapi({ description: '名称', example: '示例名称' }),
  relatedEntityId: z.number().int().positive().optional().openapi({ description: '关联实体ID', example: 1 }),
  // 其他更新字段schema...
});
```

### 2.5 多对多关联字段配置

#### 2.5.1 关联字段配置 (RelationFieldOptions)

新增 `relationFields` 配置选项，用于处理多对多关联字段：

```typescript
import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
import { PolicyNews } from '@/server/modules/silver-users/policy-news.entity';
import { PolicyNewsSchema, CreatePolicyNewsDto, UpdatePolicyNewsDto } from '@/server/modules/silver-users/policy-news.schema';
import { File } from '@/server/modules/files/file.entity';
import { authMiddleware } from '@/server/middleware/auth.middleware';

const policyNewsRoutes = createCrudRoutes({
  entity: PolicyNews,
  createSchema: CreatePolicyNewsDto,
  updateSchema: UpdatePolicyNewsDto,
  getSchema: PolicyNewsSchema,
  listSchema: PolicyNewsSchema,
  relations: ['files'],
  middleware: [authMiddleware],
  relationFields: {
    fileIds: {
      relationName: 'files',        // 实体中的关联属性名
      targetEntity: File,           // 关联的目标实体类
      joinTableName: 'policy_news_files' // 可选：中间表名
    }
  }
});
```

#### 2.5.2 RelationFieldOptions 类型定义

```typescript
interface RelationFieldOptions {
  [fieldName: string]: {
    relationName: string;      // 实体中的关联属性名
    targetEntity: new () => any; // 关联的目标实体类
    joinTableName?: string;    // 中间表名（可选）
  };
}
```

#### 2.5.3 使用示例

**实体定义：**
```typescript
@Entity('policy_news')
export class PolicyNews {
  @PrimaryGeneratedColumn({ unsigned: true })
  id!: number;

  @Column({ name: 'news_title', type: 'varchar', length: 255 })
  newsTitle!: string;

  // 关联文件的多对多关系
  @ManyToMany(() => File)
  @JoinTable({
    name: 'policy_news_files',
    joinColumn: { name: 'policy_news_id', referencedColumnName: 'id' },
    inverseJoinColumn: { name: 'file_id', referencedColumnName: 'id' }
  })
  files?: File[];
}
```

**请求格式：**
```typescript
// 创建政策资讯
POST /api/v1/policy-news
{
  "newsTitle": "新政策解读",
  "fileIds": [1, 2, 3]  // 关联文件ID数组
}

// 更新政策资讯
PUT /api/v1/policy-news/1
{
  "newsTitle": "更新后的政策解读",
  "fileIds": [2, 4, 5]  // 更新后的文件ID数组
}
```

#### 2.5.4 特性说明

- **自动处理**：系统会自动处理多对多关联的中间表操作
- **事务安全**：所有关联操作都在事务中执行
- **空数组支持**：传空数组 `[]` 表示清除所有关联
- **向后兼容**：不影响现有实体的使用方式
- **类型安全**：支持完整的TypeScript类型检查

## 4. 高级用法

### 4.1 关联字段搜索与筛选

通用CRUD模块支持通过关联关系进行字段搜索和筛选，使用点号表示法指定关联字段路径（格式：`relation.field` 或 `relation.nestedRelation.field`）：

```typescript
// 筛选关联字段示例
const filters = {
  'contract.client.name': 'John',           // 精确匹配：client.name = 'John'
  'contract.client.age': { gte: 18 },       // 范围查询：client.age >= 18
  'contract.client.department.name': '%IT%' // 模糊匹配：department.name LIKE '%IT%'
};

// API调用示例
GET /api/v1/entities?filters={"contract.client.name":"John","contract.client.age":{"gte":18}}
```

**别名生成规则：**
- 单级关联：`contract.client` → 别名：`contract_client`
- 嵌套关联：`contract.client.department` → 别名：`contract_client_department`
- 字段引用：`contract_client.name`, `contract_client_department.name`

**支持的搜索与筛选操作：**
- 关键词搜索：支持在 `searchFields` 中配置关联字段进行关键词搜索
- 精确匹配：`{ "relation.field": "value" }`
- 模糊匹配：`{ "relation.field": "%value%" }`
- 范围查询：`{ "relation.field": { "gte": min, "lte": max } }`
- IN查询：`{ "relation.field": [value1, value2, ...] }`

### 4.2 自定义中间件

可以为CRUD路由添加自定义中间件，如认证和权限控制：

```typescript
import { authMiddleware } from '@/server/middleware/auth.middleware';
import { permissionMiddleware } from '@/server/middleware/permission.middleware';

const yourEntityRoutes = createCrudRoutes({
  // ...其他配置
  middleware: [
    authMiddleware, 
    permissionMiddleware(['your_entity:read', 'your_entity:write'])
  ]
});
```

### 4.2 扩展路由

生成基础CRUD路由后，可以添加自定义路由：

```typescript
import { OpenAPIHono } from '@hono/zod-openapi';

const app = new OpenAPIHono()
  .route('/', yourEntityRoutes)
  .get('/custom-action', (c) => {
    // 自定义路由处理逻辑
    return c.json({ message: '自定义操作' });
  });

export default app;
```

### 4.3 重写服务方法

当通用CRUD服务的默认实现不满足需求时，可以重写相应方法：

```typescript
export class YourEntityService extends GenericCrudService<YourEntity> {
  // ...构造函数
  
  async getList(
    page: number = 1,
    pageSize: number = 10,
    keyword?: string,
    searchFields?: string[],
    where: Partial<YourEntity> = {},
    relations: string[] = [],
    order: { [P in keyof YourEntity]?: 'ASC' | 'DESC' } = {}
  ): Promise<[YourEntity[], number]> {
    // 添加自定义过滤条件
    where.isDeleted = 0; // 例如：默认过滤已删除数据
    
    return super.getList(page, pageSize, keyword, searchFields, where, relations, order);
  }
}
```

## 5. 错误处理

通用CRUD模块已内置标准错误处理机制：

1. 参数验证错误：返回400状态码和验证错误详情
2. 资源不存在：返回404状态码
3. 服务器错误：返回500状态码和错误消息

所有错误响应格式统一为：
```json
{
  "code": 错误代码,
  "message": "错误描述",
  "errors": 可选的详细错误信息
}
```

## 6. 最佳实践

1. **单一职责**：通用CRUD仅处理标准数据操作，复杂业务逻辑应在具体服务类中实现
2. **命名规范**：
   - 服务类：`[EntityName]Service`
   - 路由文件：遵循RESTful资源命名规范，使用复数形式
3. **权限控制**：始终为CRUD路由添加适当的认证和授权中间件
4. **数据验证**：确保Zod schema包含完整的验证规则和OpenAPI元数据
5. **搜索优化**：合理设置`searchFields`，避免在大表的文本字段上进行模糊搜索
6. **分页处理**：所有列表接口必须支持分页，避免返回大量数据
7. **关联查询**：使用`relations`配置时，避免过度关联导致性能问题
8. **事务管理**：复杂操作应使用事务确保数据一致性