Files
huang 931e140e8e
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m35s
feat: 实现 IoT 卡轮询系统(支持千万级卡规模)
实现功能:
- 实名状态检查轮询(可配置间隔)
- 卡流量检查轮询(支持跨月流量追踪)
- 套餐检查与超额自动停机
- 分布式并发控制(Redis 信号量)
- 手动触发轮询(单卡/批量/条件筛选)
- 数据清理配置与执行
- 告警规则与历史记录
- 实时监控统计(队列/性能/并发)

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

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 17:32:44 +08:00

8.7 KiB
Raw Blame History

ADDED Requirements

Requirement: 套餐流量汇总

系统 SHALL 根据套餐类型(单卡套餐、设备级套餐)汇总流量使用情况。

Scenario: 单卡套餐流量读取

  • WHEN 检查套餐A套餐类型为单卡套餐单张卡绑定
  • THEN 系统直接读取该卡的 current_month_usage_mb作为套餐已用流量

Scenario: 设备级套餐流量汇总

  • WHEN 检查套餐B套餐类型为设备级套餐设备D下有 3 张卡
  • THEN 系统查询设备下所有卡的 current_month_usage_mb求和得到套餐已用流量

Scenario: 设备级套餐部分卡无流量数据

  • WHEN 设备级套餐部分卡从未查询过流量current_month_usage_mb 为 NULL 或 0
  • THEN 系统将 NULL 视为 0继续汇总其他卡的流量

Scenario: 套餐无关联卡

  • WHEN 检查套餐C套餐下没有关联的卡
  • THEN 系统记录警告日志,套餐已用流量为 0

Requirement: 虚流量对比

系统 SHALL 对比套餐的实际流量使用与虚流量,判断是否超额。

Scenario: 未超额

  • WHEN 套餐A的实际流量为 800 MB虚流量为 1000 MB
  • THEN 系统判断未超额,更新 package_usage 状态为正常status=1

Scenario: 已超额

  • WHEN 套餐A的实际流量为 1200 MB虚流量为 1000 MB
  • THEN 系统判断已超额,更新 package_usage 状态为超额status=2触发停机流程

Scenario: 临近超额(预警)

  • WHEN 套餐A的实际流量为 950 MB虚流量为 1000 MB超过 95% 阈值
  • THEN 系统更新 package_usage 状态为预警status=3发送告警通知但不停机

Scenario: 虚流量为 0 或 NULL

  • WHEN 套餐的虚流量字段为 0 或 NULL
  • THEN 系统记录警告日志,跳过超额检查(视为无限流量套餐)

Requirement: 自动停机

系统 SHALL 在套餐超额时自动调用 Gateway 停机。

Scenario: 单卡套餐停机

  • WHEN 套餐A超额套餐类型为单卡套餐
  • THEN 系统调用 Gateway.StopCard(ICCID),停机该卡

Scenario: 设备级套餐批量停机

  • WHEN 套餐B超额套餐类型为设备级套餐设备下有 3 张卡
  • THEN 系统调用 Gateway.StopCard() 停机所有 3 张卡(串行或并行)

Scenario: Gateway 停机成功

  • WHEN Gateway.StopCard() 返回成功
  • THEN 系统更新卡的 network_status=2已停机记录操作日志

Scenario: Gateway 停机失败

  • WHEN Gateway.StopCard() 返回失败(如卡已停机、网络超时)
  • THEN 系统记录错误日志,任务失败,卡重新入队(下次继续尝试停机)

Scenario: 部分卡停机失败

  • WHEN 设备级套餐停机3 张卡中 2 张成功、1 张失败
  • THEN 系统记录成功的卡,对失败的卡单独重试或记录错误

Requirement: 数据库更新

系统 SHALL 更新套餐使用状态和卡的网络状态到数据库。

Scenario: 更新套餐使用状态

  • WHEN 套餐超额,执行停机后
  • THEN 系统执行 UPDATE package_usage SET status=2, used_mb=1200, last_check_at=NOW() WHERE id=?

Scenario: 更新卡网络状态

  • WHEN 卡A被停机
  • THEN 系统执行 UPDATE iot_cards SET network_status=2, last_status_change_at=NOW() WHERE id=?

Scenario: 数据库更新失败

  • WHEN 数据库更新失败(如连接断开)
  • THEN 系统记录错误日志,任务失败,套餐重新入队

Requirement: Redis 缓存更新

系统 SHALL 同步更新 Redis 缓存中的套餐和卡状态。

Scenario: 更新套餐缓存

  • WHEN 数据库更新成功
  • THEN 系统使用 HSET 更新 Redis 缓存polling📦{package_id})的 status、used_mb、last_check_at 字段

