fix: 换货迁移按套餐是否已开始与新资产占位决定主套餐状态
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 12m0s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 12m0s
- 未开始的主套餐(无生效、到期时间且无用量)迁移为待生效,新资产不满足实名条件(行业卡/已实名/无需实名)时标记等实名激活 - 已开始的主套餐在新资产无当前主套餐时原样迁移;新资产已有当前主套餐时改为待生效排队、清空周期与用量 - 迁移后重排新资产主套餐优先级,保证同一载体只有一个当前主套餐 - 换货审计记录套餐状态、优先级、用量的前后值 OpenSpec: fix-exchange-migration-package-state Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-11-04
|
||||
@@ -0,0 +1,97 @@
|
||||
## 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
|
||||
|
||||
无。
|
||||
@@ -0,0 +1,37 @@
|
||||
## Why
|
||||
|
||||
换货迁移套餐时(`internal/service/exchange/migration.go` 的 `migratePackageUsageWithTx`),系统只把套餐使用记录的载体改成新资产,状态、优先级、用量、有效期原样保留,不看套餐是否真正开始过,也不看新资产上已有的套餐。生产已出现后果:
|
||||
|
||||
- 2026-11-03 换货单 EXC20261103174239616885:新卡 89860624630055040571 自己有生效主套餐,迁过来的主套餐也是生效中,一张卡两个生效主套餐,流量重复扣减,预计到期时间显示"数据异常"(已手工修正,见 `docs/data-fix/2026-11-04-换货迁移套餐双生效数据修正.md`)。
|
||||
|
||||
业务原则:换货只是换载体,不改变套餐所处的阶段;同一载体同时只能有一个生效主套餐(《八月迭代后端修正技术方案汇总》3.15 M15);新资产已有生效套餐时换货不拦截、叠加数据(同文 3.12)。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 换货迁移主套餐时,按以下顺序决定迁移后的状态:
|
||||
1. **套餐是否已开始**:有生效时间、或有到期时间、或有用量、或有日流量记录、或状态为已用完,视为已开始;否则视为未开始。老系统迁移数据中"生效中、无生效时间、有固定到期日"的套餐按已开始处理,保留原到期日。
|
||||
2. **未开始的主套餐**:一律以待生效迁移,清空生效时间、到期时间与重置时间;新资产不满足实名条件时标记为等实名激活。实名条件与停复机口径一致:行业卡、已实名、或实名策略为无需实名,任一满足即视为满足。
|
||||
3. **已开始的主套餐**:新资产上没有当前主套餐(生效中或已用完、且未到期)时,原样迁移、继续生效;新资产已有当前主套餐时,改为待生效排在后面,清空生效时间、到期时间与重置时间,已用量清零,等轮到时从激活当天起重新计算完整周期。
|
||||
- 迁移后新资产上全部未结束主套餐按"当前主套餐 → 新资产原有待生效 → 迁入的主套餐(按原顺序)"统一重排优先级,不再出现优先级重复或待生效排在当前套餐之前。
|
||||
- 加油包仍原样迁移,继续挂在原主套餐下。
|
||||
- 未开始或排队的主套餐迁移后不在换货事务内激活;新资产无占位主套餐且满足激活条件时,由现有孤儿套餐扫描或实名激活流程接续生效。
|
||||
- 换货迁移导致状态、优先级或用量变化的套餐记录写审计,保留前后值。
|
||||
- 不拦截换货,不改换货接口入参与出参。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
无。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `order-refund-exchange`:新增"换货迁移后的主套餐状态"要求,规定迁移主套餐时按"是否已开始、新资产实名条件、新资产是否已有当前主套餐"决定状态与排队顺序。
|
||||
|
||||
## Impact
|
||||
|
||||
- 代码:`internal/service/exchange/migration.go`(`migratePackageUsageWithTx` 改为按规则逐条处理主套餐、重排优先级);新增换货侧的实名条件判断(行业卡、已实名、无需实名,口径同 `internal/service/iot_card/stop_resume_service.go` 的 `isRealnameOK`);当前主套餐判断沿用 `internal/service/package` 的 `FindCurrentMainPackageForQueue` 口径。
|
||||
- 接口:无变化。
|
||||
- 数据库:无迁移。
|
||||
- 存量数据:不修正。2026-11-04 换货单 1095–1104、1108 迁入的 11 条主套餐有固定到期日,按本规则属于已开始、新卡无当前主套餐时原样迁移,与现状一致;新卡未实名且已停机,未产生用量。
|
||||
- 不涉及:其他入口"等实名激活"判断不认行业卡与无需实名的问题(订单、自动续费、自动购包、实名激活),另开 Change 处理。
|
||||
@@ -0,0 +1,79 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 换货迁移后的主套餐状态
|
||||
|
||||
选择业务数据迁移的换货完成时,系统 SHALL 逐条决定旧资产主套餐使用记录(`master_usage_id` 为空,状态为待生效、生效中或已用完)迁移到新资产后的状态,保证新资产同一时刻最多一个生效中或已用完的未到期主套餐。
|
||||
|
||||
- **已开始**:主套餐有生效时间、或有到期时间、或已用量大于 0、或存在日流量记录、或状态为已用完,视为已开始;否则视为未开始。
|
||||
- **实名条件**:新资产为卡时,卡为行业卡、或已实名、或其适用实名策略(已绑定设备时取设备策略,否则取卡策略)为无需实名,视为满足;新资产为设备时,设备实名策略为无需实名、或任一已绑定卡为行业卡或已实名,视为满足。
|
||||
- **当前主套餐**:载体上状态为生效中或已用完、且未到期(到期时间为空或晚于当前时间)的主套餐。
|
||||
|
||||
规则:
|
||||
|
||||
1. 未开始的主套餐 MUST 以待生效迁移,生效时间、到期时间、上次重置时间、下次重置时间 MUST 清空;新资产不满足实名条件时 MUST 标记为等实名激活,满足时 MUST NOT 标记。
|
||||
2. 已开始的主套餐,若新资产迁移前没有当前主套餐,且它是旧资产按优先级、创建时间、标识排序的第一条已开始的当前主套餐,SHALL 原样迁移,状态、用量、有效期与重置时间不变。
|
||||
3. 其余已开始的主套餐 MUST 以待生效迁移,生效时间、到期时间、上次重置时间、下次重置时间 MUST 清空,已用量 MUST 清零,MUST NOT 标记为等实名激活;日流量记录 MUST 保留为历史。
|
||||
4. 迁移完成后,新资产全部待生效、生效中、已用完的主套餐 SHALL 按"当前主套餐 → 新资产原有待生效主套餐(保持原顺序)→ 旧资产迁入的待生效主套餐(保持原顺序)"重新编排优先级为 1 起连续整数。
|
||||
5. 旧资产上已到期但尚未被到期任务处理的主套餐 SHALL 原样迁移,不参与排队与优先级编排。
|
||||
6. 加油包 SHALL 原样迁移并保持与原主套餐的关联。
|
||||
7. 换货 MUST NOT 因新资产已有钱包余额或套餐而被拒绝;换货事务内 MUST NOT 激活待生效主套餐。
|
||||
8. 因本规则改变状态、优先级或用量的每条套餐使用记录 SHALL 写入审计,包含变更前后值与换货单号。
|
||||
|
||||
#### Scenario: 新资产无套餐,迁入在用主套餐
|
||||
|
||||
- **GIVEN** 旧资产有一条生效中、已用量大于 0 的主套餐和一条挂在其下的加油包,新资产没有任何套餐
|
||||
- **WHEN** 换货选择业务数据迁移并确认完成
|
||||
- **THEN** 主套餐在新资产上仍为生效中,用量、到期时间与重置时间不变,优先级为 1;加油包原样迁移
|
||||
|
||||
#### Scenario: 新资产已有生效主套餐,迁入在用主套餐
|
||||
|
||||
- **GIVEN** 新资产有一条生效中的主套餐,旧资产有一条生效中、已用量大于 0 的主套餐
|
||||
- **WHEN** 换货选择业务数据迁移并确认完成
|
||||
- **THEN** 新资产原主套餐保持生效、优先级为 1;迁入主套餐变为待生效、优先级为 2,生效时间、到期时间与重置时间为空,已用量为 0,未标记等实名激活,原日流量记录保留
|
||||
|
||||
#### Scenario: 新资产已用完但未到期
|
||||
|
||||
- **GIVEN** 新资产主套餐为已用完且到期时间晚于当前时间,旧资产有生效中的已开始主套餐
|
||||
- **WHEN** 换货选择业务数据迁移并确认完成
|
||||
- **THEN** 迁入主套餐变为待生效并排在新资产主套餐之后
|
||||
|
||||
#### Scenario: 新资产主套餐已过期但未被处理
|
||||
|
||||
- **GIVEN** 新资产主套餐状态为生效中但到期时间早于当前时间,旧资产有生效中的已开始主套餐
|
||||
- **WHEN** 换货选择业务数据迁移并确认完成
|
||||
- **THEN** 新资产视为没有当前主套餐,迁入主套餐保持生效中
|
||||
|
||||
#### Scenario: 未开始的主套餐迁到未实名普通卡
|
||||
|
||||
- **GIVEN** 旧资产主套餐为待生效,没有生效时间、到期时间,已用量为 0、没有日流量记录;新资产为未实名普通卡,实名策略为购买后实名
|
||||
- **WHEN** 换货选择业务数据迁移并确认完成
|
||||
- **THEN** 迁入主套餐变为待生效并标记等实名激活,生效时间、到期时间与重置时间为空,换货事务内不激活
|
||||
|
||||
#### Scenario: 老系统迁移的固定到期日套餐
|
||||
|
||||
- **GIVEN** 旧资产主套餐状态为生效中,没有生效时间、已用量为 0,但有固定到期时间;新资产没有任何套餐
|
||||
- **WHEN** 换货选择业务数据迁移并确认完成
|
||||
- **THEN** 该主套餐视为已开始,在新资产上原样保持生效中,到期时间不变
|
||||
|
||||
#### Scenario: 未开始的主套餐迁到行业卡
|
||||
|
||||
- **GIVEN** 旧资产主套餐未开始,新资产为未实名的行业卡且没有当前主套餐
|
||||
- **WHEN** 换货选择业务数据迁移并确认完成
|
||||
- **THEN** 迁入主套餐变为待生效且不标记等实名激活,之后由孤儿套餐扫描接续激活,有效期从激活当天起算
|
||||
|
||||
#### Scenario: 新资产只有待生效主套餐
|
||||
|
||||
- **GIVEN** 新资产只有一条优先级为 1 的待生效主套餐,旧资产有一条生效中的已开始主套餐
|
||||
- **WHEN** 换货选择业务数据迁移并确认完成
|
||||
- **THEN** 迁入主套餐保持生效中、优先级为 1,新资产原待生效主套餐优先级变为 2,不出现优先级重复
|
||||
|
||||
#### Scenario: 旧资产有多条主套餐
|
||||
|
||||
- **GIVEN** 新资产有一条生效中的主套餐,旧资产有一条生效中的已开始主套餐和一条待生效主套餐
|
||||
- **WHEN** 换货选择业务数据迁移并确认完成
|
||||
- **THEN** 两条迁入主套餐均为待生效,按旧资产原顺序依次排在新资产主套餐之后,优先级分别为 2、3
|
||||
|
||||
#### Scenario: 迁移后预计到期时间可计算
|
||||
|
||||
- **WHEN** 任一上述场景换货完成后查询新资产预计到期时间
|
||||
- **THEN** 系统返回推算结果,不返回数据异常
|
||||
@@ -0,0 +1,23 @@
|
||||
## 1. 迁移规则实现
|
||||
|
||||
- [x] 1.1 `internal/service/exchange/` 新增换货侧实名条件判断:卡为行业卡、已实名,或适用实名策略(已绑定设备取设备策略,否则取卡策略)为无需实名;设备为无需实名策略,或任一已绑定卡为行业卡或已实名。注释注明口径同 `StopResumeService.isRealnameOK`。
|
||||
- [x] 1.2 新增"套餐是否已开始"判断:`activated_at` 非空、`expires_at` 非空、`data_usage_mb > 0`、存在日流量记录、状态为已用完任一满足。
|
||||
- [x] 1.3 改造 `migratePackageUsageWithTx`:事务内锁定新、旧资产主套餐记录;按 `FindCurrentMainPackageForQueue` 同一口径(生效中或已用完且未到期)在已锁定记录上判断新资产迁移前是否有当前主套餐;加油包与旧资产已到期主套餐原样改载体。
|
||||
- [x] 1.4 按 spec 规则 1–3 逐条处理旧资产主套餐:未开始 → 待生效、清空时间、按实名条件设置等实名激活标记;首条已开始的当前主套餐在新资产无当前主套餐时原样迁移;其余已开始 → 待生效、清空时间、用量清零、不标记等实名激活。
|
||||
- [x] 1.5 迁移后按"当前主套餐 → 新资产原有待生效 → 迁入待生效"把新资产未结束主套餐优先级重排为 1..n。
|
||||
- [x] 1.6 对状态、优先级或用量变化的记录通过换货 `auditWriter` 写审计(前后值、换货单号)。
|
||||
- [x] 1.7 返回的迁移套餐记录标识列表保持不变;日流量记录只保留,不删除。
|
||||
|
||||
## 2. 构建与文档
|
||||
|
||||
- [x] 2.1 `gofmt`、`go build ./cmd/api ./cmd/worker`。
|
||||
- [ ] 2.2 `openspec validate --all`、`./scripts/context-health.sh`。
|
||||
|
||||
## 3. 测试环境验证(AGENTS.md 规定自动化测试 N/A,不写单元测试)
|
||||
|
||||
- [ ] 3.1 按 ENG-TEST-001 在维护者指定测试环境造数,逐一验证 spec 全部场景:新资产无套餐;新资产已有生效;新资产已用完未到期;新资产已过期未处理;未开始迁到未实名普通卡;老系统固定到期日套餐原样迁移;未开始迁到行业卡(孤儿扫描后激活);新资产只有待生效;旧资产多条主套餐。
|
||||
- [ ] 3.2 每个场景换货后查询新资产预计到期时间不为数据异常,审计记录前后值正确。
|
||||
|
||||
## 4. 存量数据
|
||||
|
||||
- [x] 4.1 核对 2026-11-04 换货单 1095–1104、1108 共 11 条主套餐:均有固定到期日,按 D1 属于已开始、新卡无当前主套餐,现状与本规则一致,不修正。
|
||||
Reference in New Issue
Block a user