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:
@@ -41,6 +41,7 @@ import (
|
||||
shopPackageBatchAllocationSvc "github.com/break/junhong_cmp_fiber/internal/service/shop_package_batch_allocation"
|
||||
shopPackageBatchPricingSvc "github.com/break/junhong_cmp_fiber/internal/service/shop_package_batch_pricing"
|
||||
shopSeriesGrantSvc "github.com/break/junhong_cmp_fiber/internal/service/shop_series_grant"
|
||||
trafficSvc "github.com/break/junhong_cmp_fiber/internal/service/traffic"
|
||||
wechatConfigSvc "github.com/break/junhong_cmp_fiber/internal/service/wechat_config"
|
||||
)
|
||||
|
||||
@@ -94,6 +95,7 @@ type services struct {
|
||||
AgentRecharge *agentRechargeSvc.Service
|
||||
PackageActivation *packageSvc.ActivationService
|
||||
Refund *refundSvc.Service
|
||||
TrafficQuery *trafficSvc.QueryService
|
||||
}
|
||||
|
||||
func initServices(s *stores, deps *Dependencies) *services {
|
||||
@@ -210,6 +212,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
||||
deps.Logger,
|
||||
),
|
||||
PackageActivation: packageActivation,
|
||||
TrafficQuery: trafficSvc.NewQueryService(deps.Redis, s.CardDailyUsage),
|
||||
Refund: refundSvc.New(
|
||||
deps.DB,
|
||||
s.RefundRequest,
|
||||
|
||||
@@ -61,6 +61,8 @@ type stores struct {
|
||||
WechatConfig *postgres.WechatConfigStore
|
||||
// 退款系统
|
||||
RefundRequest *postgres.RefundStore
|
||||
// 流量系统
|
||||
CardDailyUsage *postgres.CardDailyUsageStore
|
||||
}
|
||||
|
||||
func initStores(deps *Dependencies) *stores {
|
||||
@@ -119,5 +121,6 @@ func initStores(deps *Dependencies) *stores {
|
||||
AssetRecharge: postgres.NewAssetRechargeStore(deps.DB, deps.Redis),
|
||||
WechatConfig: postgres.NewWechatConfigStore(deps.DB, deps.Redis),
|
||||
RefundRequest: postgres.NewRefundStore(deps.DB),
|
||||
CardDailyUsage: postgres.NewCardDailyUsageStore(deps.DB),
|
||||
}
|
||||
}
|
||||
|
||||
18
internal/model/card_daily_usage.go
Normal file
18
internal/model/card_daily_usage.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// CardDailyUsage 卡日流量记录模型
|
||||
// 每卡每天一条记录,由每日落盘任务从 Redis 写入
|
||||
type CardDailyUsage struct {
|
||||
ID uint `gorm:"column:id;primaryKey;comment:记录ID" json:"id"`
|
||||
IotCardID uint `gorm:"column:iot_card_id;not null;uniqueIndex:uk_card_daily;comment:IoT卡ID" json:"iot_card_id"`
|
||||
Date time.Time `gorm:"column:date;type:date;not null;uniqueIndex:uk_card_daily;comment:日期" json:"date"`
|
||||
UsageMB float64 `gorm:"column:usage_mb;type:numeric(12,2);not null;default:0;comment:当日流量使用量(MB)" json:"usage_mb"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime;comment:更新时间" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (CardDailyUsage) TableName() string {
|
||||
return "tb_card_daily_usage"
|
||||
}
|
||||
@@ -15,6 +15,7 @@ type Carrier struct {
|
||||
BillingDay int `gorm:"column:billing_day;type:int;default:1;comment:运营商计费日(用于流量查询接口的计费周期计算,联通=27,其他=1)" json:"billing_day"`
|
||||
RealnameLinkType string `gorm:"column:realname_link_type;type:varchar(20);not null;default:'none';comment:实名链接类型 none-不支持 template-模板URL gateway-Gateway接口" json:"realname_link_type"`
|
||||
RealnameLinkTemplate string `gorm:"column:realname_link_template;type:varchar(500);default:'';comment:实名链接模板URL" json:"realname_link_template"`
|
||||
DataResetDay int `gorm:"column:data_reset_day;type:int;not null;default:1;comment:上游流量重置日(1-28) 运营商每月清零网关计数器的日期" json:"data_reset_day"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// DataUsageRecord 流量使用记录模型
|
||||
// 记录卡的流量历史,支持流量查询和分析
|
||||
// 注意:此模型是日志表,不需要软删除和审计字段
|
||||
type DataUsageRecord struct {
|
||||
ID uint `gorm:"column:id;primaryKey;comment:流量使用记录ID" json:"id"`
|
||||
IotCardID uint `gorm:"column:iot_card_id;index;not null;comment:IoT卡ID" json:"iot_card_id"`
|
||||
DataUsageMB int64 `gorm:"column:data_usage_mb;type:bigint;not null;comment:流量使用量(MB)" json:"data_usage_mb"`
|
||||
DataIncreaseMB int64 `gorm:"column:data_increase_mb;type:bigint;default:0;comment:相比上次的增量(MB)" json:"data_increase_mb"`
|
||||
CheckTime time.Time `gorm:"column:check_time;not null;comment:检查时间" json:"check_time"`
|
||||
Source string `gorm:"column:source;type:varchar(50);default:'polling';comment:数据来源 polling-轮询 manual-手动 gateway-回调" json:"source"`
|
||||
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime;comment:创建时间" json:"created_at"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (DataUsageRecord) TableName() string {
|
||||
return "tb_data_usage_record"
|
||||
}
|
||||
@@ -76,7 +76,7 @@ type AssetRealtimeStatusResponse struct {
|
||||
AssetID uint `json:"asset_id" description:"资产ID"`
|
||||
NetworkStatus int `json:"network_status,omitempty" description:"网络状态(asset_type=card时有效):0停机 1开机"`
|
||||
RealNameStatus int `json:"real_name_status,omitempty" description:"实名状态(asset_type=card时有效):0未实名 1已实名"`
|
||||
CurrentMonthUsageMB float64 `json:"current_month_usage_mb,omitempty" description:"本月已用流量MB(asset_type=card时有效)"`
|
||||
CurrentMonthUsageMB float64 `json:"current_month_usage_mb,omitempty" description:"系统累计的自然月流量MB(asset_type=card时有效)"`
|
||||
LastSyncTime *time.Time `json:"last_sync_time,omitempty" description:"最后同步时间(asset_type=card时有效)"`
|
||||
DeviceProtectStatus string `json:"device_protect_status,omitempty" description:"保护期状态(asset_type=device时有效):none/stop/start"`
|
||||
Cards []BoundCardInfo `json:"cards,omitempty" description:"绑定卡状态列表(asset_type=device时有效)"`
|
||||
|
||||
@@ -5,6 +5,7 @@ type CreateCarrierRequest struct {
|
||||
CarrierName string `json:"carrier_name" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"运营商名称"`
|
||||
CarrierType string `json:"carrier_type" validate:"required,oneof=CMCC CUCC CTCC CBN" required:"true" description:"运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)"`
|
||||
Description string `json:"description" validate:"omitempty,max=500" maxLength:"500" description:"运营商描述"`
|
||||
DataResetDay *int `json:"data_reset_day" validate:"omitempty,min=1,max=28" minimum:"1" maximum:"28" description:"上游流量重置日(1-28),运营商每月清零网关计数器的日期,默认1"`
|
||||
RealnameLinkType *string `json:"realname_link_type" validate:"omitempty,oneof=none template gateway" description:"实名链接类型 none-不支持 template-模板URL gateway-Gateway接口"`
|
||||
RealnameLinkTemplate *string `json:"realname_link_template" validate:"omitempty,max=500" maxLength:"500" description:"实名链接模板URL,支持 {iccid}/{msisdn}/{virtual_no} 占位符"`
|
||||
}
|
||||
@@ -12,6 +13,7 @@ type CreateCarrierRequest struct {
|
||||
type UpdateCarrierRequest struct {
|
||||
CarrierName *string `json:"carrier_name" validate:"omitempty,min=1,max=100" minLength:"1" maxLength:"100" description:"运营商名称"`
|
||||
Description *string `json:"description" validate:"omitempty,max=500" maxLength:"500" description:"运营商描述"`
|
||||
DataResetDay *int `json:"data_reset_day" validate:"omitempty,min=1,max=28" minimum:"1" maximum:"28" description:"上游流量重置日(1-28),运营商每月清零网关计数器的日期"`
|
||||
RealnameLinkType *string `json:"realname_link_type" validate:"omitempty,oneof=none template gateway" description:"实名链接类型 none-不支持 template-模板URL gateway-Gateway接口"`
|
||||
RealnameLinkTemplate *string `json:"realname_link_template" validate:"omitempty,max=500" maxLength:"500" description:"实名链接模板URL,支持 {iccid}/{msisdn}/{virtual_no} 占位符"`
|
||||
}
|
||||
@@ -34,6 +36,7 @@ type CarrierResponse struct {
|
||||
CarrierName string `json:"carrier_name" description:"运营商名称"`
|
||||
CarrierType string `json:"carrier_type" description:"运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)"`
|
||||
Description string `json:"description" description:"运营商描述"`
|
||||
DataResetDay int `json:"data_reset_day" description:"上游流量重置日(1-28)"`
|
||||
RealnameLinkType string `json:"realname_link_type" description:"实名链接类型 none-不支持 template-模板URL gateway-Gateway接口"`
|
||||
RealnameLinkTemplate string `json:"realname_link_template" description:"实名链接模板URL"`
|
||||
Status int `json:"status" description:"状态 (1:启用, 0:禁用)"`
|
||||
|
||||
@@ -30,7 +30,7 @@ type IotCard struct {
|
||||
RealNameStatus int `gorm:"column:real_name_status;type:int;default:0;not null;comment:实名状态 0-未实名 1-已实名" json:"real_name_status"`
|
||||
NetworkStatus int `gorm:"column:network_status;type:int;default:0;not null;comment:网络状态 0-停机 1-开机" json:"network_status"`
|
||||
DataUsageMB int64 `gorm:"column:data_usage_mb;type:bigint;default:0;comment:累计流量使用(MB)" json:"data_usage_mb"`
|
||||
CurrentMonthUsageMB float64 `gorm:"column:current_month_usage_mb;type:decimal(10,2);default:0;comment:本月已用流量(MB) - Gateway返回的自然月流量总量" json:"current_month_usage_mb"`
|
||||
CurrentMonthUsageMB float64 `gorm:"column:current_month_usage_mb;type:decimal(10,2);default:0;comment:系统累计的自然月流量(MB)" json:"current_month_usage_mb"`
|
||||
CurrentMonthStartDate *time.Time `gorm:"column:current_month_start_date;type:date;comment:本月开始日期 - 用于检测跨月流量重置" json:"current_month_start_date"`
|
||||
LastMonthTotalMB float64 `gorm:"column:last_month_total_mb;type:decimal(10,2);default:0;comment:上月结束时的总流量(MB) - 用于跨月流量计算" json:"last_month_total_mb"`
|
||||
EnablePolling bool `gorm:"column:enable_polling;type:boolean;default:true;comment:是否参与轮询 true-参与 false-不参与" json:"enable_polling"`
|
||||
@@ -44,14 +44,15 @@ type IotCard struct {
|
||||
FirstRechargeTriggeredBySeriesJSON string `gorm:"column:first_recharge_triggered_by_series;type:jsonb;default:'{}';comment:按套餐系列追踪的首充触发状态" json:"-"`
|
||||
|
||||
// 任务 24.1: 停复机相关字段
|
||||
FirstRealnameAt *time.Time `gorm:"column:first_realname_at;comment:首次实名时间(用于触发首次实名激活)" json:"first_realname_at,omitempty"`
|
||||
StoppedAt *time.Time `gorm:"column:stopped_at;comment:停机时间" json:"stopped_at,omitempty"`
|
||||
ResumedAt *time.Time `gorm:"column:resumed_at;comment:最近复机时间" json:"resumed_at,omitempty"`
|
||||
StopReason string `gorm:"column:stop_reason;type:varchar(50);comment:停机原因(traffic_exhausted=流量耗尽,manual=手动停机,arrears=欠费)" json:"stop_reason,omitempty"`
|
||||
AssetStatus int `gorm:"column:asset_status;type:int;not null;default:1;comment:业务状态 1-在库 2-已销售 3-已换货 4-已停用" json:"asset_status"`
|
||||
Generation int `gorm:"column:generation;type:int;not null;default:1;comment:资产世代编号" json:"generation"`
|
||||
IsStandalone bool `gorm:"column:is_standalone;type:boolean;default:true;not null;comment:是否为独立卡(未绑定设备) 由触发器自动维护" json:"is_standalone"`
|
||||
VirtualNo string `gorm:"column:virtual_no;type:varchar(50);uniqueIndex:idx_iot_card_virtual_no,where:deleted_at IS NULL AND virtual_no IS NOT NULL AND virtual_no <> '';comment:虚拟号(可空,全局唯一)" json:"virtual_no,omitempty"`
|
||||
FirstRealnameAt *time.Time `gorm:"column:first_realname_at;comment:首次实名时间(用于触发首次实名激活)" json:"first_realname_at,omitempty"`
|
||||
StoppedAt *time.Time `gorm:"column:stopped_at;comment:停机时间" json:"stopped_at,omitempty"`
|
||||
ResumedAt *time.Time `gorm:"column:resumed_at;comment:最近复机时间" json:"resumed_at,omitempty"`
|
||||
StopReason string `gorm:"column:stop_reason;type:varchar(50);comment:停机原因(traffic_exhausted=流量耗尽,manual=手动停机,arrears=欠费)" json:"stop_reason,omitempty"`
|
||||
AssetStatus int `gorm:"column:asset_status;type:int;not null;default:1;comment:业务状态 1-在库 2-已销售 3-已换货 4-已停用" json:"asset_status"`
|
||||
Generation int `gorm:"column:generation;type:int;not null;default:1;comment:资产世代编号" json:"generation"`
|
||||
IsStandalone bool `gorm:"column:is_standalone;type:boolean;default:true;not null;comment:是否为独立卡(未绑定设备) 由触发器自动维护" json:"is_standalone"`
|
||||
VirtualNo string `gorm:"column:virtual_no;type:varchar(50);uniqueIndex:idx_iot_card_virtual_no,where:deleted_at IS NULL AND virtual_no IS NOT NULL AND virtual_no <> '';comment:虚拟号(可空,全局唯一)" json:"virtual_no,omitempty"`
|
||||
LastGatewayReadingMB float64 `gorm:"column:last_gateway_reading_mb;type:float;not null;default:0;comment:上次轮询时网关返回的流量读数(MB)" json:"last_gateway_reading_mb"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
|
||||
@@ -35,11 +35,15 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateCarrierRequest) (*d
|
||||
}
|
||||
|
||||
carrier := &model.Carrier{
|
||||
CarrierCode: req.CarrierCode,
|
||||
CarrierName: req.CarrierName,
|
||||
CarrierType: req.CarrierType,
|
||||
Description: req.Description,
|
||||
Status: constants.StatusEnabled,
|
||||
CarrierCode: req.CarrierCode,
|
||||
CarrierName: req.CarrierName,
|
||||
CarrierType: req.CarrierType,
|
||||
Description: req.Description,
|
||||
Status: constants.StatusEnabled,
|
||||
DataResetDay: 1,
|
||||
}
|
||||
if req.DataResetDay != nil {
|
||||
carrier.DataResetDay = *req.DataResetDay
|
||||
}
|
||||
if req.RealnameLinkType != nil {
|
||||
carrier.RealnameLinkType = *req.RealnameLinkType
|
||||
@@ -87,6 +91,9 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateCarrierReq
|
||||
if req.Description != nil {
|
||||
carrier.Description = *req.Description
|
||||
}
|
||||
if req.DataResetDay != nil {
|
||||
carrier.DataResetDay = *req.DataResetDay
|
||||
}
|
||||
if req.RealnameLinkType != nil {
|
||||
carrier.RealnameLinkType = *req.RealnameLinkType
|
||||
}
|
||||
@@ -189,6 +196,7 @@ func (s *Service) toResponse(c *model.Carrier) *dto.CarrierResponse {
|
||||
CarrierName: c.CarrierName,
|
||||
CarrierType: c.CarrierType,
|
||||
Description: c.Description,
|
||||
DataResetDay: c.DataResetDay,
|
||||
RealnameLinkType: c.RealnameLinkType,
|
||||
RealnameLinkTemplate: c.RealnameLinkTemplate,
|
||||
Status: c.Status,
|
||||
|
||||
91
internal/service/traffic/query_service.go
Normal file
91
internal/service/traffic/query_service.go
Normal file
@@ -0,0 +1,91 @@
|
||||
// Package traffic 提供流量数据查询服务
|
||||
// 合并 Redis 缓冲(今日)和 DB(历史)两段数据源,对外提供统一的日粒度流量查询
|
||||
package traffic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// DailyUsageItem 日粒度流量记录
|
||||
type DailyUsageItem struct {
|
||||
Date string `json:"date"` // 日期,格式 YYYY-MM-DD
|
||||
UsageMB float64 `json:"usage_mb"` // 当天累计使用流量(MB)
|
||||
}
|
||||
|
||||
// QueryService 流量查询服务
|
||||
// 合并 Redis(当天实时)和 DB(历史落盘)两段数据源
|
||||
type QueryService struct {
|
||||
redis *redis.Client
|
||||
cardDailyUsageStore *postgres.CardDailyUsageStore
|
||||
}
|
||||
|
||||
// NewQueryService 创建流量查询服务实例
|
||||
func NewQueryService(redisClient *redis.Client, cardDailyUsageStore *postgres.CardDailyUsageStore) *QueryService {
|
||||
return &QueryService{
|
||||
redis: redisClient,
|
||||
cardDailyUsageStore: cardDailyUsageStore,
|
||||
}
|
||||
}
|
||||
|
||||
// GetDailyUsage 查询卡在指定日期范围内的日粒度流量
|
||||
// 今天的数据从 Redis 缓冲读取(可能尚未落盘),历史数据从 DB 读取,合并后按日期 ASC 排序返回
|
||||
func (s *QueryService) GetDailyUsage(ctx context.Context, cardID uint, startDate, endDate time.Time) ([]*DailyUsageItem, error) {
|
||||
now := time.Now()
|
||||
todayStr := now.Format("2006-01-02")
|
||||
|
||||
// 判断结束日期是否包含今天
|
||||
endDateStr := endDate.Format("2006-01-02")
|
||||
includeToday := endDateStr >= todayStr
|
||||
|
||||
// 历史部分:从 DB 查询(排除今天,因为今天的数据可能还在 Redis 中)
|
||||
dbEndDate := endDate
|
||||
if includeToday {
|
||||
// DB 只查到昨天,今天从 Redis 取
|
||||
dbEndDate = now.AddDate(0, 0, -1)
|
||||
}
|
||||
|
||||
var items []*DailyUsageItem
|
||||
|
||||
// 只有 startDate <= dbEndDate 时才查 DB
|
||||
if !startDate.After(dbEndDate) {
|
||||
records, err := s.cardDailyUsageStore.ListByCardIDAndDateRange(ctx, cardID, startDate, dbEndDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range records {
|
||||
items = append(items, &DailyUsageItem{
|
||||
Date: r.Date.Format("2006-01-02"),
|
||||
UsageMB: r.UsageMB,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 今天部分:从 Redis 读取
|
||||
if includeToday && !startDate.After(now) {
|
||||
key := constants.RedisCardDailyTrafficKey(cardID, todayStr)
|
||||
val, err := s.redis.Get(ctx, key).Result()
|
||||
if err == nil && val != "" {
|
||||
if usageMB, parseErr := strconv.ParseFloat(val, 64); parseErr == nil && usageMB > 0 {
|
||||
items = append(items, &DailyUsageItem{
|
||||
Date: todayStr,
|
||||
UsageMB: usageMB,
|
||||
})
|
||||
}
|
||||
}
|
||||
// Redis key 不存在(redis.Nil)时忽略,表示今天暂无流量
|
||||
}
|
||||
|
||||
// 按日期 ASC 排序
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return items[i].Date < items[j].Date
|
||||
})
|
||||
|
||||
return items, nil
|
||||
}
|
||||
57
internal/store/postgres/card_daily_usage_store.go
Normal file
57
internal/store/postgres/card_daily_usage_store.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
151
internal/task/daily_traffic_flush.go
Normal file
151
internal/task/daily_traffic_flush.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
)
|
||||
|
||||
// DailyTrafficFlushHandler 每日流量落盘任务处理器
|
||||
// 凌晨 2 点分批 SCAN 昨日 Redis key → UPSERT 到 tb_card_daily_usage → 删 key
|
||||
type DailyTrafficFlushHandler struct {
|
||||
redis *redis.Client
|
||||
cardDailyUsageStore *postgres.CardDailyUsageStore
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewDailyTrafficFlushHandler(
|
||||
redisClient *redis.Client,
|
||||
cardDailyUsageStore *postgres.CardDailyUsageStore,
|
||||
logger *zap.Logger,
|
||||
) *DailyTrafficFlushHandler {
|
||||
return &DailyTrafficFlushHandler{
|
||||
redis: redisClient,
|
||||
cardDailyUsageStore: cardDailyUsageStore,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DailyTrafficFlushHandler) HandleDailyTrafficFlush(ctx context.Context, _ *asynq.Task) error {
|
||||
startTime := time.Now()
|
||||
|
||||
yesterday := time.Now().AddDate(0, 0, -1).Format("2006-01-02")
|
||||
pattern := fmt.Sprintf("traffic:daily:*:%s", yesterday)
|
||||
|
||||
h.logger.Info("开始每日流量落盘",
|
||||
zap.String("date", yesterday),
|
||||
zap.String("pattern", pattern))
|
||||
|
||||
// 分批 SCAN 收集所有昨日 key
|
||||
var allKeys []string
|
||||
var cursor uint64
|
||||
for {
|
||||
keys, nextCursor, err := h.redis.Scan(ctx, cursor, pattern, 500).Result()
|
||||
if err != nil {
|
||||
h.logger.Error("SCAN Redis key 失败", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
allKeys = append(allKeys, keys...)
|
||||
cursor = nextCursor
|
||||
if cursor == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(allKeys) == 0 {
|
||||
h.logger.Info("无需落盘,昨日无流量数据", zap.String("date", yesterday))
|
||||
return nil
|
||||
}
|
||||
|
||||
h.logger.Info("收集到待落盘 key",
|
||||
zap.String("date", yesterday),
|
||||
zap.Int("count", len(allKeys)))
|
||||
|
||||
yesterdayDate, _ := time.Parse("2006-01-02", yesterday)
|
||||
|
||||
// 分批处理:每批 200 条
|
||||
flushedCount := 0
|
||||
for i := 0; i < len(allKeys); i += 200 {
|
||||
end := i + 200
|
||||
if end > len(allKeys) {
|
||||
end = len(allKeys)
|
||||
}
|
||||
batch := allKeys[i:end]
|
||||
|
||||
var records []*model.CardDailyUsage
|
||||
var flushedKeys []string
|
||||
|
||||
for _, key := range batch {
|
||||
cardID, err := parseCardIDFromKey(key)
|
||||
if err != nil {
|
||||
h.logger.Warn("解析 Redis key 失败", zap.String("key", key), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
val, err := h.redis.Get(ctx, key).Float64()
|
||||
if err != nil {
|
||||
h.logger.Warn("读取 Redis 值失败", zap.String("key", key), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
records = append(records, &model.CardDailyUsage{
|
||||
IotCardID: cardID,
|
||||
Date: yesterdayDate,
|
||||
UsageMB: val,
|
||||
})
|
||||
flushedKeys = append(flushedKeys, key)
|
||||
}
|
||||
|
||||
// 批量 UPSERT 到 DB
|
||||
if len(records) > 0 {
|
||||
if err := h.cardDailyUsageStore.UpsertBatch(ctx, records); err != nil {
|
||||
h.logger.Error("批量 UPSERT 失败",
|
||||
zap.Int("batch_size", len(records)),
|
||||
zap.Error(err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除已落盘的 Redis key
|
||||
if len(flushedKeys) > 0 {
|
||||
pipe := h.redis.Pipeline()
|
||||
for _, key := range flushedKeys {
|
||||
pipe.Del(ctx, key)
|
||||
}
|
||||
if _, err := pipe.Exec(ctx); err != nil {
|
||||
h.logger.Warn("批量删除 Redis key 失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
flushedCount += len(records)
|
||||
}
|
||||
|
||||
h.logger.Info("每日流量落盘完成",
|
||||
zap.String("date", yesterday),
|
||||
zap.Int("flushed_count", flushedCount),
|
||||
zap.Duration("duration", time.Since(startTime)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseCardIDFromKey 从 Redis key "traffic:daily:{cardID}:{date}" 中解析 cardID
|
||||
func parseCardIDFromKey(key string) (uint, error) {
|
||||
parts := strings.Split(key, ":")
|
||||
if len(parts) != 4 {
|
||||
return 0, fmt.Errorf("invalid key format: %s", key)
|
||||
}
|
||||
id, err := strconv.ParseUint(parts[2], 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint(id), nil
|
||||
}
|
||||
@@ -35,7 +35,6 @@ type PollingHandler struct {
|
||||
iotCardStore *postgres.IotCardStore
|
||||
concurrencyStore *postgres.PollingConcurrencyConfigStore
|
||||
deviceSimBindingStore *postgres.DeviceSimBindingStore
|
||||
dataUsageRecordStore *postgres.DataUsageRecordStore
|
||||
packageUsageStore *postgres.PackageUsageStore
|
||||
usageService *packagepkg.UsageService
|
||||
logger *zap.Logger
|
||||
@@ -58,7 +57,6 @@ func NewPollingHandler(
|
||||
iotCardStore: postgres.NewIotCardStore(db, redis),
|
||||
concurrencyStore: postgres.NewPollingConcurrencyConfigStore(db),
|
||||
deviceSimBindingStore: postgres.NewDeviceSimBindingStore(db, redis),
|
||||
dataUsageRecordStore: postgres.NewDataUsageRecordStore(db),
|
||||
packageUsageStore: postgres.NewPackageUsageStore(db, redis),
|
||||
usageService: usageService,
|
||||
logger: logger,
|
||||
@@ -253,14 +251,10 @@ func (h *PollingHandler) HandleCarddataCheck(ctx context.Context, t *asynq.Task)
|
||||
zap.Uint64("card_id", cardID))
|
||||
}
|
||||
|
||||
// 计算流量增量(处理跨月)
|
||||
now := time.Now()
|
||||
updates := h.calculateFlowUpdates(card, gatewayFlowMB, now)
|
||||
updates, flowIncrementMB := h.calculateFlowUpdates(card, gatewayFlowMB, now)
|
||||
updates["last_data_check_at"] = now
|
||||
|
||||
// 计算本次流量增量(用于套餐扣减)
|
||||
flowIncrementMB := h.calculateFlowIncrement(card, gatewayFlowMB, now)
|
||||
|
||||
// 更新数据库
|
||||
if err := h.db.Model(&model.IotCard{}).
|
||||
Where("id = ?", cardID).
|
||||
@@ -268,8 +262,7 @@ func (h *PollingHandler) HandleCarddataCheck(ctx context.Context, t *asynq.Task)
|
||||
h.logger.Error("更新卡流量信息失败", zap.Uint64("card_id", cardID), zap.Error(err))
|
||||
}
|
||||
|
||||
// 插入流量历史记录(异步,不阻塞主流程)
|
||||
go h.insertDataUsageRecord(context.Background(), uint(cardID), gatewayFlowMB, card.CurrentMonthUsageMB, now, payload.IsManual)
|
||||
go h.insertDataUsageRecord(context.Background(), uint(cardID), flowIncrementMB, now)
|
||||
|
||||
// 更新 Redis 缓存
|
||||
h.updateCardCache(ctx, uint(cardID), map[string]any{
|
||||
@@ -308,26 +301,28 @@ func (h *PollingHandler) HandleCarddataCheck(ctx context.Context, t *asynq.Task)
|
||||
return h.requeueCard(ctx, uint(cardID), constants.TaskTypePollingCarddata)
|
||||
}
|
||||
|
||||
// calculateFlowIncrement 任务 18.2: 计算本次流量增量
|
||||
func (h *PollingHandler) calculateFlowIncrement(card *model.IotCard, gatewayFlowMB float64, now time.Time) float64 {
|
||||
// 获取本月1号
|
||||
currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||
|
||||
// 判断是否跨月
|
||||
isCrossMonth := card.CurrentMonthStartDate == nil ||
|
||||
card.CurrentMonthStartDate.Before(currentMonthStart)
|
||||
|
||||
if isCrossMonth {
|
||||
// 跨月了:本月流量就是增量
|
||||
return gatewayFlowMB
|
||||
// getCarrierResetDay 读取运营商的上游流量重置日
|
||||
func (h *PollingHandler) getCarrierResetDay(carrierID uint) int {
|
||||
var carrier model.Carrier
|
||||
if err := h.db.Select("data_reset_day").First(&carrier, carrierID).Error; err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// 同月内:计算增量
|
||||
increment := gatewayFlowMB - card.CurrentMonthUsageMB
|
||||
if increment < 0 {
|
||||
return 0
|
||||
if carrier.DataResetDay == 0 {
|
||||
return 1
|
||||
}
|
||||
return increment
|
||||
return carrier.DataResetDay
|
||||
}
|
||||
|
||||
// isResetWindow 判断今天是否在运营商重置日窗口内
|
||||
// 窗口 = 重置日当天 + 前一天(容错网关数据延迟)
|
||||
func isResetWindow(now time.Time, resetDay int) bool {
|
||||
today := now.Day()
|
||||
if today == resetDay {
|
||||
return true
|
||||
}
|
||||
resetDate := time.Date(now.Year(), now.Month(), resetDay, 0, 0, 0, 0, now.Location())
|
||||
prevDay := resetDate.AddDate(0, 0, -1).Day()
|
||||
return today == prevDay
|
||||
}
|
||||
|
||||
// shouldStopCard 任务 18.4: 检查是否应该停机(所有套餐用完)
|
||||
@@ -389,47 +384,64 @@ func (h *PollingHandler) stopCardByUsageExhausted(ctx context.Context, card *mod
|
||||
zap.String("iccid", card.ICCID))
|
||||
}
|
||||
|
||||
// calculateFlowUpdates 计算流量更新值(处理跨月逻辑)
|
||||
func (h *PollingHandler) calculateFlowUpdates(card *model.IotCard, gatewayFlowMB float64, now time.Time) map[string]any {
|
||||
// calculateFlowUpdates 计算流量更新值和增量(合并原 calculateFlowUpdates 和 calculateFlowIncrement)
|
||||
// 增量算法:increment = 当前网关读数 - 上次网关读数,检测上游重置和跨自然月
|
||||
func (h *PollingHandler) calculateFlowUpdates(card *model.IotCard, gatewayFlowMB float64, now time.Time) (map[string]any, float64) {
|
||||
updates := make(map[string]any)
|
||||
|
||||
// 获取本月1号
|
||||
currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||
// 增量 = 当前读数 - 上次读数
|
||||
increment := gatewayFlowMB - card.LastGatewayReadingMB
|
||||
|
||||
// 判断是否跨月
|
||||
if increment < 0 {
|
||||
// 当前值比上次小,检查是否为上游运营商正常重置
|
||||
resetDay := h.getCarrierResetDay(card.CarrierID)
|
||||
if isResetWindow(now, resetDay) {
|
||||
// 在重置日窗口内,视为正常上游重置,本次原始值即为增量
|
||||
increment = gatewayFlowMB
|
||||
h.logger.Info("检测到上游运营商重置",
|
||||
zap.Uint("card_id", card.ID),
|
||||
zap.Float64("gateway_flow", gatewayFlowMB),
|
||||
zap.Float64("last_reading", card.LastGatewayReadingMB),
|
||||
zap.Int("reset_day", resetDay))
|
||||
} else {
|
||||
// 非重置日出现值下降,异常,不计入
|
||||
h.logger.Warn("流量异常:非重置日出现值下降",
|
||||
zap.Uint("card_id", card.ID),
|
||||
zap.Float64("gateway_flow", gatewayFlowMB),
|
||||
zap.Float64("last_reading", card.LastGatewayReadingMB))
|
||||
increment = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 始终更新网关读数
|
||||
updates["last_gateway_reading_mb"] = gatewayFlowMB
|
||||
|
||||
// 检测跨自然月
|
||||
currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||
isCrossMonth := card.CurrentMonthStartDate == nil ||
|
||||
card.CurrentMonthStartDate.Before(currentMonthStart)
|
||||
|
||||
if isCrossMonth {
|
||||
// 跨月了:保存上月总量,重置本月
|
||||
h.logger.Info("检测到跨月,重置流量计数",
|
||||
zap.Uint("card_id", card.ID),
|
||||
zap.Float64("last_month_total", card.CurrentMonthUsageMB),
|
||||
zap.Float64("new_month_usage", gatewayFlowMB))
|
||||
|
||||
// 计算本次增量:上月最后值 + 本月当前值
|
||||
increment := card.CurrentMonthUsageMB + gatewayFlowMB
|
||||
zap.Float64("last_month_total", card.CurrentMonthUsageMB))
|
||||
|
||||
updates["last_month_total_mb"] = card.CurrentMonthUsageMB
|
||||
updates["current_month_start_date"] = currentMonthStart
|
||||
updates["current_month_usage_mb"] = gatewayFlowMB
|
||||
updates["data_usage_mb"] = card.DataUsageMB + int64(increment)
|
||||
} else {
|
||||
// 同月内:计算增量
|
||||
increment := gatewayFlowMB - card.CurrentMonthUsageMB
|
||||
if increment < 0 {
|
||||
// Gateway 返回值比记录的小,可能是数据异常,不更新
|
||||
h.logger.Warn("流量异常:Gateway返回值小于记录值",
|
||||
zap.Uint("card_id", card.ID),
|
||||
zap.Float64("gateway_flow", gatewayFlowMB),
|
||||
zap.Float64("recorded_flow", card.CurrentMonthUsageMB))
|
||||
increment = 0
|
||||
}
|
||||
|
||||
updates["current_month_usage_mb"] = gatewayFlowMB
|
||||
// 跨月重置为 0,再加上本次增量
|
||||
if increment > 0 {
|
||||
updates["data_usage_mb"] = card.DataUsageMB + int64(increment)
|
||||
updates["current_month_usage_mb"] = increment
|
||||
} else {
|
||||
updates["current_month_usage_mb"] = float64(0)
|
||||
}
|
||||
} else if increment > 0 {
|
||||
// 同月内,增量累加(使用 gorm.Expr 保证原子性)
|
||||
updates["current_month_usage_mb"] = gorm.Expr("current_month_usage_mb + ?", increment)
|
||||
}
|
||||
|
||||
// 更新卡生命周期总用量
|
||||
if increment > 0 {
|
||||
updates["data_usage_mb"] = gorm.Expr("data_usage_mb + ?", int64(increment))
|
||||
}
|
||||
|
||||
// 首次流量查询初始化
|
||||
@@ -437,7 +449,7 @@ func (h *PollingHandler) calculateFlowUpdates(card *model.IotCard, gatewayFlowMB
|
||||
updates["current_month_start_date"] = currentMonthStart
|
||||
}
|
||||
|
||||
return updates
|
||||
return updates, increment
|
||||
}
|
||||
|
||||
// HandlePackageCheck 处理套餐检查任务
|
||||
@@ -813,44 +825,24 @@ func (h *PollingHandler) updateStats(ctx context.Context, taskType string, succe
|
||||
_, _ = pipe.Exec(ctx)
|
||||
}
|
||||
|
||||
// insertDataUsageRecord 插入流量历史记录
|
||||
func (h *PollingHandler) insertDataUsageRecord(ctx context.Context, cardID uint, currentUsageMB, previousUsageMB float64, checkTime time.Time, isManual bool) {
|
||||
// 计算流量增量
|
||||
var dataIncreaseMB int64
|
||||
if currentUsageMB > previousUsageMB {
|
||||
dataIncreaseMB = int64(currentUsageMB - previousUsageMB)
|
||||
// insertDataUsageRecord 写入日流量缓冲到 Redis
|
||||
// 增量 <= 0 时直接跳过;增量 > 0 时 INCRBYFLOAT 写 Redis + 48h TTL
|
||||
func (h *PollingHandler) insertDataUsageRecord(ctx context.Context, cardID uint, incrementMB float64, checkTime time.Time) {
|
||||
if incrementMB <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 确定数据来源
|
||||
source := "polling"
|
||||
if isManual {
|
||||
source = "manual"
|
||||
}
|
||||
dateStr := checkTime.Format("2006-01-02")
|
||||
key := constants.RedisCardDailyTrafficKey(cardID, dateStr)
|
||||
|
||||
// 创建流量记录
|
||||
record := &model.DataUsageRecord{
|
||||
IotCardID: cardID,
|
||||
DataUsageMB: int64(currentUsageMB),
|
||||
DataIncreaseMB: dataIncreaseMB,
|
||||
CheckTime: checkTime,
|
||||
Source: source,
|
||||
}
|
||||
|
||||
// 插入记录
|
||||
if err := h.dataUsageRecordStore.Create(ctx, record); err != nil {
|
||||
h.logger.Warn("插入流量历史记录失败",
|
||||
if err := h.redis.IncrByFloat(ctx, key, incrementMB).Err(); err != nil {
|
||||
h.logger.Warn("写入日流量缓冲失败",
|
||||
zap.Uint("card_id", cardID),
|
||||
zap.Int64("data_usage_mb", record.DataUsageMB),
|
||||
zap.Int64("data_increase_mb", record.DataIncreaseMB),
|
||||
zap.String("source", source),
|
||||
zap.Float64("increment_mb", incrementMB),
|
||||
zap.Error(err))
|
||||
} else {
|
||||
h.logger.Debug("流量历史记录已插入",
|
||||
zap.Uint("card_id", cardID),
|
||||
zap.Int64("data_usage_mb", record.DataUsageMB),
|
||||
zap.Int64("data_increase_mb", record.DataIncreaseMB),
|
||||
zap.String("source", source))
|
||||
return
|
||||
}
|
||||
h.redis.Expire(ctx, key, 48*time.Hour)
|
||||
}
|
||||
|
||||
// updateCardCache 更新卡缓存
|
||||
@@ -910,6 +902,16 @@ func (h *PollingHandler) getCardWithCache(ctx context.Context, cardID uint) (*mo
|
||||
card.CurrentMonthUsageMB = usage
|
||||
}
|
||||
}
|
||||
if v, ok := result["last_gateway_reading_mb"]; ok {
|
||||
if reading, err := strconv.ParseFloat(v, 64); err == nil {
|
||||
card.LastGatewayReadingMB = reading
|
||||
}
|
||||
}
|
||||
if v, ok := result["data_usage_mb"]; ok {
|
||||
if usage, err := strconv.ParseInt(v, 10, 64); err == nil {
|
||||
card.DataUsageMB = usage
|
||||
}
|
||||
}
|
||||
if v, ok := result["series_id"]; ok {
|
||||
if id, err := strconv.ParseUint(v, 10, 64); err == nil {
|
||||
seriesID := uint(id)
|
||||
@@ -930,14 +932,16 @@ func (h *PollingHandler) getCardWithCache(ctx context.Context, cardID uint) (*mo
|
||||
go func() {
|
||||
cacheCtx := context.Background()
|
||||
cacheData := map[string]any{
|
||||
"id": card.ID,
|
||||
"iccid": card.ICCID,
|
||||
"card_category": card.CardCategory,
|
||||
"real_name_status": card.RealNameStatus,
|
||||
"network_status": card.NetworkStatus,
|
||||
"carrier_id": card.CarrierID,
|
||||
"current_month_usage_mb": card.CurrentMonthUsageMB,
|
||||
"cached_at": time.Now().Unix(),
|
||||
"id": card.ID,
|
||||
"iccid": card.ICCID,
|
||||
"card_category": card.CardCategory,
|
||||
"real_name_status": card.RealNameStatus,
|
||||
"network_status": card.NetworkStatus,
|
||||
"carrier_id": card.CarrierID,
|
||||
"current_month_usage_mb": card.CurrentMonthUsageMB,
|
||||
"last_gateway_reading_mb": card.LastGatewayReadingMB,
|
||||
"data_usage_mb": card.DataUsageMB,
|
||||
"cached_at": time.Now().Unix(),
|
||||
}
|
||||
if card.SeriesID != nil {
|
||||
cacheData["series_id"] = *card.SeriesID
|
||||
|
||||
Reference in New Issue
Block a user