## 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 无。