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>
8.7 KiB
8.7 KiB
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 Set(polling: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 Hash(polling: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 详情