007.005.transplant-order-management-module.story.md 19 KB

Story 007.005: 移植订单管理模块(order → @d8d/allin-order-module)

Status

Ready for Development

Story

As a 开发者, I want 将order模块从allin_system-master移植为独立包@d8d/allin-order-module,完成实体重构、文件模块集成和循环依赖处理, so that 我们可以将Allin系统的订单管理功能集成到当前项目中,遵循现有的模块化架构和编码标准,并实现与@d8d/file-module的文件集成和与@d8d/allin-disability-module的依赖解耦。

Acceptance Criteria

  1. 创建allin-packages/order-module目录结构
  2. 完成实体转换:3个实体(EmploymentOrder、OrderPerson、OrderPersonAsset)转换
  3. 枚举常量集成:使用@d8d/allin-enums包中的OrderStatusWorkStatus枚举
  4. 文件实体集成:修改OrderPersonAsset实体,添加fileId字段引用File实体
  5. 解决循环依赖:与disability-module的相互引用处理
  6. 完成服务层转换:订单业务逻辑,包含资产文件关联处理
  7. 完成路由层转换:Hono路由实现,API接收fileId参数
  8. 完成验证系统转换:订单相关验证,包含文件类型验证和枚举值验证
  9. 配置package.json:依赖管理,包含对@d8d/file-module@d8d/allin-enums的依赖
  10. 编写API集成测试:覆盖订单全生命周期,包含资产文件关联测试和枚举值测试
  11. 通过类型检查和基本测试验证
  12. 验证循环依赖解决方案的有效性

Tasks / Subtasks

