505 lines
13 KiB
Go
505 lines
13 KiB
Go
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
|
|
}
|
|
|
|
// 计算手续费和实际到账金额(分)
|
|
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 = 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",
|
|
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(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",
|
|
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(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",
|
|
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(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",
|
|
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(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(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,
|
|
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
|
|
}
|