Migrate project from typing_practiceweb

This commit is contained in:
2026-05-14 16:04:14 +08:00
parent dbc5425706
commit 0e87d5546d
225 changed files with 92811 additions and 0 deletions

View File

@@ -0,0 +1,106 @@
import { Request, Response } from 'express';
import { OAuth2Client } from '../models/oauth2';
import { generateRandomString } from '../utils/crypto';
export class AdminController {
// 创建OAuth2客户端
async createOAuth2Client(req: Request, res: Response) {
const { name, redirectUris, scope } = req.body;
try {
const clientId = generateRandomString(24);
const clientSecret = generateRandomString(32);
const client = new OAuth2Client({
name,
clientId,
clientSecret,
redirectUris,
scope: scope.split(' '),
grants: ['authorization_code']
});
await client.save();
res.json({
clientId,
clientSecret,
name,
redirectUris,
scope
});
} catch (error) {
res.status(500).json({ error: 'server_error1' });
}
}
// 获取OAuth2客户端列表
async listOAuth2Clients(req: Request, res: Response) {
try {
const clients = await OAuth2Client.find({}, {
clientSecret: 0
});
res.json(clients);
} catch (error) {
res.status(500).json({ error: 'server_error2' });
}
}
// 获取单个OAuth2客户端
async getOAuth2Client(req: Request, res: Response) {
try {
const { id } = req.params;
const client = await OAuth2Client.findOne({ clientId: id }, {
clientSecret: 0
});
if (!client) {
return res.status(404).json({ error: 'client_not_found' });
}
res.json(client);
} catch (error) {
res.status(500).json({ error: 'server_error9' });
}
}
// 更新OAuth2客户端
async updateOAuth2Client(req: Request, res: Response) {
try {
const { id } = req.params;
const { name, redirectUris, scope } = req.body;
const client = await OAuth2Client.findOne({ clientId: id });
if (!client) {
return res.status(404).json({ error: 'client_not_found' });
}
const updateData = {
name,
redirectUris,
scope: scope.split(' ')
};
await OAuth2Client.updateOne({ clientId: id }, updateData);
res.json({ ...updateData, clientId: id });
} catch (error) {
res.status(500).json({ error: 'server_error3' });
}
}
// 删除OAuth2客户端
async deleteOAuth2Client(req: Request, res: Response) {
try {
const { id } = req.params;
const result = await OAuth2Client.deleteOne({ clientId: id });
if (result.deletedCount === 0) {
return res.status(404).json({ error: 'client_not_found' });
}
res.status(204).send();
} catch (error) {
res.status(500).json({ error: 'server_error4' });
}
}
}

View File

@@ -0,0 +1,14 @@
import { Router } from 'express';
import { AdminController } from './admin.controller';
const router = Router();
const adminController = new AdminController();
// OAuth2客户端管理路由
router.post('/oauth2/clients', adminController.createOAuth2Client);
router.get('/oauth2/clients', adminController.listOAuth2Clients);
router.get('/oauth2/clients/:id', adminController.getOAuth2Client);
router.put('/oauth2/clients/:id', adminController.updateOAuth2Client);
router.delete('/oauth2/clients/:id', adminController.deleteOAuth2Client);
export default router;

324
server/routes/admin.ts Normal file
View File

@@ -0,0 +1,324 @@
// server/routes/admin.ts
import express from 'express';
import { adminAuth } from '../middleware/auth';
import { User } from '../models/User';
import bcrypt from 'bcrypt';
import { OAuth2Client } from '../models/oauth2';
import { AdminController } from './admin.controller';
import multer from 'multer';
import fs from 'fs';
import path from 'path';
import csv from 'csv-parser';
import { Word, WordSet } from '../models/Vocabulary';
import mongoose from 'mongoose';
const router = express.Router();
const adminController = new AdminController();
// 配置 multer 用于文件上传
const storage = multer.diskStorage({
destination: function (req, file, cb) {
const uploadDir = path.join(__dirname, '../uploads');
// 确保上传目录存在
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: function (req, file, cb) {
cb(null, `${Date.now()}-${file.originalname}`);
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: 5 * 1024 * 1024 // 限制5MB
},
fileFilter: function (req, file, cb) {
// 只允许上传CSV文件
if (file.mimetype === 'text/csv' || file.originalname.endsWith('.csv')) {
cb(null, true);
} else {
cb(new Error('只支持CSV文件'));
}
}
});
router.get('/users', adminAuth, async (req, res) => {
try {
const users = await User.find()
.select('-password')
.sort({ createdAt: -1 });
res.json(users);
} catch (error) {
res.status(500).json({ message: '获取用户列表失败' });
}
});
// 添加 PUT 路由用于更新用户
router.put('/users/:id', adminAuth, async (req, res) => {
try {
// 检查用户名是否已存在(排除当前用户)
const existingUser = await User.findOne({
username: req.body.username,
_id: { $ne: req.params.id }
});
if (existingUser) {
return res.status(400).json({ message: '用户名已被使用' });
}
// 检查邮箱是否已存在(排除当前用户)
if (req.body.email) {
const existingEmail = await User.findOne({
email: req.body.email,
_id: { $ne: req.params.id }
});
if (existingEmail) {
return res.status(400).json({ message: '邮箱已被使用' });
}
}
const user = await User.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, select: '-password' }
);
if (!user) {
return res.status(404).json({ message: '用户不存在' });
}
res.json(user);
} catch (error) {
console.error('Error updating user:', error);
res.status(500).json({ message: '更新用户失败' });
}
});
router.post('/users', adminAuth, async (req, res) => {
try {
const { username, email, password, fullname, isAdmin } = req.body;
// 验证必填字段
if (!username || !email || !password || !fullname) {
return res.status(400).json({ message: '请填写所有必填字段' });
}
// 检查用户名是否已存在
const existingUser = await User.findOne({ username });
if (existingUser) {
return res.status(400).json({ message: '用户名已被使用' });
}
// 检查邮箱是否已存在
const existingEmail = await User.findOne({ email });
if (existingEmail) {
return res.status(400).json({ message: '邮箱已被使用' });
}
// 创建新用户
const newUser = new User({
username,
email,
password: password,
fullname,
isAdmin: isAdmin || false
});
await newUser.save();
// 返回用户信息(不包含密码)
const userResponse = await User.findById(newUser._id).select('-password');
res.status(201).json(userResponse);
} catch (error) {
console.error('Error creating user:', error);
res.status(500).json({ message: '创建用户失败' });
}
});
// 添加删除用户路由
router.delete('/users/:id', adminAuth, async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ message: '用户不存在' });
}
// 防止删除最后一个管理员
if (user.isAdmin) {
const adminCount = await User.countDocuments({ isAdmin: true });
if (adminCount <= 1) {
return res.status(400).json({ message: '不能删除最后一个管理员' });
}
}
await User.findByIdAndDelete(req.params.id);
res.json({ message: '用户已删除' });
} catch (error) {
console.error('Error deleting user:', error);
res.status(500).json({ message: '删除用户失败' });
}
});
// OAuth2 客户端管理路由
router.get('/oauth2/clients', adminAuth, (req, res) => adminController.listOAuth2Clients(req, res));
router.post('/oauth2/clients', adminAuth, (req, res) => adminController.createOAuth2Client(req, res));
router.put('/oauth2/clients/:id', adminAuth, (req, res) => adminController.updateOAuth2Client(req, res));
router.delete('/oauth2/clients/:id', adminAuth, (req, res) => adminController.deleteOAuth2Client(req, res));
// ===== 单词库管理路由 =====
// 获取所有单词集
router.get('/vocabulary/word-sets', adminAuth, async (req, res) => {
try {
const wordSets = await WordSet.find()
.select('name description totalWords createdAt owner')
.populate('owner', 'username fullname')
.sort({ createdAt: -1 });
res.json(wordSets);
} catch (error) {
console.error('获取单词集失败:', error);
res.status(500).json({ message: '获取单词集失败' });
}
});
// 上传单词文件并创建单词集(管理员版本)
router.post('/vocabulary/upload', adminAuth, upload.single('file'), async (req, res) => {
if (!req.file) {
return res.status(400).json({ message: '请上传文件' });
}
try {
const { name, description, ownerId } = req.body;
const fileName = req.file.filename;
const filePath = req.file.path;
// 如果没有指定所有者,默认为当前管理员
const owner = ownerId || req.user._id;
// 检查是否已存在同名单词集
const existingSet = await WordSet.findOne({
name: name || path.basename(fileName, '.csv'),
owner: owner
});
if (existingSet) {
// 删除上传的文件
fs.unlinkSync(filePath);
return res.status(400).json({ message: '已存在同名单词集' });
}
// 创建单词集
const wordSet = await WordSet.create({
name: name || path.basename(fileName, '.csv'),
description: description || '',
owner: owner,
totalWords: 0
});
const results: any[] = [];
let wordCount = 0;
// 处理CSV文件
fs.createReadStream(filePath)
.pipe(csv())
.on('data', (data) => {
// 检查必要的字段是否存在
if (data.word && data.translation) {
results.push({
word: data.word.trim(),
translation: data.translation.trim(),
pronunciation: data.pronunciation?.trim(),
example: data.example?.trim(),
wordSet: wordSet._id
});
wordCount++;
}
})
.on('end', async () => {
try {
// 批量插入单词
if (results.length > 0) {
await Word.insertMany(results);
// 更新单词集的单词数量
await WordSet.findByIdAndUpdate(wordSet._id, { totalWords: wordCount });
}
// 删除上传的文件
fs.unlinkSync(filePath);
res.status(201).json({
message: `成功创建单词集并导入${wordCount}个单词`,
wordSet
});
} catch (error) {
// 如果插入单词失败,删除创建的单词集
await WordSet.findByIdAndDelete(wordSet._id);
console.error('导入单词失败:', error);
res.status(500).json({ message: '导入单词失败' });
}
})
.on('error', (error) => {
console.error('处理CSV文件失败:', error);
res.status(500).json({ message: '处理CSV文件失败' });
});
} catch (error) {
console.error('上传文件失败:', error);
res.status(500).json({ message: '上传文件失败' });
}
});
// 删除单词集
router.delete('/vocabulary/word-sets/:id', adminAuth, async (req, res) => {
try {
const { id } = req.params;
// 检查单词集是否存在
const wordSet = await WordSet.findById(id);
if (!wordSet) {
return res.status(404).json({ message: '未找到单词集' });
}
// 删除单词集及其关联的所有单词
await Word.deleteMany({ wordSet: id });
await WordSet.findByIdAndDelete(id);
res.json({ message: '单词集已删除' });
} catch (error) {
console.error('删除单词集失败:', error);
res.status(500).json({ message: '删除单词集失败' });
}
});
// 单词集详情(包括所有单词)
router.get('/vocabulary/word-sets/:id/words', adminAuth, async (req, res) => {
try {
const { id } = req.params;
// 检查单词集是否存在
const wordSet = await WordSet.findById(id);
if (!wordSet) {
return res.status(404).json({ message: '未找到单词集' });
}
// 获取单词集中的所有单词
const words = await Word.find({ wordSet: id });
res.json({
wordSet,
words
});
} catch (error) {
console.error('获取单词集详情失败:', error);
res.status(500).json({ message: '获取单词集详情失败' });
}
});
export default router;

