continue fixing

This commit is contained in:
2026-06-13 10:35:06 +08:00
parent 76c2f17229
commit b19c811048
8 changed files with 240 additions and 43 deletions

View File

@@ -11,6 +11,7 @@ import path from 'path';
import csv from 'csv-parser';
import { Word, WordRecord, WordSet, VocabularyTestAttempt, VocabularyTestRecord } from '../models/Vocabulary';
import mongoose from 'mongoose';
import { INVALIDATING_VOCABULARY_RISK_FLAGS } from '../utils/vocabularyRisk';
const router = express.Router();
const adminController = new AdminController();
@@ -60,18 +61,55 @@ const WORD_RECORD_MODES: WordRecordModeKey[] = [
'multipleChoice'
];
const INVALIDATING_VOCABULARY_RISK_FLAGS = [
'too_fast',
'too_slow',
'missing_input_value_trace',
'input_value_mismatch',
'uniform_answer_intervals',
'repeated_whole_second_intervals'
];
const getObjectIdString = (value: any): string => value?._id?.toString?.() || value?.toString?.() || '';
const escapeRegex = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const parseOptionalPositiveInt = (value: unknown, fieldName: string) => {
const raw = String(value ?? '').trim();
if (!raw) return { value: undefined as number | undefined };
if (!/^\d+$/.test(raw)) {
return { error: `${fieldName}必须是正整数` };
}
const parsed = Number(raw);
if (!Number.isFinite(parsed) || parsed <= 0) {
return { error: `${fieldName}必须大于0` };
}
return { value: parsed };
};
const buildVocabularySummaryFilters = (query: Record<string, any>) => {
const studentNo = String(query.studentNo || '').trim();
const wordCountResult = parseOptionalPositiveInt(query.minWords, '单次练习单词数量');
if ('error' in wordCountResult) return { error: wordCountResult.error };
const minWords = wordCountResult.value;
const userMatchStages = studentNo
? [{
$match: {
'user.username': {
$regex: escapeRegex(studentNo),
$options: 'i'
}
}
}]
: [];
const baseMatch: Record<string, any> = {};
if (typeof minWords === 'number') {
baseMatch['stats.totalWords'] = { $gte: minWords };
}
return {
studentNo,
minWords,
userMatchStages,
baseMatch
};
};
const parseAdminDateRange = (source: Record<string, any>) => {
const start = new Date(String(source.start || source.startTime || ''));
const end = new Date(String(source.end || source.endTime || ''));
@@ -545,20 +583,16 @@ router.get('/vocabulary/test-pass-summary', adminAuth, async (req, res) => {
}
const { start, end } = range as { start: Date; end: Date };
const studentNo = String(req.query.studentNo || '').trim();
const userMatchStages = studentNo
? [{
$match: {
'user.username': {
$regex: escapeRegex(studentNo),
$options: 'i'
}
}
}]
: [];
const summaryFilters = buildVocabularySummaryFilters(req.query as Record<string, any>);
if ('error' in summaryFilters) {
return res.status(400).json({ message: summaryFilters.error });
}
const { userMatchStages, baseMatch } = summaryFilters;
const validPassItems = await VocabularyTestRecord.aggregate([
{
$match: {
...baseMatch,
invalidated: { $ne: true },
'stats.endTime': { $gte: start, $lte: end },
'stats.correctWords': { $gt: 0 }
@@ -610,6 +644,7 @@ router.get('/vocabulary/test-pass-summary', adminAuth, async (req, res) => {
const recordItems = await VocabularyTestRecord.aggregate([
{
$match: {
...baseMatch,
'stats.endTime': { $gte: start, $lte: end }
}
},
@@ -737,6 +772,12 @@ router.get('/vocabulary/test-pass-summary/:userId/details', adminAuth, async (re
}
const { start, end } = range as { start: Date; end: Date };
const summaryFilters = buildVocabularySummaryFilters(req.query as Record<string, any>);
if ('error' in summaryFilters) {
return res.status(400).json({ message: summaryFilters.error });
}
const { baseMatch } = summaryFilters;
const { userId } = req.params;
if (!mongoose.isValidObjectId(userId)) {
return res.status(400).json({ message: '学生ID无效' });
@@ -749,6 +790,7 @@ router.get('/vocabulary/test-pass-summary/:userId/details', adminAuth, async (re
const records = await VocabularyTestRecord.find({
user: user._id,
...baseMatch,
'stats.endTime': { $gte: start, $lte: end }
})
.populate('wordSet', 'name description')
@@ -971,6 +1013,12 @@ router.post('/vocabulary/test-pass-summary/clear', adminAuth, async (req, res) =
}
const { start, end } = range as { start: Date; end: Date };
const summaryFilters = buildVocabularySummaryFilters(req.body || {});
if ('error' in summaryFilters) {
return res.status(400).json({ message: summaryFilters.error });
}
const { baseMatch } = summaryFilters;
const { userId } = req.body || {};
if (!mongoose.isValidObjectId(userId)) {
return res.status(400).json({ message: '学生ID无效' });
@@ -984,6 +1032,7 @@ router.post('/vocabulary/test-pass-summary/clear', adminAuth, async (req, res) =
const records = await VocabularyTestRecord.find({
user: user._id,
invalidated: { $ne: true },
...baseMatch,
'stats.endTime': { $gte: start, $lte: end },
'stats.correctWords': { $gt: 0 }
})