归档: add-polling-card-status-with-verbose-log,同步主规范
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m24s

change 目录归档至 openspec/changes/archive/2026-04-15-add-polling-card-status-with-verbose-log/
新建主规范 openspec/specs/polling-card-status-task/spec.md(6个需求)
追加 verbose log 需求至 openspec/specs/polling-task-handlers/spec.md(4个新需求)
protect handler 规范更新字段名:network_status → network_status_at_check + action_taken

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-15 12:23:05 +08:00
parent 854330a4b4
commit 8bf1282378
8 changed files with 188 additions and 31 deletions

View File

@@ -0,0 +1,75 @@
### 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` 对应分片队列中有条目