1. 创建包结构和配置文件 (AC: 1)

  • 创建目录:allin-packages/order-module/
  • 初始化package.json:参考allin-packages/salary-module/package.json
    • 参考文件: allin-packages/salary-module/package.json
    • 修改点: 包名改为@d8d/allin-order-module,添加对@d8d/file-module@d8d/allin-enums的依赖
    • 关键依赖: @d8d/file-module, @d8d/allin-enums, @d8d/core-module, @d8d/shared-crud, @d8d/shared-utils
    • 注意吸取经验: 根据故事007.006的经验,需要在pnpm-workspace.yaml中添加allin-packages/*配置
  • 配置tsconfig.json:参考allin-packages/salary-module/tsconfig.json
    • 参考文件: allin-packages/salary-module/tsconfig.json
  • 配置vitest.config.ts:参考allin-packages/salary-module/vitest.config.ts
    • 参考文件: allin-packages/salary-module/vitest.config.ts
  • 配置workspace:在根目录pnpm-workspace.yaml中添加allin-packages/order-module
    • 修改文件: /mnt/code/188-179-template-6/pnpm-workspace.yaml
    • 添加内容: 'allin-packages/order-module'

2. 迁移实体文件 (AC: 2, 3, 4)

2.1 迁移EmploymentOrder实体

源文件路径: allin_system-master/server/src/order/employment_order.entity.ts 目标文件路径: allin-packages/order-module/src/entities/employment-order.entity.ts 参考对照文件:

  • allin-packages/salary-module/src/entities/salary-level.entity.ts(实体结构)
  • allin-packages/enums/src/enums/order-status.enum.ts(枚举引用)
  • allin-packages/enums/src/enums/work-status.enum.ts(枚举引用)

修改要点:

  1. 将主键字段名从orderId改为id以遵循GenericCrudService约定
  2. orderStatus字段类型从string改为OrderStatus枚举
  3. 添加workStatus字段,使用WorkStatus枚举
  4. 将下划线字段名转换为驼峰命名:order_nameorderName, platform_idplatformId
  5. 添加详细的TypeORM装饰器配置
  6. 保持数据库表名不变:@Entity('employment_order')

2.2 迁移OrderPerson实体

源文件路径: allin_system-master/server/src/order/order_person.entity.ts 目标文件路径: allin-packages/order-module/src/entities/order-person.entity.ts 参考对照文件:

  • allin-packages/disability-module/src/entities/disabled-person.entity.ts(实体结构)
  • 注意循环依赖: 原实体引用DisabledPerson实体,需要解耦处理

修改要点:

  1. 将主键字段名从opId改为id
  2. personId字段类型保持为number(不直接引用DisabledPerson实体)
  3. 将下划线字段名转换为驼峰命名
  4. 添加详细的TypeORM装饰器配置
  5. 保持数据库表名不变:@Entity('order_person')
  6. 循环依赖处理: 移除对DisabledPerson的直接引用,改为使用personId: number

2.3 迁移OrderPersonAsset实体

源文件路径: allin_system-master/server/src/order/order_person_asset.entity.ts 目标文件路径: allin-packages/order-module/src/entities/order-person-asset.entity.ts 参考对照文件:

  • allin-packages/disability-module/src/entities/disabled-photo.entity.ts(文件实体集成)
  • packages/file-module/src/entities/file.entity.ts(文件实体引用)

修改要点:

  1. 将主键字段名从opId改为id
  2. 文件模块集成: 移除assetUrl: string字段,添加fileId: number字段
  3. 添加与File实体的关联关系:@ManyToOne(() => File)
  4. 循环依赖处理: 移除对DisabledPerson的直接引用,改为使用personId: number
  5. AssetTypeAssetFileType枚举移到实体文件中(模块内部枚举)
  6. 将下划线字段名转换为驼峰命名
  7. 添加详细的TypeORM装饰器配置
  8. 保持数据库表名不变:@Entity('order_person_asset')

3. 创建DTO和Schema文件 (AC: 8)

目标文件路径:

  • allin-packages/order-module/src/dto/create-order.dto.ts
  • allin-packages/order-module/src/dto/update-order.dto.ts
  • allin-packages/order-module/src/dto/create-order-person.dto.ts
  • allin-packages/order-module/src/dto/create-order-person-asset.dto.ts
  • allin-packages/order-module/src/schemas/order.schema.ts

参考对照文件:

  • allin-packages/salary-module/src/dto/create-salary.dto.ts(DTO结构)
  • allin-packages/salary-module/src/dto/update-salary.dto.ts
  • allin-packages/salary-module/src/schemas/salary.schema.ts(Zod schema)
  • allin-packages/disability-module/src/schemas/disabled-person.schema.ts(文件ID验证)

修改要点:

  1. 将class-validator DTO转换为Zod Schema
  2. 在OrderPersonAsset的Schema中添加fileId验证
  3. 添加枚举值验证:使用OrderStatusWorkStatus枚举
  4. 保持与原始API相同的字段名和验证规则

4. 迁移控制器和服务 (AC: 6, 7)

4.1 迁移OrderService

源文件路径: allin_system-master/server/src/order/order.service.ts 目标文件路径: allin-packages/order-module/src/services/order.service.ts 参考对照文件:

  • allin-packages/salary-module/src/services/salary.service.ts(服务层结构)
  • allin-packages/disability-module/src/services/disabled-person.service.ts(文件验证逻辑)

修改要点:

  1. 将NestJS服务转换为继承GenericCrudService<EmploymentOrder>
  2. 循环依赖处理: 移除对DisabledPersonRepository的直接依赖
  3. 文件验证: 在创建/更新OrderPersonAsset时验证fileId的有效性
  4. 将业务逻辑从NestJS风格转换为Hono风格
  5. 保持原始的业务逻辑和错误处理

4.2 迁移OrderController为Hono路由

源文件路径: allin_system-master/server/src/order/order.controller.ts 目标文件路径: allin-packages/order-module/src/routes/order.routes.ts 参考对照文件:

  • allin-packages/salary-module/src/routes/salary.routes.ts(Hono路由结构)
  • allin-packages/disability-module/src/routes/disabled-person.routes.ts(文件ID参数处理)

修改要点:

  1. 将NestJS控制器转换为Hono路由
  2. 将class-validator管道验证转换为Zod验证
  3. 更新API端点路径,保持与原始API兼容
  4. 在OrderPersonAsset路由中添加fileId参数处理
  5. 配置OpenAPI文档

5. 创建模块主文件 (AC: 1)

目标文件路径:

  • allin-packages/order-module/src/index.ts
  • allin-packages/order-module/src/order.module.ts

参考对照文件:

  • allin-packages/salary-module/src/index.ts(导出结构)
  • allin-packages/salary-module/src/salary.module.ts(模块定义)

6. 创建测试文件 (AC: 10)

目标文件路径:

  • allin-packages/order-module/tests/unit/order.controller.test.ts
  • allin-packages/order-module/tests/unit/order.service.test.ts
  • allin-packages/order-module/tests/integration/order.integration.test.ts

参考对照文件:

  • allin-packages/salary-module/tests/unit/salary.controller.test.ts(单元测试结构)
  • allin-packages/salary-module/tests/integration/salary.integration.test.ts(集成测试结构)
  • allin-packages/disability-module/tests/integration/disability.integration.test.ts(文件集成测试)

测试重点:

  1. 枚举测试: 验证OrderStatusWorkStatus枚举的正确使用
  2. 文件集成测试: 测试OrderPersonAsset与File实体的关联
  3. 循环依赖测试: 验证与disability-module的解耦效果
  4. API兼容性测试: 验证与原始NestJS API的兼容性

7. 处理循环依赖 (AC: 5)

问题: 订单模块与disability-module存在相互引用 源文件分析:

  • 订单模块引用: DisabledPerson实体
  • 残疾人模块引用: EmploymentOrderOrderPerson实体

解决方案:

  1. 接口抽象: 在订单模块中创建PersonReference接口,避免直接引用DisabledPerson实体
  2. ID引用: 使用personId: number代替直接的实体引用
  3. 事件解耦: 通过事件或消息队列处理跨模块业务逻辑
  4. 参考处理: 参考allin-packages/salary-module@d8d/geo-areas的集成方式

实施步骤:

  1. 在订单模块中定义PersonReference类型
  2. 修改OrderPerson实体,使用personId: number代替person: DisabledPerson
  3. 修改OrderPersonAsset实体,使用personId: number代替person: DisabledPerson
  4. 在服务层通过ID验证人员存在性(可选:通过API调用验证)
  5. 更新残疾人管理模块,移除对订单实体的直接引用

8. 集成测试和验证 (AC: 11, 12)

  • 运行单元测试:pnpm test
  • 运行集成测试:pnpm test:integration
  • 类型检查:pnpm typecheck
  • 构建验证:pnpm build
  • 循环依赖验证:检查模块间导入关系

Dev Notes

从之前故事吸取的经验教训:

从故事007.004(残疾人管理模块)吸取的经验教训:

  1. PostgreSQL类型兼容问题:需要处理tinyint → smallint,datetime → timestamp的类型转换
  2. Schema验证错误:实体字段与schema定义不匹配(createTime/updateTime → uploadTime/remarkTime/visitTime)
  3. 测试数据问题:缺少subBankName、operatorId等必填字段
  4. 错误处理:DELETE端点返回404而不是200,GET聚合端点返回404而不是500
  5. 文件模块集成:银行卡实体使用fileId字段而不是cardPhotoUrl
  6. 实体主键命名:确保所有实体使用id作为主键字段名,而不是orderIdopId等特定名称
  7. 文件实体集成:正确配置与@d8d/file-module的关联关系,参考disabled-photo.entity.ts

从故事007.007(薪资管理模块)吸取的经验教训:

  1. 唯一性约束冲突:测试数据创建了相同(provinceId, cityId)组合的记录,需要修改测试数据
  2. Schema验证问题:allowance、insurance、housingFund字段"expected number, received string",需要改为z.coerce.number()
  3. 路由冲突:CRUD路由可能与自定义路由冲突,需要暂时注释掉CRUD路由聚合
  4. 调试信息:在路由中添加console.debug便于问题排查
  5. decimal字段处理:需要正确处理字符串格式的decimal字段
  6. 测试期望值:按区域过滤测试期望值需要调整
  7. workspace配置:在根目录pnpm-workspace.yaml中添加新包路径'allin-packages/order-module'
  8. 测试依赖:集成测试需要配置测试数据库,参考salary-module的tests/integration/setup.ts
  9. 类型检查问题:使用pnpm typecheck检查类型错误,特别注意枚举导入和循环依赖
  10. API兼容性:保持与原始NestJS API相同的请求/响应格式
  11. 枚举使用:通过@d8d/allin-enums包导入OrderStatus和WorkStatus枚举
  12. 区域包集成经验:参考salary-module与geo-areas的集成方式处理外部依赖

关键经验教训总结(基于007.004和007.007):

数据库和类型相关:

  1. PostgreSQL类型兼容:注意tinyint → smallint,datetime → timestamp的类型转换
  2. Decimal字段处理:数据库返回的decimal字段可能是字符串格式,需要使用z.coerce.number()处理
  3. Schema一致性:确保实体字段名与Schema定义完全匹配,特别注意时间字段命名

测试相关:

  1. 测试数据完整性:确保测试数据包含所有必填字段(如subBankName、operatorId等)
  2. 唯一性约束:测试数据避免创建违反唯一性约束的记录
  3. 测试期望值:根据实际数据调整测试断言期望值
  4. 调试信息:在路由中添加console.debug便于问题排查

路由和API相关:

  1. 路由冲突:CRUD路由可能与自定义路由冲突,需要合理设计路由结构
  2. 错误响应:确保DELETE返回200(成功)而不是404,GET聚合端点正确处理404场景
  3. API兼容性:保持与原始NestJS API相同的请求/响应格式

模块集成相关:

  1. 文件模块集成:正确使用fileId字段而不是URL字段,建立与File实体的关联
  2. 循环依赖处理:使用ID引用代替直接实体引用,避免模块间循环依赖
  3. 枚举集成:通过@d8d/allin-enums包导入共享枚举

开发流程相关:

  1. workspace配置:及时在pnpm-workspace.yaml中添加新包路径
  2. 类型检查:使用pnpm typecheck检查类型错误,特别注意枚举导入
  3. 测试依赖:集成测试需要正确配置测试数据库和实体

技术细节:

  1. OrderStatus和WorkStatus枚举集成

    import { OrderStatus, WorkStatus } from '@d8d/allin-enums';
    
    @Column({
     name: 'order_status',
     type: 'enum',
     enum: OrderStatus,
     default: OrderStatus.DRAFT,
     comment: '订单状态'
    })
    orderStatus!: OrderStatus;
    
    @Column({
     name: 'work_status',
     type: 'enum',
     enum: WorkStatus,
     default: WorkStatus.NOT_WORKING,
     comment: '工作状态'
    })
    workStatus!: WorkStatus;
    
  2. OrderPersonAsset实体文件集成

    import { File } from '@d8d/file-module/entities';
    
    @Column({
     name: 'file_id',
     type: 'int',
     nullable: false,
     comment: '文件ID,引用files表'
    })
    fileId!: number;
    
    @ManyToOne(() => File)
    @JoinColumn({ name: 'file_id' })
    file!: File;
    
  3. 循环依赖处理方案

    // 原代码(有循环依赖):
    // @ManyToOne(() => DisabledPerson)
    // person!: DisabledPerson;
    
    // 新代码(解耦):
    @Column({
     name: 'person_id',
     type: 'int',
     nullable: false,
     comment: '人员ID'
    })
    personId!: number;
    
    // 可选:定义引用接口
    export interface PersonReference {
     id: number;
     name: string;
     idCard: string;
    }
    
  4. AssetType和AssetFileType枚举

    • 这些是模块内部枚举,不需要移到enums包
    • order-person-asset.entity.ts中定义即可
    • 保持与原始值一致:TAX = 'tax', SALARY = 'salary'

文件模块集成步骤:

  1. 安装依赖:@d8d/file-module
  2. 在OrderPersonAsset实体中建立与File实体的关联
  3. 在DTO中验证fileId的有效性
  4. 在服务层处理文件关联逻辑
  5. 在集成测试中模拟文件上传和关联

测试策略:

  1. 单元测试:覆盖所有控制器和服务方法
  2. 集成测试:测试数据库操作和API端点
  3. 枚举验证测试:测试OrderStatus和WorkStatus枚举的正确使用
  4. 文件关联测试:测试OrderPersonAsset与File实体的关联
  5. 循环依赖测试:验证解耦方案的有效性
  6. API兼容性测试:验证与原始NestJS API的兼容性

Dependencies

  • @d8d/allin-enums
  • @d8d/file-module
  • @d8d/shared-crud
  • @d8d/shared-utils
  • @d8d/shared-types
  • @d8d/auth-module
  • @d8d/user-module
  • typeorm
  • zod
  • hono
  • vitest (dev dependency)
  • @d8d/shared-test-util (dev dependency)

Test Plan

  1. 单元测试:覆盖所有控制器和服务方法
  2. 集成测试:测试数据库操作和API端点
  3. 枚举验证:测试OrderStatus和WorkStatus枚举的正确使用
  4. 文件关联测试:测试OrderPersonAsset与File实体的关联
  5. 循环依赖测试:验证与disability-module的解耦效果
  6. API兼容性测试:验证与原始NestJS API的兼容性

Definition of Done

  • 所有实体成功迁移并转换为TypeORM + Zod
  • 完整的CRUD API端点可用
  • 集成@d8d/allin-enums包中的枚举
  • 集成@d8d/file-module包,正确处理文件关联
  • 解决与disability-module的循环依赖
  • 所有测试通过(单元测试、集成测试)
  • 类型检查通过
  • 构建成功
  • 代码符合项目代码规范
  • 文档完整(README、API文档)

实施建议(基于007.004和007.007经验)

1. 数据库类型处理

// 处理PostgreSQL类型兼容
@Column({
  name: 'order_status',
  type: 'enum',
  enum: OrderStatus,
  default: OrderStatus.DRAFT
})
orderStatus!: OrderStatus;

// 处理decimal字段
@Column({
  name: 'total_amount',
  type: 'decimal',
  precision: 10,
  scale: 2
})
totalAmount!: number; // Schema中使用z.coerce.number()

2. Schema验证设计

// 使用z.coerce.number()处理数据库返回的字符串
const CreateOrderSchema = z.object({
  totalAmount: z.coerce.number().min(0),
  // 确保字段名与实体完全一致
  createTime: z.string().datetime().optional(),
  updateTime: z.string().datetime().optional(),
});

3. 测试数据准备

// 确保测试数据包含所有必填字段
const testOrderData = {
  orderName: '测试订单',
  platformId: 1,
  orderStatus: OrderStatus.DRAFT,
  workStatus: WorkStatus.NOT_WORKING,
  totalAmount: 1000.00,
  // 不要遗漏任何必填字段
};

4. 路由设计避免冲突

// 合理设计路由结构,避免CRUD路由与自定义路由冲突
const orderRoutes = new Hono()
  .basePath('/orders')
  .route('/', customRoutes)  // 自定义路由
  .route('/', crudRoutes);   // CRUD路由(设置为readOnly: true)

5. 错误处理规范

// DELETE成功返回200,GET聚合端点正确处理404
app.delete('/:id', async (c) => {
  const result = await service.delete(id);
  return c.json({ success: true }, 200); // 不是404
});

app.get('/aggregated/:id', async (c) => {
  const data = await service.findAggregated(id);
  if (!data) {
    return c.json({ error: 'Not found' }, 404); // 正确处理404
  }
  return c.json(data);
});

6. 调试信息添加

// 在关键路由中添加console.debug便于问题排查
app.post('/create', async (c) => {
  const body = await c.req.json();
  console.debug('创建订单请求:', body);
  // ...处理逻辑
});

Dev Agent Record

将在开发完成后填写


故事创建时间:2025-12-02 创建者:Bob (Scrum Master) 更新说明:重新创建故事,吸取之前移植故事的经验教训,在任务中标注迁移文件的路径和参考对照文件的路径 经验教训更新:2025-12-02,基于故事007.004和007.007的经验教训补充