296
server/routes/auth.ts Normal file
View File

@@ -0,0 +1,296 @@
import express, { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { config } from '../config';
import { MongoError } from 'mongodb';
import { User, type IUser, type UserStats } from '../models/User';
import bcrypt from 'bcrypt';
import { Session } from 'express-session';
import mongoose from 'mongoose';
interface CustomSession extends Session {
userId?: string;
isAuthenticated?: boolean;
}
// 扩展 Request 类型
interface CustomRequest extends Request {
session: CustomSession;
}
// 定义 JWT payload 的类型
interface UserPayload {
_id: string;
username: string;
fullname: string;
email: string;
isAdmin: boolean;
exp?: number;
}
// 扩展 Express 的 Request 类型
declare global {
namespace Express {
interface Request {
user?: UserPayload;
}
}
}
const router = express.Router();
// 定义路由处理函数类型
type RouteHandler = (
req: Request,
res: Response,
next: NextFunction
) => Promise<void | any>; // 允许任何返回值
// 登录处理函数
const loginHandler: RouteHandler = async (req: CustomRequest, res) => {
try {
const { username, password } = req.body;
console.log('Login attempt:', { username });
const user = await User.findOne({ username });
if (!user) {
console.log('User not found');
res.status(401).json({ message: '用户名不存在' });
return;
}
const isPasswordValid = await user.comparePassword(password);
if (!isPasswordValid) {
console.log('Password mismatch');
return res.status(401).json({ message: '密码错误' });
}
// 确保设置这些session值
req.session.userId = (user._id as mongoose.Types.ObjectId).toString();
req.session.isAuthenticated = true;
// 如果有重定向参数则重定向回原始OAuth2请求
const redirectUrl = req.query.redirect || '/';
// 生成 JWT token
const token = jwt.sign(
{
_id: user._id,
username: user.username,
fullname: user.fullname,
email: user.email,
isAdmin: user.isAdmin
},
config.JWT_SECRET,
{ expiresIn: '24h' }
);
// 调试:打印 token 信息(不打印完整 token
console.log('Generated token info:', {
username: user.username,
tokenLength: token.length,
tokenStart: token.substring(0, 20) + '...'
});
console.log('Login successful:', {
username, fullname:user.fullname,
isAdmin: user.isAdmin,
hasToken: !!token
});
res.json({
token,
user: {
_id: user._id,
username: user.username,
fullname: user.fullname,
email: user.email,
isAdmin: user.isAdmin
},
success: true,
redirectUrl
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ message: '登录失败' });
}
};
// 注册处理函数
const registerHandler: RouteHandler = async (req, res) => {
try {
const { username, password, email,fullname } = req.body;
console.log('Registration attempt:', { username, email,fullname });
if (!email) {
return res.status(400).json({ message: '邮箱是必填项' });
}
// 检查用户名是否存在
const existingUsername = await User.findOne({ username });
if (existingUsername) {
return res.status(400).json({ message: '用户名已存在' });
}
const existingEmail = await User.findOne({ email });
if (existingEmail) {
return res.status(400).json({ message: '邮箱已被使用' });
}
const userCount = await User.countDocuments();
const isAdmin = userCount === 0;
const user = new User({
username,
password,
email,fullname ,
isAdmin
});
await user.save();
const token = jwt.sign(
{
_id: user._id,
username: user.username,
fullname: user.fullname,
email: user.email,
isAdmin: user.isAdmin
},
config.JWT_SECRET,
{ expiresIn: '24h' }
);
console.log('Registration successful:', {
username: user.username,
email: user.email,
isAdmin: user.isAdmin,
hasToken: !!token
});
res.status(201).json({
token,
user: {
_id: user._id,
username: user.username,
fullname:user.fullname,
email: user.email,
isAdmin: user.isAdmin
}
});
} catch (error) {
console.error('Registration error:', error);
if (error instanceof MongoError && error.code === 11000) {
res.status(400).json({ message: '用户名或邮箱已存在' });
} else {
res.status(500).json({
message: '注册失败:' + (error instanceof Error ? error.message : '未知错误')
});
}
}
};
// 中间件:验证 token
const authMiddleware: RouteHandler = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ message: '未登录' });
}
const token = authHeader.substring(7);
const decoded = jwt.verify(token, config.JWT_SECRET) as UserPayload;
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ message: '登录已过期,请重新登录' });
}
};
// 修改密码处理函数
const changePasswordHandler: RouteHandler = async (req, res) => {
try {
if (!req.user?._id) {
return res.status(401).json({ message: '未登录' });
}
const { oldPassword, newPassword } = req.body;
// 参数验证
if (!oldPassword || !newPassword) {
return res.status(400).json({ message: '请提供原密码和新密码' });
}
if (newPassword.length < 6) {
return res.status(400).json({ message: '新密码长度至少6个字符' });
}
// 查找用户
const user = await User.findById(req.user._id);
if (!user) {
return res.status(404).json({ message: '用户不存在' });
}
// 验证原密码
const isPasswordValid = await user.comparePassword(oldPassword);
if (!isPasswordValid) {
console.log('Old password mismatch for user:', user.username);
return res.status(401).json({ message: '原密码错误' });
}
// 更新密码
user.password = newPassword;
await user.save();
console.log('Password changed successfully for user:', user.username);
res.json({ message: '密码修改成功' });
} catch (error) {
console.error('Change password error:', error);
res.status(500).json({
message: '修改密码失败:' + (error instanceof Error ? error.message : '未知错误')
});
}
};
// 登出处理函数
const logoutHandler: RouteHandler = async (req: CustomRequest, res) => {
try {
console.log('Logout request received');
// 清除 session
if (req.session) {
req.session.destroy((err) => {
if (err) {
console.error('Session destruction error:', err);
return res.status(500).json({ message: '登出失败' });
}
// 清除 cookie
res.clearCookie('connect.sid');
console.log('Logout successful');
res.json({ success: true, message: '登出成功' });
});
} else {
console.log('No session to destroy');
res.json({ success: true, message: '登出成功' });
}
} catch (error) {
console.error('Logout error:', error);
res.status(500).json({ message: '登出失败' });
}
};
// 登录路由
router.post('/login', loginHandler);
// 注册路由
router.post('/register', registerHandler);
// 登出路由
router.post('/logout', logoutHandler);
// 修改密码路由
router.post('/change-password', authMiddleware, changePasswordHandler);
export default router;

View File

@@ -0,0 +1,80 @@
import express from 'express';
import { CodeExample } from '../models/CodeExample';
const router = express.Router();
// 获取所有代码示例
router.get('/', async (req, res) => {
try {
const examples = await CodeExample.find().sort({ createdAt: -1 });
res.json(examples);
} catch (error) {
res.status(500).json({ message: 'Error fetching code examples' });
}
});
// 获取特定难度级别的代码示例
router.get('/:level', async (req, res) => {
try {
const { level } = req.params;
// 验证难度级别是否有效
const validLevels = ['basic', 'intermediate', 'advanced'];
if (!validLevels.includes(level)) {
return res.status(400).json({ message: '无效的代码难度级别' });
}
// 查找匹配难度级别的代码示例
const example = await CodeExample.findOne({ level }).sort({ createdAt: -1 });
if (!example) {
return res.status(404).json({ message: '未找到该难度级别的代码示例' });
}
// 返回代码示例内容
res.json({
content: example.content,
level: example.level,
title: example.title
});
} catch (error) {
console.error('获取代码示例错误:', error);
res.status(500).json({ message: '获取代码示例失败' });
}
});
// 创建新代码示例
router.post('/', async (req, res) => {
try {
const example = new CodeExample(req.body);
await example.save();
res.status(201).json(example);
} catch (error) {
res.status(500).json({ message: 'Error creating code example' });
}
});
// 更新代码示例
router.put('/:id', async (req, res) => {
try {
const example = await CodeExample.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true }
);
res.json(example);
} catch (error) {
res.status(500).json({ message: 'Error updating code example' });
}
});
// 删除代码示例
router.delete('/:id', async (req, res) => {
try {
await CodeExample.findByIdAndDelete(req.params.id);
res.status(204).send();
} catch (error) {
res.status(500).json({ message: 'Error deleting code example' });
}
});
export default router;

