## MODIFIED Requirements

### Requirement: EvaluateAndAct——停复机统一入口

`StopResumeService` 新增 `EvaluateAndAct(ctx context.Context, card *model.IotCard) error` 方法，封装完整的停复机判断和执行逻辑。

> **签名说明**：删除原设计的 `carrierType string, carrierID uint` 参数（仅用于日志，放入签名会误导实现者）。  
> 设备/单卡维度通过 `card.DeviceID` 推导，日志上下文通过函数内部的 `zap.Field` 记录。

**删除** `internal/task/polling_handler.go` 中的以下函数（迁移到 StopResumeService）：
- `checkStopResume`
- `shouldStopCard`
- `hasAvailablePackage`（旧版，被新 `hasValidPackage` 替代）
- `stopCardByUsageExhausted`
- `resumeCardByPackageAvailable`

所有 Task Handler（carddata、package、protect）在需要停复机决策时，统一调用 `stopResumeService.EvaluateAndAct()`。

#### Scenario: Task Handler 调用统一停复机入口
- **GIVEN** `carddata_handler.go` 完成流量数据采集和 DB 写入
- **WHEN** 调用 `stopResumeService.EvaluateAndAct(ctx, card)`
- **THEN** StopResumeService 完整执行停复机判断逻辑；Handler 不包含任何无条件停复机相关代码（`shouldStop`、`hasPackage` 等函数不出现在 Handler 文件中）

#### Scenario: 停机原因按优先级记录
- **GIVEN** 卡在线，同时满足「无套餐」和「流量耗尽」和「未实名」三个条件
- **WHEN** `EvaluateAndAct` 执行 `checkStopReasons`
- **THEN** 按优先级取最高：`no_package > traffic_exhausted > not_realname`；`stop_reason` 字段记录 `no_package`；停机后 DB 中该卡 `stop_reason='no_package'`

#### Scenario: EvaluateAndAct 幂等
- **GIVEN** 卡已停机，`stop_reason='no_package'`，无套餐状态未变化
- **WHEN** 下次轮询再次调用 `EvaluateAndAct`
- **THEN** 检测到卡已停机（`network_status=0`），进入复机判断分支；复机条件不满足（仍无套餐），不发起 Gateway 调用，返回 nil；DB 状态不变

---

### Requirement: 停机条件——三种场景全面覆盖

卡在线（`network_status=1`）时，满足以下**任一**条件触发停机：

**条件 A（no_package）**：无有效套餐
- 独立卡：`tb_package_usage` 中无 `iot_card_id=卡ID AND status IN (0,1)` 的记录
- 绑定设备的卡：`tb_package_usage` 中无 `device_id=设备ID AND status IN (0,1)` 的记录
- status=0（待激活）和 status=1（激活中）均视为有效套餐

**条件 B（traffic_exhausted）**：虚流量耗尽
- 活跃套餐 `status=2`（系统标记为虚流量耗尽），或
- 活跃套餐 `data_usage_mb >= data_limit_mb`（且 `data_limit_mb > 0`，防止无限流量卡误判）

**条件 C（not_realname）**：非行业卡且未实名
- `card_category != 'industry'` 且 `real_name_status = 0`

#### Scenario: 无套餐触发停机（独立卡）
- **GIVEN** cardID=100 的独立卡（无 device_id），`network_status=1`
- **WHEN** `tb_package_usage` 中无任何 `iot_card_id=100 AND status IN(0,1)` 的记录
- **THEN** `checkStopReasons` 返回 `["no_package"]`；发起 Gateway 停机调用（3次重试）；DB 更新 `network_status=0, stop_reason='no_package'`

#### Scenario: 无套餐触发停机（设备卡，修复Bug1）
- **GIVEN** cardID=200 的卡绑定 deviceID=50，`network_status=1`
- **WHEN** `tb_package_usage` 中无 `iot_card_id=200` 的记录，但有 `device_id=50 AND status=1` 的记录（购买了设备套餐）
- **THEN** `hasValidPackage` 检测到设备套餐存在，返回 true；不触发停机；卡保持在线状态（修复之前只查 iot_card_id 导致误停机的 Bug）

