Files
nl-game-api/internal/handler/admin.go
2026-08-14 13:17:03 +08:00

373 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"fmt"
"strconv"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"nl-game-api-gin/internal/database"
"nl-game-api-gin/internal/model"
"nl-game-api-gin/internal/service"
"nl-game-api-gin/pkg/resp"
)
// AdminStats 数据概览:用户/游戏/订单总量与今日动态
func AdminStats(c *gin.Context) {
var userCount, gameCount, orderCount, todayPlays, todaySigns int64
var todayPoints int
todayStart := time.Now().Truncate(24 * time.Hour).Unix()
// 各类总量统计
database.DB.Model(&model.User{}).Count(&userCount)
database.DB.Model(&model.Game{}).Where("status = 1").Count(&gameCount)
database.DB.Model(&model.Order{}).Count(&orderCount)
database.DB.Model(&model.GameRecord{}).Where("created_at >= ?", todayStart).Count(&todayPlays)
database.DB.Model(&model.SignIn{}).Where("created_at >= ?", todayStart).Count(&todaySigns)
// 今日发放积分(只统计正向变动)
database.DB.Model(&model.PointRecord{}).
Where("created_at >= ? AND change_points > 0", todayStart).
Select("COALESCE(SUM(change_points),0)").Scan(&todayPoints)
resp.OK(c, gin.H{
"user_count": userCount, // 用户总数
"game_count": gameCount, // 上架游戏数
"order_count": orderCount, // 订单总数
"today_plays": todayPlays, // 今日游玩次数
"today_signs": todaySigns, // 今日签到人数
"today_points": todayPoints, // 今日发放积分
})
}
// AdminThemes 主题列表 + 当前启用主题
func AdminThemes(c *gin.Context) {
var themes []model.Theme
database.DB.Where("status = 1").Order("sort").Find(&themes)
resp.OK(c, gin.H{
"list": themes,
"active": service.GetConfig(model.ConfKeyActiveTheme, "arcade"),
})
}
// switchThemeReq 切换主题请求
type switchThemeReq struct {
Code string `json:"code" binding:"required"` // 目标主题编码
}
// AdminSwitchTheme 切换全站主题(写 site_configs.active_theme前台下次拉取生效
func AdminSwitchTheme(c *gin.Context) {
var req switchThemeReq
if err := c.ShouldBindJSON(&req); err != nil {
resp.Fail(c, "参数错误")
return
}
var theme model.Theme
if err := database.DB.Where("code = ? AND status = 1", req.Code).First(&theme).Error; err != nil {
resp.Fail(c, "主题不存在")
return
}
if err := service.SetConfig(model.ConfKeyActiveTheme, theme.Code); err != nil {
resp.Fail(c, "切换失败")
return
}
resp.OK(c, gin.H{"active": theme.Code})
}
// updateSiteReq 站点信息更新请求
type updateSiteReq struct {
SiteName string `json:"site_name"` // 站点名称(空=不修改)
Announcement string `json:"announcement"` // 公告内容(空=不修改)
CarouselStyle string `json:"carousel_style"` // 大厅轮播效果slide/fade/coverflow/cards空=不修改)
CarouselInterval int `json:"carousel_interval"` // 自动轮播间隔秒0=不修改,限 2~30
}
// 允许的轮播效果集合(与前端 GameCarousel 组件实现一一对应)
var carouselStyles = map[string]bool{"slide": true, "fade": true, "coverflow": true, "cards": true}
// AdminUpdateSite 更新站点配置
func AdminUpdateSite(c *gin.Context) {
var req updateSiteReq
if err := c.ShouldBindJSON(&req); err != nil {
resp.Fail(c, "参数错误")
return
}
if req.SiteName != "" {
service.SetConfig(model.ConfKeySiteName, req.SiteName)
}
if req.Announcement != "" {
service.SetConfig(model.ConfKeyAnnouncement, req.Announcement)
}
if req.CarouselStyle != "" {
if !carouselStyles[req.CarouselStyle] {
resp.Fail(c, "不支持的轮播效果")
return
}
service.SetConfig(model.ConfKeyCarouselStyle, req.CarouselStyle)
}
if req.CarouselInterval > 0 {
if req.CarouselInterval < 2 || req.CarouselInterval > 30 {
resp.Fail(c, "轮播间隔需在 2~30 秒之间")
return
}
service.SetConfig(model.ConfKeyCarouselInterval, strconv.Itoa(req.CarouselInterval))
}
resp.OK(c, nil)
}
// AdminUsers 用户列表(支持用户名/昵称关键词搜索、账号状态筛选,分页)
func AdminUsers(c *gin.Context) {
page, size, offset := pageParams(c)
keyword := c.Query("keyword")
status, _ := strconv.Atoi(c.Query("status")) // 0=全部 1=正常 2=封禁
query := database.DB.Model(&model.User{})
if keyword != "" {
like := "%" + keyword + "%"
query = query.Where("username LIKE ? OR nickname LIKE ?", like, like)
}
if status == model.UserStatusNormal || status == model.UserStatusBanned {
query = query.Where("status = ?", status)
}
var total int64
list := make([]model.User, 0)
query.Count(&total)
query.Order("id DESC").Limit(size).Offset(offset).Find(&list)
resp.OK(c, gin.H{"list": list, "total": total, "page": page, "size": size})
}
// AdminUserPoints 指定用户的积分流水(后台调积分弹窗展示,分页)
func AdminUserPoints(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
page, size, offset := pageParams(c)
var total int64
list := make([]model.PointRecord, 0)
database.DB.Model(&model.PointRecord{}).Where("user_id = ?", id).Count(&total)
database.DB.Where("user_id = ?", id).Order("id DESC").Limit(size).Offset(offset).Find(&list)
resp.OK(c, gin.H{"list": list, "total": total, "page": page, "size": size})
}
// adminUpdateUserReq 用户更新请求
type adminUpdateUserReq struct {
Status int `json:"status"` // 账号状态1正常 2封禁0=不修改)
PointsOp string `json:"points_op"` // 积分操作recharge充值 / deduct扣除 / set调整为空=不操作)
PointsValue int `json:"points_value"` // 操作数值:充值/扣除为变动量(>0调整为目标余额>=0
PointsAdjust int `json:"points_adjust"` // 兼容旧版正加负减0=不操作,新版前端不再使用)
Remark string `json:"remark"` // 流水备注
VipLevel *int `json:"vip_level"` // 目标VIP等级0=取消VIP1~5nil=不修改)
VipAddDays int `json:"vip_add_days"` // VIP有效期增减天数0=不修改)
}
// AdminUpdateUser 封禁/解封、积分充值/扣除/调整、调整VIP
func AdminUpdateUser(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req adminUpdateUserReq
if err := c.ShouldBindJSON(&req); err != nil {
resp.Fail(c, "参数错误")
return
}
var user model.User
if err := database.DB.First(&user, id).Error; err != nil {
resp.Fail(c, "用户不存在")
return
}
// 修改账号状态
if req.Status == model.UserStatusNormal || req.Status == model.UserStatusBanned {
if user.Role == model.RoleAdmin && req.Status == model.UserStatusBanned {
resp.Fail(c, "不能封禁管理员")
return
}
database.DB.Model(&user).Update("status", req.Status)
}
// 调整VIP等级/有效期
if req.VipLevel != nil || req.VipAddDays != 0 {
now := time.Now().Unix()
newLevel := user.VipLevel
newExpire := user.VipExpire
if req.VipLevel != nil {
if *req.VipLevel < 0 || *req.VipLevel > 5 {
resp.Fail(c, "VIP等级需在 0~5 之间")
return
}
newLevel = *req.VipLevel
if newLevel == 0 {
// 取消VIP到期时间一并清零
newExpire = 0
} else if newExpire <= now {
// 从无到有或已过期:默认给 30 天
newExpire = now + 30*86400
}
}
if req.VipAddDays != 0 {
if newLevel == 0 {
resp.Fail(c, "请先设置VIP等级再调整天数")
return
}
base := newExpire
if base < now {
base = now
}
newExpire = base + int64(req.VipAddDays)*86400
if newExpire <= now {
// 扣到过期以前直接取消VIP
newLevel, newExpire = 0, 0
}
}
database.DB.Model(&user).Updates(map[string]any{"vip_level": newLevel, "vip_expire": newExpire})
}
// 积分操作:充值 / 扣除 / 调整为目标值(走统一积分服务,自动写流水)
if req.PointsOp != "" {
change := 0
defRemark := ""
switch req.PointsOp {
case "recharge": // 充值:加分
if req.PointsValue <= 0 {
resp.Fail(c, "充值数量需大于 0")
return
}
change = req.PointsValue
defRemark = "后台充值"
case "deduct": // 扣除:减分(余额不足会被积分服务拦下)
if req.PointsValue <= 0 {
resp.Fail(c, "扣除数量需大于 0")
return
}
change = -req.PointsValue
defRemark = "后台扣除"
case "set": // 调整为:直接设定目标余额,按差额记流水
if req.PointsValue < 0 {
resp.Fail(c, "目标积分不能为负数")
return
}
change = req.PointsValue - user.Points
defRemark = fmt.Sprintf("后台调整为 %d", req.PointsValue)
default:
resp.Fail(c, "未知的积分操作类型")
return
}
if change != 0 {
remark := req.Remark
if remark == "" {
remark = defRemark
}
if _, err := service.ChangePoints(nil, user.ID, change, model.PointTypeAdmin, 0, remark); err != nil {
resp.Fail(c, err.Error())
return
}
}
} else if req.PointsAdjust != 0 {
// 兼容旧版协议:正加负减
remark := req.Remark
if remark == "" {
remark = "后台手动调整"
}
if _, err := service.ChangePoints(nil, user.ID, req.PointsAdjust, model.PointTypeAdmin, 0, remark); err != nil {
resp.Fail(c, err.Error())
return
}
}
resp.OK(c, nil)
}
// AdminGames 游戏列表(含下架游戏)
func AdminGames(c *gin.Context) {
list := make([]model.Game, 0)
database.DB.Order("sort").Find(&list)
resp.OK(c, list)
}
// adminUpdateGameReq 游戏更新请求
type adminUpdateGameReq struct {
Price *int `json:"price"` // 价格nil=不修改)
Status *int `json:"status"` // 状态1上架 2下架nil=不修改)
Sort *int `json:"sort"` // 排序值nil=不修改)
ScorePerPoint *int `json:"score_per_point"` // 得分换算多少分换1积分0=不产积分nil=不修改)
MaxPointsPerPlay *int `json:"max_points_per_play"` // 单局最多可得积分nil=不修改)
Carousel *int `json:"carousel"` // 大厅轮播0=不参与,>0=参与并按值升序nil=不修改)
}
// AdminUpdateGame 修改游戏价格/上下架/排序/积分规则
func AdminUpdateGame(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req adminUpdateGameReq
if err := c.ShouldBindJSON(&req); err != nil {
resp.Fail(c, "参数错误")
return
}
updates := map[string]any{}
if req.Price != nil && *req.Price >= 0 {
updates["price"] = *req.Price
}
if req.Status != nil && (*req.Status == 1 || *req.Status == 2) {
updates["status"] = *req.Status
}
if req.Sort != nil {
updates["sort"] = *req.Sort
}
if req.ScorePerPoint != nil && *req.ScorePerPoint >= 0 {
updates["score_per_point"] = *req.ScorePerPoint
}
if req.MaxPointsPerPlay != nil && *req.MaxPointsPerPlay >= 0 {
updates["max_points_per_play"] = *req.MaxPointsPerPlay
}
if req.Carousel != nil && *req.Carousel >= 0 {
updates["carousel"] = *req.Carousel
}
if len(updates) == 0 {
resp.Fail(c, "没有可更新的字段")
return
}
if err := database.DB.Model(&model.Game{}).Where("id = ?", id).Updates(updates).Error; err != nil {
resp.Fail(c, "保存失败,请重试")
return
}
resp.OK(c, nil)
}
// adminOrderItem 订单列表项(附下单用户信息)
type adminOrderItem struct {
model.Order
Username string `json:"username"` // 用户账号
Nickname string `json:"nickname"` // 用户昵称
}
// adminOrderQuery 构造订单查询(列表、计数与合计共用同一组筛选条件)
// rangeKey 时间范围today=今天 / 7d=近7天 / 30d=近30天 / 空=全部
func adminOrderQuery(orderNo, userKw, rangeKey string) *gorm.DB {
q := database.DB.Table("orders o").
Joins("LEFT JOIN users u ON u.id = o.user_id").
Where("o.deleted_at = 0")
if orderNo != "" {
q = q.Where("o.order_no LIKE ?", "%"+orderNo+"%")
}
if userKw != "" {
q = q.Where("u.username LIKE ?", "%"+userKw+"%")
}
now := time.Now()
switch rangeKey {
case "today":
day := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
q = q.Where("o.created_at >= ?", day.Unix())
case "7d":
q = q.Where("o.created_at >= ?", now.AddDate(0, 0, -7).Unix())
case "30d":
q = q.Where("o.created_at >= ?", now.AddDate(0, 0, -30).Unix())
}
return q
}
// AdminOrders 全站订单列表(支持按订单号、用户账号搜索与时间范围筛选,分页,返回筛选结果的积分合计)
func AdminOrders(c *gin.Context) {
page, size, offset := pageParams(c)
orderNo := c.Query("order_no")
userKw := c.Query("user")
rangeKey := c.Query("range")
var total int64
var sum int64
list := make([]adminOrderItem, 0)
adminOrderQuery(orderNo, userKw, rangeKey).Count(&total)
adminOrderQuery(orderNo, userKw, rangeKey).Select("COALESCE(SUM(o.total_points),0)").Scan(&sum)
adminOrderQuery(orderNo, userKw, rangeKey).
Select("o.*, u.username, u.nickname").
Order("o.id DESC").Limit(size).Offset(offset).Scan(&list)
resp.OK(c, gin.H{"list": list, "total": total, "page": page, "size": size, "sum_points": sum})
}