## ADDED Requirements

### Requirement: 独立配置管理模块（PollingConfigManager）

新建 `internal/polling/config_manager.go`，提供 `PollingConfigManager` 类型，从 `scheduler.go` 中拆分配置相关职责。

**文件目标**：< 150 行，职责：DB 加载 → 内存缓存 → Redis 同步 → 定时刷新。

**从 Scheduler 提取的方法**：
- `loadConfigs(ctx)`：从 DB 加载所有启用的 `tb_polling_config`
- `syncConfigsToRedis(configs)`：写入 Redis Hash（TTL 24小时）
- `MatchConfig(card)`：按优先级顺序返回第一个匹配的配置
- `matchConfigConditions(config, card)`：判断卡是否满足配置条件
- `getCardCondition(card)`：获取卡的匹配条件（carrier、simtype 等）

**Scheduler 修改后**：不包含任何 DB 查询或配置加载逻辑，通过 `configManager.MatchConfig(card)` 获取配置。

#### Scenario: 启动时加载配置并同步 Redis
- **GIVEN** Worker 进程启动，DB 中有 5 条启用的 PollingConfig
- **WHEN** `PollingConfigManager.Load(ctx)` 被调用
- **THEN** 从 DB 加载 5 条配置，写入内存缓存（sync.RWMutex 保护），同步到 Redis Hash（TTL 24小时）；加载完成日志记录「已加载 5 条轮询配置」

#### Scenario: 配置按优先级匹配
- **GIVEN** 内存中有 3 条配置，优先级分别为 1、5、10（数字越小优先级越高）
- **WHEN** 调用 `MatchConfig(card)` 匹配某张卡
- **THEN** 按优先级升序遍历，返回第一个满足所有条件的配置；无匹配时返回 nil，该卡不加入轮询队列

#### Scenario: 配置匹配使用读锁不阻塞
- **GIVEN** Scheduler 高频调用 `MatchConfig(card)`（每秒可能数千次）
- **WHEN** 同时有定时刷新正在写入新配置（写锁）
- **THEN** 读取操作等待写锁释放后继续，不发生数据竞争；写锁持有时间极短（内存赋值），不影响 Scheduler 调度性能

---

### Requirement: 定时刷新配置（5分钟周期）

`PollingConfigManager.Start(ctx)` 启动后台 goroutine，每 5 分钟自动重新加载配置。

#### Scenario: 定时刷新不重启 Worker
- **GIVEN** Worker 进程已运行 10 分钟，管理员在控制台修改了某条 PollingConfig 的轮询间隔
- **WHEN** 距上次加载超过 5 分钟，定时器触发
- **THEN** 自动重新从 DB 加载配置，更新内存缓存，新配置在下一轮调度中生效；无需重启 Worker 进程

#### Scenario: 刷新失败不影响当前配置
- **GIVEN** 定时刷新时 DB 连接超时
- **WHEN** `Load(ctx)` 返回 error
- **THEN** 内存缓存保持原有配置不变（不清空），记录 Error 日志「配置刷新失败: xxx」；下一轮（5分钟后）重试

---

### Requirement: 独立卡初始化模块（CardInitializer）

新建 `internal/polling/initializer.go`，提供 `CardInitializer` 类型，从 `scheduler.go` 中拆分渐进式初始化职责。

**文件目标**：< 250 行，职责：分批 DB 查询 → 分片路由入队 → 进度追踪 → enable_polling 过滤。

**从 Scheduler 提取的方法**：
- `progressiveInit(ctx)`：分批加载卡并入队
- `initCardsBatch(ctx, offset, limit)`：加载单批次卡
- `initCardPolling(ctx, card)`：为单张卡匹配配置并入队

**Scheduler 修改后**：不包含任何初始化逻辑，通过 `cardInitializer.Run(ctx)` 异步启动，通过 `cardInitializer.GetProgress()` 查询进度。

