package withdrawal import ( "context" "errors" "strings" "hfb_sys/backend/internal/model" "hfb_sys/backend/internal/modules/wallet" "gorm.io/gorm" "gorm.io/gorm/clause" ) func (r *Repository) resolveSiteID(ctx context.Context, siteCode string) (uint64, error) { siteCode = strings.TrimSpace(siteCode) if siteCode == "" { return 0, nil } var site model.DistributionSite if err := r.db.WithContext(ctx).Where("site_code = ? AND status = 'active'", siteCode).First(&site).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return 0, errors.New("distribution site unavailable") } return 0, err } return site.ID, nil } // 用户创建提现申请 func (r *Repository) Create(ctx context.Context, userID uint64, req CreateWithdrawalRequest, siteCode ...string) (*WithdrawalDTO, error) { db := r.db.WithContext(ctx) // 验证收款账号 var paymentAccount model.UserPaymentAccount if err := db.Where("id = ? AND user_id = ? AND status = ?", req.PaymentAccountID, userID, "active"). First(&paymentAccount).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("payment account not found") } return nil, err } var code string if len(siteCode) > 0 { code = siteCode[0] } siteID, err := r.resolveSiteID(ctx, code) if err != nil { return nil, err } // 计算手续费和实际到账金额(分) feeCent := int64(float64(req.AmountCent) * WithdrawalFeeRate) actualAmountCent := req.AmountCent - feeCent // 生成提现单号 withdrawNo, err := generateWithdrawNo() if err != nil { return nil, err } // 解密账号(用于存储快照) decryptedNo, err := r.decryptAccountNo(paymentAccount.AccountNo) if err != nil { decryptedNo = paymentAccount.AccountNo } withdrawal := model.WithdrawalRequest{ WithdrawNo: withdrawNo, UserID: userID, DistributionSiteID: siteID, AmountCent: req.AmountCent, FeeCent: feeCent, ActualAmountCent: actualAmountCent, PaymentAccountID: &req.PaymentAccountID, AccountType: paymentAccount.AccountType, AccountName: paymentAccount.AccountName, AccountNo: maskAccountNo(decryptedNo, paymentAccount.AccountType), BankName: paymentAccount.BankName, BankBranch: paymentAccount.BankBranch, Status: "pending", } // 事务处理 err = db.Transaction(func(tx *gorm.DB) error { // 创建提现申请 if err := tx.Create(&withdrawal).Error; err != nil { return err } // 冻结用户余额 if err := wallet.AppendEntries(tx, wallet.Entry{ UserID: userID, DistributionSiteID: siteID, Direction: "out", AmountCent: req.AmountCent, BalanceType: "available", BizType: "withdraw_freeze", BizNo: withdrawNo, Remark: "提现冻结", }, wallet.Entry{ UserID: userID, DistributionSiteID: siteID, Direction: "in", AmountCent: req.AmountCent, BalanceType: "frozen", BizType: "withdraw_freeze", BizNo: withdrawNo, Remark: "提现冻结", }); err != nil { return err } return nil }) if err != nil { return nil, err } return r.FindByID(ctx, userID, withdrawal.ID) } // 用户查询提现列表 func (r *Repository) List(ctx context.Context, userID uint64, page, pageSize int, siteCode ...string) (*PaginatedResult, error) { db := r.db.WithContext(ctx) var code string if len(siteCode) > 0 { code = siteCode[0] } siteID, err := r.resolveSiteID(ctx, code) if err != nil { return nil, err } countDB := db.Model(&model.WithdrawalRequest{}).Where("user_id = ?", userID) dataDB := db.Where("user_id = ?", userID) if siteID > 0 { countDB = countDB.Where("distribution_site_id = ?", siteID) dataDB = dataDB.Where("distribution_site_id = ?", siteID) } else { countDB = countDB.Where("distribution_site_id = 0 OR distribution_site_id IS NULL") dataDB = dataDB.Where("distribution_site_id = 0 OR distribution_site_id IS NULL") } var total int64 if err := countDB.Count(&total).Error; err != nil { return nil, err } offset := (page - 1) * pageSize var withdrawals []model.WithdrawalRequest if err := dataDB.Order("created_at DESC"). Offset(offset).Limit(pageSize). Find(&withdrawals).Error; err != nil { return nil, err } items := make([]WithdrawalDTO, 0, len(withdrawals)) for _, w := range withdrawals { items = append(items, toDTO(w)) } return &PaginatedResult{ Items: items, Total: total, Page: page, PageSize: pageSize, }, nil } // 用户查询提现详情 // 用户查询提现详情 func (r *Repository) FindByID(ctx context.Context, userID, id uint64) (*WithdrawalDTO, error) { var withdrawal model.WithdrawalRequest if err := r.db.WithContext(ctx).Where("id = ? AND user_id = ?", id, userID). First(&withdrawal).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrWithdrawalNotFound } return nil, err } dto := toDTO(withdrawal) return &dto, nil } // 用户取消提现 // 用户取消提现 func (r *Repository) Cancel(ctx context.Context, userID, id uint64) error { return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var withdrawal model.WithdrawalRequest if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("id = ? AND user_id = ?", id, userID). First(&withdrawal).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrWithdrawalNotFound } return err } // 事务内加行锁后重新校验,只有待审核状态可以取消,避免与管理员审核并发重复解冻 if withdrawal.Status != "pending" { return ErrWithdrawalLocked } // 更新状态 if err := tx.Model(&withdrawal).Update("status", "cancelled").Error; err != nil { return err } // 解冻余额 if err := wallet.AppendEntries(tx, wallet.Entry{ UserID: userID, DistributionSiteID: withdrawal.DistributionSiteID, Direction: "out", AmountCent: withdrawal.AmountCent, BalanceType: "frozen", BizType: "withdraw_cancel", BizNo: withdrawal.WithdrawNo, Remark: "用户取消提现", }, wallet.Entry{ UserID: userID, DistributionSiteID: withdrawal.DistributionSiteID, Direction: "in", AmountCent: withdrawal.AmountCent, BalanceType: "available", BizType: "withdraw_cancel", BizNo: withdrawal.WithdrawNo, Remark: "用户取消提现", }); err != nil { return err } return nil }) } // 管理员查询提现列表