From 8cd695063104b33f844b84b986204908cf34da54 Mon Sep 17 00:00:00 2001 From: XuJiacheng Date: Tue, 30 Dec 2025 12:00:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E8=80=83=E8=AF=95?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E6=8E=A5=E5=8F=A3=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E4=BB=8E=E4=BB=BB=E5=8A=A1=E5=8F=8D=E6=8E=A8=E7=A7=91=E7=9B=AE?= =?UTF-8?q?=E5=90=8D=E7=A7=B0=EF=BC=9B=E6=9B=B4=E6=96=B0=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B=E4=BB=A5=E9=AA=8C=E8=AF=81=E6=96=B0=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/controllers/quizController.ts | 14 +++- api/models/quiz.ts | 15 +++-- data/survey.db | Bin 647168 -> 647168 bytes package.json | 2 +- src/layouts/AdminLayout.tsx | 2 +- src/pages/ResultPage.tsx | 1 + test/user-records-subjectname.test.ts | 92 ++++++++++++++++++++++++++ 7 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 test/user-records-subjectname.test.ts diff --git a/api/controllers/quizController.ts b/api/controllers/quizController.ts index f5ea83f..cb823fc 100644 --- a/api/controllers/quizController.ts +++ b/api/controllers/quizController.ts @@ -163,12 +163,24 @@ export class QuizController { const result = await QuizModel.submitQuiz({ userId, answers: processedAnswers }); if (subjectId || taskId) { + let finalSubjectId: string | null = subjectId || null; + const finalTaskId: string | null = taskId || null; + + // 任务考试场景下,如果前端未传subjectId,则从任务中反查 + if (!finalSubjectId && finalTaskId) { + const { ExamTaskModel } = await import('../models/examTask'); + const task = await ExamTaskModel.findById(finalTaskId); + if (task?.subjectId) { + finalSubjectId = task.subjectId; + } + } + const sql = ` UPDATE quiz_records SET subject_id = ?, task_id = ? WHERE id = ? `; - await import('../database').then(({ run }) => run(sql, [subjectId || null, taskId || null, result.record.id])); + await import('../database').then(({ run }) => run(sql, [finalSubjectId, finalTaskId, result.record.id])); } res.json({ diff --git a/api/models/quiz.ts b/api/models/quiz.ts index d925e3e..07571b9 100644 --- a/api/models/quiz.ts +++ b/api/models/quiz.ts @@ -140,11 +140,13 @@ export class QuizModel { const recordsSql = ` SELECT r.id, r.user_id as userId, r.total_score as totalScore, r.correct_count as correctCount, r.total_count as totalCount, r.score_percentage as scorePercentage, r.status, r.created_at as createdAt, - r.subject_id as subjectId, s.name as subjectName, + COALESCE(r.subject_id, t.subject_id) as subjectId, + COALESCE(s.name, ts.name) as subjectName, r.task_id as taskId, t.name as taskName FROM quiz_records r - LEFT JOIN exam_subjects s ON r.subject_id = s.id LEFT JOIN exam_tasks t ON r.task_id = t.id + LEFT JOIN exam_subjects s ON r.subject_id = s.id + LEFT JOIN exam_subjects ts ON t.subject_id = ts.id WHERE r.user_id = ? ORDER BY r.created_at DESC LIMIT ? OFFSET ? @@ -169,11 +171,16 @@ export class QuizModel { SELECT r.id, r.user_id as userId, u.name as userName, u.phone as userPhone, r.total_score as totalScore, r.correct_count as correctCount, r.total_count as totalCount, r.score_percentage as scorePercentage, r.status, - r.created_at as createdAt, r.subject_id as subjectId, s.name as subjectName, - r.task_id as taskId + r.created_at as createdAt, + COALESCE(r.subject_id, t.subject_id) as subjectId, + COALESCE(s.name, ts.name) as subjectName, + r.task_id as taskId, + t.name as taskName FROM quiz_records r JOIN users u ON r.user_id = u.id + LEFT JOIN exam_tasks t ON r.task_id = t.id LEFT JOIN exam_subjects s ON r.subject_id = s.id + LEFT JOIN exam_subjects ts ON t.subject_id = ts.id ORDER BY r.created_at DESC LIMIT ? OFFSET ? `; diff --git a/data/survey.db b/data/survey.db index 7b37a2b2d84e74bdc11b8a7a31c1295c06d12e6c..b43fe6dda1c1eb357e5be77e6cbb054f925a8759 100644 GIT binary patch delta 4519 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 diff --git a/package.json b/package.json index bd745a5..56ae8d3 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "build": "tsc && vite build", "preview": "vite preview", "start": "node dist/api/server.js", - "test": "node --import tsx --test test/admin-task-stats.test.ts test/question-text-import.test.ts test/swipe-detect.test.ts test/user-tasks.test.ts", + "test": "node --import tsx --test test/admin-task-stats.test.ts test/question-text-import.test.ts test/swipe-detect.test.ts test/user-tasks.test.ts test/user-records-subjectname.test.ts", "check": "tsc --noEmit" }, "dependencies": { diff --git a/src/layouts/AdminLayout.tsx b/src/layouts/AdminLayout.tsx index eb595b2..c13337a 100644 --- a/src/layouts/AdminLayout.tsx +++ b/src/layouts/AdminLayout.tsx @@ -128,7 +128,7 @@ const AdminLayout = ({ children }: { children: React.ReactNode }) => {
- 欢迎,{admin?.username} + 欢迎您,{admin?.username} { d="M12 3.6l2.63 5.33 5.89.86-4.26 4.15 1.01 5.86L12 17.69 6.73 19.8l1.01-5.86-4.26-4.15 5.89-.86L12 3.6z" fill="currentColor" stroke="none" + className="text-yellow-500" /> ); } diff --git a/test/user-records-subjectname.test.ts b/test/user-records-subjectname.test.ts new file mode 100644 index 0000000..bc1681b --- /dev/null +++ b/test/user-records-subjectname.test.ts @@ -0,0 +1,92 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { randomUUID } from 'node:crypto'; + +process.env.NODE_ENV = 'test'; +process.env.DB_PATH = ':memory:'; + +const jsonFetch = async ( + baseUrl: string, + path: string, + options?: { method?: string; body?: unknown }, +) => { + const res = await fetch(`${baseUrl}${path}`, { + method: options?.method ?? 'GET', + headers: options?.body ? { 'Content-Type': 'application/json' } : undefined, + body: options?.body ? JSON.stringify(options.body) : undefined, + }); + + const text = await res.text(); + let json: any = null; + try { + json = text ? JSON.parse(text) : null; + } catch { + json = null; + } + return { status: res.status, json, text }; +}; + +test('用户答题记录接口应返回subjectName(任务记录可从任务反推科目)', async () => { + const { initDatabase, run } = await import('../api/database'); + await initDatabase(); + + const { app } = await import('../api/server'); + const server = app.listen(0); + + try { + const addr = server.address(); + assert.ok(addr && typeof addr === 'object'); + const baseUrl = `http://127.0.0.1:${addr.port}`; + + const userId = randomUUID(); + const subjectId = randomUUID(); + const taskId = randomUUID(); + const recordId = randomUUID(); + + await run(`INSERT INTO users (id, name, phone, password) VALUES (?, ?, ?, ?)`, [ + userId, + '测试用户', + '13800138111', + '', + ]); + + await run( + `INSERT INTO exam_subjects (id, name, type_ratios, category_ratios, total_score, duration_minutes) + VALUES (?, ?, ?, ?, ?, ?)`, + [ + subjectId, + '测试科目-任务关联', + JSON.stringify({ single: 100 }), + JSON.stringify({ 通用: 100 }), + 100, + 60, + ], + ); + + const now = new Date().toISOString(); + await run( + `INSERT INTO exam_tasks (id, name, subject_id, start_at, end_at, selection_config) + VALUES (?, ?, ?, ?, ?, ?)`, + [taskId, '测试任务', subjectId, now, now, null], + ); + + // 模拟旧数据:quiz_records.subject_id 为空,仅有 task_id + await run( + `INSERT INTO quiz_records (id, user_id, subject_id, task_id, total_score, correct_count, total_count, created_at, score_percentage, status) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [recordId, userId, null, taskId, 80, 16, 20, now, 80, '优秀'], + ); + + const res = await jsonFetch(baseUrl, `/api/quiz/records/${userId}`); + assert.equal(res.status, 200); + assert.equal(res.json?.success, true); + assert.ok(Array.isArray(res.json?.data)); + + const row = (res.json?.data as any[]).find((r) => r.id === recordId); + assert.ok(row); + assert.equal(row.taskName, '测试任务'); + assert.equal(row.subjectName, '测试科目-任务关联'); + } finally { + server.close(); + } +});