diff --git a/server/routes/admin.ts b/server/routes/admin.ts index 04da042..5ffa985 100644 --- a/server/routes/admin.ts +++ b/server/routes/admin.ts @@ -12,7 +12,10 @@ 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'; +import { + applyVocabularySummaryItemFilters, + buildVocabularySummaryFilters +} from '../utils/vocabularyAuditFilters'; const router = express.Router(); const adminController = new AdminController(); @@ -542,7 +545,7 @@ router.get('/vocabulary/test-pass-summary', adminAuth, async (req, res) => { return res.status(400).json({ message: summaryFilters.error }); } - const { userMatchStages, baseMatch } = summaryFilters; + const { userMatchStages, baseMatch, minUniqueWords } = summaryFilters; const validPassItems = await VocabularyTestRecord.aggregate([ { $match: { @@ -691,7 +694,7 @@ router.get('/vocabulary/test-pass-summary', adminAuth, async (req, res) => { }); }); - const items = Array.from(itemMap.values()).sort((left, right) => { + const items = applyVocabularySummaryItemFilters(Array.from(itemMap.values()), minUniqueWords).sort((left, right) => { const passedDelta = (right.passedWords || 0) - (left.passedWords || 0); if (passedDelta !== 0) return passedDelta; diff --git a/server/utils/vocabularyAuditFilters.test.ts b/server/utils/vocabularyAuditFilters.test.ts index c61702b..d03acc3 100644 --- a/server/utils/vocabularyAuditFilters.test.ts +++ b/server/utils/vocabularyAuditFilters.test.ts @@ -1,27 +1,26 @@ import assert from 'node:assert/strict'; -import { buildVocabularySummaryFilters } from './vocabularyAuditFilters'; +import { + applyVocabularySummaryItemFilters, + buildVocabularySummaryFilters +} from './vocabularyAuditFilters'; -const correctWordFilter = buildVocabularySummaryFilters({ minCorrectWords: '40' }); -assert.deepEqual(correctWordFilter, { - minCorrectWords: 40, +const uniqueWordFilter = buildVocabularySummaryFilters({ minUniqueWords: '40' }); +assert.deepEqual(uniqueWordFilter, { + minUniqueWords: 40, userMatchStages: [], - baseMatch: { - 'stats.correctWords': { $gte: 40 } - } + baseMatch: {} }); -const legacyFilter = buildVocabularySummaryFilters({ minWords: '35' }); +const legacyFilter = buildVocabularySummaryFilters({ minCorrectWords: '35' }); assert.deepEqual(legacyFilter, { - minCorrectWords: 35, + minUniqueWords: 35, userMatchStages: [], - baseMatch: { - 'stats.correctWords': { $gte: 35 } - } + baseMatch: {} }); const emptyFilter = buildVocabularySummaryFilters({}); assert.deepEqual(emptyFilter, { - minCorrectWords: undefined, + minUniqueWords: undefined, userMatchStages: [], baseMatch: {} }); @@ -36,7 +35,14 @@ assert.deepEqual(studentFilter.userMatchStages, [{ } }]); -const invalidFilter = buildVocabularySummaryFilters({ minCorrectWords: '4.5' }); -assert.deepEqual(invalidFilter, { error: '正确单词数量必须是正整数' }); +const invalidFilter = buildVocabularySummaryFilters({ minUniqueWords: '4.5' }); +assert.deepEqual(invalidFilter, { error: '去重单词数量必须是正整数' }); + +const filteredItems = applyVocabularySummaryItemFilters([ + { userId: 'below', uniqueWords: 39 }, + { userId: 'equal', uniqueWords: 40 }, + { userId: 'above', uniqueWords: 41 } +], 40); +assert.deepEqual(filteredItems.map(item => item.userId), ['equal', 'above']); console.log('vocabularyAuditFilters tests passed'); diff --git a/server/utils/vocabularyAuditFilters.ts b/server/utils/vocabularyAuditFilters.ts index f87247c..cdaa7ee 100644 --- a/server/utils/vocabularyAuditFilters.ts +++ b/server/utils/vocabularyAuditFilters.ts @@ -18,11 +18,11 @@ const parseOptionalPositiveInt = (value: unknown, fieldName: string) => { export const buildVocabularySummaryFilters = (query: Record) => { const studentNo = String(query.studentNo || '').trim(); - const rawMinCorrectWords = query.minCorrectWords ?? query.minWords; - const wordCountResult = parseOptionalPositiveInt(rawMinCorrectWords, '正确单词数量'); + const rawMinUniqueWords = query.minUniqueWords ?? query.minCorrectWords ?? query.minWords; + const wordCountResult = parseOptionalPositiveInt(rawMinUniqueWords, '去重单词数量'); if ('error' in wordCountResult) return { error: wordCountResult.error }; - const minCorrectWords = wordCountResult.value; + const minUniqueWords = wordCountResult.value; const userMatchStages = studentNo ? [{ $match: { @@ -35,13 +35,18 @@ export const buildVocabularySummaryFilters = (query: Record) => { : []; const baseMatch: Record = {}; - if (typeof minCorrectWords === 'number') { - baseMatch['stats.correctWords'] = { $gte: minCorrectWords }; - } return { - minCorrectWords, + minUniqueWords, userMatchStages, baseMatch }; }; + +export const applyVocabularySummaryItemFilters = ( + items: T[], + minUniqueWords?: number +): T[] => { + if (typeof minUniqueWords !== 'number') return items; + return items.filter(item => Number(item.uniqueWords || 0) >= minUniqueWords); +}; diff --git a/src/api/admin.ts b/src/api/admin.ts index 1ede8e0..e75c057 100644 --- a/src/api/admin.ts +++ b/src/api/admin.ts @@ -128,7 +128,7 @@ export interface VocabularyAuditQueryParams { start: string; end: string; studentNo?: string; - minCorrectWords?: string; + minUniqueWords?: 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; minCorrectWords?: string }): Promise => { + clearVocabularyPassSummary: async (data: { userId: string; start: string; end: string; studentNo?: string; minUniqueWords?: 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 a26cae1..625abd7 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 [minCorrectWords, setMinCorrectWords] = useState(''); + const [minUniqueWords, setMinUniqueWords] = 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, - minCorrectWords: minCorrectWords.trim() || undefined + minUniqueWords: minUniqueWords.trim() || undefined }); const fetchSummary = async () => { @@ -571,9 +571,9 @@ const AdminVocabularyScoreAudit: React.FC = () => { setMinCorrectWords(event.target.value)} + placeholder="最少去重单词数" + value={minUniqueWords} + onChange={event => setMinUniqueWords(event.target.value)} onPressEnter={fetchSummary} style={{ width: 180 }} />