### Requirement: 注册卡状态轮询任务类型常量 系统 SHALL 在 `pkg/constants/constants.go` 中定义常量 `TaskTypePollingCardStatus = "polling:card_status"`,与其他轮询类型常量在同一常量块内。 #### Scenario: 常量可被引用 - **WHEN** 其他包引用 `constants.TaskTypePollingCardStatus` - **THEN** 编译通过,值为 `"polling:card_status"` --- ### Requirement: 轮询配置支持卡状态检查间隔 `PollingConfig` model(`tb_polling_config`)SHALL 包含 `CardStatusCheckInterval *int` 字段(GORM column: `card_status_check_interval`),NULL 表示该配置不启用卡状态轮询。 #### Scenario: 配置间隔为正整数时卡状态轮询启用 - **WHEN** `PollingConfig.CardStatusCheckInterval` 不为 NULL 且值 > 0 - **THEN** `getEnabledTaskTypes` 返回的列表中包含 `TaskTypePollingCardStatus` #### Scenario: 配置间隔为 NULL 时卡状态轮询不启用 - **WHEN** `PollingConfig.CardStatusCheckInterval` 为 NULL - **THEN** `getEnabledTaskTypes` 返回的列表中不包含 `TaskTypePollingCardStatus` --- ### Requirement: IotCard 记录最后一次卡状态检查时间 `IotCard` model(`tb_iot_card`)SHALL 包含 `LastCardStatusCheckAt *time.Time` 字段(GORM column: `last_card_status_check_at`),每次卡状态轮询成功执行后更新。 #### Scenario: 每次执行卡状态轮询后更新时间戳 - **WHEN** `PollingCardStatusHandler.Handle` 成功执行(无论状态是否变化) - **THEN** `tb_iot_card.last_card_status_check_at` 更新为当前时间 --- ### Requirement: PollingCardStatusHandler 查询 Gateway 卡状态并同步 DB 系统 SHALL 实现 `PollingCardStatusHandler`,遵循与 `PollingRealnameHandler` 相同的结构:获取并发信号量 → 从缓存/DB 获取卡信息 → 调用 `gateway.QueryCardStatus` → 解析状态 → 更新 DB → 触发停复机评估 → 更新统计 → 重入队。 #### Scenario: Gateway 返回"正常"时卡 network_status 为 1 - **WHEN** `gateway.QueryCardStatus` 返回 `CardStatus = "正常"` 或 `"准备"` - **THEN** `newNetworkStatus = 1`(`NetworkStatusOnline`) #### Scenario: Gateway 返回"停机"时卡 network_status 为 0 - **WHEN** `gateway.QueryCardStatus` 返回 `CardStatus = "停机"` - **THEN** `newNetworkStatus = 0`(`NetworkStatusOffline`) #### Scenario: 状态未变化时不触发停复机评估 - **WHEN** Gateway 返回的 `newNetworkStatus` 与 `card.NetworkStatus` 相同 - **THEN** 不调用 `stopResumeSvc.EvaluateAndAct` #### Scenario: 状态发生变化时更新 DB + 缓存 + 触发评估 - **WHEN** Gateway 返回的 `newNetworkStatus` 与 `card.NetworkStatus` 不同 - **THEN** `tb_iot_card.network_status` 更新为新值,Redis 缓存同步更新,调用 `stopResumeSvc.EvaluateAndAct` #### Scenario: Gateway 查询失败时重入队不更新状态 - **WHEN** `gateway.QueryCardStatus` 返回 error - **THEN** 记录 Warn 日志,更新失败统计,重入队后返回,不修改 DB #### Scenario: 并发已满时重新入队 - **WHEN** `acquireConcurrency` 返回 false - **THEN** 调用 `requeueCard` 后返回,不执行 Gateway 查询 --- ### Requirement: 卡状态轮询类型纳入队列管理 `allTaskTypes`(`queue_manager.go`)SHALL 包含 `TaskTypePollingCardStatus`,使得 `RemoveFromAllQueues`、调度器出队、初始化器均覆盖卡状态队列。 #### Scenario: 卡删除时清理卡状态队列 - **WHEN** 调用 `RemoveFromAllQueues(ctx, cardID)` - **THEN** 卡 ID 从 `polling:card_status` 的所有分片队列中移除 --- ### Requirement: 初始化器写入卡状态队列 `PollingInitializer` SHALL 在批量初始化时,为每张卡的 `polling:card_status` 分片 Sorted Set 写入初始条目(与 realname/carddata/package/protect 保持一致)。 #### Scenario: Worker 启动时卡状态队列被初始化 - **WHEN** `PollingInitializer.StartBackground` 完成批量加载 - **THEN** 每张启用轮询且匹配配置的卡在 `polling:card_status` 对应分片队列中有条目