package withdrawal import ( "crypto/rand" "encoding/hex" "encoding/json" "errors" "fmt" "time" "hfb_sys/backend/internal/model" "hfb_sys/backend/internal/modules/wallet" "hfb_sys/backend/pkg/crypto" "gorm.io/gorm" ) type Repository struct { db *gorm.DB walletRepo *wallet.Repository } func NewRepository(db *gorm.DB, walletRepo *wallet.Repository) *Repository { return &Repository{ db: db, walletRepo: walletRepo, } } // 用户创建提现申请 func (r *Repository) Create(userID uint64, req CreateWithdrawalRequest) (*WithdrawalDTO, error) { // 验证收款账号 var paymentAccount model.UserPaymentAccount if err := r.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 } // 计算手续费和实际到账金额 fee := req.Amount * WithdrawalFeeRate actualAmount := req.Amount - fee // 生成提现单号 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, Amount: req.Amount, Fee: fee, ActualAmount: actualAmount, PaymentAccountID: &req.PaymentAccountID, AccountType: paymentAccount.AccountType, AccountName: paymentAccount.AccountName, AccountNo: maskAccountNo(decryptedNo, paymentAccount.AccountType), BankName: paymentAccount.BankName, BankBranch: paymentAccount.BankBranch, Status: "pending", } // 事务处理 err = r.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, Direction: "out", Amount: req.Amount, BalanceType: "available", BizType: "withdraw_freeze", BizNo: withdrawNo, Remark: "提现冻结", }, wallet.Entry{ UserID: userID, Direction: "in", Amount: req.Amount, BalanceType: "frozen", BizType: "withdraw_freeze", BizNo: withdrawNo, Remark: "提现冻结", }); err != nil { return err } return nil }) if err != nil { return nil, err } return r.FindByID(userID, withdrawal.ID) } // 用户查询提现列表 func (r *Repository) List(userID uint64, page, pageSize int) (*PaginatedResult, error) { var total int64 if err := r.db.Model(&model.WithdrawalRequest{}). Where("user_id = ?", userID). Count(&total).Error; err != nil { return nil, err } offset := (page - 1) * pageSize var withdrawals []model.WithdrawalRequest if err := r.db.Where("user_id = ?", userID). 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(userID, id uint64) (*WithdrawalDTO, error) { var withdrawal model.WithdrawalRequest if err := r.db.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(userID, id uint64) error { var withdrawal model.WithdrawalRequest if err := r.db.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 } return r.db.Transaction(func(tx *gorm.DB) error { // 更新状态 if err := tx.Model(&withdrawal).Update("status", "cancelled").Error; err != nil { return err } // 解冻余额 if err := wallet.AppendEntries(tx, wallet.Entry{ UserID: userID, Direction: "out", Amount: withdrawal.Amount, BalanceType: "frozen", BizType: "withdraw_cancel", BizNo: withdrawal.WithdrawNo, Remark: "用户取消提现", }, wallet.Entry{ UserID: userID, Direction: "in", Amount: withdrawal.Amount, BalanceType: "available", BizType: "withdraw_cancel", BizNo: withdrawal.WithdrawNo, Remark: "用户取消提现", }); err != nil { return err } return nil }) } // 管理员查询提现列表 func (r *Repository) AdminList(query AdminListQuery) (*AdminPaginatedResult, error) { db := r.db.Model(&model.WithdrawalRequest{}) if query.Status != "" { db = db.Where("status = ?", query.Status) } if query.UserID > 0 { db = db.Where("user_id = ?", query.UserID) } var total int64 if err := db.Count(&total).Error; err != nil { return nil, err } offset := (query.Page - 1) * query.Size var withdrawals []model.WithdrawalRequest if err := db.Order("created_at DESC"). Offset(offset).Limit(query.Size). Find(&withdrawals).Error; err != nil { return nil, err } items := make([]WithdrawalDetailDTO, 0, len(withdrawals)) for _, w := range withdrawals { dto, err := r.toDetailDTO(w) if err != nil { continue } items = append(items, *dto) } return &AdminPaginatedResult{ Items: items, Total: total, Page: query.Page, PageSize: query.Size, }, nil } // 管理员查询提现详情 func (r *Repository) AdminFindByID(id uint64) (*WithdrawalDetailDTO, error) { var withdrawal model.WithdrawalRequest if err := r.db.First(&withdrawal, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrWithdrawalNotFound } return nil, err } return r.toDetailDTO(withdrawal) } // 管理员审核提现 func (r *Repository) Review(adminID, id uint64, req ReviewWithdrawalRequest) (*WithdrawalDetailDTO, error) { var withdrawal model.WithdrawalRequest if err := r.db.First(&withdrawal, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrWithdrawalNotFound } return nil, err } // 只有待审核状态可以审核 if withdrawal.Status != "pending" { return nil, ErrWithdrawalLocked } now := time.Now() if req.Approved { // 审核通过,进入处理中状态 withdrawal.Status = "processing" } else { // 审核拒绝,解冻余额 withdrawal.Status = "rejected" } withdrawal.ReviewedBy = &adminID withdrawal.ReviewedAt = &now withdrawal.ReviewRemark = req.Remark err := r.db.Transaction(func(tx *gorm.DB) error { if err := tx.Save(&withdrawal).Error; err != nil { return err } // 如果拒绝,解冻余额 if !req.Approved { if err := wallet.AppendEntries(tx, wallet.Entry{ UserID: withdrawal.UserID, Direction: "out", Amount: withdrawal.Amount, BalanceType: "frozen", BizType: "withdraw_reject", BizNo: withdrawal.WithdrawNo, Remark: fmt.Sprintf("提现被拒绝: %s", req.Remark), }, wallet.Entry{ UserID: withdrawal.UserID, Direction: "in", Amount: withdrawal.Amount, BalanceType: "available", BizType: "withdraw_reject", BizNo: withdrawal.WithdrawNo, Remark: fmt.Sprintf("提现被拒绝: %s", req.Remark), }); err != nil { return err } } return nil }) if err != nil { return nil, err } return r.AdminFindByID(id) } // 管理员确认打款 func (r *Repository) ConfirmPayment(adminID, id uint64, req ConfirmPaymentRequest) (*WithdrawalDetailDTO, error) { var withdrawal model.WithdrawalRequest if err := r.db.First(&withdrawal, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrWithdrawalNotFound } return nil, err } // 只有处理中状态可以确认打款 if withdrawal.Status != "processing" { return nil, ErrWithdrawalLocked } now := time.Now() withdrawal.Status = "completed" withdrawal.PaidBy = &adminID withdrawal.PaidAt = &now withdrawal.PaymentProofURL = req.PaymentProofURL withdrawal.PaymentRemark = req.Remark err := r.db.Transaction(func(tx *gorm.DB) error { if err := tx.Save(&withdrawal).Error; err != nil { return err } // 扣除冻结余额 if err := wallet.AppendEntries(tx, wallet.Entry{ UserID: withdrawal.UserID, Direction: "out", Amount: withdrawal.Amount, BalanceType: "frozen", BizType: "withdraw_complete", BizNo: withdrawal.WithdrawNo, Remark: "提现完成", }); err != nil { return err } return nil }) if err != nil { return nil, err } return r.AdminFindByID(id) } // 转换为用户DTO func toDTO(w model.WithdrawalRequest) WithdrawalDTO { return WithdrawalDTO{ ID: w.ID, WithdrawNo: w.WithdrawNo, UserID: w.UserID, Amount: w.Amount, Fee: w.Fee, ActualAmount: w.ActualAmount, AccountType: w.AccountType, AccountName: w.AccountName, AccountNo: w.AccountNo, BankName: w.BankName, Status: w.Status, ReviewRemark: w.ReviewRemark, CreatedAt: w.CreatedAt, UpdatedAt: w.UpdatedAt, ReviewedAt: w.ReviewedAt, PaidAt: w.PaidAt, } } // 转换为管理员详细DTO func (r *Repository) toDetailDTO(w model.WithdrawalRequest) (*WithdrawalDetailDTO, error) { // 查询用户信息 var user model.User r.db.Select("nickname, phone").First(&user, w.UserID) // 查询审核人信息 var reviewedByName string if w.ReviewedBy != nil { var admin model.AdminUser if err := r.db.Select("nickname").First(&admin, *w.ReviewedBy).Error; err == nil { reviewedByName = admin.Nickname } } // 查询打款人信息 var paidByName string if w.PaidBy != nil { var admin model.AdminUser if err := r.db.Select("nickname").First(&admin, *w.PaidBy).Error; err == nil { paidByName = admin.Nickname } } // 获取完整账号和收款二维码(管理员可见) fullAccountNo := w.AccountNo var certificateURLs []string if w.PaymentAccountID != nil { var paymentAccount model.UserPaymentAccount if err := r.db.First(&paymentAccount, *w.PaymentAccountID).Error; err == nil { // 解密账号 decrypted, err := crypto.Decrypt(paymentAccount.AccountNo) if err == nil { fullAccountNo = decrypted } // 解析收款二维码 if paymentAccount.CertificateURLs != nil { json.Unmarshal(paymentAccount.CertificateURLs, &certificateURLs) } } } return &WithdrawalDetailDTO{ ID: w.ID, WithdrawNo: w.WithdrawNo, UserID: w.UserID, UserNickname: user.Nickname, UserPhone: user.Phone, Amount: w.Amount, Fee: w.Fee, ActualAmount: w.ActualAmount, PaymentAccountID: w.PaymentAccountID, AccountType: w.AccountType, AccountName: w.AccountName, AccountNo: fullAccountNo, BankName: w.BankName, BankBranch: w.BankBranch, CertificateURLs: certificateURLs, Status: w.Status, ReviewedBy: w.ReviewedBy, ReviewedByName: reviewedByName, ReviewedAt: w.ReviewedAt, ReviewRemark: w.ReviewRemark, PaidBy: w.PaidBy, PaidByName: paidByName, PaidAt: w.PaidAt, PaymentProofURL: w.PaymentProofURL, PaymentRemark: w.PaymentRemark, CreatedAt: w.CreatedAt, UpdatedAt: w.UpdatedAt, }, nil } // 生成提现单号 func generateWithdrawNo() (string, error) { buf := make([]byte, 8) if _, err := rand.Read(buf); err != nil { return "", err } return fmt.Sprintf("WD%d%s", time.Now().Unix(), hex.EncodeToString(buf)[:8]), nil } // 账号脱敏 func maskAccountNo(accountNo, accountType string) string { length := len(accountNo) if length <= 4 { return accountNo } switch accountType { case "alipay", "wechat": if length == 11 { return accountNo[:3] + "****" + accountNo[7:] } return accountNo[:2] + "****" + accountNo[length-2:] case "bank": if length > 8 { return accountNo[:4] + "****" + accountNo[length-4:] } return accountNo[:2] + "****" + accountNo[length-2:] } return accountNo } // 简化的解密函数(实际应该调用 paymentaccount 模块的解密) func (r *Repository) decryptAccountNo(encrypted string) (string, error) { // 这里应该调用 paymentaccount 包的解密函数 // 为了简化,直接返回(实际使用时需要正确实现) return encrypted, nil }