Files
junhong_cmp_fiber/openspec/changes/fix-exchange-migration-package-state/design.md
邹进 96ff9c1e35
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 12m0s
fix: 换货迁移按套餐是否已开始与新资产占位决定主套餐状态
- 未开始的主套餐(无生效、到期时间且无用量)迁移为待生效,新资产不满足实名条件(行业卡/已实名/无需实名)时标记等实名激活
- 已开始的主套餐在新资产无当前主套餐时原样迁移;新资产已有当前主套餐时改为待生效排队、清空周期与用量
- 迁移后重排新资产主套餐优先级,保证同一载体只有一个当前主套餐
- 换货审计记录套餐状态、优先级、用量的前后值

OpenSpec: fix-exchange-migration-package-state

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 23:25:06 +09:00

98 lines
6.9 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
`migratePackageUsageWithTx`(`internal/service/exchange/migration.go:104`)在换货完成事务内,把旧资产全部待生效、生效中、已用完的套餐使用记录批量改载体(`iot_card_id` / `device_id`),其余字段不动,日流量记录只刷新 `updated_at`。
相关现状:
- 当前主套餐判断:`packagepkg.FindCurrentMainPackageForQueue` / `HasCurrentMainPackageForQueue`(`internal/service/package/addon_main_package.go`),口径为生效中或已用完、未到期。订单、自动续费、自动购包都用它决定新主套餐是否排队。
- 待生效主套餐的接续激活:
- 前一个到期:到期任务 `PackageActivationHandler.processExpiredPackage` → 激活队首。
- 无占位主套餐:`findAndActivateOrphanPackages`(`internal/polling/package_activation_handler.go:168`)定时扫描"有待生效、无生效中或已用完主套餐"的载体,取队首(优先级、创建时间、标识)提交 `ActivateSpecificPackage`;后者对标记等实名激活的套餐检查实名,未标记的直接激活,激活时从当天起重新计算有效期与重置时间,并处理复机、轮询、审计等副作用。
- 实名后:`ActivateByRealname` 激活标记等实名激活的套餐。
- `activatePendingUsage` 激活时不重置 `data_usage_mb`。
- 实名条件口径:`StopResumeService.isRealnameOK`(`internal/service/iot_card/stop_resume_service.go:332`)认行业卡、已实名、无需实名策略;套餐激活侧的 `isCarrierRealnamed` 只认已实名(另开 Change 处理,本 Change 不改)。
- 预计到期推算 `internal/query/packageexpiry/query.go` 在多个生效/已用完主套餐、待生效排在当前之前时返回数据异常。
## Goals / Non-Goals
**Goals:**
- 换货迁移后新资产最多一个当前主套餐,主套餐优先级连续、无重复。
- 未开始的套餐迁移后不直接生效;新资产满足实名条件时由既有机制接续激活。
- 已开始的套餐在新资产无占位时无感知继续使用。
**Non-Goals:**
- 不改换货接口、不拦截换货、不改钱包余额、累计充值、标签迁移。
- 不改订单、自动续费、自动购包、实名激活的实名判断。
- 不在代码里修复存量数据。
- 不处理加油包与主套餐的重新挂靠。
## Decisions
### D1 "已开始"的判定
满足任一即已开始:`activated_at` 非空、`expires_at` 非空、`data_usage_mb > 0`、存在日流量记录、状态为已用完。
- 老系统迁移数据存在"状态生效中、生效时间为空、到期时间固定"的记录(2026-11-04 换货单 1095–1104、1108)。固定到期日是老系统卖给客户的权益,换货不应改写,因此有到期时间即视为已开始;真正未开始的只剩从未激活的待生效套餐。
- 换货单 1107 的套餐生效时间为空但有 10328 MB 用量,需要用量与日流量兜底。
备选一:只看 `activated_at`。会把 1107 判为未开始、清空进度,拒绝。
备选二:不看 `expires_at`。会把老系统固定到期日的套餐改为待生效、实名后重算 12 个月,改变客户已有到期日,拒绝。
### D2 实名条件判断放在换货服务内
新增换货侧私有函数,按 spec 口径判断新资产是否满足实名条件:卡看行业卡、实名状态、适用实名策略;设备看设备策略与已绑定卡。
- 只影响等实名激活标记,口径需与停复机一致。
- `isRealnameOK` 是 `StopResumeService` 私有方法且只接受卡;为一处调用把它导出或抽公共包,会顺带改动停复机模块,违反"不主动重构"。
备选:复用套餐侧 `isCarrierRealnamed`。不认行业卡,迁到行业卡的套餐会永远等实名,拒绝。
### D3 换货事务内不激活,交给孤儿扫描与实名激活
未开始与排队的主套餐只写为待生效和正确的等实名激活标记。
- 新资产无占位主套餐且无需等实名:孤儿扫描下一轮提交 `ActivateSpecificPackage`,统一走激活副作用(审计、观测、优先轮询、复机)。
- 需要等实名:新资产实名后由 `ActivateByRealname` 激活。
备选:换货服务注入 `ActivationService` 在事务后立即激活。要新增依赖与失败补偿,且孤儿扫描已覆盖同一场景,拒绝。代价是激活最多延迟一个扫描周期。
### D4 排队的已开始套餐清零用量、重算完整周期
与 M15 排队购买一致:排队主套餐轮到时从激活当天起算完整周期。用量清零是因为 `activatePendingUsage` 不重置用量,不清零会把旧周期用量带入新周期。换货前用量保留在日流量记录中作为历史。与 2026-11-04 生产手工修正(46981)结果一致。
备选:保留剩余天数。需要新增"剩余时长"字段并改激活逻辑,影响面超出本 Change,拒绝。
### D5 统一重排优先级
迁移后对新资产全部未结束主套餐(待生效、生效中、已用完且未到期)按"当前主套餐 → 新资产原有待生效 → 迁入待生效"重排为 1..n。
- 现有新购排队写 `max + 1`,迁移场景下新资产原有待生效与迁入的生效主套餐可能都是 1,只追加无法消除重复。
- 孤儿扫描与预计到期推算都按优先级、创建时间、标识排序,连续编号后结果确定。
### D6 锁与事务
在换货完成事务内先对新、旧资产的主套餐记录 `SELECT ... FOR UPDATE`,再判断与写入,避免与同时进行的购买、到期接续交叉。沿用换货现有事务,失败整体回滚并按既有规则记录迁移失败。
### D7 审计
状态、优先级或用量发生变化的记录,通过换货服务现有 `auditWriter` 写套餐使用记录变更审计,前后值包含状态、优先级、用量、生效、到期、重置时间与等实名激活标记,附换货单号。仅改载体、其余不变的记录不额外写审计。
## Risks / Trade-offs
- [激活延迟] 无需等实名的未开始套餐要等孤儿扫描下一轮才生效 → 扫描周期分钟级;换货后新资产本无占位套餐,客户影响小。
- [旧资产已过期未处理的主套餐] 原样迁移后,到期任务处理前新资产可能短暂出现两条状态生效中的记录 → 其中一条已过期,到期任务会处理;扣量与推算以未到期为准。
- [实名口径两处维护] 换货侧与停复机侧各有一份 → 在函数注释注明口径来源,后续"统一实名判断"Change 合并。
- [清零用量] 排队套餐换货前的周期内用量不再显示在套餐上 → 日流量记录保留,可追溯。
## Migration Plan
- 无数据库迁移,无配置变化。
- 按维护者生产发布流程手工发布;回滚即回退二进制。
- 存量不修正:2026-11-04 换货单 1095–1104、1108 的 11 条主套餐有固定到期日,按 D1 属于已开始,现状即为本规则下的结果。
## Open Questions
无。