# 后端模块包规范

## 版本信息
| 版本 | 日期 | 描述 | 作者 |
|------|------|------|------|
| 1.0 | 2025-12-02 | 基于史诗007系列移植经验创建 | Claude Code |

## 概述
本文档定义了后端模块包的设计、开发和集成规范，基于史诗007系列（Allin系统模块移植）的实际经验总结。这些规范旨在确保模块包的一致性、可维护性和可集成性。

## 1. 包结构规范

### 1.1 目录结构
```
allin-packages/{module-name}-module/
├── package.json                    # 包配置
├── tsconfig.json                   # TypeScript配置
├── vitest.config.ts                # 测试配置
├── src/
│   ├── entities/                   # 实体定义
│   │   └── {entity-name}.entity.ts
│   ├── services/                   # 服务层
│   │   └── {service-name}.service.ts
│   ├── routes/                     # 路由层
│   │   ├── {module}-custom.routes.ts   # 自定义路由
│   │   ├── {module}-crud.routes.ts     # CRUD路由
│   │   └── {module}.routes.ts          # 主路由
│   ├── schemas/                    # 验证Schema
│   │   └── {schema-name}.schema.ts
│   ├── types/                      # 类型定义
│   │   └── index.ts
│   └── index.ts                    # 包入口
└── tests/
    └── integration/                # 集成测试
        └── {module}.integration.test.ts
```

### 1.2 包命名规范
- **前缀**: `@d8d/allin-`
- **后缀**: `-module`
- **示例**: `@d8d/allin-channel-module`, `@d8d/allin-platform-module`

### 1.3 workspace配置
```yaml
# pnpm-workspace.yaml
packages:
  - 'allin-packages/*'              # 自动包含所有allin包
  - 'allin-packages/{module-name}-module'  # 或显式指定
```

## 2. 实体设计规范

### 2.1 主键命名
```typescript
// ✅ 正确：使用id作为主键名
@PrimaryGeneratedColumn({ name: 'channel_id' })
id!: number;

// ❌ 错误：使用特定名称
@PrimaryGeneratedColumn({ name: 'channel_id' })
channelId!: number;
```

### 2.2 字段命名转换
```typescript
// 数据库下划线命名 → TypeScript驼峰命名
@Column({ name: 'channel_name' })
channelName!: string;

@Column({ name: 'contact_person' })
contactPerson!: string;

@Column({ name: 'create_time' })
createTime!: Date;
```

### 2.3 唯一性约束
```typescript
// 单字段唯一性
@Unique(['channelName'])

// 复合字段唯一性（如公司名称在同一平台下唯一）
@Unique(['companyName', 'platformId'])
```

### 2.4 关联关系配置
```typescript
// 多对一关系（如公司关联平台）
@ManyToOne(() => Platform, { eager: true })
@JoinColumn({ name: 'platform_id' })
platform!: Platform;

// 一对多关系
@OneToMany(() => Company, company => company.platform)
companies!: Company[];
```

### 2.5 软删除实现
```typescript
@Column({
  name: 'status',
  type: 'tinyint',
  default: 1,
  comment: '状态：0-删除，1-正常'
})
status!: number;
```

## 3. 数据库类型规范

### 3.1 PostgreSQL类型兼容
```typescript
// 源类型 → 目标类型
@Column({ name: 'some_flag', type: 'tinyint' })    // tinyint → smallint
someFlag!: number;

@Column({ name: 'create_time', type: 'datetime' }) // datetime → timestamp
createTime!: Date;
```

### 3.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),
});
```

### 3.3 枚举值一致性
```typescript
// 保持与数据库值一致（小写字符串，下划线分隔）
enum OrderStatus {
  DRAFT = 'draft',
  CONFIRMED = 'confirmed',
  IN_PROGRESS = 'in_progress',
  COMPLETED = 'completed',
  CANCELLED = 'cancelled'
}

// 数字枚举
enum DisabilityLevel {
  ONE = 1,
  TWO = 2,
  THREE = 3,
  FOUR = 4
}
```

## 4. 服务层规范

### 4.1 GenericCrudService继承
```typescript
export class ChannelService extends GenericCrudService<Channel> {
  constructor(dataSource: DataSource) {
    super(dataSource, Channel);
  }
}
```

### 4.2 方法覆盖模式
```typescript
// 覆盖create方法：添加唯一性检查
async create(data: CreateChannelDto): Promise<Channel> {
  // 业务逻辑：检查名称唯一性
  const existing = await this.repository.findOne({
    where: { channelName: data.channelName }
  });

  if (existing) {
    throw new Error('渠道名称已存在');
  }

  return super.create(data);
}