Scenario: 更新卡缓存

  • WHEN 卡状态更新成功
  • THEN 系统使用 HSET 更新 Redis 缓存polling:card:{card_id})的 network_status 字段

Scenario: 缓存更新失败不影响主流程

  • WHEN Redis 更新失败
  • THEN 系统记录警告日志,但任务仍视为成功(缓存可以通过定时同步或懒加载恢复)

Requirement: 操作日志记录

系统 SHALL 记录停机操作到操作日志表。

Scenario: 记录停机操作

  • WHEN 卡A被停机
  • THEN 系统插入操作日志card_id、operation_type='stop'、reason='套餐超额'、operator='系统自动'、created_at=NOW()

Scenario: 操作日志包含套餐信息

  • WHEN 记录停机操作
  • THEN 操作日志包含 package_id、used_mb、virtual_flow 等上下文信息

Scenario: 操作日志插入失败

  • WHEN 操作日志插入失败
  • THEN 系统记录警告日志,但任务仍视为成功(操作日志非关键路径)

Requirement: 手动触发队列

系统 SHALL 支持从手动触发队列获取套餐并优先处理。

Scenario: 卡流量更新后触发

  • WHEN 卡A的流量检查完成卡A有关联套餐
  • THEN 系统将套餐ID加入 polling:manual:package 手动触发队列

Scenario: 手动触发队列优先处理

  • WHEN 调度循环执行,手动触发队列有 10 个套餐
  • THEN 系统先从手动触发队列取出所有套餐生成高优先级任务ProcessIn(0)),清空手动触发队列

Scenario: 再处理定时队列

  • WHEN 手动触发队列处理完成后
  • THEN 系统继续处理定时队列中到期的套餐

Requirement: 定时队列扫描

系统 SHALL 定期扫描所有套餐的流量使用情况,防止遗漏。

Scenario: 周期性扫描套餐

  • WHEN 调度循环执行,当前时间为 T
  • THEN 系统从 Redis Sorted Setpolling:queue:package获取 score <= T 的套餐(最多 1000 个)

Scenario: 生成 Asynq 任务

  • WHEN 获取到 100 个到期的套餐
  • THEN 系统为每个套餐生成 Asynq 任务TaskTypeIotPackageCheck入队到 iot_polling_package 队列

Scenario: 从队列移除已调度的套餐

  • WHEN 任务生成完成
  • THEN 系统使用 ZREM 从 Redis 队列移除这些套餐(检查完成后会重新加入)

Requirement: 并发控制

系统 SHALL 使用 Redis 信号量控制套餐检查的并发数。

Scenario: 获取并发信号量成功

  • WHEN 套餐检查任务开始执行,当前并发数为 20配置的最大并发数为 30
  • THEN 系统使用 INCR 增加计数,获取信号量成功,执行任务

Scenario: 并发已满

  • WHEN 套餐检查任务开始执行,当前并发数为 30配置的最大并发数为 30
  • THEN 系统 INCR 后发现超过限制DECR 归还,任务返回 SkipRetry不执行等待下次调度

Scenario: 任务完成释放信号量

  • WHEN 套餐检查任务完成(成功或失败)
  • THEN 系统使用 DECR 释放信号量

Requirement: 重新入队

系统 SHALL 在检查完成后,将套餐重新加入轮询队列。

Scenario: 计算下次检查时间

  • WHEN 套餐检查完成,当前配置的检查间隔为 3600 秒
  • THEN 系统计算 next_check_time = NOW() + 3600秒

Scenario: 加回队列

  • WHEN 计算出下次检查时间
  • THEN 系统使用 ZADD 将套餐ID和时间戳加入 polling:queue:package

Scenario: 任务失败也重新入队

  • WHEN 任务因 Gateway 超时失败
  • THEN 系统仍然将套餐重新入队(按原计划下次检查),不阻塞后续检查

Requirement: 监控统计

系统 SHALL 记录套餐检查的成功率和耗时。

Scenario: 记录成功统计

  • WHEN 套餐检查成功完成,耗时 345 毫秒
  • THEN 系统更新 Redis Hashpolling:stats:package增加 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 日志,包含 package_id、package_type、virtual_flow

Scenario: 记录成功日志

  • WHEN 套餐检查成功完成
  • THEN 系统记录 Info 日志,包含 package_id、used_mb、virtual_flow、is_exceeded、duration_ms

Scenario: 记录停机日志

  • WHEN 执行停机操作
  • THEN 系统记录 Warn 日志,包含 package_id、card_ids、reason='套餐超额'

Scenario: 记录失败日志

  • WHEN 套餐检查失败
  • THEN 系统记录 Error 日志,包含 package_id、error 详情