#### Scenario: 流量耗尽触发停机
- **GIVEN** 卡在线，活跃套餐 `data_limit_mb=1000, data_usage_mb=1001`
- **WHEN** `isTrafficExhausted` 检测
- **THEN** `data_usage_mb(1001) >= data_limit_mb(1000)` 条件满足；返回 true；触发停机，`stop_reason='traffic_exhausted'`

#### Scenario: 套餐 status=2 触发停机
- **GIVEN** 卡在线，活跃套餐 `status=2`（系统已标记虚流量耗尽）
- **WHEN** `isTrafficExhausted` 检测
- **THEN** 检测到 `status=2`；返回 true；触发停机，`stop_reason='traffic_exhausted'`

#### Scenario: 无限流量套餐不误判流量耗尽
- **GIVEN** 卡在线，活跃套餐 `data_limit_mb=0`（无限流量），`data_usage_mb=9999`
- **WHEN** `isTrafficExhausted` 检测
- **THEN** `data_limit_mb=0` 时跳过用量比较（防止无限流量卡被误停机）；返回 false；不触发停机

#### Scenario: 未实名普通卡触发停机
- **GIVEN** 卡在线，`card_category='normal'`，`real_name_status=0`，有有效套餐且流量未耗尽
- **WHEN** `checkStopReasons` 检测条件 C
- **THEN** `!isRealnameOK(card)` 为 true；触发停机，`stop_reason='not_realname'`

#### Scenario: 行业卡无需实名不停机
- **GIVEN** 卡在线，`card_category='industry'`，`real_name_status=0`，有有效套餐
- **WHEN** `checkStopReasons` 检测
- **THEN** `isRealnameOK(card)` 返回 true（行业卡豁免实名要求）；不触发停机；卡保持在线

---

### Requirement: 复机条件——全部满足才复机

卡停机（`network_status=0`）时，以下**全部**条件满足才触发自动复机：

1. `stop_reason IN ('traffic_exhausted', 'no_package', 'not_realname')`（排除手动停机、运营商停机等其他原因）
2. 有有效套餐且流量未耗尽（`hasValidPackage=true` 且 `isTrafficExhausted=false`）
3. 行业卡 OR 已实名（`card_category='industry'` OR `real_name_status=1`）

#### Scenario: 购买套餐后自动复机
- **GIVEN** cardID=300 因 `no_package` 停机，`stop_reason='no_package'`，现在购买了套餐（status=1）
- **WHEN** 下次轮询执行 `EvaluateAndAct`
- **THEN** 三个复机条件全部满足（stop_reason合规 + 有套餐 + 已实名或行业卡）；发起 Gateway 复机调用（3次重试）；DB 更新 `network_status=1, stop_reason=''`

#### Scenario: 手动停机不自动复机
- **GIVEN** 卡 `stop_reason='manual'`（管理员手动停机）
- **WHEN** 该卡购买了套餐，完成实名认证，轮询执行 `EvaluateAndAct`
- **THEN** 条件1不满足（`'manual'` 不在 IN 列表中）；不触发自动复机；卡保持停机状态；需管理员手动复机

#### Scenario: 流量耗尽停机后购买新套餐复机
- **GIVEN** 卡因 `traffic_exhausted` 停机，`stop_reason='traffic_exhausted'`；旧套餐已过期（status=3），新购套餐 status=1，usage=0
- **WHEN** 轮询执行 `EvaluateAndAct`
- **THEN** 条件1满足（traffic_exhausted）；条件2满足（新套餐有效，usage<limit）；条件3满足（已实名）；触发复机

#### Scenario: 未实名卡实名后复机
- **GIVEN** 普通卡因 `not_realname` 停机，用户完成实名认证（`real_name_status=1`）
- **WHEN** 实名检查轮询执行，更新 DB 后调用 `EvaluateAndAct`
- **THEN** 条件1满足；条件2满足（有套餐且未耗尽）；条件3满足（`real_name_status=1`）；触发复机

---

### Requirement: 设备维度停复机——覆盖所有绑定卡

