## Context

当前 `polling_carddata` 链路已经能正确更新卡维度流量读数，但套餐扣减存在两个缺口：

1. 载体路由缺口：扣减调用固定使用 `carrier_type=iot_card`，导致绑定设备且套餐挂在 `device_id` 时无法命中可扣套餐。
2. 精度缺口：上游流量读数是 `float64 MB`，扣减时转 `int64`，会丢失 `<1MB` 的高频增量。

系统现有分层为 Handler → Service → Store → Model，扣减核心在 `UsageService.DeductDataUsage`，轮询和手动刷新都依赖该能力。为避免分叉逻辑，本次设计将修复集中在 Service 层并保持调用链兼容。

## Goals / Non-Goals

**Goals:**
- 修复绑定设备卡的套餐扣减命中问题，保证设备级套餐能被正确扣减。
- 修复小数增量丢失问题，保证高频小流量场景下长期扣减准确。
- 明确流量字段语义：`current_month_usage_mb` 与运营商周期口径解耦。
- 保持现有 API、数据库表结构和任务调度不破坏。

**Non-Goals:**
- 不重构轮询体系，不调整 Asynq 任务类型。
- 不新增数据库迁移。
- 不改变套餐优先级策略（仍是加油包优先、主套餐兜底）。
- 不新增自动化测试范围（按项目约束，仅提供手动验证方案）。

## Decisions

### 决策 1：载体命中策略下沉到 UsageService

- 方案：`DeductDataUsage` 先按请求载体查询生效套餐；若 `iot_card` 未命中且该卡存在有效设备绑定，则回退按 `device` 查询并扣减。
- 理由：
  - 轮询与手动刷新共用一套扣减逻辑，避免在多个 Handler/Service 重复实现路由判断。
  - 符合分层约束，业务判断集中在 Service 层，Store 层保持通用查询职责。
- 备选方案：在 `polling_carddata_handler` 单点改传 `device`。
  - 未采用原因：手动刷新链路仍会错；后续新入口也会重复踩坑。

### 决策 2：小数增量使用“余量累加”而非直接四舍五入

- 方案：保留扣减单位为整数 MB，但把小数部分按载体维度写 Redis 余量键（带 TTL）；后续增量先与余量相加，再计算本次可扣整数 MB。
- 理由：
  - 不改变 `tb_package_usage.data_usage_mb` 的整数结构，兼容现有统计与查询。
  - 避免每次四舍五入带来的长期偏差。
  - Redis 读写轻量，满足轮询高频场景性能要求。
- 备选方案：直接把 `data_usage_mb` 改为小数。
  - 未采用原因：涉及模型、查询、统计口径和历史数据兼容，变更面过大。

### 决策 3：字段口径保持双轨，不再混用

- 方案：
  - `current_month_usage_mb`：继续定义为“系统自然月累计（卡维度）”。
  - `last_gateway_reading_mb`：用于表达“运营商当前周期累计读数（卡维度）”。
- 理由：
  - 运营商重置日可能不是每月 1 号，不能用自然月字段替代运营商周期口径。
  - 绑定设备不改变流量采集主体，流量仍来自卡 ICCID。
- 备选方案：让 `current_month_usage_mb` 直接改为运营商周期口径。
  - 未采用原因：会破坏现有自然月统计语义，影响已有展示与分析逻辑。

## Risks / Trade-offs

- 风险：余量键读写失败时会影响小数累计精度。
  - Mitigation：失败仅降级为“本次不累计小数”，并记录告警日志，主流程不中断。

- 风险：`iot_card -> device` 回退可能在极端并发下出现重复判断。
  - Mitigation：扣减仍在事务内按当前生效套餐计算，保持幂等结果。

- 风险：前端继续误用 `current_month_usage_mb` 作为运营商周期口径。
  - Mitigation：在 spec 和接口描述中显式标注口径，前端改读 `last_gateway_reading_mb`。

## Migration Plan

1. 先上线 Service 层扣减修复（载体回退 + 小数余量）。
2. 通过日志与数据库手动核对重点卡（绑定设备卡、低流量高频卡）。
3. 前端切换运营商周期展示口径到 `last_gateway_reading_mb`。
4. 观察 1-2 个运营商结算周期，无异常后关闭问题。

回滚策略：
- 若出现异常，可回滚到旧版本代码；该变更无 DB schema 迁移，不涉及数据结构回滚。

## Open Questions

- 余量 Redis 键 TTL 的最终值（建议 7 天）是否需要按运营商周期动态设置。
- 是否需要在管理端 realtime-status 直接返回 `last_gateway_reading_mb`（当前先由前端按现有可用字段接入）。
