diff --git a/internal/service/package/usage_service.go b/internal/service/package/usage_service.go index a00d1db..323d7a2 100644 --- a/internal/service/package/usage_service.go +++ b/internal/service/package/usage_service.go @@ -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,40 @@ 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 + } + + var pkg model.PackageUsage + if err := query.Order("id DESC").First(&pkg).Error; 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 +}