| 版本 | 日期 | 描述 | 作者 |
|---|---|---|---|
| 1.0 | 2025-12-02 | 基于史诗007系列移植经验创建 | Claude Code |
本文档定义了后端模块包的设计、开发和集成规范,基于史诗007系列(Allin系统模块移植)的实际经验总结。这些规范旨在确保模块包的一致性、可维护性和可集成性。
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
@d8d/allin--module@d8d/allin-channel-module, @d8d/allin-platform-module# pnpm-workspace.yaml
packages:
- 'allin-packages/*' # 自动包含所有allin包
- 'allin-packages/{module-name}-module' # 或显式指定
// ✅ 正确:使用id作为主键名
@PrimaryGeneratedColumn({ name: 'channel_id' })
id!: number;
// ❌ 错误:使用特定名称
@PrimaryGeneratedColumn({ name: 'channel_id' })
channelId!: number;
// 数据库下划线命名 → TypeScript驼峰命名
@Column({ name: 'channel_name' })
channelName!: string;
@Column({ name: 'contact_person' })
contactPerson!: string;
@Column({ name: 'create_time' })
createTime!: Date;
// 单字段唯一性
@Unique(['channelName'])
// 复合字段唯一性(如公司名称在同一平台下唯一)
@Unique(['companyName', 'platformId'])
// 多对一关系(如公司关联平台)
@ManyToOne(() => Platform, { eager: true })
@JoinColumn({ name: 'platform_id' })
platform!: Platform;
// 一对多关系
@OneToMany(() => Company, company => company.platform)
companies!: Company[];
@Column({
name: 'status',
type: 'tinyint',
default: 1,
comment: '状态:0-删除,1-正常'
})
status!: number;
// 源类型 → 目标类型
@Column({ name: 'some_flag', type: 'tinyint' }) // tinyint → smallint
someFlag!: number;
@Column({ name: 'create_time', type: 'datetime' }) // datetime → timestamp
createTime!: Date;
// 实体定义
@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),
});
// 保持与数据库值一致(小写字符串,下划线分隔)
enum OrderStatus {
DRAFT = 'draft',
CONFIRMED = 'confirmed',
IN_PROGRESS = 'in_progress',
COMPLETED = 'completed',
CANCELLED = 'cancelled'
}
// 数字枚举
enum DisabilityLevel {
ONE = 1,
TWO = 2,
THREE = 3,
FOUR = 4
}
export class ChannelService extends GenericCrudService<Channel> {
constructor(dataSource: DataSource) {
super(dataSource, Channel);
}
}
// 覆盖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 };
}
// 按名称搜索
async searchByName(name: string): Promise<Channel[]> {
return this.repository.find({
where: { channelName: Like(`%${name}%`) }
});
}
// 主路由文件:聚合自定义路由和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;
// 保持与原始NestJS API相同的端点路径和功能
// 原始:POST /channel/createChannel
// 新:POST /channels/createChannel
app.post('/createChannel', async (c) => {
// 实现逻辑
});
// 正确处理布尔返回值
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);
}
});
// 提供明确的错误信息
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);
}
});
// 创建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),
});
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),
});
// 使用fileId字段而非URL字段
@Column({
name: 'file_id',
type: 'int',
nullable: false,
comment: '文件ID,引用files表'
})
fileId!: number;
@ManyToOne(() => File)
@JoinColumn({ name: 'file_id' })
file!: File;
// 原代码(有循环依赖):
// @ManyToOne(() => DisabledPerson)
// person!: DisabledPerson;
// 新代码(解耦):
@Column({
name: 'person_id',
type: 'int',
nullable: false,
comment: '人员ID'
})
personId!: number;
{
"name": "@d8d/allin-company-module",
"dependencies": {
"@d8d/allin-platform-module": "workspace:*",
"@d8d/file-module": "workspace:*",
"@d8d/allin-enums": "workspace:*"
}
}
// 确保测试数据包含所有必填字段
const testData = {
channelName: '测试渠道',
contactPerson: '测试联系人',
contactPhone: '13800138000',
status: 1,
// 不要遗漏任何必填字段
};
// 避免测试数据违反唯一性约束
test('创建重复名称的渠道应失败', async () => {
// 使用不同的测试数据避免冲突
const data1 = { channelName: '渠道A', ...otherFields };
const data2 = { channelName: '渠道B', ...otherFields }; // 使用不同的名称
await service.create(data1);
await expect(service.create(data1)).rejects.toThrow(); // 第二次创建应失败
});
// 在关键路由中添加调试信息
app.post('/create', async (c) => {
const body = await c.req.json();
console.debug('创建请求:', body);
// ...处理逻辑
});
# 创建新包后,确保在pnpm-workspace.yaml中添加
# 或在根目录配置'allin-packages/*'自动包含
# 开发过程中运行类型检查
pnpm typecheck
# 针对特定包
cd allin-packages/channel-module && pnpm typecheck
{
"name": "@d8d/allin-enums",
"type": "module",
"main": "src/index.ts", # workspace中直接引用源码
"types": "src/index.ts", # 类型定义直接使用源码
"scripts": {
"test": "vitest run",
"typecheck": "tsc --noEmit"
# 不需要"build"脚本(workspace中直接引用源码)
}
}
/**
* 订单状态枚举
* - draft: 草稿状态,可编辑
* - confirmed: 已确认,不可编辑
* - in_progress: 进行中,有工作人员处理
* - completed: 已完成,可归档
* - cancelled: 已取消,用户主动取消
*/
enum OrderStatus {
DRAFT = 'draft',
CONFIRMED = 'confirmed',
IN_PROGRESS = 'in_progress',
COMPLETED = 'completed',
CANCELLED = 'cancelled'
}
200 OK:操作成功201 Created:创建成功400 Bad Request:请求参数错误404 Not Found:资源不存在409 Conflict:资源冲突(如唯一性冲突)500 Internal Server Error:服务器内部错误{
"success": false,
"message": "渠道名称已存在",
"code": "CHANNEL_NAME_EXISTS",
"timestamp": "2025-12-02T10:30:00Z"
}
// 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); // ❌ 错误
});
@d8d/allin-{name}-moduleid