From 4a1e44f9e1379bfeb6a100b405fc825bb520d76c Mon Sep 17 00:00:00 2001 From: huang Date: Wed, 15 Apr 2026 11:17:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../.openspec.yaml | 2 + .../design.md | 63 +++++++++++++++ .../proposal.md | 43 ++++++++++ .../specs/polling-card-status-task/spec.md | 77 ++++++++++++++++++ .../specs/polling-task-handlers/spec.md | 81 +++++++++++++++++++ .../tasks.md | 57 +++++++++++++ .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/asset-recharge-adaptation/spec.md | 0 .../specs/client-order-purchase/spec.md | 0 .../specs/force-recharge-check/spec.md | 0 .../specs/order-payment/spec.md | 0 .../specs/payment-record/spec.md | 0 .../specs/recharge-order/spec.md | 0 .../specs/wallet-recharge/spec.md | 0 .../tasks.md | 0 17 files changed, 323 insertions(+) create mode 100644 openspec/changes/add-polling-card-status-with-verbose-log/.openspec.yaml create mode 100644 openspec/changes/add-polling-card-status-with-verbose-log/design.md create mode 100644 openspec/changes/add-polling-card-status-with-verbose-log/proposal.md create mode 100644 openspec/changes/add-polling-card-status-with-verbose-log/specs/polling-card-status-task/spec.md create mode 100644 openspec/changes/add-polling-card-status-with-verbose-log/specs/polling-task-handlers/spec.md create mode 100644 openspec/changes/add-polling-card-status-with-verbose-log/tasks.md rename openspec/changes/{refactor-order-payment-module => archive/2026-04-15-refactor-order-payment-module}/.openspec.yaml (100%) rename openspec/changes/{refactor-order-payment-module => archive/2026-04-15-refactor-order-payment-module}/design.md (100%) rename openspec/changes/{refactor-order-payment-module => archive/2026-04-15-refactor-order-payment-module}/proposal.md (100%) rename openspec/changes/{refactor-order-payment-module => archive/2026-04-15-refactor-order-payment-module}/specs/asset-recharge-adaptation/spec.md (100%) rename openspec/changes/{refactor-order-payment-module => archive/2026-04-15-refactor-order-payment-module}/specs/client-order-purchase/spec.md (100%) rename openspec/changes/{refactor-order-payment-module => archive/2026-04-15-refactor-order-payment-module}/specs/force-recharge-check/spec.md (100%) rename openspec/changes/{refactor-order-payment-module => archive/2026-04-15-refactor-order-payment-module}/specs/order-payment/spec.md (100%) rename openspec/changes/{refactor-order-payment-module => archive/2026-04-15-refactor-order-payment-module}/specs/payment-record/spec.md (100%) rename openspec/changes/{refactor-order-payment-module => archive/2026-04-15-refactor-order-payment-module}/specs/recharge-order/spec.md (100%) rename openspec/changes/{refactor-order-payment-module => archive/2026-04-15-refactor-order-payment-module}/specs/wallet-recharge/spec.md (100%) rename openspec/changes/{refactor-order-payment-module => archive/2026-04-15-refactor-order-payment-module}/tasks.md (100%) diff --git a/openspec/changes/add-polling-card-status-with-verbose-log/.openspec.yaml b/openspec/changes/add-polling-card-status-with-verbose-log/.openspec.yaml new file mode 100644 index 0000000..859e7bb --- /dev/null +++ b/openspec/changes/add-polling-card-status-with-verbose-log/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-15 diff --git a/openspec/changes/add-polling-card-status-with-verbose-log/design.md b/openspec/changes/add-polling-card-status-with-verbose-log/design.md new file mode 100644 index 0000000..8022c59 --- /dev/null +++ b/openspec/changes/add-polling-card-status-with-verbose-log/design.md @@ -0,0 +1,63 @@ +## Context + +轮询系统采用分片 Redis Sorted Set + Asynq 任务队列的架构,支持 4 种轮询类型(realname / carddata / package / protect)。每种类型对应一个 Asynq handler,所有 handler 通过 `PollingBase` 共享并发控制、缓存和重入队逻辑。 + +Gateway 已提供 `QueryCardStatus` 接口(返回 `CardStatus: "准备" | "正常" | "停机"`),IotCard model 已有 `NetworkStatus int`(0=停机,1=开机),但轮询系统从未主动调用该接口同步状态。 + +## Goals / Non-Goals + +**Goals:** +- 新增第 5 种轮询类型 `polling:card_status`,与现有 4 种类型完全对等集成 +- `PollingBase` 新增 `verboseLog bool` 字段,所有 handler 在成功查询到上游数据后可选输出详细日志 +- 保持向后兼容:现有轮询配置不受影响,`card_status_check_interval` 为 NULL 时该类型不参与轮询 + +**Non-Goals:** +- 不修改停复机决策逻辑(`EvaluateAndAct` 已处理) +- 不新增 HTTP API(纯后台轮询任务) +- 不对现有 4 种 handler 做任何功能性改动,只追加 verbose log 调用 + +## Decisions + +**决策 1:Gateway 状态映射规则** +- `"准备"` → `NetworkStatusOnline (1)`:卡已就绪,视为开机 +- `"正常"` → `NetworkStatusOnline (1)`:正常使用 +- `"停机"` → `NetworkStatusOffline (0)`:停机 +- 理由:与现有 `realname` handler 的布尔映射风格一致,映射逻辑内联在 handler 内,不引入额外函数 + +**决策 2:状态变更后触发 EvaluateAndAct** +- 上游报告状态变化时,先更新 DB + 缓存,再调用 `stopResumeSvc.EvaluateAndAct(freshCard)` +- 理由:与 realname/carddata handler 行为完全一致,停复机决策集中在 `EvaluateAndAct` 中 +- 替代方案:仅写 DB 不触发评估 —— 拒绝,因为状态变化后不及时评估可能导致卡停复机延迟 + +**决策 3:verboseLog 通过配置文件控制** +- 在 `pkg/config/config.go` 新增 `PollingConfig.VerboseLog bool`,`NewPollingBase` 接受该参数 +- `verboseLog` 开启时,每次成功从 Gateway 获取数据后以 `Info` 级别输出日志 +- 理由:配置文件方式符合项目 Viper 规范,比 Redis key 实现简单,重启代价可接受(轮询系统重启本身就有初始化流程) +- 替代方案:Redis key 动态开关 —— 额外的 Redis 查询开销,且 key 管理增加运维复杂度 + +**决策 4:allTaskTypes 驱动初始化和出队** +- `queue_manager.go` 中的 `allTaskTypes` slice 控制:初始化时写入哪些队列、`RemoveFromAllQueues` 清理哪些队列、Scheduler 出队哪些类型 +- 新类型追加到该 slice 即可自动接入所有相关流程,无需修改调度器核心逻辑 + +**决策 5:数据库迁移** +- `tb_polling_config` 新增 `card_status_check_interval INT NULL`,NULL 表示不启用 +- `tb_iot_card` 新增 `last_card_status_check_at TIMESTAMPTZ NULL`,与其他 `last_*_check_at` 字段保持一致 +- 两列均 `DEFAULT NULL`,完全向后兼容,无需数据迁移 + +## Risks / Trade-offs + +- **[风险] card_status 轮询调用量增加约 25%(原 4 种 → 5 种)** → 缓解:`card_status_check_interval` 可配置,初期设置较长间隔(如 300 秒),观察 Gateway 负载后调整 +- **[风险] verbose_log 开启后日志量大幅增加** → 缓解:默认关闭(`false`),生产环境仅在排查问题时临时开启;日志文件已配置 Lumberjack 轮转 +- **[权衡] 状态以上游为准** → 若上游短暂故障返回异常状态(如误报"停机"),handler 查询失败时会 warn 并重新入队,不更新状态;只有成功响应才触发写 DB + +## Migration Plan + +1. 执行数据库迁移(`make migrate-up`) +2. 更新 Worker 二进制并重启(无需 API 服务重启) +3. 在轮询配置后台为需要卡状态轮询的配置项设置 `card_status_check_interval` 值 +4. 初始化器将在 Worker 启动时自动将全量卡写入新的 `polling:card_status` 队列 +5. 如需回滚:将 `card_status_check_interval` 设置为 NULL 即可停止该类型轮询,无需重启 + +## Open Questions + +无。 diff --git a/openspec/changes/add-polling-card-status-with-verbose-log/proposal.md b/openspec/changes/add-polling-card-status-with-verbose-log/proposal.md new file mode 100644 index 0000000..136650b --- /dev/null +++ b/openspec/changes/add-polling-card-status-with-verbose-log/proposal.md @@ -0,0 +1,43 @@ +## Why + +轮询系统目前缺少对卡开停机状态(NetworkStatus)的主动轮询:已有实名状态、流量、套餐、保护期四类轮询,但从未通过 `polling:card_status` 任务主动向 Gateway 查询卡的实时开停机状态。此外,所有轮询 handler 在成功查询上游数据后均无日志输出,测试和线上排查时无法判断轮询是否正常运转。 + +## What Changes + +- **新增** `polling:card_status` 轮询任务类型:定期调用 Gateway `QueryCardStatus` 接口,将返回的卡状态(准备/正常/停机)同步到 `tb_iot_card.network_status` 字段,状态发生变化时触发停复机评估 +- **新增** `polling.verbose_log` 配置开关:开启后,所有轮询 handler 在成功获取上游数据时以 `Info` 级别输出详细日志(ICCID、返回值、是否发生变化),关闭时保持现有行为(仅记录变更和错误) +- **新增** `tb_polling_config.card_status_check_interval` 列:配置卡状态检查间隔(秒),NULL 表示不启用该轮询类型 +- **新增** `tb_iot_card.last_card_status_check_at` 列:记录最后一次卡状态检查时间,与其他 `last_*_check_at` 字段保持一致 + +## Capabilities + +### New Capabilities + +- `polling-card-status-task`: 卡开停机状态轮询——新的任务类型常量、handler 实现、队列注册、调度器集成、生命周期管理、初始化器支持 + +### Modified Capabilities + +- `polling-task-handlers`: 在所有现有 handler(realname、carddata、package、protect)的成功查询路径上添加 verbose log 支持;`PollingBase` 新增 `verboseLog bool` 字段 + +## Impact + +- **新文件**: `internal/task/polling_cardstatus_handler.go` +- **修改文件**: + - `pkg/constants/constants.go` — 新增任务类型常量 + - `pkg/config/config.go` — 新增 `PollingConfig` 结构体(含 `VerboseLog`) + - `pkg/config/defaults/config.yaml` — 新增 `polling.verbose_log` 默认值 + - `internal/model/polling.go` — `PollingConfig` 新增 `CardStatusCheckInterval` + - `internal/model/iot_card.go` — 新增 `LastCardStatusCheckAt` 字段 + - `internal/task/polling_base.go` — 新增 `verboseLog bool` 字段 + - `internal/task/polling_realname_handler.go` — 添加 verbose log + - `internal/task/polling_carddata_handler.go` — 添加 verbose log + - `internal/task/polling_package_handler.go` — 添加 verbose log + - `internal/task/polling_protect_handler.go` — 添加 verbose log + - `internal/task/polling_utils.go` — `getIntervalByTaskType` 新增 case + - `internal/polling/queue_manager.go` — `allTaskTypes` 追加新类型 + - `internal/polling/lifecycle_service.go` — `getEnabledTaskTypes` + `calcInitialDelay` 新增 case + - `internal/polling/initializer.go` — 初始化时加入 card_status 队列 + - `pkg/queue/handler.go` — 注册新 handler + - `cmd/worker/main.go` — `NewPollingBase` 传入 `verboseLog` 参数 +- **数据库迁移**: `tb_polling_config` 加列 + `tb_iot_card` 加列 +- **无 API 变更**,无破坏性改动 diff --git a/openspec/changes/add-polling-card-status-with-verbose-log/specs/polling-card-status-task/spec.md b/openspec/changes/add-polling-card-status-with-verbose-log/specs/polling-card-status-task/spec.md new file mode 100644 index 0000000..6d3e552 --- /dev/null +++ b/openspec/changes/add-polling-card-status-with-verbose-log/specs/polling-card-status-task/spec.md @@ -0,0 +1,77 @@ +## ADDED Requirements + +### 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` 对应分片队列中有条目 diff --git a/openspec/changes/add-polling-card-status-with-verbose-log/specs/polling-task-handlers/spec.md b/openspec/changes/add-polling-card-status-with-verbose-log/specs/polling-task-handlers/spec.md new file mode 100644 index 0000000..13da3f2 --- /dev/null +++ b/openspec/changes/add-polling-card-status-with-verbose-log/specs/polling-task-handlers/spec.md @@ -0,0 +1,81 @@ +## ADDED Requirements + +### Requirement: PollingBase 支持 verboseLog 标志 +`PollingBase` 结构体 SHALL 新增 `verboseLog bool` 字段,`NewPollingBase` 函数 SHALL 接受 `verboseLog bool` 参数并赋值。`cmd/worker/main.go` SHALL 从 `cfg.Polling.VerboseLog` 读取并传入。 + +#### Scenario: verboseLog 为 true 时详细日志启用 +- **WHEN** `cfg.Polling.VerboseLog = true` 且 Worker 启动 +- **THEN** `PollingBase.verboseLog = true`,所有 handler 在成功查询上游时输出 Info 日志 + +#### Scenario: verboseLog 为 false 时详细日志不输出 +- **WHEN** `cfg.Polling.VerboseLog = false`(默认) +- **THEN** 成功查询上游时不输出额外日志,行为与修改前相同 + +--- + +### Requirement: 配置文件包含 polling.verbose_log 字段 +`pkg/config/config.go` SHALL 新增 `PollingConfig struct` 含 `VerboseLog bool`,并加入 `Config` 结构体。`pkg/config/defaults/config.yaml` SHALL 包含 `polling.verbose_log: false`。 + +#### Scenario: 默认配置 verbose_log 为 false +- **WHEN** 未设置 `JUNHONG_POLLING_VERBOSE_LOG` 环境变量 +- **THEN** `cfg.Polling.VerboseLog = false` + +#### Scenario: 环境变量覆盖开启 verbose_log +- **WHEN** 设置 `JUNHONG_POLLING_VERBOSE_LOG=true` +- **THEN** `cfg.Polling.VerboseLog = true` + +--- + +### Requirement: realname handler 成功查询后输出 verbose 日志 +`PollingRealnameHandler.Handle` SHALL 在 `gateway.QueryRealnameStatus` 成功返回后,当 `h.base.verboseLog` 为 true 时,以 `Info` 级别记录日志,包含:`card_id`、`iccid`、`real_status`(bool)、`new_status`(int)、`changed`(bool)。 + +#### Scenario: verboseLog 开启时实名查询成功输出日志 +- **WHEN** `verboseLog = true` 且 `QueryRealnameStatus` 成功返回 +- **THEN** 输出包含 iccid + real_status + new_status + changed 的 Info 日志 + +#### Scenario: verboseLog 关闭时实名查询成功不输出额外日志 +- **WHEN** `verboseLog = false` 且 `QueryRealnameStatus` 成功返回(状态未变) +- **THEN** 不输出 verbose 日志 + +--- + +### Requirement: carddata handler 成功查询后输出 verbose 日志 +`PollingCarddataHandler.Handle` SHALL 在 `gateway.QueryFlow` 成功返回后,当 `h.base.verboseLog` 为 true 时,以 `Info` 级别记录日志,包含:`card_id`、`iccid`、`gateway_flow_mb`(float64)、`increment_mb`(float64)、`is_cross_month`(bool)。 + +#### Scenario: verboseLog 开启时流量查询成功输出日志 +- **WHEN** `verboseLog = true` 且 `QueryFlow` 成功返回 +- **THEN** 输出包含 iccid + gateway_flow_mb + increment_mb + is_cross_month 的 Info 日志 + +#### Scenario: verboseLog 关闭时流量查询成功不输出额外日志 +- **WHEN** `verboseLog = false` 且 `QueryFlow` 成功返回(流量无增量) +- **THEN** 不输出 verbose 日志 + +--- + +### Requirement: package handler 成功加载卡信息后输出 verbose 日志 +`PollingPackageHandler.Handle` SHALL 在 `GetByID` 成功返回 `freshCard` 后、调用 `EvaluateAndAct` 之前,当 `h.base.verboseLog` 为 true 时,以 `Info` 级别记录日志,包含:`card_id`、`iccid`、`network_status`(int)、`stop_reason`(string)。 + +> 注:package handler 不查询 Gateway,verbose log 记录的是本次评估时读取到的本地卡状态,便于排查"停复机评估为何结果不符预期"类问题。 + +#### Scenario: verboseLog 开启时套餐检查加载卡成功输出日志 +- **WHEN** `verboseLog = true` 且 `iotCardStore.GetByID` 成功返回 `freshCard` +- **THEN** 输出包含 iccid + network_status + stop_reason 的 Info 日志 + +#### Scenario: verboseLog 关闭时套餐检查不输出额外日志 +- **WHEN** `verboseLog = false` +- **THEN** 不输出 verbose 日志(现有 "套餐检查:执行停复机评估" Info 日志保持不变) + +--- + +### Requirement: protect handler 成功执行保护期检查后输出 verbose 日志 +`PollingProtectHandler.Handle` SHALL 在 switch 块执行完毕、调用 `UpdateFields(last_protect_check_at)` 之前,当 `h.base.verboseLog` 为 true 时,以 `Info` 级别记录日志,包含:`card_id`、`iccid`、`stop_protect`(bool)、`start_protect`(bool)、`network_status`(int)。 + +> 注:该日志在 switch 的所有分支(强制停机、强制复机、EvaluateAndAct、无操作)均成功执行后输出;若某分支因 Gateway 失败提前 return,则不输出(已有 Error 日志覆盖)。 + +#### Scenario: verboseLog 开启时保护期检查成功输出日志 +- **WHEN** `verboseLog = true` 且 switch 块各分支均执行成功(未提前 return) +- **THEN** 输出包含 iccid + stop_protect + start_protect + network_status 的 Info 日志 + +#### Scenario: verboseLog 关闭时保护期检查不输出额外日志 +- **WHEN** `verboseLog = false` +- **THEN** 不输出 verbose 日志 diff --git a/openspec/changes/add-polling-card-status-with-verbose-log/tasks.md b/openspec/changes/add-polling-card-status-with-verbose-log/tasks.md new file mode 100644 index 0000000..7e902bc --- /dev/null +++ b/openspec/changes/add-polling-card-status-with-verbose-log/tasks.md @@ -0,0 +1,57 @@ +## 1. 数据库迁移 + +- [ ] 1.1 创建迁移文件 `000120_add_polling_card_status.up.sql`:`tb_polling_config` 新增 `card_status_check_interval INT NULL`,`tb_iot_card` 新增 `last_card_status_check_at TIMESTAMPTZ NULL` +- [ ] 1.2 创建对应回滚文件 `000120_add_polling_card_status.down.sql`:DROP COLUMN 两列 +- [ ] 1.3 执行 `make migrate-up` 并验证:`make migrate-version` 确认 dirty=false,使用 PostgreSQL MCP 验证两列已创建 + +## 2. 常量与配置 + +- [ ] 2.1 `pkg/constants/constants.go`:轮询任务类型常量块新增 `TaskTypePollingCardStatus = "polling:card_status"` +- [ ] 2.2 `pkg/config/config.go`:新增 `PollingConfig struct { VerboseLog bool }`,在 `Config` 中加入 `Polling PollingConfig` 字段 +- [ ] 2.3 `pkg/config/defaults/config.yaml`:在 polling 配置区新增 `verbose_log: false` +- [ ] 2.4 验证:`LspDiagnostics` 检查 `pkg/constants/constants.go` 和 `pkg/config/config.go` 无报错 + +## 3. 数据模型更新 + +- [ ] 3.1 `internal/model/polling.go`:`PollingConfig` 结构体新增 `CardStatusCheckInterval *int`(GORM column: `card_status_check_interval`) +- [ ] 3.2 `internal/model/iot_card.go`:`IotCard` 结构体新增 `LastCardStatusCheckAt *time.Time`(GORM column: `last_card_status_check_at`) +- [ ] 3.3 验证:`LspDiagnostics` 检查两个 model 文件无报错 + +## 4. PollingBase 新增 verboseLog 支持 + +- [ ] 4.1 `internal/task/polling_base.go`:`PollingBase` 结构体新增 `verboseLog bool` 字段;`NewPollingBase` 函数签名新增 `verboseLog bool` 参数并赋值 +- [ ] 4.2 `cmd/worker/main.go`:`task.NewPollingBase(...)` 调用新增 `cfg.Polling.VerboseLog` 参数 +- [ ] 4.3 验证:`LspDiagnostics` 检查 `polling_base.go` 和 `main.go` 无报错 + +## 5. 现有 Handler 添加 verbose 日志 + +- [ ] 5.1 `internal/task/polling_realname_handler.go`:在 `QueryRealnameStatus` 成功返回后,判断 `h.base.verboseLog` 为 true 时输出 Info 日志(card_id、iccid、real_status、new_status、changed) +- [ ] 5.2 `internal/task/polling_carddata_handler.go`:在 `QueryFlow` 成功返回后,判断 `h.base.verboseLog` 为 true 时输出 Info 日志(card_id、iccid、gateway_flow_mb、increment_mb、is_cross_month) +- [ ] 5.3 `internal/task/polling_package_handler.go`:在 `freshCard` 加载成功后、调用 `EvaluateAndAct` 之前,判断 `h.base.verboseLog` 为 true 时输出 Info 日志(card_id、iccid、network_status、stop_reason) +- [ ] 5.4 `internal/task/polling_protect_handler.go`:在 switch 块执行完毕、`UpdateFields(last_protect_check_at)` 之前,判断 `h.base.verboseLog` 为 true 时输出 Info 日志(card_id、iccid、stop_protect、start_protect、network_status) +- [ ] 5.5 验证:`LspDiagnostics` 检查上述 4 个 handler 文件无报错 + +## 6. 卡状态轮询 Handler 实现 + +- [ ] 6.1 新建 `internal/task/polling_cardstatus_handler.go`:实现 `PollingCardStatusHandler`(字段:base、gateway、iotCardStore、stopResumeSvc),完整实现 `Handle` 方法 +- [ ] 6.2 `Handle` 方法逻辑:获取并发信号量 → 获取卡信息(带缓存)→ 调用 `gateway.QueryCardStatus` → 映射状态("停机"→0,其他→1)→ verbose log 判断 → 比较状态是否变化 → 写 DB(`last_card_status_check_at` + 可选 `network_status`)→ 状态变化时更新缓存并调用 `EvaluateAndAct` → 更新统计 → 重入队 +- [ ] 6.3 验证:`LspDiagnostics` 检查新文件无报错 + +## 7. 轮询系统基础设施集成 + +- [ ] 7.1 `internal/task/polling_utils.go`:`getIntervalByTaskType` 新增 `case constants.TaskTypePollingCardStatus`,返回 `cfg.CardStatusCheckInterval` +- [ ] 7.2 `internal/polling/queue_manager.go`:`allTaskTypes` slice 追加 `constants.TaskTypePollingCardStatus` +- [ ] 7.3 `internal/polling/lifecycle_service.go`:`getEnabledTaskTypes` 新增 card_status 条件(`CardStatusCheckInterval != nil && *CardStatusCheckInterval > 0`);`calcInitialDelay` 新增对应 case +- [ ] 7.4 `internal/polling/initializer.go`:在 `initBatch()` 的 if-block 链中新增一段,当 `cfg.CardStatusCheckInterval != nil && *cfg.CardStatusCheckInterval > 0` 时,以 `card.LastCardStatusCheckAt`(新增字段)为基准调用 `calculateNextCheckTime`,向 `TaskTypePollingCardStatus` 分片队列写入 ZAdd 条目;位置紧跟 ProtectCheckInterval 块之后,格式与其他 4 种类型一致 +- [ ] 7.5 验证:`LspDiagnostics` 检查上述 4 个文件无报错 + +## 8. Worker Handler 注册 + +- [ ] 8.1 `pkg/queue/handler.go`:在 `RegisterHandlers` 中创建 `cardStatusHandler := task.NewPollingCardStatusHandler(...)` 并注册 `h.mux.HandleFunc(constants.TaskTypePollingCardStatus, cardStatusHandler.Handle)` +- [ ] 8.2 验证:`LspDiagnostics` 检查 `handler.go` 无报错;`go build ./cmd/worker/...` 成功编译 + +## 9. 整体验证 + +- [ ] 9.1 执行 `go build ./...` 确认整个项目编译通过 +- [ ] 9.2 使用 PostgreSQL MCP 验证迁移列已正确创建(类型、NULL 约束) +- [ ] 9.3 检查 `make migrate-down && make migrate-up` 回滚和重新迁移均成功 diff --git a/openspec/changes/refactor-order-payment-module/.openspec.yaml b/openspec/changes/archive/2026-04-15-refactor-order-payment-module/.openspec.yaml similarity index 100% rename from openspec/changes/refactor-order-payment-module/.openspec.yaml rename to openspec/changes/archive/2026-04-15-refactor-order-payment-module/.openspec.yaml diff --git a/openspec/changes/refactor-order-payment-module/design.md b/openspec/changes/archive/2026-04-15-refactor-order-payment-module/design.md similarity index 100% rename from openspec/changes/refactor-order-payment-module/design.md rename to openspec/changes/archive/2026-04-15-refactor-order-payment-module/design.md diff --git a/openspec/changes/refactor-order-payment-module/proposal.md b/openspec/changes/archive/2026-04-15-refactor-order-payment-module/proposal.md similarity index 100% rename from openspec/changes/refactor-order-payment-module/proposal.md rename to openspec/changes/archive/2026-04-15-refactor-order-payment-module/proposal.md diff --git a/openspec/changes/refactor-order-payment-module/specs/asset-recharge-adaptation/spec.md b/openspec/changes/archive/2026-04-15-refactor-order-payment-module/specs/asset-recharge-adaptation/spec.md similarity index 100% rename from openspec/changes/refactor-order-payment-module/specs/asset-recharge-adaptation/spec.md rename to openspec/changes/archive/2026-04-15-refactor-order-payment-module/specs/asset-recharge-adaptation/spec.md diff --git a/openspec/changes/refactor-order-payment-module/specs/client-order-purchase/spec.md b/openspec/changes/archive/2026-04-15-refactor-order-payment-module/specs/client-order-purchase/spec.md similarity index 100% rename from openspec/changes/refactor-order-payment-module/specs/client-order-purchase/spec.md rename to openspec/changes/archive/2026-04-15-refactor-order-payment-module/specs/client-order-purchase/spec.md diff --git a/openspec/changes/refactor-order-payment-module/specs/force-recharge-check/spec.md b/openspec/changes/archive/2026-04-15-refactor-order-payment-module/specs/force-recharge-check/spec.md similarity index 100% rename from openspec/changes/refactor-order-payment-module/specs/force-recharge-check/spec.md rename to openspec/changes/archive/2026-04-15-refactor-order-payment-module/specs/force-recharge-check/spec.md diff --git a/openspec/changes/refactor-order-payment-module/specs/order-payment/spec.md b/openspec/changes/archive/2026-04-15-refactor-order-payment-module/specs/order-payment/spec.md similarity index 100% rename from openspec/changes/refactor-order-payment-module/specs/order-payment/spec.md rename to openspec/changes/archive/2026-04-15-refactor-order-payment-module/specs/order-payment/spec.md diff --git a/openspec/changes/refactor-order-payment-module/specs/payment-record/spec.md b/openspec/changes/archive/2026-04-15-refactor-order-payment-module/specs/payment-record/spec.md similarity index 100% rename from openspec/changes/refactor-order-payment-module/specs/payment-record/spec.md rename to openspec/changes/archive/2026-04-15-refactor-order-payment-module/specs/payment-record/spec.md diff --git a/openspec/changes/refactor-order-payment-module/specs/recharge-order/spec.md b/openspec/changes/archive/2026-04-15-refactor-order-payment-module/specs/recharge-order/spec.md similarity index 100% rename from openspec/changes/refactor-order-payment-module/specs/recharge-order/spec.md rename to openspec/changes/archive/2026-04-15-refactor-order-payment-module/specs/recharge-order/spec.md diff --git a/openspec/changes/refactor-order-payment-module/specs/wallet-recharge/spec.md b/openspec/changes/archive/2026-04-15-refactor-order-payment-module/specs/wallet-recharge/spec.md similarity index 100% rename from openspec/changes/refactor-order-payment-module/specs/wallet-recharge/spec.md rename to openspec/changes/archive/2026-04-15-refactor-order-payment-module/specs/wallet-recharge/spec.md diff --git a/openspec/changes/refactor-order-payment-module/tasks.md b/openspec/changes/archive/2026-04-15-refactor-order-payment-module/tasks.md similarity index 100% rename from openspec/changes/refactor-order-payment-module/tasks.md rename to openspec/changes/archive/2026-04-15-refactor-order-payment-module/tasks.md