From 2be25df7d217eb0bb36d046117afe09e46498b9a Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Mon, 25 May 2026 20:39:45 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8F=90=E7=8E=B0=E4=BC=98=E5=8C=96=20?= =?UTF-8?q?=E2=80=94=20=E5=8F=96=E6=B6=88=E7=8A=B6=E6=80=81=20+=20?= =?UTF-8?q?=E6=94=B6=E6=AC=BE=E6=96=B9=E5=BC=8F=20+=20TableName=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20(HIF-42)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- apis/admin/user.api | 1 + apis/public/user.api | 17 ++++- .../database/02123_withdrawal_method.down.sql | 4 ++ .../database/02123_withdrawal_method.up.sql | 4 ++ .../public/user/cancelWithdrawalHandler.go | 25 +++++++ internal/handler/routes.go | 3 + .../admin/user/getWithdrawalListLogic.go | 6 ++ internal/logic/admin/user/withdrawalCommon.go | 9 +-- internal/logic/common/withdrawal.go | 3 +- .../public/user/cancelWithdrawalLogic.go | 68 +++++++++++++++++++ .../public/user/commissionWithdrawLogic.go | 34 ++++++++-- .../public/user/queryWithdrawalLogLogic.go | 3 + internal/model/log/log.go | 3 +- internal/model/user/user.go | 7 +- internal/types/types.go | 15 +++- pkg/constant/withdrawal.go | 8 +++ pkg/xerr/errCode.go | 1 + pkg/xerr/errMsg.go | 1 + 18 files changed, 194 insertions(+), 18 deletions(-) create mode 100644 initialize/migrate/database/02123_withdrawal_method.down.sql create mode 100644 initialize/migrate/database/02123_withdrawal_method.up.sql create mode 100644 internal/handler/public/user/cancelWithdrawalHandler.go create mode 100644 internal/logic/public/user/cancelWithdrawalLogic.go create mode 100644 pkg/constant/withdrawal.go diff --git a/apis/admin/user.api b/apis/admin/user.api index aa6243f..c107826 100644 --- a/apis/admin/user.api +++ b/apis/admin/user.api @@ -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"` diff --git a/apis/public/user.api b/apis/public/user.api index e55919f..5a909b9 100644 --- a/apis/public/user.api +++ b/apis/public/user.api @@ -109,13 +109,19 @@ type ( Rules []string `json:"rules" validate:"required"` } CommissionWithdrawRequest { - Amount int64 `json:"amount"` - Content string `json:"content"` + Amount int64 `json:"amount"` + Method uint8 `json:"method"` + Account string `json:"account"` + QrCodeUrl string `json:"qr_code_url"` + Content string `json:"content"` } WithdrawalLog { Id int64 `json:"id"` UserId int64 `json:"user_id"` Amount int64 `json:"amount"` + Method uint8 `json:"method"` + Account string `json:"account"` + QrCodeUrl string `json:"qr_code_url"` Content string `json:"content"` Status uint8 `json:"status"` Reason string `json:"reason,omitempty"` @@ -130,6 +136,9 @@ type ( List []WithdrawalLog `json:"list"` Total int64 `json:"total"` } + CancelWithdrawalRequest { + WithdrawalId int64 `json:"withdrawal_id" validate:"required,gt=0"` + } GetDeviceOnlineStatsResponse { WeeklyStats []WeeklyStat `json:"weekly_stats"` ConnectionRecords ConnectionRecords `json:"connection_records"` @@ -356,6 +365,10 @@ service ppanel { @handler QueryWithdrawalLog get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse) + @doc "Cancel withdrawal" + @handler CancelWithdrawal + post /withdrawal_cancel (CancelWithdrawalRequest) + @doc "Device Online Statistics" @handler DeviceOnlineStatistics get /device_online_statistics returns (GetDeviceOnlineStatsResponse) diff --git a/initialize/migrate/database/02123_withdrawal_method.down.sql b/initialize/migrate/database/02123_withdrawal_method.down.sql new file mode 100644 index 0000000..83b2a18 --- /dev/null +++ b/initialize/migrate/database/02123_withdrawal_method.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE `withdrawals` + DROP COLUMN `qr_code_url`, + DROP COLUMN `account`, + DROP COLUMN `method`; diff --git a/initialize/migrate/database/02123_withdrawal_method.up.sql b/initialize/migrate/database/02123_withdrawal_method.up.sql new file mode 100644 index 0000000..57f3104 --- /dev/null +++ b/initialize/migrate/database/02123_withdrawal_method.up.sql @@ -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`; diff --git a/internal/handler/public/user/cancelWithdrawalHandler.go b/internal/handler/public/user/cancelWithdrawalHandler.go new file mode 100644 index 0000000..ac51d9a --- /dev/null +++ b/internal/handler/public/user/cancelWithdrawalHandler.go @@ -0,0 +1,25 @@ +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" +) + +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) + err := l.CancelWithdrawal(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 5e387ea..b39fe5a 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -1126,6 +1126,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Query Withdrawal Log publicUserGroupRouter.GET("/withdrawal_log", publicUser.QueryWithdrawalLogHandler(serverCtx)) + + // Cancel Withdrawal + publicUserGroupRouter.POST("/withdrawal_cancel", publicUser.CancelWithdrawalHandler(serverCtx)) } publicUserWsGroupRouter := router.Group("/v1/public/user") diff --git a/internal/logic/admin/user/getWithdrawalListLogic.go b/internal/logic/admin/user/getWithdrawalListLogic.go index ff262af..d3089e4 100644 --- a/internal/logic/admin/user/getWithdrawalListLogic.go +++ b/internal/logic/admin/user/getWithdrawalListLogic.go @@ -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 { @@ -59,6 +62,9 @@ func (l *GetWithdrawalListLogic) GetWithdrawalList(req *types.GetWithdrawalListR Id: row.Id, UserId: row.UserId, Amount: row.Amount, + Method: row.Method, + Account: row.Account, + QrCodeUrl: row.QrCodeUrl, Content: row.Content, Status: row.Status, Reason: row.Reason, diff --git a/internal/logic/admin/user/withdrawalCommon.go b/internal/logic/admin/user/withdrawalCommon.go index 2cb8578..9a2e3dd 100644 --- a/internal/logic/admin/user/withdrawalCommon.go +++ b/internal/logic/admin/user/withdrawalCommon.go @@ -8,6 +8,7 @@ import ( "github.com/perfect-panel/server/internal/model/log" usermodel "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" + "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" @@ -25,9 +26,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, constant.WithdrawalStatusPending). Updates(map[string]interface{}{ - "status": 1, + "status": constant.WithdrawalStatusApproved, "reason": "", }).Error; err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "approve withdrawal failed: %v", err) @@ -52,9 +53,9 @@ func rejectWithdrawal(ctx context.Context, svcCtx *svc.ServiceContext, withdrawa } if err := tx.Model(&usermodel.Withdrawal{}). - Where("id = ? AND status = 0", withdrawalID). + Where("id = ? AND status = ?", withdrawalID, constant.WithdrawalStatusPending). Updates(map[string]interface{}{ - "status": 2, + "status": constant.WithdrawalStatusRejected, "reason": reason, }).Error; err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "reject withdrawal failed: %v", err) diff --git a/internal/logic/common/withdrawal.go b/internal/logic/common/withdrawal.go index 6a8cbe4..39cca29 100644 --- a/internal/logic/common/withdrawal.go +++ b/internal/logic/common/withdrawal.go @@ -6,6 +6,7 @@ import ( "github.com/perfect-panel/server/internal/model/log" usermodel "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/pkg/constant" "github.com/pkg/errors" "gorm.io/gorm" "gorm.io/gorm/clause" @@ -41,7 +42,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 != constant.WithdrawalStatusPending { return nil, errors.New("withdrawal status invalid") } return &withdrawal, nil diff --git a/internal/logic/public/user/cancelWithdrawalLogic.go b/internal/logic/public/user/cancelWithdrawalLogic.go new file mode 100644 index 0000000..9570970 --- /dev/null +++ b/internal/logic/public/user/cancelWithdrawalLogic.go @@ -0,0 +1,68 @@ +package user + +import ( + "context" + + logicCommon "github.com/perfect-panel/server/internal/logic/common" + "github.com/perfect-panel/server/internal/model/log" + usermodel "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) error { + u, ok := l.ctx.Value(constant.CtxKeyUser).(*usermodel.User) + if !ok { + l.Error("current user is not found in context") + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + + return l.svcCtx.DB.WithContext(l.ctx).Transaction(func(tx *gorm.DB) error { + withdrawal, err := logicCommon.LoadPendingWithdrawalForUpdate(l.ctx, tx, req.WithdrawalId) + if err != nil { + if err.Error() == "withdrawal status invalid" { + return errors.Wrapf(xerr.NewErrCode(xerr.WithdrawalStatusInvalid), "withdrawal %d already processed", req.WithdrawalId) + } + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "load withdrawal failed: %v", err) + } + + if withdrawal.UserId != u.Id { + return errors.Wrapf(xerr.NewErrCode(xerr.WithdrawalCancelForbidden), "cannot cancel other user's withdrawal %d", req.WithdrawalId) + } + + if err := tx.Model(&usermodel.Withdrawal{}). + Where("id = ? AND status = ?", req.WithdrawalId, constant.WithdrawalStatusPending). + Updates(map[string]interface{}{"status": constant.WithdrawalStatusCancelled}).Error; err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "cancel withdrawal failed: %v", err) + } + + if err := l.svcCtx.UserModel.UpdateCommission(l.ctx, u.Id, withdrawal.Amount, tx); err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "refund commission failed: %v", err) + } + + if err := logicCommon.WriteCommissionLog(tx, u.Id, log.CommissionTypeWithdrawCancel, withdrawal.Amount, ""); err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "write commission log failed: %v", err) + } + + return nil + }) +} diff --git a/internal/logic/public/user/commissionWithdrawLogic.go b/internal/logic/public/user/commissionWithdrawLogic.go index bbe9541..5cf1624 100644 --- a/internal/logic/public/user/commissionWithdrawLogic.go +++ b/internal/logic/public/user/commissionWithdrawLogic.go @@ -2,6 +2,7 @@ package user import ( "context" + "strings" "time" logicCommon "github.com/perfect-panel/server/internal/logic/common" @@ -38,6 +39,21 @@ func (l *CommissionWithdrawLogic) CommissionWithdraw(req *types.CommissionWithdr 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) @@ -65,11 +81,14 @@ func (l *CommissionWithdrawLogic) CommissionWithdraw(req *types.CommissionWithdr } err = tx.Model(&user.Withdrawal{}).Create(&user.Withdrawal{ - UserId: u.Id, - Amount: req.Amount, - Content: req.Content, - Status: 0, - Reason: "", + 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 { @@ -85,8 +104,11 @@ func (l *CommissionWithdrawLogic) CommissionWithdraw(req *types.CommissionWithdr return &types.WithdrawalLog{ UserId: u.Id, Amount: req.Amount, + Method: req.Method, + Account: req.Account, + QrCodeUrl: req.QrCodeUrl, Content: req.Content, - Status: 0, + Status: constant.WithdrawalStatusPending, Reason: "", CreatedAt: now.UnixMilli(), UpdatedAt: now.UnixMilli(), diff --git a/internal/logic/public/user/queryWithdrawalLogLogic.go b/internal/logic/public/user/queryWithdrawalLogLogic.go index 79e8a2c..2f85cd1 100644 --- a/internal/logic/public/user/queryWithdrawalLogLogic.go +++ b/internal/logic/public/user/queryWithdrawalLogLogic.go @@ -61,6 +61,9 @@ func (l *QueryWithdrawalLogLogic) QueryWithdrawalLog(req *types.QueryWithdrawalL Id: row.Id, UserId: row.UserId, Amount: row.Amount, + Method: row.Method, + Account: row.Account, + QrCodeUrl: row.QrCodeUrl, Content: row.Content, Status: row.Status, Reason: row.Reason, diff --git a/internal/model/log/log.go b/internal/model/log/log.go index c970d91..34cfbe4 100644 --- a/internal/model/log/log.go +++ b/internal/model/log/log.go @@ -49,7 +49,8 @@ const ( CommissionTypeWithdraw uint16 = 334 // withdraw CommissionTypeAdjust uint16 = 335 // Admin Adjust CommissionTypeConvertBalance uint16 = 336 // Convert to Balance - CommissionTypeWithdrawReject uint16 = 337 // Withdraw rejected refund + CommissionTypeWithdrawReject uint16 = 337 // Withdraw rejected refund + CommissionTypeWithdrawCancel uint16 = 338 // Withdraw cancelled refund GiftTypeIncrease uint16 = 341 // Increase GiftTypeReduce uint16 = 342 // Reduce ) diff --git a/internal/model/user/user.go b/internal/model/user/user.go index c9ff2bd..95e10d9 100644 --- a/internal/model/user/user.go +++ b/internal/model/user/user.go @@ -167,12 +167,15 @@ 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"` + Method uint8 `gorm:"type:tinyint(1);default:0;comment:Payment Method: 0:Other 1:Alipay 2:WeChat 3:Bank"` + Account string `gorm:"type:varchar(255);default:'';comment:Payment Account"` + QrCodeUrl string `gorm:"type:varchar(500);default:'';comment:QR Code Image URL"` + 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"` CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"` UpdatedAt time.Time `gorm:"comment:Update Time"` } func (*Withdrawal) TableName() string { - return "user_withdrawal" + return "withdrawals" } diff --git a/internal/types/types.go b/internal/types/types.go index 385aec1..115471d 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -289,8 +289,11 @@ type CommissionLog struct { } type CommissionWithdrawRequest struct { - Amount int64 `json:"amount"` - Content string `json:"content"` + Amount int64 `json:"amount"` + Method uint8 `json:"method"` + Account string `json:"account"` + QrCodeUrl string `json:"qr_code_url"` + Content string `json:"content"` } type ConnectionRecords struct { @@ -2284,11 +2287,16 @@ type QueryWithdrawalLogListResponse struct { Total int64 `json:"total"` } +type CancelWithdrawalRequest struct { + WithdrawalId int64 `json:"withdrawal_id" validate:"required,gt=0"` +} + type GetWithdrawalListRequest struct { Page int `form:"page"` Size int `form:"size"` UserId *int64 `form:"user_id,omitempty"` Status *uint8 `form:"status,omitempty"` + Method *uint8 `form:"method,omitempty"` } type GetWithdrawalListResponse struct { @@ -3604,6 +3612,9 @@ type WithdrawalLog struct { Id int64 `json:"id"` UserId int64 `json:"user_id"` Amount int64 `json:"amount"` + Method uint8 `json:"method"` + Account string `json:"account"` + QrCodeUrl string `json:"qr_code_url"` Content string `json:"content"` Status uint8 `json:"status"` Reason string `json:"reason,omitempty"` diff --git a/pkg/constant/withdrawal.go b/pkg/constant/withdrawal.go new file mode 100644 index 0000000..36c5c1b --- /dev/null +++ b/pkg/constant/withdrawal.go @@ -0,0 +1,8 @@ +package constant + +const ( + WithdrawalStatusPending uint8 = 0 + WithdrawalStatusApproved uint8 = 1 + WithdrawalStatusRejected uint8 = 2 + WithdrawalStatusCancelled uint8 = 3 +) diff --git a/pkg/xerr/errCode.go b/pkg/xerr/errCode.go index 9313241..7fca984 100644 --- a/pkg/xerr/errCode.go +++ b/pkg/xerr/errCode.go @@ -38,6 +38,7 @@ const ( FamilyStatusInvalid uint32 = 20018 FamilyOwnerOperationForbidden uint32 = 20019 WithdrawalStatusInvalid uint32 = 20020 + WithdrawalCancelForbidden uint32 = 20021 ) // Node error diff --git a/pkg/xerr/errMsg.go b/pkg/xerr/errMsg.go index 4ec5ae2..5aa7358 100644 --- a/pkg/xerr/errMsg.go +++ b/pkg/xerr/errMsg.go @@ -47,6 +47,7 @@ func init() { FamilyStatusInvalid: "家庭组状态无效", FamilyOwnerOperationForbidden: "家庭组所有者不允许此操作", WithdrawalStatusInvalid: "提现状态无效", + WithdrawalCancelForbidden: "无权取消该提现", // Node error NodeExist: "Node already exists",