65
server/routes/keywords.ts Normal file
View File

@@ -0,0 +1,65 @@
import express from 'express';
import { CodeExample } from '../models/CodeExample'; // 改用 CodeExample 模型
const router = express.Router();
// 获取关键字
router.get('/', async (req, res) => {
try {
// 从 CodeExample 集合中查询 level 为 'keyword' 的数据
const keywordExample = await CodeExample.findOne({ level: 'keyword' });
if (!keywordExample) {
return res.status(404).json({ error: '未找到关键字数据' });
}
res.json({ content: keywordExample.content });
} catch (error) {
console.error('获取关键字失败:', error);
res.status(500).json({ error: '获取关键字失败' });
}
});
// 添加创建/更新关键字的路由
router.post('/', async (req, res) => {
try {
const { content } = req.body;
if (!content) {
return res.status(400).json({ error: '关键字内容不能为空' });
}
const keywords = await CodeExample.create({
content,
updatedAt: new Date()
});
res.status(201).json(keywords);
} catch (error) {
console.error('创建关键字失败:', error);
res.status(500).json({ error: '创建关键字失败' });
}
});
// 更新关键字
router.put('/', async (req, res) => {
try {
const { content } = req.body;
if (!content) {
return res.status(400).json({ error: '关键字内容不能为空' });
}
const keywords = await CodeExample.findOneAndUpdate(
{},
{ content, updatedAt: new Date() },
{ new: true, upsert: true }
);
res.json(keywords);
} catch (error) {
console.error('更新关键字失败:', error);
res.status(500).json({ error: '更新关键字失败' });
}
});
export default router;

View File

@@ -0,0 +1,146 @@
// 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;

View File

@@ -0,0 +1,163 @@
// server/routes/minesweeper.ts
import express, { Request, Response } from 'express';
import { MinesweeperRecord, MinesweeperDifficulty } from '../models/MinesweeperRecord';
import { auth } from '../middleware/auth';
const router = express.Router();
// 提交游戏记录(需要登录)
router.post('/record', auth, async (req: Request, res: Response) => {
try {
const { difficulty, timeSeconds, won } = req.body;
if (!req.user?._id) {
return res.status(401).json({ error: '未登录' });
}
// 验证难度级别
const validDifficulties: MinesweeperDifficulty[] = ['beginner', 'intermediate', 'expert', 'brutal'];
if (!validDifficulties.includes(difficulty)) {
return res.status(400).json({ error: '无效的难度级别' });
}
// 验证时间
if (typeof timeSeconds !== 'number' || timeSeconds < 0) {
return res.status(400).json({ error: '无效的时间' });
}
// 验证胜负
if (typeof won !== 'boolean') {
return res.status(400).json({ error: '无效的游戏结果' });
}
// 创建游戏记录
const record = new MinesweeperRecord({
userId: req.user._id,
username: req.user.username,
fullname: req.user.fullname || req.user.username,
difficulty,
timeSeconds,
won
});
await record.save();
res.status(201).json({
message: '游戏记录保存成功',
record
});
} catch (error) {
console.error('保存游戏记录失败:', error);
res.status(500).json({ error: '保存游戏记录失败' });
}
});
// 获取排行榜
router.get('/leaderboard/:difficulty', async (req: Request, res: Response) => {
try {
const { difficulty } = req.params;
const { page = '1', limit = '10' } = req.query;
// 验证难度级别
const validDifficulties: MinesweeperDifficulty[] = ['beginner', 'intermediate', 'expert', 'brutal'];
if (!validDifficulties.includes(difficulty as MinesweeperDifficulty)) {
return res.status(400).json({ error: '无效的难度级别' });
}
const pageNum = parseInt(page as string);
const pageSize = parseInt(limit as string);
const skipCount = (pageNum - 1) * pageSize;
// 获取排行榜数据
const records = await MinesweeperRecord.getLeaderboard(
difficulty as MinesweeperDifficulty,
skipCount,
pageSize
);
// 获取总用户数(只统计获胜的用户)
const totalUsers = await MinesweeperRecord.distinct('userId', {
difficulty,
won: true
});
res.json({
records,
total: totalUsers.length,
currentPage: pageNum,
totalPages: Math.ceil(totalUsers.length / pageSize),
difficulty
});
} catch (error) {
console.error('获取排行榜失败:', error);
res.status(500).json({ error: '获取排行榜失败' });
}
});
// 获取用户的个人最佳成绩(需要登录)
router.get('/personal-best/:difficulty', auth, async (req: Request, res: Response) => {
try {
const { difficulty } = req.params;
if (!req.user?._id) {
return res.status(401).json({ error: '未登录' });
}
// 验证难度级别
const validDifficulties: MinesweeperDifficulty[] = ['beginner', 'intermediate', 'expert', 'brutal'];
if (!validDifficulties.includes(difficulty as MinesweeperDifficulty)) {
return res.status(400).json({ error: '无效的难度级别' });
}
// 查找用户的最佳成绩
const bestRecord = await MinesweeperRecord.findOne({
userId: req.user._id,
difficulty,
won: true
}).sort({ timeSeconds: 1 });
if (!bestRecord) {
return res.json({
hasBest: false,
message: '暂无最佳成绩'
});
}
res.json({
hasBest: true,
bestTime: bestRecord.timeSeconds,
createdAt: bestRecord.createdAt
});
} catch (error) {
console.error('获取个人最佳成绩失败:', error);
res.status(500).json({ error: '获取个人最佳成绩失败' });
}
});
// 获取用户的游戏统计(需要登录)
router.get('/stats', auth, async (req: Request, res: Response) => {
try {
if (!req.user?._id) {
return res.status(401).json({ error: '未登录' });
}
const stats = await MinesweeperRecord.aggregate([
{ $match: { userId: req.user._id } },
{
$group: {
_id: '$difficulty',
totalGames: { $sum: 1 },
wonGames: { $sum: { $cond: ['$won', 1, 0] } },
bestTime: { $min: { $cond: ['$won', '$timeSeconds', null] } }
}
}
]);
res.json({ stats });
} catch (error) {
console.error('获取游戏统计失败:', error);
res.status(500).json({ error: '获取游戏统计失败' });
}
});
export default router;

View File

