Files
邹进 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

6.9 KiB
Raw Permalink Blame History

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

无。