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) }