278 lines
9.3 KiB
Go
278 lines
9.3 KiB
Go
|
|
package handler
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"sort"
|
|||
|
|
"strconv"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"github.com/gin-gonic/gin"
|
|||
|
|
|
|||
|
|
"nl-game-api-gin/internal/database"
|
|||
|
|
"nl-game-api-gin/internal/middleware"
|
|||
|
|
"nl-game-api-gin/internal/model"
|
|||
|
|
"nl-game-api-gin/internal/service"
|
|||
|
|
"nl-game-api-gin/pkg/resp"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// pageParams 解析分页参数(页码默认1、每页默认10、上限50)
|
|||
|
|
func pageParams(c *gin.Context) (page, size, offset int) {
|
|||
|
|
page, _ = strconv.Atoi(c.DefaultQuery("page", "1"))
|
|||
|
|
size, _ = strconv.Atoi(c.DefaultQuery("size", "10"))
|
|||
|
|
if page < 1 {
|
|||
|
|
page = 1
|
|||
|
|
}
|
|||
|
|
// 上限 500:支持前端「批量拉取 + 虚拟滚动」模式一次取一大页(查询始终带 LIMIT,无全表风险)
|
|||
|
|
if size < 1 || size > 500 {
|
|||
|
|
size = 10
|
|||
|
|
}
|
|||
|
|
return page, size, (page - 1) * size
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Profile 获取当前用户资料
|
|||
|
|
func Profile(c *gin.Context) {
|
|||
|
|
var user model.User
|
|||
|
|
if err := database.DB.First(&user, middleware.UID(c)).Error; err != nil {
|
|||
|
|
resp.Unauthorized(c, "用户不存在")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
resp.OK(c, user)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// updateProfileReq 修改资料请求体
|
|||
|
|
type updateProfileReq struct {
|
|||
|
|
Nickname string `json:"nickname" binding:"required,max=20"` // 新昵称
|
|||
|
|
Avatar string `json:"avatar" binding:"required,max=8"` // 新 emoji 头像
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// UpdateProfile 修改昵称与头像
|
|||
|
|
func UpdateProfile(c *gin.Context) {
|
|||
|
|
var req updateProfileReq
|
|||
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|||
|
|
resp.Fail(c, "参数有误")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
database.DB.Model(&model.User{}).Where("id = ?", middleware.UID(c)).
|
|||
|
|
Updates(map[string]any{"nickname": req.Nickname, "avatar": req.Avatar})
|
|||
|
|
resp.OK(c, nil)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// PointRecords 我的积分流水(分页)
|
|||
|
|
func PointRecords(c *gin.Context) {
|
|||
|
|
page, size, offset := pageParams(c)
|
|||
|
|
uid := middleware.UID(c)
|
|||
|
|
var total int64
|
|||
|
|
list := make([]model.PointRecord, 0)
|
|||
|
|
database.DB.Model(&model.PointRecord{}).Where("user_id = ?", uid).Count(&total)
|
|||
|
|
database.DB.Where("user_id = ?", uid).Order("id DESC").Limit(size).Offset(offset).Find(&list)
|
|||
|
|
resp.OK(c, gin.H{"list": list, "total": total, "page": page, "size": size})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// LoginLogs 我的登录记录(分页)
|
|||
|
|
func LoginLogs(c *gin.Context) {
|
|||
|
|
page, size, offset := pageParams(c)
|
|||
|
|
uid := middleware.UID(c)
|
|||
|
|
var total int64
|
|||
|
|
list := make([]model.LoginLog, 0)
|
|||
|
|
database.DB.Model(&model.LoginLog{}).Where("user_id = ?", uid).Count(&total)
|
|||
|
|
database.DB.Where("user_id = ?", uid).Order("id DESC").Limit(size).Offset(offset).Find(&list)
|
|||
|
|
resp.OK(c, gin.H{"list": list, "total": total, "page": page, "size": size})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// gameRecordItem 游戏记录列表项(附游戏名称与图标)
|
|||
|
|
type gameRecordItem struct {
|
|||
|
|
model.GameRecord
|
|||
|
|
GameName string `json:"game_name"` // 游戏名称
|
|||
|
|
GameIcon string `json:"game_icon"` // 游戏图标
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GameRecords 我的单机游戏记录(分页,联表补游戏信息)
|
|||
|
|
func GameRecords(c *gin.Context) {
|
|||
|
|
page, size, offset := pageParams(c)
|
|||
|
|
uid := middleware.UID(c)
|
|||
|
|
var total int64
|
|||
|
|
list := make([]gameRecordItem, 0)
|
|||
|
|
database.DB.Model(&model.GameRecord{}).Where("user_id = ?", uid).Count(&total)
|
|||
|
|
database.DB.Table("game_records gr").
|
|||
|
|
Select("gr.*, g.name AS game_name, g.icon AS game_icon").
|
|||
|
|
Joins("LEFT JOIN games g ON g.id = gr.game_id").
|
|||
|
|
Where("gr.user_id = ?", uid).
|
|||
|
|
Order("gr.id DESC").Limit(size).Offset(offset).Scan(&list)
|
|||
|
|
resp.OK(c, gin.H{"list": list, "total": total, "page": page, "size": size})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// battleRecordItem 对战记录列表项(附游戏名称与图标)
|
|||
|
|
type battleRecordItem struct {
|
|||
|
|
model.BattleRecord
|
|||
|
|
GameName string `json:"game_name"` // 游戏名称
|
|||
|
|
GameIcon string `json:"game_icon"` // 游戏图标
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// BattleRecords 我的联机对战记录(分页)
|
|||
|
|
func BattleRecords(c *gin.Context) {
|
|||
|
|
page, size, offset := pageParams(c)
|
|||
|
|
uid := middleware.UID(c)
|
|||
|
|
var total int64
|
|||
|
|
list := make([]battleRecordItem, 0)
|
|||
|
|
database.DB.Model(&model.BattleRecord{}).Where("user_id = ?", uid).Count(&total)
|
|||
|
|
database.DB.Table("battle_records br").
|
|||
|
|
Select("br.*, g.name AS game_name, g.icon AS game_icon").
|
|||
|
|
Joins("LEFT JOIN games g ON g.id = br.game_id").
|
|||
|
|
Where("br.user_id = ?", uid).
|
|||
|
|
Order("br.id DESC").Limit(size).Offset(offset).Scan(&list)
|
|||
|
|
resp.OK(c, gin.H{"list": list, "total": total, "page": page, "size": size})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// userPropItem 用户道具列表项(附道具详情)
|
|||
|
|
type userPropItem struct {
|
|||
|
|
PropID int `json:"prop_id"` // 道具ID
|
|||
|
|
Code string `json:"code"` // 道具编码
|
|||
|
|
Name string `json:"name"` // 道具名称
|
|||
|
|
Icon string `json:"icon"` // 图标
|
|||
|
|
Description string `json:"description"` // 用途说明
|
|||
|
|
Quantity int `json:"quantity"` // 持有数量
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// UserProps 我的道具背包(只返回数量大于0的)
|
|||
|
|
func UserProps(c *gin.Context) {
|
|||
|
|
list := make([]userPropItem, 0)
|
|||
|
|
database.DB.Table("user_props up").
|
|||
|
|
Select("up.prop_id, p.code, p.name, p.icon, p.description, up.quantity").
|
|||
|
|
Joins("JOIN props p ON p.id = up.prop_id").
|
|||
|
|
Where("up.user_id = ? AND up.quantity > 0", middleware.UID(c)).
|
|||
|
|
Order("p.sort").Scan(&list)
|
|||
|
|
resp.OK(c, list)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// UserGames 我已购买的游戏ID列表(判断解锁状态用)
|
|||
|
|
func UserGames(c *gin.Context) {
|
|||
|
|
ids := make([]int, 0)
|
|||
|
|
database.DB.Model(&model.UserGame{}).Where("user_id = ?", middleware.UID(c)).Pluck("game_id", &ids)
|
|||
|
|
resp.OK(c, ids)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// myGameItem 「我的游戏」列表项:游戏信息 + 来源 + 我的游玩统计
|
|||
|
|
type myGameItem struct {
|
|||
|
|
model.Game
|
|||
|
|
Source string `json:"source"` // 来源:bought=已购 weekly=本周周免 played=玩过的免费游戏
|
|||
|
|
GotAt int64 `json:"got_at"` // 获得时间(已购=购买时间,其余=0)
|
|||
|
|
Plays int `json:"plays"` // 我的总局数(单机+对战)
|
|||
|
|
Best int `json:"best"` // 我的最高分(单机游戏)
|
|||
|
|
LastPlayed int64 `json:"last_played"` // 最近一次游玩时间(0=没玩过)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// UserMyGames 个人中心「我的游戏」:已购游戏 + 本周VIP周免 + 玩过的免费游戏,附游玩统计
|
|||
|
|
// 排序:已购按购买时间倒序 → 周免 → 玩过的按最近游玩倒序
|
|||
|
|
func UserMyGames(c *gin.Context) {
|
|||
|
|
uid := middleware.UID(c)
|
|||
|
|
// 我的游玩统计(单机记录:局数/最高分/最近时间,按游戏聚合)
|
|||
|
|
type statRow struct {
|
|||
|
|
GameID int `gorm:"column:game_id"`
|
|||
|
|
Plays int `gorm:"column:plays"`
|
|||
|
|
Best int `gorm:"column:best"`
|
|||
|
|
Last int64 `gorm:"column:last"`
|
|||
|
|
}
|
|||
|
|
statMap := map[int]statRow{}
|
|||
|
|
var stats []statRow
|
|||
|
|
database.DB.Raw(`SELECT game_id, COUNT(*) AS plays, MAX(score) AS best, MAX(created_at) AS last
|
|||
|
|
FROM game_records WHERE user_id = ? GROUP BY game_id`, uid).Scan(&stats)
|
|||
|
|
for _, s := range stats {
|
|||
|
|
statMap[s.GameID] = s
|
|||
|
|
}
|
|||
|
|
// 对战局数并入统计(联机游戏没有单机得分,best 保持 0)
|
|||
|
|
var bstats []statRow
|
|||
|
|
database.DB.Raw(`SELECT game_id, COUNT(*) AS plays, 0 AS best, MAX(created_at) AS last
|
|||
|
|
FROM battle_records WHERE user_id = ? GROUP BY game_id`, uid).Scan(&bstats)
|
|||
|
|
for _, s := range bstats {
|
|||
|
|
m := statMap[s.GameID]
|
|||
|
|
m.GameID = s.GameID
|
|||
|
|
m.Plays += s.Plays
|
|||
|
|
if s.Last > m.Last {
|
|||
|
|
m.Last = s.Last
|
|||
|
|
}
|
|||
|
|
statMap[s.GameID] = m
|
|||
|
|
}
|
|||
|
|
// 已购游戏(购买时间倒序)
|
|||
|
|
var ugs []model.UserGame
|
|||
|
|
database.DB.Where("user_id = ?", uid).Order("id DESC").Find(&ugs)
|
|||
|
|
boughtAt := map[int]int64{}
|
|||
|
|
boughtOrder := make([]int, 0, len(ugs))
|
|||
|
|
for _, ug := range ugs {
|
|||
|
|
if _, ok := boughtAt[ug.GameID]; !ok {
|
|||
|
|
boughtAt[ug.GameID] = ug.CreatedAt
|
|||
|
|
boughtOrder = append(boughtOrder, ug.GameID)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// 本周周免集合(非VIP为空)
|
|||
|
|
weeklySet := map[int]bool{}
|
|||
|
|
var user model.User
|
|||
|
|
if err := database.DB.First(&user, uid).Error; err == nil {
|
|||
|
|
weeklySet = service.VipWeeklyFreeSet(&user, time.Now())
|
|||
|
|
}
|
|||
|
|
// 汇总需要展示的游戏ID:已购 + 周免 + 玩过的
|
|||
|
|
idSet := map[int]bool{}
|
|||
|
|
for _, id := range boughtOrder {
|
|||
|
|
idSet[id] = true
|
|||
|
|
}
|
|||
|
|
for id := range weeklySet {
|
|||
|
|
idSet[id] = true
|
|||
|
|
}
|
|||
|
|
for id := range statMap {
|
|||
|
|
idSet[id] = true
|
|||
|
|
}
|
|||
|
|
if len(idSet) == 0 {
|
|||
|
|
resp.OK(c, []myGameItem{})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
ids := make([]int, 0, len(idSet))
|
|||
|
|
for id := range idSet {
|
|||
|
|
ids = append(ids, id)
|
|||
|
|
}
|
|||
|
|
var games []model.Game
|
|||
|
|
database.DB.Where("id IN ? AND status = 1", ids).Find(&games)
|
|||
|
|
gameMap := map[int]model.Game{}
|
|||
|
|
for _, g := range games {
|
|||
|
|
gameMap[g.ID] = g
|
|||
|
|
}
|
|||
|
|
build := func(id int, source string) (myGameItem, bool) {
|
|||
|
|
g, ok := gameMap[id]
|
|||
|
|
if !ok {
|
|||
|
|
return myGameItem{}, false
|
|||
|
|
}
|
|||
|
|
s := statMap[id]
|
|||
|
|
return myGameItem{
|
|||
|
|
Game: g, Source: source, GotAt: boughtAt[id],
|
|||
|
|
Plays: s.Plays, Best: s.Best, LastPlayed: s.Last,
|
|||
|
|
}, true
|
|||
|
|
}
|
|||
|
|
list := make([]myGameItem, 0, len(idSet))
|
|||
|
|
// 1. 已购(购买时间倒序;已下架的会被 build 过滤)
|
|||
|
|
for _, id := range boughtOrder {
|
|||
|
|
if it, ok := build(id, "bought"); ok {
|
|||
|
|
list = append(list, it)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
boughtCount := len(list)
|
|||
|
|
// 2. 本周周免(未购买的才标周免),按游戏排序权重稳定展示
|
|||
|
|
for id := range weeklySet {
|
|||
|
|
if boughtAt[id] == 0 {
|
|||
|
|
if it, ok := build(id, "weekly"); ok {
|
|||
|
|
list = append(list, it)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
weeklyPart := list[boughtCount:]
|
|||
|
|
sort.SliceStable(weeklyPart, func(i, j int) bool { return weeklyPart[i].Sort < weeklyPart[j].Sort })
|
|||
|
|
// 3. 玩过的其它游戏(免费游戏等,按最近游玩倒序)
|
|||
|
|
played := make([]myGameItem, 0)
|
|||
|
|
for id := range statMap {
|
|||
|
|
if boughtAt[id] == 0 && !weeklySet[id] {
|
|||
|
|
if it, ok := build(id, "played"); ok {
|
|||
|
|
played = append(played, it)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
sort.SliceStable(played, func(i, j int) bool { return played[i].LastPlayed > played[j].LastPlayed })
|
|||
|
|
list = append(list, played...)
|
|||
|
|
resp.OK(c, list)
|
|||
|
|
}
|