package postgres import ( "context" "time" "gorm.io/gorm" "gorm.io/gorm/clause" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/pkg/errors" ) // ReleaseUnsettledForRequestsInTx 按申请释放未结算的提现审批尝试冻结事实。 // // 语义:对每个申请,按 `released_at IS NULL` 条件更新写入 released_at,并返回各申请 // 本次实际释放的金额合计(金额取 attempt.amount 稳定冻结事实)。已释放的尝试不会被重复释放, // 因此返回值对该申请是幂等的:重复调用得到空映射或 0。 // // 该原语只写「尝试已释放」事实,不调整钱包余额;调用方必须按返回金额在同一事务内解冻钱包, // 使尝试事实与钱包变动保持一致。终态消费者、重提与退款佣金回扣共用本原语, // 避免出现「尝试声称未结算而钱包已无冻结」的矛盾事实。 // // 加锁顺序:本函数只负责尝试行(按 ID 升序 FOR UPDATE)。调用方必须先锁申请行, // 再进入本函数锁尝试行,最后才锁钱包行,即全库统一的「申请行 → 尝试行 → 钱包行」。 // 三条路径均遵守该顺序:提现终态消费者(withdrawal_approval.go)、重提与释放接缝 // (withdrawal.go:releaseUnsettledForRequests)、退款佣金回扣(refund/service.go: // collectPendingWithdrawalRejects 在钱包加锁前收集)。 func ReleaseUnsettledForRequestsInTx( ctx context.Context, tx *gorm.DB, requestIDs []uint, now time.Time, ) (map[uint]int64, error) { released := make(map[uint]int64, len(requestIDs)) if tx == nil || len(requestIDs) == 0 { return released, nil } var attempts []model.CommissionWithdrawalRequestAttempt if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}). Where("request_id IN ? AND released_at IS NULL", requestIDs). Order("id ASC").Find(&attempts).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询未结算提现审批尝试失败") } for index := range attempts { attempt := &attempts[index] // 行已由上面的 FOR UPDATE 锁定且筛选条件为 released_at IS NULL, // 不存在并发改写,故条件更新必然命中,无需再判 RowsAffected。 if err := tx.WithContext(ctx).Model(&model.CommissionWithdrawalRequestAttempt{}). Where("id = ? AND released_at IS NULL", attempt.ID). Update("released_at", now).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "标记提现审批尝试已释放失败") } released[attempt.RequestID] += attempt.Amount } return released, nil } // CountRequestsWithAttemptsInTx 返回给定申请中「已存在审批尝试记录」的申请 ID 集合。 // 用于区分接入企业微信审批的申请与从未关联审批实例的存量申请。 func CountRequestsWithAttemptsInTx(ctx context.Context, tx *gorm.DB, requestIDs []uint) (map[uint]struct{}, error) { result := make(map[uint]struct{}, len(requestIDs)) if tx == nil || len(requestIDs) == 0 { return result, nil } var found []uint if err := tx.WithContext(ctx).Model(&model.CommissionWithdrawalRequestAttempt{}). Where("request_id IN ?", requestIDs). Distinct().Pluck("request_id", &found).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询提现审批尝试记录归属失败") } for _, requestID := range found { result[requestID] = struct{}{} } return result, nil }