ffff
This commit is contained in:
@@ -12,7 +12,10 @@ import csv from 'csv-parser';
|
|||||||
import { Word, WordRecord, WordSet, VocabularyTestAttempt, VocabularyTestRecord } from '../models/Vocabulary';
|
import { Word, WordRecord, WordSet, VocabularyTestAttempt, VocabularyTestRecord } from '../models/Vocabulary';
|
||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { INVALIDATING_VOCABULARY_RISK_FLAGS } from '../utils/vocabularyRisk';
|
import { INVALIDATING_VOCABULARY_RISK_FLAGS } from '../utils/vocabularyRisk';
|
||||||
import { buildVocabularySummaryFilters } from '../utils/vocabularyAuditFilters';
|
import {
|
||||||
|
applyVocabularySummaryItemFilters,
|
||||||
|
buildVocabularySummaryFilters
|
||||||
|
} from '../utils/vocabularyAuditFilters';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const adminController = new AdminController();
|
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 });
|
return res.status(400).json({ message: summaryFilters.error });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { userMatchStages, baseMatch } = summaryFilters;
|
const { userMatchStages, baseMatch, minUniqueWords } = summaryFilters;
|
||||||
const validPassItems = await VocabularyTestRecord.aggregate([
|
const validPassItems = await VocabularyTestRecord.aggregate([
|
||||||
{
|
{
|
||||||
$match: {
|
$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);
|
const passedDelta = (right.passedWords || 0) - (left.passedWords || 0);
|
||||||
if (passedDelta !== 0) return passedDelta;
|
if (passedDelta !== 0) return passedDelta;
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { buildVocabularySummaryFilters } from './vocabularyAuditFilters';
|
import {
|
||||||
|
applyVocabularySummaryItemFilters,
|
||||||
|
buildVocabularySummaryFilters
|
||||||
|
} from './vocabularyAuditFilters';
|
||||||
|
|
||||||
const correctWordFilter = buildVocabularySummaryFilters({ minCorrectWords: '40' });
|
const uniqueWordFilter = buildVocabularySummaryFilters({ minUniqueWords: '40' });
|
||||||
assert.deepEqual(correctWordFilter, {
|
assert.deepEqual(uniqueWordFilter, {
|
||||||
minCorrectWords: 40,
|
minUniqueWords: 40,
|
||||||
userMatchStages: [],
|
userMatchStages: [],
|
||||||
baseMatch: {
|
baseMatch: {}
|
||||||
'stats.correctWords': { $gte: 40 }
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const legacyFilter = buildVocabularySummaryFilters({ minWords: '35' });
|
const legacyFilter = buildVocabularySummaryFilters({ minCorrectWords: '35' });
|
||||||
assert.deepEqual(legacyFilter, {
|
assert.deepEqual(legacyFilter, {
|
||||||
minCorrectWords: 35,
|
minUniqueWords: 35,
|
||||||
userMatchStages: [],
|
userMatchStages: [],
|
||||||
baseMatch: {
|
baseMatch: {}
|
||||||
'stats.correctWords': { $gte: 35 }
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const emptyFilter = buildVocabularySummaryFilters({});
|
const emptyFilter = buildVocabularySummaryFilters({});
|
||||||
assert.deepEqual(emptyFilter, {
|
assert.deepEqual(emptyFilter, {
|
||||||
minCorrectWords: undefined,
|
minUniqueWords: undefined,
|
||||||
userMatchStages: [],
|
userMatchStages: [],
|
||||||
baseMatch: {}
|
baseMatch: {}
|
||||||
});
|
});
|
||||||
@@ -36,7 +35,14 @@ assert.deepEqual(studentFilter.userMatchStages, [{
|
|||||||
}
|
}
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
const invalidFilter = buildVocabularySummaryFilters({ minCorrectWords: '4.5' });
|
const invalidFilter = buildVocabularySummaryFilters({ minUniqueWords: '4.5' });
|
||||||
assert.deepEqual(invalidFilter, { error: '正确单词数量必须是正整数' });
|
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');
|
console.log('vocabularyAuditFilters tests passed');
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ const parseOptionalPositiveInt = (value: unknown, fieldName: string) => {
|
|||||||
|
|
||||||
export const buildVocabularySummaryFilters = (query: Record<string, any>) => {
|
export const buildVocabularySummaryFilters = (query: Record<string, any>) => {
|
||||||
const studentNo = String(query.studentNo || '').trim();
|
const studentNo = String(query.studentNo || '').trim();
|
||||||
const rawMinCorrectWords = query.minCorrectWords ?? query.minWords;
|
const rawMinUniqueWords = query.minUniqueWords ?? query.minCorrectWords ?? query.minWords;
|
||||||
const wordCountResult = parseOptionalPositiveInt(rawMinCorrectWords, '正确单词数量');
|
const wordCountResult = parseOptionalPositiveInt(rawMinUniqueWords, '去重单词数量');
|
||||||
if ('error' in wordCountResult) return { error: wordCountResult.error };
|
if ('error' in wordCountResult) return { error: wordCountResult.error };
|
||||||
|
|
||||||
const minCorrectWords = wordCountResult.value;
|
const minUniqueWords = wordCountResult.value;
|
||||||
const userMatchStages = studentNo
|
const userMatchStages = studentNo
|
||||||
? [{
|
? [{
|
||||||
$match: {
|
$match: {
|
||||||
@@ -35,13 +35,18 @@ export const buildVocabularySummaryFilters = (query: Record<string, any>) => {
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
const baseMatch: Record<string, any> = {};
|
const baseMatch: Record<string, any> = {};
|
||||||
if (typeof minCorrectWords === 'number') {
|
|
||||||
baseMatch['stats.correctWords'] = { $gte: minCorrectWords };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
minCorrectWords,
|
minUniqueWords,
|
||||||
userMatchStages,
|
userMatchStages,
|
||||||
baseMatch
|
baseMatch
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const applyVocabularySummaryItemFilters = <T extends { uniqueWords?: number }>(
|
||||||
|
items: T[],
|
||||||
|
minUniqueWords?: number
|
||||||
|
): T[] => {
|
||||||
|
if (typeof minUniqueWords !== 'number') return items;
|
||||||
|
return items.filter(item => Number(item.uniqueWords || 0) >= minUniqueWords);
|
||||||
|
};
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export interface VocabularyAuditQueryParams {
|
|||||||
start: string;
|
start: string;
|
||||||
end: string;
|
end: string;
|
||||||
studentNo?: string;
|
studentNo?: string;
|
||||||
minCorrectWords?: string;
|
minUniqueWords?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClearVocabularyPassSummaryResponse {
|
export interface ClearVocabularyPassSummaryResponse {
|
||||||
@@ -347,7 +347,7 @@ export const adminApi = {
|
|||||||
return api.get<VocabularyPassSummaryResponse>('/admin/vocabulary/test-pass-summary', { params });
|
return api.get<VocabularyPassSummaryResponse>('/admin/vocabulary/test-pass-summary', { params });
|
||||||
},
|
},
|
||||||
|
|
||||||
clearVocabularyPassSummary: async (data: { userId: string; start: string; end: string; studentNo?: string; minCorrectWords?: string }): Promise<ClearVocabularyPassSummaryResponse> => {
|
clearVocabularyPassSummary: async (data: { userId: string; start: string; end: string; studentNo?: string; minUniqueWords?: string }): Promise<ClearVocabularyPassSummaryResponse> => {
|
||||||
return api.post<ClearVocabularyPassSummaryResponse>('/admin/vocabulary/test-pass-summary/clear', data);
|
return api.post<ClearVocabularyPassSummaryResponse>('/admin/vocabulary/test-pass-summary/clear', data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ const getAnswerRows = (record: VocabularyAuditRecordDetail): VocabularyAuditAnsw
|
|||||||
const AdminVocabularyScoreAudit: React.FC = () => {
|
const AdminVocabularyScoreAudit: React.FC = () => {
|
||||||
const [range, setRange] = useState<[string, string] | null>(null);
|
const [range, setRange] = useState<[string, string] | null>(null);
|
||||||
const [studentNo, setStudentNo] = useState('');
|
const [studentNo, setStudentNo] = useState('');
|
||||||
const [minCorrectWords, setMinCorrectWords] = useState('');
|
const [minUniqueWords, setMinUniqueWords] = useState('');
|
||||||
const [summary, setSummary] = useState<VocabularyPassSummaryResponse | null>(null);
|
const [summary, setSummary] = useState<VocabularyPassSummaryResponse | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [clearingUserId, setClearingUserId] = useState<string | null>(null);
|
const [clearingUserId, setClearingUserId] = useState<string | null>(null);
|
||||||
@@ -106,7 +106,7 @@ const AdminVocabularyScoreAudit: React.FC = () => {
|
|||||||
|
|
||||||
const getFilterParams = () => ({
|
const getFilterParams = () => ({
|
||||||
studentNo: studentNo.trim() || undefined,
|
studentNo: studentNo.trim() || undefined,
|
||||||
minCorrectWords: minCorrectWords.trim() || undefined
|
minUniqueWords: minUniqueWords.trim() || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchSummary = async () => {
|
const fetchSummary = async () => {
|
||||||
@@ -571,9 +571,9 @@ const AdminVocabularyScoreAudit: React.FC = () => {
|
|||||||
<Input
|
<Input
|
||||||
allowClear
|
allowClear
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="单次最少正确数"
|
placeholder="最少去重单词数"
|
||||||
value={minCorrectWords}
|
value={minUniqueWords}
|
||||||
onChange={event => setMinCorrectWords(event.target.value)}
|
onChange={event => setMinUniqueWords(event.target.value)}
|
||||||
onPressEnter={fetchSummary}
|
onPressEnter={fetchSummary}
|
||||||
style={{ width: 180 }}
|
style={{ width: 180 }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user