# 后端模块包规范

## 版本信息
| 版本 | 日期 | 描述 | 作者 |
|------|------|------|------|
| 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<Channel> {
  constructor(dataSource: DataSource) {
    super(dataSource, Channel);
  }
}
```

### 3.2 方法覆盖模式（使用override）

**实际实现**:
```typescript
export class ChannelService extends GenericCrudService<Channel> {
  constructor(dataSource: DataSource) {
    super(dataSource, Channel);
  }

  /**
   * 创建渠道 - 覆盖父类方法，添加名称唯一性检查
   */
  override async create(data: Partial<Channel>, userId?: string | number): Promise<Channel> {
    // 检查渠道名称是否已存在（只检查正常状态的渠道）
    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<Channel>, userId?: string | number): Promise<Channel | null> {
    // 检查渠道是否存在
    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<boolean> {
    // 软删除：设置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<AuthContext>()
  .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路由）必须使用 `createRoute` 定义，并在返回响应前使用 `parseWithAwait` 验证和转换数据。

**完整示例**:
```typescript
import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
import { z } from '@hono/zod-openapi';
import { parseWithAwait, createZodErrorResponse } from '@d8d/shared-utils';
import { ErrorSchema, ZodErrorSchema } from '@d8d/shared-utils/schema/error';
import { ChannelSchema, CreateChannelSchema } from '../schemas/channel.schema';

// 定义路由（包含请求和响应Schema）
const createChannelRoute = createRoute({
  method: 'post',
  path: '/create',
  middleware: [authMiddleware],
  request: {
    body: {
      content: {
        'application/json': { schema: CreateChannelSchema }
      }
    }
  },
  responses: {
    200: {
      description: '创建成功',
      content: {
        'application/json': { schema: ChannelSchema }
      }
    },
    // ✅ 400使用 ZodErrorSchema（因为使用 createZodErrorResponse）
    400: {
      description: '参数错误',
      content: { 'application/json': { schema: ZodErrorSchema } }
    },
    // ✅ 其他错误使用 ErrorSchema
    401: {
      description: '认证失败',
      content: { 'application/json': { schema: ErrorSchema } }
    },
    500: {
      description: '服务器错误',
      content: { 'application/json': { schema: ErrorSchema } }
    }
  }
});

// 实现路由处理函数
channelCustomRoutes.openapi(createChannelRoute, async (c) => {
  try {
    const data = c.req.valid('json');
    const channelService = new ChannelService(AppDataSource);
    const result = await channelService.create(data);

    // ✅ 必须：使用 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);
  }
});
```

#### 数组响应处理

**推荐做法**: 定义数组响应 Schema

```typescript
// Schema定义
export const ChannelListResponseSchema = z.object({
  data: z.array(ChannelSchema).openapi({
    description: '渠道列表'
  }),
  total: z.number().int().min(0).openapi({
    description: '总数',
    example: 10
  })
});

// 路由定义
const listChannelRoute = createRoute({
  method: 'get',
  path: '/list',
  responses: {
    200: {
      description: '获取列表成功',
      content: {
        'application/json': { schema: ChannelListResponseSchema }
      }
    },
    400: {
      description: '参数错误',
      content: { 'application/json': { schema: ZodErrorSchema } }
    }
  }
});

// 路由实现
channelCustomRoutes.openapi(listChannelRoute, async (c) => {
  try {
    const result = await channelService.getList(query);

    // ✅ 推荐：直接使用 parseWithAwait 验证整个响应对象
    const validatedResult = await parseWithAwait(ChannelListResponseSchema, result);
    return c.json(validatedResult, 200);
  } catch (error) {
    if (error instanceof z.ZodError) {
      return c.json(createZodErrorResponse(error), 400);
    }
    return c.json({ code: 500, message: error.message }, 500);
  }
});
```

**❌ 不推荐**: 使用 `Promise.all` 循环验证

```typescript
// ❌ 不推荐：这种方式效率较低
const validatedData = await Promise.all(
  result.data.map(item => parseWithAwait(ChannelSchema, item))
);
return c.json({ data: validatedData, total: result.total }, 200);
```

#### 关键要点

**响应Schema定义**:
- **400响应使用 `ZodErrorSchema`**: 因为使用 `createZodErrorResponse` 返回详细的验证错误
- **其他错误使用 `ErrorSchema`**: 401, 404, 500 等使用简单的错误格式
- **数组响应定义专用Schema**: 使用 `z.array(ItemSchema)` 定义列表响应Schema

**代码实现**:
- **必须使用 `createRoute`**: 所有自定义路由必须使用 `createRoute` 定义
- **必须使用 `parseWithAwait`**: 所有自定义路由返回前必须验证数据
- **捕获 ZodError**: 在catch块中处理 `z.ZodError` 异常
- **使用 `createZodErrorResponse`**: 提供统一的Zod错误响应格式
- **避免使用 Promise.all 循环验证**: 应定义数组响应Schema，直接验证整个响应对象

### 4.4 关键要点

- **使用 `OpenAPIHono`**: 而非普通的 `Hono`
- **使用 `AuthContext` 泛型**: 提供类型安全的认证上下文
- **路由聚合**: 分别定义自定义路由和CRUD路由，然后聚合
- **不设置 `basePath`**: 在聚合路由时处理路径
- **自定义路由必须使用 `createRoute`**: 所有自定义路由必须使用 `createRoute` 定义
- **自定义路由必须使用 `parseWithAwait`**: 验证响应数据符合Schema定义
- **400响应使用 `ZodErrorSchema`**: 因为使用 `createZodErrorResponse` 返回详细验证错误
- **其他错误使用 `ErrorSchema`**: 401, 404, 500 等使用简单错误格式

## 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<Date>().openapi({
    description: '创建时间',
    example: '2024-01-01T00:00:00Z'
  }),
  updateTime: z.coerce.date<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();

// 列表响应Schema
export const ChannelListResponseSchema = z.object({
  data: z.array(ChannelSchema).openapi({
    description: '渠道列表'
  }),
  total: z.number().int().min(0).openapi({
    description: '总数',
    example: 10
  })
});
```

### 5.2 Zod 4.0 coerce使用说明

**重要**: Zod 4.0 中，`z.coerce.date()` 和 `z.coerce.number()` 需要添加泛型参数来指定类型。

```typescript
// ✅ 正确：Zod 4.0 - 使用泛型指定类型
z.coerce.date<Date>()       // 转换为Date类型
z.coerce.number<number>()    // 转换为number类型

// ❌ 错误：不指定泛型（Zod 4.0中类型推断可能不准确）
z.coerce.date()
z.coerce.number()
```

### 5.3 类型使用说明

**重要**: Schema只用于请求参数验证和响应定义，**不需要导出推断的TypeScript类型**。

```typescript
// ❌ 不需要：导出推断类型（实际项目中不会被使用）
export type Channel = z.infer<typeof ChannelSchema>;
export type CreateChannelDto = z.infer<typeof CreateChannelSchema>;
export type UpdateChannelDto = z.infer<typeof UpdateChannelSchema>;
```

**原因**：
- UI包通过RPC直接从API路由推断类型
- `@hono/zod-openapi` 自动生成OpenAPI文档
- 前端使用 `hc.rpc()` 自动获得类型安全的客户端

**正确做法**：只导出Schema常量，不导出推断类型。

### 5.4 关键要点

- **使用 `.openapi()` 装饰器**: 添加描述和示例
- **使用 `z.coerce.date<Date>()` 和 `z.coerce.number<number>()`**: Zod 4.0需要添加泛型参数
- **使用 `.nullable().optional()`**: 处理可空字段
- **定义列表响应Schema**: 使用 `z.array(ItemSchema)` 定义数组响应，而不是循环验证
- **不导出推断类型**: 类型由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<boolean> {
  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<number>()处理字符串）
const CreateSchema = z.object({
  totalAmount: z.coerce.number<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<Channel> = {}): Partial<Channel> {
    const timestamp = Date.now();
    return {
      channelName: `测试渠道_${timestamp}`,
      channelType: '测试类型',
      contactPerson: '测试联系人',
      contactPhone: '13800138000',
      status: 1,
      ...overrides
    };
  }

  static async createTestChannel(
    dataSource: DataSource,
    overrides: Partial<Channel> = {}
  ): Promise<Channel> {
    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<Channel>, userId?: string | number): Promise<Channel> {
  // ...
}
```

## 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<Date>()` 和 `z.coerce.number<number>()` 泛型语法
- [ ] 路由使用 `OpenAPIHono` 和 `AuthContext`
- [ ] 自定义路由使用 `parseWithAwait` 验证响应数据
- [ ] 自定义路由使用 `createZodErrorResponse` 处理Zod错误
- [ ] 路由400响应使用 `ZodErrorSchema`，其他错误使用 `ErrorSchema`
- [ ] 测试数据工厂使用时间戳保证唯一性
- [ ] vitest.config.ts 设置 `fileParallelism: false`
- [ ] 类型检查通过
- [ ] 所有测试通过

## 相关文档

- [后端模块包测试规范](./backend-module-testing-standards.md)
- [测试策略概述](./testing-strategy.md)
- [编码标准](./coding-standards.md)

---

**文档状态**: 正式版
**基于实际实现**: 2025-12-26
