Migrate project from typing_practiceweb
This commit is contained in:
325
server/routes/practiceRecords.ts
Normal file
325
server/routes/practiceRecords.ts
Normal 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;
|
||||
Reference in New Issue
Block a user