From 26696f4e8099813e15fc806d3c925f823cd9d7b6 Mon Sep 17 00:00:00 2001 From: win Date: Sat, 25 Apr 2026 02:48:07 +0800 Subject: [PATCH] x --- .claude/plan/minesweeper-leaderboard-admin.md | 404 ++++++++++++++++++ internal/api/game/handler.go | 38 +- internal/api/game/handler_test.go | 81 ++-- 3 files changed, 472 insertions(+), 51 deletions(-) create mode 100644 .claude/plan/minesweeper-leaderboard-admin.md diff --git a/.claude/plan/minesweeper-leaderboard-admin.md b/.claude/plan/minesweeper-leaderboard-admin.md new file mode 100644 index 0000000..7a7dc3b --- /dev/null +++ b/.claude/plan/minesweeper-leaderboard-admin.md @@ -0,0 +1,404 @@ +# 📋 实施计划:扫雷排行榜管理后台 + 去除免费模式 + +## 背景理解 + +用户说明: +1. **没有免费模式**:`minesweeper_free` 这个 game_type 已废弃,前后端都要移除 +2. **排行榜需要在管理后台展示**:当前排行榜只有 App 端接口,管理后台缺少排行榜 Tab +3. **"积分"含义模糊**:排行榜里的 `total_rank_points` 字段是"游戏对战分",不是平台积分(points),页面上要加说明 + +--- + +## 任务类型 +- [x] 全栈(后端 + 前端并行) + +--- + +## 技术方案 + +### 后端 +在 `internal/api/game/handler.go` 新增一个 Admin 专用排行榜接口: +- `GET /api/admin/games/leaderboard` — 管理后台查排行榜(分页、支持搜索用户昵称) +- `GET /api/admin/games/records` — 管理后台查每局对战记录(分页、支持按用户/时间筛选) + +两个接口都走读库,无需鉴权以外的特殊处理。 + +### 前端 +在 `web/admin/src/views/operations/minesweeper/index.vue` 新增两个 Tab: +- **排行榜 Tab**:表格展示所有玩家的排行数据,含"对战分"说明 +- **对战记录 Tab**:按局查每场游戏的明细 + +同时去掉前端中所有 `minesweeper_free` 的相关逻辑和 `game_type` 切换选项。 + +--- + +## 实施步骤 + +### Step 1 — 后端:新增 Admin 排行榜接口 + +**文件**: `internal/api/game/handler.go`(在现有 Admin API 区域末尾追加) + +```go +// GetAdminLeaderboard Admin查询扫雷排行榜 +// @Router /api/admin/games/leaderboard [get] +func (h *handler) GetAdminLeaderboard() core.HandlerFunc { + return func(ctx core.Context) { + var req struct { + Page int `form:"page"` + PageSize int `form:"page_size"` + Nickname string `form:"nickname"` // 可选:按昵称模糊搜索 + } + _ = ctx.ShouldBindQuery(&req) + if req.Page <= 0 { req.Page = 1 } + if req.PageSize <= 0 || req.PageSize > 100 { req.PageSize = 20 } + + offset := (req.Page - 1) * req.PageSize + + type row struct { + UserID int64 `json:"user_id"` + Nickname string `json:"nickname"` + Avatar string `json:"avatar"` + TotalRankPoints int64 `json:"total_rank_points"` + MatchesPlayed int `json:"matches_played"` + Wins int `json:"wins"` + Losses int `json:"losses"` + WinRate float64 `json:"win_rate"` + BestScore int `json:"best_score"` + AvgScore float64 `json:"avg_score"` + } + + query := h.db.GetDbR().Table("minesweeper_leaderboard l"). + Select("l.user_id, COALESCE(u.nick_name,'') AS nickname, COALESCE(u.avatar_url,'') AS avatar, l.total_rank_points, l.matches_played, l.wins, l.losses, CAST(l.win_rate AS DECIMAL(7,4)) AS win_rate, l.best_score, CAST(l.avg_score AS DECIMAL(12,2)) AS avg_score"). + Joins("LEFT JOIN users u ON u.id = l.user_id"). + Where("l.game_type = ?", "minesweeper") + + if req.Nickname != "" { + query = query.Where("u.nick_name LIKE ?", "%"+req.Nickname+"%") + } + + var total int64 + query.Count(&total) + + var rows []row + query.Order("l.total_rank_points DESC, l.wins DESC, l.best_score DESC"). + Limit(req.PageSize).Offset(offset).Scan(&rows) + + // 补名次 + list := make([]map[string]any, 0, len(rows)) + for i, r := range rows { + list = append(list, map[string]any{ + "rank": offset + i + 1, + "user_id": r.UserID, + "nickname": r.Nickname, + "avatar": r.Avatar, + "total_rank_points": r.TotalRankPoints, + "matches_played": r.MatchesPlayed, + "wins": r.Wins, + "losses": r.Losses, + "win_rate": r.WinRate, + "best_score": r.BestScore, + "avg_score": r.AvgScore, + }) + } + + ctx.Payload(map[string]any{ + "total": total, + "page": req.Page, + "page_size": req.PageSize, + "list": list, + }) + } +} + +// GetAdminGameRecords Admin查询扫雷对战记录 +// @Router /api/admin/games/records [get] +func (h *handler) GetAdminGameRecords() core.HandlerFunc { + return func(ctx core.Context) { + var req struct { + Page int `form:"page"` + PageSize int `form:"page_size"` + UserID int64 `form:"user_id"` + MatchID string `form:"match_id"` + } + _ = ctx.ShouldBindQuery(&req) + if req.Page <= 0 { req.Page = 1 } + if req.PageSize <= 0 || req.PageSize > 100 { req.PageSize = 20 } + + offset := (req.Page - 1) * req.PageSize + + type row struct { + ID int64 `json:"id"` + MatchID string `json:"match_id"` + UserID int64 `json:"user_id"` + Nickname string `json:"nickname"` + IsWinner bool `json:"is_winner"` + RankPosition int `json:"rank_position"` + TotalPlayers int `json:"total_players"` + Score int `json:"score"` + DamageDealt int `json:"damage_dealt"` + ChestsCollected int `json:"chests_collected"` + RankPoints int `json:"rank_points"` + SettledAt string `json:"settled_at"` + } + + query := h.db.GetDbR().Table("minesweeper_game_records r"). + Select("r.id, r.match_id, r.user_id, COALESCE(u.nick_name,'') AS nickname, r.is_winner, r.rank_position, r.total_players, r.score, r.damage_dealt, r.chests_collected, r.rank_points, r.settled_at"). + Joins("LEFT JOIN users u ON u.id = r.user_id"). + Where("r.game_type = ?", "minesweeper") + + if req.UserID > 0 { + query = query.Where("r.user_id = ?", req.UserID) + } + if req.MatchID != "" { + query = query.Where("r.match_id = ?", req.MatchID) + } + + var total int64 + query.Count(&total) + + var rows []row + query.Order("r.settled_at DESC").Limit(req.PageSize).Offset(offset).Scan(&rows) + + ctx.Payload(map[string]any{ + "total": total, + "page": req.Page, + "page_size": req.PageSize, + "list": rows, + }) + } +} +``` + +### Step 2 — 后端:注册新路由 + +**文件**: `internal/router/router.go` + +在 admin 认证路由区域找到 game 相关路由,追加: +```go +adminAuthApiRouter.GET("/games/leaderboard", gameHandler.GetAdminLeaderboard()) +adminAuthApiRouter.GET("/games/records", gameHandler.GetAdminGameRecords()) +``` + +### Step 3 — 后端:SettleGame 去掉免费模式分支 + +**文件**: `internal/api/game/handler.go` + +`isFreeMode` 判断相关逻辑仍可保留(对 `minesweeper` 类型无影响),但移除文档/注释中所有 `minesweeper_free` 提及。 +实际上后端逻辑本身没问题,如果 Nakama 不再发送 `minesweeper_free` 类型就不会触发,无需修改业务逻辑。 + +### Step 4 — 前端:在 index.vue 新增"排行榜"Tab + +**文件**: `web/admin/src/views/operations/minesweeper/index.vue` + +#### 4.1 在 `` 中新增两个 Tab pane(追加在"配置预览"之前) + +```html + + +
+ +
+ + 刷新 +
+ + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + +
+
+ + + 刷新 +
+ + + + + + + + + + + + + + +
+ +
+
+
+``` + +#### 4.2 在 `