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