修正数据
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m55s

This commit is contained in:
2026-04-27 18:10:08 +08:00
parent bb33232b1b
commit fe4c545308
57 changed files with 4477 additions and 226 deletions

View File

@@ -740,8 +740,5 @@ func formatAmount(amountFen int64) string {
// calculateVirtualRatio 计算虚流量比例
// enable_virtual_data=true 且 virtual_data_mb>0 时 = real_data_mb/virtual_data_mb否则 = 1.0
func calculateVirtualRatio(enableVirtualData bool, realDataMB, virtualDataMB int64) float64 {
if enableVirtualData && virtualDataMB > 0 {
return float64(realDataMB) / float64(virtualDataMB)
}
return 1.0
return model.CalculateDisplayGainRatio(enableVirtualData, realDataMB, virtualDataMB)
}

View File

@@ -76,7 +76,11 @@ func (s *UsageService) DeductDataUsage(ctx context.Context, carrierType string,
return nil
}
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
shouldSuspend := false
suspendCarrierType := ""
var suspendCarrierID uint
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
targetCarrierType, targetCarrierID, packages, err := s.resolveActivePackages(ctx, tx, carrierType, carrierID)
if err != nil {
return err
@@ -96,7 +100,8 @@ func (s *UsageService) DeductDataUsage(ctx context.Context, carrierType string,
}
// 计算当前套餐剩余额度
remainingQuota := pkg.DataLimitMB - pkg.DataUsageMB
effectiveThresholdMB := pkg.EffectiveThresholdMB()
remainingQuota := pkg.RemainingQuotaMB()
if remainingQuota <= 0 {
// 套餐已用完,标记为已用完
if err := tx.Model(pkg).Update("status", constants.PackageUsageStatusDepleted).Error; err != nil {
@@ -120,7 +125,7 @@ func (s *UsageService) DeductDataUsage(ctx context.Context, carrierType string,
}
// 检查是否用完
if newUsage >= pkg.DataLimitMB {
if effectiveThresholdMB > 0 && newUsage >= effectiveThresholdMB {
updates["status"] = constants.PackageUsageStatusDepleted
}
@@ -139,6 +144,7 @@ func (s *UsageService) DeductDataUsage(ctx context.Context, carrierType string,
zap.Uint("usage_id", pkg.ID),
zap.Int64("deduct_mb", deductFromPkg),
zap.Int64("new_usage_mb", newUsage),
zap.Int64("effective_threshold_mb", effectiveThresholdMB),
zap.Int64("data_limit_mb", pkg.DataLimitMB))
}
@@ -153,12 +159,25 @@ func (s *UsageService) DeductDataUsage(ctx context.Context, carrierType string,
}
// 任务 10.5: 检查是否所有套餐都用完(触发停机)
if err := s.checkAndTriggerSuspension(ctx, tx, targetCarrierType, targetCarrierID); err != nil {
shouldSuspendCurrent, err := s.checkAndTriggerSuspension(ctx, tx, targetCarrierType, targetCarrierID)
if err != nil {
return err
}
shouldSuspend = shouldSuspendCurrent
suspendCarrierType = targetCarrierType
suspendCarrierID = targetCarrierID
return nil
})
if err != nil {
return err
}
if shouldSuspend {
s.triggerSuspensionAfterCommit(suspendCarrierType, suspendCarrierID)
}
return nil
}
// convertToDeductMB 将浮点增量转换为可扣减的整数 MB
@@ -328,70 +347,98 @@ func (s *UsageService) updateDailyRecord(ctx context.Context, tx *gorm.DB, packa
}
// checkAndTriggerSuspension 任务 10.5: 检查停机条件
func (s *UsageService) checkAndTriggerSuspension(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint) error {
// 查询是否还有生效中的套餐
var activeCount int64
func (s *UsageService) checkAndTriggerSuspension(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint) (bool, error) {
query := tx.Model(&model.PackageUsage{}).
Where("status = ?", constants.PackageUsageStatusActive)
Where("status IN ?", []int{constants.PackageUsageStatusActive, constants.PackageUsageStatusDepleted})
if carrierType == constants.AssetTypeIotCard {
query = query.Where("iot_card_id = ?", carrierID)
} else if carrierType == constants.AssetTypeDevice {
query = query.Where("device_id = ?", carrierID)
} else {
return errors.New(errors.CodeInvalidParam, "无效的载体类型")
return false, errors.New(errors.CodeInvalidParam, "无效的载体类型")
}
if err := query.Count(&activeCount).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询生效套餐数量失败")
var packages []*model.PackageUsage
if err := query.Find(&packages).Error; err != nil {
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐状态失败")
}
// 如果没有生效中的套餐,触发停机操作
if activeCount == 0 {
hasAvailablePackage := false
for _, pkg := range packages {
if pkg == nil {
continue
}
if pkg.IsTrafficDepleted() {
if pkg.Status != constants.PackageUsageStatusDepleted {
if err := tx.Model(pkg).Update("status", constants.PackageUsageStatusDepleted).Error; err != nil {
return false, errors.Wrap(errors.CodeDatabaseError, err, "更新套餐耗尽状态失败")
}
}
continue
}
hasAvailablePackage = true
break
}
if !hasAvailablePackage {
s.logger.Warn("所有套餐已用完,触发停机",
zap.String("carrier_type", carrierType),
zap.Uint("carrier_id", carrierID))
if carrierType == constants.AssetTypeIotCard {
if s.stopResumeCallback != nil {
go func() {
stopCtx := context.Background()
if err := s.stopResumeCallback.CheckAndStopCard(stopCtx, carrierID); err != nil {
s.logger.Error("调用停机服务失败",
zap.Uint("card_id", carrierID),
zap.Error(err))
}
}()
}
} else if carrierType == constants.AssetTypeDevice {
if s.stopResumeCallback == nil {
s.logger.Warn("停复机回调未注入,跳过设备绑定卡停机",
zap.Uint("device_id", carrierID))
} else {
bindings, err := s.deviceSimBindingStore.ListByDeviceID(context.Background(), carrierID)
if err != nil {
s.logger.Error("查询设备绑定卡失败",
zap.Uint("device_id", carrierID),
zap.Error(err))
} else if len(bindings) == 0 {
s.logger.Debug("设备无绑定卡,跳过停机",
zap.Uint("device_id", carrierID))
} else {
for _, b := range bindings {
cardID := b.IotCardID
go func(cID uint) {
stopCtx := context.Background()
if err := s.stopResumeCallback.CheckAndStopCard(stopCtx, cID); err != nil {
s.logger.Error("调用停机服务失败",
zap.Uint("card_id", cID),
zap.Error(err))
}
}(cardID)
}
}
}
}
return true, nil
}
return nil
return false, nil
}
// triggerSuspensionAfterCommit 在事务提交后触发停机检查,避免回调读到未提交状态。
func (s *UsageService) triggerSuspensionAfterCommit(carrierType string, carrierID uint) {
if s.stopResumeCallback == nil {
if carrierType == constants.AssetTypeDevice {
s.logger.Warn("停复机回调未注入,跳过设备绑定卡停机",
zap.Uint("device_id", carrierID))
}
return
}
if carrierType == constants.AssetTypeIotCard {
go func() {
stopCtx := context.Background()
if err := s.stopResumeCallback.CheckAndStopCard(stopCtx, carrierID); err != nil {
s.logger.Error("调用停机服务失败",
zap.Uint("card_id", carrierID),
zap.Error(err))
}
}()
return
}
if carrierType != constants.AssetTypeDevice {
return
}
bindings, err := s.deviceSimBindingStore.ListByDeviceID(context.Background(), carrierID)
if err != nil {
s.logger.Error("查询设备绑定卡失败",
zap.Uint("device_id", carrierID),
zap.Error(err))
return
}
if len(bindings) == 0 {
s.logger.Debug("设备无绑定卡,跳过停机",
zap.Uint("device_id", carrierID))
return
}
for _, b := range bindings {
cardID := b.IotCardID
go func(cID uint) {
stopCtx := context.Background()
if err := s.stopResumeCallback.CheckAndStopCard(stopCtx, cID); err != nil {
s.logger.Error("调用停机服务失败",
zap.Uint("card_id", cID),
zap.Error(err))
}
}(cardID)
}
}