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,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;