## ADDED Requirements

### Requirement: Worker 启动时快速初始化

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

#### Scenario: 启动时只加载配置

- **WHEN** Worker 进程启动
- **THEN** 系统在 10 秒内完成配置加载（轮询配置、并发控制配置），启动调度器 Goroutine，Worker 进程可用

#### 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 Hash（polling:stats:realname），记录 last_schedule_at 和 last_schedule_count

#### Scenario: 更新队列长度

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