后端提现增加
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
package paymentaccount
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type PaymentAccountDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
UserID uint64 `json:"user_id"`
|
||||
AccountType string `json:"account_type"`
|
||||
AccountName string `json:"account_name"`
|
||||
AccountNo string `json:"account_no"` // 脱敏显示
|
||||
BankName string `json:"bank_name"`
|
||||
BankBranch string `json:"bank_branch"`
|
||||
CertificateURLs []string `json:"certificate_urls"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CreatePaymentAccountRequest struct {
|
||||
AccountType string `json:"account_type" binding:"required,oneof=alipay wechat bank"`
|
||||
AccountName string `json:"account_name" binding:"required"`
|
||||
AccountNo string `json:"account_no" binding:"required"`
|
||||
BankName string `json:"bank_name"`
|
||||
BankBranch string `json:"bank_branch"`
|
||||
CertificateURLs []string `json:"certificate_urls"`
|
||||
}
|
||||
|
||||
type UpdatePaymentAccountRequest struct {
|
||||
BankBranch string `json:"bank_branch"`
|
||||
CertificateURLs []string `json:"certificate_urls"`
|
||||
IsDefault *bool `json:"is_default"`
|
||||
}
|
||||
|
||||
type PaginatedResult struct {
|
||||
Items []PaymentAccountDTO `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package paymentaccount
|
||||
|
||||
import (
|
||||
"hfb_sys/backend/pkg/response"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
service *Service
|
||||
}
|
||||
|
||||
func NewHandler(service *Service) *Handler {
|
||||
return &Handler{service: service}
|
||||
}
|
||||
|
||||
func (h *Handler) List(c *gin.Context) {
|
||||
userID, ok := currentUserID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
result, err := h.service.List(userID, page, pageSize)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
func (h *Handler) FindByID(c *gin.Context) {
|
||||
userID, ok := currentUserID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
return
|
||||
}
|
||||
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
account, err := h.service.FindByID(userID, id)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, account)
|
||||
}
|
||||
|
||||
func (h *Handler) Create(c *gin.Context) {
|
||||
userID, ok := currentUserID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
return
|
||||
}
|
||||
|
||||
var req CreatePaymentAccountRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "请求参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
account, err := h.service.Create(userID, req)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, account)
|
||||
}
|
||||
|
||||
func (h *Handler) Update(c *gin.Context) {
|
||||
userID, ok := currentUserID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
return
|
||||
}
|
||||
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdatePaymentAccountRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "请求参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
account, err := h.service.Update(userID, id, req)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, account)
|
||||
}
|
||||
|
||||
func (h *Handler) Delete(c *gin.Context) {
|
||||
userID, ok := currentUserID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
return
|
||||
}
|
||||
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.service.Delete(userID, id); err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, gin.H{"deleted": true})
|
||||
}
|
||||
|
||||
func (h *Handler) SetDefault(c *gin.Context) {
|
||||
userID, ok := currentUserID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
return
|
||||
}
|
||||
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.service.SetDefault(userID, id); err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, gin.H{"updated": true})
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
func currentUserID(c *gin.Context) (uint64, bool) {
|
||||
val, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
return 0, false
|
||||
}
|
||||
userID, ok := val.(uint64)
|
||||
return userID, ok
|
||||
}
|
||||
|
||||
func parseID(c *gin.Context) (uint64, bool) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "无效的ID")
|
||||
return 0, false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
|
||||
func writeError(c *gin.Context, err error) {
|
||||
switch err {
|
||||
case ErrAccountNotFound:
|
||||
response.NotFound(c, "收款账号不存在")
|
||||
case ErrAccountNameMismatch:
|
||||
response.BadRequest(c, "账户名必须与实名认证姓名一致")
|
||||
case ErrRealnameRequired:
|
||||
response.BadRequest(c, "请先完成实名认证")
|
||||
case ErrAccountLimit:
|
||||
response.BadRequest(c, "收款账号数量已达上限(最多5个)")
|
||||
case ErrCannotDeleteDefault:
|
||||
response.BadRequest(c, "无法删除默认账号")
|
||||
default:
|
||||
response.InternalServerError(c, "操作失败")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
package paymentaccount
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 加密密钥(生产环境应从配置文件读取)
|
||||
const encryptionKey = "your-32-byte-secret-key-here!!" // 32字节
|
||||
|
||||
type Repository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewRepository(db *gorm.DB) *Repository {
|
||||
return &Repository{db: db}
|
||||
}
|
||||
|
||||
func (r *Repository) List(userID uint64, page, pageSize int) (*PaginatedResult, error) {
|
||||
var total int64
|
||||
if err := r.db.Model(&model.UserPaymentAccount{}).
|
||||
Where("user_id = ? AND status = ?", userID, "active").
|
||||
Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
var accounts []model.UserPaymentAccount
|
||||
if err := r.db.Where("user_id = ? AND status = ?", userID, "active").
|
||||
Order("is_default DESC, created_at DESC").
|
||||
Offset(offset).Limit(pageSize).
|
||||
Find(&accounts).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]PaymentAccountDTO, 0, len(accounts))
|
||||
for _, acc := range accounts {
|
||||
dto, err := r.toDTO(acc)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, *dto)
|
||||
}
|
||||
|
||||
return &PaginatedResult{
|
||||
Items: items,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) FindByID(userID, id uint64) (*PaymentAccountDTO, error) {
|
||||
var account model.UserPaymentAccount
|
||||
if err := r.db.Where("id = ? AND user_id = ? AND status = ?", id, userID, "active").
|
||||
First(&account).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrAccountNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return r.toDTO(account)
|
||||
}
|
||||
|
||||
func (r *Repository) Create(userID uint64, req CreatePaymentAccountRequest) (*PaymentAccountDTO, error) {
|
||||
// 加密账号
|
||||
encryptedNo, err := encrypt(req.AccountNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 处理凭证URLs
|
||||
var certURLs datatypes.JSON
|
||||
if len(req.CertificateURLs) > 0 {
|
||||
certURLs, _ = json.Marshal(req.CertificateURLs)
|
||||
}
|
||||
|
||||
// 如果是第一个账号,自动设为默认
|
||||
isDefault := false
|
||||
var count int64
|
||||
r.db.Model(&model.UserPaymentAccount{}).Where("user_id = ? AND status = ?", userID, "active").Count(&count)
|
||||
if count == 0 {
|
||||
isDefault = true
|
||||
}
|
||||
|
||||
account := model.UserPaymentAccount{
|
||||
UserID: userID,
|
||||
AccountType: req.AccountType,
|
||||
AccountName: req.AccountName,
|
||||
AccountNo: encryptedNo,
|
||||
BankName: req.BankName,
|
||||
BankBranch: req.BankBranch,
|
||||
CertificateURLs: certURLs,
|
||||
IsDefault: isDefault,
|
||||
Status: "active",
|
||||
}
|
||||
|
||||
if err := r.db.Create(&account).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.FindByID(userID, account.ID)
|
||||
}
|
||||
|
||||
func (r *Repository) Update(userID, id uint64, req UpdatePaymentAccountRequest) (*PaymentAccountDTO, error) {
|
||||
var account model.UserPaymentAccount
|
||||
if err := r.db.Where("id = ? AND user_id = ? AND status = ?", id, userID, "active").
|
||||
First(&account).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrAccountNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
|
||||
if req.BankBranch != "" {
|
||||
updates["bank_branch"] = req.BankBranch
|
||||
}
|
||||
|
||||
if len(req.CertificateURLs) > 0 {
|
||||
certURLs, _ := json.Marshal(req.CertificateURLs)
|
||||
updates["certificate_urls"] = certURLs
|
||||
}
|
||||
|
||||
if req.IsDefault != nil && *req.IsDefault {
|
||||
// 先取消其他默认账号
|
||||
r.db.Model(&model.UserPaymentAccount{}).
|
||||
Where("user_id = ? AND id != ?", userID, id).
|
||||
Update("is_default", false)
|
||||
updates["is_default"] = true
|
||||
}
|
||||
|
||||
if len(updates) > 0 {
|
||||
if err := r.db.Model(&account).Updates(updates).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return r.FindByID(userID, id)
|
||||
}
|
||||
|
||||
func (r *Repository) Delete(userID, id uint64) error {
|
||||
var account model.UserPaymentAccount
|
||||
if err := r.db.Where("id = ? AND user_id = ? AND status = ?", id, userID, "active").
|
||||
First(&account).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrAccountNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 软删除
|
||||
return r.db.Model(&account).Update("status", "disabled").Error
|
||||
}
|
||||
|
||||
func (r *Repository) SetDefault(userID, id uint64) error {
|
||||
// 验证账号存在
|
||||
var account model.UserPaymentAccount
|
||||
if err := r.db.Where("id = ? AND user_id = ? AND status = ?", id, userID, "active").
|
||||
First(&account).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrAccountNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 取消其他默认账号
|
||||
if err := tx.Model(&model.UserPaymentAccount{}).
|
||||
Where("user_id = ? AND id != ?", userID, id).
|
||||
Update("is_default", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
// 设置当前为默认
|
||||
return tx.Model(&account).Update("is_default", true).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) CountByUser(userID uint64) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&model.UserPaymentAccount{}).
|
||||
Where("user_id = ? AND status = ?", userID, "active").
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *Repository) ValidateRealname(userID uint64, accountName string) error {
|
||||
var user model.User
|
||||
if err := r.db.First(&user, userID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if user.RealnameStatus != "verified" {
|
||||
return ErrRealnameRequired
|
||||
}
|
||||
|
||||
// 获取实名信息
|
||||
var realname model.UserRealname
|
||||
if err := r.db.Where("user_id = ? AND status = ?", userID, "success").
|
||||
First(&realname).Error; err != nil {
|
||||
return ErrRealnameRequired
|
||||
}
|
||||
|
||||
// 验证姓名匹配(去除空格后比较)
|
||||
if strings.ReplaceAll(realname.MaskedName, " ", "") != strings.ReplaceAll(accountName, " ", "") {
|
||||
return ErrAccountNameMismatch
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) toDTO(account model.UserPaymentAccount) (*PaymentAccountDTO, error) {
|
||||
// 解密账号并脱敏
|
||||
decrypted, err := decrypt(account.AccountNo)
|
||||
if err != nil {
|
||||
decrypted = account.AccountNo // 降级处理
|
||||
}
|
||||
maskedNo := maskAccountNo(decrypted, account.AccountType)
|
||||
|
||||
// 解析凭证URLs
|
||||
var certURLs []string
|
||||
if account.CertificateURLs != nil {
|
||||
json.Unmarshal(account.CertificateURLs, &certURLs)
|
||||
}
|
||||
|
||||
return &PaymentAccountDTO{
|
||||
ID: account.ID,
|
||||
UserID: account.UserID,
|
||||
AccountType: account.AccountType,
|
||||
AccountName: account.AccountName,
|
||||
AccountNo: maskedNo,
|
||||
BankName: account.BankName,
|
||||
BankBranch: account.BankBranch,
|
||||
CertificateURLs: certURLs,
|
||||
IsDefault: account.IsDefault,
|
||||
Status: account.Status,
|
||||
CreatedAt: account.CreatedAt,
|
||||
UpdatedAt: account.UpdatedAt,
|
||||
}, 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
|
||||
}
|
||||
|
||||
// AES加密
|
||||
func encrypt(plainText string) (string, error) {
|
||||
block, err := aes.NewCipher([]byte(encryptionKey))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
plainBytes := []byte(plainText)
|
||||
cipherBytes := make([]byte, aes.BlockSize+len(plainBytes))
|
||||
iv := cipherBytes[:aes.BlockSize]
|
||||
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
stream.XORKeyStream(cipherBytes[aes.BlockSize:], plainBytes)
|
||||
|
||||
return base64.StdEncoding.EncodeToString(cipherBytes), nil
|
||||
}
|
||||
|
||||
// AES解密
|
||||
func decrypt(cipherText string) (string, error) {
|
||||
block, err := aes.NewCipher([]byte(encryptionKey))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cipherBytes, err := base64.StdEncoding.DecodeString(cipherText)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(cipherBytes) < aes.BlockSize {
|
||||
return "", errors.New("ciphertext too short")
|
||||
}
|
||||
|
||||
iv := cipherBytes[:aes.BlockSize]
|
||||
cipherBytes = cipherBytes[aes.BlockSize:]
|
||||
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
stream.XORKeyStream(cipherBytes, cipherBytes)
|
||||
|
||||
return string(cipherBytes), nil
|
||||
}
|
||||
|
||||
// 获取解密后的账号(仅供内部使用,如提现申请时)
|
||||
func (r *Repository) GetDecryptedAccountNo(userID, id uint64) (string, error) {
|
||||
var account model.UserPaymentAccount
|
||||
if err := r.db.Where("id = ? AND user_id = ?", id, userID).First(&account).Error; err != nil {
|
||||
return "", err
|
||||
}
|
||||
return decrypt(account.AccountNo)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package paymentaccount
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrDependencyUnavailable = errors.New("dependency unavailable")
|
||||
ErrAccountNotFound = errors.New("payment account not found")
|
||||
ErrAccountNameMismatch = errors.New("account name must match realname")
|
||||
ErrRealnameRequired = errors.New("realname verification required")
|
||||
ErrAccountLimit = errors.New("maximum payment accounts reached")
|
||||
ErrCannotDeleteDefault = errors.New("cannot delete default account")
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo *Repository
|
||||
}
|
||||
|
||||
func NewService(repo *Repository) *Service {
|
||||
return &Service{repo: repo}
|
||||
}
|
||||
|
||||
func (s *Service) List(userID uint64, page, pageSize int) (*PaginatedResult, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
return s.repo.List(userID, page, pageSize)
|
||||
}
|
||||
|
||||
func (s *Service) FindByID(userID, id uint64) (*PaymentAccountDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.FindByID(userID, id)
|
||||
}
|
||||
|
||||
func (s *Service) Create(userID uint64, req CreatePaymentAccountRequest) (*PaymentAccountDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
// 验证实名状态
|
||||
if err := s.repo.ValidateRealname(userID, req.AccountName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 检查账号数量限制(最多5个)
|
||||
count, err := s.repo.CountByUser(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count >= 5 {
|
||||
return nil, ErrAccountLimit
|
||||
}
|
||||
return s.repo.Create(userID, req)
|
||||
}
|
||||
|
||||
func (s *Service) Update(userID, id uint64, req UpdatePaymentAccountRequest) (*PaymentAccountDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.Update(userID, id, req)
|
||||
}
|
||||
|
||||
func (s *Service) Delete(userID, id uint64) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.Delete(userID, id)
|
||||
}
|
||||
|
||||
func (s *Service) SetDefault(userID, id uint64) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.SetDefault(userID, id)
|
||||
}
|
||||
@@ -45,6 +45,8 @@ func (s *Service) Withdraw(userID uint64, req WithdrawRequest) (*AccountDTO, err
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
// 提现功能已迁移到 withdrawal 模块
|
||||
// 用户应通过 /api/withdrawals 接口提交提现申请
|
||||
return nil, ErrFeaturePending
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package withdrawal
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// 用户端 DTO
|
||||
type WithdrawalDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
WithdrawNo string `json:"withdraw_no"`
|
||||
UserID uint64 `json:"user_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
Fee float64 `json:"fee"`
|
||||
ActualAmount float64 `json:"actual_amount"`
|
||||
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
|
||||
type WithdrawalDetailDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
WithdrawNo string `json:"withdraw_no"`
|
||||
UserID uint64 `json:"user_id"`
|
||||
UserNickname string `json:"user_nickname"`
|
||||
UserPhone string `json:"user_phone"`
|
||||
Amount float64 `json:"amount"`
|
||||
Fee float64 `json:"fee"`
|
||||
ActualAmount float64 `json:"actual_amount"`
|
||||
PaymentAccountID *uint64 `json:"payment_account_id"`
|
||||
AccountType string `json:"account_type"`
|
||||
AccountName string `json:"account_name"`
|
||||
AccountNo string `json:"account_no"` // 管理员可见完整账号
|
||||
BankName string `json:"bank_name"`
|
||||
BankBranch string `json:"bank_branch"`
|
||||
Status string `json:"status"`
|
||||
ReviewedBy *uint64 `json:"reviewed_by"`
|
||||
ReviewedByName string `json:"reviewed_by_name"`
|
||||
ReviewedAt *time.Time `json:"reviewed_at"`
|
||||
ReviewRemark string `json:"review_remark"`
|
||||
PaidBy *uint64 `json:"paid_by"`
|
||||
PaidByName string `json:"paid_by_name"`
|
||||
PaidAt *time.Time `json:"paid_at"`
|
||||
PaymentProofURL string `json:"payment_proof_url"`
|
||||
PaymentRemark string `json:"payment_remark"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CreateWithdrawalRequest struct {
|
||||
PaymentAccountID uint64 `json:"payment_account_id" binding:"required"`
|
||||
Amount float64 `json:"amount" binding:"required,gt=0"`
|
||||
}
|
||||
|
||||
type ReviewWithdrawalRequest struct {
|
||||
Approved bool `json:"approved" binding:"required"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type ConfirmPaymentRequest struct {
|
||||
PaymentProofURL string `json:"payment_proof_url"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type PaginatedResult struct {
|
||||
Items []WithdrawalDTO `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
type AdminPaginatedResult struct {
|
||||
Items []WithdrawalDetailDTO `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
type AdminListQuery struct {
|
||||
Status string `form:"status"`
|
||||
UserID uint64 `form:"user_id"`
|
||||
Page int `form:"page"`
|
||||
Size int `form:"page_size"`
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package withdrawal
|
||||
|
||||
import (
|
||||
"hfb_sys/backend/pkg/response"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
service *Service
|
||||
}
|
||||
|
||||
func NewHandler(service *Service) *Handler {
|
||||
return &Handler{service: service}
|
||||
}
|
||||
|
||||
// ========== 用户端接口 ==========
|
||||
|
||||
func (h *Handler) Create(c *gin.Context) {
|
||||
userID, ok := currentUserID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
return
|
||||
}
|
||||
|
||||
var req CreateWithdrawalRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "请求参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
withdrawal, err := h.service.Create(userID, req)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, withdrawal)
|
||||
}
|
||||
|
||||
func (h *Handler) List(c *gin.Context) {
|
||||
userID, ok := currentUserID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
result, err := h.service.List(userID, page, pageSize)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
func (h *Handler) FindByID(c *gin.Context) {
|
||||
userID, ok := currentUserID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
return
|
||||
}
|
||||
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
withdrawal, err := h.service.FindByID(userID, id)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, withdrawal)
|
||||
}
|
||||
|
||||
func (h *Handler) Cancel(c *gin.Context) {
|
||||
userID, ok := currentUserID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
return
|
||||
}
|
||||
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.service.Cancel(userID, id); err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, gin.H{"cancelled": true})
|
||||
}
|
||||
|
||||
// ========== 管理员端接口 ==========
|
||||
|
||||
func (h *Handler) AdminList(c *gin.Context) {
|
||||
var query AdminListQuery
|
||||
if err := c.ShouldBindQuery(&query); err != nil {
|
||||
query.Page = 1
|
||||
query.Size = 20
|
||||
}
|
||||
|
||||
result, err := h.service.AdminList(query)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
func (h *Handler) AdminFindByID(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
withdrawal, err := h.service.AdminFindByID(id)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, withdrawal)
|
||||
}
|
||||
|
||||
func (h *Handler) Review(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var req ReviewWithdrawalRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "请求参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
withdrawal, err := h.service.Review(adminID, id, req)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, withdrawal)
|
||||
}
|
||||
|
||||
func (h *Handler) ConfirmPayment(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var req ConfirmPaymentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "请求参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
withdrawal, err := h.service.ConfirmPayment(adminID, id, req)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, withdrawal)
|
||||
}
|
||||
|
||||
// ========== 辅助函数 ==========
|
||||
|
||||
func currentUserID(c *gin.Context) (uint64, bool) {
|
||||
val, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
return 0, false
|
||||
}
|
||||
userID, ok := val.(uint64)
|
||||
return userID, ok
|
||||
}
|
||||
|
||||
func currentAdminID(c *gin.Context) (uint64, bool) {
|
||||
val, exists := c.Get("admin_id")
|
||||
if !exists {
|
||||
return 0, false
|
||||
}
|
||||
adminID, ok := val.(uint64)
|
||||
return adminID, ok
|
||||
}
|
||||
|
||||
func parseID(c *gin.Context) (uint64, bool) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequest(c, "无效的ID")
|
||||
return 0, false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
|
||||
func writeError(c *gin.Context, err error) {
|
||||
switch err {
|
||||
case ErrWithdrawalNotFound:
|
||||
response.NotFound(c, "提现申请不存在")
|
||||
case ErrInvalidAmount:
|
||||
response.BadRequest(c, "提现金额无效")
|
||||
case ErrInsufficientBalance:
|
||||
response.BadRequest(c, "余额不足")
|
||||
case ErrMinWithdrawalAmount:
|
||||
response.BadRequest(c, "提现金额低于最小限额")
|
||||
case ErrMaxWithdrawalAmount:
|
||||
response.BadRequest(c, "提现金额超过最大限额")
|
||||
case ErrWithdrawalLocked:
|
||||
response.BadRequest(c, "提现申请状态已锁定,无法操作")
|
||||
case ErrUnauthorized:
|
||||
response.Unauthorized(c, "无权限操作")
|
||||
default:
|
||||
response.InternalServerError(c, "操作失败")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
package withdrawal
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/wallet"
|
||||
|
||||
"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
|
||||
if w.PaymentAccountID != nil {
|
||||
var paymentAccount model.UserPaymentAccount
|
||||
if err := r.db.First(&paymentAccount, *w.PaymentAccountID).Error; err == nil {
|
||||
decrypted, err := r.decryptAccountNo(paymentAccount.AccountNo)
|
||||
if err == nil {
|
||||
fullAccountNo = decrypted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package withdrawal
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrDependencyUnavailable = errors.New("dependency unavailable")
|
||||
ErrWithdrawalNotFound = errors.New("withdrawal not found")
|
||||
ErrInvalidAmount = errors.New("invalid amount")
|
||||
ErrInsufficientBalance = errors.New("insufficient balance")
|
||||
ErrMinWithdrawalAmount = errors.New("amount below minimum withdrawal")
|
||||
ErrMaxWithdrawalAmount = errors.New("amount exceeds maximum withdrawal")
|
||||
ErrWithdrawalLocked = errors.New("withdrawal status locked")
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
)
|
||||
|
||||
const (
|
||||
MinWithdrawalAmount = 10.0 // 最低提现金额
|
||||
MaxWithdrawalAmount = 5000.0 // 单笔最高提现金额
|
||||
WithdrawalFeeRate = 0.0 // 手续费率(暂时0%)
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo *Repository
|
||||
}
|
||||
|
||||
func NewService(repo *Repository) *Service {
|
||||
return &Service{repo: repo}
|
||||
}
|
||||
|
||||
// 用户端方法
|
||||
func (s *Service) Create(userID uint64, req CreateWithdrawalRequest) (*WithdrawalDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
|
||||
// 验证金额
|
||||
if req.Amount < MinWithdrawalAmount {
|
||||
return nil, ErrMinWithdrawalAmount
|
||||
}
|
||||
if req.Amount > MaxWithdrawalAmount {
|
||||
return nil, ErrMaxWithdrawalAmount
|
||||
}
|
||||
|
||||
return s.repo.Create(userID, req)
|
||||
}
|
||||
|
||||
func (s *Service) List(userID uint64, page, pageSize int) (*PaginatedResult, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
return s.repo.List(userID, page, pageSize)
|
||||
}
|
||||
|
||||
func (s *Service) FindByID(userID, id uint64) (*WithdrawalDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.FindByID(userID, id)
|
||||
}
|
||||
|
||||
func (s *Service) Cancel(userID, id uint64) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.Cancel(userID, id)
|
||||
}
|
||||
|
||||
// 管理员端方法
|
||||
func (s *Service) AdminList(query AdminListQuery) (*AdminPaginatedResult, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if query.Page < 1 {
|
||||
query.Page = 1
|
||||
}
|
||||
if query.Size < 1 || query.Size > 100 {
|
||||
query.Size = 20
|
||||
}
|
||||
return s.repo.AdminList(query)
|
||||
}
|
||||
|
||||
func (s *Service) AdminFindByID(id uint64) (*WithdrawalDetailDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.AdminFindByID(id)
|
||||
}
|
||||
|
||||
func (s *Service) Review(adminID, id uint64, req ReviewWithdrawalRequest) (*WithdrawalDetailDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.Review(adminID, id, req)
|
||||
}
|
||||
|
||||
func (s *Service) ConfirmPayment(adminID, id uint64, req ConfirmPaymentRequest) (*WithdrawalDetailDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.ConfirmPayment(adminID, id, req)
|
||||
}
|
||||
Reference in New Issue
Block a user