新增几大中心功能

This commit is contained in:
Administrator
2026-04-23 15:29:07 +08:00
parent 5c2a146a44
commit c6354ee065
123 changed files with 13074 additions and 229 deletions

View File

@@ -0,0 +1,234 @@
package admin
import (
"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"
"github.com/gin-gonic/gin"
)
type AppAssetTransactionsApi struct{}
type appAssetTransactionListItem struct {
ID uint `json:"id"`
AppUserID uint `json:"appUserId"`
Username string `json:"username"`
AssetType string `json:"assetType"`
RechargeType string `json:"rechargeType"`
DeltaLi int64 `json:"deltaLi"`
BeforeLi int64 `json:"beforeLi"`
AfterLi int64 `json:"afterLi"`
Reason string `json:"reason"`
OperatorUserID uint `json:"operatorUserId"`
OperatorName string `json:"operatorName"`
CreatedAt time.Time `json:"createdAt"`
}
// GetAppAssetTransactionsList
// @Tags AppAssetTransactionsAdmin
// @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 assetType query string false "资产类型account/game_coin"
// @Param rechargeType query string false "变动类型充值类型offline/online_not_arrived/activity_subsidy/manual_adjustment"
// @Param operatorUserId query int false "操作人ID"
// @Param keyword query string false "关键字(原因模糊)"
// @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-asset-transactions [get]
func (a *AppAssetTransactionsApi) GetAppAssetTransactionsList(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
operatorUserID uint
)
if raw := strings.TrimSpace(c.Query("userId")); raw != "" {
if v, err := strconv.Atoi(raw); err == nil && v > 0 {
userID = uint(v)
}
}
if raw := strings.TrimSpace(c.Query("operatorUserId")); raw != "" {
if v, err := strconv.Atoi(raw); err == nil && v > 0 {
operatorUserID = uint(v)
}
}
username := strings.TrimSpace(c.Query("username"))
assetType := strings.TrimSpace(c.Query("assetType"))
rechargeType := strings.TrimSpace(c.Query("rechargeType"))
keyword := strings.TrimSpace(c.Query("keyword"))
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
}
}
// tx + join app_users + join sys_users
db := global.GVA_DB.Table("app_asset_transactions tx").
Joins("JOIN app_users u ON u.id = tx.app_user_id").
Joins("LEFT JOIN sys_users su ON su.id = tx.operator_user_id")
if userID > 0 {
db = db.Where("tx.app_user_id = ?", userID)
}
if username != "" {
db = db.Where("u.username ILIKE ?", "%"+username+"%")
}
if assetType != "" {
db = db.Where("tx.asset_type = ?", assetType)
}
if rechargeType != "" {
db = db.Where("tx.recharge_type = ?", rechargeType)
}
if operatorUserID > 0 {
db = db.Where("tx.operator_user_id = ?", operatorUserID)
}
if keyword != "" {
db = db.Where("tx.reason ILIKE ?", "%"+keyword+"%")
}
if startAt != nil {
db = db.Where("tx.created_at >= ?", *startAt)
}
if endAt != nil {
db = db.Where("tx.created_at <= ?", *endAt)
}
var total int64
if err := db.Count(&total).Error; err != nil {
response.FailWithMessage(err.Error(), c)
return
}
type row struct {
appModel.AppAssetTransaction
Username string `gorm:"column:username"`
OperatorName string `gorm:"column:operator_name"`
}
var rows []row
if err := db.Select([]string{
"tx.id",
"tx.app_user_id",
"tx.asset_type",
"tx.recharge_type",
"tx.delta_li",
"tx.before_li",
"tx.after_li",
"tx.reason",
"tx.operator_user_id",
"tx.created_at",
"u.username AS username",
"su.username AS operator_name",
}).Order("tx.id DESC").Limit(pageSize).Offset((page - 1) * pageSize).Scan(&rows).Error; err != nil {
response.FailWithMessage(err.Error(), c)
return
}
items := make([]appAssetTransactionListItem, 0, len(rows))
for _, r := range rows {
items = append(items, appAssetTransactionListItem{
ID: r.ID,
AppUserID: r.AppUserID,
Username: r.Username,
AssetType: string(r.AssetType),
RechargeType: string(r.RechargeType),
DeltaLi: r.DeltaLi,
BeforeLi: r.BeforeLi,
AfterLi: r.AfterLi,
Reason: r.Reason,
OperatorUserID: r.OperatorUserID,
OperatorName: r.OperatorName,
CreatedAt: r.CreatedAt,
})
}
response.OkWithDetailed(response.PageResult{
List: items,
Total: total,
Page: page,
PageSize: pageSize,
}, "获取成功", c)
}
// GetAppAssetTransactionById
// @Tags AppAssetTransactionsAdmin
// @Summary 后台获取玩家资产流水详情
// @Security ApiKeyAuth
// @Produce application/json
// @Param id path int true "流水ID"
// @Success 200 {object} response.Response{data=appAssetTransactionListItem,msg=string} "获取成功"
// @Router /admin/app-asset-transactions/{id} [get]
func (a *AppAssetTransactionsApi) GetAppAssetTransactionById(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
response.FailWithMessage("参数错误", c)
return
}
type row struct {
appModel.AppAssetTransaction
Username string `gorm:"column:username"`
OperatorName string `gorm:"column:operator_name"`
}
var r row
err := global.GVA_DB.Table("app_asset_transactions tx").
Joins("JOIN app_users u ON u.id = tx.app_user_id").
Joins("LEFT JOIN sys_users su ON su.id = tx.operator_user_id").
Select([]string{
"tx.id",
"tx.app_user_id",
"tx.asset_type",
"tx.recharge_type",
"tx.delta_li",
"tx.before_li",
"tx.after_li",
"tx.reason",
"tx.operator_user_id",
"tx.created_at",
"u.username AS username",
"su.username AS operator_name",
}).Where("tx.id = ?", id).Scan(&r).Error
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
response.OkWithDetailed(appAssetTransactionListItem{
ID: r.ID,
AppUserID: r.AppUserID,
Username: r.Username,
AssetType: string(r.AssetType),
RechargeType: string(r.RechargeType),
DeltaLi: r.DeltaLi,
BeforeLi: r.BeforeLi,
AfterLi: r.AfterLi,
Reason: r.Reason,
OperatorUserID: r.OperatorUserID,
OperatorName: r.OperatorName,
CreatedAt: r.CreatedAt,
}, "获取成功", c)
}

