Files
junhong_cmp_fiber/openspec/changes/polling-system-implementation/specs/polling-scheduler/spec.md
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

6.9 KiB
Raw Blame History

ADDED Requirements

Requirement: Worker 启动时快速初始化

系统 SHALL 在 Worker 进程启动时快速完成初始化(<10秒不阻塞服务启动。

Scenario: 启动时只加载配置

  • WHEN Worker 进程启动
  • THEN 系统在 10 秒内完成配置加载(轮询配置、并发控制配置),启动调度器 GoroutineWorker 进程可用

Scenario: 后台异步加载卡数据

  • WHEN Worker 快速启动完成后
  • THEN 系统在后台启动异步任务,分批加载卡数据到 Redis每批 10万张处理后 sleep 1秒

Scenario: 记录初始化进度

  • WHEN 后台初始化任务运行中
  • THEN 系统在 Redis 存储初始化进度(已处理数量、总数量、百分比、预计完成时间),管理员可查询进度

Requirement: 渐进式卡数据加载

系统 SHALL 使用渐进式策略加载百万级卡数据到 Redis避免打爆数据库。

Scenario: 分批读取卡数据

  • WHEN 初始化任务从数据库读取卡数据
  • THEN 系统使用游标(主键范围)分批读取,每批 10万张使用 WHERE id > last_id ORDER BY id LIMIT 100000

Scenario: 批量写入 Redis

  • WHEN 读取到一批卡数据后
  • THEN 系统使用 Redis Pipeline 批量写入HSET 卡信息、ZADD 队列、SADD 配置匹配关系),减少网络往返

Scenario: 限流保护数据库

  • WHEN 每批卡数据处理完成后
  • THEN 系统 sleep 1秒避免连续高频查询打爆数据库

Scenario: 断点续传

  • WHEN Worker 重启,初始化未完成
  • THEN 系统从 Redis 读取上次进度从上次最大ID继续加载不重新开始

Requirement: 懒加载机制

系统 SHALL 支持懒加载机制,当卡未初始化但被访问时,实时加载到 Redis。

Scenario: 卡缓存未命中时加载

  • WHEN 手动触发检查卡ID为 123456但 Redis 中没有该卡缓存
  • THEN 系统从数据库读取卡信息,匹配配置,写入 Redis 缓存,加入轮询队列,继续执行检查

Scenario: 新卡创建时自动加载

  • WHEN 用户创建新卡或批量导入卡
  • THEN 系统在 Service 层调用 PollingService.OnCardCreated(),自动加载到 Redis

Scenario: 热点卡优先

  • WHEN 初始化未完成,用户频繁访问某些卡
  • THEN 这些卡通过懒加载机制优先初始化到 Redis

Requirement: 调度循环执行

系统 SHALL 每 10 秒执行一次调度循环,从 Redis 队列获取到期的卡并生成任务。

Scenario: 定时触发调度

  • WHEN 调度器启动后
  • THEN 系统每 10 秒执行一次调度循环(使用 time.Ticker

Scenario: 获取到期的卡

  • WHEN 调度循环执行,当前时间为 T
  • THEN 系统从 Redis Sorted Set 使用 ZRANGEBYSCORE 获取 score <= T 的卡(最多 1000 张)

Scenario: 生成 Asynq 任务

  • WHEN 获取到 500 张到期的卡
  • THEN 系统为每张卡生成 Asynq 任务TaskTypeIotRealNameCheck入队到 iot_polling_realname 队列

Scenario: 从队列移除已调度的卡

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

Requirement: 手动触发优先处理

系统 SHALL 优先处理手动触发队列中的卡,确保手动触发立即执行。

Scenario: 先处理手动触发队列

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

Scenario: 再处理定时队列

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

Requirement: 配置匹配引擎

系统 SHALL 为每张卡匹配最合适的轮询配置,基于优先级选择。

Scenario: 按优先级匹配配置

  • WHEN 卡A的状态为未实名not_real_name、运营商为移动carrier_id=1
  • THEN 系统读取所有启用配置,按 priority ASC 排序,依次检查匹配条件,返回第一个匹配的配置

Scenario: 精确匹配优先

  • WHEN 配置1优先级10匹配"未实名+移动"配置2优先级20匹配"未实名"卡A为"未实名+移动"
  • THEN 系统匹配配置1优先级更高

Scenario: 无匹配配置

  • WHEN 卡A的状态无法匹配任何启用的配置
  • THEN 系统返回 nil卡A不参与轮询

Scenario: 卡状态变化重新匹配

  • WHEN 卡A从未实名变为已实名
  • THEN 系统调用 OnCardStatusChanged(),重新匹配配置,更新 Redis 缓存和队列

Requirement: 下次检查时间计算

系统 SHALL 根据配置的检查间隔计算卡的下次检查时间。

Scenario: 首次加入队列

  • WHEN 卡A首次加入轮询队列配置的实名检查间隔为 60 秒
  • THEN 系统计算 next_check_time = NOW() + 60秒使用 ZADD 加入队列

Scenario: 检查完成后重新入队

  • WHEN 卡A的实名检查完成配置的检查间隔为 60 秒
  • THEN 系统计算 next_check_time = NOW() + 60秒使用 ZADD 重新加入队列

Scenario: 配置更新后重新计算

  • WHEN 管理员将配置的检查间隔从 60 秒修改为 120 秒
  • THEN 系统为所有匹配该配置的卡重新计算 next_check_time更新队列

Requirement: 卡生命周期回调

系统 SHALL 在卡的生命周期事件发生时,自动同步到轮询系统。

Scenario: 新卡创建时加入轮询

  • WHEN IotCardService.Create() 创建新卡enable_polling=true
  • THEN Service 调用 PollingService.OnCardCreated(),卡自动加入轮询系统

Scenario: 批量导入时加入轮询

  • WHEN IotCardImportHandler 批量导入 10000 张卡
  • THEN Handler 调用 PollingService.OnBatchCardsCreated(),使用 Pipeline 批量加入轮询系统

Scenario: 卡删除时移除轮询

  • WHEN IotCardService.Delete() 软删除卡
  • THEN Service 调用 PollingService.OnCardDeleted(),从所有队列移除,删除缓存

Scenario: 禁用轮询时移除队列

  • WHEN IotCardService.Update() 更新卡enable_polling=false
  • THEN Service 调用 PollingService.OnCardDisabled(),从队列移除但保留缓存

Scenario: 启用轮询时加入队列

  • WHEN IotCardService.Update() 更新卡enable_polling=true
  • THEN Service 调用 PollingService.OnCardEnabled(),重新匹配配置并加入队列

Requirement: 监控统计更新

系统 SHALL 在每次调度后更新监控统计数据。

Scenario: 记录调度信息

  • WHEN 调度循环完成,处理了 500 张卡
  • THEN 系统更新 Redis Hashpolling:stats:realname记录 last_schedule_at 和 last_schedule_count

Scenario: 更新队列长度

  • WHEN 任何时候查询队列状态
  • THEN 系统使用 ZCARD 实时读取队列长度