# 套餐队列激活当前行为 ## Purpose 描述主套餐到期后的队列接续、无占位主套餐时的孤儿恢复,以及异步激活结果和锁冲突的重试语义,确保待生效套餐不因候选窗口或事务可见性而长期无法激活。 ## Requirements ### Requirement: 当前主套餐过期后自动激活下一个 系统 SHALL 在生效中或已用完主套餐到期时,先提交旧主套餐及关联加油包的状态事务,再投递同一载体队首待生效套餐的现有 Asynq 激活任务;系统 MUST NOT 在旧套餐事务提交前投递任务。 #### Scenario: 事务提交后投递队首套餐 - **WHEN** 轮询处理一个到期的 `status=1` 或 `status=2` 主套餐,且存在队首待生效套餐 - **THEN** 系统先提交旧主套餐 `status=3` 和关联加油包 `status=4` 的事务 - **AND** 提交成功后查询稳定队首并投递 `package:queue:activation` - **AND** 消费者读取时旧主套餐不再处于 `status IN (1,2)` #### Scenario: 过期事务失败 - **WHEN** 更新旧主套餐或关联加油包失败 - **THEN** 事务回滚 - **AND** 系统不得投递下一套餐激活任务 - **AND** 下一轮过期扫描仍可重新处理 #### Scenario: 提交后入队失败 - **WHEN** 旧套餐事务已经提交,但 Asynq 入队失败或进程退出 - **THEN** 队首套餐保持 `status=0` - **AND** 周期孤儿扫描重新发现并补投该套餐 #### Scenario: 无待生效套餐 - **WHEN** 旧套餐过期提交后不存在有效队首待生效套餐 - **THEN** 系统不投递激活任务 - **AND** 沿用既有无套餐停机检查 ### Requirement: 孤儿待生效套餐必须公平恢复 系统 SHALL 在数据库中先选择每个卡或设备载体唯一的队首待生效主套餐,并排除仍有 `status IN (1,2)` 占位主套餐的载体,最后才执行单轮 100 个真实孤儿上限。队首顺序 MUST 为 `priority ASC, created_at ASC, id ASC`。 #### Scenario: 固定窗口全部为非孤儿 - **WHEN** 排序靠前的 100 条待生效记录均有占位主套餐,窗口之后存在真实孤儿 - **THEN** 数据库先排除前 100 条非孤儿 - **AND** 窗口后的真实孤儿进入本轮候选并被投递 #### Scenario: 同一载体有多条 pending - **WHEN** 一个真实孤儿载体存在多条待生效主套餐 - **THEN** 本轮只选择 priority 最小、created_at 最早、id 最小的一条 - **AND** 该载体只占一个恢复名额 #### Scenario: 生效中或已用完套餐占位 - **WHEN** pending 所属载体存在 `status=1` 或 `status=2` 主套餐 - **THEN** 该载体不得进入孤儿候选 ### Requirement: Asynq 激活结果必须准确且可重试 系统 SHALL 仅在本次实际把套餐推进为 `status=1` 时记录新激活成功。Redis 激活锁冲突 MUST 返回错误,使 Asynq 按既有 `MaxRetry(3)` 重试,不得确认未执行任务成功。 #### Scenario: 锁冲突触发重试 - **WHEN** 消费者未取得载体级套餐激活锁 - **THEN** Service 返回套餐激活冲突错误 - **AND** Handler 将错误返回 Asynq - **AND** 本次不记录激活成功 #### Scenario: 本次实际激活 - **WHEN** 套餐为待生效、载体无占位主套餐且满足激活条件 - **THEN** Service 返回 `activated=true` - **AND** Handler 记录包含套餐使用记录和触发类型的成功日志 #### Scenario: 条件暂不满足 - **WHEN** Service 复检发现占位套餐或等待实名条件 - **THEN** Service 返回 `activated=false`且不修改套餐 - **AND** Handler 记录未激活原因,不记录成功