新增文本题库导入功能,题目新增“解析”字段
This commit is contained in:
262
test/question-text-import.test.ts
Normal file
262
test/question-text-import.test.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
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('题库文本导入增量模式按内容覆盖', async () => {
|
||||
const { initDatabase, run, get } = 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 oldId = randomUUID();
|
||||
await run(
|
||||
`INSERT INTO questions (id, content, type, options, answer, score, category)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[oldId, '重复题目', 'single', JSON.stringify(['A', 'B']), 'A', 5, '通用'],
|
||||
);
|
||||
|
||||
const res = await jsonFetch(baseUrl, '/api/questions/import-text', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
mode: 'incremental',
|
||||
questions: [
|
||||
{
|
||||
content: '重复题目',
|
||||
type: 'multiple',
|
||||
category: '数学',
|
||||
options: ['选项A', '选项B', '选项C'],
|
||||
answer: ['选项A', '选项C'],
|
||||
analysis: '解析:这是一道示例题',
|
||||
score: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.json?.success, true);
|
||||
assert.equal(res.json?.data?.inserted, 0);
|
||||
assert.equal(res.json?.data?.updated, 1);
|
||||
|
||||
const row = await get(`SELECT * FROM questions WHERE content = ?`, ['重复题目']);
|
||||
assert.equal(row.id, oldId);
|
||||
assert.equal(row.type, 'multiple');
|
||||
assert.equal(row.category, '数学');
|
||||
assert.equal(row.score, 10);
|
||||
assert.equal(row.analysis, '解析:这是一道示例题');
|
||||
assert.equal(JSON.parse(row.options).length, 3);
|
||||
assert.deepEqual(JSON.parse(row.answer), ['选项A', '选项C']);
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
});
|
||||
|
||||
test('题库文本导入覆盖模式清空题库与答题记录', async () => {
|
||||
const { initDatabase, run, get } = 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 oldQuestionId = randomUUID();
|
||||
const recordId = randomUUID();
|
||||
const answerId = randomUUID();
|
||||
|
||||
await run(`INSERT INTO users (id, name, phone, password) VALUES (?, ?, ?, ?)`, [
|
||||
userId,
|
||||
'测试用户',
|
||||
`138${Math.floor(Math.random() * 1e8).toString().padStart(8, '0')}`,
|
||||
'',
|
||||
]);
|
||||
|
||||
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,
|
||||
],
|
||||
);
|
||||
|
||||
await run(
|
||||
`INSERT INTO questions (id, content, type, options, answer, score, category)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[oldQuestionId, '旧题目', 'single', JSON.stringify(['A', 'B']), 'A', 5, '通用'],
|
||||
);
|
||||
|
||||
await run(
|
||||
`INSERT INTO quiz_records (id, user_id, subject_id, task_id, total_score, correct_count, total_count, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[recordId, userId, subjectId, null, 5, 1, 1, new Date().toISOString()],
|
||||
);
|
||||
|
||||
await run(
|
||||
`INSERT INTO quiz_answers (id, record_id, question_id, user_answer, score, is_correct, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[answerId, recordId, oldQuestionId, 'A', 5, 1, new Date().toISOString()],
|
||||
);
|
||||
|
||||
const res = await jsonFetch(baseUrl, '/api/questions/import-text', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
mode: 'overwrite',
|
||||
questions: [
|
||||
{
|
||||
content: '新题目',
|
||||
type: 'single',
|
||||
category: '通用',
|
||||
options: ['A', 'B'],
|
||||
answer: 'A',
|
||||
analysis: '解析:新题目的说明',
|
||||
score: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.json?.success, true);
|
||||
assert.equal(res.json?.data?.inserted, 1);
|
||||
assert.equal(res.json?.data?.updated, 0);
|
||||
|
||||
const questionCount = await get(`SELECT COUNT(*) as total FROM questions`);
|
||||
const recordCount = await get(`SELECT COUNT(*) as total FROM quiz_records`);
|
||||
const answerCount = await get(`SELECT COUNT(*) as total FROM quiz_answers`);
|
||||
|
||||
assert.equal(questionCount.total, 1);
|
||||
assert.equal(recordCount.total, 0);
|
||||
assert.equal(answerCount.total, 0);
|
||||
|
||||
const row = await get(`SELECT * FROM questions WHERE content = ?`, ['新题目']);
|
||||
assert.equal(row.analysis, '解析:新题目的说明');
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
});
|
||||
|
||||
test('答题记录详情应返回题目解析字段', 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 questionId = randomUUID();
|
||||
const recordId = randomUUID();
|
||||
const answerId = randomUUID();
|
||||
const createdAt = new Date().toISOString();
|
||||
|
||||
await run(`INSERT INTO users (id, name, phone, password) VALUES (?, ?, ?, ?)`, [
|
||||
userId,
|
||||
'测试用户',
|
||||
'13800138000',
|
||||
'',
|
||||
]);
|
||||
|
||||
await run(
|
||||
`INSERT INTO questions (id, content, type, options, answer, analysis, score, category)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[questionId, '带解析题目', 'single', JSON.stringify(['A', 'B']), 'A', '解析内容', 5, '通用'],
|
||||
);
|
||||
|
||||
await run(
|
||||
`INSERT INTO quiz_records (id, user_id, subject_id, task_id, total_score, correct_count, total_count, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[recordId, userId, null, null, 5, 1, 1, createdAt],
|
||||
);
|
||||
|
||||
await run(
|
||||
`INSERT INTO quiz_answers (id, record_id, question_id, user_answer, score, is_correct, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[answerId, recordId, questionId, 'A', 5, 1, createdAt],
|
||||
);
|
||||
|
||||
const res = await jsonFetch(baseUrl, `/api/quiz/records/detail/${recordId}`);
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(res.json?.success, true);
|
||||
assert.equal(res.json?.data?.record?.id, recordId);
|
||||
assert.equal(Array.isArray(res.json?.data?.answers), true);
|
||||
assert.equal(res.json?.data?.answers?.[0]?.questionAnalysis, '解析内容');
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
}
|
||||
});
|
||||
|
||||
test('前端文本解析支持 | 分隔格式', async () => {
|
||||
const { parseTextQuestions } = await import('../src/utils/questionTextImport');
|
||||
|
||||
const input = [
|
||||
'题型|题目类别|分值|题目内容|选项|答案|解析',
|
||||
'单选|通用|5|我国首都是哪里?|北京|上海|广州|深圳|A|我国首都为北京',
|
||||
'多选|通用|5|以下哪些是水果?|苹果|白菜|香蕉|西红柿|A,C,D|水果包括苹果/香蕉/西红柿',
|
||||
'判断|通用|2|地球是圆的||正确|地球接近球体',
|
||||
].join('\n');
|
||||
|
||||
const res = parseTextQuestions(input);
|
||||
assert.deepEqual(res.errors, []);
|
||||
assert.equal(res.questions.length, 3);
|
||||
|
||||
const single = res.questions.find((q: any) => q.type === 'single');
|
||||
assert.ok(single);
|
||||
assert.deepEqual(single.options, ['北京', '上海', '广州', '深圳']);
|
||||
assert.equal(single.answer, '北京');
|
||||
assert.equal(single.analysis, '我国首都为北京');
|
||||
|
||||
const multiple = res.questions.find((q: any) => q.type === 'multiple');
|
||||
assert.ok(multiple);
|
||||
assert.deepEqual(multiple.options, ['苹果', '白菜', '香蕉', '西红柿']);
|
||||
assert.deepEqual(multiple.answer, ['苹果', '香蕉', '西红柿']);
|
||||
assert.equal(multiple.analysis, '水果包括苹果/香蕉/西红柿');
|
||||
|
||||
const judgment = res.questions.find((q: any) => q.type === 'judgment');
|
||||
assert.ok(judgment);
|
||||
assert.equal(judgment.answer, '正确');
|
||||
assert.equal(judgment.analysis, '地球接近球体');
|
||||
});
|
||||
Reference in New Issue
Block a user