refactor: 流量系统重构 — 增量累加算法 + 日粒度缓冲 + 旧详单清理
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m16s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m16s
核心改造: - 增量算法:流量计算从覆盖式改为增量累加(gateway - lastReading),支持上游运营商重置检测 - 日流量缓冲:insertDataUsageRecord 改为 Redis INCRBYFLOAT,每日凌晨落盘到 tb_card_daily_usage - 运营商:新增 data_reset_day 字段(联通=27,其余=1) - IoT卡:新增 last_gateway_reading_mb 字段存储上次网关读数 - 查询层:新建 TrafficQueryService 合并 Redis(今日)+ DB(历史)数据源 - 清理:删除 DataUsageRecord model/store,移除 polling_handler 旧引用 迁移:000094-000097(carrier字段、iot_card字段、数据初始化、日流量表)
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 运营商上游流量重置日配置
|
||||
`tb_carrier` SHALL 包含 `data_reset_day` 字段(INT, 1-28),表示该运营商每月上游流量重置日(即上游运营商清零网关计数器的日期)。创建/编辑运营商时 SHALL 支持设置此字段。
|
||||
|
||||
注意:此字段与套餐级别的 `data_reset_cycle`(daily/monthly/yearly)是**完全独立的两个维度**。`data_reset_day` 用于检测上游网关值下降是否为正常重置,`data_reset_cycle` 用于我们系统套餐的已用量定时归零。
|
||||
|
||||
#### Scenario: 创建运营商时指定重置日
|
||||
- **WHEN** 管理员创建运营商,指定 `data_reset_day = 27`
|
||||
- **THEN** 该运营商记录的 `data_reset_day` SHALL 为 27
|
||||
|
||||
#### Scenario: 轮询时读取重置日判断上游重置
|
||||
- **WHEN** 轮询系统检测到网关流量值下降(`increment < 0`)
|
||||
- **THEN** 系统 SHALL 读取该卡对应运营商的 `data_reset_day`
|
||||
- **AND** 使用 `isResetWindow(now, resetDay)` 判断是否在重置日窗口内(重置日当天 + 前一天容错)
|
||||
- **AND** 窗口内视为正常上游重置,窗口外记录 Warn 日志并丢弃异常值
|
||||
@@ -0,0 +1,37 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: IoT 卡网关读数记录
|
||||
`tb_iot_card` SHALL 包含 `last_gateway_reading_mb` 字段(FLOAT, 默认 0),记录上次轮询时网关返回的流量读数,用于计算增量。
|
||||
|
||||
#### Scenario: 轮询更新网关读数
|
||||
- **WHEN** 轮询系统获取到网关流量值
|
||||
- **THEN** 系统 SHALL 将 `last_gateway_reading_mb` 更新为本次网关返回值(无论增量是否 > 0)
|
||||
|
||||
### Requirement: 月流量增量累加
|
||||
`current_month_usage_mb` SHALL 使用增量累加(`+= increment`)而非直接覆盖(`= gatewayValue`)。增量 = 当前网关读数 - 上次网关读数(`last_gateway_reading_mb`)。
|
||||
|
||||
#### Scenario: 正常流量增长
|
||||
- **WHEN** 上次读数 100MB,本次读数 105MB
|
||||
- **THEN** `current_month_usage_mb` SHALL 增加 5MB(而非被覆盖为 105MB)
|
||||
- **AND** `data_usage_mb`(卡生命周期总用量)SHALL 同步增加 5MB
|
||||
|
||||
#### Scenario: 上游自然月重置
|
||||
- **WHEN** 上次读数 500MB,本次读数 10MB,且在运营商重置日窗口内
|
||||
- **THEN** `increment` SHALL 为 10MB(本次原始值即为增量),`current_month_usage_mb += 10`
|
||||
|
||||
#### Scenario: 非重置日异常下降
|
||||
- **WHEN** 上次读数 500MB,本次读数 10MB,但不在重置日窗口内
|
||||
- **THEN** `increment` SHALL 为 0,记录 Warn 日志,`current_month_usage_mb` 不变
|
||||
|
||||
### Requirement: 跨自然月重置
|
||||
当检测到系统跨自然月时,`current_month_usage_mb` SHALL 重置为 0(不再等于 `gatewayFlowMB`),`last_month_total_mb` SHALL 记录上月累计值。
|
||||
|
||||
#### Scenario: 跨月轮询
|
||||
- **WHEN** 上次轮询在 3 月,本次轮询在 4 月
|
||||
- **THEN** `last_month_total_mb` = 原 `current_month_usage_mb`,`current_month_usage_mb` = 0,`current_month_start_date` 更新为本月 1 日
|
||||
|
||||
### Requirement: 新卡首次轮询
|
||||
新入库的卡 `last_gateway_reading_mb` 默认为 0,首次轮询的增量 = 网关返回的全量值。这是预期行为。批量导入已有使用量的卡时,导入脚本应同步设置 `last_gateway_reading_mb`。
|
||||
|
||||
### Requirement: 增量函数合并
|
||||
`calculateFlowUpdates()` 和 `calculateFlowIncrement()` SHALL 合并为一个函数,返回 `(updates map[string]any, increment float64)`。消除两个独立增量计算函数的不一致风险。
|
||||
@@ -0,0 +1,16 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 流量详单存储方式
|
||||
卡级流量详单 SHALL 从无差别直接写 DB 改为 Redis 缓冲 + 日落盘。`insertDataUsageRecord()` SHALL NOT 再创建 `DataUsageRecord` 记录,直接替换为 Redis 写入。
|
||||
|
||||
#### Scenario: 轮询写流量数据
|
||||
- **WHEN** 轮询系统检测到卡的流量变化(增量 > 0)
|
||||
- **THEN** 系统 SHALL 仅写 Redis 缓冲(`INCRBYFLOAT`),不写 DB
|
||||
|
||||
#### Scenario: 旧代码清理
|
||||
- **WHEN** 新存储路径完全就绪
|
||||
- **THEN** SHALL 删除 `DataUsageRecord` model、`DataUsageRecordStore`、bootstrap 中的相关引用
|
||||
- **AND** `tb_data_usage_record` 旧表数据暂保留在 DB(后续通过数据清理功能清除),代码层完全去除依赖
|
||||
|
||||
### Clarification: 套餐级日记录不受影响
|
||||
`tb_package_usage_daily_record`(套餐级日记录)由 `UsageService.updateDailyRecord()` 在轮询将增量记录到套餐已用量时同步写入,与本次改造的卡级 `tb_card_daily_usage` 是两个完全不同的维度,互不影响。
|
||||
@@ -0,0 +1,30 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 流量增量写入 Redis 缓冲
|
||||
轮询检测到流量增量 > 0 时,系统 SHALL 使用 `INCRBYFLOAT` 将增量写入 Redis key(格式 `traffic:daily:{card_id}:{YYYY-MM-DD}`),TTL 48 小时。增量 <= 0 时 SHALL NOT 写入。**直接替换旧的 `insertDataUsageRecord()` 写 DB 路径,不双写。**
|
||||
|
||||
#### Scenario: 有流量增量时写 Redis
|
||||
- **WHEN** 轮询检测到卡 ID=100 今日流量增量为 5.2 MB
|
||||
- **THEN** 系统 SHALL 执行 `INCRBYFLOAT traffic:daily:100:2026-03-28 5.2`,并设置 48h TTL
|
||||
|
||||
#### Scenario: 无流量增量时跳过
|
||||
- **WHEN** 轮询检测到流量增量 <= 0
|
||||
- **THEN** 系统 SHALL NOT 执行任何 Redis 或 DB 写入
|
||||
|
||||
### Requirement: 每日落盘定时任务
|
||||
系统 SHALL 每天凌晨 2 点(Asia/Shanghai)通过 Asynq Scheduler 触发落盘任务。
|
||||
|
||||
#### Scenario: 正常落盘(分批处理)
|
||||
- **WHEN** 凌晨 2 点落盘任务执行
|
||||
- **THEN** 系统 SHALL 分批 SCAN 所有 `traffic:daily:*:{昨日}` key(每批 COUNT 500)
|
||||
- **AND** 分批 UPSERT 到 `tb_card_daily_usage`(每批 200 条,**覆盖语义**保证幂等:`ON CONFLICT DO UPDATE SET usage_mb = EXCLUDED.usage_mb`)
|
||||
- **AND** 分批 Pipeline 删除已落盘的 Redis key
|
||||
- **AND** 记录日志:落盘条数、耗时
|
||||
|
||||
#### Scenario: 落盘失败重试
|
||||
- **WHEN** 落盘任务执行失败
|
||||
- **THEN** Asynq SHALL 自动重试(MaxRetry=3,超时 5 分钟),Redis key 因 48h TTL 仍可用
|
||||
- **AND** 因 UPSERT 使用覆盖语义,重试时不会导致数据翻倍
|
||||
|
||||
### Requirement: tb_card_daily_usage 表结构
|
||||
`usage_mb` 字段 SHALL 使用 `NUMERIC(12,2)` 类型(匹配 Redis `INCRBYFLOAT` 的浮点精度和现有 `current_month_usage_mb` 的 `decimal(10,2)` 类型),不使用 BIGINT。
|
||||
@@ -0,0 +1,15 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 统一流量查询服务
|
||||
`TrafficQueryService.GetDailyUsage()` SHALL 合并 Redis(今日)和 DB(历史)两段数据,返回指定日期范围内的每日流量列表。
|
||||
|
||||
#### Scenario: 查询含今日的日期范围
|
||||
- **WHEN** 查询 2026-03-25 到 2026-03-28(今天)
|
||||
- **THEN** 系统 SHALL 从 `tb_card_daily_usage` 查 3/25~3/27,从 Redis 查 3/28,合并排序后返回
|
||||
|
||||
#### Scenario: 查询纯历史日期范围
|
||||
- **WHEN** 查询 2026-03-01 到 2026-03-20
|
||||
- **THEN** 系统 SHALL 仅从 `tb_card_daily_usage` 查询,不访问 Redis
|
||||
|
||||
### Requirement: CardDailyUsageStore 共用
|
||||
落盘任务(`daily_traffic_flush.go`)和查询服务(`TrafficQueryService`)SHALL 共用同一个 `CardDailyUsageStore`,避免重复实现 UPSERT 和查询逻辑。
|
||||
Reference in New Issue
Block a user