Compare commits

2 Commits

Author SHA1 Message Date
fc6af43baa 修复:溢出流量应优先记到主套餐而非加油包
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m47s
主套餐和加油包同时 Depleted 时,recordOverflowToDepletedPackage
原先取 id 最大的套餐,可能取到加油包,语义不对。

改为优先查 master_usage_id IS NULL(主套餐),找不到时
再回退取任意 Depleted 套餐。
2026-07-01 12:06:19 +09:00
52bdbbae25 修复:套餐耗尽后流量详单断档问题
问题:套餐 status=Depleted 后,queryActivePackages 查不到套餐,
DeductDataUsage 直接 return CodeNoAvailablePackage,导致上游
仍有真实流量时,tb_package_usage_daily_record 不再写入,详单断档。

修复:无 Active 套餐时,找最近一条 Depleted 套餐,将溢出流量
累加到 data_usage_mb 并写入当日 daily_record,保持详单连续性。
status 不变,不重复触发停机。
2026-07-01 12:03:39 +09:00

View File

@@ -88,13 +88,22 @@ func (s *UsageService) DeductDataUsage(ctx context.Context, carrierType string,
return err
}
today := time.Now().Format("2006-01-02")
if len(packages) == 0 {
// 无生效套餐:尝试找 Depleted 套餐记录溢出流量详单,避免详单断档
if recordErr := s.recordOverflowToDepletedPackage(ctx, tx, targetCarrierType, targetCarrierID, deductUsageMB, today); recordErr != nil {
s.logger.Warn("记录溢出流量到已耗尽套餐失败",
zap.String("carrier_type", targetCarrierType),
zap.Uint("carrier_id", targetCarrierID),
zap.Int64("usage_mb", deductUsageMB),
zap.Error(recordErr))
}
return errors.New(errors.CodeNoAvailablePackage, "没有可用套餐")
}
// 按优先级扣减流量
remainingUsage := deductUsageMB
today := time.Now().Format("2006-01-02")
for index, pkg := range packages {
if remainingUsage <= 0 {
@@ -446,3 +455,45 @@ func (s *UsageService) triggerSuspensionAfterCommit(carrierType string, carrierI
}(cardID)
}
}
// recordOverflowToDepletedPackage 在所有套餐已耗尽时,将溢出流量写入最近的 Depleted 套餐日记录。
// 目的:保持流量详单连续性,即便套餐用完期间上游仍有流量使用,详单不会断档。
// 只写日记录和累加 data_usage_mb不改变套餐 status不触发停机。
func (s *UsageService) recordOverflowToDepletedPackage(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint, overflowMB int64, today string) error {
query := tx.Where("status = ?", constants.PackageUsageStatusDepleted)
switch carrierType {
case constants.AssetTypeIotCard:
query = query.Where("iot_card_id = ?", carrierID)
case constants.AssetTypeDevice:
query = query.Where("device_id = ?", carrierID)
default:
return nil
}
// 优先选主套餐master_usage_id IS NULL主套餐不存在时再退而求其次取加油包
var pkg model.PackageUsage
err := query.Where("master_usage_id IS NULL").Order("id DESC").First(&pkg).Error
if err == gorm.ErrRecordNotFound {
err = query.Order("id DESC").First(&pkg).Error
}
if err != nil {
// 找不到任何 Depleted 套餐,静默忽略
return nil
}
newUsage := pkg.DataUsageMB + overflowMB
if err := tx.Model(&pkg).Update("data_usage_mb", newUsage).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新溢出流量失败")
}
if err := s.updateDailyRecord(ctx, tx, pkg.ID, today, overflowMB, newUsage); err != nil {
return err
}
s.logger.Info("记录溢出流量到已耗尽套餐",
zap.Uint("package_usage_id", pkg.ID),
zap.Int64("overflow_mb", overflowMB),
zap.Int64("new_usage_mb", newUsage))
return nil
}