Files
junhong_cmp_fiber/openspec/changes/add-exchange-data-migration-status/design.md
break 370fd3e67f
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 10m49s
update
2026-09-03 09:28:28 +08:00

7.2 KiB
Raw Blame History

Context

proposal.md。现有换货单以 migrate_datamigration_completed 两个布尔字段记录意图和成功结果;Service.Complete 在同一数据库事务中完成资产归属、客户绑定、资产状态和可选业务数据迁移。迁移报错会回滚整个事务,现有失败审计不保存可供列表查询的迁移失败状态。

现有迁移函数已在同一事务中处理钱包余额、套餐使用记录、累计字段和资产标签。物流换货的发货后状态允许再次确认完成;直接换货创建即在同一事务完成,创建失败时不持久化换货单。

Goals / Non-Goals

Goals:

  • 为已持久化换货单提供稳定、可查询的迁移状态和安全失败原因。
  • 保持业务数据迁移和换货完成的原子性,并让物流换货的失败可由超级管理员或平台用户重试。
  • 兼容既有响应字段和历史换货数据。

Non-Goals:

  • 不改变直接换货创建失败即整体回滚、无换货单留存的现有行为。
  • 不修改迁移项目、增加迁移明细表、迁移手机号—资产关联,或改变资产归属和个人客户—资产绑定的既有换货动作。
  • 不新增列表筛选、导出或路由。

Decisions

1. 使用状态字段取代布尔字段推断

tb_exchange_order 新增非空 migration_status 和非空 migration_failure_reason。状态使用字符串 not_migratedpendingmigratedfailed,中文名称仅在应用层投影;失败原因最长 500 字符且为空表示无失败原因。

新字段表达完整结果,保留 migrate_datamigration_completedmigration_balance 供兼容客户端及既有业务使用。新建/发货时按是否选择迁移写入 not_migratedpending;成功完成写入 migrated 并清空失败原因。

备选方案是在现有两个布尔字段上叠加前端规则。放弃原因是无法表达失败和失败原因,且容易把待迁移与失败混淆。

2. 主事务回滚后以短事务落失败状态与审计

业务数据迁移仍与换货完成共享原有 GORM 事务。任何一步失败均回滚资产状态、归属、客户绑定、钱包、套餐和标签的本次修改,确保重试从完整且未部分迁移的事实开始。

外层识别到迁移失败后,另开短事务,以换货单仍处于可确认完成状态为条件更新 migration_status=failed 与经安全截断的失败原因,并写入对应失败审计。失败状态的持久化不得与已回滚的业务数据迁移共用事务。

备选方案是让迁移失败提交部分换货结果。放弃原因是会产生无法可靠补偿的钱包和套餐事实,且违背本期整套迁移原子执行的产品边界。

3. 只为失败的物流换货增加受限重试

保持既有确认完成入口。换货单为物流流程、业务状态仍为已发货待确认且迁移状态为 failed 时,仅超级管理员或平台用户可再次确认;用例在事务内重新锁定并验证状态,然后从钱包余额开始重新执行全部迁移。未失败的换货沿用现有可确认权限和状态门禁。

直接换货继续创建即完成;其迁移失败会回滚整笔创建,不留换货单或失败状态,避免为单一失败路径引入新的直接换货中间状态与重试接口。

4. 一次成对迁移完成历史映射

新增一对当前根迁移,不修改历史迁移。迁移新增列后,以既有字段回填:migrate_data=false 映射为 not_migratedmigrate_data=true AND migration_completed=true 映射为 migrated;其余 migrate_data=true 映射为 pending。历史记录的失败原因置空。

不增加索引:本期没有迁移状态筛选或后台批处理查询,现有列表分页读取已直接投影换货单字段。

行为与数据契约

数据投影与历史映射

  • tb_exchange_order 新增 migration_status varchar(20) NOT NULLmigration_failure_reason varchar(500) NOT NULL DEFAULT ''DTO 列表与详情新增 migration_statusmigration_status_name,并仅在状态为 failed 时返回 migration_failure_reason
  • 上线迁移将 migrate_data=false 映射 not_migratedmigrate_data=true AND migration_completed=true 映射 migrated,其余已存在 migrate_data=true 映射 pending;不推断历史失败原因。

创建、发货与确认完成

  • POST /exchanges:沿用现有创建入参和权限。物流单创建时按 migrate_data 初始化 not_migratedpending;直接换货在同一创建事务中执行完成和可选迁移,任一步失败则整个创建回滚,不返回换货单或 failed 状态。
  • POST /exchanges/:id/ship:沿用既有物流状态机和发货字段;不改变迁移状态,选择迁移的单仍为 pending
  • POST /exchanges/:id/complete:先在同一事务锁定换货单并验证既有“已发货待确认”状态和数据范围。not_migrated 只执行固有资产归属及个人客户绑定;pending 执行钱包余额、有效套餐使用、累计充值、资产标签的完整迁移及固有动作。成功时写 migrated、清空失败原因、写完成时间和既有成功审计。

失败与受限重试

  • pending 迁移任一步失败时,主事务必须回滚资产归属、个人客户绑定、钱包、套餐、累计字段、标签和完成状态;外层另开短事务,条件为换货单仍是可确认完成状态,写 failed、安全截断至 500 字符的失败原因及失败审计。
  • migration_status=failed 的物流单,POST /exchanges/:id/complete 仅超级管理员或平台用户可重试;锁定后从钱包余额开始重跑全部迁移,禁止仅重试某一子项。非平台账号返回无权,非失败单沿用既有完成状态门禁,不将完成接口变成通用重复执行入口。
  • 手机号—资产关联永不在上述动作中读取、复制或删除;新资产后续按自身 H5 手机号绑定规则处理。

读取行为

  • GET /exchangesGET /exchanges/:id 沿用既有换货数据范围,返回新状态字段;旧 migrate_datamigration_completedmigration_balance 保持原响应兼容,但调用方不得再以其组合判断迁移结果。

Risks / Trade-offs

  • [失败原因可能包含底层敏感或不稳定信息] → 使用稳定错误的安全摘要并限制长度,禁止直接返回数据库、外部服务或敏感载荷。
  • [失败状态更新与失败审计二次事务异常] → 复用既有换货失败审计的次级故障记录方式;状态更新与成功必达审计同事务,更新失败时返回原失败并保留诊断。
  • [并发确认造成重复迁移] → 重用换货单 FOR UPDATE 锁与预期业务状态更新;只有仍为 failed 的失败重试可以进入受限路径。
  • [旧客户端只读取布尔字段] → 保持原字段及其成功语义,新字段只增不删。

Migration Plan

  1. 在隔离数据库执行新迁移,核对历史映射、非空约束和 down 后 Schema。
  2. 发布同时包含迁移、写侧状态转换、列表/详情 DTO 投影和审计更新的版本。
  3. 发生应用回滚时,先回滚应用至仍兼容新增列的版本;仅在确认没有依赖新状态的数据或功能后执行 down 迁移。