修复设备套餐流量不同步的问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m32s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m32s
This commit is contained in:
@@ -79,16 +79,17 @@ type BoundCardInfo struct {
|
||||
|
||||
// AssetRealtimeStatusResponse 资产实时状态(只读DB/Redis)
|
||||
type AssetRealtimeStatusResponse struct {
|
||||
AssetType string `json:"asset_type" description:"资产类型:card 或 device"`
|
||||
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已实名"`
|
||||
RealnamePolicy string `json:"realname_policy,omitempty" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
|
||||
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时有效)"`
|
||||
DeviceRealtime *DeviceGatewayInfo `json:"device_realtime" description:"设备实时状态(来自 Gateway sync-info,始终返回,失败时仅含 gateway_msg)"`
|
||||
AssetType string `json:"asset_type" description:"资产类型:card 或 device"`
|
||||
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已实名"`
|
||||
RealnamePolicy string `json:"realname_policy,omitempty" description:"实名认证策略 (none:无需实名, before_order:先实名后充值/购买, after_order:先充值/购买后实名)"`
|
||||
CurrentMonthUsageMB float64 `json:"current_month_usage_mb,omitempty" description:"系统自然月累计流量(MB,asset_type=card时有效,不等于运营商周期口径)"`
|
||||
LastGatewayReadingMB float64 `json:"last_gateway_reading_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时有效)"`
|
||||
DeviceRealtime *DeviceGatewayInfo `json:"device_realtime" description:"设备实时状态(来自 Gateway sync-info,始终返回,失败时仅含 gateway_msg)"`
|
||||
}
|
||||
|
||||
// AssetPackageResponse 资产套餐信息
|
||||
|
||||
@@ -45,7 +45,8 @@ type StandaloneIotCardResponse struct {
|
||||
NetworkStatus int `json:"network_status" description:"网络状态 (0:停机, 1:开机)"`
|
||||
NetworkStatusName string `json:"network_status_name" description:"网络状态名称(中文)"`
|
||||
DataUsageMB int64 `json:"data_usage_mb" description:"累计流量使用(MB)"`
|
||||
CurrentMonthUsageMB float64 `json:"current_month_usage_mb" description:"本月已用流量(MB)"`
|
||||
CurrentMonthUsageMB float64 `json:"current_month_usage_mb" description:"系统自然月累计流量(MB,不等于运营商周期口径)"`
|
||||
LastGatewayReadingMB float64 `json:"last_gateway_reading_mb" description:"运营商当前周期累计流量读数(MB,展示运营商口径时使用)"`
|
||||
CurrentMonthStartDate *time.Time `json:"current_month_start_date" description:"本月开始日期"`
|
||||
LastMonthTotalMB float64 `json:"last_month_total_mb" description:"上月流量总量(MB)"`
|
||||
LastDataCheckAt *time.Time `json:"last_data_check_at" description:"最后流量检查时间"`
|
||||
|
||||
@@ -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)" 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"`
|
||||
@@ -53,7 +53,7 @@ type IotCard struct {
|
||||
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"`
|
||||
DeviceVirtualNo string `gorm:"column:device_virtual_no;type:varchar(100);not null;default:'';comment:绑定设备的虚拟号快照(未绑定时为空字符串)" json:"device_virtual_no"`
|
||||
LastGatewayReadingMB float64 `gorm:"column:last_gateway_reading_mb;type:float;not null;default:0;comment:上次轮询时网关返回的流量读数(MB)" json:"last_gateway_reading_mb"`
|
||||
LastGatewayReadingMB float64 `gorm:"column:last_gateway_reading_mb;type:float;not null;default:0;comment:运营商当前周期累计流量读数(MB,来自网关)" json:"last_gateway_reading_mb"`
|
||||
RealnamePolicy string `gorm:"column:realname_policy;type:varchar(20);default:'after_order';not null;comment:实名认证策略(none=无需实名,before_order=先实名后充值/购买,after_order=先充值/购买后实名)" json:"realname_policy"`
|
||||
|
||||
// ICCID 双列存储,用于支持 19/20 位 ICCID 精确路由查询
|
||||
|
||||
@@ -347,6 +347,7 @@ func (s *Service) GetRealtimeStatus(ctx context.Context, assetType string, id ui
|
||||
resp.NetworkStatus = card.NetworkStatus
|
||||
resp.RealNameStatus = card.RealNameStatus
|
||||
resp.CurrentMonthUsageMB = card.CurrentMonthUsageMB
|
||||
resp.LastGatewayReadingMB = card.LastGatewayReadingMB
|
||||
resp.LastSyncTime = card.LastSyncTime
|
||||
|
||||
case "device":
|
||||
|
||||
@@ -35,7 +35,7 @@ type PollingCallback interface {
|
||||
// 用于在手动刷新流量后触发套餐扣减,避免循环依赖
|
||||
type DataDeductor interface {
|
||||
// DeductDataUsage 按优先级扣减套餐流量
|
||||
DeductDataUsage(ctx context.Context, carrierType string, carrierID uint, usageMB int64) error
|
||||
DeductDataUsage(ctx context.Context, carrierType string, carrierID uint, usageMB float64) error
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
@@ -279,6 +279,7 @@ func (s *Service) toStandaloneResponse(card *model.IotCard, shopMap map[uint]str
|
||||
NetworkStatusName: constants.GetNetworkStatusName(card.NetworkStatus),
|
||||
DataUsageMB: card.DataUsageMB,
|
||||
CurrentMonthUsageMB: card.CurrentMonthUsageMB,
|
||||
LastGatewayReadingMB: card.LastGatewayReadingMB,
|
||||
CurrentMonthStartDate: card.CurrentMonthStartDate,
|
||||
LastMonthTotalMB: card.LastMonthTotalMB,
|
||||
LastDataCheckAt: card.LastDataCheckAt,
|
||||
@@ -884,7 +885,7 @@ func (s *Service) RefreshCardDataFromGateway(ctx context.Context, iccid string)
|
||||
|
||||
// 有增量时触发套餐流量扣减
|
||||
if flowIncrementMB > 0 && s.dataDeductor != nil {
|
||||
if err := s.dataDeductor.DeductDataUsage(ctx, "iot_card", card.ID, int64(flowIncrementMB)); err != nil {
|
||||
if err := s.dataDeductor.DeductDataUsage(ctx, constants.AssetTypeIotCard, card.ID, flowIncrementMB); err != nil {
|
||||
s.logger.Warn("手动刷新:套餐流量扣减失败",
|
||||
zap.Uint("card_id", card.ID),
|
||||
zap.Float64("increment_mb", flowIncrementMB),
|
||||
|
||||
@@ -2,6 +2,8 @@ package packagepkg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
@@ -13,6 +15,13 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
// usageRemainderTTL 流量小数余量缓存时长
|
||||
usageRemainderTTL = 7 * 24 * time.Hour
|
||||
// usageRemainderEpsilon 浮点运算容差
|
||||
usageRemainderEpsilon = 1e-9
|
||||
)
|
||||
|
||||
// StopResumeCallback 任务 24.6: 停复机回调接口
|
||||
// 用于在流量用完时触发停机操作
|
||||
type StopResumeCallback interface {
|
||||
@@ -57,28 +66,20 @@ func (s *UsageService) SetStopResumeCallback(callback StopResumeCallback) {
|
||||
// DeductDataUsage 任务 10.2-10.6: 按优先级扣减流量
|
||||
// 扣减顺序:加油包(按 priority ASC) → 主套餐
|
||||
// 流量用完时自动标记 status=2,所有套餐用完时触发停机
|
||||
func (s *UsageService) DeductDataUsage(ctx context.Context, carrierType string, carrierID uint, usageMB int64) error {
|
||||
func (s *UsageService) DeductDataUsage(ctx context.Context, carrierType string, carrierID uint, usageMB float64) error {
|
||||
if usageMB <= 0 {
|
||||
return errors.New(errors.CodeInvalidParam, "扣减流量必须大于0")
|
||||
}
|
||||
|
||||
deductUsageMB := s.convertToDeductMB(ctx, carrierType, carrierID, usageMB)
|
||||
if deductUsageMB <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// 查询所有生效中的套餐(按优先级排序)
|
||||
var packages []*model.PackageUsage
|
||||
query := tx.Where("status = ?", constants.PackageUsageStatusActive)
|
||||
|
||||
if carrierType == "iot_card" {
|
||||
query = query.Where("iot_card_id = ?", carrierID)
|
||||
} else if carrierType == "device" {
|
||||
query = query.Where("device_id = ?", carrierID)
|
||||
} else {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的载体类型")
|
||||
}
|
||||
|
||||
// 加油包按 priority ASC 排序,主套餐在后
|
||||
if err := query.Order("CASE WHEN master_usage_id IS NOT NULL THEN 0 ELSE 1 END, priority ASC").
|
||||
Find(&packages).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询生效套餐失败")
|
||||
targetCarrierType, targetCarrierID, packages, err := s.resolveActivePackages(ctx, tx, carrierType, carrierID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(packages) == 0 {
|
||||
@@ -86,7 +87,7 @@ func (s *UsageService) DeductDataUsage(ctx context.Context, carrierType string,
|
||||
}
|
||||
|
||||
// 按优先级扣减流量
|
||||
remainingUsage := usageMB
|
||||
remainingUsage := deductUsageMB
|
||||
today := time.Now().Format("2006-01-02")
|
||||
|
||||
for _, pkg := range packages {
|
||||
@@ -144,15 +145,15 @@ func (s *UsageService) DeductDataUsage(ctx context.Context, carrierType string,
|
||||
// 如果流量扣减未完成,说明所有套餐都不够
|
||||
if remainingUsage > 0 {
|
||||
s.logger.Warn("流量不足",
|
||||
zap.String("carrier_type", carrierType),
|
||||
zap.Uint("carrier_id", carrierID),
|
||||
zap.Int64("requested_mb", usageMB),
|
||||
zap.String("carrier_type", targetCarrierType),
|
||||
zap.Uint("carrier_id", targetCarrierID),
|
||||
zap.Int64("requested_mb", deductUsageMB),
|
||||
zap.Int64("remaining_mb", remainingUsage))
|
||||
return errors.New(errors.CodeInsufficientQuota, "流量不足")
|
||||
}
|
||||
|
||||
// 任务 10.5: 检查是否所有套餐都用完(触发停机)
|
||||
if err := s.checkAndTriggerSuspension(ctx, tx, carrierType, carrierID); err != nil {
|
||||
if err := s.checkAndTriggerSuspension(ctx, tx, targetCarrierType, targetCarrierID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -160,6 +161,132 @@ func (s *UsageService) DeductDataUsage(ctx context.Context, carrierType string,
|
||||
})
|
||||
}
|
||||
|
||||
// convertToDeductMB 将浮点增量转换为可扣减的整数 MB
|
||||
// 规则:先合并 Redis 中的小数余量,再按整数 MB 扣减;余数回写 Redis
|
||||
func (s *UsageService) convertToDeductMB(ctx context.Context, carrierType string, carrierID uint, usageMB float64) int64 {
|
||||
deductUsageMB := int64(math.Floor(usageMB + usageRemainderEpsilon))
|
||||
remainder := usageMB - float64(deductUsageMB)
|
||||
|
||||
if s.redis == nil {
|
||||
return deductUsageMB
|
||||
}
|
||||
|
||||
remainderKey := constants.RedisPackageUsageRemainderKey(carrierType, carrierID)
|
||||
cachedRemainder, err := s.getUsageRemainder(ctx, remainderKey)
|
||||
if err != nil {
|
||||
s.logger.Warn("读取流量小数余量失败,降级为本次直接取整扣减",
|
||||
zap.String("carrier_type", carrierType),
|
||||
zap.Uint("carrier_id", carrierID),
|
||||
zap.Float64("usage_mb", usageMB),
|
||||
zap.Error(err))
|
||||
return deductUsageMB
|
||||
}
|
||||
|
||||
totalUsage := usageMB + cachedRemainder
|
||||
deductUsageMB = int64(math.Floor(totalUsage + usageRemainderEpsilon))
|
||||
remainder = totalUsage - float64(deductUsageMB)
|
||||
if remainder < usageRemainderEpsilon {
|
||||
remainder = 0
|
||||
}
|
||||
|
||||
if err := s.persistUsageRemainder(ctx, remainderKey, remainder); err != nil {
|
||||
s.logger.Warn("写入流量小数余量失败,本次仅按整数扣减",
|
||||
zap.String("carrier_type", carrierType),
|
||||
zap.Uint("carrier_id", carrierID),
|
||||
zap.Float64("remainder_mb", remainder),
|
||||
zap.Error(err))
|
||||
}
|
||||
|
||||
return deductUsageMB
|
||||
}
|
||||
|
||||
// getUsageRemainder 读取载体流量小数余量
|
||||
func (s *UsageService) getUsageRemainder(ctx context.Context, key string) (float64, error) {
|
||||
value, err := s.redis.Get(ctx, key).Result()
|
||||
if err == redis.Nil {
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
remainder, parseErr := strconv.ParseFloat(value, 64)
|
||||
if parseErr != nil {
|
||||
// 脏值直接删除,避免持续影响后续扣减
|
||||
_ = s.redis.Del(ctx, key).Err()
|
||||
return 0, parseErr
|
||||
}
|
||||
return remainder, nil
|
||||
}
|
||||
|
||||
// persistUsageRemainder 持久化载体流量小数余量
|
||||
func (s *UsageService) persistUsageRemainder(ctx context.Context, key string, remainder float64) error {
|
||||
if remainder <= usageRemainderEpsilon {
|
||||
return s.redis.Del(ctx, key).Err()
|
||||
}
|
||||
value := strconv.FormatFloat(remainder, 'f', 6, 64)
|
||||
return s.redis.Set(ctx, key, value, usageRemainderTTL).Err()
|
||||
}
|
||||
|
||||
// resolveActivePackages 统一解析可扣减载体
|
||||
// 优先按请求载体查找;当 iot_card 无生效套餐且存在设备绑定时,回退到 device 载体
|
||||
func (s *UsageService) resolveActivePackages(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint) (string, uint, []*model.PackageUsage, error) {
|
||||
packages, err := s.queryActivePackages(tx, carrierType, carrierID)
|
||||
if err != nil {
|
||||
return "", 0, nil, err
|
||||
}
|
||||
if len(packages) > 0 {
|
||||
return carrierType, carrierID, packages, nil
|
||||
}
|
||||
|
||||
if carrierType != constants.AssetTypeIotCard {
|
||||
return carrierType, carrierID, packages, nil
|
||||
}
|
||||
|
||||
binding, err := s.deviceSimBindingStore.GetActiveBindingByCardID(ctx, carrierID)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return carrierType, carrierID, packages, nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", 0, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询卡绑定设备失败")
|
||||
}
|
||||
|
||||
devicePackages, queryErr := s.queryActivePackages(tx, constants.AssetTypeDevice, binding.DeviceID)
|
||||
if queryErr != nil {
|
||||
return "", 0, nil, queryErr
|
||||
}
|
||||
if len(devicePackages) == 0 {
|
||||
return carrierType, carrierID, packages, nil
|
||||
}
|
||||
|
||||
s.logger.Info("卡载体无生效套餐,回退设备载体扣减流量",
|
||||
zap.Uint("card_id", carrierID),
|
||||
zap.Uint("device_id", binding.DeviceID))
|
||||
|
||||
return constants.AssetTypeDevice, binding.DeviceID, devicePackages, nil
|
||||
}
|
||||
|
||||
// queryActivePackages 查询指定载体下的生效套餐(按扣减优先级排序)
|
||||
func (s *UsageService) queryActivePackages(tx *gorm.DB, carrierType string, carrierID uint) ([]*model.PackageUsage, error) {
|
||||
query := tx.Where("status = ?", constants.PackageUsageStatusActive)
|
||||
|
||||
switch carrierType {
|
||||
case constants.AssetTypeIotCard:
|
||||
query = query.Where("iot_card_id = ?", carrierID)
|
||||
case constants.AssetTypeDevice:
|
||||
query = query.Where("device_id = ?", carrierID)
|
||||
default:
|
||||
return nil, errors.New(errors.CodeInvalidParam, "无效的载体类型")
|
||||
}
|
||||
|
||||
var packages []*model.PackageUsage
|
||||
if err := query.
|
||||
Order("CASE WHEN master_usage_id IS NOT NULL THEN 0 ELSE 1 END, priority ASC").
|
||||
Find(&packages).Error; err != nil {
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询生效套餐失败")
|
||||
}
|
||||
return packages, nil
|
||||
}
|
||||
|
||||
// updateDailyRecord 任务 10.6: 更新日流量记录
|
||||
func (s *UsageService) updateDailyRecord(ctx context.Context, tx *gorm.DB, packageUsageID uint, dateStr string, dailyUsageMB, cumulativeUsageMB int64) error {
|
||||
// 解析日期字符串
|
||||
@@ -207,10 +334,12 @@ func (s *UsageService) checkAndTriggerSuspension(ctx context.Context, tx *gorm.D
|
||||
query := tx.Model(&model.PackageUsage{}).
|
||||
Where("status = ?", constants.PackageUsageStatusActive)
|
||||
|
||||
if carrierType == "iot_card" {
|
||||
if carrierType == constants.AssetTypeIotCard {
|
||||
query = query.Where("iot_card_id = ?", carrierID)
|
||||
} else if carrierType == "device" {
|
||||
} else if carrierType == constants.AssetTypeDevice {
|
||||
query = query.Where("device_id = ?", carrierID)
|
||||
} else {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的载体类型")
|
||||
}
|
||||
|
||||
if err := query.Count(&activeCount).Error; err != nil {
|
||||
@@ -223,7 +352,7 @@ func (s *UsageService) checkAndTriggerSuspension(ctx context.Context, tx *gorm.D
|
||||
zap.String("carrier_type", carrierType),
|
||||
zap.Uint("carrier_id", carrierID))
|
||||
|
||||
if carrierType == "iot_card" {
|
||||
if carrierType == constants.AssetTypeIotCard {
|
||||
if s.stopResumeCallback != nil {
|
||||
go func() {
|
||||
stopCtx := context.Background()
|
||||
@@ -234,7 +363,7 @@ func (s *UsageService) checkAndTriggerSuspension(ctx context.Context, tx *gorm.D
|
||||
}
|
||||
}()
|
||||
}
|
||||
} else if carrierType == "device" {
|
||||
} else if carrierType == constants.AssetTypeDevice {
|
||||
if s.stopResumeCallback == nil {
|
||||
s.logger.Warn("停复机回调未注入,跳过设备绑定卡停机",
|
||||
zap.Uint("device_id", carrierID))
|
||||
|
||||
@@ -120,7 +120,7 @@ func (h *PollingCarddataHandler) Handle(ctx context.Context, t *asynq.Task) erro
|
||||
h.base.updateCardCache(ctx, cardID, cacheUpdates)
|
||||
|
||||
if flowIncrementMB > 0 && h.usageService != nil {
|
||||
if deductErr := h.usageService.DeductDataUsage(ctx, "iot_card", cardID, int64(flowIncrementMB)); deductErr != nil {
|
||||
if deductErr := h.usageService.DeductDataUsage(ctx, constants.AssetTypeIotCard, cardID, flowIncrementMB); deductErr != nil {
|
||||
h.base.logger.Warn("套餐流量扣减失败",
|
||||
zap.Uint("card_id", cardID), zap.Float64("increment_mb", flowIncrementMB), zap.Error(deductErr))
|
||||
}
|
||||
|
||||
@@ -408,6 +408,13 @@ func RedisCardDailyTrafficKey(cardID uint, date string) string {
|
||||
return fmt.Sprintf("traffic:daily:%d:%s", cardID, date)
|
||||
}
|
||||
|
||||
// RedisPackageUsageRemainderKey 生成套餐扣减流量小数余量缓存键
|
||||
// 用途:缓存载体维度的 <1MB 小数余量,累计后再触发整数 MB 扣减
|
||||
// 过期时间:7 天
|
||||
func RedisPackageUsageRemainderKey(carrierType string, carrierID uint) string {
|
||||
return fmt.Sprintf("package:usage:remainder:%s:%d", carrierType, carrierID)
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 轮询分片队列相关 Redis Key
|
||||
// ========================================
|
||||
|
||||
Reference in New Issue
Block a user