fixed a bug
This commit is contained in:
@@ -929,13 +929,14 @@ router.post('/test-answer', authMiddleware, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { attemptId, questionToken, userAnswer, submittedAt, answerProof, interactions } = req.body;
|
const { attemptId, questionToken, userAnswer, submittedAt, answerProof, interactions } = req.body;
|
||||||
const userId = req.user?._id;
|
const userId = req.user?._id;
|
||||||
|
const softRiskFlags: string[] = [];
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return res.status(401).json({ message: INVALID_CREDENTIAL_MESSAGE });
|
return res.status(401).json({ message: INVALID_CREDENTIAL_MESSAGE });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAllowedVocabularyOrigin(req)) {
|
if (!isAllowedVocabularyOrigin(req)) {
|
||||||
return res.status(403).json({ message: INVALID_CREDENTIAL_MESSAGE });
|
softRiskFlags.push('invalid_origin');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mongoose.isValidObjectId(attemptId)) {
|
if (!mongoose.isValidObjectId(attemptId)) {
|
||||||
@@ -957,9 +958,7 @@ router.post('/test-answer', authMiddleware, async (req, res) => {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
if (!Number.isFinite(expiresAt.getTime()) || expiresAt.getTime() < now) {
|
if (!Number.isFinite(expiresAt.getTime()) || expiresAt.getTime() < now) {
|
||||||
attemptAny.status = 'expired';
|
softRiskFlags.push('attempt_expired');
|
||||||
await attempt.save();
|
|
||||||
return res.status(410).json({ message: INVALID_CREDENTIAL_MESSAGE });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attemptAny.status !== 'active') {
|
if (attemptAny.status !== 'active') {
|
||||||
@@ -985,16 +984,14 @@ router.post('/test-answer', authMiddleware, async (req, res) => {
|
|||||||
|
|
||||||
const submittedAtMs = Number.parseInt(String(submittedAt), 10);
|
const submittedAtMs = Number.parseInt(String(submittedAt), 10);
|
||||||
if (!Number.isFinite(submittedAtMs)) {
|
if (!Number.isFinite(submittedAtMs)) {
|
||||||
return res.status(400).json({ message: INVALID_CREDENTIAL_MESSAGE });
|
softRiskFlags.push('invalid_submitted_at');
|
||||||
}
|
} else if (Math.abs(now - submittedAtMs) > 5 * 60 * 1000) {
|
||||||
|
softRiskFlags.push('submitted_at_out_of_sync');
|
||||||
if (Math.abs(now - submittedAtMs) > 5 * 60 * 1000) {
|
|
||||||
return res.status(400).json({ message: INVALID_CREDENTIAL_MESSAGE });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const wordId = getObjectIdString(attemptAny.questionWordIds[tokenIndex]);
|
const wordId = getObjectIdString(attemptAny.questionWordIds[tokenIndex]);
|
||||||
if (!wordId || !verifyQuestionToken(token, attemptId.toString(), wordId, tokenIndex, issuedAt)) {
|
if (!wordId || !verifyQuestionToken(token, attemptId.toString(), wordId, tokenIndex, issuedAt)) {
|
||||||
return res.status(400).json({ message: INVALID_CREDENTIAL_MESSAGE });
|
softRiskFlags.push('invalid_question_token');
|
||||||
}
|
}
|
||||||
|
|
||||||
let normalizedUserAnswer = String(userAnswer ?? '').trim().slice(0, 500);
|
let normalizedUserAnswer = String(userAnswer ?? '').trim().slice(0, 500);
|
||||||
@@ -1003,24 +1000,31 @@ router.post('/test-answer', authMiddleware, async (req, res) => {
|
|||||||
const optionTexts = attemptAny.optionTexts?.[tokenIndex] || [];
|
const optionTexts = attemptAny.optionTexts?.[tokenIndex] || [];
|
||||||
const selectedOptionIndex = optionTokens.findIndex((optionToken: string) => safeEqualString(optionToken, normalizedUserAnswer));
|
const selectedOptionIndex = optionTokens.findIndex((optionToken: string) => safeEqualString(optionToken, normalizedUserAnswer));
|
||||||
if (selectedOptionIndex < 0) {
|
if (selectedOptionIndex < 0) {
|
||||||
return res.status(400).json({ message: INVALID_CREDENTIAL_MESSAGE });
|
softRiskFlags.push('invalid_option_token');
|
||||||
|
const fallbackOptionIndex = optionTexts.findIndex((text: string) => text === normalizedUserAnswer);
|
||||||
|
normalizedUserAnswer = fallbackOptionIndex >= 0
|
||||||
|
? optionTexts[fallbackOptionIndex]
|
||||||
|
: String(userAnswer ?? '').trim().slice(0, 500);
|
||||||
|
} else {
|
||||||
|
normalizedUserAnswer = optionTexts[selectedOptionIndex] || '';
|
||||||
}
|
}
|
||||||
normalizedUserAnswer = optionTexts[selectedOptionIndex] || '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectedProof = buildAnswerProof(attemptId.toString(), token, String(userAnswer ?? '').trim().slice(0, 500), submittedAtMs);
|
const expectedProof = buildAnswerProof(attemptId.toString(), token, String(userAnswer ?? '').trim().slice(0, 500), submittedAtMs);
|
||||||
if (!safeEqualString(expectedProof, String(answerProof ?? ''))) {
|
if (!safeEqualString(expectedProof, String(answerProof ?? ''))) {
|
||||||
return res.status(400).json({ message: INVALID_CREDENTIAL_MESSAGE });
|
softRiskFlags.push('invalid_answer_proof');
|
||||||
}
|
}
|
||||||
|
|
||||||
const questionShownAtMs = tokenIndex === 0
|
const questionShownAtMs = tokenIndex === 0
|
||||||
? issuedAt.getTime()
|
? issuedAt.getTime()
|
||||||
: new Date(attemptAny.answers?.[tokenIndex - 1]?.submittedAt || issuedAt).getTime();
|
: new Date(attemptAny.answers?.[tokenIndex - 1]?.submittedAt || issuedAt).getTime();
|
||||||
const duration = Math.max(0, (submittedAtMs - questionShownAtMs) / 1000);
|
const effectiveSubmittedAtMs = Number.isFinite(submittedAtMs) ? submittedAtMs : now;
|
||||||
|
const duration = Math.max(0, (effectiveSubmittedAtMs - questionShownAtMs) / 1000);
|
||||||
const interactionSummary = summarizeInteractionEvents(interactions, questionShownAtMs);
|
const interactionSummary = summarizeInteractionEvents(interactions, questionShownAtMs);
|
||||||
const riskFlags = Array.from(new Set([
|
const riskFlags = Array.from(new Set([
|
||||||
...analyzeAnswerRisk(duration, interactionSummary, attemptAny.testType),
|
...analyzeAnswerRisk(duration, interactionSummary, attemptAny.testType),
|
||||||
...analyzeInputTraceRisk(interactions, normalizedUserAnswer, attemptAny.testType)
|
...analyzeInputTraceRisk(interactions, normalizedUserAnswer, attemptAny.testType),
|
||||||
|
...softRiskFlags
|
||||||
]));
|
]));
|
||||||
|
|
||||||
const word = await Word.findOne({
|
const word = await Word.findOne({
|
||||||
@@ -1042,7 +1046,7 @@ router.post('/test-answer', authMiddleware, async (req, res) => {
|
|||||||
userAnswer: evaluation.userAnswer,
|
userAnswer: evaluation.userAnswer,
|
||||||
correctAnswer: evaluation.correctAnswer,
|
correctAnswer: evaluation.correctAnswer,
|
||||||
isCorrect: evaluation.isCorrect,
|
isCorrect: evaluation.isCorrect,
|
||||||
submittedAt: new Date(submittedAtMs),
|
submittedAt: new Date(effectiveSubmittedAtMs),
|
||||||
duration,
|
duration,
|
||||||
riskFlags,
|
riskFlags,
|
||||||
interactionSummary
|
interactionSummary
|
||||||
@@ -1148,13 +1152,14 @@ router.post('/test-record', authMiddleware, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { attemptId } = req.body;
|
const { attemptId } = req.body;
|
||||||
const userId = req.user?._id;
|
const userId = req.user?._id;
|
||||||
|
const finalRiskFlags: string[] = [];
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return res.status(401).json({ message: INVALID_CREDENTIAL_MESSAGE });
|
return res.status(401).json({ message: INVALID_CREDENTIAL_MESSAGE });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAllowedVocabularyOrigin(req)) {
|
if (!isAllowedVocabularyOrigin(req)) {
|
||||||
return res.status(403).json({ message: INVALID_CREDENTIAL_MESSAGE });
|
finalRiskFlags.push('invalid_origin');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mongoose.isValidObjectId(attemptId)) {
|
if (!mongoose.isValidObjectId(attemptId)) {
|
||||||
@@ -1176,9 +1181,7 @@ router.post('/test-record', authMiddleware, async (req, res) => {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
if (!Number.isFinite(expiresAt.getTime()) || expiresAt.getTime() < now) {
|
if (!Number.isFinite(expiresAt.getTime()) || expiresAt.getTime() < now) {
|
||||||
attemptAny.status = 'expired';
|
finalRiskFlags.push('attempt_expired');
|
||||||
await attempt.save();
|
|
||||||
return res.status(410).json({ message: INVALID_CREDENTIAL_MESSAGE });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attemptAny.status !== 'active') {
|
if (attemptAny.status !== 'active') {
|
||||||
@@ -1196,7 +1199,7 @@ router.post('/test-record', authMiddleware, async (req, res) => {
|
|||||||
const elapsedSeconds = Math.max(0, (lastSubmittedAtMs - issuedAt.getTime()) / 1000);
|
const elapsedSeconds = Math.max(0, (lastSubmittedAtMs - issuedAt.getTime()) / 1000);
|
||||||
const minimumSeconds = Math.max(2, attemptAny.questionTokens.length * MIN_SECONDS_PER_QUESTION);
|
const minimumSeconds = Math.max(2, attemptAny.questionTokens.length * MIN_SECONDS_PER_QUESTION);
|
||||||
if (elapsedSeconds < minimumSeconds) {
|
if (elapsedSeconds < minimumSeconds) {
|
||||||
return res.status(400).json({ message: INVALID_CREDENTIAL_MESSAGE });
|
finalRiskFlags.push('too_fast');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1221,6 +1224,7 @@ router.post('/test-record', authMiddleware, async (req, res) => {
|
|||||||
const totalWords = evaluatedResults.length;
|
const totalWords = evaluatedResults.length;
|
||||||
const originallyCorrectWords = evaluatedResults.filter(result => result.isCorrect).length;
|
const originallyCorrectWords = evaluatedResults.filter(result => result.isCorrect).length;
|
||||||
const batchRiskFlags = [
|
const batchRiskFlags = [
|
||||||
|
...finalRiskFlags,
|
||||||
...(isMultipleChoiceAttempt ? [] : analyzeBatchRisk(attemptAny.answerDurations || [])),
|
...(isMultipleChoiceAttempt ? [] : analyzeBatchRisk(attemptAny.answerDurations || [])),
|
||||||
...analyzeVocabularyAttemptResultRisk({
|
...analyzeVocabularyAttemptResultRisk({
|
||||||
testType: attemptAny.testType,
|
testType: attemptAny.testType,
|
||||||
|
|||||||
@@ -4,8 +4,16 @@ export const DEFAULT_VOCABULARY_FULL_CORRECT_WORD_THRESHOLD = 50;
|
|||||||
export const VOCABULARY_FULL_CORRECT_RISK_FLAG = 'large_batch_all_correct';
|
export const VOCABULARY_FULL_CORRECT_RISK_FLAG = 'large_batch_all_correct';
|
||||||
|
|
||||||
export const INVALIDATING_VOCABULARY_RISK_FLAGS: string[] = [
|
export const INVALIDATING_VOCABULARY_RISK_FLAGS: string[] = [
|
||||||
|
'invalid_origin',
|
||||||
|
'invalid_answer_proof',
|
||||||
|
'invalid_question_token',
|
||||||
|
'invalid_option_token',
|
||||||
|
'invalid_submitted_at',
|
||||||
|
'submitted_at_out_of_sync',
|
||||||
|
'attempt_expired',
|
||||||
'too_fast',
|
'too_fast',
|
||||||
'too_slow',
|
'too_slow',
|
||||||
|
'missing_choice_interaction',
|
||||||
'missing_input_value_trace',
|
'missing_input_value_trace',
|
||||||
'input_value_mismatch',
|
'input_value_mismatch',
|
||||||
'uniform_answer_intervals',
|
'uniform_answer_intervals',
|
||||||
|
|||||||
@@ -619,7 +619,7 @@ const VocabularyStudy: React.FC = () => {
|
|||||||
));
|
));
|
||||||
setUserAnswer(response.optionToken);
|
setUserAnswer(response.optionToken);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(INVALID_CREDENTIAL_MESSAGE);
|
setUserAnswer(option.text);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -647,7 +647,10 @@ const VocabularyStudy: React.FC = () => {
|
|||||||
answerSubmittingRef.current = false;
|
answerSubmittingRef.current = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (testType === 'multiple-choice' && options.length > 0 && options.every(option => option.token !== normalizedUserAnswer)) {
|
const isKnownMultipleChoiceAnswer = options.some(option =>
|
||||||
|
option.token === normalizedUserAnswer || option.text === normalizedUserAnswer
|
||||||
|
);
|
||||||
|
if (testType === 'multiple-choice' && options.length > 0 && !isKnownMultipleChoiceAnswer) {
|
||||||
answerSubmittingRef.current = false;
|
answerSubmittingRef.current = false;
|
||||||
message.warning('请选择一个选项后再提交');
|
message.warning('请选择一个选项后再提交');
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user