529 lines
17 KiB
Go
529 lines
17 KiB
Go
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)
|
||
}
|
||
|