feat: improve student practice experience

This commit is contained in:
Codex
2026-06-29 15:12:35 +08:00
parent 134f7830dd
commit b96897b2be
12 changed files with 443 additions and 63 deletions

View File

@@ -226,6 +226,33 @@ function judgeAnswer(row: QuestionAnswerRow, selectedOptions: string[], answerTe
return null;
}
function hasObjectiveAnswer(row: QuestionAnswerRow) {
return normalizeNumberArray(row.correct_option_indices).length > 0 ||
(row.correct_option_index !== null && row.correct_option_index !== undefined);
}
function optionalBodyBoolean(body: Record<string, unknown>, key: string): boolean | undefined {
const value = body[key];
if (value === undefined || value === null) return undefined;
if (typeof value === 'boolean') return value;
throw new HttpError(400, `${key} must be boolean`, 'INVALID_BOOLEAN_FIELD');
}
function resolveJudgedAnswer(
row: QuestionAnswerRow,
selectedOptions: string[],
answerText: string,
selfJudgedCorrect: boolean | undefined,
) {
if (selfJudgedCorrect !== undefined) {
if (hasObjectiveAnswer(row)) {
throw new HttpError(400, 'Self judgment is only allowed for subjective questions', 'SELF_JUDGMENT_NOT_ALLOWED');
}
return selfJudgedCorrect;
}
return judgeAnswer(row, selectedOptions, answerText);
}
function optionalBodyString(body: Record<string, unknown>, key: string) {
const value = body[key];
return typeof value === 'string' && value.trim() ? value.trim() : null;
@@ -699,6 +726,7 @@ export async function submitAnswerRoute(ctx: RequestContext) {
const questionId = requiredString(body, 'questionId');
const selectedOptions = optionalStringArray(body, 'selectedOptions');
const answerText = optionalString(body, 'answerText');
const selfJudgedCorrect = optionalBodyBoolean(body, 'selfJudgedCorrect');
const practiceSessionId = optionalString(body, 'practiceSessionId') || null;
const question = await queryOne<QuestionAnswerRow>(
@@ -717,10 +745,9 @@ export async function submitAnswerRoute(ctx: RequestContext) {
throw new HttpError(404, 'Question not found', 'QUESTION_NOT_FOUND');
}
const judged = judgeAnswer(question, selectedOptions, answerText);
const result = await transaction(async client => {
await assertAnswerSessionAccess(client, { tenantId, userId, practiceSessionId, questionId });
const judged = resolveJudgedAnswer(question, selectedOptions, answerText, selfJudgedCorrect);
const answerResult = await client.query(
`
@@ -774,7 +801,10 @@ export async function submitAnswerRoute(ctx: RequestContext) {
[tenantId, userId, judged],
);
return answerResult.rows[0];
return {
...answerResult.rows[0],
selfJudged: selfJudgedCorrect !== undefined,
};
});
return { item: result };