From f9311292cce9d73cf20c91f6be0d184dcab1e162 Mon Sep 17 00:00:00 2001 From: Shuming Liu Date: Sat, 13 Jun 2026 10:47:59 +0800 Subject: [PATCH] more.. --- server/routes/admin.ts | 48 +------------------- server/utils/vocabularyAuditFilters.test.ts | 42 +++++++++++++++++ server/utils/vocabularyAuditFilters.ts | 47 +++++++++++++++++++ src/api/admin.ts | 4 +- src/components/AdminVocabularyScoreAudit.tsx | 10 ++-- 5 files changed, 97 insertions(+), 54 deletions(-) create mode 100644 server/utils/vocabularyAuditFilters.test.ts create mode 100644 server/utils/vocabularyAuditFilters.ts diff --git a/server/routes/admin.ts b/server/routes/admin.ts index 5f476f3..04da042 100644 --- a/server/routes/admin.ts +++ b/server/routes/admin.ts @@ -12,6 +12,7 @@ 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'; +import { buildVocabularySummaryFilters } from '../utils/vocabularyAuditFilters'; const router = express.Router(); const adminController = new AdminController(); @@ -62,53 +63,6 @@ const WORD_RECORD_MODES: WordRecordModeKey[] = [ ]; 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) => { - 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 = {}; - if (typeof minWords === 'number') { - baseMatch['stats.totalWords'] = { $gte: minWords }; - } - - return { - studentNo, - minWords, - userMatchStages, - baseMatch - }; -}; const parseAdminDateRange = (source: Record) => { const start = new Date(String(source.start || source.startTime || '')); diff --git a/server/utils/vocabularyAuditFilters.test.ts b/server/utils/vocabularyAuditFilters.test.ts new file mode 100644 index 0000000..c61702b --- /dev/null +++ b/server/utils/vocabularyAuditFilters.test.ts @@ -0,0 +1,42 @@ +import assert from 'node:assert/strict'; +import { buildVocabularySummaryFilters } from './vocabularyAuditFilters'; + +const correctWordFilter = buildVocabularySummaryFilters({ minCorrectWords: '40' }); +assert.deepEqual(correctWordFilter, { + minCorrectWords: 40, + userMatchStages: [], + baseMatch: { + 'stats.correctWords': { $gte: 40 } + } +}); + +const legacyFilter = buildVocabularySummaryFilters({ minWords: '35' }); +assert.deepEqual(legacyFilter, { + minCorrectWords: 35, + userMatchStages: [], + baseMatch: { + 'stats.correctWords': { $gte: 35 } + } +}); + +const emptyFilter = buildVocabularySummaryFilters({}); +assert.deepEqual(emptyFilter, { + minCorrectWords: undefined, + userMatchStages: [], + baseMatch: {} +}); + +const studentFilter = buildVocabularySummaryFilters({ studentNo: 'A.12' }); +assert.deepEqual(studentFilter.userMatchStages, [{ + $match: { + 'user.username': { + $regex: 'A\\.12', + $options: 'i' + } + } +}]); + +const invalidFilter = buildVocabularySummaryFilters({ minCorrectWords: '4.5' }); +assert.deepEqual(invalidFilter, { error: '正确单词数量必须是正整数' }); + +console.log('vocabularyAuditFilters tests passed'); diff --git a/server/utils/vocabularyAuditFilters.ts b/server/utils/vocabularyAuditFilters.ts new file mode 100644 index 0000000..f87247c --- /dev/null +++ b/server/utils/vocabularyAuditFilters.ts @@ -0,0 +1,47 @@ +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 }; +}; + +export const buildVocabularySummaryFilters = (query: Record) => { + const studentNo = String(query.studentNo || '').trim(); + const rawMinCorrectWords = query.minCorrectWords ?? query.minWords; + const wordCountResult = parseOptionalPositiveInt(rawMinCorrectWords, '正确单词数量'); + if ('error' in wordCountResult) return { error: wordCountResult.error }; + + const minCorrectWords = wordCountResult.value; + const userMatchStages = studentNo + ? [{ + $match: { + 'user.username': { + $regex: escapeRegex(studentNo), + $options: 'i' + } + } + }] + : []; + + const baseMatch: Record = {}; + if (typeof minCorrectWords === 'number') { + baseMatch['stats.correctWords'] = { $gte: minCorrectWords }; + } + + return { + minCorrectWords, + userMatchStages, + baseMatch + }; +}; diff --git a/src/api/admin.ts b/src/api/admin.ts index 28bb786..1ede8e0 100644 --- a/src/api/admin.ts +++ b/src/api/admin.ts @@ -128,7 +128,7 @@ export interface VocabularyAuditQueryParams { start: string; end: string; studentNo?: string; - minWords?: string; + minCorrectWords?: string; } export interface ClearVocabularyPassSummaryResponse { @@ -347,7 +347,7 @@ export const adminApi = { return api.get('/admin/vocabulary/test-pass-summary', { params }); }, - clearVocabularyPassSummary: async (data: { userId: string; start: string; end: string; studentNo?: string; minWords?: string }): Promise => { + clearVocabularyPassSummary: async (data: { userId: string; start: string; end: string; studentNo?: string; minCorrectWords?: string }): Promise => { return api.post('/admin/vocabulary/test-pass-summary/clear', data); }, diff --git a/src/components/AdminVocabularyScoreAudit.tsx b/src/components/AdminVocabularyScoreAudit.tsx index 0f55cac..a26cae1 100644 --- a/src/components/AdminVocabularyScoreAudit.tsx +++ b/src/components/AdminVocabularyScoreAudit.tsx @@ -94,7 +94,7 @@ const getAnswerRows = (record: VocabularyAuditRecordDetail): VocabularyAuditAnsw const AdminVocabularyScoreAudit: React.FC = () => { const [range, setRange] = useState<[string, string] | null>(null); const [studentNo, setStudentNo] = useState(''); - const [minWords, setMinWords] = useState(''); + const [minCorrectWords, setMinCorrectWords] = useState(''); const [summary, setSummary] = useState(null); const [loading, setLoading] = useState(false); const [clearingUserId, setClearingUserId] = useState(null); @@ -106,7 +106,7 @@ const AdminVocabularyScoreAudit: React.FC = () => { const getFilterParams = () => ({ studentNo: studentNo.trim() || undefined, - minWords: minWords.trim() || undefined + minCorrectWords: minCorrectWords.trim() || undefined }); const fetchSummary = async () => { @@ -571,9 +571,9 @@ const AdminVocabularyScoreAudit: React.FC = () => { setMinWords(event.target.value)} + placeholder="单次最少正确数" + value={minCorrectWords} + onChange={event => setMinCorrectWords(event.target.value)} onPressEnter={fetchSummary} style={{ width: 180 }} />