chore: 归档轮询系统实现变更 (polling-system-implementation)
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 44s

已完成千万级卡规模轮询系统的完整实现和集成测试验证,将变更归档到 openspec/changes/archive/2026-02-10-polling-system-implementation/

主要成果:
- 三大轮询任务:实名检查、卡流量检查、套餐流量检查
- 快速启动(<10秒)和渐进式初始化
- 完整运营工具:配置管理、并发控制、监控面板、告警系统、数据清理、手动触发
- 任务完成度:215/216(99.5%)
- 所有 24 个新增接口已生成 OpenAPI 文档

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 10:28:47 +08:00
parent 931e140e8e
commit 804145332b
15 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,187 @@
## 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 实时读取队列长度