@@ -0,0 +1,356 @@
import { Request, Response } from 'express';
import { OAuth2Client, OAuth2AuthorizationCode, OAuth2AccessToken } from '../models/oauth2';
import { User } from '../models/User';
import { generateRandomString } from '../utils/crypto';
import { Session } from 'express-session';
import fetch from 'node-fetch';
import jwt from 'jsonwebtoken';
import { config } from '../config';
// JWT Payload 类型定义
interface UserPayload {
_id: string;
username: string;
fullname: string;
email: string;
isAdmin: boolean;
}
// 添加 session 接口声明
interface CustomSession extends Session {
userId?: string;
isAuthenticated?: boolean;
}
// 扩展 Request 类型
interface CustomRequest extends Request {
session: CustomSession;
}
export class OAuth2Controller {
// 授权端点
async authorize(req: CustomRequest, res: Response) {
try {
// 调试日志:输出请求信息
console.log('--- OAuth2 authorize 调试信息 ---');
console.log('req.headers:', req.headers);
console.log('req.originalUrl:', req.originalUrl);
const { client_id, redirect_uri, scope, response_type, state } = req.query;
// 统一使用 JWT 认证,从多个来源获取 token
let token: string | undefined;
let currentUser: UserPayload | null = null;
// 1. 检查 Authorization 头
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
token = authHeader.substring(7);
}
// 2. 检查 cookie如果存在
if (!token && req.headers.cookie) {
const cookies: Record<string, string> = {};
req.headers.cookie.split(';').forEach(cookie => {
const parts = cookie.trim().split('=');
if (parts.length === 2) {
cookies[parts[0]] = parts[1];
}
});
// 尝试从 cookie 中获取 token
if (cookies.token) {
token = cookies.token;
}
}
// 3. 验证 token 并获取用户信息
if (token) {
try {
const decoded = jwt.verify(token, config.JWT_SECRET);
if (typeof decoded === 'object' && decoded !== null && '_id' in decoded) {
// 伪造session让后续逻辑认为已登录
req.session.userId = decoded._id as string;
req.session.isAuthenticated = true;
currentUser = decoded as UserPayload;
console.log('JWT验证成功用户信息:', {
userId: currentUser._id,
username: currentUser.username
});
}
} catch (error) {
console.log('JWT验证失败:', error);
}
}
// 如果用户未登录,重定向到登录页面
if (!currentUser) {
console.log('用户未登录,重定向到登录页');
const redirectPath = '/api' + req.originalUrl;
const loginUrl = `/login?redirect=${encodeURIComponent(redirectPath)}`;
return res.redirect(loginUrl);
}
// 用户已登录继续OAuth2授权流程
console.log('用户已登录继续OAuth2授权流程');
// 从数据库中获取完整用户信息
const user = await User.findById(currentUser._id);
if (!user) {
console.log('数据库中未找到用户:', currentUser._id);
return res.status(401).json({ error: 'user_not_found' });
}
// 验证客户端
const client = await OAuth2Client.findOne({ clientId: client_id });
if (!client) {
return res.status(400).json({ error: 'invalid_client' });
}
console.log('OAuth2 authorize start:', {
user: user.username,
clientId: client_id
});
// 查找现有的用户关联
const existingLink = await OAuth2Client.findOne({
clientId: client_id,
'linkedUsers.username': user.username
});
console.log('OAuth2 link check:', {
username: user.username,
exists: !!existingLink,
linkDetails: existingLink
});
// 只在没有现有关联时创建新的关联
if (!existingLink) {
console.log('Creating new OAuth link');
await OAuth2Client.updateOne(
{ clientId: client_id },
{
$addToSet: {
linkedUsers: {
userId: user._id,
username: user.username,
email: user.email
}
}
}
);
} else {
console.log('Using existing OAuth link - proceeding with authorization');
}
// 生成授权码
const code = generateRandomString(32);
const authCode = new OAuth2AuthorizationCode({
code,
clientId: client_id,
userId: user._id,
redirectUri: redirect_uri,
scope: (typeof scope === 'string' ? scope.split(' ') : []) || [],
expiresAt: new Date(Date.now() + 10 * 60 * 1000)
});
await authCode.save();
console.log('Generated auth code:', { code, userId: user._id });
// 重定向回客户端
if (!redirect_uri || typeof redirect_uri !== 'string') {
return res.status(400).json({ error: 'invalid_redirect_uri' });
}
const redirectUrl = new URL(redirect_uri);
redirectUrl.searchParams.set('code', code);
redirectUrl.searchParams.set('state', state?.toString() || '');
console.log('Redirecting to:', redirectUrl.toString());
return res.redirect(redirectUrl.toString());
} catch (error: any) {
console.error('OAuth2 authorize error:', error);
throw error;
}
}
// 令牌端点
async token(req: Request, res: Response) {
try {
console.log('=== OAuth2 Token Debug Info ===');
console.log('Token request body:', req.body);
const { grant_type, code, client_id, client_secret, redirect_uri } = req.body;
// 验证客户端
const client = await OAuth2Client.findOne({
clientId: client_id
});
if (!client) {
console.log('Client not found:', client_id);
return res.status(401).json({ error: 'invalid_client' });
}
if (grant_type === 'authorization_code') {
// 查找授权码
const authCode = await OAuth2AuthorizationCode.findOne({ code });
console.log('Found auth code:', authCode);
if (!authCode || authCode.expiresAt < new Date()) {
console.log('Invalid or expired auth code');
return res.status(400).json({ error: 'invalid_grant' });
}
// 查找用户
const user = await User.findById(authCode.userId);
// 生成访问令牌
const accessToken = generateRandomString(32);
const token = new OAuth2AccessToken({
accessToken,
clientId: client_id,
userId: authCode.userId,
scope: authCode.scope,
expiresAt: new Date(Date.now() + 60 * 60 * 1000)
});
await token.save();
console.log('Generated access token:', accessToken);
// 生成 id_token只要 scope 里有 openid
let id_token;
if (authCode.scope.includes('openid')) {
const jwt = require('jsonwebtoken');
id_token = jwt.sign(
{
sub: user._id.toString(),
name: user.username,
fullname: user.fullname,
email: user.email,
// 可根据需要添加其它 claim
},
process.env.OPENID_SESSION_SECRET || 'your-secret-keyq', // 用于签名的密钥
{
algorithm: 'HS256',
expiresIn: '1h',
issuer: process.env.OPENID_ISSUER || 'https://d1kt.cn',
audience: client_id,
}
);
}
// 返回令牌响应
return res.json({
access_token: accessToken,
token_type: 'Bearer',
expires_in: 3600,
scope: authCode.scope.join(' '),
...(id_token ? { id_token } : {}),
});
}
return res.status(400).json({ error: 'unsupported_grant_type' });
} catch (error: any) {
console.error('OAuth2 Token Error:', {
message: error.message,
stack: error.stack,
body: req.body
});
return res.status(500).json({
error: 'server_error',
error_description: error.message
});
}
}
// 用户信息端点
async userinfo(req: Request, res: Response) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'invalid_token' });
}
const accessToken = authHeader.substring(7);
try {
const token = await OAuth2AccessToken.findOne({
accessToken,
expiresAt: { $gt: new Date() }
});
if (!token) {
return res.status(401).json({ error: 'invalid_token' });
}
// 查找已关联的用户
const linkedUser = await OAuth2Client.findOne({
clientId: token.clientId,
'linkedUsers.userId': token.userId
});
const user = await User.findById(token.userId);
if (!user) {
return res.status(401).json({ error: 'user_not_found' });
}
// 根据scope返回用户信息
const userInfo: any = {};
if (token.scope.includes('openid')) {
userInfo.sub = user._id;
}
if (token.scope.includes('profile')) {
userInfo.name = user.username;
userInfo.fullname = user.fullname;
}
if (token.scope.includes('email')) {
userInfo.email = user.email;
}
if (token.scope.includes('firstname')) {
userInfo.firstname = user.username;
}
if (token.scope.includes('lastname')) {
userInfo.lastname = user.fullname;
}
if (token.scope.includes('username')) {
userInfo.username = user.username;
}
console.log('user', user);
console.log('userInfo', userInfo);
console.log('Token scopes:', token.scope);
console.log('最终返回的 userInfo:', userInfo);
res.json(userInfo);
} catch (error) {
res.status(500).json({ error: 'server_error6' });
}
}
async getMoodleSesskey(req: Request, res: Response) {
console.log('Received request for moodle-sesskey'); // 请求开始日志
try {
console.log('Fetching Moodle login page...');
const response = await fetch('https://m.d1kt.cn/login/index.php');
const html = await response.text();
console.log('Moodle response received, length:', html.length); // 检查是否获取到响应
const sesskeyMatch = html.match(/sesskey":"([^"]+)/);
console.log('Sesskey match result:', sesskeyMatch); // 检查正则匹配结果
const sesskey = sesskeyMatch ? sesskeyMatch[1] : '';
if (!sesskey) {
console.log('No sesskey found in response'); // 未找到 sesskey
return res.status(500).json({ error: 'Failed to get sesskey' });
}
console.log('Successfully got sesskey:', sesskey); // 成功获取 sesskey
res.json({ sesskey });
} catch (error) {
console.error('Get sesskey error:', error); // 详细的错误信息
res.status(500).json({ error: 'Failed to get sesskey' });
}
}
}

View File

@@ -0,0 +1,16 @@
import { Router } from 'express';
import { OAuth2Controller } from './oauth2.controller';
const router = Router();
const oauth2Controller = new OAuth2Controller();
// OAuth2 路由定义
router.get('/authorize', oauth2Controller.authorize);
router.post('/token', oauth2Controller.token);
router.get('/userinfo', oauth2Controller.userinfo);
router.get('/moodle-sesskey', (req, res, next) => {
console.log('OAuth2 route: /moodle-sesskey accessed');
oauth2Controller.getMoodleSesskey(req, res);
});
export default router;

View File

@@ -0,0 +1,325 @@
// server/routes/practiceRecords.ts
import express, { Request, Response } from 'express';
import { PracticeRecord } from '../models/PracticeRecord';
import { auth } from '../middleware/auth';
import { Types, Error as MongooseError, startSession } from 'mongoose';
import { User, type IUser, type UserStats } from '../models/User';
import { validatePracticeSubmission } from '../middleware/practiceValidation';
const router = express.Router();
// 验证练习记录数据
interface PracticeStats {
totalWords: number;
correctWords: number;
accuracy: number;
wordsPerMinute: number;
startTime: Date;
endTime: Date;
duration: number;
}
interface PracticeRecordBody {
type: string;
stats: PracticeStats;
}
// 保存练习记录
router.post('/', auth, validatePracticeSubmission, async (req: Request, res: Response) => {
try {
console.log('Received practice record request:', {
body: req.body,
user: req.user
});
// 验证用户信息
if (!req.user ?._id || !req.user?.username) {
console.error('Missing user information in request');
return res.status(401).json({
error: '用户信息无效',
code: 'INVALID_USER'
});
}
// 验证请求体
const { type, stats } = req.body as PracticeRecordBody;
if (!type || !stats) {
console.error('Invalid request body:', { type, stats });
return res.status(400).json({
error: '练习记录数据不完整',
code: 'INVALID_DATA'
});
}
// 检查是否存在最近的重复提交
const recentRecord = await PracticeRecord.findOne({
userId: new Types.ObjectId(req.user._id),
'stats.startTime': stats.startTime,
});
if (recentRecord) {
console.log('Detected duplicate submission:', {
userId: req.user._id,
existingRecord: recentRecord._id
});
return res.status(409).json({
error: '检测到重复提交',
code: 'DUPLICATE_SUBMISSION',
existingRecordId: recentRecord._id
});
}
// 验证统计数据
if (
typeof stats.totalWords !== 'number' ||
typeof stats.correctWords !== 'number' ||
typeof stats.accuracy !== 'number' ||
typeof stats.wordsPerMinute !== 'number' ||
!stats.startTime ||
!stats.endTime ||
typeof stats.duration !== 'number'
) {
console.error('Invalid stats data:', stats);
return res.status(400).json({
error: '练习统计数据无效',
code: 'INVALID_STATS'
});
}
// 创建记录
const record = new PracticeRecord({
userId: new Types.ObjectId(req.user._id),
username: req.user.username,
fullname: req.user.fullname,
type,
stats: {
...stats,
startTime: new Date(stats.startTime),
endTime: new Date(stats.endTime),
},
inputEvents: req.body.inputEvents
});
// 保存练习记录 (移除 session)
await record.save();
// 查找用户并更新统计信息
const user = await User.findById(req.user._id);
if (!user) {
throw new Error('User not found');
}
// 更新用户统计信息
if (!user.stats) {
user.stats = {
totalPracticeCount: 0,
totalWords: 0,
totalAccuracy: 0,
totalSpeed: 0,
accuracyHistory: [],
todayPracticeTime: 0,
lastPracticeDate: new Date()
};
}
await user.updatePracticeStats({
words: stats.totalWords,
accuracy: stats.accuracy,
duration: stats.duration,
speed: stats.wordsPerMinute
});
console.log('Practice record and user stats updated successfully');
res.status(201).json(record);
} catch (error: unknown) {
console.error('保存练习记录失败:', {
error,
message: error instanceof Error ? error.message : '未知错误',
stack: error instanceof Error ? error.stack : undefined
});
if (error instanceof MongooseError.ValidationError) {
return res.status(400).json({
error: '数据验证失败',
details: error.message,
code: 'VALIDATION_ERROR'
});
}
res.status(500).json({
error: '保存练习记录失败',
code: 'SAVE_ERROR'
});
}
});
// 获取用户统计信息
router.get('/statistics', auth, async (req: Request, res: Response) => {
try {
if (!req.user?._id) {
return res.status(401).json({
error: '用户信息无效',
code: 'INVALID_USER'
});
}
const user = await User.findById(req.user._id);
if (!user) {
return res.status(404).json({
error: '用户不存在',
code: 'USER_NOT_FOUND'
});
}
res.json({
practiceCount: user.stats.totalPracticeCount || 0,
totalWords: user.stats.totalWords || 0,
avgAccuracy: user.stats.totalAccuracy || 0, // 直接使用存储的平均准确率
avgSpeed: user.stats.totalSpeed || 0,
todayPracticeTime: user.stats.todayPracticeTime || 0,
accuracyTrend: user.stats.accuracyHistory.slice(-10), // 取最近10次的准确率记录
lastPracticeDate: user.stats.lastPracticeDate || null
});
} catch (error) {
console.error('获取统计数据失败:', error);
res.status(500).json({
error: '获取统计数据失败',
code: 'FETCH_ERROR'
});
}
});
// 获取用户的练习记录
router.get('/my-records', auth, async (req: Request, res: Response) => {
try {
if (!req.user?._id) {
return res.status(401).json({
error: '用户信息无效',
code: 'INVALID_USER'
});
}
const records = await PracticeRecord.find({
userId: new Types.ObjectId(req.user._id)
})
.sort({ createdAt: -1 })
.select('-__v'); // 排除版本字段
console.log('Retrieved records count:', records.length);
res.json(records);
} catch (error) {
console.error('获取记录失败:', error);
res.status(500).json({
error: '获取记录失败',
code: 'FETCH_ERROR'
});
}
});
// 管理员获取所有练习记录
router.get('/all', auth, async (req: Request, res: Response) => {
try {
// 验证管理员权限
if (!req.user?.isAdmin) {
return res.status(403).json({
error: '需要管理员权限',
code: 'FORBIDDEN'
});
}
const { date, search } = req.query;
const query: any = {};
// 添加日期筛选
if (date) {
const startDate = new Date(date as string);
const endDate = new Date(startDate);
endDate.setDate(endDate.getDate() + 1);
query.createdAt = {
$gte: startDate,
$lt: endDate
};
}
// 添加搜索条件
if (search) {
const searchRegex = new RegExp(search as string, 'i');
query['$or'] = [
{ 'userInfo.fullname': searchRegex },
{ 'userInfo.username': searchRegex }
];
}
const records = await PracticeRecord.aggregate([
{
$lookup: {
from: 'users',
localField: 'userId',
foreignField: '_id',
as: 'userInfo'
}
},
{ $unwind: '$userInfo' },
{ $match: query }, // 移动 $match 到 $lookup 后面以支持用户信息搜索
{
$project: {
_id: 1,
type: 1,
stats: 1,
createdAt: 1,
fullname: '$userInfo.fullname',
username: '$userInfo.username'
}
},
{ $sort: { fullname: 1, createdAt: -1 } }
]);
res.json(records);
} catch (error) {
console.error('获取所有练习记录失败:', error);
res.status(500).json({
error: '获取记录失败',
code: 'FETCH_ERROR'
});
}
});
// 获取特定记录的详情
router.get('/:id', auth, async (req: Request, res: Response) => {
try {
if (!Types.ObjectId.isValid(req.params.id)) {
return res.status(400).json({
error: '无效的记录ID',
code: 'INVALID_ID'
});
}
const record = await PracticeRecord.findById(req.params.id)
.select('-__v');
if (!record) {
return res.status(404).json({
error: '记录不存在',
code: 'NOT_FOUND'
});
}
// 验证用户是否有权限访问该记录
if (record.userId.toString() !== req.user?._id) {
return res.status(403).json({
error: '无权访问此记录',
code: 'FORBIDDEN'
});
}
res.json(record);
} catch (error) {
console.error('获取记录失败:', error);
res.status(500).json({
error: '获取记录失败',
code: 'FETCH_ERROR'
});
}
});
export default router;

View File

@@ -0,0 +1,15 @@
import express from 'express';
import { PracticeType } from '../models/PracticeType';
const router = express.Router();
router.get('/', async (req, res) => {
try {
const practiceTypes = await PracticeType.find();
res.json(practiceTypes);
} catch (error) {
res.status(500).json({ message: 'Error fetching practice types' });
}
});
export default router;

89
server/routes/sudoku.ts Normal file
View File

@@ -0,0 +1,89 @@
import express, { Request, Response } from 'express';
import { SudokuRecord, SudokuDifficulty } from '../models/SudokuRecord';
import { auth } from '../middleware/auth';
const router = express.Router();
// 提交数独记录(需要登录)
router.post('/record', auth, async (req: Request, res: Response) => {
try {
const { difficulty, timeSeconds, won } = req.body;
if (!req.user?._id) {
return res.status(401).json({ error: '未登录' });
}
const validDifficulties: SudokuDifficulty[] = ['easy', 'medium', 'hard'];
if (!validDifficulties.includes(difficulty)) {
return res.status(400).json({ error: '无效的难度级别' });
}
if (typeof timeSeconds !== 'number' || timeSeconds < 0) {
return res.status(400).json({ error: '无效的时间' });
}
if (typeof won !== 'boolean') {
return res.status(400).json({ error: '无效的游戏结果' });
}
const record = new SudokuRecord({
userId: req.user._id,
username: req.user.username,
fullname: req.user.fullname || req.user.username,
difficulty,
timeSeconds,
won
});
await record.save();
res.status(201).json({
message: '游戏记录保存成功',
record
});
} catch (error) {
console.error('保存数独记录失败:', error);
res.status(500).json({ error: '保存数独记录失败' });
}
});
// 获取数独排行榜
router.get('/leaderboard/:difficulty', async (req: Request, res: Response) => {
try {
const { difficulty } = req.params;
const { page = '1', limit = '10' } = req.query;
const validDifficulties: SudokuDifficulty[] = ['easy', 'medium', 'hard'];
if (!validDifficulties.includes(difficulty as SudokuDifficulty)) {
return res.status(400).json({ error: '无效的难度级别' });
}
const pageNum = parseInt(page as string, 10);
const pageSize = parseInt(limit as string, 10);
const skipCount = (pageNum - 1) * pageSize;
const records = await SudokuRecord.getLeaderboard(
difficulty as SudokuDifficulty,
skipCount,
pageSize
);
const totalUsers = await SudokuRecord.distinct('userId', {
difficulty,
won: true
});
res.json({
records,
total: totalUsers.length,
currentPage: pageNum,
totalPages: Math.ceil(totalUsers.length / pageSize),
difficulty
});
} catch (error) {
console.error('获取数独排行榜失败:', error);
res.status(500).json({ error: '获取数独排行榜失败' });
}
});
export default router;

11
server/routes/system.ts Normal file
View File

@@ -0,0 +1,11 @@
// server/routes/system.ts
import express from 'express';
const router = express.Router();
router.get('/server-time', (req, res) => {
res.json({
serverTime: Date.now()
});
});
export default router;

View File

@@ -0,0 +1,227 @@
// server/routes/towerDefense.ts
import express, { Request, Response } from 'express';
import { TowerDefenseRecord } from '../models/TowerDefenseRecord';
import { TowerDefenseSave } from '../models/TowerDefenseSave';
import { auth } from '../middleware/auth';
const router = express.Router();
// 简单的内存去重/速率限制(仅用于示例,生产环境请使用 Redis 等持久/分布式存储)
const lastSubmissionByUser = new Map<string, { ts: number; score: number; wave: number }>();
const submissionWindow = new Map<string, { windowStart: number; count: number }>();
const RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute
const RATE_LIMIT_MAX = 20; // max submissions per user per window
// 提交塔防记录(需要登录)
router.post('/record', auth, async (req: Request, res: Response) => {
try {
const { wave, score, timeSeconds } = req.body;
if (!req.user?._id) {
return res.status(401).json({ error: '未登录' });
}
const uid = String(req.user._id);
// rate limit
try {
const now = Date.now();
const win = submissionWindow.get(uid) || { windowStart: now, count: 0 };
if (now - win.windowStart > RATE_LIMIT_WINDOW_MS) {
win.windowStart = now;
win.count = 0;
}
win.count++;
submissionWindow.set(uid, win);
if (win.count > RATE_LIMIT_MAX) {
return res.status(429).json({ error: '提交过于频繁,请稍后再试' });
}
} catch (e) {
console.warn('rate limit check error', e);
}
if (typeof wave !== 'number' || wave < 0) {
return res.status(400).json({ error: '无效的波次数据' });
}
if (typeof score !== 'number' || score < 0) {
return res.status(400).json({ error: '无效的分数数据' });
}
if (typeof timeSeconds !== 'number' || timeSeconds < 0) {
return res.status(400).json({ error: '无效的游戏时长' });
}
// dedupe:防止重复上报(比如 iframe 连续发送多次)
try {
const last = lastSubmissionByUser.get(uid);
const now = Date.now();
if (last && last.score === score && last.wave === wave && (now - last.ts) < 5000) {
// 视为重复提交
return res.status(200).json({ message: '重复提交,已忽略' });
}
// 保存最近提交摘要
lastSubmissionByUser.set(uid, { ts: now, score, wave });
} catch (e) {
console.warn('dedupe check error', e);
}
const record = new TowerDefenseRecord({
userId: req.user._id,
username: req.user.username,
fullname: req.user.fullname || req.user.username,
wave,
score,
timeSeconds
});
await record.save();
// 存储成功后可以清理或记录更多指标
// (保留 lastSubmission 已记录)
res.status(201).json({
message: '记录保存成功',
record
});
} catch (error) {
console.error('保存塔防记录失败:', error);
res.status(500).json({ error: '服务器内部错误' });
}
});
// 获取排行榜
router.get('/leaderboard', async (req: Request, res: Response) => {
try {
const { page = '1', limit = '10' } = req.query;
const pageNum = parseInt(page as string);
const pageSize = parseInt(limit as string);
const skipCount = (pageNum - 1) * pageSize;
const records = await TowerDefenseRecord.getLeaderboard(skipCount, pageSize);
const totalUsers = await TowerDefenseRecord.distinct('userId');
res.json({
records,
total: totalUsers.length,
currentPage: pageNum,
totalPages: Math.ceil(totalUsers.length / pageSize)
});
} catch (error) {
console.error('获取排行榜失败:', error);
res.status(500).json({ error: '获取排行榜失败' });
}
});
// 获取个人最佳
router.get('/personal-best', auth, async (req: Request, res: Response) => {
try {
if (!req.user?._id) {
return res.status(401).json({ error: '未登录' });
}
const bestRecord = await TowerDefenseRecord.findOne({
userId: req.user._id
}).sort({ score: -1 });
if (!bestRecord) {
return res.json({ hasBest: false });
}
res.json({
hasBest: true,
bestScore: bestRecord.score,
bestWave: bestRecord.wave,
createdAt: bestRecord.createdAt
});
} catch (error) {
console.error('获取个人最佳失败:', error);
res.status(500).json({ error: '获取个人最佳失败' });
}
});
// 保存当前游戏进度(需要登录)
router.post('/save', auth, async (req: Request, res: Response) => {
try {
if (!req.user?._id) {
return res.status(401).json({ error: '未登录' });
}
const { state } = req.body;
if (!state) {
return res.status(400).json({ error: '缺少保存的游戏状态' });
}
const name = String(Date.now());
const saveDoc = new TowerDefenseSave({
userId: req.user._id,
name,
state
});
await saveDoc.save();
res.status(201).json({ message: '已保存进度', save: saveDoc });
} catch (error) {
console.error('保存游戏进度失败:', error);
res.status(500).json({ error: '保存游戏进度失败' });
}
});
// 列出当前用户的保存记录
router.get('/saves', auth, async (req: Request, res: Response) => {
try {
if (!req.user?._id) {
return res.status(401).json({ error: '未登录' });
}
const saves = await TowerDefenseSave.find({ userId: req.user._id }).sort({ createdAt: -1 }).limit(50);
res.json({ saves });
} catch (error) {
console.error('获取保存列表失败:', error);
res.status(500).json({ error: '获取保存列表失败' });
}
});
// 获取指定的保存项
router.get('/save/:id', auth, async (req: Request, res: Response) => {
try {
if (!req.user?._id) {
return res.status(401).json({ error: '未登录' });
}
const { id } = req.params;
const save = await TowerDefenseSave.findById(id);
if (!save) return res.status(404).json({ error: '未找到保存项' });
if (String(save.userId) !== String(req.user._id)) return res.status(403).json({ error: '无权访问该保存项' });
res.json({ save });
} catch (error) {
console.error('获取保存项失败:', error);
res.status(500).json({ error: '获取保存项失败' });
}
});
// 删除指定保存项
router.delete('/save/:id', auth, async (req: Request, res: Response) => {
try {
if (!req.user?._id) {
return res.status(401).json({ error: '未登录' });
}
const { id } = req.params;
const save = await TowerDefenseSave.findById(id);
if (!save) return res.status(404).json({ error: '未找到保存项' });
if (String(save.userId) !== String(req.user._id)) return res.status(403).json({ error: '无权删除该保存项' });
await TowerDefenseSave.deleteOne({ _id: id });
res.json({ message: '已删除' });
} catch (error) {
console.error('删除保存项失败:', error);
res.status(500).json({ error: '删除保存项失败' });
}
});
export default router;

View File

@@ -0,0 +1,56 @@
import express from 'express';
import { User } from '../models/User';
import { WordRecord } from '../models/Vocabulary';
const router = express.Router();
/**
* 查询指定用户名前缀的用户及其通过单词数
* GET /api/user-word-pass?prefix=2023101
*/
router.get('/user-word-pass', async (req, res) => {
try {
const { prefix } = req.query;
if (!prefix || typeof prefix !== 'string') {
return res.status(400).json({ message: '缺少前缀参数' });
}
// 1. 查找所有以 prefix 开头的用户
const users = await User.find({ username: { $regex: `^${prefix}` } })
.select('_id username fullname')
.lean();
if (users.length === 0) {
return res.json([]);
}
// 2. 查找这些用户的通过单词数
const userIds = users.map(u => u._id);
// 聚合统计每个用户 isFullyMastered 为 true 的数量
const wordPassStats = await WordRecord.aggregate([
{ $match: { user: { $in: userIds }, isFullyMastered: true } },
{ $group: { _id: '$user', passCount: { $sum: 1 } } }
]);
// 组装统计结果
const passMap = new Map<string, number>();
wordPassStats.forEach(item => {
passMap.set(item._id.toString(), item.passCount);
});
// 3. 合并并排序
const result = users.map(u => ({
username: u.username,
fullname: u.fullname,
passCount: passMap.get(u._id.toString()) || 0
})).sort((a, b) => b.passCount - a.passCount);
res.json(result);
} catch (error) {
console.error('查询用户通过单词数失败:', error);
res.status(500).json({ message: '服务器错误' });
}
});
export default router;

24
server/routes/visitor.ts Normal file
View File

@@ -0,0 +1,24 @@
import express from 'express';
const router = express.Router();
// 获取访问者IP地址
router.get('/ip', (req, res) => {
try {
// 获取X-Forwarded-For头信息如果通过代理
const forwardedIp = req.headers['x-forwarded-for'];
// 如果存在X-Forwarded-For则使用第一个IP最接近用户的代理
// 否则使用直接连接的IP
const ip = forwardedIp
? (typeof forwardedIp === 'string' ? forwardedIp.split(',')[0].trim() : forwardedIp[0])
: req.ip || req.connection.remoteAddress;
res.json({ ip });
} catch (error) {
console.error('获取IP地址错误:', error);
res.status(500).json({ message: '获取IP地址失败' });
}
});
export default router;

699
server/routes/vocabulary.ts Normal file
View File

@@ -0,0 +1,699 @@
// server/routes/vocabulary.ts
import express from 'express';
import multer from 'multer';
import fs from 'fs';
import path from 'path';
import { auth as authMiddleware } from '../middleware/auth';
import { Word, WordSet, WordRecord, VocabularyTestRecord } from '../models/Vocabulary';
import mongoose from 'mongoose';
import csv from 'csv-parser';
import { User } from '../models/User';
const router = express.Router();
// 配置 multer 用于文件上传
const storage = multer.diskStorage({
destination: function (req, file, cb) {
const uploadDir = path.join(__dirname, '../uploads');
// 确保上传目录存在
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: function (req, file, cb) {
cb(null, `${Date.now()}-${file.originalname}`);
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: 5 * 1024 * 1024 // 限制5MB
},
fileFilter: function (req, file, cb) {
// 只允许上传CSV文件
if (file.mimetype === 'text/csv' || file.originalname.endsWith('.csv')) {
cb(null, true);
} else {
cb(new Error('只支持CSV文件'));
}
}
});
// 获取所有单词集
router.get('/word-sets', authMiddleware, async (req, res) => {
try {
const wordSets = await WordSet.find()
.select('name description totalWords createdAt')
.sort({ createdAt: -1 });
res.json(wordSets);
} catch (error) {
console.error('获取单词集失败:', error);
res.status(500).json({ message: '获取单词集失败' });
}
});
// 创建新的单词集
router.post('/word-sets', authMiddleware, async (req, res) => {
try {
const { name, description } = req.body;
// 检查是否已存在同名单词集
const existingSet = await WordSet.findOne({
name: name
});
if (existingSet) {
return res.status(400).json({ message: '已存在同名单词集' });
}
const newWordSet = await WordSet.create({
name,
description,
totalWords: 0
});
res.status(201).json(newWordSet);
} catch (error) {
console.error('创建单词集失败:', error);
res.status(500).json({ message: '创建单词集失败' });
}
});
// 上传单词文件并创建单词集
router.post('/upload', authMiddleware, upload.single('file'), async (req, res) => {
if (!req.file) {
return res.status(400).json({ message: '请上传文件' });
}
try {
const { name } = req.body;
const fileName = req.file.filename;
const filePath = req.file.path;
// 检查是否已存在同名单词集
const existingSet = await WordSet.findOne({
name: name || path.basename(fileName, '.csv')
});
if (existingSet) {
// 删除上传的文件
fs.unlinkSync(filePath);
return res.status(400).json({ message: '已存在同名单词集' });
}
// 创建单词集
const wordSet = await WordSet.create({
name: name || path.basename(fileName, '.csv'),
totalWords: 0
});
const results: any[] = [];
let wordCount = 0;
// 处理CSV文件
fs.createReadStream(filePath)
.pipe(csv())
.on('data', (data) => {
// 检查必要的字段是否存在
if (data.word && data.translation) {
results.push({
word: data.word.trim(),
translation: data.translation.trim(),
pronunciation: data.pronunciation?.trim(),
example: data.example?.trim(),
wordSet: wordSet._id
});
wordCount++;
}
})
.on('end', async () => {
try {
// 批量插入单词
if (results.length > 0) {
await Word.insertMany(results);
// 更新单词集的单词数量
await WordSet.findByIdAndUpdate(wordSet._id, { totalWords: wordCount });
}
// 删除上传的文件
fs.unlinkSync(filePath);
res.status(201).json({
message: `成功创建单词集并导入${wordCount}个单词`,
wordSet
});
} catch (error) {
// 如果插入单词失败,删除创建的单词集
await WordSet.findByIdAndDelete(wordSet._id);
console.error('导入单词失败:', error);
res.status(500).json({ message: '导入单词失败' });
}
})
.on('error', (error) => {
console.error('处理CSV文件失败:', error);
res.status(500).json({ message: '处理CSV文件失败' });
});
} catch (error) {
console.error('上传文件失败:', error);
res.status(500).json({ message: '上传文件失败' });
}
});
// 获取学习单词(智能抽取算法,嵌套结构版)
router.get('/study-words/:wordSetId', authMiddleware, async (req, res) => {
try {
const { wordSetId } = req.params;
let targetCount = 100;
if (req.query.count) {
const parsed = parseInt(req.query.count as string, 10);
if (!isNaN(parsed) && parsed > 0) {
targetCount = Math.min(parsed, 100);
}
}
// 检查单词集是否存在
const wordSet = await WordSet.findOne({ _id: wordSetId });
if (!wordSet) {
return res.status(404).json({ message: '未找到单词集' });
}
// 1. 获取该单词集下所有单词
const allWords = await Word.find({ wordSet: wordSetId });
const allWordIds = allWords.map(w => w._id.toString());
// 2. 获取该用户所有WordRecord嵌套结构
const allRecords = await WordRecord.find({ user: req.user._id, word: { $in: allWordIds } });
// 用 wordId 做 key
const recordMap = new Map();
allRecords.forEach(r => {
recordMap.set(r.word.toString(), r);
});
// 3. 分类
const wrongBookWords: any[] = [];
const notMasteredWords: any[] = [];
const masteredWords: { word: any, lastMasteredAt: Date }[] = [];
const neverLearnedWords: any[] = [];
for (const word of allWords) {
const wordId = word._id.toString();
const record = recordMap.get(wordId);
// 没有任何记录
if (!record) {
neverLearnedWords.push(word);
continue;
}
// 判断是否在错词本
const modes = ['chineseToEnglish', 'audioToEnglish', 'multipleChoice'];
const anyInWrongBook = modes.some(m => record[m]?.inWrongBook);
if (anyInWrongBook) {
wrongBookWords.push(word);
continue;
}
// 判断是否全部掌握
if (record.isFullyMastered) {
masteredWords.push({ word, lastMasteredAt: record.lastFullyMasteredAt });
continue;
}
// 未完全掌握但有学习记录
if (modes.some(m => record[m] && Object.keys(record[m]).length > 0)) {
notMasteredWords.push(word);
continue;
}
}
// 过滤一周内掌握的单词
const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
let eligibleMasteredWords = masteredWords.filter(item =>
item.lastMasteredAt && item.lastMasteredAt < oneWeekAgo
);
// 4. 按比例抽取
let wrongBookQuota = Math.round(targetCount * 0.4);
let masteredQuota = Math.round(targetCount * 0.1);
let neverLearnedQuota = targetCount - wrongBookQuota - masteredQuota; // 剩下的给未学和新词
// 错词本优先
let selectedWrongBook = wrongBookWords.slice(0, wrongBookQuota);
let selectedMastered = eligibleMasteredWords
.sort((a, b) => (a.lastMasteredAt?.getTime() || 0) - (b.lastMasteredAt?.getTime() || 0))
.slice(0, masteredQuota)
.map(item => item.word);
function shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
let selectedNeverLearned = shuffle([...neverLearnedWords]).slice(0, neverLearnedQuota);
// 补足不足部分
let remain = targetCount - (selectedWrongBook.length + selectedMastered.length + selectedNeverLearned.length);
// 先补未掌握(不在错词本的)
let notMasteredPool = notMasteredWords.filter(w => !selectedWrongBook.includes(w));
let selectedNotMastered: any[] = [];
if (remain > 0 && notMasteredPool.length > 0) {
selectedNotMastered = notMasteredPool.slice(0, remain);
remain -= selectedNotMastered.length;
}
// 再补新词
if (remain > 0) {
const moreNew = neverLearnedWords.slice(neverLearnedQuota, neverLearnedQuota + remain);
selectedNeverLearned = selectedNeverLearned.concat(moreNew);
remain -= moreNew.length;
}
// 再补已掌握
if (remain > 0) {
const moreMastered = eligibleMasteredWords
.sort((a, b) => (a.lastMasteredAt?.getTime() || 0) - (b.lastMasteredAt?.getTime() || 0))
.slice(masteredQuota, masteredQuota + remain)
.map(item => item.word);
selectedMastered = selectedMastered.concat(moreMastered);
remain -= moreMastered.length;
}
// 再补错词本
if (remain > 0) {
const moreWrong = wrongBookWords.slice(wrongBookQuota, wrongBookQuota + remain);
selectedWrongBook = selectedWrongBook.concat(moreWrong);
remain -= moreWrong.length;
}
// 合并所有选中的单词
let finalWords = [
...selectedWrongBook,
...selectedNotMastered,
...selectedMastered,
...selectedNeverLearned
];
// 去重
const seen = new Set();
finalWords = finalWords.filter(w => {
const id = w._id.toString();
if (seen.has(id)) return false;
seen.add(id);
return true;
});
// 最终数量限制
finalWords = finalWords.slice(0, targetCount);
res.json(finalWords);
} catch (error) {
console.error('获取学习单词失败:', error);
res.status(500).json({ message: '获取学习单词失败' });
}
});
// 记录单词学习结果嵌套结构版upsert防止重复
router.post('/word-record', (req, res, next) => {
console.log('收到 /word-record 请求', req.method, req.body);
next();
}, authMiddleware, async (req, res) => {
try {
const { wordId, isCorrect, testType } = req.body;
const userId = req.user._id;
// 检查单词是否存在
const word = await Word.findById(wordId);
if (!word) {
return res.status(404).json({ message: '未找到单词' });
}
// 嵌套字段名映射
const modeMap = {
'chinese-to-english': 'chineseToEnglish',
'audio-to-english': 'audioToEnglish',
'multiple-choice': 'multipleChoice'
};
const modeKey = modeMap[testType];
if (!modeKey) {
return res.status(400).json({ message: '未知的测试类型' });
}
// 先查当前记录
let record = await WordRecord.findOne({ user: userId, word: wordId });
let modeObj = record ? (record[modeKey] || {}) : {};
// 更新 streak、totalCorrect、totalWrong
if (isCorrect) {
modeObj.streak = (modeObj.streak || 0) + 1;
modeObj.totalCorrect = (modeObj.totalCorrect || 0) + 1;
} else {
modeObj.streak = 0;
modeObj.totalWrong = (modeObj.totalWrong || 0) + 1;
}
modeObj.lastTestedAt = new Date();
// 判定是否掌握
if (modeObj.streak >= 1) {
modeObj.mastered = true;
modeObj.lastMasteredAt = new Date();
modeObj.inWrongBook = false; // 掌握后自动移出错词本
}
// 判定是否进入错词本
if (!modeObj.mastered && modeObj.totalWrong >= 5) {
modeObj.inWrongBook = true;
}
// 构造更新对象
const updateObj = {};
updateObj[modeKey] = modeObj;
record = await WordRecord.findOneAndUpdate(
{ user: userId, word: wordId },
{ $set: updateObj, $setOnInsert: { user: userId, word: wordId, createdAt: new Date() } },
{ upsert: true, new: true }
);
// 检查三种模式是否都已掌握
const allModes = ['chineseToEnglish', 'audioToEnglish', 'multipleChoice'];
const isFullyMastered = allModes.every(m => record[m]?.mastered);
let lastMasteredAt: Date | null = null;
if (isFullyMastered) {
// 取三种模式中最近一次掌握的时间
lastMasteredAt = allModes.reduce((latest: Date | null, m) => {
const t = record[m]?.lastMasteredAt;
if (!latest && t) return t;
if (t instanceof Date && latest instanceof Date && t > latest) {
return t;
}
return latest;
}, null);
// 取三种模式中最近一次测试的时间,如果比掌握时间更新则使用测试时间
const lastTestedAt = allModes.reduce((latest: Date | null, m) => {
const t = record[m]?.lastTestedAt;
if (!latest && t) return t;
if (t instanceof Date && latest instanceof Date && t > latest) {
return t;
}
return latest;
}, null);
// 使用最新的日期(掌握时间或测试时间)
if (lastTestedAt && (!lastMasteredAt || lastTestedAt > lastMasteredAt)) {
lastMasteredAt = lastTestedAt;
}
// 更新isFullyMastered和lastFullyMasteredAt字段
if (!record.isFullyMastered || !record.lastFullyMasteredAt ||
(lastMasteredAt && record.lastFullyMasteredAt < lastMasteredAt)) {
record = await WordRecord.findOneAndUpdate(
{ _id: record._id },
{
$set: {
isFullyMastered: true,
lastFullyMasteredAt: lastMasteredAt
}
},
{ new: true }
);
}
} else if (record.isFullyMastered) {
// 如果之前标记为完全掌握,但现在不是,更新状态
record = await WordRecord.findOneAndUpdate(
{ _id: record._id },
{ $set: { isFullyMastered: false } },
{ new: true }
);
}
// 返回当前单词的所有模式掌握状态和是否在错词本
const masteryStatus = {};
allModes.forEach(m => {
masteryStatus[m] = {
mastered: record[m]?.mastered || false,
streak: record[m]?.streak || 0,
totalCorrect: record[m]?.totalCorrect || 0,
totalWrong: record[m]?.totalWrong || 0,
inWrongBook: record[m]?.inWrongBook || false,
lastMasteredAt: record[m]?.lastMasteredAt || null
};
});
res.status(201).json({
message: '记录已保存',
masteryStatus,
isFullyMastered: record.isFullyMastered,
lastMasteredAt: record.lastFullyMasteredAt,
inWrongBook: allModes.some(m => record[m]?.inWrongBook)
});
} catch (error) {
console.error('保存单词学习记录失败:', error);
res.status(500).json({ message: '保存单词学习记录失败' });
}
});
// 保存测试记录
router.post('/test-record', authMiddleware, async (req, res) => {
try {
const { wordSetId, testType, stats } = req.body;
// 检查单词集是否存在并属于当前用户
const wordSet = await WordSet.findOne({
_id: wordSetId
});
if (!wordSet) {
return res.status(404).json({ message: '未找到单词集' });
}
// 创建测试记录
await VocabularyTestRecord.create({
user: req.user._id,
wordSet: wordSetId,
testType,
stats: {
totalWords: stats.totalWords,
correctWords: stats.correctWords,
accuracy: stats.accuracy,
startTime: new Date(stats.startTime),
endTime: new Date(stats.endTime),
duration: stats.duration
}
});
res.status(201).json({ message: '测试记录已保存' });
} catch (error) {
console.error('保存测试记录失败:', error);
res.status(500).json({ message: '保存测试记录失败' });
}
});
// 获取单词详情
router.get('/word/:wordId', authMiddleware, async (req, res) => {
try {
const { wordId } = req.params;
const word = await Word.findById(wordId);
if (!word) {
return res.status(404).json({ message: '未找到单词' });
}
// 检查用户是否有权限访问这个单词
const wordSet = await WordSet.findOne({
_id: word.wordSet
});
if (!wordSet) {
return res.status(403).json({ message: '没有权限访问该单词' });
}
res.json(word);
} catch (error) {
console.error('获取单词详情失败:', error);
res.status(500).json({ message: '获取单词详情失败' });
}
});
// 获取单词测试记录
router.get('/test-records', authMiddleware, async (req, res) => {
try {
const testRecords = await VocabularyTestRecord.find({
user: req.user._id
}).populate('wordSet', 'name').sort({ createdAt: -1 });
res.json(testRecords);
} catch (error) {
console.error('获取测试记录失败:', error);
res.status(500).json({ message: '获取测试记录失败' });
}
});
// 排行榜接口:按掌握单词数和正确率排序
router.get('/leaderboard', authMiddleware, async (req, res) => {
try {
// 1. 查出所有 WordRecord
const allRecords = await WordRecord.find({});
// 2. 直接使用isFullyMastered统计每个用户掌握的单词数
const userMasteredCount = {};
const userStats = {};
for (const rec of allRecords) {
const userId = rec.user.toString();
// 初始化用户统计数据
if (!userStats[userId]) {
userStats[userId] = { correct: 0, total: 0 };
}
// 使用isFullyMastered直接统计掌握单词数
if (rec.isFullyMastered) {
userMasteredCount[userId] = (userMasteredCount[userId] || 0) + 1;
}
// 统计正确率
['chineseToEnglish', 'audioToEnglish', 'multipleChoice'].forEach(mode => {
const m = rec[mode];
if (m) {
userStats[userId].correct += m.totalCorrect || 0;
userStats[userId].total += (m.totalCorrect || 0) + (m.totalWrong || 0);
}
});
}
// 3. 查询用户名
const userIds = Object.keys(userMasteredCount);
const users = await mongoose.model('User').find({ _id: { $in: userIds } }, { fullname: 1 });
const userMap = {};
users.forEach(u => { userMap[u._id.toString()] = u.fullname; });
// 4. 组装排行榜数组
const leaderboard = userIds.map(userId => ({
userId,
fullname: userMap[userId] || '未知用户',
totalWordsLearned: userMasteredCount[userId],
accuracy: userStats[userId] && userStats[userId].total > 0
? Math.round((userStats[userId].correct / userStats[userId].total) * 100)
: 0,
rank: 0 // 添加rank属性初始值为0
}));
// 5. 排序
leaderboard.sort((a, b) => {
if (b.totalWordsLearned !== a.totalWordsLearned) {
return b.totalWordsLearned - a.totalWordsLearned;
}
return b.accuracy - a.accuracy;
});
// 6. 添加排名
leaderboard.forEach((item, idx) => {
item.rank = idx + 1;
});
res.json(leaderboard);
} catch (error) {
console.error('获取排行榜失败:', error);
res.status(500).json({ message: '获取排行榜失败' });
}
});
// 获取单词集详细信息
router.get('/word-set/:id', authMiddleware, async (req, res) => {
try {
const wordSet = await WordSet.findById(req.params.id);
if (!wordSet) {
return res.status(404).json({ message: '未找到单词集' });
}
res.json(wordSet);
} catch (error) {
console.error('获取单词集详细信息失败:', error);
res.status(500).json({ message: '获取单词集详细信息失败' });
}
});
// 更新单词集
router.put('/word-set/:id', authMiddleware, async (req, res) => {
try {
const { name, description } = req.body;
const wordSet = await WordSet.findByIdAndUpdate(
req.params.id,
{ name, description },
{ new: true }
);
if (!wordSet) {
return res.status(404).json({ message: '未找到单词集' });
}
res.json(wordSet);
} catch (error) {
console.error('更新单词集失败:', error);
res.status(500).json({ message: '更新单词集失败' });
}
});
// 获取单词集中的单词
router.get('/word-set/:id/words', authMiddleware, async (req, res) => {
try {
const wordSetId = req.params.id;
console.log('Received request for wordSetId:', wordSetId); // 打印请求的 wordSetId
// 检查数据库中是否存在该单词集
const wordSetExists = await WordSet.exists({ _id: wordSetId });
console.log('WordSet exists:', wordSetExists); // 打印单词集是否存在
if (!wordSetExists) {
console.log('WordSet not found for wordSetId:', wordSetId);
return res.status(404).json({ message: '请求的资源不存在' });
}
// 查询单词集中的单词
const words = await Word.find({ wordSet: wordSetId });
console.log('Words found:', words.length); // 打印找到的单词数量
if (!words || words.length === 0) {
console.log('No words found for wordSetId:', wordSetId); // 打印未找到的情况
return res.status(404).json({ message: '请求的资源不存在' });
}
res.json(words);
} catch (error) {
console.error('获取单词失败:', error);
res.status(500).json({ message: '获取单词失败' });
}
});
// 更新单词
router.put('/words', authMiddleware, async (req, res) => {
try {
const { words } = req.body;
console.log('Received words to update:', JSON.stringify(words, null, 2)); // 更详细的日志
for (const word of words) {
const { _id, word: wordText, translation, pronunciation, example } = word;
console.log(`Updating word ${_id}:`, { wordText, translation, pronunciation, example }); // 每个单词的更新日志
const updatedWord = await Word.findByIdAndUpdate(
_id,
{
word: wordText,
translation,
pronunciation,
example
},
{ new: true } // 返回更新后的文档
);
console.log('Updated word result:', updatedWord); // 更新结果日志
}
res.json({ message: '单词更新成功' });
} catch (error) {
console.error('更新单词失败:', error);
res.status(500).json({ message: '更新单词失败' });
}
});
export default router;