新功能(#41): 提现优化 — 收款方式选择 + 取消提现 + 收款码上传
Build docker and publish / build (20.15.1) (push) Failing after 8m37s
Build docker and publish / build (20.15.1) (push) Failing after 8m37s
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -235,6 +235,7 @@ type (
|
||||
Size int `form:"size"`
|
||||
UserId *int64 `form:"user_id,omitempty"`
|
||||
Status *uint8 `form:"status,omitempty"`
|
||||
Method *uint8 `form:"method,omitempty"`
|
||||
}
|
||||
GetWithdrawalListResponse {
|
||||
List []WithdrawalLog `json:"list"`
|
||||
|
||||
+15
-2
@@ -109,8 +109,11 @@ type (
|
||||
Rules []string `json:"rules" validate:"required"`
|
||||
}
|
||||
CommissionWithdrawRequest {
|
||||
Amount int64 `json:"amount"`
|
||||
Content string `json:"content"`
|
||||
Amount int64 `json:"amount"`
|
||||
Content string `json:"content"`
|
||||
Method uint8 `json:"method" validate:"oneof=0 1 2 3"`
|
||||
Account string `json:"account,omitempty"`
|
||||
QrCodeUrl string `json:"qr_code_url,omitempty"`
|
||||
}
|
||||
WithdrawalLog {
|
||||
Id int64 `json:"id"`
|
||||
@@ -119,9 +122,15 @@ type (
|
||||
Content string `json:"content"`
|
||||
Status uint8 `json:"status"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Method uint8 `json:"method"`
|
||||
Account string `json:"account"`
|
||||
QrCodeUrl string `json:"qr_code_url"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
CancelWithdrawalRequest {
|
||||
WithdrawalId int64 `json:"withdrawal_id" validate:"required,gt=0"`
|
||||
}
|
||||
QueryWithdrawalLogListRequest {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
@@ -352,6 +361,10 @@ service ppanel {
|
||||
@handler CommissionWithdraw
|
||||
post /commission_withdraw (CommissionWithdrawRequest) returns (WithdrawalLog)
|
||||
|
||||
@doc "Cancel pending withdrawal"
|
||||
@handler CancelWithdrawal
|
||||
post /withdrawal_cancel (CancelWithdrawalRequest) returns (WithdrawalLog)
|
||||
|
||||
@doc "Query Withdrawal Log"
|
||||
@handler QueryWithdrawalLog
|
||||
get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE `withdrawals`
|
||||
DROP COLUMN `qr_code_url`,
|
||||
DROP COLUMN `account`,
|
||||
DROP COLUMN `method`;
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE `withdrawals`
|
||||
ADD COLUMN `method` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '收款方式 0:其他 1:支付宝 2:微信 3:银行卡' AFTER `content`,
|
||||
ADD COLUMN `account` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '收款账号' AFTER `method`,
|
||||
ADD COLUMN `qr_code_url` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '收款码图片URL' AFTER `account`;
|
||||
@@ -0,0 +1,26 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/public/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Cancel Withdrawal
|
||||
func CancelWithdrawalHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.CancelWithdrawalRequest
|
||||
_ = c.ShouldBind(&req)
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
|
||||
l := user.NewCancelWithdrawalLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.CancelWithdrawal(&req)
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
@@ -1059,6 +1059,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
// Commission Withdraw
|
||||
publicUserGroupRouter.POST("/commission_withdraw", publicUser.CommissionWithdrawHandler(serverCtx))
|
||||
|
||||
// Cancel Withdrawal
|
||||
publicUserGroupRouter.POST("/withdrawal_cancel", publicUser.CancelWithdrawalHandler(serverCtx))
|
||||
|
||||
// Delete Current User Account
|
||||
publicUserGroupRouter.DELETE("/current_user_account", publicUser.DeleteCurrentUserAccountHandler(serverCtx))
|
||||
|
||||
|
||||
@@ -42,6 +42,9 @@ func (l *GetWithdrawalListLogic) GetWithdrawalList(req *types.GetWithdrawalListR
|
||||
if req.Status != nil {
|
||||
query = query.Where("status = ?", *req.Status)
|
||||
}
|
||||
if req.Method != nil {
|
||||
query = query.Where("method = ?", *req.Method)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
@@ -62,6 +65,9 @@ func (l *GetWithdrawalListLogic) GetWithdrawalList(req *types.GetWithdrawalListR
|
||||
Content: row.Content,
|
||||
Status: row.Status,
|
||||
Reason: row.Reason,
|
||||
Method: row.Method,
|
||||
Account: row.Account,
|
||||
QrCodeUrl: row.QrCodeUrl,
|
||||
CreatedAt: row.CreatedAt.UnixMilli(),
|
||||
UpdatedAt: row.UpdatedAt.UnixMilli(),
|
||||
})
|
||||
|
||||
@@ -37,9 +37,9 @@ func approveWithdrawal(ctx context.Context, svcCtx *svc.ServiceContext, withdraw
|
||||
}
|
||||
|
||||
if err := tx.Model(&usermodel.Withdrawal{}).
|
||||
Where("id = ? AND status = 0", withdrawalID).
|
||||
Where("id = ? AND status = ?", withdrawalID, usermodel.WithdrawalStatusPending).
|
||||
Updates(map[string]interface{}{
|
||||
"status": 1,
|
||||
"status": usermodel.WithdrawalStatusApproved,
|
||||
"reason": "",
|
||||
}).Error; err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "approve withdrawal failed: %v", err)
|
||||
@@ -80,9 +80,9 @@ func rejectWithdrawal(ctx context.Context, svcCtx *svc.ServiceContext, withdrawa
|
||||
// Commission was NOT deducted at application time under the new logic,
|
||||
// so rejection requires no refund — only a status update.
|
||||
return tx.Model(&usermodel.Withdrawal{}).
|
||||
Where("id = ? AND status = 0", withdrawalID).
|
||||
Where("id = ? AND status = ?", withdrawalID, usermodel.WithdrawalStatusPending).
|
||||
Updates(map[string]interface{}{
|
||||
"status": 2,
|
||||
"status": usermodel.WithdrawalStatusRejected,
|
||||
"reason": reason,
|
||||
}).Error
|
||||
})
|
||||
|
||||
@@ -41,7 +41,7 @@ func LoadPendingWithdrawalForUpdate(ctx context.Context, tx *gorm.DB, withdrawal
|
||||
First(&withdrawal).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if withdrawal.Status != 0 {
|
||||
if withdrawal.Status != usermodel.WithdrawalStatusPending {
|
||||
return nil, errors.New("withdrawal status invalid")
|
||||
}
|
||||
return &withdrawal, nil
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
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 CancelWithdrawalLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewCancelWithdrawalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CancelWithdrawalLogic {
|
||||
return &CancelWithdrawalLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *CancelWithdrawalLogic) CancelWithdrawal(req *types.CancelWithdrawalRequest) (resp *types.WithdrawalLog, err error) {
|
||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
if !ok {
|
||||
l.Error("current user is not found in context")
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||
}
|
||||
|
||||
var withdrawal *user.Withdrawal
|
||||
err = l.svcCtx.DB.WithContext(l.ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var txErr error
|
||||
withdrawal, txErr = logicCommon.LoadPendingWithdrawalForUpdate(l.ctx, tx, req.WithdrawalId)
|
||||
if txErr != nil {
|
||||
if txErr.Error() == "withdrawal status invalid" {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.WithdrawalStatusInvalid), "withdrawal %d is not in pending state", req.WithdrawalId)
|
||||
}
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "load withdrawal failed: %v", txErr)
|
||||
}
|
||||
|
||||
if withdrawal.UserId != u.Id {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.PermissionDenied), "user %d cannot cancel withdrawal belonging to user %d", u.Id, withdrawal.UserId)
|
||||
}
|
||||
|
||||
if txErr = tx.Model(&user.Withdrawal{}).
|
||||
Where("id = ? AND status = ?", req.WithdrawalId, user.WithdrawalStatusPending).
|
||||
Update("status", user.WithdrawalStatusCancelled).Error; txErr != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "cancel withdrawal failed: %v", txErr)
|
||||
}
|
||||
|
||||
if txErr = l.svcCtx.UserModel.UpdateCommission(l.ctx, withdrawal.UserId, withdrawal.Amount, tx); txErr != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "refund commission failed: %v", txErr)
|
||||
}
|
||||
|
||||
if txErr = logicCommon.WriteCommissionLog(tx, withdrawal.UserId, log.CommissionTypeWithdrawCancel, withdrawal.Amount, ""); txErr != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "write commission log failed: %v", txErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = l.svcCtx.UserModel.ClearUserCache(l.ctx, u)
|
||||
|
||||
return &types.WithdrawalLog{
|
||||
Id: withdrawal.Id,
|
||||
UserId: withdrawal.UserId,
|
||||
Amount: withdrawal.Amount,
|
||||
Content: withdrawal.Content,
|
||||
Status: user.WithdrawalStatusCancelled,
|
||||
Reason: "",
|
||||
Method: withdrawal.Method,
|
||||
Account: withdrawal.Account,
|
||||
QrCodeUrl: withdrawal.QrCodeUrl,
|
||||
CreatedAt: withdrawal.CreatedAt.UnixMilli(),
|
||||
UpdatedAt: withdrawal.UpdatedAt.UnixMilli(),
|
||||
}, nil
|
||||
}
|
||||
@@ -35,12 +35,28 @@ func (l *CommissionWithdrawLogic) CommissionWithdraw(req *types.CommissionWithdr
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||
}
|
||||
|
||||
// Validate payment method fields
|
||||
switch req.Method {
|
||||
case user.WithdrawalMethodAlipay, user.WithdrawalMethodWechat:
|
||||
if req.QrCodeUrl == "" {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "qr_code_url is required for method %d", req.Method)
|
||||
}
|
||||
case user.WithdrawalMethodBank:
|
||||
if req.Account == "" {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "account is required for bank transfer")
|
||||
}
|
||||
default: // WithdrawalMethodOther
|
||||
if req.Account == "" && req.Content == "" {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "account or content is required for other methods")
|
||||
}
|
||||
}
|
||||
|
||||
// Sum all pending (status=0) withdrawals to compute available balance.
|
||||
// Available = commission - pendingTotal; commission is only deducted on approval.
|
||||
var pendingTotal int64
|
||||
if err = l.svcCtx.DB.WithContext(l.ctx).
|
||||
Model(&user.Withdrawal{}).
|
||||
Where("user_id = ? AND status = 0", u.Id).
|
||||
Where("user_id = ? AND status = ?", u.Id, user.WithdrawalStatusPending).
|
||||
Select("COALESCE(SUM(amount), 0)").
|
||||
Scan(&pendingTotal).Error; err != nil {
|
||||
l.Errorf("Failed to query pending withdrawals for user %d: %v", u.Id, err)
|
||||
@@ -56,11 +72,14 @@ func (l *CommissionWithdrawLogic) CommissionWithdraw(req *types.CommissionWithdr
|
||||
var w user.Withdrawal
|
||||
err = l.svcCtx.DB.WithContext(l.ctx).Transaction(func(tx *gorm.DB) error {
|
||||
w = user.Withdrawal{
|
||||
UserId: u.Id,
|
||||
Amount: req.Amount,
|
||||
Content: req.Content,
|
||||
Status: 0,
|
||||
Reason: "",
|
||||
UserId: u.Id,
|
||||
Amount: req.Amount,
|
||||
Content: req.Content,
|
||||
Status: user.WithdrawalStatusPending,
|
||||
Reason: "",
|
||||
Method: req.Method,
|
||||
Account: req.Account,
|
||||
QrCodeUrl: req.QrCodeUrl,
|
||||
}
|
||||
return tx.Create(&w).Error
|
||||
})
|
||||
@@ -70,11 +89,15 @@ func (l *CommissionWithdrawLogic) CommissionWithdraw(req *types.CommissionWithdr
|
||||
}
|
||||
|
||||
return &types.WithdrawalLog{
|
||||
Id: w.Id,
|
||||
UserId: u.Id,
|
||||
Amount: req.Amount,
|
||||
Content: req.Content,
|
||||
Status: 0,
|
||||
Status: user.WithdrawalStatusPending,
|
||||
Reason: "",
|
||||
Method: req.Method,
|
||||
Account: req.Account,
|
||||
QrCodeUrl: req.QrCodeUrl,
|
||||
CreatedAt: w.CreatedAt.UnixMilli(),
|
||||
UpdatedAt: w.UpdatedAt.UnixMilli(),
|
||||
}, nil
|
||||
|
||||
@@ -64,6 +64,9 @@ func (l *QueryWithdrawalLogLogic) QueryWithdrawalLog(req *types.QueryWithdrawalL
|
||||
Content: row.Content,
|
||||
Status: row.Status,
|
||||
Reason: row.Reason,
|
||||
Method: row.Method,
|
||||
Account: row.Account,
|
||||
QrCodeUrl: row.QrCodeUrl,
|
||||
CreatedAt: row.CreatedAt.UnixMilli(),
|
||||
UpdatedAt: row.UpdatedAt.UnixMilli(),
|
||||
})
|
||||
|
||||
@@ -51,6 +51,7 @@ const (
|
||||
CommissionTypeAdjust uint16 = 335 // Admin Adjust
|
||||
CommissionTypeConvertBalance uint16 = 336 // Convert to Balance
|
||||
CommissionTypeWithdrawReject uint16 = 337 // Withdraw rejected refund
|
||||
CommissionTypeWithdrawCancel uint16 = 338 // 用户取消提现退佣金
|
||||
GiftTypeIncrease uint16 = 341 // Increase
|
||||
GiftTypeReduce uint16 = 342 // Reduce
|
||||
)
|
||||
|
||||
@@ -167,8 +167,11 @@ type Withdrawal struct {
|
||||
UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"`
|
||||
Amount int64 `gorm:"not null;comment:Withdrawal Amount"`
|
||||
Content string `gorm:"type:text;comment:Withdrawal Content"`
|
||||
Status uint8 `gorm:"type:tinyint(1);default:0;comment:Withdrawal Status: 0: Pending 1: Approved 2: Rejected"`
|
||||
Status uint8 `gorm:"type:tinyint(1);default:0;comment:Withdrawal Status: 0: Pending 1: Approved 2: Rejected 3: Cancelled"`
|
||||
Reason string `gorm:"type:varchar(500);default:'';comment:Rejection Reason"`
|
||||
Method uint8 `gorm:"type:tinyint(1);default:0;comment:收款方式 0:其他 1:支付宝 2:微信 3:银行卡"`
|
||||
Account string `gorm:"type:varchar(255);default:'';comment:收款账号"`
|
||||
QrCodeUrl string `gorm:"type:varchar(500);default:'';comment:收款码图片URL"`
|
||||
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
|
||||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package user
|
||||
|
||||
const (
|
||||
WithdrawalStatusPending uint8 = 0 // 待审核
|
||||
WithdrawalStatusApproved uint8 = 1 // 已通过
|
||||
WithdrawalStatusRejected uint8 = 2 // 已拒绝
|
||||
WithdrawalStatusCancelled uint8 = 3 // 已取消(用户自行撤回)
|
||||
)
|
||||
|
||||
const (
|
||||
WithdrawalMethodOther uint8 = 0 // 其他
|
||||
WithdrawalMethodAlipay uint8 = 1 // 支付宝
|
||||
WithdrawalMethodWechat uint8 = 2 // 微信
|
||||
WithdrawalMethodBank uint8 = 3 // 银行卡
|
||||
)
|
||||
+13
-2
@@ -296,8 +296,11 @@ type CommissionLog struct {
|
||||
}
|
||||
|
||||
type CommissionWithdrawRequest struct {
|
||||
Amount int64 `json:"amount"`
|
||||
Content string `json:"content"`
|
||||
Amount int64 `json:"amount"`
|
||||
Content string `json:"content"`
|
||||
Method uint8 `json:"method" validate:"oneof=0 1 2 3"`
|
||||
Account string `json:"account,omitempty"`
|
||||
QrCodeUrl string `json:"qr_code_url,omitempty"`
|
||||
}
|
||||
|
||||
type ConnectionRecords struct {
|
||||
@@ -2315,6 +2318,10 @@ type QueryUserSubscribeNodeListResponse struct {
|
||||
List []UserSubscribeInfo `json:"list"`
|
||||
}
|
||||
|
||||
type CancelWithdrawalRequest struct {
|
||||
WithdrawalId int64 `json:"withdrawal_id" validate:"required,gt=0"`
|
||||
}
|
||||
|
||||
type QueryWithdrawalLogListRequest struct {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
@@ -2330,6 +2337,7 @@ type GetWithdrawalListRequest struct {
|
||||
Size int `form:"size"`
|
||||
UserId *int64 `form:"user_id,omitempty"`
|
||||
Status *uint8 `form:"status,omitempty"`
|
||||
Method *uint8 `form:"method,omitempty"`
|
||||
}
|
||||
|
||||
type GetWithdrawalListResponse struct {
|
||||
@@ -3648,6 +3656,9 @@ type WithdrawalLog struct {
|
||||
Content string `json:"content"`
|
||||
Status uint8 `json:"status"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Method uint8 `json:"method"`
|
||||
Account string `json:"account"`
|
||||
QrCodeUrl string `json:"qr_code_url"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user