feat: 添加选手详情与比赛记录接口以支持小程序端

- 新增 `/api/ladder/player` 接口,兼容小程序端选手详情查询
- 新增 `/api/match/history` 接口,用于获取选手比赛记录
- 选手详情接口增加 `loseCount` 字段,完善比赛数据统计
- 比赛记录接口提供分页查询,包含对手信息与比赛结果详情
This commit is contained in:
ethanfly 2026-01-30 02:47:41 +08:00
parent 74ed19eee1
commit 75760d25fd
4 changed files with 252 additions and 61 deletions

View File

@ -1,28 +1,46 @@
const { LadderUser, User, Store } = require('../models');
const { LADDER_LEVEL_NAMES, LADDER_LEVEL_DESC, POWER_CALC } = require('../config/constants');
const { success, error, getPagination, pageResult } = require('../utils/helper');
const { Op } = require('sequelize');
const sequelize = require('../config/database');
const { LadderUser, User, Store } = require("../models");
const {
LADDER_LEVEL_NAMES,
LADDER_LEVEL_DESC,
POWER_CALC,
} = require("../config/constants");
const {
success,
error,
getPagination,
pageResult,
} = require("../utils/helper");
const { Op } = require("sequelize");
const sequelize = require("../config/database");
class LadderController {
// 获取天梯排名
async getRanking(req, res) {
try {
const { store_id, gender, level, page = 1, pageSize = 50, is_display } = req.query;
const {
store_id,
gender,
level,
page = 1,
pageSize = 50,
is_display,
} = req.query;
const { limit, offset } = getPagination(page, pageSize);
if (!store_id) {
return res.status(400).json(error('缺少门店ID', 400));
return res.status(400).json(error("缺少门店ID", 400));
}
const where = {
store_id,
status: 1
status: 1,
};
// 如果不是大屏显示,则需要满足每月最低参赛场次限制
if (!is_display) {
where.monthly_match_count = { [Op.gte]: POWER_CALC.MIN_MONTHLY_MATCHES };
where.monthly_match_count = {
[Op.gte]: POWER_CALC.MIN_MONTHLY_MATCHES,
};
}
if (gender) {
@ -35,11 +53,11 @@ class LadderController {
const { rows, count } = await LadderUser.findAndCountAll({
where,
include: [
{ model: User, as: 'user', attributes: ['nickname', 'avatar'] }
{ model: User, as: "user", attributes: ["nickname", "avatar"] },
],
order: [['power_score', 'DESC']],
order: [["power_score", "DESC"]],
limit,
offset
offset,
});
// 添加排名
@ -57,13 +75,16 @@ class LadderController {
powerScore: lu.power_score,
matchCount: lu.match_count,
winCount: lu.win_count,
winRate: lu.match_count > 0 ? Math.round(lu.win_count / lu.match_count * 100) : 0
winRate:
lu.match_count > 0
? Math.round((lu.win_count / lu.match_count) * 100)
: 0,
}));
res.json(pageResult(list, count, page, pageSize));
} catch (err) {
console.error('获取排名失败:', err);
res.status(500).json(error('获取失败'));
console.error("获取排名失败:", err);
res.status(500).json(error("获取失败"));
}
}
@ -74,13 +95,17 @@ class LadderController {
const ladderUser = await LadderUser.findByPk(id, {
include: [
{ model: User, as: 'user', attributes: ['nickname', 'avatar', 'member_code'] },
{ model: Store, as: 'store', attributes: ['id', 'name'] }
]
{
model: User,
as: "user",
attributes: ["nickname", "avatar", "member_code"],
},
{ model: Store, as: "store", attributes: ["id", "name"] },
],
});
if (!ladderUser || ladderUser.status !== 1) {
return res.status(404).json(error('用户不存在', 404));
return res.status(404).json(error("用户不存在", 404));
}
// 计算排名
@ -90,62 +115,138 @@ class LadderController {
gender: ladderUser.gender,
status: 1,
power_score: { [Op.gt]: ladderUser.power_score },
monthly_match_count: { [Op.gte]: POWER_CALC.MIN_MONTHLY_MATCHES }
}
monthly_match_count: { [Op.gte]: POWER_CALC.MIN_MONTHLY_MATCHES },
},
});
res.json(success({
id: ladderUser.id,
userId: ladderUser.user_id,
realName: ladderUser.real_name,
nickname: ladderUser.user?.nickname,
avatar: ladderUser.user?.avatar,
memberCode: ladderUser.user?.member_code,
gender: ladderUser.gender,
level: ladderUser.level,
levelName: LADDER_LEVEL_NAMES[ladderUser.level],
levelDesc: LADDER_LEVEL_DESC[ladderUser.level],
powerScore: ladderUser.power_score,
matchCount: ladderUser.match_count,
winCount: ladderUser.win_count,
monthlyMatchCount: ladderUser.monthly_match_count,
winRate: ladderUser.match_count > 0 ? Math.round(ladderUser.win_count / ladderUser.match_count * 100) : 0,
rank: higherCount + 1,
storeId: ladderUser.store_id,
storeName: ladderUser.store?.name,
lastMatchTime: ladderUser.last_match_time
}));
res.json(
success({
id: ladderUser.id,
userId: ladderUser.user_id,
realName: ladderUser.real_name,
nickname: ladderUser.user?.nickname,
avatar: ladderUser.user?.avatar,
memberCode: ladderUser.user?.member_code,
gender: ladderUser.gender,
level: ladderUser.level,
levelName: LADDER_LEVEL_NAMES[ladderUser.level],
levelDesc: LADDER_LEVEL_DESC[ladderUser.level],
powerScore: ladderUser.power_score,
matchCount: ladderUser.match_count,
winCount: ladderUser.win_count,
monthlyMatchCount: ladderUser.monthly_match_count,
winRate:
ladderUser.match_count > 0
? Math.round(
(ladderUser.win_count / ladderUser.match_count) * 100,
)
: 0,
rank: higherCount + 1,
storeId: ladderUser.store_id,
storeName: ladderUser.store?.name,
lastMatchTime: ladderUser.last_match_time,
}),
);
} catch (err) {
console.error('获取用户详情失败:', err);
res.status(500).json(error('获取失败'));
console.error("获取用户详情失败:", err);
res.status(500).json(error("获取失败"));
}
}
// 选手详情(兼容小程序端:/api/ladder/player?id=xxx
async getPlayerDetail(req, res) {
try {
const id = req.query && req.query.id ? String(req.query.id) : null;
if (!id) {
return res.status(400).json(error("缺少选手ID", 400));
}
const ladderUser = await LadderUser.findByPk(id, {
include: [
{
model: User,
as: "user",
attributes: ["nickname", "avatar", "member_code"],
},
{ model: Store, as: "store", attributes: ["id", "name"] },
],
});
if (!ladderUser || ladderUser.status !== 1) {
return res.status(404).json(error("用户不存在", 404));
}
const higherCount = await LadderUser.count({
where: {
store_id: ladderUser.store_id,
gender: ladderUser.gender,
status: 1,
power_score: { [Op.gt]: ladderUser.power_score },
monthly_match_count: { [Op.gte]: POWER_CALC.MIN_MONTHLY_MATCHES },
},
});
const matchCount = ladderUser.match_count || 0;
const winCount = ladderUser.win_count || 0;
res.json(
success({
id: ladderUser.id,
userId: ladderUser.user_id,
realName: ladderUser.real_name,
nickname: ladderUser.user && ladderUser.user.nickname,
avatar: ladderUser.user && ladderUser.user.avatar,
memberCode: ladderUser.user && ladderUser.user.member_code,
gender: ladderUser.gender,
level: ladderUser.level,
levelName: LADDER_LEVEL_NAMES[ladderUser.level],
levelDesc: LADDER_LEVEL_DESC[ladderUser.level],
powerScore: ladderUser.power_score,
matchCount: matchCount,
winCount: winCount,
loseCount: Math.max(matchCount - winCount, 0),
monthlyMatchCount: ladderUser.monthly_match_count,
winRate:
matchCount > 0 ? Math.round((winCount / matchCount) * 100) : 0,
rank: higherCount + 1,
storeId: ladderUser.store_id,
storeName: ladderUser.store && ladderUser.store.name,
lastMatchTime: ladderUser.last_match_time,
}),
);
} catch (err) {
console.error("获取用户详情失败:", err);
res.status(500).json(error("获取失败"));
}
}
// 获取等级说明
async getLevelInfo(req, res) {
try {
const levels = Object.keys(LADDER_LEVEL_NAMES).map(level => ({
const levels = Object.keys(LADDER_LEVEL_NAMES).map((level) => ({
level: parseInt(level),
name: LADDER_LEVEL_NAMES[level],
description: LADDER_LEVEL_DESC[level]
description: LADDER_LEVEL_DESC[level],
}));
res.json(success({
levels,
powerCalcRules: {
baseWin: POWER_CALC.BASE_WIN,
baseLose: POWER_CALC.BASE_LOSE,
underdogThreshold: POWER_CALC.UNDERDOG_THRESHOLD,
underdogRate: POWER_CALC.UNDERDOG_RATE,
maxChange: POWER_CALC.MAX_CHANGE,
newbieProtection: POWER_CALC.NEWBIE_PROTECTION,
minMonthlyMatches: POWER_CALC.MIN_MONTHLY_MATCHES,
challengeCooldown: POWER_CALC.CHALLENGE_COOLDOWN
}
}));
res.json(
success({
levels,
powerCalcRules: {
baseWin: POWER_CALC.BASE_WIN,
baseLose: POWER_CALC.BASE_LOSE,
underdogThreshold: POWER_CALC.UNDERDOG_THRESHOLD,
underdogRate: POWER_CALC.UNDERDOG_RATE,
maxChange: POWER_CALC.MAX_CHANGE,
newbieProtection: POWER_CALC.NEWBIE_PROTECTION,
minMonthlyMatches: POWER_CALC.MIN_MONTHLY_MATCHES,
challengeCooldown: POWER_CALC.CHALLENGE_COOLDOWN,
},
}),
);
} catch (err) {
console.error('获取等级信息失败:', err);
res.status(500).json(error('获取失败'));
console.error("获取等级信息失败:", err);
res.status(500).json(error("获取失败"));
}
}
}

View File

@ -756,6 +756,90 @@ class MatchController {
}
}
// 获取选手比赛记录(用于选手详情页)
async getPlayerHistory(req, res) {
try {
const { player_id, page = 1, pageSize = 20 } = req.query;
const { limit, offset } = getPagination(page, pageSize);
if (!player_id) {
return res.status(400).json(error('缺少选手ID', 400));
}
const player = await LadderUser.findByPk(player_id);
if (!player || player.status !== 1) {
return res.json(pageResult([], 0, page, pageSize));
}
const { rows, count } = await MatchGame.findAndCountAll({
where: {
[Op.or]: [
{ player1_id: player.id },
{ player2_id: player.id }
],
confirm_status: CONFIRM_STATUS.CONFIRMED
},
include: [
{ model: Match, as: 'match', attributes: ['id', 'name', 'type', 'weight'] }
],
order: [['confirmed_at', 'DESC']],
limit,
offset
});
const opponentIds = [];
rows.forEach((g) => {
if (g.player1_id === player.id) opponentIds.push(g.player2_id);
else opponentIds.push(g.player1_id);
});
const uniqueOpponentIds = Array.from(new Set(opponentIds));
const opponents = await LadderUser.findAll({
where: { id: { [Op.in]: uniqueOpponentIds } },
include: [{ model: User, as: 'user', attributes: ['nickname', 'avatar'] }]
});
const opponentMap = new Map(opponents.map((o) => [String(o.id), o]));
const list = rows.map((game) => {
const isPlayer1 = game.player1_id === player.id;
const opponentId = isPlayer1 ? game.player2_id : game.player1_id;
const opponent = opponentMap.get(String(opponentId));
const myScore = isPlayer1 ? game.player1_score : game.player2_score;
const opponentScore = isPlayer1 ? game.player2_score : game.player1_score;
const isWin = game.winner_id === player.id;
const typeName = game.match && game.match.type === MATCH_TYPES.CHALLENGE ? '挑战赛' : '排位赛';
return {
id: game.id,
matchId: game.match_id,
name: (game.match && game.match.name) || typeName,
type: game.match && game.match.type,
typeName,
createTime: game.confirmed_at,
desc: opponent ? `vs ${opponent.real_name} ${myScore}:${opponentScore}` : `${myScore}:${opponentScore}`,
result: isWin ? 'win' : 'lose',
resultName: isWin ? '胜' : '负',
powerChange: isWin ? game.winner_power_change : game.loser_power_change,
opponent: opponent
? {
id: opponent.id,
realName: opponent.real_name,
nickname: opponent.user && opponent.user.nickname,
avatar: opponent.user && opponent.user.avatar,
level: opponent.level,
powerScore: opponent.power_score
}
: null
};
});
res.json(pageResult(list, count, page, pageSize));
} catch (err) {
console.error('获取选手比赛记录失败:', err);
res.status(500).json(error('获取失败'));
}
}
// 获取正在进行中的比赛
async getOngoingMatches(req, res) {
try {

View File

@ -9,6 +9,9 @@ router.get('/ranking', ladderController.getRanking);
// 获取天梯用户详情
router.get('/user/:id', ladderController.getUserDetail);
// 选手详情(兼容小程序端:/api/ladder/player?id=xxx
router.get('/player', ladderController.getPlayerDetail);
// 获取等级说明
router.get('/levels', ladderController.getLevelInfo);

View File

@ -39,6 +39,9 @@ router.post('/ranking/confirm-score', authUser, matchController.confirmRankingSc
// 获取正在进行中的比赛
router.get('/ongoing', authUser, matchController.getOngoingMatches);
// 获取选手比赛记录(用于选手详情页)
router.get('/history', authUser, matchController.getPlayerHistory);
// 获取我的比赛记录
router.get('/my-matches', authUser, matchController.getMyMatches);