fixed a bug avoid crack answer
This commit is contained in:
@@ -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<TestResult[]>([]);
|
||||
const [answerFeedback, setAnswerFeedback] = useState<TestResult | null>(null);
|
||||
const [testAttemptId, setTestAttemptId] = useState<string>('');
|
||||
const [pendingAnswers, setPendingAnswers] = useState<PendingAnswer[]>([]);
|
||||
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 && (
|
||||
<div style={{
|
||||
padding: 15,
|
||||
backgroundColor: answerFeedback?.isCorrect ? '#f6ffed' : '#fff2f0',
|
||||
backgroundColor: '#f6f8fa',
|
||||
borderRadius: 4,
|
||||
marginBottom: 15,
|
||||
border: `1px solid ${answerFeedback?.isCorrect ? '#b7eb8f' : '#ffccc7'}`
|
||||
border: '1px solid #d9d9d9'
|
||||
}}>
|
||||
<div style={{
|
||||
fontWeight: 'bold',
|
||||
color: answerFeedback?.isCorrect ? '#389e0d' : '#cf1322',
|
||||
color: '#1677ff',
|
||||
marginBottom: 5
|
||||
}}>
|
||||
{answerFeedback?.isCorrect ? '回答正确' : '回答错误'}
|
||||
本题已提交
|
||||
</div>
|
||||
{answerFeedback && (
|
||||
<>
|
||||
<div>正确答案: {answerFeedback.correctAnswer}</div>
|
||||
<div>你的答案: {answerFeedback.userAnswer || '(空)'}</div>
|
||||
</>
|
||||
)}
|
||||
<div>正在进入下一题...</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user