## MODIFIED Requirements ### Requirement: 当前主套餐过期后自动激活下一个 系统 SHALL 在生效中或已用完的主套餐到期时,先提交旧主套餐和关联加油包的状态变化,再通过现有套餐激活应用能力同步推进同一载体的队首待生效主套餐;过期接续路径 MUST NOT 在旧主套餐事务内投递 `package:queue:activation` 任务。 成功激活队首套餐时,系统 SHALL 在套餐激活事务内同步追加现有 `card.observation.series.requested` Outbox;任一写入失败时激活事务 MUST 回滚。 #### Scenario: 旧主套餐提交过期后同步接续队首套餐 - **WHEN** 轮询处理一个已到期的 `status=1` 或 `status=2` 主套餐,且同载体存在队首待生效套餐 - **THEN** 系统先提交旧主套餐 `status=3` 和关联加油包失效的数据库事务 - **AND** 事务提交后调用现有套餐激活应用能力 - **AND** 应用能力在新事务内重新校验占位状态并激活队首套餐 - **AND** 过期接续路径不投递 `package:queue:activation` 任务 #### Scenario: 激活与卡观测 Outbox 原子提交 - **WHEN** 队首套餐满足激活条件 - **THEN** 系统在同一事务内把套餐推进为 `status=1`并写入激活、到期及适用的重置时间 - **AND** 同一事务追加稳定的 `card.observation.series.requested` Outbox - **AND** Outbox 写入失败时套餐状态回滚为待生效 #### Scenario: 过期事务失败时不接续 - **WHEN** 更新旧主套餐或级联失效加油包的事务失败 - **THEN** 事务回滚 - **AND** 系统不得调用队首套餐激活能力 - **AND** 下一轮过期扫描仍可重新处理该旧主套餐 #### Scenario: 提交后进程退出由孤儿扫描恢复 - **WHEN** 旧主套餐过期事务已经提交,但进程在调用接续能力前退出 - **THEN** 待生效套餐保持 `status=0` - **AND** 下一轮孤儿扫描识别该载体并直接调用同一套餐激活能力 #### Scenario: 无待生效套餐时保持无套餐状态 - **WHEN** 旧主套餐过期事务提交后,同载体不存在有效的 `status=0` 主套餐 - **THEN** 套餐激活应用能力不推进任何记录 - **AND** 载体进入无主套餐状态并沿用既有停机检查流程 ## ADDED Requirements ### Requirement: 孤儿待生效套餐必须公平恢复 系统 SHALL 在数据库中先选出每个卡或设备载体唯一的队首待生效主套餐,并排除仍存在 `status IN (1,2)` 占位主套餐的载体,最后才对真实孤儿结果执行单轮上限;系统 MUST NOT 先从全库待生效记录截取固定窗口再逐条判断占位状态。 队首顺序 SHALL 为 `priority ASC, created_at ASC, id ASC`。单轮最多处理 100 个真实孤儿载体,同一载体不得占用多个恢复名额。每个候选 SHALL 直接调用现有套餐激活应用能力,不投递 `package:queue:activation` 任务。 #### Scenario: 前一百条待生效记录均有占位套餐 - **WHEN** 全库排序靠前的 100 条待生效记录所属载体均存在 `status IN (1,2)` 主套餐,且窗口之后存在一条无占位主套餐的真实孤儿 - **THEN** 数据库先排除这 100 条非孤儿记录 - **AND** 真实孤儿进入本轮恢复候选 - **AND** 系统直接调用套餐激活应用能力推进其队首套餐 #### Scenario: 同一载体存在多条待生效套餐 - **WHEN** 一个无占位主套餐的载体存在多条 `status=0` 主套餐 - **THEN** 本轮只选择 `priority` 最小、其次 `created_at` 最早、最后 `id` 最小的一条 - **AND** 该载体只占用一个恢复名额 - **AND** 后续套餐继续保持排队状态 #### Scenario: 生效中和已用完套餐均属于占位 - **WHEN** 待生效套餐所属载体仍有 `status=1` 或 `status=2` 的主套餐 - **THEN** 该载体不得进入孤儿恢复候选 - **AND** 待生效套餐保持排队状态 #### Scenario: 单轮真实孤儿超过上限 - **WHEN** 数据库存在超过 100 个真实孤儿载体 - **THEN** 系统按稳定顺序选择前 100 个载体调用接续能力 - **AND** 未选中的真实孤儿在后续轮询中继续具备候选资格 ### Requirement: 同步接续结果必须准确记录 系统 SHALL 仅在套餐激活应用能力返回 `activated=true` 时记录本轮激活成功。锁冲突、占位状态变化、等待实名和已完成幂等结果不得记录为新的激活成功。 #### Scenario: 本轮成功激活 - **WHEN** 指定载体无占位主套餐且队首套餐满足激活条件 - **THEN** 应用能力返回 `activated=true` - **AND** 轮询记录包含套餐使用记录、载体和触发来源的成功结果 #### Scenario: 激活锁冲突 - **WHEN** 同步接续未获得载体级 Redis 激活锁 - **THEN** 应用能力返回现有套餐激活冲突错误 - **AND** 本轮不得记录激活成功 - **AND** 下一轮过期或孤儿扫描继续恢复 #### Scenario: 条件暂不满足 - **WHEN** 应用能力复检发现载体已有占位主套餐或队首仍等待实名 - **THEN** 应用能力不修改套餐状态 - **AND** 轮询记录明确原因 - **AND** 后续扫描仍可重新判断