146 lines
4.2 KiB
TypeScript
146 lines
4.2 KiB
TypeScript
// server/routes/leaderboard.ts
|
|
import express, { Request, Response } from 'express';
|
|
import { PracticeRecord } from '../models/PracticeRecord';
|
|
import { auth } from '../middleware/auth';
|
|
|
|
// 定义排序字段类型
|
|
type SortField = 'totalWords' | 'accuracy' | 'duration' | 'speed';
|
|
|
|
// 排序字段映射
|
|
const sortFieldMapping = {
|
|
totalWords: { field: '$totalWords', label: '总单词数' },
|
|
accuracy: { field: '$avgAccuracy', label: '平均正确率' },
|
|
duration: { field: '$totalDuration', label: '练习总时长' },
|
|
speed: { field: '$avgSpeed', label: '平均速度' }
|
|
};
|
|
|
|
const router = express.Router();
|
|
|
|
// 获取排行榜数据
|
|
router.get('/:type', async (req: Request, res: Response) => {
|
|
try {
|
|
const { type } = req.params;
|
|
const {
|
|
page = '1',
|
|
limit = '10',
|
|
sortBy = 'totalWords' // 默认按总单词数排序
|
|
} = req.query;
|
|
|
|
const pageNum = parseInt(page as string);
|
|
const pageSize = parseInt(limit as string);
|
|
const skipCount = (pageNum - 1) * pageSize;
|
|
const sortField = sortBy as SortField;
|
|
|
|
if (!Object.keys(sortFieldMapping).includes(sortField)) {
|
|
return res.status(400).json({ error: '无效的排序字段' });
|
|
}
|
|
|
|
// 聚合查询
|
|
const records = await PracticeRecord.aggregate([
|
|
{ $match: { type } },
|
|
{
|
|
$group: {
|
|
_id: '$userId',
|
|
username: { $first: '$username' },
|
|
fullname: { $first: '$fullname' },
|
|
totalWords: { $sum: '$stats.totalWords' },
|
|
avgAccuracy: { $avg: '$stats.accuracy' },
|
|
totalDuration: { $sum: '$stats.duration' },
|
|
avgSpeed: { $avg: '$stats.wordsPerMinute' },
|
|
lastPractice: { $max: '$stats.endTime' },
|
|
practiceCount: { $sum: 1 }
|
|
}
|
|
},
|
|
{
|
|
$sort: {
|
|
[sortFieldMapping[sortField].field.substring(1)]: -1
|
|
}
|
|
},
|
|
{ $skip: skipCount },
|
|
{ $limit: pageSize },
|
|
{
|
|
$project: {
|
|
userId: '$_id',
|
|
username: 1,
|
|
fullname: 1,
|
|
stats: {
|
|
totalWords: '$totalWords',
|
|
accuracy: '$avgAccuracy',
|
|
duration: '$totalDuration',
|
|
wordsPerMinute: '$avgSpeed',
|
|
practiceCount: '$practiceCount',
|
|
lastPractice: '$lastPractice'
|
|
}
|
|
}
|
|
}
|
|
]);
|
|
|
|
// 获取总用户数
|
|
const totalUsers = await PracticeRecord.distinct('userId', { type });
|
|
|
|
res.json({
|
|
records,
|
|
total: totalUsers.length,
|
|
currentPage: pageNum,
|
|
totalPages: Math.ceil(totalUsers.length / pageSize),
|
|
sortField,
|
|
sortLabel: sortFieldMapping[sortField].label
|
|
});
|
|
} catch (error) {
|
|
console.error('获取排行榜失败:', error);
|
|
res.status(500).json({ error: '获取排行榜失败' });
|
|
}
|
|
});
|
|
|
|
// 获取用户排名
|
|
router.get('/:type/my-rank', auth, async (req: Request, res: Response) => {
|
|
try {
|
|
const { type } = req.params;
|
|
const { sortBy = 'totalWords' } = req.query;
|
|
const sortField = sortBy as SortField;
|
|
|
|
if (!req.user?._id) {
|
|
return res.status(401).json({ error: '未登录' });
|
|
}
|
|
|
|
if (!Object.keys(sortFieldMapping).includes(sortField)) {
|
|
return res.status(400).json({ error: '无效的排序字段' });
|
|
}
|
|
|
|
// 获取所有用户的排序后记录
|
|
const allRecords = await PracticeRecord.aggregate([
|
|
{ $match: { type } },
|
|
{
|
|
$group: {
|
|
_id: '$userId',
|
|
totalWords: { $sum: '$stats.totalWords' },
|
|
avgAccuracy: { $avg: '$stats.accuracy' },
|
|
totalDuration: { $sum: '$stats.duration' },
|
|
avgSpeed: { $avg: '$stats.wordsPerMinute' }
|
|
}
|
|
},
|
|
{
|
|
$sort: {
|
|
[sortFieldMapping[sortField].field.substring(1)]: -1
|
|
}
|
|
}
|
|
]);
|
|
|
|
// 找到用户的排名
|
|
const userRank = allRecords.findIndex(record =>
|
|
record._id.toString() === req.user!._id
|
|
) + 1;
|
|
|
|
res.json({
|
|
rank: userRank > 0 ? userRank : null,
|
|
totalParticipants: allRecords.length,
|
|
sortField,
|
|
sortLabel: sortFieldMapping[sortField].label
|
|
});
|
|
} catch (error) {
|
|
console.error('获取排名失败:', error);
|
|
res.status(500).json({ error: '获取排名失败' });
|
|
}
|
|
});
|
|
|
|
export default router; |