Files

278 lines
9.3 KiB
Go
Raw Permalink Normal View History

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