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字段、数据初始化、日流量表)
8.0 KiB
8.0 KiB
任务清单:refactor-traffic-system
⚠️ 架构改造,涉及 DB 迁移 + 轮询核心路径修改,建议低峰期发布。 严格串行:E-2(增量算法)→ E-1(Redis 缓冲)→ E-3(查询适配)→ 清理。
任务组 1:E-2 月流量改增量累加(P2-27)
1.1 DB 迁移 + Model 扩展
- 1.1.1 新建迁移:
ALTER TABLE tb_carrier ADD COLUMN data_reset_day INT NOT NULL DEFAULT 1; - 1.1.2 在同一迁移中 UPDATE 已有运营商:联通
data_reset_day=27,其余=1(需确认现有运营商数据) - 1.1.3 新建迁移:
ALTER TABLE tb_iot_card ADD COLUMN last_gateway_reading_mb FLOAT NOT NULL DEFAULT 0; - 1.1.4
internal/model/carrier.go新增DataResetDay int字段 - 1.1.5
internal/model/iot_card.go新增LastGatewayReadingMB float64字段 - 1.1.6 运营商相关 DTO 新增
DataResetDay字段 - 1.1.7 运营商 Create/Update Handler/Service 支持
data_reset_day参数(1-28 范围校验) - 1.1.8 执行迁移,通过 DBHub 确认字段已添加
- 1.1.9 验证:
go build ./...编译通过
1.2 增量算法改写
- 1.2.1 新增辅助方法
getCarrierResetDay(carrierID uint) int,从 DB/缓存读取运营商重置日 - 1.2.2 新增辅助函数
isResetWindow(now time.Time, resetDay int) bool:窗口 = 重置日当天 + 前一天(容错网关延迟)。resetDay=27时窗口含 26、27;resetDay=1时窗口含上月最后一天和 1 号 - 1.2.3 合并
calculateFlowUpdates()和calculateFlowIncrement()为一个函数,签名改为calculateFlowUpdates(card, gatewayFlowMB, now) (map[string]any, float64),返回(updates, increment):- 计算增量
increment = gatewayFlowMB - card.LastGatewayReadingMB - 检测上游运营商重置(increment < 0 时用
isResetWindow判断) - 检测跨自然月:跨月时
current_month_usage_mb = 0(不再= gatewayFlowMB),last_month_total_mb = 旧值 - 正常增量:
current_month_usage_mb改为gorm.Expr("current_month_usage_mb + ?", increment) - 更新
data_usage_mb(卡生命周期总用量):gorm.Expr("data_usage_mb + ?", int64(increment)) - 始终更新
last_gateway_reading_mb = gatewayFlowMB
- 计算增量
- 1.2.4 删除旧的
calculateFlowIncrement()函数 - 1.2.5 更新调用方:
HandleCarddataCheck()中改为使用合并后的返回值updates, flowIncrementMB := h.calculateFlowUpdates(card, gatewayFlowMB, now) - 1.2.6 验证:
go build ./...编译通过
1.3 数据初始化脚本
- 1.3.1 新建迁移文件(或独立 SQL 脚本):
UPDATE tb_iot_card SET last_gateway_reading_mb = current_month_usage_mb WHERE last_gateway_reading_mb = 0 AND current_month_usage_mb > 0; - 1.3.2 通过 DBHub 验证执行结果:
SELECT COUNT(*) FROM tb_iot_card WHERE last_gateway_reading_mb = 0 AND current_month_usage_mb > 0;应为 0
任务组 2:E-1 流量详单改日粒度缓冲(P2-26)
2.1 日流量表 + Model + Store + Redis Key
- 2.1.1 新建迁移:
CREATE TABLE tb_card_daily_usage ( id BIGSERIAL PRIMARY KEY, iot_card_id BIGINT NOT NULL, date DATE NOT NULL, usage_mb NUMERIC(12,2) NOT NULL DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT uk_card_daily UNIQUE (iot_card_id, date) ); CREATE INDEX idx_card_daily_date ON tb_card_daily_usage (date); - 2.1.2 新建
internal/model/card_daily_usage.goGORM Model(UsageMB使用float64类型,对应 DBNUMERIC(12,2)) - 2.1.3 新建
internal/store/postgres/card_daily_usage_store.go,实现Upsert()(覆盖语义,ON CONFLICT DO UPDATE SET usage_mb = EXCLUDED.usage_mb)和ListByCardIDAndDateRange()方法。此 Store 同时供落盘任务(2.3)和查询层(3.x)使用 - 2.1.4 在
pkg/constants/redis.go新增RedisCardDailyTrafficKey(cardID uint, date string) string - 2.1.5 在
pkg/constants/constants.go新增TaskTypeDailyTrafficFlush常量 - 2.1.6 执行迁移,通过 DBHub 确认表已创建
- 2.1.7 验证:
go build ./...编译通过
2.2 insertDataUsageRecord 改造
- 2.2.1 改写
insertDataUsageRecord():增量 <= 0 直接 return;增量 > 0 时INCRBYFLOAT写 Redis +Expire48h。不再写tb_data_usage_record(直接替换,不双写) - 2.2.2 验证:
go build ./...编译通过
2.3 每日落盘任务
- 2.3.1 新建
internal/task/daily_traffic_flush.go,实现落盘 Handler:- 分批 SCAN
traffic:daily:*:{昨日}(每批 COUNT 500) - 解析 card_id 和 date,收集所有 entry
- 分批 UPSERT 到
tb_card_daily_usage(每批 200 条,使用 2.1.3 的CardDailyUsageStore.Upsert(),覆盖语义保证幂等) - 分批 Pipeline 删除已落盘的 Redis key
- 记录日志:落盘条数、耗时
- 分批 SCAN
- 2.3.2 在
pkg/queue/handler.go中注册落盘 Handler - 2.3.3 在 Asynq Scheduler 中配置每天凌晨 2 点(Asia/Shanghai)触发
TaskTypeDailyTrafficFlush,超时设为 5 分钟 - 2.3.4 验证:
go build ./...编译通过
任务组 3:E-3 流量查询层适配(P2-31)
3.1 TrafficQueryService + bootstrap 注册
- 3.1.1 新建
internal/service/traffic/query_service.go,实现GetDailyUsage(ctx, cardID, startDate, endDate)方法:今天 → Redis,历史 → DB(使用 2.1.3 的CardDailyUsageStore),合并排序返回 - 3.1.2 在 bootstrap 中注册
TrafficQueryService
3.2 管理端接口适配
- 3.2.1 更新
GET /api/admin/assets/:type/:id/realtime-status(service/asset/service.go):CurrentMonthUsageMB语义从"上游原始值"变为"系统累计值",确认注释和 DTO description 更新 - 3.2.2 更新
GET /api/admin/assets/resolve/:identifier(handler/admin/asset.go):resolve 返回的流量概况使用新语义 - 3.2.3 更新
GET /api/admin/assets/device/:id/realtime-status(service/asset/service.go):设备级聚合绑定卡的CurrentMonthUsageMB,确认聚合逻辑和注释更新
3.3 C端接口适配
- 3.3.1 更新
GET /api/c/v1/asset/info(handler/app/client_asset.go):C端首页资产信息中的流量数据使用新语义 - 3.3.2 更新
GET /api/c/v1/asset/packages(handler/app/client_asset.go):套餐列表中的用量展示 - 3.3.3 更新
GET /api/c/v1/asset/refresh(handler/app/client_asset.go):刷新后返回的流量数据 - 3.3.4 更新
GET /api/admin/assets/:type/:id/packages(service/asset/service.go):管理端套餐列表
3.4 DTO 和注释更新
- 3.4.1 更新
AssetRealtimeStatusResponse.CurrentMonthUsageMB的 description 标签:从"Gateway返回的自然月流量总量"改为"系统累计的自然月流量" - 3.4.2 更新
IotCard.CurrentMonthUsageMB的 GORM comment:同上 - 3.4.3 验证:
go build ./...编译通过
任务组 4:旧流量详单代码清理
- 4.1 删除
internal/model/data_usage.go(DataUsageRecordmodel) - 4.2 删除
internal/store/postgres/data_usage_record_store.go - 4.3 清理
internal/bootstrap/stores.go、services.go、worker_stores.go、worker_services.go、handlers.go中DataUsageRecord相关引用 - 4.4 清理
pkg/queue/types.go中DataUsageRecord相关字段 - 4.5 清理
scripts/cleanup/main.go中tb_data_usage_record引用(改为tb_card_daily_usage) - 4.6 删除
internal/task/polling_handler.go中旧的insertDataUsageRecord相关的已废弃代码(如有残留) - 4.7 验证:
go build ./...编译通过
收尾验证
- 5.1 执行
go build ./...,确认全量编译通过 - 5.2 通过 DBHub 确认新增表和字段存在
- 5.3 通过
rg "RedisCardDailyTrafficKey\|TaskTypeDailyTrafficFlush\|TrafficQueryService\|CardDailyUsageStore"确认新组件已连通 - 5.4 通过
rg "DataUsageRecord\|data_usage_record"确认旧代码已清理(只允许出现在迁移文件或注释中)