View File

@@ -0,0 +1,301 @@
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"
appSvc "git.echol.cn/loser/Go-Web-Template/server/service/app"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type AppInviteCodesApi struct{}
type appInviteCodeListItem struct {
ID uint `json:"id"`
CreatedByUserID uint `json:"createdByUserId"`
CodeLast4 string `json:"codeLast4"`
Status appModel.InviteCodeStatus `json:"status"`
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
UsedByUserID *uint `json:"usedByUserId,omitempty"`
UsedAt *time.Time `json:"usedAt,omitempty"`
CreatedAt time.Time `json:"createdAt"`
}
type appInviteCodeDetailResponse struct {
Code appModel.AppInviteCode `json:"code"`
Inviter struct {
ID uint `json:"id"`
Username string `json:"username"`
} `json:"inviter"`
Invitee *struct {
ID uint `json:"id"`
Username string `json:"username"`
} `json:"invitee,omitempty"`
Relation *appModel.AppUserInviteRelation `json:"relation,omitempty"`
}
type issueInviteCodeRequest struct {
UserID uint `json:"userId" binding:"required"`
ExpireHours *int `json:"expireHours"`
}
type issueInviteCodeResponse struct {
Code string `json:"code"`
CodeLast4 string `json:"codeLast4"`
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
}
type clearUnusedRequest struct {
UserID uint `json:"userId" binding:"required"`
Scope string `json:"scope"` // active|all
}
// GetAppInviteCodesList
// @Tags AppInviteCodesAdmin
// @Summary 后台获取玩家邀请码列表
// @Security ApiKeyAuth
// @Produce application/json
// @Param page query int false "页码" default(1)
// @Param pageSize query int false "每页大小" default(10)
// @Param status query string false "状态 unused|used|revoked|expired"
// @Param createdByUserId query int false "邀请人用户ID"
// @Param usedByUserId query int false "使用人用户ID"
// @Param codeLast4 query string false "末4位"
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
// @Router /admin/app-invite-codes [get]
func (a *AppInviteCodesApi) GetAppInviteCodesList(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
}
status := strings.TrimSpace(c.Query("status"))
createdByUserID, _ := strconv.Atoi(c.Query("createdByUserId"))
usedByUserID, _ := strconv.Atoi(c.Query("usedByUserId"))
codeLast4 := strings.TrimSpace(c.Query("codeLast4"))
db := global.GVA_DB.Model(&appModel.AppInviteCode{})
if status != "" && status != "expired" {
db = db.Where("status = ?", status)
}
if createdByUserID > 0 {
db = db.Where("created_by_user_id = ?", createdByUserID)
}
if usedByUserID > 0 {
db = db.Where("used_by_user_id = ?", usedByUserID)
}
if codeLast4 != "" {
db = db.Where("code_last4 = ?", codeLast4)
}
// 仅查询“已过期”视图expires_at < now AND status=unused
if status == "expired" {
now := time.Now()
db = db.Where("status = ?", appModel.InviteCodeStatusUnused).Where("expires_at IS NOT NULL AND expires_at <= ?", now)
}
var total int64
if err := db.Count(&total).Error; err != nil {
response.FailWithMessage(err.Error(), c)
return
}
var list []appModel.AppInviteCode
if err := db.Order("id desc").Limit(pageSize).Offset((page - 1) * pageSize).Find(&list).Error; err != nil {
response.FailWithMessage(err.Error(), c)
return
}
items := make([]appInviteCodeListItem, 0, len(list))
for _, rec := range list {
items = append(items, appInviteCodeListItem{
ID: rec.ID,
CreatedByUserID: rec.CreatedByUserID,
CodeLast4: rec.CodeLast4,
Status: rec.Status,
ExpiresAt: rec.ExpiresAt,
UsedByUserID: rec.UsedByUserID,
UsedAt: rec.UsedAt,
CreatedAt: rec.CreatedAt,
})
}
response.OkWithDetailed(response.PageResult{
List: items,
Total: total,
Page: page,
PageSize: pageSize,
}, "获取成功", c)
}
// GetAppInviteCodeById
// @Tags AppInviteCodesAdmin
// @Summary 后台获取玩家邀请码详情
// @Security ApiKeyAuth
// @Produce application/json
// @Param id path int true "邀请码ID"
// @Success 200 {object} response.Response{data=appInviteCodeDetailResponse,msg=string} "获取成功"
// @Router /admin/app-invite-codes/{id} [get]
func (a *AppInviteCodesApi) GetAppInviteCodeById(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
response.FailWithMessage("参数错误", c)
return
}
var code appModel.AppInviteCode
if err := global.GVA_DB.First(&code, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.FailWithMessage("记录不存在", c)
return
}
response.FailWithMessage(err.Error(), c)
return
}
var inviter appModel.AppUser
_ = global.GVA_DB.Select("id, username").First(&inviter, code.CreatedByUserID).Error
var relation appModel.AppUserInviteRelation
relErr := global.GVA_DB.Where("invite_code_id = ?", code.ID).First(&relation).Error
var invitee *appModel.AppUser = nil
if relErr == nil {
var u appModel.AppUser
if err := global.GVA_DB.Select("id, username").First(&u, relation.InviteeUserID).Error; err == nil {
invitee = &u
}
}
payload := appInviteCodeDetailResponse{Code: code}
payload.Inviter.ID = inviter.ID
payload.Inviter.Username = inviter.Username
if invitee != nil {
payload.Invitee = &struct {
ID uint `json:"id"`
Username string `json:"username"`
}{ID: invitee.ID, Username: invitee.Username}
}
if relErr == nil {
payload.Relation = &relation
}
response.OkWithData(payload, c)
}
// RevokeAppInviteCode
// @Tags AppInviteCodesAdmin
// @Summary 作废玩家邀请码
// @Security ApiKeyAuth
// @Produce application/json
// @Param id path int true "邀请码ID"
// @Success 200 {object} response.Response{msg=string} "作废成功"
// @Router /admin/app-invite-codes/{id}/revoke [post]
func (a *AppInviteCodesApi) RevokeAppInviteCode(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
response.FailWithMessage("参数错误", c)
return
}
var code appModel.AppInviteCode
if err := global.GVA_DB.First(&code, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.FailWithMessage("记录不存在", c)
return
}
response.FailWithMessage(err.Error(), c)
return
}
if code.Status == appModel.InviteCodeStatusUsed {
response.FailWithMessage("已使用的邀请码不可作废", c)
return
}
if code.Status == appModel.InviteCodeStatusRevoked {
response.OkWithMessage("已作废", c)
return
}
if err := global.GVA_DB.Model(&code).Update("status", appModel.InviteCodeStatusRevoked).Error; err != nil {
response.FailWithMessage(err.Error(), c)
return
}
response.OkWithMessage("作废成功", c)
}
// IssueAppInviteCodeForUser
// @Tags AppInviteCodesAdmin
// @Summary 后台代用户生成邀请码(返回明文一次性)
// @Security ApiKeyAuth
// @Produce application/json
// @Param data body issueInviteCodeRequest true "用户ID"
// @Success 200 {object} response.Response{data=issueInviteCodeResponse,msg=string} "生成成功"
// @Router /admin/app-invite-codes/issue [post]
func (a *AppInviteCodesApi) IssueAppInviteCodeForUser(c *gin.Context) {
var req issueInviteCodeRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
// 先强制作废该用户所有 active unused
now := time.Now()
revokeDB := global.GVA_DB.Model(&appModel.AppInviteCode{}).
Where("created_by_user_id = ? AND status = ?", req.UserID, appModel.InviteCodeStatusUnused).
Where(global.GVA_DB.Where("expires_at IS NULL").Or("expires_at > ?", now)).
Update("status", appModel.InviteCodeStatusRevoked)
if revokeDB.Error != nil {
response.FailWithMessage(revokeDB.Error.Error(), c)
return
}
// 复用现有生成逻辑(按系统参数 expireHours若你传了 expireHours本期先忽略后续可扩展为临时覆盖。
res, err := appSvc.AppInviteCodeServiceApp.Generate(req.UserID)
if err != nil {
// 若生成被“已有未使用码”拦住,说明 revoke 未覆盖(例如过期时间为空/逻辑差异),直接返回错误即可。
response.FailWithMessage(err.Error(), c)
return
}
response.OkWithData(issueInviteCodeResponse{Code: res.Code, CodeLast4: res.CodeLast4, ExpiresAt: res.ExpiresAt}, c)
}
// ClearUnusedInviteCodes
// @Tags AppInviteCodesAdmin
// @Summary 强制清空用户未使用邀请码(批量作废)
// @Security ApiKeyAuth
// @Produce application/json
// @Param data body clearUnusedRequest true "用户ID"
// @Success 200 {object} response.Response{msg=string} "清空成功"
// @Router /admin/app-invite-codes/clear-unused [post]
func (a *AppInviteCodesApi) ClearUnusedInviteCodes(c *gin.Context) {
var req clearUnusedRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
scope := strings.TrimSpace(req.Scope)
if scope == "" {
scope = "active"
}
db := global.GVA_DB.Model(&appModel.AppInviteCode{}).
Where("created_by_user_id = ? AND status = ?", req.UserID, appModel.InviteCodeStatusUnused)
if scope == "active" {
now := time.Now()
db = db.Where(global.GVA_DB.Where("expires_at IS NULL").Or("expires_at > ?", now))
}
if err := db.Update("status", appModel.InviteCodeStatusRevoked).Error; err != nil {
response.FailWithMessage(err.Error(), c)
return
}
response.OkWithMessage("清空成功", c)
}

