refactor: 流量系统重构 — 增量累加算法 + 日粒度缓冲 + 旧详单清理
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:
2026-03-30 09:59:30 +08:00
parent f5dd2ce4ab
commit cebcada950
44 changed files with 1149 additions and 1613 deletions

View File

@@ -0,0 +1,57 @@
package postgres
import (
"context"
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"github.com/break/junhong_cmp_fiber/internal/model"
)
// CardDailyUsageStore 卡日流量记录存储
// 同时供落盘任务和查询层使用
type CardDailyUsageStore struct {
db *gorm.DB
}
func NewCardDailyUsageStore(db *gorm.DB) *CardDailyUsageStore {
return &CardDailyUsageStore{db: db}
}
// Upsert 覆盖写入日流量记录(幂等安全)
// ON CONFLICT (iot_card_id, date) DO UPDATE SET usage_mb = EXCLUDED.usage_mb
func (s *CardDailyUsageStore) Upsert(ctx context.Context, record *model.CardDailyUsage) error {
return s.db.WithContext(ctx).
Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "iot_card_id"}, {Name: "date"}},
DoUpdates: clause.AssignmentColumns([]string{"usage_mb", "updated_at"}),
}).
Create(record).Error
}
// UpsertBatch 批量覆盖写入日流量记录
func (s *CardDailyUsageStore) UpsertBatch(ctx context.Context, records []*model.CardDailyUsage) error {
if len(records) == 0 {
return nil
}
return s.db.WithContext(ctx).
Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "iot_card_id"}, {Name: "date"}},
DoUpdates: clause.AssignmentColumns([]string{"usage_mb", "updated_at"}),
}).
CreateInBatches(records, 200).Error
}
// ListByCardIDAndDateRange 查询卡在日期范围内的日流量记录
func (s *CardDailyUsageStore) ListByCardIDAndDateRange(ctx context.Context, cardID uint, startDate, endDate time.Time) ([]*model.CardDailyUsage, error) {
var records []*model.CardDailyUsage
if err := s.db.WithContext(ctx).
Where("iot_card_id = ? AND date >= ? AND date <= ?", cardID, startDate, endDate).
Order("date ASC").
Find(&records).Error; err != nil {
return nil, err
}
return records, nil
}

View File

@@ -1,91 +0,0 @@
package postgres
import (
"context"
"time"
"gorm.io/gorm"
"github.com/break/junhong_cmp_fiber/internal/model"
)
// DataUsageRecordStore 流量使用记录存储
type DataUsageRecordStore struct {
db *gorm.DB
}
// NewDataUsageRecordStore 创建流量使用记录存储实例
func NewDataUsageRecordStore(db *gorm.DB) *DataUsageRecordStore {
return &DataUsageRecordStore{db: db}
}
// Create 创建流量使用记录
func (s *DataUsageRecordStore) Create(ctx context.Context, record *model.DataUsageRecord) error {
return s.db.WithContext(ctx).Create(record).Error
}
// CreateBatch 批量创建流量使用记录
func (s *DataUsageRecordStore) CreateBatch(ctx context.Context, records []*model.DataUsageRecord) error {
if len(records) == 0 {
return nil
}
return s.db.WithContext(ctx).CreateInBatches(records, 100).Error
}
// GetLatestByCardID 获取卡的最新流量记录
func (s *DataUsageRecordStore) GetLatestByCardID(ctx context.Context, cardID uint) (*model.DataUsageRecord, error) {
var record model.DataUsageRecord
if err := s.db.WithContext(ctx).
Where("iot_card_id = ?", cardID).
Order("check_time DESC").
First(&record).Error; err != nil {
return nil, err
}
return &record, nil
}
// ListByCardID 获取卡的流量记录列表
func (s *DataUsageRecordStore) ListByCardID(ctx context.Context, cardID uint, startTime, endTime *time.Time, page, pageSize int) ([]*model.DataUsageRecord, int64, error) {
var records []*model.DataUsageRecord
var total int64
query := s.db.WithContext(ctx).Model(&model.DataUsageRecord{}).Where("iot_card_id = ?", cardID)
if startTime != nil {
query = query.Where("check_time >= ?", *startTime)
}
if endTime != nil {
query = query.Where("check_time <= ?", *endTime)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
offset := (page - 1) * pageSize
if err := query.Order("check_time DESC").Offset(offset).Limit(pageSize).Find(&records).Error; err != nil {
return nil, 0, err
}
return records, total, nil
}
// DeleteOlderThan 删除指定时间之前的记录(用于数据清理)
func (s *DataUsageRecordStore) DeleteOlderThan(ctx context.Context, before time.Time, batchSize int) (int64, error) {
result := s.db.WithContext(ctx).
Where("check_time < ?", before).
Limit(batchSize).
Delete(&model.DataUsageRecord{})
return result.RowsAffected, result.Error
}
// CountByCardID 统计卡的流量记录数量
func (s *DataUsageRecordStore) CountByCardID(ctx context.Context, cardID uint) (int64, error) {
var count int64
if err := s.db.WithContext(ctx).Model(&model.DataUsageRecord{}).
Where("iot_card_id = ?", cardID).
Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}