package refund import ( "context" "math" "math/big" "slices" "strconv" "go.uber.org/zap" "gorm.io/gorm" "gorm.io/gorm/clause" "github.com/break/junhong_cmp_fiber/internal/infrastructure/audit" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/pkg/auditcontext" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) // clawbackAllocation 是一条原佣金的回溯分摊结果。 // Amount 为正的应回溯金额;写入回溯明细时取负数,原佣金记录本身不变。 type clawbackAllocation struct { Commission model.CommissionRecord Amount int64 } // clawbackCommission 处理一次退款的佣金回溯后处理。 // // 返回非 nil 表示后处理尚未闭合(准入未满足或订单佣金未终态),由既有 Outbox / 补偿任务重试; // 返回 nil 表示已闭合:已生成回溯明细、确认无需回溯或审批异常转人工。 // // 准入由退款申请状态、审批异常标记与退款方式唯一决定: // 仅退款申请已通过(原路退款还须渠道明确成功)且无审批异常时生成回溯明细。 // 失败分类标记面向「尝试是否终结」,与「退款是否真正完成」正交,因此不参与准入判定。 func (s *Service) clawbackCommission(ctx context.Context, refundID uint) error { var refund model.RefundRequest if err := s.db.WithContext(ctx).Where("id = ?", refundID).First(&refund).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询退款单失败") } ctx = auditcontext.With(ctx, auditcontext.Context{ ActorKind: constants.AuditActorSystemTask, ActorID: constants.AuditActorIDRefundCommissionPostProcessing, ActorName: "退款佣金自动回扣任务", Source: constants.AuditSourceWorker, CorrelationID: refund.RefundNo, }) // commission_deducted 只是投影:已闭合即无需重复处理,本函数不依赖它做逐条幂等判定。 if refund.CommissionDeducted { return nil } // 审批异常(企业微信通过后撤销)不生成任何回溯事实,但必须闭合后处理并转人工, // 否则补偿扫描会在每轮重复处理同一批退款单。 if refund.AnomalyFlag != 0 { return s.settleClawback(ctx, &refund, constants.AuditActionRefundAnomalyFlagged, "退款审批异常,佣金回溯转人工处理", clawbackEventID(refund.ID, "clawback-anomaly"), map[string]any{ "anomaly_flag": refund.AnomalyFlag, "anomaly_reason": refund.AnomalyReason, "order_id": refund.OrderID, "clawback_required": false, }) } // 准入:只有已通过的退款才可能完成退款,待审批、原路处理中与渠道明确失败都必须等待。 if refund.Status != model.RefundStatusApproved { return errors.New(errors.CodeServiceUnavailable, "退款申请尚未通过,暂不生成佣金回溯") } if refund.Method == constants.RefundMethodOriginalRoute && refund.ChannelRefundStatus != constants.RefundChannelStatusSucceeded { return errors.New(errors.CodeServiceUnavailable, "原路退款渠道结果尚未明确成功,暂不生成佣金回溯") } // 订单佣金未终态时等待:此时「无已发放佣金」只代表尚未计算完成或尚待人工修正, // 提前判定为无需回溯会让后续入账的佣金永久失去回溯机会,违反「MUST NOT 提前判定为无需回溯」。 // // 终态由两件事共同决定,缺一不可: // 1. 订单佣金流程已离开「待计算」; // 2. 该订单不存在任何未终态佣金记录(已冻结 1、解冻中 2、待人工修正 99)。 // 不能用 order.commission_status = 3 判定终态:该值只表示「存在 99 记录」, // 而人工处置(shop_commission.ResolveCommissionRecord)把 99 改成已发放/已失效后并不回写订单状态, // 订单会长期停在 3;只看订单状态会把「待人工修正」误判为终态并提前闭合。 // // 收敛条件(依据全仓 tb_commission_record.status 写点实测):该表状态只由 // commission_calculation(创建置 3 或 99、入账置 3)、recharge_order(创建置 3)与 // shop_commission.ResolveCommissionRecord(人工处置置 3 或 4)写入,从不写入 1/2 // ——1/2 是仅供历史数据读取的遗留枚举,纳入未终态集合属防御性判断。 // 因此现实中的等待只可能由 99 引起,且处置后必然收敛:置 3 → 生成回溯;置 4 → 确认无佣金可回溯并闭合。 var order model.Order if err := s.db.WithContext(ctx).Select("id, commission_status").First(&order, refund.OrderID).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询退款佣金关联订单失败") } if order.CommissionStatus == model.CommissionStatusPending { return errors.New(errors.CodeServiceUnavailable, "订单佣金尚未计算完成,等待终态后再回溯") } var openCommissionCount int64 if err := s.db.WithContext(ctx).Model(&model.CommissionRecord{}). Where("order_id = ? AND status IN ?", refund.OrderID, []int{ constants.CommissionStatusFrozen, constants.CommissionStatusUnfreezing, constants.CommissionStatusPendingReview, }).Count(&openCommissionCount).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "统计订单未终态佣金记录失败") } if openCommissionCount > 0 { return errors.New(errors.CodeServiceUnavailable, "订单存在未终态佣金记录,等待终态后再回溯") } // 可回溯原佣金固定为已发放记录,稳定顺序取 original_commission_id 升序: // 该顺序决定舍入补差落点,必须固定,否则同一输入可能得出不同分摊结果。 var commissions []model.CommissionRecord if err := s.db.WithContext(ctx). Where("order_id = ? AND status = ?", refund.OrderID, constants.CommissionStatusReleased). Order("id ASC").Find(&commissions).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询退款原佣金记录失败") } if len(commissions) == 0 { // 终态确无佣金:不产生任何钱包变动。 return s.settleClawback(ctx, &refund, constants.AuditActionRefundClawbackNotRequired, "退款无需回溯佣金", clawbackEventID(refund.ID, "clawback-not-required"), map[string]any{ "order_id": refund.OrderID, "commission_status": order.CommissionStatus, "released_commission_count": 0, "clawback_required": false, }) } denominator, err := s.resolveClawbackDenominator(ctx, &refund) if err != nil { return err } numerator := s.resolveClawbackNumerator(&refund) if denominator <= 0 { // 冻结实收缺失或非正:记录可恢复失败,不落库、不改余额、不置位, // 禁止用订单标价或申请金额替代。 s.logger.Error("佣金回溯:本次退款冻结实收金额非正,等待权威金额补齐", zap.Uint("refund_id", refund.ID), zap.String("refund_no", refund.RefundNo), zap.Int64("frozen_actual_received_amount", refund.FrozenActualReceivedAmount)) return errors.New(errors.CodeServiceUnavailable, "退款冻结实收金额缺失或非正,无法计算佣金回溯") } if numerator <= 0 { // 已通过的退款必然携带权威成功退款金额;缺失时同样按可恢复失败处理,不生成零金额回溯。 s.logger.Error("佣金回溯:本次退款成功金额非正,等待权威金额补齐", zap.Uint("refund_id", refund.ID), zap.String("refund_no", refund.RefundNo), zap.String("method", refund.Method), zap.Int64("channel_refund_amount", refund.ChannelRefundAmount)) return errors.New(errors.CodeServiceUnavailable, "退款成功金额缺失或非正,无法计算佣金回溯") } // 剩余可回溯余额按明细聚合:同一原佣金可被同订单的多次退款累计回溯,累计不得超过原佣金金额。 clawed, err := s.loadClawedAmounts(ctx, commissions) if err != nil { return err } amounts := make([]int64, len(commissions)) clawedAmounts := make([]int64, len(commissions)) for i := range commissions { amounts[i] = commissions[i].Amount clawedAmounts[i] = clawed[commissions[i].ID] } amountsAllocated := allocateClawbackAmounts(amounts, clawedAmounts, numerator, denominator) allocations := make([]clawbackAllocation, 0, len(commissions)) total := int64(0) for i := range commissions { allocations = append(allocations, clawbackAllocation{Commission: commissions[i], Amount: amountsAllocated[i]}) total += amountsAllocated[i] } if total <= 0 { // 比例向下取整后为 0,或全部原佣金的剩余可回溯余额已用尽:本次退款不再产生资金事实。 return s.settleClawback(ctx, &refund, constants.AuditActionRefundClawbackNotRequired, "退款无需回溯佣金", clawbackEventID(refund.ID, "clawback-not-required"), map[string]any{ "order_id": refund.OrderID, "commission_status": order.CommissionStatus, "released_commission_count": len(commissions), "clawback_required": false, "numerator": numerator, "denominator": denominator, }) } return s.applyClawback(ctx, &refund, allocations) } // clawbackEventID 生成回溯审计事件的稳定键,重复执行返回同一事件,审计写入因此天然幂等。 func clawbackEventID(refundID uint, suffix string) string { return "refund:" + strconv.FormatUint(uint64(refundID), 10) + ":" + suffix } // resolveClawbackDenominator 取本次退款的冻结实收金额作为回溯比例分母。 // 退款行缺失权威金额时回落本次审批尝试记录的冻结事实;两者都没有时返回 0,由调用方按可恢复失败处理。 func (s *Service) resolveClawbackDenominator(ctx context.Context, refund *model.RefundRequest) (int64, error) { if refund.FrozenActualReceivedAmount > 0 { return refund.FrozenActualReceivedAmount, nil } if refund.LatestAttemptID == 0 { return 0, nil } var attempt model.RefundRequestAttempt err := s.db.WithContext(ctx).Select("id, frozen_actual_received_amount"). First(&attempt, refund.LatestAttemptID).Error if err == nil { return attempt.FrozenActualReceivedAmount, nil } if err == gorm.ErrRecordNotFound { return 0, nil } return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询退款审批尝试冻结实收金额失败") } // resolveClawbackNumerator 取本次退款成功金额作为回溯比例分子。 // 原路退款取渠道明确成功金额,其余方式取审批退款金额(含退回原钱包与客户收款信息方式)。 func (s *Service) resolveClawbackNumerator(refund *model.RefundRequest) int64 { if refund.Method == constants.RefundMethodOriginalRoute { return refund.ChannelRefundAmount } if refund.ApprovedRefundAmount != nil { return *refund.ApprovedRefundAmount } return 0 } // loadClawedAmounts 聚合各原佣金已生成的回溯累计金额(正数视图)。 // 累计口径覆盖全部退款单:同一原佣金的累计回溯不得超过其金额,与具体退款无关。 func (s *Service) loadClawedAmounts(ctx context.Context, commissions []model.CommissionRecord) (map[uint]int64, error) { clawed := make(map[uint]int64, len(commissions)) if len(commissions) == 0 { return clawed, nil } ids := make([]uint, 0, len(commissions)) for i := range commissions { ids = append(ids, commissions[i].ID) } type clawedRow struct { OriginalCommissionID uint Total int64 } var rows []clawedRow if err := s.db.WithContext(ctx).Model(&model.CommissionClawbackRecord{}). Select("original_commission_id, COALESCE(SUM(amount), 0) AS total"). Where("original_commission_id IN ?", ids). Group("original_commission_id").Scan(&rows).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "聚合原佣金已回溯金额失败") } for _, row := range rows { // 明细金额为负数,转为正数视图供分摊计算使用。 clawed[row.OriginalCommissionID] = -row.Total } return clawed, nil } // allocateClawbackAmounts 按分计算各原佣金的应回溯金额,入参与返回均按 original_commission_id 升序。 // // 计算规则: // - 逐条基准 base_i = min(floor(amount_i × N / D), 剩余_i); // - 应回溯总额 T = min(floor(Σamount_i × N / D), Σ剩余_i); // - 舍入差 T − Σbase_i 自稳定顺序末条起向前分摊,每条不超过该条剩余_i。 // // 乘法使用 math/big 中间量,全程整数,禁止浮点与溢出;余额不足由调用方允许负余额,不在此处裁剪。 func allocateClawbackAmounts(amounts []int64, clawed []int64, numerator int64, denominator int64) []int64 { result := make([]int64, len(amounts)) if len(amounts) == 0 || numerator <= 0 || denominator <= 0 { return result } remaining := make([]int64, len(amounts)) totalAmount := new(big.Int) totalRemaining := new(big.Int) for i := range amounts { left := amounts[i] - clawed[i] if amounts[i] <= 0 { left = 0 } if left < 0 { left = 0 } remaining[i] = left totalAmount.Add(totalAmount, big.NewInt(amounts[i])) totalRemaining.Add(totalRemaining, big.NewInt(left)) } totalTarget := mulDivFloor(totalAmount, numerator, denominator) totalTarget = minBig(totalTarget, totalRemaining) totalBase := new(big.Int) for i := range amounts { base := mulDivFloorInt64(amounts[i], numerator, denominator) if base > remaining[i] { base = remaining[i] } result[i] = base totalBase.Add(totalBase, big.NewInt(base)) } // 补差自末条起向前分摊;最末条不足以吸收全部余差时继续向前一条分摊。 gap := new(big.Int).Sub(totalTarget, totalBase) for i := len(amounts) - 1; i >= 0 && gap.Sign() > 0; i-- { room := remaining[i] - result[i] if room <= 0 { continue } add := minBig(gap, big.NewInt(room)) result[i] += add.Int64() gap.Sub(gap, add) } return result } // mulDivFloor 计算 floor(value × numerator / denominator),使用大整数中间量避免溢出。 func mulDivFloor(value *big.Int, numerator int64, denominator int64) *big.Int { if value.Sign() <= 0 || numerator <= 0 || denominator <= 0 { return big.NewInt(0) } product := new(big.Int).Mul(value, big.NewInt(numerator)) return product.Div(product, big.NewInt(denominator)) } // mulDivFloorInt64 计算 floor(value × numerator / denominator) 并收敛到 int64。 // 结果不可能为负;超过 int64 上限时按上限收敛,由后续剩余额度裁剪兜底。 func mulDivFloorInt64(value int64, numerator int64, denominator int64) int64 { result := mulDivFloor(big.NewInt(value), numerator, denominator) if !result.IsInt64() { return math.MaxInt64 } return result.Int64() } // minBig 返回两个非负大整数中的较小值。 func minBig(left *big.Int, right *big.Int) *big.Int { if left.Cmp(right) <= 0 { return new(big.Int).Set(left) } return new(big.Int).Set(right) } // settleClawback 在没有资金动作时闭合退款佣金后处理:条件置位完成后处理事实并写入同一事务的审计。 // 置位带 commission_deducted = false 保护,重复执行不会二次闭合。 func (s *Service) settleClawback( ctx context.Context, refund *model.RefundRequest, actionCode string, summary string, eventID string, metadata map[string]any, ) error { if s.auditWriter == nil { return errors.New(errors.CodeInvalidStatus, "退款统一审计接缝未配置") } err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { result := tx.WithContext(ctx).Model(&model.RefundRequest{}). Where("id = ? AND commission_deducted = ?", refund.ID, false). Update("commission_deducted", true) if result.Error != nil { return errors.Wrap(errors.CodeDatabaseError, result.Error, "标记退款佣金后处理已闭合失败") } if result.RowsAffected == 0 { return nil } primary := audit.RefundResource(refund, constants.AuditResourceRelationPrimary, constants.AuditResourceRoleRefundTarget) primary.BeforeData = map[string]any{"commission_deducted": false} primary.AfterData = map[string]any{"commission_deducted": true} primary.SubjectVisibility = constants.AuditSubjectResult primary.SubjectSummary = summary resources := []audit.ResourceInput{primary} var order model.Order if err := tx.WithContext(ctx).First(&order, refund.OrderID).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询退款佣金关联订单审计快照失败") } orderResource := audit.OrderResource(&order, constants.AuditResourceRelationReference, constants.AuditResourceRoleRefundOrder) orderResource.SubjectVisibility = constants.AuditSubjectInternalOnly resources = append(resources, orderResource) if _, err := s.auditWriter.AppendAndGet(ctx, tx, audit.AppendInput{ EventID: eventID, ActionCode: actionCode, Summary: summary, ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultSuccess, CorrelationID: refund.RefundNo, Metadata: metadata, Resources: resources, }); err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "写入退款佣金后处理闭合审计失败") } return nil }) if err != nil { return err } s.logger.Info("退款佣金回溯后处理已闭合", zap.Uint("refund_id", refund.ID), zap.String("refund_no", refund.RefundNo), zap.String("action", actionCode)) return nil } // clawbackShopState 记录一个店铺在本次回溯中的钱包与资金事实,用于同事务审计。 type clawbackShopState struct { shop model.Shop wallet model.AgentWallet balanceFrom int64 balanceTo int64 } // applyClawback 在同一事务内释放待审提现冻结、生成回溯明细、扣减佣金钱包并写入审计。 // // 事务内顺序固定:锁提现申请行 → 锁尝试行 → 解冻冻结 → 置驳回 → 插回溯明细 → 扣 balance(允许为负) // → 写负数流水 → 审计。任一步失败整体回滚,不释放提现、不写部分明细或部分流水。 func (s *Service) applyClawback(ctx context.Context, refund *model.RefundRequest, allocations []clawbackAllocation) error { if s.auditWriter == nil { return errors.New(errors.CodeInvalidStatus, "退款统一审计接缝未配置") } // generatedCount 记录本次实际落库的明细条数,用于日志与审计的真实性(不含命中唯一键跳过的条目)。 generatedCount := 0 err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // 第 2 层幂等:锁定原佣金行并复核状态,状态被并发改写即整体回滚等待重试。 ids := make([]uint, 0, len(allocations)) for i := range allocations { ids = append(ids, allocations[i].Commission.ID) } var locked []model.CommissionRecord if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}). Where("id IN ?", ids).Order("id ASC").Find(&locked).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "锁定退款原佣金记录失败") } statusByID := make(map[uint]int, len(locked)) for i := range locked { statusByID[locked[i].ID] = locked[i].Status } for i := range allocations { if statusByID[allocations[i].Commission.ID] != constants.CommissionStatusReleased { return errors.New(errors.CodeConflict, "退款原佣金状态已变化") } } // 第 1 层幂等的事实来源是 (refund_id, original_commission_id) 唯一约束; // 这里按已有明细跳过已生成的条目,避免唯一冲突回滚整次后处理的其余资金事实。 var existing []model.CommissionClawbackRecord if err := tx.WithContext(ctx).Where("refund_id = ?", refund.ID).Find(&existing).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询退款已有佣金回溯明细失败") } generated := make(map[uint]struct{}, len(existing)) for i := range existing { generated[existing[i].OriginalCommissionID] = struct{}{} } // 按店铺分组并按店铺升序加锁,避免与并发回溯或提现终态消费者形成死锁环。 shopOrder := make([]uint, 0, len(allocations)) shopIndex := make(map[uint][]int, len(allocations)) for i := range allocations { shopID := allocations[i].Commission.ShopID if _, ok := shopIndex[shopID]; !ok { shopOrder = append(shopOrder, shopID) } shopIndex[shopID] = append(shopIndex[shopID], i) } sortUintAsc(shopOrder) var ( records []*model.CommissionClawbackRecord transactions []*model.AgentWalletTransaction shops []clawbackShopState ) remark := "退款佣金回溯" for _, shopID := range shopOrder { // 加锁顺序全库统一:申请行 → 尝试行 → 钱包行(钱包永远最后加锁)。 rejects, err := s.collectPendingWithdrawalRejects(ctx, tx, shopID) if err != nil { return err } var wallet model.AgentWallet if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}). Where("shop_id = ? AND wallet_type = ?", shopID, constants.AgentWalletTypeCommission). First(&wallet).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "锁定退款佣金钱包失败") } // 先释放待审核提现冻结,再扣减余额:既有约束要求冻结额不高于非负余额。 if err := s.applyPendingWithdrawalRejects(ctx, tx, &wallet, refund, rejects); err != nil { return err } balanceFrom := wallet.Balance for _, index := range shopIndex[shopID] { allocation := allocations[index] if allocation.Amount <= 0 { continue } if _, ok := generated[allocation.Commission.ID]; ok { continue } commissionID := allocation.Commission.ID record := &model.CommissionClawbackRecord{ RefundID: refund.ID, OriginalCommissionID: commissionID, OrderID: allocation.Commission.OrderID, ShopID: allocation.Commission.ShopID, OrderNo: refund.OrderNo, RefundNo: refund.RefundNo, CommissionSource: allocation.Commission.CommissionSource, Amount: -allocation.Amount, BalanceAfter: wallet.Balance - allocation.Amount, Withdrawable: false, Status: constants.CommissionStatusClawback, } if err := tx.WithContext(ctx).Create(record).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "创建佣金回溯明细失败") } // 佣金钱包允许余额为负:余额不足不跳过、不拒绝回溯。 result := tx.WithContext(ctx).Model(&model.AgentWallet{}). Where("id = ? AND version = ?", wallet.ID, wallet.Version). Updates(map[string]any{"balance": gorm.Expr("balance - ?", allocation.Amount), "version": gorm.Expr("version + 1")}) if result.Error != nil { return errors.Wrap(errors.CodeDatabaseError, result.Error, "扣减退款佣金钱包失败") } if result.RowsAffected != 1 { return errors.New(errors.CodeConflict, "退款佣金钱包版本已变化") } balanceBefore := wallet.Balance wallet.Balance -= allocation.Amount wallet.Version++ refType, refID := constants.ReferenceTypeCommission, commissionID transaction := &model.AgentWalletTransaction{ AgentWalletID: wallet.ID, ShopID: wallet.ShopID, UserID: refund.Creator, TransactionType: constants.AgentTransactionTypeCommissionDeduct, Amount: -allocation.Amount, BalanceBefore: balanceBefore, BalanceAfter: wallet.Balance, Status: constants.TransactionStatusSuccess, ReferenceType: &refType, ReferenceID: &refID, Remark: &remark, Creator: refund.Creator, ShopIDTag: wallet.ShopID, } if err := tx.WithContext(ctx).Create(transaction).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "创建退款佣金回溯流水失败") } records = append(records, record) transactions = append(transactions, transaction) generatedCount++ } var shop model.Shop if err := tx.WithContext(ctx).First(&shop, shopID).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询退款佣金关联店铺审计快照失败") } shops = append(shops, clawbackShopState{shop: shop, wallet: wallet, balanceFrom: balanceFrom, balanceTo: wallet.Balance}) } // 本次未生成任何明细时(全部命中已存在的唯一键,仅投影陈旧时可达成)不写 // refund.clawback_commission 审计:该动作语义是「生成回溯明细」, // 记录数 0 且 clawback_required=true 的审计是误导性事实。置位逻辑保持不变。 if len(records) > 0 { if err := s.appendClawbackAudit(ctx, tx, refund, records, transactions, shops); err != nil { return err } } // 全部回溯明细与钱包流水完成后才置位;带条件保护,重复执行不会二次闭合。 result := tx.WithContext(ctx).Model(&model.RefundRequest{}). Where("id = ? AND commission_deducted = ?", refund.ID, false). Update("commission_deducted", true) if result.Error != nil { return errors.Wrap(errors.CodeDatabaseError, result.Error, "标记退款佣金回溯完成失败") } if result.RowsAffected != 1 { return errors.New(errors.CodeConflict, "退款佣金后处理已由其他执行闭合") } return nil }) if err != nil { return err } s.logger.Info("退款佣金回溯完成", zap.Uint("refund_id", refund.ID), zap.String("refund_no", refund.RefundNo), zap.Int("clawback_records", generatedCount)) return nil } // appendClawbackAudit 将回溯明细、佣金钱包与负数流水绑定在同一事务。 func (s *Service) appendClawbackAudit( ctx context.Context, tx *gorm.DB, refund *model.RefundRequest, records []*model.CommissionClawbackRecord, transactions []*model.AgentWalletTransaction, shops []clawbackShopState, ) error { primary := audit.RefundResource(refund, constants.AuditResourceRelationPrimary, constants.AuditResourceRoleRefundTarget) primary.SubjectVisibility = constants.AuditSubjectResult primary.SubjectSummary = "退款佣金已回溯" resources := []audit.ResourceInput{primary} var order model.Order if err := tx.WithContext(ctx).First(&order, refund.OrderID).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询退款佣金关联订单审计快照失败") } orderResource := audit.OrderResource(&order, constants.AuditResourceRelationReference, constants.AuditResourceRoleRefundOrder) orderResource.SubjectVisibility = constants.AuditSubjectInternalOnly resources = append(resources, orderResource) totalClawback := int64(0) for _, record := range records { totalClawback += record.Amount resource := audit.CommissionClawbackResource(record) resource.SubjectVisibility = constants.AuditSubjectInternalOnly resources = append(resources, resource) } for i := range transactions { resource := audit.AgentWalletTransactionResource(transactions[i], constants.AuditResourceRelationAffected, constants.AuditResourceRoleRefundTransaction) resource.SubjectVisibility = constants.AuditSubjectInternalOnly resources = append(resources, resource) } for i := range shops { walletResource := audit.AgentWalletResource(&shops[i].wallet, constants.AuditResourceRelationAffected, constants.AuditResourceRoleRefundWallet, map[string]any{"balance": shops[i].balanceFrom}, map[string]any{"balance": shops[i].balanceTo}) walletResource.SubjectVisibility = constants.AuditSubjectInternalOnly resources = append(resources, walletResource) shopResource := audit.ShopResource(&shops[i].shop, constants.AuditResourceRelationReference, constants.AuditResourceRoleRefundCommission) shopResource.SubjectVisibility = constants.AuditSubjectInternalOnly resources = append(resources, shopResource) } _, err := s.auditWriter.AppendAndGet(ctx, tx, audit.AppendInput{ EventID: clawbackEventID(refund.ID, "clawback"), ActionCode: constants.AuditActionRefundClawbackCommission, Summary: "生成退款佣金回溯明细", ScopeType: constants.AuditScopePlatform, Result: constants.AuditResultSuccess, CorrelationID: refund.RefundNo, Metadata: map[string]any{ "order_id": refund.OrderID, "clawback_record_count": len(records), "clawback_total_amount": totalClawback, "clawback_required": true, }, Resources: resources, }) if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "写入退款佣金回溯审计失败") } return nil } // sortUintAsc 就地对店铺 ID 升序排序,保证多店铺加锁顺序稳定。 func sortUintAsc(values []uint) { slices.Sort(values) }