修复退款bug
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m43s

This commit is contained in:
2026-04-23 11:30:46 +08:00
parent 1f1f31a7cb
commit 8d2e8faa84
7 changed files with 128 additions and 534 deletions

View File

@@ -491,6 +491,118 @@ func (s *ActivationService) syncCarrierStatusActivated(ctx context.Context, tx *
}
}
// InvalidatePackagesForRefund 退款精准失效套餐
// 规则:
// 1. 优先按 packageUsageID 精准失效单条套餐(且必须属于当前退款订单)
// 2. 未传 packageUsageID 时,按 orderID 失效该订单生成的套餐
// 3. 若命中主套餐,级联失效其加油包,避免残留挂靠关系
func (s *ActivationService) InvalidatePackagesForRefund(ctx context.Context, assetType string, assetID uint, orderID uint, packageUsageID *uint) error {
if orderID == 0 {
return errors.New(errors.CodeInvalidParam, "无效的订单ID")
}
validStatuses := []int{
constants.PackageUsageStatusPending,
constants.PackageUsageStatusActive,
constants.PackageUsageStatusDepleted,
}
baseQuery := s.db.WithContext(ctx).Model(&model.PackageUsage{})
switch assetType {
case "iot_card":
baseQuery = baseQuery.Where("iot_card_id = ?", assetID)
case "device":
baseQuery = baseQuery.Where("device_id = ?", assetID)
default:
return errors.New(errors.CodeInvalidParam, "无效的资产类型")
}
var targets []model.PackageUsage
if packageUsageID != nil && *packageUsageID > 0 {
var usage model.PackageUsage
err := baseQuery.
Where("id = ? AND order_id = ?", *packageUsageID, orderID).
Where("status IN ?", validStatuses).
First(&usage).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
s.logger.Info("退款精准失效:未命中可失效套餐",
zap.String("asset_type", assetType),
zap.Uint("asset_id", assetID),
zap.Uint("order_id", orderID),
zap.Uint("package_usage_id", *packageUsageID),
)
return nil
}
return errors.Wrap(errors.CodeDatabaseError, err, "查询退款关联套餐失败")
}
targets = append(targets, usage)
} else {
if err := baseQuery.
Where("order_id = ?", orderID).
Where("status IN ?", validStatuses).
Find(&targets).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询退款订单套餐失败")
}
if len(targets) == 0 {
s.logger.Info("退款精准失效:订单无可失效套餐",
zap.String("asset_type", assetType),
zap.Uint("asset_id", assetID),
zap.Uint("order_id", orderID),
)
return nil
}
}
targetIDSet := make(map[uint]struct{}, len(targets))
mainUsageIDs := make([]uint, 0, len(targets))
for _, usage := range targets {
targetIDSet[usage.ID] = struct{}{}
if usage.MasterUsageID == nil {
mainUsageIDs = append(mainUsageIDs, usage.ID)
}
}
if len(mainUsageIDs) > 0 {
var addons []model.PackageUsage
if err := baseQuery.
Where("master_usage_id IN ?", mainUsageIDs).
Where("status IN ?", validStatuses).
Find(&addons).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询主套餐关联加油包失败")
}
for _, addon := range addons {
targetIDSet[addon.ID] = struct{}{}
}
}
targetIDs := make([]uint, 0, len(targetIDSet))
for id := range targetIDSet {
targetIDs = append(targetIDs, id)
}
if len(targetIDs) == 0 {
return nil
}
result := s.db.WithContext(ctx).
Model(&model.PackageUsage{}).
Where("id IN ?", targetIDs).
Update("status", constants.PackageUsageStatusInvalidated)
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "退款失效套餐失败")
}
s.logger.Info("退款精准失效套餐完成",
zap.String("asset_type", assetType),
zap.Uint("asset_id", assetID),
zap.Uint("order_id", orderID),
zap.Int("target_count", len(targetIDs)),
zap.Int64("affected", result.RowsAffected),
)
return nil
}
// InvalidateAllPackagesByAsset 批量失效资产关联的所有有效套餐
// 退款时调用:将该资产下状态为待生效(0)、生效中(1)、已用完(2)的套餐全部标记为已失效(4)
func (s *ActivationService) InvalidateAllPackagesByAsset(ctx context.Context, assetType string, assetID uint) error {

View File

@@ -423,7 +423,7 @@ func (s *Service) deductSingleCommission(ctx context.Context, refund *model.Refu
}
// handleAssetReset 异步处理资产重置
// 包括:套餐失效、停机、世代重置generation+1、钱包重建、订单状态更新
// 包括:退款套餐精准失效、停机、世代重置generation+1、钱包重建、订单状态更新
// 失败仅记录日志,不影响审批结果
func (s *Service) handleAssetReset(ctx context.Context, refundID uint) {
logger := s.logger
@@ -465,10 +465,19 @@ func (s *Service) handleAssetReset(ctx context.Context, refundID uint) {
return
}
// 1. 失效所有套餐
// 1. 按退款单精准失效套餐(仅处理本次退款订单关联套餐
if s.packageActivationService != nil {
if err := s.packageActivationService.InvalidateAllPackagesByAsset(ctx, assetType, assetID); err != nil {
logger.Error("资产重置:失效套餐失败", zap.String("asset_type", assetType), zap.Uint("asset_id", assetID), zap.Error(err))
if err := s.packageActivationService.InvalidatePackagesForRefund(ctx, assetType, assetID, order.ID, refund.PackageUsageID); err != nil {
fields := []zap.Field{
zap.String("asset_type", assetType),
zap.Uint("asset_id", assetID),
zap.Uint("order_id", order.ID),
zap.Error(err),
}
if refund.PackageUsageID != nil {
fields = append(fields, zap.Uint("package_usage_id", *refund.PackageUsageID))
}
logger.Error("资产重置:退款套餐精准失效失败", fields...)
}
}