diff --git a/backend/internal/modules/withdrawal/admin.go b/backend/internal/modules/withdrawal/admin.go new file mode 100644 index 0000000..5b59fc1 --- /dev/null +++ b/backend/internal/modules/withdrawal/admin.go @@ -0,0 +1,193 @@ +package withdrawal + +import ( + "context" + "errors" + "fmt" + "time" + + "hfb_sys/backend/internal/model" + "hfb_sys/backend/internal/modules/wallet" + + "gorm.io/gorm" +) + +// 管理员查询提现列表 +func (r *Repository) AdminList(ctx context.Context, query AdminListQuery) (*AdminPaginatedResult, error) { + db := r.db.WithContext(ctx).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(ctx, 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(ctx context.Context, id uint64) (*WithdrawalDetailDTO, error) { + var withdrawal model.WithdrawalRequest + if err := r.db.WithContext(ctx).First(&withdrawal, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrWithdrawalNotFound + } + return nil, err + } + return r.toDetailDTO(ctx, withdrawal) +} + +// 管理员审核提现 + +// 管理员审核提现 +func (r *Repository) Review(ctx context.Context, adminID, id uint64, req ReviewWithdrawalRequest) (*WithdrawalDetailDTO, error) { + db := r.db.WithContext(ctx) + var withdrawal model.WithdrawalRequest + if err := 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 := 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", + AmountCent: withdrawal.AmountCent, + BalanceType: "frozen", + BizType: "withdraw_reject", + BizNo: withdrawal.WithdrawNo, + Remark: fmt.Sprintf("提现被拒绝: %s", req.Remark), + }, wallet.Entry{ + UserID: withdrawal.UserID, + Direction: "in", + AmountCent: withdrawal.AmountCent, + 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(ctx, id) +} + +// 管理员确认打款 + +// 管理员确认打款 +func (r *Repository) ConfirmPayment(ctx context.Context, adminID, id uint64, req ConfirmPaymentRequest) (*WithdrawalDetailDTO, error) { + db := r.db.WithContext(ctx) + var withdrawal model.WithdrawalRequest + if err := 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 := 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", + AmountCent: withdrawal.AmountCent, + BalanceType: "frozen", + BizType: "withdraw_complete", + BizNo: withdrawal.WithdrawNo, + Remark: "提现完成", + }); err != nil { + return err + } + + return nil + }) + + if err != nil { + return nil, err + } + + return r.AdminFindByID(ctx, id) +} + +// 转换为用户DTO diff --git a/backend/internal/modules/withdrawal/dto.go b/backend/internal/modules/withdrawal/dto.go index 9a2857f..42a67ea 100644 --- a/backend/internal/modules/withdrawal/dto.go +++ b/backend/internal/modules/withdrawal/dto.go @@ -6,22 +6,22 @@ import ( // 用户端 DTO type WithdrawalDTO struct { - ID uint64 `json:"id"` - WithdrawNo string `json:"withdraw_no"` - UserID uint64 `json:"user_id"` - AmountCent int64 `json:"amount_cent"` - FeeCent int64 `json:"fee_cent"` - ActualAmountCent int64 `json:"actual_amount_cent"` - AccountType string `json:"account_type"` - AccountName string `json:"account_name"` - AccountNo string `json:"account_no"` // 脱敏 - BankName string `json:"bank_name"` - Status string `json:"status"` - ReviewRemark string `json:"review_remark"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - ReviewedAt *time.Time `json:"reviewed_at"` - PaidAt *time.Time `json:"paid_at"` + ID uint64 `json:"id"` + WithdrawNo string `json:"withdraw_no"` + UserID uint64 `json:"user_id"` + AmountCent int64 `json:"amount_cent"` + FeeCent int64 `json:"fee_cent"` + ActualAmountCent int64 `json:"actual_amount_cent"` + AccountType string `json:"account_type"` + AccountName string `json:"account_name"` + AccountNo string `json:"account_no"` // 脱敏 + BankName string `json:"bank_name"` + Status string `json:"status"` + ReviewRemark string `json:"review_remark"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + ReviewedAt *time.Time `json:"reviewed_at"` + PaidAt *time.Time `json:"paid_at"` } // 管理员端详细 DTO diff --git a/backend/internal/modules/withdrawal/helpers.go b/backend/internal/modules/withdrawal/helpers.go new file mode 100644 index 0000000..bf2465d --- /dev/null +++ b/backend/internal/modules/withdrawal/helpers.go @@ -0,0 +1,50 @@ +package withdrawal + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "time" +) + +// 生成提现单号 +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 模块的解密) + +// 简化的解密函数(实际应该调用 paymentaccount 模块的解密) +func (r *Repository) decryptAccountNo(encrypted string) (string, error) { + // 这里应该调用 paymentaccount 包的解密函数 + // 为了简化,直接返回(实际使用时需要正确实现) + return encrypted, nil +} diff --git a/backend/internal/modules/withdrawal/presenter.go b/backend/internal/modules/withdrawal/presenter.go new file mode 100644 index 0000000..92cb21a --- /dev/null +++ b/backend/internal/modules/withdrawal/presenter.go @@ -0,0 +1,109 @@ +package withdrawal + +import ( + "context" + "encoding/json" + + "hfb_sys/backend/internal/model" + "hfb_sys/backend/pkg/crypto" +) + +// 转换为用户DTO +func toDTO(w model.WithdrawalRequest) WithdrawalDTO { + return WithdrawalDTO{ + ID: w.ID, + WithdrawNo: w.WithdrawNo, + UserID: w.UserID, + AmountCent: w.AmountCent, + FeeCent: w.FeeCent, + ActualAmountCent: w.ActualAmountCent, + 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 + +// 转换为管理员详细DTO +func (r *Repository) toDetailDTO(ctx context.Context, w model.WithdrawalRequest) (*WithdrawalDetailDTO, error) { + db := r.db.WithContext(ctx) + // 查询用户信息 + var user model.User + db.Select("nickname, phone").First(&user, w.UserID) + + // 查询审核人信息 + var reviewedByName string + if w.ReviewedBy != nil { + var admin model.AdminUser + if err := 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 := 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 := 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, + AmountCent: w.AmountCent, + FeeCent: w.FeeCent, + ActualAmountCent: w.ActualAmountCent, + 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 +} + +// 生成提现单号 diff --git a/backend/internal/modules/withdrawal/repository.go b/backend/internal/modules/withdrawal/repository.go index 91e4250..b74f54f 100644 --- a/backend/internal/modules/withdrawal/repository.go +++ b/backend/internal/modules/withdrawal/repository.go @@ -1,17 +1,7 @@ package withdrawal import ( - "context" - "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" ) @@ -29,483 +19,3 @@ func NewRepository(db *gorm.DB, walletRepo *wallet.Repository) *Repository { } // 用户创建提现申请 -func (r *Repository) Create(ctx context.Context, userID uint64, req CreateWithdrawalRequest) (*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 - } - - // 计算手续费和实际到账金额(分) - 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, - 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, - Direction: "out", - AmountCent: req.AmountCent, - BalanceType: "available", - BizType: "withdraw_freeze", - BizNo: withdrawNo, - Remark: "提现冻结", - }, wallet.Entry{ - UserID: userID, - 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) (*PaginatedResult, error) { - db := r.db.WithContext(ctx) - var total int64 - if err := 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 := 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(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 { - db := r.db.WithContext(ctx) - var withdrawal model.WithdrawalRequest - if err := 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 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", - AmountCent: withdrawal.AmountCent, - BalanceType: "frozen", - BizType: "withdraw_cancel", - BizNo: withdrawal.WithdrawNo, - Remark: "用户取消提现", - }, wallet.Entry{ - UserID: userID, - Direction: "in", - AmountCent: withdrawal.AmountCent, - BalanceType: "available", - BizType: "withdraw_cancel", - BizNo: withdrawal.WithdrawNo, - Remark: "用户取消提现", - }); err != nil { - return err - } - - return nil - }) -} - -// 管理员查询提现列表 -func (r *Repository) AdminList(ctx context.Context, query AdminListQuery) (*AdminPaginatedResult, error) { - db := r.db.WithContext(ctx).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(ctx, 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(ctx context.Context, id uint64) (*WithdrawalDetailDTO, error) { - var withdrawal model.WithdrawalRequest - if err := r.db.WithContext(ctx).First(&withdrawal, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, ErrWithdrawalNotFound - } - return nil, err - } - return r.toDetailDTO(ctx, withdrawal) -} - -// 管理员审核提现 -func (r *Repository) Review(ctx context.Context, adminID, id uint64, req ReviewWithdrawalRequest) (*WithdrawalDetailDTO, error) { - db := r.db.WithContext(ctx) - var withdrawal model.WithdrawalRequest - if err := 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 := 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", - AmountCent: withdrawal.AmountCent, - BalanceType: "frozen", - BizType: "withdraw_reject", - BizNo: withdrawal.WithdrawNo, - Remark: fmt.Sprintf("提现被拒绝: %s", req.Remark), - }, wallet.Entry{ - UserID: withdrawal.UserID, - Direction: "in", - AmountCent: withdrawal.AmountCent, - 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(ctx, id) -} - -// 管理员确认打款 -func (r *Repository) ConfirmPayment(ctx context.Context, adminID, id uint64, req ConfirmPaymentRequest) (*WithdrawalDetailDTO, error) { - db := r.db.WithContext(ctx) - var withdrawal model.WithdrawalRequest - if err := 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 := 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", - AmountCent: withdrawal.AmountCent, - BalanceType: "frozen", - BizType: "withdraw_complete", - BizNo: withdrawal.WithdrawNo, - Remark: "提现完成", - }); err != nil { - return err - } - - return nil - }) - - if err != nil { - return nil, err - } - - return r.AdminFindByID(ctx, id) -} - -// 转换为用户DTO -func toDTO(w model.WithdrawalRequest) WithdrawalDTO { - return WithdrawalDTO{ - ID: w.ID, - WithdrawNo: w.WithdrawNo, - UserID: w.UserID, - AmountCent: w.AmountCent, - FeeCent: w.FeeCent, - ActualAmountCent: w.ActualAmountCent, - 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(ctx context.Context, w model.WithdrawalRequest) (*WithdrawalDetailDTO, error) { - db := r.db.WithContext(ctx) - // 查询用户信息 - var user model.User - db.Select("nickname, phone").First(&user, w.UserID) - - // 查询审核人信息 - var reviewedByName string - if w.ReviewedBy != nil { - var admin model.AdminUser - if err := 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 := 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 := 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, - AmountCent: w.AmountCent, - FeeCent: w.FeeCent, - ActualAmountCent: w.ActualAmountCent, - 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 -} diff --git a/backend/internal/modules/withdrawal/user.go b/backend/internal/modules/withdrawal/user.go new file mode 100644 index 0000000..f346b76 --- /dev/null +++ b/backend/internal/modules/withdrawal/user.go @@ -0,0 +1,195 @@ +package withdrawal + +import ( + "context" + "errors" + + "hfb_sys/backend/internal/model" + "hfb_sys/backend/internal/modules/wallet" + + "gorm.io/gorm" +) + +// 用户创建提现申请 +func (r *Repository) Create(ctx context.Context, userID uint64, req CreateWithdrawalRequest) (*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 + } + + // 计算手续费和实际到账金额(分) + 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, + 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, + Direction: "out", + AmountCent: req.AmountCent, + BalanceType: "available", + BizType: "withdraw_freeze", + BizNo: withdrawNo, + Remark: "提现冻结", + }, wallet.Entry{ + UserID: userID, + 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) (*PaginatedResult, error) { + db := r.db.WithContext(ctx) + var total int64 + if err := 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 := 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(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 { + db := r.db.WithContext(ctx) + var withdrawal model.WithdrawalRequest + if err := 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 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", + AmountCent: withdrawal.AmountCent, + BalanceType: "frozen", + BizType: "withdraw_cancel", + BizNo: withdrawal.WithdrawNo, + Remark: "用户取消提现", + }, wallet.Entry{ + UserID: userID, + Direction: "in", + AmountCent: withdrawal.AmountCent, + BalanceType: "available", + BizType: "withdraw_cancel", + BizNo: withdrawal.WithdrawNo, + Remark: "用户取消提现", + }); err != nil { + return err + } + + return nil + }) +} + +// 管理员查询提现列表