// 覆盖findAll方法：返回标准格式
async findAll(options?: FindManyOptions<Channel>): Promise<{ data: Channel[], total: number }> {
  const [data, total] = await this.repository.findAndCount(options);
  return { data, total };
}
```

### 4.3 自定义业务方法
```typescript
// 按名称搜索
async searchByName(name: string): Promise<Channel[]> {
  return this.repository.find({
    where: { channelName: Like(`%${name}%`) }
  });
}
```

## 5. 路由层规范

### 5.1 路由聚合模式
```typescript
// 主路由文件：聚合自定义路由和CRUD路由
import customRoutes from './channel-custom.routes';
import crudRoutes from './channel-crud.routes';

const channelRoutes = new Hono()
  .basePath('/channels')
  .route('/', customRoutes)   // 自定义业务逻辑路由
  .route('/', crudRoutes);    // 标准CRUD路由

export default channelRoutes;
```

### 5.2 API兼容性
```typescript
// 保持与原始NestJS API相同的端点路径和功能
// 原始：POST /channel/createChannel
// 新：POST /channels/createChannel
app.post('/createChannel', async (c) => {
  // 实现逻辑
});
```

### 5.3 布尔返回值处理
```typescript
// 正确处理布尔返回值
app.post('/create', async (c) => {
  try {
    const result = await service.create(data);
    return c.json({ success: true }, 200);
  } catch (error) {
    return c.json({
      success: false,
      message: error.message || '创建失败'
    }, 400);
  }
});
```

### 5.4 错误信息明确
```typescript
// 提供明确的错误信息
app.put('/update/:id', async (c) => {
  const id = parseInt(c.req.param('id'));
  const data = await c.req.json();

  try {
    const result = await service.update(id, data);
    return c.json({ success: true });
  } catch (error) {
    return c.json({
      success: false,
      message: error.message || '平台不存在或名称重复'
    }, 400);
  }
});
```

## 6. 验证系统规范

### 6.1 Zod Schema定义
```typescript
// 创建Schema
const CreateChannelSchema = z.object({
  channelName: z.string().min(1).max(100),
  contactPerson: z.string().max(50).optional(),
  contactPhone: z.string().max(20).optional(),
  status: z.number().int().min(0).max(1).default(1),
});

// 更新Schema（id通过路径参数传递，不在body中）
const UpdateChannelSchema = CreateChannelSchema.partial();

// 查询Schema
const QueryChannelSchema = z.object({
  channelName: z.string().optional(),
  skip: z.coerce.number().int().min(0).default(0),
  take: z.coerce.number().int().min(1).max(100).default(10),
});
```

### 6.2 枚举验证
```typescript
import { OrderStatus, WorkStatus } from '@d8d/allin-enums';

const CreateOrderSchema = z.object({
  orderStatus: z.nativeEnum(OrderStatus).default(OrderStatus.DRAFT),
  workStatus: z.nativeEnum(WorkStatus).default(WorkStatus.NOT_WORKING),
});
```

## 7. 模块集成规范

### 7.1 文件模块集成
```typescript
// 使用fileId字段而非URL字段
@Column({
  name: 'file_id',
  type: 'int',
  nullable: false,
  comment: '文件ID，引用files表'
})
fileId!: number;

@ManyToOne(() => File)
@JoinColumn({ name: 'file_id' })
file!: File;
```

### 7.2 循环依赖处理
```typescript
// 原代码（有循环依赖）:
// @ManyToOne(() => DisabledPerson)
// person!: DisabledPerson;

