This commit is contained in:
@@ -117,7 +117,7 @@ func Recover(ctx context.Context, db *gorm.DB, repository *outbox.Repository, li
|
||||
logger.Warn("扫描待计算订单失败", zap.Error(err))
|
||||
} else {
|
||||
for _, order := range orders {
|
||||
recoverOne(ctx, db, repository, EventCommissionCalculate, order.ID, order.ID, &orderStats, logger)
|
||||
recoverOne(ctx, db, repository, EventCommissionCalculate, order.ID, order.ID, &orderStats, logger, false)
|
||||
}
|
||||
}
|
||||
var refunds []model.RefundRequest
|
||||
@@ -126,10 +126,10 @@ func Recover(ctx context.Context, db *gorm.DB, repository *outbox.Repository, li
|
||||
} else {
|
||||
for _, refund := range refunds {
|
||||
if !refund.CommissionDeducted {
|
||||
recoverOne(ctx, db, repository, EventRefundCommissionDeduct, refund.ID, refund.OrderID, &refundStats, logger)
|
||||
recoverOne(ctx, db, repository, EventRefundCommissionDeduct, refund.ID, refund.OrderID, &refundStats, logger, true)
|
||||
}
|
||||
if !refund.AssetReset {
|
||||
recoverOne(ctx, db, repository, EventRefundAssetProcess, refund.ID, refund.OrderID, &refundStats, logger)
|
||||
recoverOne(ctx, db, repository, EventRefundAssetProcess, refund.ID, refund.OrderID, &refundStats, logger, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,16 +138,21 @@ func Recover(ctx context.Context, db *gorm.DB, repository *outbox.Repository, li
|
||||
zap.Int("退款已补发", refundStats.Resent), zap.Int("退款无需补发", refundStats.Unchanged), zap.Int("退款失败", refundStats.Failed))
|
||||
}
|
||||
|
||||
func recoverOne(ctx context.Context, db *gorm.DB, repository *outbox.Repository, eventType string, aggregateID, orderID uint, stats *RecoveryStats, logger *zap.Logger) {
|
||||
func recoverOne(ctx context.Context, db *gorm.DB, repository *outbox.Repository, eventType string, aggregateID, orderID uint, stats *RecoveryStats, logger *zap.Logger, retryDelivered bool) {
|
||||
eventID := outboxid.Stable(eventType+":", strconv.FormatUint(uint64(aggregateID), 10))
|
||||
var event model.OutboxEvent
|
||||
err := db.WithContext(ctx).Where("event_id = ?", eventID).First(&event).Error
|
||||
if err == nil {
|
||||
if event.Status != constants.OutboxStatusFailed {
|
||||
if event.Status == constants.OutboxStatusPending || event.Status == constants.OutboxStatusDelivering {
|
||||
stats.Unchanged++
|
||||
return
|
||||
}
|
||||
result := db.WithContext(ctx).Model(&model.OutboxEvent{}).Where("id = ? AND status = ?", event.ID, constants.OutboxStatusFailed).Updates(map[string]any{
|
||||
// 已投递只代表入队成功,不代表业务处理成功;业务幂等的退款后处理允许重投。
|
||||
if event.Status == constants.OutboxStatusDelivered && !retryDelivered {
|
||||
stats.Unchanged++
|
||||
return
|
||||
}
|
||||
result := db.WithContext(ctx).Model(&model.OutboxEvent{}).Where("id = ? AND status = ?", event.ID, event.Status).Updates(map[string]any{
|
||||
"status": constants.OutboxStatusPending, "retry_count": 0, "next_attempt_at": time.Now().UTC(),
|
||||
"last_error_code": "", "last_error_summary": "", "updated_at": time.Now().UTC(),
|
||||
})
|
||||
|
||||
@@ -835,8 +835,8 @@ func (s *Service) deductAllCommission(ctx context.Context, refundID uint) {
|
||||
}
|
||||
}
|
||||
|
||||
// deductSingleCommission 扣减单条佣金记录对应的代理钱包余额
|
||||
// 使用乐观锁扣减(允许余额为负数),并创建交易流水
|
||||
// deductSingleCommission 回扣单条佣金记录:先拒绝该店铺待审核提现释放冻结余额,
|
||||
// 再扣减佣金钱包(允许余额为负),最后失效佣金记录并创建流水。
|
||||
func (s *Service) deductSingleCommission(ctx context.Context, refund *model.RefundRequest, commission *model.CommissionRecord) error {
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var current model.CommissionRecord
|
||||
@@ -869,6 +869,9 @@ func (s *Service) deductSingleCommission(ctx context.Context, refund *model.Refu
|
||||
First(&wallet).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "锁定退款佣金钱包失败")
|
||||
}
|
||||
if err := s.rejectPendingWithdrawals(ctx, tx, &wallet, current.ShopID, refund); err != nil {
|
||||
return err
|
||||
}
|
||||
result := tx.WithContext(ctx).Model(&model.AgentWallet{}).
|
||||
Where("id = ? AND version = ?", wallet.ID, wallet.Version).
|
||||
Updates(map[string]any{"balance": gorm.Expr("balance - ?", current.Amount), "version": gorm.Expr("version + 1")})
|
||||
@@ -903,6 +906,97 @@ func (s *Service) deductSingleCommission(ctx context.Context, refund *model.Refu
|
||||
})
|
||||
}
|
||||
|
||||
// rejectPendingWithdrawals 回扣佣金前拒绝该店铺所有待审核提现。
|
||||
// 提现冻结的是佣金余额,退款回扣优先级更高;先解冻并拒绝,避免已回扣佣金仍被提现。
|
||||
func (s *Service) rejectPendingWithdrawals(ctx context.Context, tx *gorm.DB, wallet *model.AgentWallet, shopID uint, refund *model.RefundRequest) error {
|
||||
var withdrawals []model.CommissionWithdrawalRequest
|
||||
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("shop_id = ? AND status = ?", shopID, constants.WithdrawalStatusPending).
|
||||
Find(&withdrawals).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询待审核佣金提现失败")
|
||||
}
|
||||
if len(withdrawals) == 0 {
|
||||
return nil
|
||||
}
|
||||
var shop model.Shop
|
||||
if err := tx.WithContext(ctx).First(&shop, shopID).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询提现店铺失败")
|
||||
}
|
||||
for i := range withdrawals {
|
||||
w := &withdrawals[i]
|
||||
before := withdrawalRejectState(w)
|
||||
if err := s.agentWalletStore.UnfreezeBalanceWithTx(ctx, tx, wallet.ID, w.Amount); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "解冻提现冻结余额失败")
|
||||
}
|
||||
refType := constants.ReferenceTypeWithdrawal
|
||||
remark := "退款佣金回扣,自动拒绝提现"
|
||||
transaction := &model.AgentWalletTransaction{
|
||||
AgentWalletID: wallet.ID, ShopID: shopID, UserID: refund.Creator,
|
||||
TransactionType: constants.AgentTransactionTypeRefund, Amount: w.Amount,
|
||||
BalanceBefore: wallet.Balance, BalanceAfter: wallet.Balance,
|
||||
Status: constants.TransactionStatusSuccess, ReferenceType: &refType, ReferenceID: &w.ID,
|
||||
Remark: &remark, Creator: refund.Creator, ShopIDTag: shopID,
|
||||
}
|
||||
if err := tx.WithContext(ctx).Create(transaction).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建自动拒绝提现流水失败")
|
||||
}
|
||||
now := time.Now()
|
||||
result := tx.WithContext(ctx).Model(&model.CommissionWithdrawalRequest{}).
|
||||
Where("id = ? AND status = ?", w.ID, constants.WithdrawalStatusPending).
|
||||
Updates(map[string]any{
|
||||
"status": constants.WithdrawalStatusRejected,
|
||||
"processed_at": now,
|
||||
"reject_reason": remark,
|
||||
"updated_at": now,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "拒绝待审核提现失败")
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return errors.New(errors.CodeConflict, "提现申请状态已变化")
|
||||
}
|
||||
w.Status = constants.WithdrawalStatusRejected
|
||||
w.ProcessedAt = &now
|
||||
w.RejectReason = remark
|
||||
if err := s.appendWithdrawalRejectAudit(ctx, tx, w, wallet, transaction, &shop, before); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func withdrawalRejectState(w *model.CommissionWithdrawalRequest) map[string]any {
|
||||
return map[string]any{
|
||||
"status": w.Status, "amount": w.Amount, "fee": w.Fee, "actual_amount": w.ActualAmount,
|
||||
"withdrawal_method": w.WithdrawalMethod, "processed_at": w.ProcessedAt, "reject_reason": w.RejectReason,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) appendWithdrawalRejectAudit(ctx context.Context, tx *gorm.DB, withdrawal *model.CommissionWithdrawalRequest, wallet *model.AgentWallet, transaction *model.AgentWalletTransaction, shop *model.Shop, before map[string]any) error {
|
||||
if s.auditWriter == nil {
|
||||
return errors.New(errors.CodeInvalidStatus, "退款统一审计接缝未配置")
|
||||
}
|
||||
primary := audit.CommissionWithdrawalResource(withdrawal, constants.AuditResourceRelationPrimary, constants.AuditResourceRoleWithdrawalTarget,
|
||||
before, withdrawalRejectState(withdrawal))
|
||||
primary.SubjectVisibility = constants.AuditSubjectResult
|
||||
primary.SubjectSummary = "退款佣金回扣自动拒绝提现"
|
||||
walletResource := audit.AgentWalletResource(wallet, constants.AuditResourceRelationAffected, constants.AuditResourceRoleWithdrawalWallet,
|
||||
map[string]any{"balance": wallet.Balance, "frozen_balance": wallet.FrozenBalance},
|
||||
map[string]any{"balance": wallet.Balance, "frozen_balance": wallet.FrozenBalance - withdrawal.Amount})
|
||||
walletResource.SubjectVisibility = constants.AuditSubjectInternalOnly
|
||||
transactionResource := audit.AgentWalletTransactionResource(transaction, constants.AuditResourceRelationAffected, constants.AuditResourceRoleWithdrawalTransaction)
|
||||
transactionResource.SubjectVisibility = constants.AuditSubjectInternalOnly
|
||||
shopResource := audit.ShopResource(shop, constants.AuditResourceRelationReference, constants.AuditResourceRoleWithdrawalShop)
|
||||
shopResource.SubjectVisibility = constants.AuditSubjectInternalOnly
|
||||
return s.auditWriter.Append(ctx, tx, audit.AppendInput{
|
||||
EventID: "commission-withdrawal:" + strconv.FormatUint(uint64(withdrawal.ID), 10) + ":refund-rejected",
|
||||
ActionCode: constants.AuditActionCommissionWithdrawalRejected, Summary: "退款佣金回扣自动拒绝提现",
|
||||
ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultSuccess,
|
||||
CorrelationID: withdrawal.WithdrawalNo, Metadata: map[string]any{"amount": withdrawal.Amount, "status": withdrawal.Status},
|
||||
Resources: []audit.ResourceInput{primary, walletResource, transactionResource, shopResource},
|
||||
})
|
||||
}
|
||||
|
||||
// handleRefundAssetProcessing 幂等处理退款后的资产状态。
|
||||
// 包括退款套餐精准失效、尝试接续待生效主套餐和必要时停机;全部完成后才设置完成标记。
|
||||
func (s *Service) handleRefundAssetProcessing(ctx context.Context, refundID uint) {
|
||||
|
||||
Reference in New Issue
Block a user