x
This commit is contained in:
parent
45ea70760b
commit
26696f4e80
404
.claude/plan/minesweeper-leaderboard-admin.md
Normal file
404
.claude/plan/minesweeper-leaderboard-admin.md
Normal file
@ -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 在 `<el-tabs>` 中新增两个 Tab pane(追加在"配置预览"之前)
|
||||
|
||||
```html
|
||||
<!-- 5. 排行榜 -->
|
||||
<el-tab-pane label="排行榜" name="leaderboard">
|
||||
<div class="tab-content">
|
||||
<el-alert
|
||||
title="对战分说明:对战分是游戏内部的排名积分,与平台积分(商城积分/兑换积分)无关。赢得对局可获得更多对战分,用于在此排行榜中排名。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
class="mb-4"
|
||||
show-icon
|
||||
/>
|
||||
<div class="flex gap-2 mb-4">
|
||||
<el-input
|
||||
v-model="lbSearch"
|
||||
placeholder="搜索玩家昵称"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
@change="fetchLeaderboard"
|
||||
/>
|
||||
<el-button @click="fetchLeaderboard">刷新</el-button>
|
||||
</div>
|
||||
<el-table :data="lbList" border stripe v-loading="lbLoading">
|
||||
<el-table-column label="排名" width="70" align="center">
|
||||
<template #default="scope">
|
||||
<span :class="scope.row.rank <= 3 ? 'font-bold text-yellow-600' : ''">
|
||||
{{ scope.row.rank }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="玩家" min-width="140">
|
||||
<template #default="scope">
|
||||
<div class="flex items-center gap-2">
|
||||
<el-avatar :src="scope.row.avatar" :size="28" v-if="scope.row.avatar" />
|
||||
<span>{{ scope.row.nickname || scope.row.user_id }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="对战分" prop="total_rank_points" width="100" align="right" sortable />
|
||||
<el-table-column label="场次" prop="matches_played" width="80" align="center" />
|
||||
<el-table-column label="胜场" prop="wins" width="70" align="center" />
|
||||
<el-table-column label="胜率" width="80" align="center">
|
||||
<template #default="scope">{{ (scope.row.win_rate * 100).toFixed(1) }}%</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最高分" prop="best_score" width="90" align="right" />
|
||||
<el-table-column label="平均分" prop="avg_score" width="90" align="right">
|
||||
<template #default="scope">{{ Number(scope.row.avg_score).toFixed(1) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-3">
|
||||
<el-pagination
|
||||
v-model:current-page="lbPage"
|
||||
v-model:page-size="lbPageSize"
|
||||
:total="lbTotal"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="fetchLeaderboard"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 6. 对战记录 -->
|
||||
<el-tab-pane label="对战记录" name="records">
|
||||
<div class="tab-content">
|
||||
<div class="flex gap-2 mb-4">
|
||||
<el-input
|
||||
v-model="recUserID"
|
||||
placeholder="按用户ID筛选"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
@change="fetchRecords"
|
||||
/>
|
||||
<el-input
|
||||
v-model="recMatchID"
|
||||
placeholder="按局ID筛选"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
@change="fetchRecords"
|
||||
/>
|
||||
<el-button @click="fetchRecords">刷新</el-button>
|
||||
</div>
|
||||
<el-table :data="recList" border stripe v-loading="recLoading" size="small">
|
||||
<el-table-column label="局ID" prop="match_id" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="玩家" min-width="120">
|
||||
<template #default="scope">{{ scope.row.nickname || scope.row.user_id }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结果" width="70" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.is_winner ? 'success' : 'info'" size="small">
|
||||
{{ scope.row.is_winner ? '胜' : '败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="名次" prop="rank_position" width="65" align="center" />
|
||||
<el-table-column label="总人数" prop="total_players" width="75" align="center" />
|
||||
<el-table-column label="得分" prop="score" width="80" align="right" />
|
||||
<el-table-column label="对战分" prop="rank_points" width="85" align="right" />
|
||||
<el-table-column label="结算时间" prop="settled_at" width="160" />
|
||||
</el-table>
|
||||
<div class="flex justify-end mt-3">
|
||||
<el-pagination
|
||||
v-model:current-page="recPage"
|
||||
v-model:page-size="recPageSize"
|
||||
:total="recTotal"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="fetchRecords"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
```
|
||||
|
||||
#### 4.2 在 `<script setup>` 中追加响应式数据和 API 函数
|
||||
|
||||
```typescript
|
||||
import request from '@/utils/http'
|
||||
|
||||
// 排行榜
|
||||
const lbSearch = ref('')
|
||||
const lbLoading = ref(false)
|
||||
const lbList = ref<any[]>([])
|
||||
const lbTotal = ref(0)
|
||||
const lbPage = ref(1)
|
||||
const lbPageSize = ref(20)
|
||||
|
||||
const fetchLeaderboard = async () => {
|
||||
lbLoading.value = true
|
||||
try {
|
||||
const res = await request.get('/admin/games/leaderboard', {
|
||||
params: { page: lbPage.value, page_size: lbPageSize.value, nickname: lbSearch.value }
|
||||
})
|
||||
lbList.value = res.list || []
|
||||
lbTotal.value = res.total || 0
|
||||
} finally {
|
||||
lbLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 对战记录
|
||||
const recUserID = ref('')
|
||||
const recMatchID = ref('')
|
||||
const recLoading = ref(false)
|
||||
const recList = ref<any[]>([])
|
||||
const recTotal = ref(0)
|
||||
const recPage = ref(1)
|
||||
const recPageSize = ref(20)
|
||||
|
||||
const fetchRecords = async () => {
|
||||
recLoading.value = true
|
||||
try {
|
||||
const res = await request.get('/admin/games/records', {
|
||||
params: {
|
||||
page: recPage.value,
|
||||
page_size: recPageSize.value,
|
||||
user_id: recUserID.value || undefined,
|
||||
match_id: recMatchID.value || undefined,
|
||||
}
|
||||
})
|
||||
recList.value = res.list || []
|
||||
recTotal.value = res.total || 0
|
||||
} finally {
|
||||
recLoading.value = false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.3 在 `onMounted` 中追加调用
|
||||
|
||||
```typescript
|
||||
onMounted(() => {
|
||||
loadConfig()
|
||||
fetchLeaderboard()
|
||||
fetchRecords()
|
||||
})
|
||||
```
|
||||
|
||||
#### 4.4 监听 Tab 切换(可选优化)
|
||||
|
||||
在 Tab 切换到对应 Tab 时按需加载,避免首次全量请求:
|
||||
```typescript
|
||||
watch(activeTab, (val) => {
|
||||
if (val === 'leaderboard') fetchLeaderboard()
|
||||
if (val === 'records') fetchRecords()
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关键文件汇总
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `internal/api/game/handler.go` | 修改 | 新增 `GetAdminLeaderboard()` 和 `GetAdminGameRecords()` 两个函数 |
|
||||
| `internal/router/router.go` | 修改 | 注册两条新路由 |
|
||||
| `web/admin/src/views/operations/minesweeper/index.vue` | 修改 | 新增排行榜和对战记录两个 Tab + script 逻辑 |
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- "积分"含义:页面上 `total_rank_points` 显示为"对战分",并加 Alert 说明,避免与平台积分(points/兑换币)混淆
|
||||
- 不需要新建文件,全部在现有文件中追加
|
||||
- 后端无需修改任何免费模式的业务逻辑;前端 Tab 中不再展示 game_type 切换选项即可
|
||||
- `activeTab` 初始值改为 `'board'`(已经是),排行榜 Tab 不作为默认 Tab
|
||||
- 分页默认每页 20 条
|
||||
|
||||
---
|
||||
|
||||
## SESSION_ID
|
||||
- CODEX_SESSION: N/A(本次直接由 Claude 规划,无外部模型调用)
|
||||
- GEMINI_SESSION: N/A
|
||||
@ -381,16 +381,19 @@ type settleResponse struct {
|
||||
Reward string `json:"reward,omitempty"`
|
||||
}
|
||||
|
||||
func calcRankPoints(win bool, score, damageDealt, damageTaken, chests, totalRounds int) int64 {
|
||||
base := int64(100)
|
||||
if win {
|
||||
base = 1000
|
||||
func calcRankPoints(rank int) int64 {
|
||||
switch rank {
|
||||
case 1:
|
||||
return 1000
|
||||
case 2:
|
||||
return -900
|
||||
case 3:
|
||||
return -1100
|
||||
case 4:
|
||||
return -1300
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
pts := base + int64(score)*10 + int64(damageDealt)*3 + int64(chests)*50 - int64(damageTaken)*2 - int64(totalRounds)
|
||||
if pts < 0 {
|
||||
pts = 0
|
||||
}
|
||||
return pts
|
||||
}
|
||||
|
||||
// SettleGame Internal游戏结算(批量全员)
|
||||
@ -411,10 +414,15 @@ func (h *handler) SettleGame() core.HandlerFunc {
|
||||
if len(req.Players) == 0 && req.UserID != "" {
|
||||
uid, _ := strconv.ParseInt(req.UserID, 10, 64)
|
||||
if uid > 0 {
|
||||
rank := 2
|
||||
if req.Win {
|
||||
rank = 1
|
||||
}
|
||||
req.Players = []settlePlayerRecord{{
|
||||
UserID: uid,
|
||||
Ticket: req.Ticket,
|
||||
Win: req.Win,
|
||||
Rank: rank,
|
||||
Score: req.Score,
|
||||
}}
|
||||
if req.GameType == "" {
|
||||
@ -457,7 +465,17 @@ func (h *handler) SettleGame() core.HandlerFunc {
|
||||
}
|
||||
|
||||
for _, p := range req.Players {
|
||||
rankPoints := calcRankPoints(p.Win, p.Score, p.DamageDealt, p.DamageTaken, p.ChestsCollected, req.TotalRounds)
|
||||
// 兜底:旧客户端漏传 Rank 时按 Win 推断
|
||||
rank := p.Rank
|
||||
if rank == 0 {
|
||||
rank = 2
|
||||
if p.Win {
|
||||
rank = 1
|
||||
}
|
||||
}
|
||||
p.Rank = rank
|
||||
p.Win = p.Rank == 1
|
||||
rankPoints := calcRankPoints(p.Rank)
|
||||
|
||||
rawJSON, _ := json.Marshal(p)
|
||||
|
||||
|
||||
@ -51,64 +51,59 @@ type settleResponse struct {
|
||||
|
||||
// ---- calcRankPoints 本地副本(保持与 handler.go 一致) ----
|
||||
|
||||
func calcRankPoints(win bool, score, damageDealt, damageTaken, chests, totalRounds int) int64 {
|
||||
base := int64(100)
|
||||
if win {
|
||||
base = 1000
|
||||
func calcRankPoints(rank int) int64 {
|
||||
switch rank {
|
||||
case 1:
|
||||
return 1000
|
||||
case 2:
|
||||
return -900
|
||||
case 3:
|
||||
return -1100
|
||||
case 4:
|
||||
return -1300
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
pts := base + int64(score)*10 + int64(damageDealt)*3 + int64(chests)*50 - int64(damageTaken)*2 - int64(totalRounds)
|
||||
if pts < 0 {
|
||||
pts = 0
|
||||
}
|
||||
return pts
|
||||
}
|
||||
|
||||
// ---- 积分公式单元测试 ----
|
||||
// ---- 名次积分单元测试 ----
|
||||
|
||||
func TestCalcRankPoints(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
win bool
|
||||
score int
|
||||
damageDealt int
|
||||
damageTaken int
|
||||
chests int
|
||||
rounds int
|
||||
expected int64
|
||||
name string
|
||||
rank int
|
||||
expected int64
|
||||
}{
|
||||
{
|
||||
name: "赢家基础积分",
|
||||
win: true, score: 0, damageDealt: 0, damageTaken: 0, chests: 0, rounds: 0,
|
||||
name: "第1名固定加分",
|
||||
rank: 1,
|
||||
expected: 1000,
|
||||
},
|
||||
{
|
||||
name: "输家基础积分",
|
||||
win: false, score: 0, damageDealt: 0, damageTaken: 0, chests: 0, rounds: 0,
|
||||
expected: 100,
|
||||
name: "第2名固定扣分",
|
||||
rank: 2,
|
||||
expected: -900,
|
||||
},
|
||||
{
|
||||
name: "赢家满加成",
|
||||
win: true, score: 50, damageDealt: 10, damageTaken: 5, chests: 3, rounds: 12,
|
||||
// 1000 + 50*10 + 10*3 + 3*50 - 5*2 - 12 = 1000+500+30+150-10-12 = 1658
|
||||
expected: 1658,
|
||||
name: "第3名固定扣分",
|
||||
rank: 3,
|
||||
expected: -1100,
|
||||
},
|
||||
{
|
||||
name: "不能为负数",
|
||||
win: false, score: 0, damageDealt: 0, damageTaken: 100, chests: 0, rounds: 1000,
|
||||
// 100 - 200 - 1000 < 0 → 0
|
||||
name: "第4名固定扣分",
|
||||
rank: 4,
|
||||
expected: -1300,
|
||||
},
|
||||
{
|
||||
name: "未知名次兜底为0",
|
||||
rank: 0,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "宝箱加成显著",
|
||||
win: true, score: 0, damageDealt: 0, damageTaken: 0, chests: 5, rounds: 0,
|
||||
// 1000 + 5*50 = 1250
|
||||
expected: 1250,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := calcRankPoints(tt.win, tt.score, tt.damageDealt, tt.damageTaken, tt.chests, tt.rounds)
|
||||
got := calcRankPoints(tt.rank)
|
||||
assert.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
@ -282,7 +277,7 @@ func TestSettleGame_Integration(t *testing.T) {
|
||||
|
||||
// 兼容旧版
|
||||
if len(req.Players) == 0 && req.UserID != "" {
|
||||
req.Players = []settlePlayerRecord{{UserID: 12345, Win: req.Win, Score: req.Score}}
|
||||
req.Players = []settlePlayerRecord{{UserID: 12345, Win: req.Win, Rank: 1, Score: req.Score}}
|
||||
}
|
||||
|
||||
if len(req.Players) == 0 {
|
||||
@ -291,9 +286,13 @@ func TestSettleGame_Integration(t *testing.T) {
|
||||
}
|
||||
|
||||
// 计算积分(验证公式被调用)
|
||||
// 计算积分(验证名次映射)
|
||||
expectedRankPoints := map[int]int64{1: 1000, 2: -900, 3: -1100, 4: -1300}
|
||||
for _, p := range req.Players {
|
||||
pts := calcRankPoints(p.Win, p.Score, p.DamageDealt, p.DamageTaken, p.ChestsCollected, req.TotalRounds)
|
||||
assert.GreaterOrEqual(t, pts, int64(0))
|
||||
pts := calcRankPoints(p.Rank)
|
||||
if expected, ok := expectedRankPoints[p.Rank]; ok {
|
||||
assert.Equal(t, expected, pts)
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, settleResponse{Success: true})
|
||||
@ -342,6 +341,6 @@ func TestSettleGame_OldBugScenario(t *testing.T) {
|
||||
|
||||
func BenchmarkCalcRankPoints(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
calcRankPoints(true, 50, 10, 5, 3, 12)
|
||||
calcRankPoints(1)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user