#### Scenario: 渐进式分批初始化避免 DB 过载
- **GIVEN** DB 中有 500 万张启用轮询的卡
- **WHEN** `CardInitializer.Run(ctx)` 被调用
- **THEN** 分批加载（每批 10 万张），批次间 Sleep 500ms；约 500 批次，每批处理后记录进度日志「初始化进度: 100000/5000000」；全部完成后 `initCompleted=true`，Scheduler 开始正常出队

#### Scenario: 跳过 enable_polling=false 的卡
- **GIVEN** 10 万张卡中，5000 张卡的 `enable_polling=false`
- **WHEN** 批次处理时遇到这 5000 张卡
- **THEN** 这 5000 张卡不加入任何轮询队列（ZADD 跳过）；其余 95000 张卡按正常流程入队

#### Scenario: 初始化幂等（重启不重复）
- **GIVEN** Worker 重启，部分卡已在分片队列中（上次初始化留下）
- **WHEN** 重新执行 `CardInitializer.Run(ctx)`
- **THEN** ZADD 操作幂等：若卡已在队列中，只更新 score 为新的初始延迟时间（不添加重复条目）；Redis Sorted Set 中不出现重复成员

#### Scenario: Scheduler 只在初始化完成后出队
- **GIVEN** Worker 刚启动，初始化尚未完成（`initCompleted=false`）
- **WHEN** Scheduler 的 `scheduleLoop` 执行
- **THEN** 检查 `cardInitializer.GetProgress().Completed`，若为 false 则跳过本轮所有分片的出队；避免初始化中途 Scheduler 取到不完整队列

#### Scenario: 进度暴露给监控接口
- **GIVEN** 管理员调用轮询状态监控接口
- **WHEN** `MonitoringService` 调用 `cardInitializer.GetProgress()`
- **THEN** 返回 `{Total: 5000000, Processed: 1500000, Completed: false}`；前端可展示初始化进度百分比

---

### Requirement: Scheduler 精简到 < 250 行（Mi1 修复）

> **注意**：本 spec 原写 `< 200 行`，已与 `design.md` 决策 9 和 `tasks.md` 统一更正为 **`< 250 行`**。  
> 原因：精简后的 Scheduler 保留了套餐过期检测和流量重置两个触发器（见决策 9），额外占用约 50 行。

`internal/polling/scheduler.go` 精简后只保留纯调度循环逻辑，所有子职责委托给新建的三个模块。

**精简后 Scheduler 的完整职责**：
1. 启动 `CardInitializer.Run(ctx)`（异步）
2. 启动 `PollingConfigManager.Start(ctx)`（定时刷新）
3. 运行 `scheduleLoop`：每秒 × N分片 × 4任务类型 的出队循环
4. 背压检测：`GetQueueDepth > 阈值` 时跳过该分片

**禁止出现在精简后 Scheduler 中的内容**：
- 任何 `db.Find()`、`db.Where()` 等 DB 操作
- 配置加载逻辑（`loadConfigs`、`syncConfigsToRedis`）
- 卡初始化逻辑（`progressiveInit`、`initCardsBatch`）
- 任何 Callback 注册（`callbacks.go` 已删除）

#### Scenario: Scheduler 保留套餐过期检测和流量重置触发
- **GIVEN** Phase 4+5 原子部署完成，精简后的 Scheduler 运行中
- **WHEN** 每 10 秒定时器触发
- **THEN** 调用 `PackageActivationHandler.HandlePackageActivationCheck(ctx)` 检查过期套餐；调用 `DataResetHandler.HandleDataReset(ctx)` 检查流量重置；这两个触发器在精简后 Scheduler 中**必须保留**（决策 9）

#### Scenario: Scheduler 精简后行数验证
- **GIVEN** Phase 4+5 重构完成
- **WHEN** 统计 `internal/polling/scheduler.go` 的代码行数
- **THEN** 文件行数（含注释）**< 250 行**（因保留套餐过期/流量重置触发器，此处统一以 design.md 决策 9 和 tasks.md 的 <250 为准）；文件中不出现 `db.Find`、`db.Where`、`loadConfig` 等字样；文件中出现 `HandlePackageActivationCheck` 和 `HandleDataReset`
