## Context

现有的套餐实名激活逻辑存在两个关联缺陷：

**缺陷 A（购买时）**：`order/service.go` 的 `activateMainPackage` 在判断 `expiry_base=from_activation` 时，无条件将套餐标记为 `pending_realname_activation=true`（status=0），未检查载体当前是否已实名。这个缺陷造成**双重故障**：

```
套餐 status=0（pending）
  ↓
tryResumeAfterPayment 检查 activatedCount = 0
  ↓
ResumeCardIfStopped 不触发
  ↓
① 套餐永久无法激活
② 购买后卡/设备不会自动开机（即使之前因无套餐而停机）
```

"先实名后购买"是代理商的典型囤货场景（如：设备出厂时已实名，用户激活时购买套餐），在此场景下套餐和开机均失效。

**缺陷 B（轮询时）**：`polling_realname_handler.go` 的 `triggerFirstRealnameActivation` 提交的 Asynq 任务硬编码 `carrier_type="iot_card"`，即使卡属于某个设备、该设备有 `pending_realname_activation=true` 的设备级套餐，也不会触发设备级激活，同样导致设备下的卡无法通过实名事件触发复机。

两个缺陷叠加导致：设备级套餐在"先实名后购买"和"购买后某张卡实名"两个场景均无法激活，且均无法触发自动复机。

**现有关键基础设施**：
- `DeviceSimBindingStore.GetActiveBindingByCardID(ctx, cardID)` — 通过卡 ID 查关联设备
- `DeviceSimBindingStore.ListByDeviceID(ctx, deviceID)` — 查设备绑定的所有卡
- `h.workerResult.Stores.DeviceSimBinding` — Worker 中已可用
- `ActivationService.ActivateByRealname(ctx, carrierType, carrierID)` — 已支持 `carrier_type="device"`，且**激活成功后内部会异步调用 `ResumeCardIfStopped(ct, cid)`**
- `StopResumeService.ResumeCardIfStopped("device", deviceID)` → `resumeDeviceCards`：遍历设备下因轮询原因停机的卡，逐卡检查实名状态后决定是否复机
- `tryResumeAfterPayment`（order service）：支付回调成功后，若 `activatedCount > 0` 则异步调用 `ResumeCardIfStopped`，支持 `iot_card` 和 `device` 两种载体类型

**复机的 stop_reason 约束**（现有设计，本次不变）：自动复机仅处理轮询系统引起的停机原因（`no_package`、`traffic_exhausted`、`not_realname`），`manual` 手动停机不会被自动复机——这是系统的有意设计，防止覆盖人工操作。

---

## Goals / Non-Goals

**Goals:**

- 购买 `from_activation` 套餐时，若载体当前已实名，直接激活（不进入 pending 状态），同时保障 `tryResumeAfterPayment` 的复机链路正常触发
- 卡首次实名时，同时触发其所属设备的设备级套餐激活，进而触发设备下符合条件卡的复机
- 修复后的逻辑统一、无歧义：`from_purchase` → 立即激活；`from_activation` → 检查实名再决策（已实名直接激活，未实名等待实名事件）

**Non-Goals:**

- 不引入新的数据库字段或迁移
- 不改变卡级套餐的现有激活流程
- 不处理历史存量被卡住的套餐（需手动 SQL 修复）
- 不改变孤儿套餐恢复机制（仅处理 `from_purchase` 场景）

---

## Decisions

### Decision 1：在 `activateMainPackage` 中内联实名状态检查

**方案 A（选择）**：在现有 `activateMainPackage` 的实名决策块（REALNAME-02 段）内扩展逻辑，直接用 `tx` 查询实名状态。

**方案 B**：抽取独立 `checkCarrierRealnamed(tx, carrierType, carrierID)` helper。

选 A 原因：修改点集中在一个函数的一个分支内，变更范围小，逻辑内聚，无需跨文件重构。

**实现细节**：
- `iot_card`：扩展已有的卡查询，同时 `SELECT "card_category", "real_name_status"`，避免额外查询
- `device`：新增一次子查询，利用已有的 `tb_device_sim_binding` 关系查询是否有已实名的绑定卡：
  ```sql
  SELECT COUNT(*) FROM tb_iot_card
  WHERE id IN (
    SELECT iot_card_id FROM tb_device_sim_binding
    WHERE device_id = ? AND bind_status = 1 AND deleted_at IS NULL
  ) AND real_name_status = 1
  ```

### Decision 2：在 `triggerFirstRealnameActivation` 之后添加 `triggerDeviceRealnameActivation`

**方案 A（选择）**：`PollingRealnameHandler` 新增 `deviceSimBindingStore` 字段 + `triggerDeviceRealnameActivation` 方法，在卡 0→1 时调用。

**方案 B**：在 `PackageActivationHandler.HandlePackageFirstActivation` 内部，收到 `carrier_type=iot_card` 后自动查关联设备再激活设备级套餐。

选 A 原因：语义更清晰——"卡实名"触发"设备套餐激活"属于实名事件的副作用，应由实名 Handler 发起；方案 B 会使激活 Handler 承担实名业务上下文的感知，违反单一职责。

**实现细节**：`triggerDeviceRealnameActivation` 调用 `deviceSimBindingStore.GetActiveBindingByCardID` 获取设备 ID，若无绑定则静默跳过，若有则提交 `carrier_type="device"` 的 `TaskTypePackageFirstActivation` 任务。

### Decision 3：`NewPollingRealnameHandler` 构造函数新增参数

直接在参数列表末尾追加 `deviceSimBindingStore *postgres.DeviceSimBindingStore`，`pkg/queue/handler.go` 的 `registerPollingHandlers` 传入已可用的 `h.workerResult.Stores.DeviceSimBinding`。

---

## Risks / Trade-offs

| 风险 | 缓解措施 |
|------|---------|
| 购买时多一次 DB 查询（device 需子查询）| 查询走索引（`tb_device_sim_binding.device_id` 有索引），影响可忽略 |
| 卡实名时提交多一个 Asynq 任务（device 任务）| 任务幂等：`ActivateByRealname` 查不到 pending 套餐直接返回 nil，无副作用 |
| `GetActiveBindingByCardID` 返回 `ErrRecordNotFound` 时不应 log.Error | 已处理：独立卡不属于设备，应静默跳过（Debug/无日志即可） |
| 存量已卡住的套餐（如本次触发排查的 id=1）不会被自动修复 | 在 Migration Plan 中给出手动 SQL |
| 手动停机（`stop_reason=manual`）的卡不会被自动复机 | 这是现有系统的有意设计，Fix 1/2 不改变此行为；手动停机需人工操作复机 |
| 历史存量套餐手动修复后，停机卡不会立即自动开机 | 需额外手动调用 Gateway 复机接口，或等待轮询系统下次 EvaluateAndAct 评估（最长等一个轮询周期） |

---

## Migration Plan

**存量问题修复**（代码部署后手动执行）：

**当前生产环境确认**：仅 `id=1`（device_id=2，`natural_month` 12 个月，`data_reset_cycle=monthly`）受影响。

**关键约束**：`expires_at` 必须匹配 Go 层 `CalculateExpiryTime` 的逻辑（自然月套餐 = 目标月末 23:59:59，不是简单的 `+N months`），同时需一并写入 `next_reset_at`。具体可执行 SQL 见 `tasks.md` 任务 4.2（三步操作：确认范围 → BEGIN/UPDATE/COMMIT → 验证）。

**回滚**：代码变更纯逻辑，无 schema 变更，直接回滚二进制即可。

---

## Open Questions

无。
