Files
Go-Web-Template/server/api/v1/admin/app_withdraw_orders.go
2026-04-23 15:29:07 +08:00

529 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package admin
import (
"errors"
"strconv"
"strings"
"time"
"git.echol.cn/loser/Go-Web-Template/server/global"
appModel "git.echol.cn/loser/Go-Web-Template/server/model/app"
"git.echol.cn/loser/Go-Web-Template/server/model/common/response"
"git.echol.cn/loser/Go-Web-Template/server/utils"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type AppWithdrawOrdersApi struct{}
type appWithdrawOrderListItem struct {
ID uint `json:"id"`
AppUserID uint `json:"appUserId"`
Username string `json:"username"`
AmountLi int64 `json:"amountLi"`
FeeLi int64 `json:"feeLi"`
NetLi int64 `json:"netLi"`
Status string `json:"status"`
CreatedAt time.Time `json:"createdAt"`
OperatorUserID uint `json:"operatorUserId"`
ApprovedBy uint `json:"approvedBy"`
ApprovedAt *time.Time `json:"approvedAt"`
PaidBy uint `json:"paidBy"`
PaidAt *time.Time `json:"paidAt"`
}
// GetAppWithdrawOrdersList
// @Tags AppWithdrawOrdersAdmin
// @Summary 后台获取提现订单列表(单位:厘)
// @Security ApiKeyAuth
// @Produce application/json
// @Param page query int false "页码" default(1)
// @Param pageSize query int false "每页大小" default(10)
// @Param userId query int false "玩家ID"
// @Param username query string false "用户名(模糊)"
// @Param status query string false "状态pending/approved/rejected/paid/failed"
// @Param startAt query string false "开始时间(ISO8601/RFC3339)"
// @Param endAt query string false "结束时间(ISO8601/RFC3339)"
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
// @Router /admin/app-withdraw-orders [get]
func (a *AppWithdrawOrdersApi) GetAppWithdrawOrdersList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 200 {
pageSize = 10
}
var userID uint
if raw := strings.TrimSpace(c.Query("userId")); raw != "" {
if v, err := strconv.Atoi(raw); err == nil && v > 0 {
userID = uint(v)
}
}
username := strings.TrimSpace(c.Query("username"))
status := strings.TrimSpace(c.Query("status"))
var startAt *time.Time
if raw := strings.TrimSpace(c.Query("startAt")); raw != "" {
if t, err := time.Parse(time.RFC3339, raw); err == nil {
startAt = &t
}
}
var endAt *time.Time
if raw := strings.TrimSpace(c.Query("endAt")); raw != "" {
if t, err := time.Parse(time.RFC3339, raw); err == nil {
endAt = &t
}
}
db := global.GVA_DB.Model(&appModel.AppWithdrawOrder{})
if userID > 0 {
db = db.Where("app_user_id = ?", userID)
}
if username != "" {
db = db.Where("username LIKE ?", "%"+username+"%")
}
if status != "" {
db = db.Where("status = ?", status)
}
if startAt != nil {
db = db.Where("created_at >= ?", *startAt)
}
if endAt != nil {
db = db.Where("created_at <= ?", *endAt)
}
var total int64
if err := db.Count(&total).Error; err != nil {
response.FailWithMessage("获取失败:"+err.Error(), c)
return
}
var list []appWithdrawOrderListItem
if err := db.
Order("id desc").
Limit(pageSize).
Offset((page - 1) * pageSize).
Select("id, app_user_id, username, amount_li, fee_li, net_li, status, created_at, operator_user_id, approved_by, approved_at, paid_by, paid_at").
Scan(&list).Error; err != nil {
response.FailWithMessage("获取失败:"+err.Error(), c)
return
}
response.OkWithData(response.PageResult{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
}, c)
}
// GetAppWithdrawOrderById
// @Tags AppWithdrawOrdersAdmin
// @Summary 后台获取提现订单详情(单位:厘)
// @Security ApiKeyAuth
// @Produce application/json
// @Param id path int true "订单ID"
// @Success 200 {object} response.Response{data=appModel.AppWithdrawOrder,msg=string} "获取成功"
// @Router /admin/app-withdraw-orders/{id} [get]
func (a *AppWithdrawOrdersApi) GetAppWithdrawOrderById(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
response.FailWithMessage("参数错误", c)
return
}
var rec appModel.AppWithdrawOrder
if err := global.GVA_DB.First(&rec, uint(id)).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.FailWithMessage("订单不存在", c)
return
}
response.FailWithMessage("获取失败:"+err.Error(), c)
return
}
response.OkWithData(rec, c)
}
type createWithdrawOrderRequest struct {
AppUserID uint `json:"appUserId" binding:"required"`
AmountLi int64 `json:"amountLi" binding:"required"`
FeeLi int64 `json:"feeLi"`
Channel string `json:"channel"`
PayeeAccount string `json:"payeeAccount"`
PayeeRealName string `json:"payeeRealName"`
ApplyRemark string `json:"applyRemark"`
}
// CreateAppWithdrawOrder
// @Tags AppWithdrawOrdersAdmin
// @Summary 后台创建提现订单(会冻结账户余额)
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param body body createWithdrawOrderRequest true "参数"
// @Success 200 {object} response.Response{msg=string} "创建成功"
// @Router /admin/app-withdraw-orders [post]
func (a *AppWithdrawOrdersApi) CreateAppWithdrawOrder(c *gin.Context) {
var req createWithdrawOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage("参数错误", c)
return
}
if req.AmountLi <= 0 {
response.FailWithMessage("amountLi 必须 > 0", c)
return
}
if req.FeeLi < 0 {
response.FailWithMessage("feeLi 不合法", c)
return
}
net := req.AmountLi - req.FeeLi
if net <= 0 {
response.FailWithMessage("手续费不能大于等于提现金额", c)
return
}
operatorID := utils.GetUserID(c)
if operatorID == 0 {
response.FailWithMessage("未登录", c)
return
}
if err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
var user appModel.AppUser
if err := tx.First(&user, req.AppUserID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("玩家不存在")
}
return err
}
var asset appModel.AppUserAsset
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("app_user_id = ?", user.ID).First(&asset).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
asset = appModel.AppUserAsset{AppUserID: user.ID}
if err := tx.Create(&asset).Error; err != nil {
return err
}
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("app_user_id = ?", user.ID).First(&asset).Error; err != nil {
return err
}
} else if err != nil {
return err
}
available := asset.AccountBalanceLi - asset.AccountFrozenLi
if available < req.AmountLi {
return errors.New("可提现余额不足")
}
asset.AccountFrozenLi += req.AmountLi
if err := tx.Model(&asset).Select("account_frozen_li").Updates(&asset).Error; err != nil {
return err
}
order := appModel.AppWithdrawOrder{
AppUserID: user.ID,
Username: user.Username,
AmountLi: req.AmountLi,
FeeLi: req.FeeLi,
NetLi: net,
Status: appModel.AppWithdrawOrderStatusPending,
Channel: strings.TrimSpace(req.Channel),
PayeeAccount: strings.TrimSpace(req.PayeeAccount),
PayeeRealName: strings.TrimSpace(req.PayeeRealName),
ApplyRemark: strings.TrimSpace(req.ApplyRemark),
OperatorUserID: operatorID,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return tx.Create(&order).Error
}); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
response.OkWithMessage("创建成功", c)
}
type auditWithdrawOrderRequest struct {
AuditRemark string `json:"auditRemark"`
RejectReason string `json:"rejectReason"`
}
// ApproveAppWithdrawOrder
// @Tags AppWithdrawOrdersAdmin
// @Summary 审核通过提现订单
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param id path int true "订单ID"
// @Param body body auditWithdrawOrderRequest false "参数"
// @Success 200 {object} response.Response{msg=string} "操作成功"
// @Router /admin/app-withdraw-orders/{id}/approve [post]
func (a *AppWithdrawOrdersApi) ApproveAppWithdrawOrder(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
response.FailWithMessage("参数错误", c)
return
}
operatorID := utils.GetUserID(c)
if operatorID == 0 {
response.FailWithMessage("未登录", c)
return
}
var req auditWithdrawOrderRequest
_ = c.ShouldBindJSON(&req)
now := time.Now()
if err := global.GVA_DB.Model(&appModel.AppWithdrawOrder{}).
Where("id = ? AND status = ?", uint(id), appModel.AppWithdrawOrderStatusPending).
Updates(map[string]any{
"status": appModel.AppWithdrawOrderStatusApproved,
"audit_remark": strings.TrimSpace(req.AuditRemark),
"approved_by": operatorID,
"approved_at": &now,
"operator_user_id": operatorID,
}).Error; err != nil {
response.FailWithMessage("操作失败:"+err.Error(), c)
return
}
response.OkWithMessage("已审核通过", c)
}
// RejectAppWithdrawOrder
// @Tags AppWithdrawOrdersAdmin
// @Summary 审核拒绝提现订单(会解冻)
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param id path int true "订单ID"
// @Param body body auditWithdrawOrderRequest true "参数"
// @Success 200 {object} response.Response{msg=string} "操作成功"
// @Router /admin/app-withdraw-orders/{id}/reject [post]
func (a *AppWithdrawOrdersApi) RejectAppWithdrawOrder(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
response.FailWithMessage("参数错误", c)
return
}
operatorID := utils.GetUserID(c)
if operatorID == 0 {
response.FailWithMessage("未登录", c)
return
}
var req auditWithdrawOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage("参数错误", c)
return
}
reason := strings.TrimSpace(req.RejectReason)
if reason == "" {
response.FailWithMessage("rejectReason 不能为空", c)
return
}
if err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
var order appModel.AppWithdrawOrder
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, uint(id)).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("订单不存在")
}
return err
}
if order.Status != appModel.AppWithdrawOrderStatusPending {
return errors.New("当前状态不可拒绝")
}
var asset appModel.AppUserAsset
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("app_user_id = ?", order.AppUserID).First(&asset).Error; err != nil {
return err
}
if asset.AccountFrozenLi < order.AmountLi {
return errors.New("冻结余额异常")
}
asset.AccountFrozenLi -= order.AmountLi
if err := tx.Model(&asset).Select("account_frozen_li").Updates(&asset).Error; err != nil {
return err
}
now := time.Now()
return tx.Model(&order).Updates(map[string]any{
"status": appModel.AppWithdrawOrderStatusRejected,
"reject_reason": reason,
"audit_remark": strings.TrimSpace(req.AuditRemark),
"approved_by": operatorID,
"approved_at": &now,
"operator_user_id": operatorID,
}).Error
}); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
response.OkWithMessage("已拒绝并解冻", c)
}
type payWithdrawOrderRequest struct {
ExternalTxID string `json:"externalTxId"`
FailureReason string `json:"failureReason"`
}
// MarkPaidAppWithdrawOrder
// @Tags AppWithdrawOrdersAdmin
// @Summary 确认已打款(会扣减余额并解冻)
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param id path int true "订单ID"
// @Param body body payWithdrawOrderRequest false "参数"
// @Success 200 {object} response.Response{msg=string} "操作成功"
// @Router /admin/app-withdraw-orders/{id}/mark-paid [post]
func (a *AppWithdrawOrdersApi) MarkPaidAppWithdrawOrder(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
response.FailWithMessage("参数错误", c)
return
}
operatorID := utils.GetUserID(c)
if operatorID == 0 {
response.FailWithMessage("未登录", c)
return
}
var req payWithdrawOrderRequest
_ = c.ShouldBindJSON(&req)
if err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
var order appModel.AppWithdrawOrder
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, uint(id)).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("订单不存在")
}
return err
}
if order.Status != appModel.AppWithdrawOrderStatusApproved {
return errors.New("当前状态不可打款确认")
}
var asset appModel.AppUserAsset
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("app_user_id = ?", order.AppUserID).First(&asset).Error; err != nil {
return err
}
if asset.AccountFrozenLi < order.AmountLi {
return errors.New("冻结余额异常")
}
before := asset.AccountBalanceLi
if before < order.AmountLi {
return errors.New("账户余额不足")
}
asset.AccountFrozenLi -= order.AmountLi
asset.AccountBalanceLi -= order.AmountLi
if err := tx.Model(&asset).Select("account_frozen_li", "account_balance_li").Updates(&asset).Error; err != nil {
return err
}
after := asset.AccountBalanceLi
txRec := appModel.AppAssetTransaction{
AppUserID: order.AppUserID,
AssetType: appModel.AppAssetTypeAccount,
RechargeType: appModel.AppRechargeType("withdraw_paid"),
DeltaLi: -order.AmountLi,
BeforeLi: before,
AfterLi: after,
Reason: "提现打款(订单#" + strconv.Itoa(int(order.ID)) + "",
OperatorUserID: operatorID,
CreatedAt: time.Now(),
}
if err := tx.Create(&txRec).Error; err != nil {
return err
}
now := time.Now()
return tx.Model(&order).Updates(map[string]any{
"status": appModel.AppWithdrawOrderStatusPaid,
"paid_by": operatorID,
"paid_at": &now,
"external_tx_id": strings.TrimSpace(req.ExternalTxID),
"operator_user_id": operatorID,
}).Error
}); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
response.OkWithMessage("已确认打款", c)
}
// MarkFailedAppWithdrawOrder
// @Tags AppWithdrawOrdersAdmin
// @Summary 标记打款失败(会解冻)
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param id path int true "订单ID"
// @Param body body payWithdrawOrderRequest true "参数"
// @Success 200 {object} response.Response{msg=string} "操作成功"
// @Router /admin/app-withdraw-orders/{id}/mark-failed [post]
func (a *AppWithdrawOrdersApi) MarkFailedAppWithdrawOrder(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
response.FailWithMessage("参数错误", c)
return
}
operatorID := utils.GetUserID(c)
if operatorID == 0 {
response.FailWithMessage("未登录", c)
return
}
var req payWithdrawOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage("参数错误", c)
return
}
reason := strings.TrimSpace(req.FailureReason)
if reason == "" {
response.FailWithMessage("failureReason 不能为空", c)
return
}
if err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
var order appModel.AppWithdrawOrder
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, uint(id)).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("订单不存在")
}
return err
}
if order.Status != appModel.AppWithdrawOrderStatusApproved {
return errors.New("当前状态不可标记失败")
}
var asset appModel.AppUserAsset
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("app_user_id = ?", order.AppUserID).First(&asset).Error; err != nil {
return err
}
if asset.AccountFrozenLi < order.AmountLi {
return errors.New("冻结余额异常")
}
asset.AccountFrozenLi -= order.AmountLi
if err := tx.Model(&asset).Select("account_frozen_li").Updates(&asset).Error; err != nil {
return err
}
return tx.Model(&order).Updates(map[string]any{
"status": appModel.AppWithdrawOrderStatusFailed,
"failure_reason": reason,
"external_tx_id": strings.TrimSpace(req.ExternalTxID),
"operator_user_id": operatorID,
}).Error
}); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
response.OkWithMessage("已标记失败并解冻", c)
}