# 后端模块包规范 ## 版本信息 | 版本 | 日期 | 描述 | 作者 | |------|------|------|------| | 2.0 | 2025-12-26 | 基于实际实现重写,修正不准确的描述 | James (Claude Code) | | 1.0 | 2025-12-02 | 基于史诗007系列移植经验创建 | Claude Code | ## 概述 本文档定义了后端模块包的设计、开发和集成规范,基于项目实际的模块包实现经验总结。 ### 包组织方式 项目采用两种包组织方式: 1. **allin-packages模式**: 每个业务模块独立成包 - 目录: `allin-packages/{module-name}-module/` - 示例: `@d8d/allin-channel-module`, `@d8d/allin-platform-module` 2. **core-module聚合模式**: 将多个相关模块打包在一起 - 目录: `packages/core-module/{module-name}/` - 示例: `@d8d/core-module/auth-module`, `@d8d/core-module/user-module` ## 1. 包结构规范 ### 1.1 目录结构 #### allin-packages模式 ``` allin-packages/{module-name}-module/ ├── package.json ├── tsconfig.json ├── vitest.config.ts ├── src/ │ ├── entities/ │ │ └── {entity-name}.entity.ts │ ├── services/ │ │ └── {service-name}.service.ts │ ├── routes/ │ │ ├── {module}-custom.routes.ts │ │ ├── {module}-crud.routes.ts │ │ ├── {module}.routes.ts │ │ └── index.ts │ ├── schemas/ │ │ └── {schema-name}.schema.ts │ ├── types/ │ │ └── index.ts │ └── index.ts └── tests/ ├── integration/ │ └── {module}.integration.test.ts └── utils/ └── test-data-factory.ts ``` #### core-module聚合模式 ``` packages/core-module/ ├── auth-module/ │ ├── src/ │ │ ├── entities/ │ │ ├── services/ │ │ ├── routes/ │ │ └── schemas/ │ └── tests/ ├── user-module/ │ └── ... └── file-module/ └── ... ``` ### 1.2 包命名规范 | 包类型 | 命名模式 | 示例 | |--------|----------|------| | allin业务包 | `@d8d/allin-{name}-module` | `@d8d/allin-channel-module` | | core子模块 | `@d8d/core-module` | 内部按路径区分 | ### 1.3 workspace配置 ```yaml # pnpm-workspace.yaml packages: - 'allin-packages/*' - 'packages/*' ``` ## 2. 实体设计规范 ### 2.1 Entity定义 **实际实现模式**: ```typescript import { Entity, Column, PrimaryGeneratedColumn, Index } from 'typeorm'; @Entity('channel_info') export class Channel { @PrimaryGeneratedColumn({ name: 'channel_id', type: 'int', unsigned: true, comment: '渠道ID' }) id!: number; @Column({ name: 'channel_name', type: 'varchar', length: 100, nullable: false, comment: '渠道名称' }) @Index('idx_channel_name', { unique: true }) channelName!: string; @Column({ name: 'status', type: 'int', default: 1, comment: '状态:1-正常,0-禁用' }) status!: number; @Column({ name: 'create_time', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', comment: '创建时间' }) createTime!: Date; @Column({ name: 'update_time', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', onUpdate: 'CURRENT_TIMESTAMP', comment: '更新时间' }) updateTime!: Date; } ``` ### 2.2 关键要点 - **完整的列定义**: 包含 `type`, `length`, `nullable`, `comment` 等属性 - **索引装饰器**: 使用 `@Index` 定义唯一索引 - **时间戳字段**: 使用 `timestamp` 类型而非 `datetime` - **默认值**: 使用 `default: () => 'CURRENT_TIMESTAMP'` ### 2.3 关联关系 **多对一关系**: ```typescript @ManyToOne(() => Platform, { eager: false }) @JoinColumn({ name: 'platform_id', referencedColumnName: 'id' }) platform!: Platform; ``` **避免循环依赖**: ```typescript // 使用字符串引用避免循环依赖 @ManyToOne('DisabledPerson', { nullable: true }) @JoinColumn({ name: 'person_id', referencedColumnName: 'id' }) person!: import('@d8d/allin-disability-module/entities').DisabledPerson | null; ``` ### 2.4 文件关联模式 ```typescript @Column({ name: 'avatar_file_id', type: 'int', unsigned: true, nullable: true }) avatarFileId!: number | null; @ManyToOne(() => File) @JoinColumn({ name: 'avatar_file_id', referencedColumnName: 'id' }) avatarFile!: File | null; ``` ## 3. 服务层规范 ### 3.1 GenericCrudService继承 ```typescript import { GenericCrudService } from '@d8d/shared-crud'; import { DataSource } from 'typeorm'; import { Channel } from '../entities/channel.entity'; export class ChannelService extends GenericCrudService { constructor(dataSource: DataSource) { super(dataSource, Channel); } } ``` ### 3.2 方法覆盖模式(使用override) **实际实现**: ```typescript export class ChannelService extends GenericCrudService { constructor(dataSource: DataSource) { super(dataSource, Channel); } /** * 创建渠道 - 覆盖父类方法,添加名称唯一性检查 */ override async create(data: Partial, userId?: string | number): Promise { // 检查渠道名称是否已存在(只检查正常状态的渠道) if (data.channelName) { const existingChannel = await this.repository.findOne({ where: { channelName: data.channelName, status: 1 } }); if (existingChannel) { throw new Error('渠道名称已存在'); } } // 设置默认值 const channelData = { contactPerson: '', contactPhone: '', channelType: '', description: '', ...data, status: 1, createTime: new Date(), updateTime: new Date() }; return super.create(channelData, userId); } /** * 更新渠道 - 覆盖父类方法,添加存在性和名称重复检查 */ override async update(id: number, data: Partial, userId?: string | number): Promise { // 检查渠道是否存在 const channel = await this.repository.findOne({ where: { id, status: 1 } }); if (!channel) { throw new Error('渠道不存在'); } // 检查名称是否与其他渠道重复 if (data.channelName && data.channelName !== channel.channelName) { const existingChannel = await this.repository.findOne({ where: { channelName: data.channelName, id: Not(id), status: 1 } }); if (existingChannel) { throw new Error('渠道名称已存在'); } } const updateData = { ...data, updateTime: new Date() }; return super.update(id, updateData, userId); } /** * 删除渠道 - 覆盖父类方法,改为软删除 */ override async delete(id: number, userId?: string | number): Promise { // 软删除:设置status为0 const result = await this.repository.update({ id }, { status: 0 }); return result.affected === 1; } } ``` ### 3.3 关键要点 - **使用 `override` 关键字**: 明确标识覆盖父类方法 - **软删除逻辑**: 使用 `status` 字段而非物理删除 - **业务逻辑检查**: 在调用父类方法前进行验证 - **设置默认值**: 为可选字段设置合理的默认值 - **时间戳管理**: 自动设置 `createTime` 和 `updateTime` ## 4. 路由层规范 ### 4.1 使用OpenAPIHono **实际实现**: ```typescript import { OpenAPIHono } from '@hono/zod-openapi'; import { AuthContext } from '@d8d/shared-types'; import channelCustomRoutes from './channel-custom.routes'; import { channelCrudRoutes } from './channel-crud.routes'; // 创建路由实例 - 聚合自定义路由和CRUD路由 const channelRoutes = new OpenAPIHono() .route('/', channelCustomRoutes) .route('/', channelCrudRoutes); export { channelRoutes }; export default channelRoutes; ``` ### 4.2 导出模式 **routes/index.ts**: ```typescript export * from './channel.routes'; export * from './channel-custom.routes'; export * from './channel-crud.routes'; ``` ### 4.3 自定义路由响应规范 **重要**: 自定义路由(非CRUD路由)在返回响应前**必须使用 `parseWithAwait`** 验证和转换数据。 **实际实现**: ```typescript import { parseWithAwait, createZodErrorResponse } from '@d8d/shared-utils'; import { z } from '@hono/zod-openapi'; import { ChannelSchema } from '../schemas/channel.schema'; // 自定义路由 channelCustomRoutes.get('/statistics/:id', async (c) => { try { const id = c.req.param('id'); const channelService = new ChannelService(AppDataSource); const result = await channelService.getStatistics(Number(id)); // ✅ 必须:使用 parseWithAwait 验证和转换响应数据 const validatedResult = await parseWithAwait(ChannelSchema, result); return c.json(validatedResult, 200); } catch (error) { if (error instanceof z.ZodError) { // ✅ 推荐:使用 createZodErrorResponse 处理Zod验证错误 return c.json(createZodErrorResponse(error), 400); } return c.json({ code: 500, message: error.message }, 500); } }); ``` **数组响应处理**: ```typescript // 处理数组数据 const validatedData = await Promise.all( result.data.map(item => parseWithAwait(ChannelSchema, item)) ); return c.json({ data: validatedData, total: result.total }, 200); ``` **关键要点**: - **必须使用 `parseWithAwait`**: 所有自定义路由返回前必须使用 `parseWithAwait` 验证数据 - **捕获 ZodError**: 在catch块中处理 `z.ZodError` 异常 - **使用 `createZodErrorResponse`**: 提供统一的错误响应格式 - **数组使用 `Promise.all`**: 批量验证数组数据 ### 4.4 关键要点 - **使用 `OpenAPIHono`**: 而非普通的 `Hono` - **使用 `AuthContext` 泛型**: 提供类型安全的认证上下文 - **路由聚合**: 分别定义自定义路由和CRUD路由,然后聚合 - **不设置 `basePath`**: 在聚合路由时处理路径 - **自定义路由必须使用 `parseWithAwait`**: 验证响应数据符合Schema定义 ## 5. Schema规范 ### 5.1 使用Zod + OpenAPI装饰器 **实际实现**: ```typescript import { z } from '@hono/zod-openapi'; // 渠道实体Schema export const ChannelSchema = z.object({ id: z.number().int().positive().openapi({ description: '渠道ID', example: 1 }), channelName: z.string().max(100).openapi({ description: '渠道名称', example: '微信小程序' }), channelType: z.string().max(50).openapi({ description: '渠道类型', example: '小程序' }), contactPerson: z.string().max(50).openapi({ description: '联系人', example: '张三' }), contactPhone: z.string().max(20).openapi({ description: '联系电话', example: '13800138000' }), description: z.string().nullable().optional().openapi({ description: '描述', example: '微信小程序渠道' }), status: z.number().int().min(0).max(1).default(1).openapi({ description: '状态:1-正常,0-禁用', example: 1 }), createTime: z.coerce.date().openapi({ description: '创建时间', example: '2024-01-01T00:00:00Z' }), updateTime: z.coerce.date().openapi({ description: '更新时间', example: '2024-01-01T00:00:00Z' }) }); // 创建渠道DTO export const CreateChannelSchema = z.object({ channelName: z.string().min(1).max(100).openapi({ description: '渠道名称', example: '微信小程序' }), channelType: z.string().max(50).optional().openapi({ description: '渠道类型', example: '小程序' }), contactPerson: z.string().max(50).optional().openapi({ description: '联系人', example: '张三' }), contactPhone: z.string().max(20).optional().openapi({ description: '联系电话', example: '13800138000' }), description: z.string().optional().openapi({ description: '描述', example: '微信小程序渠道' }) }); // 更新渠道DTO(所有字段可选) export const UpdateChannelSchema = CreateChannelSchema.partial(); ``` ### 5.2 Zod 4.0 coerce使用说明 **重要**: Zod 4.0 中,`z.coerce.date()` 和 `z.coerce.number()` 需要添加泛型参数来指定类型。 ```typescript // ✅ 正确:Zod 4.0 - 使用泛型指定类型 z.coerce.date() // 转换为Date类型 z.coerce.number() // 转换为number类型 // ❌ 错误:不指定泛型(Zod 4.0中类型推断可能不准确) z.coerce.date() z.coerce.number() ``` ### 5.3 类型使用说明 **重要**: Schema只用于请求参数验证和响应定义,**不需要导出推断的TypeScript类型**。 ```typescript // ❌ 不需要:导出推断类型(实际项目中不会被使用) export type Channel = z.infer; export type CreateChannelDto = z.infer; export type UpdateChannelDto = z.infer; ``` **原因**: - UI包通过RPC直接从API路由推断类型 - `@hono/zod-openapi` 自动生成OpenAPI文档 - 前端使用 `hc.rpc()` 自动获得类型安全的客户端 **正确做法**:只导出Schema常量,不导出推断类型。 ### 5.4 关键要点 - **使用 `.openapi()` 装饰器**: 添加描述和示例 - **使用 `z.coerce.date()` 和 `z.coerce.number()`**: Zod 4.0需要添加泛型参数 - **使用 `.nullable().optional()`**: 处理可空字段 - **不导出推断类型**: 类型由RPC自动推断,不需要手动导出 ## 6. 软删除规范 ### 6.1 字段定义 ```typescript @Column({ name: 'status', type: 'int', default: 1, comment: '状态:1-正常,0-禁用' }) status!: number; ``` ### 6.2 Service层实现 ```typescript // 覆盖delete方法实现软删除 override async delete(id: number, userId?: string | number): Promise { const result = await this.repository.update({ id }, { status: 0 }); return result.affected === 1; } ``` ### 6.3 查询时过滤 ```typescript // 查询时只查询正常状态的记录 const channel = await this.repository.findOne({ where: { id, status: 1 } }); ``` ## 7. 数据库类型规范 ### 7.1 PostgreSQL类型映射 | 数据库类型 | TypeORM类型 | 备注 | |------------|-------------|------| | `int unsigned` | `int` + `unsigned: true` | 主键常用 | | `varchar(n)` | `varchar` + `length: n` | 字符串 | | `text` | `text` | 长文本 | | `timestamp` | `timestamp` | 时间戳 | | `decimal(p,s)` | `decimal` + `precision/scale` | 金额 | | `int` (状态) | `int` | 状态枚举 | ### 7.2 Decimal字段处理 ```typescript // 实体定义 @Column({ name: 'total_amount', type: 'decimal', precision: 10, scale: 2 }) totalAmount!: number; // Schema验证(使用z.coerce.number()处理字符串) const CreateSchema = z.object({ totalAmount: z.coerce.number().min(0), }); ``` ## 8. 错误处理规范 ### 8.1 标准错误响应格式 ```typescript // 正确的错误响应格式 return c.json({ code: 400, message: '渠道名称已存在' }, 400); ``` ### 8.2 HTTP状态码使用 | 状态码 | 使用场景 | |--------|----------| | 200 | 操作成功(包括删除) | | 201 | 创建成功 | | 400 | 请求参数错误 | | 401 | 未授权 | | 404 | 资源不存在 | | 500 | 服务器内部错误 | ### 8.3 DELETE操作响应 ```typescript // DELETE成功应返回200,而不是404 app.delete('/:id', async (c) => { const result = await service.delete(id); return c.json({ success: true }, 200); // ✅ 正确 // return c.json({ success: true }, 404); // ❌ 错误 }); ``` ## 9. 包配置规范 ### 9.1 package.json ```json { "name": "@d8d/allin-channel-module", "version": "1.0.0", "type": "module", "main": "src/index.ts", "types": "src/index.ts", "scripts": { "test": "vitest run", "test:coverage": "vitest run --coverage", "typecheck": "tsc --noEmit" }, "dependencies": { "@d8d/shared-crud": "workspace:*", "@d8d/shared-types": "workspace:*", "@d8d/shared-utils": "workspace:*", "typeorm": "^0.3.20" }, "devDependencies": { "@hono/zod-openapi": "latest", "vitest": "latest" } } ``` ### 9.2 依赖配置 ```json { "dependencies": { "@d8d/allin-platform-module": "workspace:*", "@d8d/file-module": "workspace:*", "@d8d/allin-enums": "workspace:*" } } ``` ## 10. 测试规范 详细的测试规范请参考 [后端模块包测试规范](./backend-module-testing-standards.md)。 ### 10.1 测试数据工厂 ```typescript export class TestDataFactory { static createChannelData(overrides: Partial = {}): Partial { const timestamp = Date.now(); return { channelName: `测试渠道_${timestamp}`, channelType: '测试类型', contactPerson: '测试联系人', contactPhone: '13800138000', status: 1, ...overrides }; } static async createTestChannel( dataSource: DataSource, overrides: Partial = {} ): Promise { const channelData = this.createChannelData(overrides); const channelRepo = dataSource.getRepository(Channel); const channel = channelRepo.create(channelData); return await channelRepo.save(channel); } } ``` ### 10.2 测试配置 ```typescript // vitest.config.ts export default defineConfig({ test: { globals: true, environment: 'node', include: ['tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], fileParallelism: false // 避免数据库连接冲突 } }); ``` ## 11. 开发流程 ### 11.1 类型检查 ```bash # 开发过程中运行类型检查 pnpm typecheck # 针对特定包 cd allin-packages/channel-module && pnpm typecheck ``` ### 11.2 运行测试 ```bash # 进入模块目录 cd allin-packages/channel-module # 运行测试 pnpm test # 运行集成测试 pnpm test:integration # 生成覆盖率报告 pnpm test:coverage ``` ### 11.3 注释规范 ```typescript /** * 创建渠道 - 覆盖父类方法,添加名称唯一性检查 * @param data 渠道数据 * @param userId 操作用户ID * @returns 创建的渠道 * @throws Error 当渠道名称已存在时 */ override async create(data: Partial, userId?: string | number): Promise { // ... } ``` ## 12. 参考实现 ### 12.1 已完成模块 **allin-packages**: - `channel-module`: 基础CRUD模块 - `platform-module`: 基础依赖包 - `company-module`: 模块间依赖 - `disability-module`: 文件模块集成 **core-module**: - `auth-module`: 认证模块 - `user-module`: 用户模块 - `file-module`: 文件模块 ### 12.2 最佳实践 1. **使用 `override` 关键字**: 明确标识覆盖父类方法 2. **完整的列定义**: 包含 `type`, `comment`, `nullable` 等属性 3. **OpenAPI文档**: 使用 `.openapi()` 装饰器添加文档 4. **类型推断导出**: 导出 `z.infer` 推断的类型 5. **软删除实现**: 使用 `status` 字段而非物理删除 6. **时间戳管理**: 自动设置 `createTime` 和 `updateTime` 7. **错误处理**: 提供明确的错误消息 ## 附录:检查清单 ### 新模块包创建检查清单 - [ ] 包名符合规范:`@d8d/allin-{name}-module` 或 `@d8d/core-module` - [ ] 目录结构完整:entities, services, routes, schemas, tests - [ ] Entity包含完整列定义:type, comment, nullable等 - [ ] Service使用 `override` 关键字 - [ ] 软删除实现:使用 `status` 字段 - [ ] Schema使用 `.openapi()` 装饰器 - [ ] Schema不导出推断类型(类型由RPC自动推断) - [ ] Schema使用 `z.coerce.date()` 和 `z.coerce.number()` 泛型语法 - [ ] 路由使用 `OpenAPIHono` 和 `AuthContext` - [ ] 自定义路由使用 `parseWithAwait` 验证响应数据 - [ ] 自定义路由使用 `createZodErrorResponse` 处理Zod错误 - [ ] 测试数据工厂使用时间戳保证唯一性 - [ ] vitest.config.ts 设置 `fileParallelism: false` - [ ] 类型检查通过 - [ ] 所有测试通过 ## 相关文档 - [后端模块包测试规范](./backend-module-testing-standards.md) - [测试策略概述](./testing-strategy.md) - [编码标准](./coding-standards.md) --- **文档状态**: 正式版 **基于实际实现**: 2025-12-26