// 新代码（解耦）:
@Column({
  name: 'person_id',
  type: 'int',
  nullable: false,
  comment: '人员ID'
})
personId!: number;
```

### 7.3 依赖配置
```json
{
  "name": "@d8d/allin-company-module",
  "dependencies": {
    "@d8d/allin-platform-module": "workspace:*",
    "@d8d/file-module": "workspace:*",
    "@d8d/allin-enums": "workspace:*"
  }
}
```

### 7.4 基础包设计
- **基础包**（如platform-module）：不需要依赖其他allin模块
- **业务包**（如company-module）：可以依赖基础包和其他业务包
- **工具包**（如enums包）：提供共享常量，被其他模块依赖

## 8. 测试规范

### 8.1 测试数据完整性
```typescript
// 确保测试数据包含所有必填字段
const testData = {
  channelName: '测试渠道',
  contactPerson: '测试联系人',
  contactPhone: '13800138000',
  status: 1,
  // 不要遗漏任何必填字段
};
```

### 8.2 唯一性约束测试
```typescript
// 避免测试数据违反唯一性约束
test('创建重复名称的渠道应失败', async () => {
  // 使用不同的测试数据避免冲突
  const data1 = { channelName: '渠道A', ...otherFields };
  const data2 = { channelName: '渠道B', ...otherFields }; // 使用不同的名称

  await service.create(data1);
  await expect(service.create(data1)).rejects.toThrow(); // 第二次创建应失败
});
```

### 8.3 调试信息
```typescript
// 在关键路由中添加调试信息
app.post('/create', async (c) => {
  const body = await c.req.json();
  console.debug('创建请求:', body);
  // ...处理逻辑
});
```

### 8.4 测试覆盖率标准
- **单元测试**：核心业务逻辑 > 80%
- **集成测试**：API端点覆盖 ≥ 60%
- **错误场景**：覆盖各种错误场景和边界条件

## 9. 开发流程规范

### 9.1 workspace配置
```bash
# 创建新包后，确保在pnpm-workspace.yaml中添加
# 或在根目录配置'allin-packages/*'自动包含
```

### 9.2 类型检查
```bash
# 开发过程中运行类型检查
pnpm typecheck

# 针对特定包
cd allin-packages/channel-module && pnpm typecheck
```

### 9.3 包配置优化
```json
{
  "name": "@d8d/allin-enums",
  "type": "module",
  "main": "src/index.ts",      # workspace中直接引用源码
  "types": "src/index.ts",     # 类型定义直接使用源码
  "scripts": {
    "test": "vitest run",
    "typecheck": "tsc --noEmit"
    # 不需要"build"脚本（workspace中直接引用源码）
  }
}
```

### 9.4 注释规范
```typescript
/**
 * 订单状态枚举
 * - draft: 草稿状态，可编辑
 * - confirmed: 已确认，不可编辑
 * - in_progress: 进行中，有工作人员处理
 * - completed: 已完成，可归档
 * - cancelled: 已取消，用户主动取消
 */
enum OrderStatus {
  DRAFT = 'draft',
  CONFIRMED = 'confirmed',
  IN_PROGRESS = 'in_progress',
  COMPLETED = 'completed',
  CANCELLED = 'cancelled'
}
```

## 10. 错误处理规范

### 10.1 HTTP状态码
- `200 OK`：操作成功
- `201 Created`：创建成功
- `400 Bad Request`：请求参数错误
- `404 Not Found`：资源不存在
- `409 Conflict`：资源冲突（如唯一性冲突）
- `500 Internal Server Error`：服务器内部错误

### 10.2 错误响应格式
```json
{
  "success": false,
  "message": "渠道名称已存在",
  "code": "CHANNEL_NAME_EXISTS",
  "timestamp": "2025-12-02T10:30:00Z"
}
```

### 10.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); // ❌ 错误
});
```

## 11. 参考实现

### 11.1 已完成模块参考
- **channel-module** (007.001)：基础CRUD模块实现
- **company-module** (007.002)：模块间依赖处理
- **platform-module** (007.006)：基础依赖包设计
- **disability-module** (007.004)：文件模块集成
- **salary-module** (007.007)：外部包集成（geo-areas）

### 11.2 最佳实践总结
1. **保持API兼容性**：移植时保持原始API功能
2. **统一错误处理**：提供明确的错误信息
3. **避免循环依赖**：使用ID引用解耦模块
4. **完整测试覆盖**：覆盖成功和失败场景
5. **及时类型检查**：开发过程中持续运行类型检查

## 附录：检查清单

### 新模块包创建检查清单
- [ ] 包名符合规范：`@d8d/allin-{name}-module`
- [ ] 目录结构完整：entities, services, routes, schemas, tests
- [ ] workspace配置：在pnpm-workspace.yaml中添加或配置通配符
- [ ] 实体主键命名为`id`
- [ ] 字段命名正确转换（下划线→驼峰）
- [ ] 唯一性约束正确配置
- [ ] 服务层继承GenericCrudService
- [ ] 路由聚合模式正确
- [ ] Schema验证完整
- [ ] 测试数据完整且不违反约束
- [ ] 类型检查通过
- [ ] 所有测试通过
- [ ] 错误处理规范
- [ ] 注释完整清晰