- 新增成对迁移 000222:tb_exchange_order 增加非空 migration_status 与 migration_failure_reason,按既有 migrate_data/migration_completed 回填历史, 并加四值 CHECK 约束,不新增索引 - 模型与常量定义四种迁移状态及中文名称,保留既有布尔字段兼容语义 - 物流换货创建恒 not_migrated,发货按请求落 pending/not_migrated, 完成成功写 migrated/not_migrated 并清空失败原因、同步兼容字段 - 直接换货创建即完成,任一步失败整体回滚,不持久化换货单、不产生 failed - 迁移失败回滚全部业务修改后,在独立短事务内条件更新 failed 与安全失败原因 并写失败审计,RowsAffected 为 0 时跳过状态写入但仍写审计 - failed 物流单重试仅限超级管理员或平台用户,授权以锁内 FOR UPDATE 判定为准, 重试从钱包余额起整表重跑;非 failed 单沿用既有完成门禁 - 列表与详情返回迁移状态与中文名称,仅 failed 返回失败原因;既有三字段保持兼容 - 换货导出在「状态」列后新增中文「迁移状态」列,不导出失败原因 - 同步 order-refund-exchange 主 spec 与验证证据,归档本 Change - 登记 KNOWN-ISSUE-001:既有标签复制 OnConflict 未声明部分索引谓词(42P10), 旧资产带标签时迁移最后一步失败,待另立变更修复
9.6 KiB
Context
见 proposal.md。现有换货单以 migrate_data 和 migration_completed 两个布尔字段记录意图和成功结果;Service.Complete 在同一数据库事务中完成资产归属、客户绑定、资产状态和可选业务数据迁移。迁移报错会回滚整个事务,现有失败审计不保存可供列表查询的迁移失败状态。
现有迁移函数已在同一事务中处理钱包余额、套餐使用记录、累计字段和资产标签。物流换货的发货后状态允许再次确认完成;直接换货创建即在同一事务完成,创建失败时不持久化换货单。
Goals / Non-Goals
Goals:
- 为已持久化换货单提供稳定、可查询的迁移状态和安全失败原因。
- 保持业务数据迁移和换货完成的原子性,并让物流换货的失败可由超级管理员或平台用户重试。
- 兼容既有响应字段和历史换货数据。
Non-Goals:
- 不改变直接换货创建失败即整体回滚、无换货单留存的现有行为。
- 不修改迁移项目、增加迁移明细表、迁移手机号—资产关联,或改变资产归属和个人客户—资产绑定的既有换货动作。
- 不新增列表筛选或路由;换货导出仅新增一列迁移状态,不导出失败原因。
- 不提供迁移失败换货单的放弃或取消出口:
failed单只能通过重试成功离开,如需放弃能力须另立需求。
Decisions
1. 使用状态字段取代布尔字段推断
在 tb_exchange_order 新增非空 migration_status 和非空 migration_failure_reason。状态使用字符串 not_migrated、pending、migrated、failed,中文名称仅在应用层投影;失败原因最长 500 字符且为空表示无失败原因。
新字段表达完整结果,保留 migrate_data、migration_completed 和 migration_balance 供兼容客户端及既有业务使用。物流换货创建即写 not_migrated(创建阶段不写 migrate_data);发货时按请求 migrate_data 写入 pending 或 not_migrated;成功完成写入 migrated、清空失败原因,并同步既有 migration_completed 与 migration_balance。
新增列带默认值,迁移按「ADD COLUMN(带默认值)→ 以既有字段回填 → 添加 chk_exchange_order_migration_status 约束校验四值」顺序执行;数据库层白名单与 pkg/constants 取值一致。
备选方案是在现有两个布尔字段上叠加前端规则。放弃原因是无法表达失败和失败原因,且容易把待迁移与失败混淆。
2. 主事务回滚后以短事务落失败状态与审计
业务数据迁移仍与换货完成共享原有 GORM 事务。任何一步失败均回滚资产状态、归属、客户绑定、钱包、套餐和标签的本次修改,确保重试从完整且未部分迁移的事实开始。
外层识别到迁移失败后,另开短事务,在同一个短事务内写入失败状态与失败审计;该短事务不得与已回滚的主事务共用连接或事务。条件更新为「换货单仍处于可确认完成状态」:RowsAffected 为 0 时跳过状态写入但仍写失败审计,避免并发确认下把已成功完成的换货单改回 failed。
失败原因按固定规则安全化:只拼接 AppError 链上的中文 Message,丢弃非 AppError 的底层 cause,按 rune 截断至不超过 500 字符;数据库等基础设施错误降级为固定安全摘要;业务类原因(例如旧资产钱包存在冻结余额)保留可见。禁止把数据库、外部服务或敏感载荷原文写入该字段。
备选方案是让迁移失败提交部分换货结果。放弃原因是会产生无法可靠补偿的钱包和套餐事实,且违背本期整套迁移原子执行的产品边界。
3. 只为失败的物流换货增加受限重试
保持既有确认完成入口。换货单为物流流程、业务状态仍为已发货待确认且迁移状态为 failed 时,仅超级管理员或平台用户可再次确认。授权以锁内判定为准:用例在 FOR UPDATE 锁定换货单后读取 migration_status;事务外预读只用于快速拒绝,不作为授权依据。授权通过后从钱包余额开始重新执行全部迁移。未失败的换货沿用现有可确认权限和状态门禁,代理既有完成权限不被削弱。
直接换货继续创建即完成;其迁移失败会回滚整笔创建,不留换货单或失败状态,避免为单一失败路径引入新的直接换货中间状态与重试接口。
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 DEFAULT 'not_migrated'、migration_failure_reason varchar(500) NOT NULL DEFAULT '',并以chk_exchange_order_migration_status约束四值;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:沿用现有创建入参和权限。物流单创建时恒为not_migrated(创建阶段不写migrate_data,迁移意图在发货时确定);直接换货仍在同一创建事务内完成,任一步失败则整个创建回滚,不持久化换货单、不产生failed状态。POST /exchanges/:id/ship:沿用既有物流状态机和发货字段;按请求migrate_data写入pending或not_migrated,不改变既有物流状态机。POST /exchanges/:id/complete:先在同一事务锁定换货单并验证既有“已发货待确认”状态和数据范围。not_migrated只执行固有资产归属及个人客户绑定;pending执行钱包余额、有效套餐使用、累计充值、资产标签的完整迁移及固有动作。成功时写migrated、清空失败原因、同步migration_completed与migration_balance,并写完成时间和既有成功审计。
失败与受限重试
- 当
pending迁移任一步失败时,主事务必须回滚资产归属、个人客户绑定、钱包、套餐、累计字段、标签和完成状态;外层另开短事务,在同一短事务内写failed、按上述规则安全化的失败原因与失败审计,条件为换货单仍是可确认完成状态;RowsAffected为 0 时跳过状态写入但仍写审计。该短事务不与已回滚的主事务共用连接或事务,符合ENG-TX-001的例外条件(回滚后的 failed/denied 事实与审计使用独立短事务,且以业务单仍处于允许该失败事实的状态为条件更新)。 - 对
migration_status=failed的物流单,POST /exchanges/:id/complete仅超级管理员或平台用户可重试;授权以FOR UPDATE锁定后读到的migration_status为准,事务外预读只做快速拒绝。授权通过后从钱包余额开始重跑全部迁移,禁止仅重试某一子项。非超级管理员、非平台账号返回无权,非失败单沿用既有完成状态门禁、不削弱代理既有完成权限,不将完成接口变成通用重复执行入口。 - 手机号—资产关联永不在上述动作中读取、复制或删除;新资产后续按自身 H5 手机号绑定规则处理。
读取行为
GET /exchanges与GET /exchanges/:id沿用既有换货数据范围,返回新状态字段;旧migrate_data、migration_completed、migration_balance保持原响应兼容,但调用方不得再以其组合判断迁移结果。- 换货导出沿用既有导出数据范围,在「状态」列之后新增一列迁移状态并使用中文名称;导出不输出失败原因,导出列不参与任何迁移、完成或数据范围规则。
Risks / Trade-offs
- [失败原因可能包含底层敏感或不稳定信息] → 只拼接 AppError 链上的中文
Message、丢弃非 AppError 的cause并按 rune 截断至 500 字符;数据库等基础设施错误降级为固定安全摘要,业务类原因保留可见,禁止写入数据库、外部服务或敏感载荷原文。 - [失败状态写入与失败审计的短事务二次异常] → 状态与审计同属一个回滚后短事务,
RowsAffected为 0 时跳过状态写入仍写审计;短事务异常复用既有换货失败审计的次级故障记录方式,并返回原失败、保留诊断。 - [并发确认造成重复迁移] → 重用换货单
FOR UPDATE锁与预期业务状态更新;只有仍为failed的失败重试可以进入受限路径。 - [旧客户端只读取布尔字段] → 保持原字段及其成功语义,新字段只增不删。
Migration Plan
- 在隔离数据库执行新迁移(ADD COLUMN 带默认值 → 按既有字段回填 → 添加
chk_exchange_order_migration_status约束),核对历史映射、非空与四值约束以及 down 后 Schema。 - 发布同时包含迁移、写侧状态转换、列表/详情 DTO 投影、换货导出新列和审计更新的版本。
- 发生应用回滚时,先回滚应用至仍兼容新增列的版本;仅在确认没有依赖新状态的数据或功能后执行 down 迁移。