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

83 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Context
`proposal.md`。现有换货单以 `migrate_data``migration_completed` 两个布尔字段记录意图和成功结果;`Service.Complete` 在同一数据库事务中完成资产归属、客户绑定、资产状态和可选业务数据迁移。迁移报错会回滚整个事务,现有失败审计不保存可供列表查询的迁移失败状态。
现有迁移函数已在同一事务中处理钱包余额、套餐使用记录、累计字段和资产标签。物流换货的发货后状态允许再次确认完成;直接换货创建即在同一事务完成,创建失败时不持久化换货单。
## Goals / Non-Goals
**Goals:**
- 为已持久化换货单提供稳定、可查询的迁移状态和安全失败原因。
- 保持业务数据迁移和换货完成的原子性,并让物流换货的失败可由超级管理员或平台用户重试。
- 兼容既有响应字段和历史换货数据。
**Non-Goals:**
- 不改变直接换货创建失败即整体回滚、无换货单留存的现有行为。
- 不修改迁移项目、增加迁移明细表、迁移手机号—资产关联,或改变资产归属和个人客户—资产绑定的既有换货动作。
- 不新增列表筛选、导出或路由。
## Decisions
### 1. 使用状态字段取代布尔字段推断
`tb_exchange_order` 新增非空 `migration_status` 和非空 `migration_failure_reason`。状态使用字符串 `not_migrated``pending``migrated``failed`,中文名称仅在应用层投影;失败原因最长 500 字符且为空表示无失败原因。
新字段表达完整结果,保留 `migrate_data``migration_completed``migration_balance` 供兼容客户端及既有业务使用。新建/发货时按是否选择迁移写入 `not_migrated``pending`;成功完成写入 `migrated` 并清空失败原因。
备选方案是在现有两个布尔字段上叠加前端规则。放弃原因是无法表达失败和失败原因,且容易把待迁移与失败混淆。
### 2. 主事务回滚后以短事务落失败状态与审计
业务数据迁移仍与换货完成共享原有 GORM 事务。任何一步失败均回滚资产状态、归属、客户绑定、钱包、套餐和标签的本次修改,确保重试从完整且未部分迁移的事实开始。
外层识别到迁移失败后,另开短事务,以换货单仍处于可确认完成状态为条件更新 `migration_status=failed` 与经安全截断的失败原因,并写入对应失败审计。失败状态的持久化不得与已回滚的业务数据迁移共用事务。
备选方案是让迁移失败提交部分换货结果。放弃原因是会产生无法可靠补偿的钱包和套餐事实,且违背本期整套迁移原子执行的产品边界。
### 3. 只为失败的物流换货增加受限重试
保持既有确认完成入口。换货单为物流流程、业务状态仍为已发货待确认且迁移状态为 `failed` 时,仅超级管理员或平台用户可再次确认;用例在事务内重新锁定并验证状态,然后从钱包余额开始重新执行全部迁移。未失败的换货沿用现有可确认权限和状态门禁。
直接换货继续创建即完成;其迁移失败会回滚整笔创建,不留换货单或失败状态,避免为单一失败路径引入新的直接换货中间状态与重试接口。
### 4. 一次成对迁移完成历史映射
新增一对当前根迁移,不修改历史迁移。迁移新增列后,以既有字段回填:`migrate_data=false` 映射为 `not_migrated``migrate_data=true AND migration_completed=true` 映射为 `migrated`;其余 `migrate_data=true` 映射为 `pending`。历史记录的失败原因置空。
不增加索引:本期没有迁移状态筛选或后台批处理查询,现有列表分页读取已直接投影换货单字段。
## 行为与数据契约
### 数据投影与历史映射
- `tb_exchange_order` 新增 `migration_status varchar(20) NOT NULL``migration_failure_reason varchar(500) NOT NULL DEFAULT ''`DTO 列表与详情新增 `migration_status``migration_status_name`,并仅在状态为 `failed` 时返回 `migration_failure_reason`
- 上线迁移将 `migrate_data=false` 映射 `not_migrated``migrate_data=true AND migration_completed=true` 映射 `migrated`,其余已存在 `migrate_data=true` 映射 `pending`;不推断历史失败原因。
### 创建、发货与确认完成
- `POST /exchanges`:沿用现有创建入参和权限。物流单创建时按 `migrate_data` 初始化 `not_migrated``pending`;直接换货在同一创建事务中执行完成和可选迁移,任一步失败则整个创建回滚,不返回换货单或 `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 /exchanges``GET /exchanges/:id` 沿用既有换货数据范围,返回新状态字段;旧 `migrate_data``migration_completed``migration_balance` 保持原响应兼容,但调用方不得再以其组合判断迁移结果。
## Risks / Trade-offs
- [失败原因可能包含底层敏感或不稳定信息] → 使用稳定错误的安全摘要并限制长度,禁止直接返回数据库、外部服务或敏感载荷。
- [失败状态更新与失败审计二次事务异常] → 复用既有换货失败审计的次级故障记录方式;状态更新与成功必达审计同事务,更新失败时返回原失败并保留诊断。
- [并发确认造成重复迁移] → 重用换货单 `FOR UPDATE` 锁与预期业务状态更新;只有仍为 `failed` 的失败重试可以进入受限路径。
- [旧客户端只读取布尔字段] → 保持原字段及其成功语义,新字段只增不删。
## Migration Plan
1. 在隔离数据库执行新迁移,核对历史映射、非空约束和 down 后 Schema。
2. 发布同时包含迁移、写侧状态转换、列表/详情 DTO 投影和审计更新的版本。
3. 发生应用回滚时,先回滚应用至仍兼容新增列的版本;仅在确认没有依赖新状态的数据或功能后执行 down 迁移。