收口审计治理与套餐任务进展
Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展 Confidence: medium Scope-risk: broad Directive: 后续修改需保持审计事件与业务事务边界一致 Tested: git diff --cached --check Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
cardObservationApp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
|
||||
"github.com/break/junhong_cmp_fiber/internal/infrastructure/audit"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
@@ -30,6 +31,12 @@ type ActivationService struct {
|
||||
logger *zap.Logger
|
||||
resumeCallback ResumeCallback // 复机回调,可选
|
||||
observationSeriesEvents cardObservationApp.SeriesEventWriter
|
||||
auditWriter *audit.Writer
|
||||
}
|
||||
|
||||
// SetLifecycleAudit 注入套餐权益生命周期统一审计 Writer。
|
||||
func (s *ActivationService) SetLifecycleAudit(writer *audit.Writer) {
|
||||
s.auditWriter = writer
|
||||
}
|
||||
|
||||
// SetObservationSeriesEventWriter 注入套餐激活成功观测序列 Outbox Writer。
|
||||
@@ -90,9 +97,11 @@ func (s *ActivationService) ActivateByRealname(ctx context.Context, carrierType
|
||||
now := time.Now()
|
||||
|
||||
activated := false
|
||||
var failedUsage *model.PackageUsage
|
||||
// 在事务中激活套餐
|
||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
for _, usage := range pendingUsages {
|
||||
failedUsage = usage
|
||||
// 查询套餐信息
|
||||
var pkg model.Package
|
||||
if err := tx.First(&pkg, usage.PackageID).Error; err != nil {
|
||||
@@ -150,12 +159,28 @@ func (s *ActivationService) ActivateByRealname(ctx context.Context, carrierType
|
||||
updates["next_reset_at"] = *nextResetAt
|
||||
}
|
||||
|
||||
if err := tx.Model(usage).Updates(updates).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "激活套餐失败")
|
||||
beforeData := packageUsageStateData(usage)
|
||||
result := tx.Model(usage).Where("status = ?", constants.PackageUsageStatusPending).Updates(updates)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "激活套餐失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
continue
|
||||
}
|
||||
activatedUsage := *usage
|
||||
activatedUsage.Status = constants.PackageUsageStatusActive
|
||||
activatedUsage.PendingRealnameActivation = false
|
||||
activatedUsage.ActivatedAt = &activatedAt
|
||||
activatedUsage.ExpiresAt = &expiresAt
|
||||
activatedUsage.NextResetAt = nextResetAt
|
||||
if err := s.appendActivationObservation(ctx, tx, usage, carrierType, carrierID, now); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendPackageUsageAudit(ctx, tx, s.auditWriter, constants.AuditActionPackageUsageActivated, "实名后激活套餐权益", []packageUsageAuditChange{{
|
||||
Usage: &activatedUsage, BeforeData: beforeData, AfterData: packageUsageStateData(&activatedUsage),
|
||||
}}, nil, map[string]any{"activation_source": "realname"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.syncCarrierStatusActivated(ctx, tx, usage, carrierType, carrierID)
|
||||
|
||||
@@ -171,6 +196,7 @@ func (s *ActivationService) ActivateByRealname(ctx context.Context, carrierType
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
recordPackageUsageFailure(ctx, s.db, s.auditWriter, constants.AuditActionPackageUsageActivated, "实名后激活套餐权益失败", failedUsage, err)
|
||||
return err
|
||||
}
|
||||
if activated {
|
||||
@@ -198,6 +224,7 @@ func (s *ActivationService) ActivateQueuedPackage(ctx context.Context, carrierTy
|
||||
defer s.redis.Del(ctx, lockKey)
|
||||
|
||||
activated := false
|
||||
var failedUsage *model.PackageUsage
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// 任务 9.5: 检测并标记过期的主套餐
|
||||
now := time.Now()
|
||||
@@ -213,9 +240,17 @@ func (s *ActivationService) ActivateQueuedPackage(ctx context.Context, carrierTy
|
||||
}
|
||||
|
||||
for _, expiredMain := range expiredMainUsages {
|
||||
failedUsage = expiredMain
|
||||
// 更新主套餐状态为已过期
|
||||
if err := tx.Model(expiredMain).Update("status", constants.PackageUsageStatusExpired).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新过期主套餐状态失败")
|
||||
mainBeforeData := packageUsageStateData(expiredMain)
|
||||
result := tx.Model(expiredMain).
|
||||
Where("status = ?", constants.PackageUsageStatusActive).
|
||||
Update("status", constants.PackageUsageStatusExpired)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新过期主套餐状态失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
expiresAt := now
|
||||
@@ -227,7 +262,13 @@ func (s *ActivationService) ActivateQueuedPackage(ctx context.Context, carrierTy
|
||||
zap.Time("expires_at", expiresAt))
|
||||
|
||||
// 任务 9.7: 加油包级联失效
|
||||
if err := s.invalidateAddons(ctx, tx, expiredMain.ID); err != nil {
|
||||
addons, err := s.invalidateAddons(ctx, tx, expiredMain.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expiredUsage := *expiredMain
|
||||
expiredUsage.Status = constants.PackageUsageStatusExpired
|
||||
if err := s.appendExpirationAudit(ctx, tx, &expiredUsage, mainBeforeData, addons); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -242,6 +283,7 @@ func (s *ActivationService) ActivateQueuedPackage(ctx context.Context, carrierTy
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
recordPackageUsageFailure(ctx, s.db, s.auditWriter, constants.AuditActionPackageUsageExpired, "套餐权益到期处理失败", failedUsage, err)
|
||||
return err
|
||||
}
|
||||
if activated {
|
||||
@@ -341,13 +383,14 @@ func (s *ActivationService) ActivateSpecificPackage(ctx context.Context, package
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.activatePendingUsage(ctx, tx, ¤tUsage, &pkg, carrierType, carrierID, time.Now(), "指定套餐已激活"); err != nil {
|
||||
if err := s.activatePendingUsage(ctx, tx, ¤tUsage, &pkg, carrierType, carrierID, time.Now(), "specific", "指定套餐已激活"); err != nil {
|
||||
return err
|
||||
}
|
||||
activated = true
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
recordPackageUsageFailure(ctx, s.db, s.auditWriter, constants.AuditActionPackageUsageActivated, "激活指定套餐权益失败", &usage, err)
|
||||
return err
|
||||
}
|
||||
if activated {
|
||||
@@ -381,22 +424,30 @@ func (s *ActivationService) ActivateNextPendingMainPackage(ctx context.Context,
|
||||
defer s.redis.Del(ctx, lockKey)
|
||||
|
||||
activated := false
|
||||
var failedUsage *model.PackageUsage
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
hasActive, err := s.hasActiveMainPackage(ctx, tx, carrierType, carrierID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hasActive {
|
||||
s.logger.Info("载体已有占位主套餐,本轮不接续",
|
||||
zap.String("carrier_type", carrierType),
|
||||
zap.Uint("carrier_id", carrierID))
|
||||
return nil
|
||||
}
|
||||
|
||||
nextMain, err := s.getNextPendingMainPackage(ctx, tx, carrierType, carrierID)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
s.logger.Info("载体没有待生效主套餐,本轮不接续",
|
||||
zap.String("carrier_type", carrierType),
|
||||
zap.Uint("carrier_id", carrierID))
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
failedUsage = nextMain
|
||||
|
||||
canActivate, err := s.canActivatePendingUsage(ctx, tx, nextMain, carrierType, carrierID)
|
||||
if err != nil {
|
||||
@@ -416,13 +467,14 @@ func (s *ActivationService) ActivateNextPendingMainPackage(ctx context.Context,
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐信息失败")
|
||||
}
|
||||
|
||||
if err := s.activatePendingUsage(ctx, tx, nextMain, &pkg, carrierType, carrierID, time.Now(), "队首待生效套餐已激活"); err != nil {
|
||||
if err := s.activatePendingUsage(ctx, tx, nextMain, &pkg, carrierType, carrierID, time.Now(), "queue", "队首待生效套餐已激活"); err != nil {
|
||||
return err
|
||||
}
|
||||
activated = true
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
recordPackageUsageFailure(ctx, s.db, s.auditWriter, constants.AuditActionPackageUsageActivated, "激活排队套餐权益失败", failedUsage, err)
|
||||
return false, err
|
||||
}
|
||||
if activated {
|
||||
@@ -438,16 +490,16 @@ func (s *ActivationService) HasActiveMainPackage(ctx context.Context, carrierTyp
|
||||
}
|
||||
|
||||
// invalidateAddons 任务 9.7: 加油包级联失效
|
||||
func (s *ActivationService) invalidateAddons(ctx context.Context, tx *gorm.DB, masterUsageID uint) error {
|
||||
func (s *ActivationService) invalidateAddons(ctx context.Context, tx *gorm.DB, masterUsageID uint) ([]packageUsageAuditChange, error) {
|
||||
var addons []*model.PackageUsage
|
||||
if err := tx.Where("master_usage_id = ?", masterUsageID).
|
||||
Where("status IN ?", []int{constants.PackageUsageStatusActive, constants.PackageUsageStatusPending}).
|
||||
Find(&addons).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询加油包失败")
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询加油包失败")
|
||||
}
|
||||
|
||||
if len(addons) == 0 {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
addonIDs := make([]uint, len(addons))
|
||||
@@ -459,14 +511,20 @@ func (s *ActivationService) invalidateAddons(ctx context.Context, tx *gorm.DB, m
|
||||
if err := tx.Model(&model.PackageUsage{}).
|
||||
Where("id IN ?", addonIDs).
|
||||
Update("status", constants.PackageUsageStatusInvalidated).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "批量失效加油包失败")
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "批量失效加油包失败")
|
||||
}
|
||||
|
||||
s.logger.Info("加油包已级联失效",
|
||||
zap.Uint("master_usage_id", masterUsageID),
|
||||
zap.Int("addon_count", len(addons)))
|
||||
|
||||
return nil
|
||||
changes := make([]packageUsageAuditChange, 0, len(addons))
|
||||
for _, addon := range addons {
|
||||
beforeData := packageUsageStateData(addon)
|
||||
addon.Status = constants.PackageUsageStatusInvalidated
|
||||
changes = append(changes, packageUsageAuditChange{Usage: addon, BeforeData: beforeData, AfterData: packageUsageStateData(addon)})
|
||||
}
|
||||
return changes, nil
|
||||
}
|
||||
|
||||
// activateNextMainPackage 任务 9.6: 激活下一个待生效主套餐
|
||||
@@ -502,7 +560,7 @@ func (s *ActivationService) activateNextMainPackage(ctx context.Context, tx *gor
|
||||
return false, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐信息失败")
|
||||
}
|
||||
|
||||
if err := s.activatePendingUsage(ctx, tx, nextMain, &pkg, carrierType, carrierID, now, "排队主套餐已激活"); err != nil {
|
||||
if err := s.activatePendingUsage(ctx, tx, nextMain, &pkg, carrierType, carrierID, now, "queue", "排队主套餐已激活"); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
@@ -577,7 +635,7 @@ func (s *ActivationService) isCarrierRealnamed(ctx context.Context, tx *gorm.DB,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ActivationService) activatePendingUsage(ctx context.Context, tx *gorm.DB, usage *model.PackageUsage, pkg *model.Package, carrierType string, carrierID uint, now time.Time, logMessage string) error {
|
||||
func (s *ActivationService) activatePendingUsage(ctx context.Context, tx *gorm.DB, usage *model.PackageUsage, pkg *model.Package, carrierType string, carrierID uint, now time.Time, activationSource, logMessage string) error {
|
||||
terms, err := ResolveUsageTerms(usage, pkg, s.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -608,12 +666,27 @@ func (s *ActivationService) activatePendingUsage(ctx context.Context, tx *gorm.D
|
||||
updates["next_reset_at"] = *nextResetAt
|
||||
}
|
||||
|
||||
if err := tx.Model(usage).Updates(updates).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "激活排队主套餐失败")
|
||||
beforeData := packageUsageStateData(usage)
|
||||
result := tx.Model(usage).Where("status = ?", constants.PackageUsageStatusPending).Updates(updates)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "激活排队主套餐失败")
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return nil
|
||||
}
|
||||
usage.Status = constants.PackageUsageStatusActive
|
||||
usage.PendingRealnameActivation = false
|
||||
usage.ActivatedAt = &activatedAt
|
||||
usage.ExpiresAt = &expiresAt
|
||||
usage.NextResetAt = nextResetAt
|
||||
if err := s.appendActivationObservation(ctx, tx, usage, carrierType, carrierID, now); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendPackageUsageAudit(ctx, tx, s.auditWriter, constants.AuditActionPackageUsageActivated, logMessage, []packageUsageAuditChange{{
|
||||
Usage: usage, BeforeData: beforeData, AfterData: packageUsageStateData(usage),
|
||||
}}, nil, map[string]any{"activation_source": activationSource}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.syncCarrierStatusActivated(ctx, tx, usage, carrierType, carrierID)
|
||||
|
||||
@@ -711,137 +784,139 @@ func (s *ActivationService) InvalidatePackagesForRefund(ctx context.Context, ass
|
||||
constants.PackageUsageStatusDepleted,
|
||||
}
|
||||
|
||||
// 换货会把套餐使用记录迁移到新资产,但原订单与套餐使用记录的关联保持不变。
|
||||
// 因此退款必须以订单和套餐使用记录为权威定位键,不能再用旧资产 ID 缩小查询范围。
|
||||
baseQuery := s.db.WithContext(ctx).Model(&model.PackageUsage{})
|
||||
|
||||
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
|
||||
var failedUsage *model.PackageUsage
|
||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// 换货后权益仍保留原订单关系,退款必须按订单定位并使用权益当前资产快照。
|
||||
query := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("status IN ?", validStatuses)
|
||||
var targets []model.PackageUsage
|
||||
if packageUsageID != nil && *packageUsageID > 0 {
|
||||
var usage model.PackageUsage
|
||||
if err := query.Where("id = ? AND order_id = ?", *packageUsageID, orderID).First(&usage).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询退款关联套餐失败")
|
||||
}
|
||||
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 {
|
||||
targets = append(targets, usage)
|
||||
} else if err := query.Where("order_id = ?", orderID).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)
|
||||
mainUsageIDs := make([]uint, 0, len(targets))
|
||||
for i := range targets {
|
||||
if targets[i].MasterUsageID == nil {
|
||||
mainUsageIDs = append(mainUsageIDs, targets[i].ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(mainUsageIDs) > 0 {
|
||||
var addons []model.PackageUsage
|
||||
if err := s.db.WithContext(ctx).Model(&model.PackageUsage{}).
|
||||
Where("master_usage_id IN ?", mainUsageIDs).
|
||||
Where("status IN ?", validStatuses).
|
||||
Find(&addons).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询主套餐关联加油包失败")
|
||||
if len(mainUsageIDs) > 0 {
|
||||
var addons []model.PackageUsage
|
||||
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("master_usage_id IN ? AND status IN ?", mainUsageIDs, validStatuses).Find(&addons).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询主套餐关联加油包失败")
|
||||
}
|
||||
targets = append(targets, addons...)
|
||||
}
|
||||
for _, addon := range addons {
|
||||
targetIDSet[addon.ID] = struct{}{}
|
||||
failedUsage = &targets[0]
|
||||
targetIDs := make([]uint, 0, len(targets))
|
||||
changes := make([]packageUsageAuditChange, 0, len(targets))
|
||||
for i := range targets {
|
||||
targetIDs = append(targetIDs, targets[i].ID)
|
||||
beforeData := packageUsageStateData(&targets[i])
|
||||
targets[i].Status = constants.PackageUsageStatusInvalidated
|
||||
if refundID > 0 {
|
||||
targets[i].RefundID = &refundID
|
||||
}
|
||||
targets[i].RefundNo = refundNo
|
||||
changes = append(changes, packageUsageAuditChange{Usage: &targets[i], BeforeData: beforeData, AfterData: packageUsageStateData(&targets[i])})
|
||||
}
|
||||
updates := map[string]any{"status": constants.PackageUsageStatusInvalidated}
|
||||
if refundID > 0 {
|
||||
updates["refund_id"] = refundID
|
||||
}
|
||||
if refundNo != "" {
|
||||
updates["refund_no"] = refundNo
|
||||
}
|
||||
result := tx.WithContext(ctx).Model(&model.PackageUsage{}).
|
||||
Where("id IN ? AND status IN ?", targetIDs, validStatuses).Updates(updates)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "退款失效套餐失败")
|
||||
}
|
||||
if result.RowsAffected != int64(len(targetIDs)) {
|
||||
return errors.New(errors.CodeConflict, "退款套餐权益状态已变化")
|
||||
}
|
||||
var refund *model.RefundRequest
|
||||
if refundID > 0 {
|
||||
refund = &model.RefundRequest{}
|
||||
if err := tx.WithContext(ctx).Where("id = ?", refundID).First(refund).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐权益关联退款单审计快照失败")
|
||||
}
|
||||
}
|
||||
return appendPackageUsageAudit(ctx, tx, s.auditWriter, constants.AuditActionPackageUsageRefundInvalidated, "退款失效套餐权益", changes, refund, map[string]any{
|
||||
"asset_type": assetType, "asset_id": assetID, "order_id": orderID, "refund_id": refundID, "refund_no": refundNo,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
recordPackageUsageFailure(ctx, s.db, s.auditWriter, constants.AuditActionPackageUsageRefundInvalidated, "退款失效套餐权益失败", failedUsage, err)
|
||||
return err
|
||||
}
|
||||
|
||||
targetIDs := make([]uint, 0, len(targetIDSet))
|
||||
for id := range targetIDSet {
|
||||
targetIDs = append(targetIDs, id)
|
||||
}
|
||||
if len(targetIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
updates := map[string]any{
|
||||
"status": constants.PackageUsageStatusInvalidated,
|
||||
}
|
||||
if refundID > 0 {
|
||||
updates["refund_id"] = refundID
|
||||
}
|
||||
if refundNo != "" {
|
||||
updates["refund_no"] = refundNo
|
||||
}
|
||||
|
||||
result := s.db.WithContext(ctx).
|
||||
Model(&model.PackageUsage{}).
|
||||
Where("id IN ?", targetIDs).
|
||||
Updates(updates)
|
||||
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.Uint("refund_id", refundID),
|
||||
zap.String("refund_no", refundNo),
|
||||
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 {
|
||||
query := s.db.WithContext(ctx).
|
||||
Model(&model.PackageUsage{}).
|
||||
Where("status IN ?", []int{
|
||||
constants.PackageUsageStatusPending,
|
||||
constants.PackageUsageStatusActive,
|
||||
constants.PackageUsageStatusDepleted,
|
||||
})
|
||||
|
||||
switch assetType {
|
||||
case "iot_card":
|
||||
query = query.Where("iot_card_id = ?", assetID)
|
||||
case "device":
|
||||
query = query.Where("device_id = ?", assetID)
|
||||
default:
|
||||
validStatuses := []int{
|
||||
constants.PackageUsageStatusPending,
|
||||
constants.PackageUsageStatusActive,
|
||||
constants.PackageUsageStatusDepleted,
|
||||
}
|
||||
if assetType != constants.AssetTypeIotCard && assetType != constants.AssetTypeDevice {
|
||||
return errors.New(errors.CodeInvalidParam, "无效的资产类型")
|
||||
}
|
||||
|
||||
result := query.Update("status", constants.PackageUsageStatusInvalidated)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "批量失效套餐失败")
|
||||
var failedUsage *model.PackageUsage
|
||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
query := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("status IN ?", validStatuses)
|
||||
query = query.Where(assetType+"_id = ?", assetID)
|
||||
var targets []model.PackageUsage
|
||||
if err := query.Find(&targets).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询资产关联套餐权益失败")
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
return nil
|
||||
}
|
||||
failedUsage = &targets[0]
|
||||
ids := make([]uint, 0, len(targets))
|
||||
changes := make([]packageUsageAuditChange, 0, len(targets))
|
||||
for i := range targets {
|
||||
ids = append(ids, targets[i].ID)
|
||||
beforeData := packageUsageStateData(&targets[i])
|
||||
targets[i].Status = constants.PackageUsageStatusInvalidated
|
||||
changes = append(changes, packageUsageAuditChange{Usage: &targets[i], BeforeData: beforeData, AfterData: packageUsageStateData(&targets[i])})
|
||||
}
|
||||
result := tx.WithContext(ctx).Model(&model.PackageUsage{}).
|
||||
Where("id IN ? AND status IN ?", ids, validStatuses).
|
||||
Update("status", constants.PackageUsageStatusInvalidated)
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "批量失效套餐失败")
|
||||
}
|
||||
if result.RowsAffected != int64(len(ids)) {
|
||||
return errors.New(errors.CodeConflict, "资产套餐权益状态已变化")
|
||||
}
|
||||
return appendPackageUsageAudit(ctx, tx, s.auditWriter, constants.AuditActionPackageUsageAssetInvalidated, "资产失效套餐权益", changes, nil, map[string]any{
|
||||
"asset_type": assetType, "asset_id": assetID,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
recordPackageUsageFailure(ctx, s.db, s.auditWriter, constants.AuditActionPackageUsageAssetInvalidated, "资产失效套餐权益失败", failedUsage, err)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("批量失效套餐完成",
|
||||
zap.String("asset_type", assetType),
|
||||
zap.Uint("asset_id", assetID),
|
||||
zap.Int64("affected", result.RowsAffected),
|
||||
)
|
||||
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user