# Spec Delta: 主套餐排队生效机制（激活链修复 + 孤儿恢复）

## MODIFIED Requirements

### Requirement: 排队激活任务精准激活指定套餐

`HandlePackageQueueActivation` Asynq 任务处理器 SHALL 直接激活 payload 中 `PackageUsageID` 指定的具体套餐，不得重新执行"查找过期包"的发现流程。

`ActivationService` SHALL 提供 `ActivateSpecificPackage(ctx, packageUsageID uint) error` 方法，职责为：加载指定套餐 → 幂等检查 → 计算激活时间和过期时间 → 事务内更新 status=1 → 同步载体状态 → 触发复机回调。

激活前 SHALL 获取 `RedisPackageActivationLockKey(carrierType, carrierID)` 分布式锁（30 秒超时），防止并发激活同一载体。

#### Scenario: Asynq 任务激活指定的排队套餐

- **WHEN** `HandlePackageQueueActivation` 接收到 payload `{PackageUsageID: 2, CarrierType: "device", CarrierID: 1}`
- **THEN** 系统加载 id=2 的 PackageUsage 记录
- **AND** 若该记录 status=0（待生效），则将其激活（status → 1，activated_at → now，expires_at → 计算值）
- **AND** 激活成功后更新关联载体状态

#### Scenario: 幂等保护：已激活的套餐不重复处理

- **WHEN** `HandlePackageQueueActivation` 接收到的套餐 id 对应的套餐已是 status=1（生效中）
- **THEN** 系统直接返回成功，不做任何修改
- **AND** 不产生重复激活的副作用

#### Scenario: 激活时正确计算到期时间

- **WHEN** `ActivateSpecificPackage` 激活一个排队的主套餐
- **THEN** `activated_at` 为激活时刻（`time.Now()`）
- **AND** `expires_at` 根据关联 Package 的 `CalendarType`（`natural_month` 或 `by_day`）、`DurationMonths`、`DurationDays` 正确计算
- **AND** 若 `DataResetCycle != "none"`，则同步更新 `next_reset_at`

---

## ADDED Requirements

### Requirement: 孤儿待激活套餐自动恢复

系统 SHALL 在每次 `HandlePackageActivationCheck` 执行时（每 10 秒）检测孤儿载体，即：存在 `status=0 AND master_usage_id IS NULL AND pending_realname_activation=false` 的主套餐，但该载体不存在任何 `status=1` 的主套餐。

检测到孤儿后，系统 SHALL 为每个孤儿载体提交 `TaskTypePackageQueueActivation` Asynq 任务，激活其 priority 最小的待生效主套餐。单次扫描处理上限 SHALL 为 100 个载体。

此机制 SHALL 覆盖以下两种场景：
1. Worker 服务重启后，重启前已标记过期但 Asynq 任务丢失的载体
2. Asynq 任务 MaxRetry 耗尽后进入 dead queue 的载体

#### Scenario: 重启后孤儿套餐在 10 秒内被自动恢复

- **WHEN** Worker 服务重启，数据库中存在载体（device_id=1）无 status=1 主套餐、但有 status=0 且 pending_realname_activation=false 的主套餐
- **THEN** Worker 启动后的首次 `HandlePackageActivationCheck`（最长 10 秒内）检测到该孤儿载体
- **AND** 提交激活任务
- **AND** 待生效套餐在 20 秒内完成激活（status → 1）

#### Scenario: Asynq 任务失败后孤儿机制兜底

- **WHEN** 激活任务因 Asynq MaxRetry 耗尽进入 dead queue，载体套餐仍为 status=0
- **THEN** 孤儿扫描在下一个 10 秒周期内检测到该载体
- **AND** 重新提交激活任务
- **AND** 套餐最终完成激活

#### Scenario: 有生效套餐的载体不被孤儿扫描触发

- **WHEN** 载体已有 status=1 的主套餐，同时存在 status=0 的排队套餐
- **THEN** 孤儿扫描不为该载体提交激活任务
- **AND** 排队套餐继续等待当前套餐过期后的正常激活流程
