This commit is contained in:
@@ -297,16 +297,21 @@ func (s *Service) fillPackageInfo(ctx context.Context, resp *dto.AssetResolveRes
|
||||
return
|
||||
}
|
||||
|
||||
pkg, err := s.packageStore.GetByID(ctx, usage.PackageID)
|
||||
if err != nil || pkg == nil {
|
||||
return
|
||||
resp.CurrentPackageUsageID = &usage.ID
|
||||
resp.CurrentPackage = usage.PackageName
|
||||
if resp.CurrentPackage == "" {
|
||||
pkg, pkgErr := s.packageStore.GetByID(ctx, usage.PackageID)
|
||||
if pkgErr == nil && pkg != nil {
|
||||
resp.CurrentPackage = pkg.PackageName
|
||||
}
|
||||
}
|
||||
|
||||
resp.CurrentPackage = pkg.PackageName
|
||||
ratio := safeVirtualRatio(pkg.VirtualRatio)
|
||||
resp.PackageTotalMB = int64(float64(usage.DataLimitMB) / ratio)
|
||||
resp.PackageUsedMB = float64(usage.DataUsageMB) / ratio
|
||||
resp.PackageRemainMB = float64(usage.DataLimitMB-usage.DataUsageMB) / ratio
|
||||
metrics := usage.BuildTrafficMetrics()
|
||||
resp.RealTotalMB = metrics.RealTotalMB
|
||||
resp.RealUsedMB = metrics.RealUsedMB
|
||||
resp.VirtualTotalMB = metrics.VirtualTotalMB
|
||||
resp.VirtualUsedMB = metrics.VirtualUsedMB
|
||||
resp.ReductionPct = metrics.ReductionPct
|
||||
}
|
||||
|
||||
// fillShopName 填充店铺名称
|
||||
@@ -698,15 +703,14 @@ func (s *Service) GetPackages(ctx context.Context, assetType string, id uint, pa
|
||||
all := make([]*dto.AssetPackageResponse, 0, len(usages))
|
||||
for _, u := range usages {
|
||||
pkg := pkgMap[u.PackageID]
|
||||
ratio := 1.0
|
||||
pkgName := ""
|
||||
metrics := u.BuildTrafficMetrics()
|
||||
pkgName := u.PackageName
|
||||
pkgType := ""
|
||||
enableVirtualData := false
|
||||
if pkg != nil {
|
||||
ratio = safeVirtualRatio(pkg.VirtualRatio)
|
||||
pkgName = pkg.PackageName
|
||||
if pkgName == "" {
|
||||
pkgName = pkg.PackageName
|
||||
}
|
||||
pkgType = pkg.PackageType
|
||||
enableVirtualData = pkg.EnableVirtualData
|
||||
}
|
||||
item := &dto.AssetPackageResponse{
|
||||
PackageUsageID: u.ID,
|
||||
@@ -720,13 +724,11 @@ func (s *Service) GetPackages(ctx context.Context, assetType string, id uint, pa
|
||||
UsageType: u.UsageType,
|
||||
Status: u.Status,
|
||||
StatusName: packageStatusName(u.Status),
|
||||
DataLimitMB: u.DataLimitMB,
|
||||
VirtualLimitMB: int64(float64(u.DataLimitMB) / ratio),
|
||||
DataUsageMB: u.DataUsageMB,
|
||||
VirtualUsedMB: float64(u.DataUsageMB) / ratio,
|
||||
VirtualRemainMB: float64(u.DataLimitMB-u.DataUsageMB) / ratio,
|
||||
VirtualRatio: ratio,
|
||||
EnableVirtualData: enableVirtualData,
|
||||
RealTotalMB: metrics.RealTotalMB,
|
||||
RealUsedMB: metrics.RealUsedMB,
|
||||
VirtualTotalMB: metrics.VirtualTotalMB,
|
||||
VirtualUsedMB: metrics.VirtualUsedMB,
|
||||
ReductionPct: metrics.ReductionPct,
|
||||
ActivatedAt: u.ActivatedAt,
|
||||
ExpiresAt: u.ExpiresAt,
|
||||
MasterUsageID: u.MasterUsageID,
|
||||
@@ -779,15 +781,14 @@ func (s *Service) GetCurrentPackage(ctx context.Context, assetType string, id ui
|
||||
}
|
||||
|
||||
pkg, pkgErr := s.packageStore.GetByID(ctx, usage.PackageID)
|
||||
ratio := 1.0
|
||||
pkgName := ""
|
||||
metrics := usage.BuildTrafficMetrics()
|
||||
pkgName := usage.PackageName
|
||||
pkgType := ""
|
||||
enableVirtualData := false
|
||||
if pkgErr == nil && pkg != nil {
|
||||
ratio = safeVirtualRatio(pkg.VirtualRatio)
|
||||
pkgName = pkg.PackageName
|
||||
if pkgName == "" {
|
||||
pkgName = pkg.PackageName
|
||||
}
|
||||
pkgType = pkg.PackageType
|
||||
enableVirtualData = pkg.EnableVirtualData
|
||||
}
|
||||
return &dto.AssetPackageResponse{
|
||||
PackageUsageID: usage.ID,
|
||||
@@ -801,13 +802,11 @@ func (s *Service) GetCurrentPackage(ctx context.Context, assetType string, id ui
|
||||
UsageType: usage.UsageType,
|
||||
Status: usage.Status,
|
||||
StatusName: packageStatusName(usage.Status),
|
||||
DataLimitMB: usage.DataLimitMB,
|
||||
VirtualLimitMB: int64(float64(usage.DataLimitMB) / ratio),
|
||||
DataUsageMB: usage.DataUsageMB,
|
||||
VirtualUsedMB: float64(usage.DataUsageMB) / ratio,
|
||||
VirtualRemainMB: float64(usage.DataLimitMB-usage.DataUsageMB) / ratio,
|
||||
VirtualRatio: ratio,
|
||||
EnableVirtualData: enableVirtualData,
|
||||
RealTotalMB: metrics.RealTotalMB,
|
||||
RealUsedMB: metrics.RealUsedMB,
|
||||
VirtualTotalMB: metrics.VirtualTotalMB,
|
||||
VirtualUsedMB: metrics.VirtualUsedMB,
|
||||
ReductionPct: metrics.ReductionPct,
|
||||
ActivatedAt: usage.ActivatedAt,
|
||||
ExpiresAt: usage.ExpiresAt,
|
||||
MasterUsageID: usage.MasterUsageID,
|
||||
@@ -838,14 +837,6 @@ func nonZeroTimePtr(t time.Time) *time.Time {
|
||||
return &t
|
||||
}
|
||||
|
||||
// safeVirtualRatio 安全获取虚流量比例,避免除零
|
||||
func safeVirtualRatio(ratio float64) float64 {
|
||||
if ratio <= 0 {
|
||||
return 1.0
|
||||
}
|
||||
return ratio
|
||||
}
|
||||
|
||||
// GetOrders 查询资产历史订单,支持换货链跨代追溯
|
||||
// page/pageSize 仅对本代订单生效;前代订单每代最多返回20条
|
||||
// 最多追溯10代,超出后 truncated=true
|
||||
|
||||
@@ -195,18 +195,23 @@ func (s *StopResumeService) isTrafficExhausted(ctx context.Context, card *model.
|
||||
}
|
||||
}
|
||||
|
||||
pkg, err := s.packageUsageStore.GetActiveOrDepletedByCarrier(ctx, carrierType, carrierID)
|
||||
packages, err := s.packageUsageStore.ListActiveOrDepletedByCarrier(ctx, carrierType, carrierID)
|
||||
if err != nil {
|
||||
if stderrors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
if pkg.Status == constants.PackageUsageStatusDepleted {
|
||||
return true, nil
|
||||
if len(packages) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
return pkg.DataLimitMB > 0 && pkg.DataUsageMB >= pkg.DataLimitMB, nil
|
||||
|
||||
for _, pkg := range packages {
|
||||
if pkg == nil {
|
||||
continue
|
||||
}
|
||||
if !pkg.IsTrafficDepleted() {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// isRealnameOK 检查卡是否满足实名要求
|
||||
@@ -221,16 +226,7 @@ func (s *StopResumeService) isRealnameOK(card *model.IotCard) bool {
|
||||
func (s *StopResumeService) checkStopReasons(ctx context.Context, card *model.IotCard) ([]string, error) {
|
||||
var reasons []string
|
||||
|
||||
// 条件A:无有效套餐(优先级最高)
|
||||
hasPackage, err := s.hasValidPackage(ctx, card)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasPackage {
|
||||
reasons = append(reasons, constants.StopReasonNoPackage)
|
||||
}
|
||||
|
||||
// 条件B:虚流量耗尽
|
||||
// 条件A:流量耗尽
|
||||
exhausted, err := s.isTrafficExhausted(ctx, card)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -239,6 +235,15 @@ func (s *StopResumeService) checkStopReasons(ctx context.Context, card *model.Io
|
||||
reasons = append(reasons, constants.StopReasonTrafficExhausted)
|
||||
}
|
||||
|
||||
// 条件B:无有效套餐(仅在未命中流量耗尽时判断)
|
||||
hasPackage, err := s.hasValidPackage(ctx, card)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasPackage && !exhausted {
|
||||
reasons = append(reasons, constants.StopReasonNoPackage)
|
||||
}
|
||||
|
||||
// 条件C:非行业卡且未实名
|
||||
if !s.isRealnameOK(card) {
|
||||
reasons = append(reasons, constants.StopReasonNotRealname)
|
||||
|
||||
@@ -2018,6 +2018,7 @@ func (s *Service) activateMainPackage(ctx context.Context, tx *gorm.DB, order *m
|
||||
}
|
||||
|
||||
// 创建套餐使用记录
|
||||
virtualTotalMBSnapshot, displayGainRatioSnapshot, enableVirtualDataSnapshot := model.BuildPackageUsageSnapshotValues(pkg)
|
||||
usage := &model.PackageUsage{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: order.Creator,
|
||||
@@ -2029,6 +2030,9 @@ func (s *Service) activateMainPackage(ctx context.Context, tx *gorm.DB, order *m
|
||||
PackageName: pkg.PackageName,
|
||||
UsageType: order.OrderType,
|
||||
DataLimitMB: pkg.RealDataMB,
|
||||
VirtualTotalMBSnapshot: virtualTotalMBSnapshot,
|
||||
DisplayGainRatioSnapshot: displayGainRatioSnapshot,
|
||||
EnableVirtualDataSnapshot: enableVirtualDataSnapshot,
|
||||
Status: status,
|
||||
Priority: priority,
|
||||
DataResetCycle: pkg.DataResetCycle,
|
||||
@@ -2103,6 +2107,7 @@ func (s *Service) activateAddonPackage(ctx context.Context, tx *gorm.DB, order *
|
||||
expiresAt = now
|
||||
}
|
||||
|
||||
virtualTotalMBSnapshot, displayGainRatioSnapshot, enableVirtualDataSnapshot := model.BuildPackageUsageSnapshotValues(pkg)
|
||||
usage := &model.PackageUsage{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: order.Creator,
|
||||
@@ -2114,6 +2119,9 @@ func (s *Service) activateAddonPackage(ctx context.Context, tx *gorm.DB, order *
|
||||
PackageName: pkg.PackageName,
|
||||
UsageType: order.OrderType,
|
||||
DataLimitMB: pkg.RealDataMB,
|
||||
VirtualTotalMBSnapshot: virtualTotalMBSnapshot,
|
||||
DisplayGainRatioSnapshot: displayGainRatioSnapshot,
|
||||
EnableVirtualDataSnapshot: enableVirtualDataSnapshot,
|
||||
Status: status,
|
||||
Priority: priority,
|
||||
MasterUsageID: &mainPackage.ID,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user