diff --git a/server/routes/vocabulary.ts b/server/routes/vocabulary.ts index 4bdc5b0..f8ba9f4 100644 --- a/server/routes/vocabulary.ts +++ b/server/routes/vocabulary.ts @@ -48,6 +48,7 @@ const WORD_RECORD_MODES: WordRecordModeKey[] = [ const ATTEMPT_TTL_MS = 30 * 60 * 1000; const MIN_SECONDS_PER_QUESTION = Number(process.env.VOCABULARY_MIN_SECONDS_PER_QUESTION || 0.4); const ATTEMPT_SECRET = process.env.VOCABULARY_ATTEMPT_SECRET || config.JWT_SECRET; +const ANSWER_PROOF_SALT = 'd1ktsalt'; const isVocabularyTestType = (value: unknown): value is VocabularyTestType => { return value === 'chinese-to-english' || @@ -104,8 +105,8 @@ const buildAnswerProof = ( userAnswer: string, submittedAt: number ): string => crypto - .createHash('sha256') - .update(`${attemptId}:${questionToken}:${submittedAt}:${String(userAnswer ?? '')}`) + .createHash('md5') + .update(`${ANSWER_PROOF_SALT}:${attemptId}:${questionToken}:${submittedAt}:${String(userAnswer ?? '')}:${ANSWER_PROOF_SALT}`) .digest('hex'); const buildMultipleChoiceOptions = (word: any, pool: any[]): string[] => { @@ -879,6 +880,7 @@ router.post('/test-record', authMiddleware, async (req, res) => { message: '测试记录已保存', attemptId: attempt._id.toString(), stats: verifiedStats, + results: evaluatedResults, submittedAt: new Date(lastSubmittedAtMs) }); } catch (error) { diff --git a/src/components/VocabularyStudy.tsx b/src/components/VocabularyStudy.tsx index 767ec2d..bd820da 100644 --- a/src/components/VocabularyStudy.tsx +++ b/src/components/VocabularyStudy.tsx @@ -93,6 +93,7 @@ interface SavedVocabularyTestRecordResponse { endTime: string | Date; duration: number; }; + results: TestResult[]; } interface TestAttemptResponse { @@ -102,6 +103,8 @@ interface TestAttemptResponse { questions: TestQuestion[]; } +const ANSWER_PROOF_SALT = 'd1ktsalt'; + const VocabularyStudy: React.FC = () => { const navigate = useNavigate(); const [loading, setLoading] = useState(false); @@ -124,7 +127,6 @@ const VocabularyStudy: React.FC = () => { const [testStarted, setTestStarted] = useState(false); const [testFinished, setTestFinished] = useState(false); const [testResults, setTestResults] = useState([]); - const [answerFeedback, setAnswerFeedback] = useState(null); const [testAttemptId, setTestAttemptId] = useState(''); const [pendingAnswers, setPendingAnswers] = useState([]); const [testAttemptLoading, setTestAttemptLoading] = useState(false); @@ -429,7 +431,6 @@ const VocabularyStudy: React.FC = () => { setTestAttemptId(''); setPendingAnswers([]); setTestResults([]); - setAnswerFeedback(null); const wordIds = studyWords.map(getWordId).filter(Boolean); if (wordIds.length === 0) { message.error('单词数据不完整,请重新加载后再试'); @@ -457,6 +458,7 @@ const VocabularyStudy: React.FC = () => { setTestResults([]); setPendingAnswers([]); setOptions(questions[0]?.options || []); + setIsModalVisible(false); setStudyStats({ totalWords: 0, correctWords: 0, @@ -493,42 +495,7 @@ const VocabularyStudy: React.FC = () => { userAnswer: string, submittedAt: number ): string => { - return CryptoJS.SHA256(`${attemptId}:${questionToken}:${submittedAt}:${userAnswer}`).toString(); - }; - - function normalizeAnswer(str: string): string { - return str - .replace(/(/g, '(') - .replace(/)/g, ')') - .replace(/,/g, ',') // 全角逗号转半角 - .replace(/\s+/g, '') // 去除所有空格 - .replace(/,/g, '') // 去除所有逗号 - .replace(/[^\w()]/g, '') // 只保留字母、数字、括号 - .toLowerCase(); - } - - const getQuestionWord = (question: TestQuestion): Word | undefined => { - return studyWords.find(word => getWordId(word) === question.wordId); - }; - - const buildLocalTestResult = (question: TestQuestion, rawUserAnswer: string): TestResult => { - const matchedWord = getQuestionWord(question); - const userAnswer = rawUserAnswer.trim(); - const correctAnswer = testType === 'multiple-choice' - ? String(matchedWord?.translation ?? question.translation ?? '').trim() - : String(matchedWord?.word ?? question.word ?? '').trim(); - const isCorrect = testType === 'multiple-choice' - ? userAnswer === correctAnswer - : normalizeAnswer(userAnswer) === normalizeAnswer(correctAnswer); - - return { - questionToken: question.questionToken, - wordId: question.wordId, - word: String(matchedWord?.word ?? question.word ?? question.translation ?? '').trim(), - userAnswer, - correctAnswer, - isCorrect - }; + return CryptoJS.MD5(`${ANSWER_PROOF_SALT}:${attemptId}:${questionToken}:${submittedAt}:${userAnswer}:${ANSWER_PROOF_SALT}`).toString(); }; // 提交答案 @@ -543,7 +510,6 @@ const VocabularyStudy: React.FC = () => { } const submittedAt = Date.now(); const normalizedUserAnswer = userAnswer.trim(); - const localResult = buildLocalTestResult(currentQuestion, normalizedUserAnswer); const answerProof = buildAnswerProof( testAttemptId, currentQuestion.questionToken, @@ -560,14 +526,8 @@ const VocabularyStudy: React.FC = () => { answerProof } ]; - const updatedTestResults = [ - ...testResults, - localResult - ]; setPendingAnswers(updatedPendingAnswers); - setTestResults(updatedTestResults); - setAnswerFeedback(localResult); setShowAnswer(true); setUserAnswer(''); @@ -576,7 +536,6 @@ const VocabularyStudy: React.FC = () => { const nextIndex = testIndex + 1; setTestIndex(nextIndex); setShowAnswer(false); - setAnswerFeedback(null); answerSubmittingRef.current = false; if (testType === 'audio-to-english') { setTimeout(() => { @@ -588,31 +547,14 @@ const VocabularyStudy: React.FC = () => { } } else { answerSubmittingRef.current = false; - finishTestWithResults(updatedPendingAnswers, updatedTestResults); + finishTestWithResults(updatedPendingAnswers); } }, 1500); }; // 使用指定结果完成测试 - const finishTestWithResults = async (finalAnswers: PendingAnswer[], finalResults: TestResult[]) => { + const finishTestWithResults = async (finalAnswers: PendingAnswer[]) => { try { - const lastSubmittedAtMs = finalAnswers.length > 0 - ? Math.max(...finalAnswers.map(item => item.submittedAt)) - : Date.now(); - const localCorrectWords = finalResults.filter(result => result.isCorrect).length; - const localStats = { - totalWords: finalResults.length, - correctWords: localCorrectWords, - accuracy: finalResults.length > 0 ? (localCorrectWords / finalResults.length) * 100 : 0, - startTime: studyStats.startTime, - endTime: new Date(lastSubmittedAtMs), - duration: Math.max(0, (lastSubmittedAtMs - studyStats.startTime.getTime()) / 1000) - }; - - setTestResults(finalResults); - setStudyStats(localStats); - setTestFinished(true); - // 添加调试信息 console.log('测试完成状态(带结果):', { finalResultsLength: finalAnswers.length, @@ -640,8 +582,11 @@ const VocabularyStudy: React.FC = () => { endTime: new Date(savedRecord.stats.endTime), duration: savedRecord.stats.duration }); + setTestResults(savedRecord.results); setPendingAnswers([]); setTestAttemptId(''); + setShowAnswer(false); + setTestFinished(true); message.success('测试完成,记录已保存'); setIsModalVisible(true); @@ -657,7 +602,7 @@ const VocabularyStudy: React.FC = () => { // 完成测试 - 兼容原来的调用方式 const finishTest = async () => { // 使用当前的测试结果完成测试 - await finishTestWithResults([...pendingAnswers], [...testResults]); + await finishTestWithResults([...pendingAnswers]); }; // 重新开始测试 @@ -670,7 +615,6 @@ const VocabularyStudy: React.FC = () => { setUserAnswer(''); setShowAnswer(false); setTestResults([]); - setAnswerFeedback(null); setPendingAnswers([]); setOptions([]); answerSubmittingRef.current = false; @@ -1271,24 +1215,18 @@ const VocabularyStudy: React.FC = () => { {showAnswer && (
- {answerFeedback?.isCorrect ? '回答正确' : '回答错误'} + 本题已提交
- {answerFeedback && ( - <> -
正确答案: {answerFeedback.correctAnswer}
-
你的答案: {answerFeedback.userAnswer || '(空)'}
- - )}
正在进入下一题...
)}