**停机**操作覆盖设备下所有在线卡（`network_status=1`）；**复机**操作对满足条件的卡执行，跳过未满足 `isRealnameOK` 的普通卡。

#### Scenario: 设备套餐耗尽停所有绑定卡
- **GIVEN** deviceID=10 下有 3 张在线卡（cardID=1,2,3），设备套餐 `data_usage_mb >= data_limit_mb`
- **WHEN** 任一绑定卡触发 `EvaluateAndAct`，检测到设备套餐流量耗尽
- **THEN** 调用 `stopDeviceCards(ctx, deviceID=10, "traffic_exhausted")`；3 张卡均发起 Gateway 停机调用；3 张卡均更新 `stop_reason='traffic_exhausted'`

#### Scenario: 设备停机调用 Gateway 带3次重试
- **GIVEN** 设备下有 3 张卡需要停机，Gateway 第一次调用返回超时
- **WHEN** `stopCardWithRetry` 执行
- **THEN** 自动重试至多 3 次；至少 1 次成功则记录成功，最终失败则记录 Error 日志「卡停机失败: cardID=xxx, error=xxx」

#### Scenario: 购买设备套餐后全卡复机
- **GIVEN** deviceID=10 下 3 张卡均因 `traffic_exhausted` 停机，购买新套餐后 status=1
- **WHEN** 轮询执行 `EvaluateAndAct`，`shouldResume` 返回 true
- **THEN** `resumeDeviceCards(ctx, deviceID=10)` 被调用；检查所有 `stop_reason IN(...)` 的停机卡；3 张卡均满足 `isRealnameOK`（均已实名）；3 张卡均触发复机

#### Scenario: 设备复机跳过未实名普通卡
- **GIVEN** deviceID=20 下有 3 张卡：card-A（已实名）、card-B（已实名）、card-C（`card_category='normal'`, `real_name_status=0`）
- **WHEN** 设备复机条件满足，执行 `resumeDeviceCards`
- **THEN** card-A 和 card-B 触发复机（各执行 Gateway 复机调用）；card-C 因 `isRealnameOK=false` 被跳过（保持停机）；card-C 的 `stop_reason` 自动更新为 `not_realname`

#### Scenario: 设备复机中单卡 Gateway 失败不阻止其他卡
- **GIVEN** 设备下 3 张卡需要复机，card-B 的 Gateway 复机调用失败（含重试）
- **WHEN** `resumeDeviceCards` 遍历执行
- **THEN** card-A 和 card-C 正常复机；card-B 记录 Error 日志并跳过，不影响其他卡的复机；`resumeDeviceCards` 整体不返回 error（尽力复机语义）

---

### Requirement: 设备维度操作幂等锁——防止并发重复 Gateway 调用

设备下多张卡分布在不同分片队列，同一调度周期内可能同时触发 `EvaluateAndAct`，进而并发调用 `stopDeviceCards` / `resumeDeviceCards`，导致同一张卡被 Gateway 重复停/复机。

`stopDeviceCards` 和 `resumeDeviceCards` 均在执行前通过 Redis `SetNX` 获取设备操作锁（`polling:device:op_lock:{deviceID}`，TTL 30 秒）。获取失败时视为"其他协程正在处理"，直接跳过。

#### Scenario: 多卡并发触发不重复调 Gateway
- **GIVEN** deviceID=10 下有 card-A（shard 3）和 card-B（shard 7），同一调度周期被并发出队
- **WHEN** card-A 和 card-B 同时触发 `EvaluateAndAct` → 均尝试调用 `stopDeviceCards(deviceID=10)`
- **THEN** 第一个调用获取设备锁成功，执行完整的停机流程；第二个调用 `SetNX` 返回 false，记录 Debug 日志「设备停机操作已在进行中，跳过: deviceID=10」，直接返回；设备下各卡只被 Gateway 停机一次

#### Scenario: 设备锁 TTL 超时后可重新获取
- **GIVEN** 设备操作锁已过期（上次操作完成后 `Del` 释放，或 TTL 30 秒到期）
- **WHEN** 下一轮轮询再次触发 `stopDeviceCards`
- **THEN** `SetNX` 成功获取锁，正常执行停机流程
