- 新增 000070 数据库迁移:支持 chat_conversations、wallet_accounts、wallet_ledger、withdrawal_requests 按 distribution_site_id 隔离 - 消息中心:会话列表、未读数统计、站点专属客服及订单群聊与主站实现双向完全隔离 - 资金账户与提现:独立按 (user_id, distribution_site_id) 建账,分销订单自动结算至对应分站钱包,提现与流水相互独立 - 前台界面:Axios 自动透传 X-Distribution-Site-Code,钱包、提现与消息页面呈现分站专属标牌与独立说明
241 lines
6.6 KiB
Go
241 lines
6.6 KiB
Go
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
|
|
})
|
|
}
|
|
|
|
// 管理员查询提现列表
|