2be25df7d2
- P04: 修复 Withdrawal.TableName() 返回正确表名 withdrawals
- P03: 新增提现状态常量 (constant/withdrawal.go),消除魔法数字 0/1/2/3;
新增 CommissionTypeWithdrawCancel (338) 和 WithdrawalCancelForbidden 错误码
- P02: 迁移 02123 为 withdrawals 表添加 method/account/qr_code_url 列;
扩展 Withdrawal GORM 模型和 API 类型(CommissionWithdrawRequest、WithdrawalLog);
申请提现时增加收款方式校验逻辑
- P01: 新增用户取消提现接口 POST /v1/public/user/withdrawal_cancel;
仅可取消自己的待审核提现,退还佣金并写佣金日志
- P05: 管理端提现列表新增 method 过滤参数
Co-authored-by: multica-agent <github@multica.ai>
117 lines
4.1 KiB
Go
117 lines
4.1 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"time"
|
|
|
|
logicCommon "github.com/perfect-panel/server/internal/logic/common"
|
|
"github.com/perfect-panel/server/internal/model/log"
|
|
"github.com/perfect-panel/server/internal/model/user"
|
|
"github.com/perfect-panel/server/internal/svc"
|
|
"github.com/perfect-panel/server/internal/types"
|
|
"github.com/perfect-panel/server/pkg/constant"
|
|
"github.com/perfect-panel/server/pkg/logger"
|
|
"github.com/perfect-panel/server/pkg/xerr"
|
|
"github.com/pkg/errors"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type CommissionWithdrawLogic struct {
|
|
logger.Logger
|
|
ctx context.Context
|
|
svcCtx *svc.ServiceContext
|
|
}
|
|
|
|
// Commission Withdraw
|
|
func NewCommissionWithdrawLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CommissionWithdrawLogic {
|
|
return &CommissionWithdrawLogic{
|
|
Logger: logger.WithContext(ctx),
|
|
ctx: ctx,
|
|
svcCtx: svcCtx,
|
|
}
|
|
}
|
|
|
|
func (l *CommissionWithdrawLogic) CommissionWithdraw(req *types.CommissionWithdrawRequest) (resp *types.WithdrawalLog, err error) {
|
|
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
|
if !ok {
|
|
logger.Error("current user is not found in context")
|
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
|
}
|
|
|
|
switch req.Method {
|
|
case 1, 2: // Alipay, WeChat
|
|
if strings.TrimSpace(req.QrCodeUrl) == "" {
|
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "qr_code_url is required for method %d", req.Method)
|
|
}
|
|
case 3: // Bank
|
|
if strings.TrimSpace(req.Account) == "" {
|
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "account is required for bank transfer")
|
|
}
|
|
default: // Other
|
|
if strings.TrimSpace(req.Account) == "" && strings.TrimSpace(req.Content) == "" {
|
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "account or content is required")
|
|
}
|
|
}
|
|
|
|
if u.Commission < req.Amount {
|
|
logger.Errorf("User %d has insufficient commission balance: %.2f, requested: %.2f", u.Id, float64(u.Commission)/100, float64(req.Amount)/100)
|
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserCommissionNotEnough), "User %d has insufficient commission balance", u.Id)
|
|
}
|
|
|
|
tx := l.svcCtx.DB.WithContext(l.ctx).Begin()
|
|
now := time.Now()
|
|
|
|
// Atomically deduct the requested amount so concurrent commission growth is preserved.
|
|
if err = l.svcCtx.DB.WithContext(l.ctx).
|
|
Model(&user.User{}).
|
|
Where("id = ? AND commission >= ?", u.Id, req.Amount).
|
|
UpdateColumn("commission", gorm.Expr("commission - ?", req.Amount)).Error; err != nil {
|
|
tx.Rollback()
|
|
l.Errorf("Failed to update user %d commission balance: %v", u.Id, err)
|
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "Failed to update user %d commission balance: %v", u.Id, err)
|
|
}
|
|
_ = l.svcCtx.UserModel.ClearUserCache(l.ctx, u)
|
|
|
|
// create withdrawal log
|
|
if err = logicCommon.WriteCommissionLog(tx, u.Id, log.CommissionTypeConvertBalance, req.Amount, ""); err != nil {
|
|
tx.Rollback()
|
|
l.Errorf("Failed to create commission log for user %d: %v", u.Id, err)
|
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "Failed to create commission log for user %d: %v", u.Id, err)
|
|
}
|
|
|
|
err = tx.Model(&user.Withdrawal{}).Create(&user.Withdrawal{
|
|
UserId: u.Id,
|
|
Amount: req.Amount,
|
|
Method: req.Method,
|
|
Account: req.Account,
|
|
QrCodeUrl: req.QrCodeUrl,
|
|
Content: req.Content,
|
|
Status: constant.WithdrawalStatusPending,
|
|
Reason: "",
|
|
}).Error
|
|
|
|
if err != nil {
|
|
tx.Rollback()
|
|
l.Errorf("Failed to create withdrawal log for user %d: %v", u.Id, err)
|
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "Failed to create withdrawal log for user %d: %v", u.Id, err)
|
|
}
|
|
if err = tx.Commit().Error; err != nil {
|
|
l.Errorf("Transaction commit failed for user %d withdrawal: %v", u.Id, err)
|
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Transaction commit failed for user %d withdrawal: %v", u.Id, err)
|
|
}
|
|
|
|
return &types.WithdrawalLog{
|
|
UserId: u.Id,
|
|
Amount: req.Amount,
|
|
Method: req.Method,
|
|
Account: req.Account,
|
|
QrCodeUrl: req.QrCodeUrl,
|
|
Content: req.Content,
|
|
Status: constant.WithdrawalStatusPending,
|
|
Reason: "",
|
|
CreatedAt: now.UnixMilli(),
|
|
UpdatedAt: now.UnixMilli(),
|
|
}, nil
|
|
}
|