View File

@@ -0,0 +1,138 @@
package admin
import (
"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"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type AppLoginLogsApi struct{}
type appLoginLogListItem struct {
ID uint `json:"id"`
AppUserID uint `json:"appUserId"`
Username string `json:"username"`
IP string `json:"ip"`
Referer string `json:"referer,omitempty"`
UserAgent string `json:"userAgent,omitempty"`
Status bool `json:"status"`
ErrorMessage string `json:"errorMessage,omitempty"`
CreatedAt time.Time `json:"createdAt"`
}
type appLoginLogListResponse struct {
List []appLoginLogListItem `json:"list"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
// GetAppLoginLogsList
// @Tags AppLoginLogsAdmin
// @Summary 后台获取玩家登录日志列表
// @Security ApiKeyAuth
// @Produce application/json
// @Param page query int true "页码"
// @Param pageSize query int true "每页条数"
// @Param username query string false "用户名"
// @Param ip query string false "IP"
// @Param status query bool false "状态true成功 false失败"
// @Param startAt query string false "开始时间(ISO8601/RFC3339)"
// @Param endAt query string false "结束时间(ISO8601/RFC3339)"
// @Success 200 {object} response.Response{data=appLoginLogListResponse,msg=string} "获取成功"
// @Router /admin/app-login-logs [get]
func (a *AppLoginLogsApi) GetAppLoginLogsList(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
}
username := strings.TrimSpace(c.Query("username"))
ip := strings.TrimSpace(c.Query("ip"))
var status *bool
if raw := strings.TrimSpace(c.Query("status")); raw != "" {
v, err := strconv.ParseBool(raw)
if err == nil {
status = &v
}
}
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.AppLoginLog{})
if username != "" {
db = db.Where("username LIKE ?", "%"+username+"%")
}
if ip != "" {
db = db.Where("ip LIKE ?", "%"+ip+"%")
}
if status != nil {
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 rows []appModel.AppLoginLog
if err := db.Order("id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&rows).Error; err != nil {
if err == gorm.ErrRecordNotFound {
rows = []appModel.AppLoginLog{}
} else {
response.FailWithMessage(err.Error(), c)
return
}
}
items := make([]appLoginLogListItem, 0, len(rows))
for _, r := range rows {
items = append(items, appLoginLogListItem{
ID: r.ID,
AppUserID: r.AppUserID,
Username: r.Username,
IP: r.IP,
Referer: r.Referer,
UserAgent: r.UserAgent,
Status: r.Status,
ErrorMessage: r.ErrorMessage,
CreatedAt: r.CreatedAt,
})
}
response.OkWithDetailed(appLoginLogListResponse{
List: items,
Total: total,
Page: page,
PageSize: pageSize,
}, "获取成功", c)
}

View File

@@ -0,0 +1,283 @@
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"
)
type AppRiskTagsApi struct{}
// GetAppRiskTagsList
// @Tags AppRiskTagsAdmin
// @Summary 后台获取风险标签列表
// @Security ApiKeyAuth
// @Produce application/json
// @Param page query int false "页码" default(1)
// @Param pageSize query int false "每页大小" default(50)
// @Param keyword query string false "关键字(名称模糊)"
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
// @Router /admin/app-risk-tags [get]
func (a *AppRiskTagsApi) GetAppRiskTagsList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "50"))
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 200 {
pageSize = 50
}
keyword := strings.TrimSpace(c.Query("keyword"))
db := global.GVA_DB.Model(&appModel.AppRiskTag{})
if keyword != "" {
db = db.Where("name LIKE ?", "%"+keyword+"%")
}
var total int64
if err := db.Count(&total).Error; err != nil {
response.FailWithMessage("获取失败:"+err.Error(), c)
return
}
var list []appModel.AppRiskTag
if err := db.Order("level desc, id desc").Limit(pageSize).Offset((page - 1) * pageSize).Find(&list).Error; err != nil {
response.FailWithMessage("获取失败:"+err.Error(), c)
return
}
response.OkWithData(response.PageResult{List: list, Total: total, Page: page, PageSize: pageSize}, c)
}
type upsertRiskTagRequest struct {
Name string `json:"name" binding:"required"`
Level int `json:"level" binding:"required"`
Color string `json:"color"`
Desc string `json:"desc"`
}
// CreateAppRiskTag
// @Tags AppRiskTagsAdmin
// @Summary 创建风险标签
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param body body upsertRiskTagRequest true "参数"
// @Success 200 {object} response.Response{msg=string} "创建成功"
// @Router /admin/app-risk-tags [post]
func (a *AppRiskTagsApi) CreateAppRiskTag(c *gin.Context) {
var req upsertRiskTagRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage("参数错误", c)
return
}
req.Name = strings.TrimSpace(req.Name)
if req.Name == "" {
response.FailWithMessage("name 不能为空", c)
return
}
if req.Level < 1 || req.Level > 5 {
response.FailWithMessage("level 必须在 1-5", c)
return
}
rec := appModel.AppRiskTag{
Name: req.Name,
Level: req.Level,
Color: strings.TrimSpace(req.Color),
Desc: strings.TrimSpace(req.Desc),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := global.GVA_DB.Create(&rec).Error; err != nil {
response.FailWithMessage("创建失败:"+err.Error(), c)
return
}
response.OkWithMessage("创建成功", c)
}
// UpdateAppRiskTag
// @Tags AppRiskTagsAdmin
// @Summary 更新风险标签
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param id path int true "标签ID"
// @Param body body upsertRiskTagRequest true "参数"
// @Success 200 {object} response.Response{msg=string} "更新成功"
// @Router /admin/app-risk-tags/{id} [put]
func (a *AppRiskTagsApi) UpdateAppRiskTag(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
response.FailWithMessage("参数错误", c)
return
}
var req upsertRiskTagRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage("参数错误", c)
return
}
req.Name = strings.TrimSpace(req.Name)
if req.Name == "" {
response.FailWithMessage("name 不能为空", c)
return
}
if req.Level < 1 || req.Level > 5 {
response.FailWithMessage("level 必须在 1-5", c)
return
}
if err := global.GVA_DB.Model(&appModel.AppRiskTag{}).Where("id = ?", uint(id)).Updates(map[string]any{
"name": req.Name,
"level": req.Level,
"color": strings.TrimSpace(req.Color),
"desc": strings.TrimSpace(req.Desc),
"updated_at": time.Now(),
}).Error; err != nil {
response.FailWithMessage("更新失败:"+err.Error(), c)
return
}
response.OkWithMessage("更新成功", c)
}
// DeleteAppRiskTag
// @Tags AppRiskTagsAdmin
// @Summary 删除风险标签
// @Security ApiKeyAuth
// @Produce application/json
// @Param id path int true "标签ID"
// @Success 200 {object} response.Response{msg=string} "删除成功"
// @Router /admin/app-risk-tags/{id} [delete]
func (a *AppRiskTagsApi) DeleteAppRiskTag(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
response.FailWithMessage("参数错误", c)
return
}
if err := global.GVA_DB.Delete(&appModel.AppRiskTag{}, uint(id)).Error; err != nil {
response.FailWithMessage("删除失败:"+err.Error(), c)
return
}
response.OkWithMessage("删除成功", c)
}
type userTagRow struct {
appModel.AppUserRiskTag
TagName string `json:"tagName" gorm:"column:tag_name"`
TagLevel int `json:"tagLevel" gorm:"column:tag_level"`
TagColor string `json:"tagColor" gorm:"column:tag_color"`
}
// GetAppUserRiskTags
// @Tags AppRiskTagsAdmin
// @Summary 获取玩家风险标签
// @Security ApiKeyAuth
// @Produce application/json
// @Param id path int true "玩家ID"
// @Success 200 {object} response.Response{data=[]userTagRow,msg=string} "获取成功"
// @Router /admin/app-users/{id}/risk-tags [get]
func (a *AppRiskTagsApi) GetAppUserRiskTags(c *gin.Context) {
userID, _ := strconv.Atoi(c.Param("id"))
if userID <= 0 {
response.FailWithMessage("参数错误", c)
return
}
var list []userTagRow
if err := global.GVA_DB.
Table("app_user_risk_tags").
Joins("LEFT JOIN app_risk_tags t ON t.id = app_user_risk_tags.tag_id").
Select("app_user_risk_tags.*, t.name AS tag_name, t.level AS tag_level, t.color AS tag_color").
Where("app_user_risk_tags.app_user_id = ?", uint(userID)).
Order("app_user_risk_tags.id desc").
Scan(&list).Error; err != nil {
response.FailWithMessage("获取失败:"+err.Error(), c)
return
}
response.OkWithData(list, c)
}
type setUserTagRequest struct {
TagID uint `json:"tagId" binding:"required"`
Remark string `json:"remark"`
}
// AddAppUserRiskTag
// @Tags AppRiskTagsAdmin
// @Summary 给玩家打标
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param id path int true "玩家ID"
// @Param body body setUserTagRequest true "参数"
// @Success 200 {object} response.Response{msg=string} "操作成功"
// @Router /admin/app-users/{id}/risk-tags [post]
func (a *AppRiskTagsApi) AddAppUserRiskTag(c *gin.Context) {
userID, _ := strconv.Atoi(c.Param("id"))
if userID <= 0 {
response.FailWithMessage("参数错误", c)
return
}
var req setUserTagRequest
if err := c.ShouldBindJSON(&req); err != nil || req.TagID == 0 {
response.FailWithMessage("参数错误", c)
return
}
operatorID := utils.GetUserID(c)
if operatorID == 0 {
response.FailWithMessage("未登录", c)
return
}
// ensure tag exists
var tag appModel.AppRiskTag
if err := global.GVA_DB.First(&tag, req.TagID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.FailWithMessage("标签不存在", c)
return
}
response.FailWithMessage("操作失败:"+err.Error(), c)
return
}
rec := appModel.AppUserRiskTag{
AppUserID: uint(userID),
TagID: req.TagID,
OperatorUserID: operatorID,
Remark: strings.TrimSpace(req.Remark),
CreatedAt: time.Now(),
}
if err := global.GVA_DB.Create(&rec).Error; err != nil {
response.FailWithMessage("操作失败:"+err.Error(), c)
return
}
response.OkWithMessage("已打标", c)
}
// RemoveAppUserRiskTag
// @Tags AppRiskTagsAdmin
// @Summary 移除玩家标签
// @Security ApiKeyAuth
// @Produce application/json
// @Param id path int true "关系ID"
// @Success 200 {object} response.Response{msg=string} "操作成功"
// @Router /admin/app-user-risk-tags/{id} [delete]
func (a *AppRiskTagsApi) RemoveAppUserRiskTag(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
response.FailWithMessage("参数错误", c)
return
}
if err := global.GVA_DB.Delete(&appModel.AppUserRiskTag{}, uint(id)).Error; err != nil {
response.FailWithMessage("操作失败:"+err.Error(), c)
return
}
response.OkWithMessage("已移除", c)
}

View File

@@ -0,0 +1,704 @@
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"
systemSvc "git.echol.cn/loser/Go-Web-Template/server/service/system"
"git.echol.cn/loser/Go-Web-Template/server/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type AppUsersApi struct{}
type appUserListItem struct {
ID uint `json:"id"`
UUID string `json:"uuid"`
Username string `json:"username"`
Enable int `json:"enable"`
NickName string `json:"nickName,omitempty"`
AccountBalanceLi int64 `json:"accountBalanceLi"`
GameCoinBalanceLi int64 `json:"gameCoinBalanceLi"`
LastLoginAt *time.Time `json:"lastLoginAt,omitempty"`
LastActiveAt *time.Time `json:"lastActiveAt,omitempty"`
IsOnline bool `json:"isOnline"`
}
type createAppUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=64"`
Password string `json:"password" binding:"required,min=6,max=128"`
Enable *bool `json:"enable"`
NickName string `json:"nickName"`
WelcomePhrase string `json:"welcomePhrase"`
}
type updateAppUserRequest struct {
Enable *bool `json:"enable"`
WelcomePhrase *string `json:"welcomePhrase"`
}
type resetPasswordRequest struct {
NewPassword string `json:"newPassword" binding:"required,min=6,max=128"`
}
type appUserDetailItem struct {
ID uint `json:"id"`
UUID string `json:"uuid"`
Username string `json:"username"`
Enable int `json:"enable"`
NickName string `json:"nickName,omitempty"`
WelcomePhrase string `json:"welcomePhrase,omitempty"`
}
type appUserOverviewResponse struct {
User struct {
ID uint `json:"id"`
UUID string `json:"uuid"`
Username string `json:"username"`
Enable int `json:"enable"`
NickName string `json:"nickName,omitempty"`
} `json:"user"`
Asset struct {
AccountBalanceLi int64 `json:"accountBalanceLi"`
GameCoinBalanceLi int64 `json:"gameCoinBalanceLi"`
} `json:"asset"`
Activity struct {
LastLoginAt *time.Time `json:"lastLoginAt,omitempty"`
LastActiveAt *time.Time `json:"lastActiveAt,omitempty"`
LastActiveIP string `json:"lastActiveIp,omitempty"`
IsOnline bool `json:"isOnline"`
} `json:"activity"`
RecentLoginLogs []appModel.AppLoginLog `json:"recentLoginLogs"`
RecentTx []appModel.AppAssetTransaction `json:"recentTx"`
Invite struct {
InviterUserID *uint `json:"inviterUserId,omitempty"`
InviterUsername string `json:"inviterUsername,omitempty"`
InvitedCount int64 `json:"invitedCount"`
HasUnusedCode bool `json:"hasUnusedCode"`
UnusedCodeLast4 string `json:"unusedCodeLast4,omitempty"`
UnusedExpiresAt *time.Time `json:"unusedExpiresAt,omitempty"`
} `json:"invite"`
}
// GetAppUsersList
// @Tags AppUsersAdmin
// @Summary 后台获取玩家列表
// @Security ApiKeyAuth
// @Produce application/json
// @Param page query int false "页码" default(1)
// @Param pageSize query int false "每页大小" default(10)
// @Param keyword query string false "用户名关键字"
// @Param isOnline query bool false "是否在线(基于 lastActiveAt 与在线窗口判断)"
// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功"
// @Router /admin/app-users [get]
func (a *AppUsersApi) GetAppUsersList(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
}
keyword := c.Query("keyword")
var isOnlineFilter *bool
if raw := strings.TrimSpace(c.Query("isOnline")); raw != "" {
if v, err := strconv.ParseBool(raw); err == nil {
isOnlineFilter = &v
}
}
db := global.GVA_DB.Model(&appModel.AppUser{})
if keyword != "" {
db = db.Where("username ILIKE ?", "%"+keyword+"%")
}
// 在线状态过滤:通过 join 活跃表,按窗口阈值判断
windowMin := systemSvc.GetAppOnlineWindowMinutes()
now := time.Now()
if isOnlineFilter != nil {
threshold := now.Add(-time.Duration(windowMin) * time.Minute)
db = db.Joins("LEFT JOIN app_user_activity act ON act.app_user_id = app_users.id")
db = db.Select("app_users.*")
if *isOnlineFilter {
db = db.Where("act.last_active_at IS NOT NULL AND act.last_active_at >= ?", threshold)
} else {
db = db.Where("act.last_active_at IS NULL OR act.last_active_at < ?", threshold)
}
}
var total int64
if err := db.Count(&total).Error; err != nil {
response.FailWithMessage(err.Error(), c)
return
}
var users []appModel.AppUser
if err := db.Order("id desc").Limit(pageSize).Offset((page - 1) * pageSize).Find(&users).Error; err != nil {
response.FailWithMessage(err.Error(), c)
return
}
ids := make([]uint, 0, len(users))
for _, u := range users {
ids = append(ids, u.ID)
}
profiles := map[uint]appModel.AppUserProfile{}
if len(ids) > 0 {
var list []appModel.AppUserProfile
_ = global.GVA_DB.Where("app_user_id IN ?", ids).Find(&list).Error
for _, p := range list {
profiles[p.AppUserID] = p
}
}
assets := map[uint]appModel.AppUserAsset{}
if len(ids) > 0 {
var list []appModel.AppUserAsset
_ = global.GVA_DB.Where("app_user_id IN ?", ids).Find(&list).Error
for _, a := range list {
assets[a.AppUserID] = a
}
}
activities := map[uint]appModel.AppUserActivity{}
if len(ids) > 0 {
var list []appModel.AppUserActivity
_ = global.GVA_DB.Where("app_user_id IN ?", ids).Find(&list).Error
for _, a := range list {
activities[a.AppUserID] = a
}
}
items := make([]appUserListItem, 0, len(users))
for _, u := range users {
p := profiles[u.ID]
asset := assets[u.ID]
act := activities[u.ID]
isOnline := false
if act.LastActiveAt != nil && windowMin > 0 {
isOnline = now.Sub(*act.LastActiveAt) <= time.Duration(windowMin)*time.Minute
}
items = append(items, appUserListItem{
ID: u.ID,
UUID: u.UUID.String(),
Username: u.Username,
Enable: u.Enable,
NickName: p.NickName,
AccountBalanceLi: asset.AccountBalanceLi,
GameCoinBalanceLi: asset.GameCoinBalanceLi,
LastLoginAt: act.LastLoginAt,
LastActiveAt: act.LastActiveAt,
IsOnline: isOnline,
})
}
response.OkWithDetailed(response.PageResult{
List: items,
Total: total,
Page: page,
PageSize: pageSize,
}, "获取成功", c)
}
type rechargeRequest struct {
AssetType string `json:"assetType" binding:"required"`
RechargeType string `json:"rechargeType" binding:"required"`
DeltaLi int64 `json:"deltaLi" binding:"required"`
Reason string `json:"reason" binding:"required"`
}
// RechargeAppUser
// @Tags AppUsersAdmin
// @Summary 后台手动充值(账户余额/游戏币,单位:厘)
// @Security ApiKeyAuth
// @Produce application/json
// @Param id path int true "玩家ID"
// @Param data body rechargeRequest true "充值信息"
// @Success 200 {object} response.Response{msg=string} "充值成功"
// @Router /admin/app-users/{id}/recharge [post]
func (a *AppUsersApi) RechargeAppUser(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
response.FailWithMessage("参数错误", c)
return
}
var req rechargeRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
req.AssetType = strings.TrimSpace(req.AssetType)
req.RechargeType = strings.TrimSpace(req.RechargeType)
req.Reason = strings.TrimSpace(req.Reason)
if req.Reason == "" {
response.FailWithMessage("原因不能为空", c)
return
}
if req.DeltaLi == 0 {
response.FailWithMessage("变动金额不能为 0", c)
return
}
if req.AssetType != "account" && req.AssetType != "game_coin" {
response.FailWithMessage("assetType 不合法", c)
return
}
if req.RechargeType != "offline" && req.RechargeType != "online_not_arrived" && req.RechargeType != "activity_subsidy" && req.RechargeType != "manual_adjustment" {
response.FailWithMessage("rechargeType 不合法", c)
return
}
if req.RechargeType == "activity_subsidy" && req.AssetType != "game_coin" {
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, id).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
}
var before, after int64
if req.AssetType == "account" {
before = asset.AccountBalanceLi
after = before + req.DeltaLi
if after < 0 {
return errors.New("账户余额不足")
}
asset.AccountBalanceLi = after
if err := tx.Model(&asset).Select("account_balance_li").Updates(&asset).Error; err != nil {
return err
}
} else {
before = asset.GameCoinBalanceLi
after = before + req.DeltaLi
if after < 0 {
return errors.New("游戏币余额不足")
}
asset.GameCoinBalanceLi = after
if err := tx.Model(&asset).Select("game_coin_balance_li").Updates(&asset).Error; err != nil {
return err
}
}
txRec := appModel.AppAssetTransaction{
AppUserID: user.ID,
AssetType: appModel.AppAssetType(req.AssetType),
RechargeType: appModel.AppRechargeType(req.RechargeType),
DeltaLi: req.DeltaLi,
BeforeLi: before,
AfterLi: after,
Reason: req.Reason,
OperatorUserID: operatorID,
CreatedAt: time.Now(),
}
return tx.Create(&txRec).Error
}); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
response.OkWithMessage("充值成功", c)
}
// GetAppUserById
// @Tags AppUsersAdmin
// @Summary 后台获取玩家详情
// @Security ApiKeyAuth
// @Produce application/json
// @Param id path int true "玩家ID"
// @Success 200 {object} response.Response{data=appUserDetailItem,msg=string} "获取成功"
// @Router /admin/app-users/{id} [get]
func (a *AppUsersApi) GetAppUserById(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
response.FailWithMessage("参数错误", c)
return
}
var user appModel.AppUser
if err := global.GVA_DB.First(&user, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.FailWithMessage("用户不存在", c)
return
}
response.FailWithMessage(err.Error(), c)
return
}
var profile appModel.AppUserProfile
_ = global.GVA_DB.Where("app_user_id = ?", user.ID).First(&profile).Error
response.OkWithData(appUserDetailItem{
ID: user.ID,
UUID: user.UUID.String(),
Username: user.Username,
Enable: user.Enable,
NickName: profile.NickName,
WelcomePhrase: profile.WelcomePhrase,
}, c)
}
// GetAppUserOverview
// @Tags AppUsersAdmin
// @Summary 后台获取玩家聚合详情(概览)
// @Security ApiKeyAuth
// @Produce application/json
// @Param id path int true "玩家ID"
// @Success 200 {object} response.Response{data=appUserOverviewResponse,msg=string} "获取成功"
// @Router /admin/app-users/{id}/overview [get]
func (a *AppUsersApi) GetAppUserOverview(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
response.FailWithMessage("参数错误", c)
return
}
var user appModel.AppUser
if err := global.GVA_DB.First(&user, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.FailWithMessage("用户不存在", c)
return
}
response.FailWithMessage(err.Error(), c)
return
}
var profile appModel.AppUserProfile
_ = global.GVA_DB.Where("app_user_id = ?", user.ID).First(&profile).Error
var asset appModel.AppUserAsset
_ = global.GVA_DB.Where("app_user_id = ?", user.ID).First(&asset).Error
var act appModel.AppUserActivity
_ = global.GVA_DB.Where("app_user_id = ?", user.ID).First(&act).Error
windowMin := systemSvc.GetAppOnlineWindowMinutes()
now := time.Now()
isOnline := false
if act.LastActiveAt != nil && windowMin > 0 {
isOnline = now.Sub(*act.LastActiveAt) <= time.Duration(windowMin)*time.Minute
}
var recentLoginLogs []appModel.AppLoginLog
_ = global.GVA_DB.Where("app_user_id = ?", user.ID).Order("id desc").Limit(20).Find(&recentLoginLogs).Error
var recentTx []appModel.AppAssetTransaction
_ = global.GVA_DB.Where("app_user_id = ?", user.ID).Order("id desc").Limit(20).Find(&recentTx).Error
// invite summary
var invitedCount int64
_ = global.GVA_DB.Model(&appModel.AppUserInviteRelation{}).Where("inviter_user_id = ?", user.ID).Count(&invitedCount).Error
var inviterUserID *uint
var inviterUsername string
{
var rel appModel.AppUserInviteRelation
if err := global.GVA_DB.Where("invitee_user_id = ?", user.ID).First(&rel).Error; err == nil {
inviterUserID = &rel.InviterUserID
var inviter appModel.AppUser
_ = global.GVA_DB.Select("id, username").First(&inviter, rel.InviterUserID).Error
inviterUsername = inviter.Username
}
}
hasUnusedCode := false
var unusedLast4 string
var unusedExpiresAt *time.Time
{
var code appModel.AppInviteCode
err := global.GVA_DB.
Where("created_by_user_id = ? AND status = ?", user.ID, appModel.InviteCodeStatusUnused).
Order("id desc").
First(&code).Error
if err == nil {
hasUnusedCode = true
unusedLast4 = code.CodeLast4
unusedExpiresAt = code.ExpiresAt
}
}
var resp appUserOverviewResponse
resp.User.ID = user.ID
resp.User.UUID = user.UUID.String()
resp.User.Username = user.Username
resp.User.Enable = user.Enable
resp.User.NickName = profile.NickName
resp.Asset.AccountBalanceLi = asset.AccountBalanceLi
resp.Asset.GameCoinBalanceLi = asset.GameCoinBalanceLi
resp.Activity.LastLoginAt = act.LastLoginAt
resp.Activity.LastActiveAt = act.LastActiveAt
resp.Activity.LastActiveIP = act.LastActiveIP
resp.Activity.IsOnline = isOnline
resp.RecentLoginLogs = recentLoginLogs
resp.RecentTx = recentTx
resp.Invite.InviterUserID = inviterUserID
resp.Invite.InviterUsername = inviterUsername
resp.Invite.InvitedCount = invitedCount
resp.Invite.HasUnusedCode = hasUnusedCode
resp.Invite.UnusedCodeLast4 = unusedLast4
resp.Invite.UnusedExpiresAt = unusedExpiresAt
response.OkWithDetailed(resp, "获取成功", c)
}
// CreateAppUser
// @Tags AppUsersAdmin
// @Summary 后台手动新增玩家
// @Security ApiKeyAuth
// @Produce application/json
// @Param data body createAppUserRequest true "玩家信息"
// @Success 200 {object} response.Response{msg=string} "创建成功"
// @Router /admin/app-users [post]
func (a *AppUsersApi) CreateAppUser(c *gin.Context) {
var req createAppUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
req.Username = strings.TrimSpace(req.Username)
req.NickName = strings.TrimSpace(req.NickName)
req.WelcomePhrase = strings.TrimSpace(req.WelcomePhrase)
enable := 1
if req.Enable != nil && !*req.Enable {
enable = 2
}
if err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
var existed appModel.AppUser
if err := tx.Where("username = ?", req.Username).First(&existed).Error; err == nil {
return errors.New("用户名已存在")
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
user := appModel.AppUser{
UUID: uuid.New(),
Username: req.Username,
Password: utils.BcryptHash(req.Password),
Enable: enable,
}
if err := tx.Create(&user).Error; err != nil {
return err
}
profile := appModel.AppUserProfile{
AppUserID: user.ID,
WelcomePhrase: req.WelcomePhrase,
NickName: firstNonEmpty(req.NickName, req.Username),
}
return tx.Create(&profile).Error
}); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
response.OkWithMessage("创建成功", c)
}
// UpdateAppUser
// @Tags AppUsersAdmin
// @Summary 编辑玩家(仅启用状态/欢迎词)
// @Security ApiKeyAuth
// @Produce application/json
// @Param id path int true "玩家ID"
// @Param data body updateAppUserRequest true "更新字段"
// @Success 200 {object} response.Response{msg=string} "更新成功"
// @Router /admin/app-users/{id} [patch]
func (a *AppUsersApi) UpdateAppUser(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
response.FailWithMessage("参数错误", c)
return
}
var req updateAppUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if err := global.GVA_DB.Transaction(func(tx *gorm.DB) error {
var user appModel.AppUser
if err := tx.First(&user, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("用户不存在")
}
return err
}
if req.Enable != nil {
if *req.Enable {
user.Enable = 1
} else {
user.Enable = 2
}
if err := tx.Model(&user).Select("enable").Updates(&user).Error; err != nil {
return err
}
}
if req.WelcomePhrase != nil {
wp := strings.TrimSpace(*req.WelcomePhrase)
var profile appModel.AppUserProfile
err := tx.Where("app_user_id = ?", user.ID).First(&profile).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
profile = appModel.AppUserProfile{
AppUserID: user.ID,
WelcomePhrase: wp,
NickName: user.Username,
}
return tx.Create(&profile).Error
}
if err != nil {
return err
}
profile.WelcomePhrase = wp
return tx.Model(&profile).Select("welcome_phrase").Updates(&profile).Error
}
return nil
}); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
response.OkWithMessage("更新成功", c)
}
// ResetAppUserPassword
// @Tags AppUsersAdmin
// @Summary 重置玩家密码
// @Security ApiKeyAuth
// @Produce application/json
// @Param id path int true "玩家ID"
// @Param data body resetPasswordRequest true "新密码"
// @Success 200 {object} response.Response{msg=string} "重置成功"
// @Router /admin/app-users/{id}/reset-password [post]
func (a *AppUsersApi) ResetAppUserPassword(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
response.FailWithMessage("参数错误", c)
return
}
var req resetPasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.FailWithMessage(err.Error(), c)
return
}
var user appModel.AppUser
if err := global.GVA_DB.First(&user, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.FailWithMessage("用户不存在", c)
return
}
response.FailWithMessage(err.Error(), c)
return
}
if err := global.GVA_DB.Model(&user).Update("password", utils.BcryptHash(req.NewPassword)).Error; err != nil {
response.FailWithMessage(err.Error(), c)
return
}
response.OkWithMessage("重置成功", c)
}
// ToggleAppUserEnable
// @Tags AppUsersAdmin
// @Summary 启用/停用玩家
// @Security ApiKeyAuth
// @Produce application/json
// @Param id path int true "玩家ID"
// @Success 200 {object} response.Response{msg=string} "操作成功"
// @Router /admin/app-users/{id}/toggle-enable [post]
func (a *AppUsersApi) ToggleAppUserEnable(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
response.FailWithMessage("参数错误", c)
return
}
var user appModel.AppUser
if err := global.GVA_DB.First(&user, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.FailWithMessage("用户不存在", c)
return
}
response.FailWithMessage(err.Error(), c)
return
}
next := 1
if user.Enable == 1 {
next = 2
}
if err := global.GVA_DB.Model(&user).Update("enable", next).Error; err != nil {
response.FailWithMessage(err.Error(), c)
return
}
response.OkWithMessage("操作成功", c)
}
// DeleteAppUser
// @Tags AppUsersAdmin
// @Summary 软删除玩家
// @Security ApiKeyAuth
// @Produce application/json
// @Param id path int true "玩家ID"
// @Success 200 {object} response.Response{msg=string} "删除成功"
// @Router /admin/app-users/{id} [delete]
func (a *AppUsersApi) DeleteAppUser(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if id <= 0 {
response.FailWithMessage("参数错误", c)
return
}
var user appModel.AppUser
if err := global.GVA_DB.First(&user, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.FailWithMessage("用户不存在", c)
return
}
response.FailWithMessage(err.Error(), c)
return
}
if err := global.GVA_DB.Delete(&user).Error; err != nil {
response.FailWithMessage(err.Error(), c)
return
}
response.OkWithMessage("删除成功", c)
}
func firstNonEmpty(values ...string) string {
for _, v := range values {
if v != "" {
return v
}
}
return ""
}

View File

@@ -0,0 +1,528 @@
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)
}

View File

@@ -0,0 +1,22 @@
package admin
import (
"git.echol.cn/loser/Go-Web-Template/server/model/admin"
"git.echol.cn/loser/Go-Web-Template/server/model/common/response"
"github.com/gin-gonic/gin"
)
type DashboardApi struct{}
// GetDashboardStats 运营仪表盘聚合统计(占位实现,避免前端 404后续可接真实聚合查询
func (a *DashboardApi) GetDashboardStats(c *gin.Context) {
stats := admin.DashboardStats{
Feedback: admin.DashboardFeedbackStats{
PendingItems: []admin.DashboardFeedbackPendingItem{},
},
Creators: admin.DashboardCreatorStats{
TopCreators: []admin.DashboardTopCreator{},
},
}
response.OkWithData(stats, c)
}

View File

@@ -0,0 +1,11 @@
package admin
type ApiGroup struct {
DashboardApi
AppUsersApi
AppInviteCodesApi
AppLoginLogsApi
AppAssetTransactionsApi
AppWithdrawOrdersApi
AppRiskTagsApi
}