373 lines
13 KiB
Go
373 lines
13 KiB
Go
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=取消VIP,1~5;nil=不修改)
|
||
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})
|
||
}
|