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