feat: 实现 IoT 卡轮询系统(支持千万级卡规模)
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m35s

实现功能:
- 实名状态检查轮询(可配置间隔)
- 卡流量检查轮询(支持跨月流量追踪)
- 套餐检查与超额自动停机
- 分布式并发控制(Redis 信号量)
- 手动触发轮询(单卡/批量/条件筛选)
- 数据清理配置与执行
- 告警规则与历史记录
- 实时监控统计(队列/性能/并发)

性能优化:
- Redis 缓存卡信息,减少 DB 查询
- Pipeline 批量写入 Redis
- 异步流量记录写入
- 渐进式初始化(10万卡/批)

压测工具(scripts/benchmark/):
- Mock Gateway 模拟上游服务
- 测试卡生成器
- 配置初始化脚本
- 实时监控脚本

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-05 17:32:44 +08:00
parent b11edde720
commit 931e140e8e
104 changed files with 16883 additions and 87 deletions

View File

@@ -0,0 +1,196 @@
## ADDED Requirements
### Requirement: 卡流量查询
系统 SHALL 调用 Gateway API 查询卡的流量使用情况,并处理跨月流量计算。
#### Scenario: 查询卡流量
- **WHEN** 流量检查任务执行卡A的 ICCID 为 "89860123456789012345"
- **THEN** 系统调用 Gateway.QueryFlow(ICCID),获取流量响应(包含月总流量 MB
#### Scenario: Gateway 返回月总流量
- **WHEN** Gateway 返回流量数据total_usage_mb=500本自然月累计
- **THEN** 系统获取到月总流量数据,准备计算增量
#### Scenario: Gateway API 超时
- **WHEN** 调用 Gateway API 超时(>30秒
- **THEN** 系统记录错误日志,任务失败,不重试,卡重新入队(按原计划下次检查)
#### Scenario: Gateway API 返回错误
- **WHEN** Gateway API 返回业务错误(如卡号不存在)
- **THEN** 系统记录错误日志,不更新数据库,任务失败,卡重新入队
### Requirement: 跨月流量计算
系统 SHALL 正确处理跨月流量计算,识别月份切换并重置月度统计。
#### Scenario: 首次查询流量(冷启动)
- **WHEN** 卡A从未查询过流量current_month_start_date 为 NULL
- **THEN** 系统初始化 current_month_start_date=当前月1日current_month_usage_mb=Gateway返回值last_month_total_mb=0
#### Scenario: 同月内流量增长
- **WHEN** 卡A上次查询在本月current_month_start_date=2024-01-01Gateway 返回 total_usage_mb=500数据库当前 current_month_usage_mb=400
- **THEN** 系统计算增量 100 MB更新 current_month_usage_mb=500
#### Scenario: 跨月流量重置
- **WHEN** 卡A上次查询在上月current_month_start_date=2024-01-01当前时间为 2024-02-05Gateway 返回 total_usage_mb=50新月重置后
- **THEN** 系统检测到跨月,保存 last_month_total_mb=400上月结束值重置 current_month_start_date=2024-02-01current_month_usage_mb=50
#### Scenario: Gateway 返回值回退(异常)
- **WHEN** Gateway 返回 total_usage_mb=300小于数据库当前值 current_month_usage_mb=400同月内
- **THEN** 系统记录警告日志,怀疑数据异常,但仍更新为 Gateway 值(信任 Gateway 数据源)
### Requirement: 数据库更新
系统 SHALL 更新卡的流量字段到数据库。
#### Scenario: 更新流量字段
- **WHEN** 计算出增量流量 100 MB
- **THEN** 系统执行 `UPDATE iot_cards SET current_month_usage_mb=500, current_month_start_date='2024-01-01', last_realname_check_at=NOW() WHERE id=?`
#### Scenario: 数据库更新失败
- **WHEN** 数据库更新失败(如连接断开)
- **THEN** 系统记录错误日志,任务失败,卡重新入队
### Requirement: Redis 缓存更新
系统 SHALL 同步更新 Redis 缓存中的卡流量信息。
#### Scenario: 更新缓存流量字段
- **WHEN** 数据库更新成功
- **THEN** 系统使用 HSET 更新 Redis 缓存polling:card:{card_id})的 current_month_usage_mb、current_month_start_date、last_month_total_mb 字段
#### Scenario: 缓存更新失败不影响主流程
- **WHEN** Redis 更新失败
- **THEN** 系统记录警告日志,但任务仍视为成功(缓存可以通过定时同步或懒加载恢复)
### Requirement: 流量历史记录
系统 SHALL 记录流量变化历史到 data_usage_records 表。
#### Scenario: 记录流量增量
- **WHEN** 计算出增量流量 100 MB
- **THEN** 系统插入记录到 data_usage_records 表card_id、usage_mb=100、usage_type='data'、recorded_at=NOW()
#### Scenario: 跨月时记录上月总量
- **WHEN** 检测到跨月,上月总流量为 400 MB
- **THEN** 系统插入一条月度汇总记录card_id、usage_mb=400、usage_type='monthly_summary'、recorded_at=上月最后一天)
#### Scenario: 历史记录插入失败
- **WHEN** 历史记录插入失败
- **THEN** 系统记录警告日志,但任务仍视为成功(历史记录非关键路径)
### Requirement: 触发套餐检查
系统 SHALL 在流量更新后触发关联套餐的流量检查。
#### Scenario: 卡有关联套餐
- **WHEN** 卡A更新流量后card_package_id 不为空
- **THEN** 系统将该套餐加入 polling:manual:package 手动触发队列,优先检查套餐流量
#### Scenario: 卡无关联套餐
- **WHEN** 卡A更新流量后card_package_id 为空
- **THEN** 系统不触发套餐检查
#### Scenario: 卡关联设备级套餐
- **WHEN** 卡A属于设备D设备D有套餐
- **THEN** 系统将设备的套餐加入手动触发队列
### Requirement: 并发控制
系统 SHALL 使用 Redis 信号量控制流量检查的并发数。
#### Scenario: 获取并发信号量成功
- **WHEN** 流量检查任务开始执行,当前并发数为 30配置的最大并发数为 50
- **THEN** 系统使用 INCR 增加计数,获取信号量成功,执行任务
#### Scenario: 并发已满
- **WHEN** 流量检查任务开始执行,当前并发数为 50配置的最大并发数为 50
- **THEN** 系统 INCR 后发现超过限制DECR 归还,任务返回 SkipRetry不执行等待下次调度
#### Scenario: 任务完成释放信号量
- **WHEN** 流量检查任务完成(成功或失败)
- **THEN** 系统使用 DECR 释放信号量
### Requirement: 重新入队
系统 SHALL 在检查完成后,将卡重新加入轮询队列。
#### Scenario: 计算下次检查时间
- **WHEN** 流量检查完成,当前配置的检查间隔为 1800 秒
- **THEN** 系统计算 next_check_time = NOW() + 1800秒
#### Scenario: 加回队列
- **WHEN** 计算出下次检查时间
- **THEN** 系统使用 ZADD 将卡ID和时间戳加入 polling:queue:carddata
#### Scenario: 任务失败也重新入队
- **WHEN** 任务因 Gateway 超时失败
- **THEN** 系统仍然将卡重新入队(按原计划下次检查),不阻塞后续检查
### Requirement: 监控统计
系统 SHALL 记录流量检查的成功率和耗时。
#### Scenario: 记录成功统计
- **WHEN** 流量检查成功完成,耗时 234 毫秒
- **THEN** 系统更新 Redis Hashpolling:stats:carddata增加 success_count_1h累加 total_duration_1h
#### Scenario: 记录失败统计
- **WHEN** 流量检查失败Gateway 超时)
- **THEN** 系统更新 Redis Hash增加 failure_count_1h
#### Scenario: 每小时重置计数器
- **WHEN** 每小时整点(如 10:00:00
- **THEN** 系统重置计数器success_count_1h、failure_count_1h、total_duration_1h保持时间窗口滚动
### Requirement: 日志记录
系统 SHALL 记录详细的流量检查日志,便于排查问题。
#### Scenario: 记录开始日志
- **WHEN** 流量检查任务开始执行
- **THEN** 系统记录 Info 日志,包含 card_id、iccid、config_id
#### Scenario: 记录成功日志
- **WHEN** 流量检查成功完成
- **THEN** 系统记录 Info 日志,包含 card_id、iccid、old_usage_mb、new_usage_mb、increment_mb、duration_ms
#### Scenario: 记录失败日志
- **WHEN** 流量检查失败
- **THEN** 系统记录 Error 日志,包含 card_id、iccid、error 详情
#### Scenario: 记录跨月日志
- **WHEN** 检测到跨月
- **THEN** 系统记录 Info 日志,包含 card_id、old_month、new_month、last_month_total_mb