Files

373 lines
13 KiB
Go
Raw Permalink Normal View History

2026-08-14 13:17:03 +08:00
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})
}