forked from wangziqi/gongxue-base
feat: add content navigation practice assembly
This commit is contained in:
@@ -16,6 +16,14 @@ const ids = {
|
||||
region: '00000000-0000-0000-0000-000000000301',
|
||||
subject: '00000000-0000-0000-0000-000000000501',
|
||||
category: '00000000-0000-0000-0000-000000000601',
|
||||
contentEntry: '00000000-0000-0000-0000-000000000611',
|
||||
contentNodeCulture: '00000000-0000-0000-0000-000000000612',
|
||||
contentNodeProfessional: '00000000-0000-0000-0000-000000000613',
|
||||
contentNodeSchoolTarget: '00000000-0000-0000-0000-000000000614',
|
||||
questionCollection: '00000000-0000-0000-0000-000000000615',
|
||||
practiceBlueprintSequential: '00000000-0000-0000-0000-000000000616',
|
||||
practiceBlueprintRandom: '00000000-0000-0000-0000-000000000617',
|
||||
practiceBlueprintMock: '00000000-0000-0000-0000-000000000618',
|
||||
question: '00000000-0000-0000-0000-000000000401',
|
||||
vocabularyUnit: '00000000-0000-0000-0000-000000000811',
|
||||
vocabularyWord: '00000000-0000-0000-0000-000000000812',
|
||||
@@ -126,6 +134,45 @@ async function testCatalogAndLearning() {
|
||||
const question = questions.items?.find(item => item.id === ids.question);
|
||||
assert.ok(question, 'main tenant should return smoke question');
|
||||
assert.equal(question.hasVideoExplanation, true, 'smoke question should expose video marker');
|
||||
assert.equal(question.contentNodeId, ids.contentNodeSchoolTarget, 'question should expose new content node binding');
|
||||
|
||||
const entries = await request('/api/catalog/content-entries', {
|
||||
query: { regionId: ids.region, entryType: 'question_practice' },
|
||||
});
|
||||
assert.ok(entries.items?.some(item => item.id === ids.contentEntry), 'catalog should expose question practice entry');
|
||||
|
||||
const rootNodes = await request('/api/catalog/content-nodes', {
|
||||
query: { entryId: ids.contentEntry, parentId: 'root' },
|
||||
});
|
||||
assert.ok(rootNodes.items?.some(item => item.id === ids.contentNodeProfessional), 'catalog should expose root professional node');
|
||||
|
||||
const schoolNodes = await request('/api/catalog/content-nodes', {
|
||||
query: { entryId: ids.contentEntry, markerType: 'school', mode: 'flat' },
|
||||
});
|
||||
assert.ok(
|
||||
schoolNodes.items?.some(item => item.id === ids.contentNodeSchoolTarget && item.markerConfig?.salesIntent === true),
|
||||
'catalog should expose school target marker for sales intent',
|
||||
);
|
||||
|
||||
const collections = await request('/api/catalog/question-collections', {
|
||||
query: { nodeId: ids.contentNodeSchoolTarget },
|
||||
});
|
||||
assert.ok(collections.items?.some(item => item.id === ids.questionCollection), 'catalog should expose node question collection');
|
||||
|
||||
const collectionQuestions = await request('/api/catalog/question-collections/questions', {
|
||||
query: { collectionId: ids.questionCollection },
|
||||
});
|
||||
assert.ok(collectionQuestions.items?.some(item => item.id === ids.question), 'collection questions should include smoke question');
|
||||
|
||||
const blueprintList = await request('/api/catalog/practice-blueprints', {
|
||||
query: { collectionId: ids.questionCollection },
|
||||
});
|
||||
assert.ok(blueprintList.items?.some(item => item.id === ids.practiceBlueprintMock), 'catalog should expose mock exam blueprint');
|
||||
|
||||
const questionsByNode = await request('/api/catalog/questions', {
|
||||
query: { contentNodeId: ids.contentNodeSchoolTarget, limit: 20 },
|
||||
});
|
||||
assert.ok(questionsByNode.items?.some(item => item.id === ids.question), 'catalog questions should filter by contentNodeId');
|
||||
|
||||
const session = await request('/api/learning/practice-sessions', {
|
||||
method: 'POST',
|
||||
@@ -133,6 +180,43 @@ async function testCatalogAndLearning() {
|
||||
});
|
||||
assert.ok(session.item?.id, 'practice session should be created');
|
||||
|
||||
const sequentialSession = await request('/api/learning/practice-sessions', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
userId: USER_ID,
|
||||
mode: 'sequential',
|
||||
collectionId: ids.questionCollection,
|
||||
questionLimit: 5,
|
||||
},
|
||||
});
|
||||
assert.equal(sequentialSession.item?.collectionId, ids.questionCollection, 'collection session should bind collection');
|
||||
assert.ok(sequentialSession.item?.questionIds?.includes(ids.question), 'collection session should snapshot question ids');
|
||||
|
||||
const nodeSession = await request('/api/learning/practice-sessions', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
userId: USER_ID,
|
||||
mode: 'random',
|
||||
contentNodeId: ids.contentNodeProfessional,
|
||||
questionLimit: 5,
|
||||
},
|
||||
});
|
||||
assert.equal(nodeSession.item?.contentNodeId, ids.contentNodeProfessional, 'node session should bind parent content node');
|
||||
assert.ok(nodeSession.item?.questionIds?.includes(ids.question), 'node session should include descendant questions');
|
||||
|
||||
const mockSession = await request('/api/learning/practice-sessions', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
userId: USER_ID,
|
||||
blueprintId: ids.practiceBlueprintMock,
|
||||
},
|
||||
});
|
||||
assert.equal(mockSession.item?.mode, 'mock_exam', 'mock blueprint should create mock_exam session');
|
||||
assert.equal(mockSession.item?.blueprintId, ids.practiceBlueprintMock, 'mock session should bind blueprint');
|
||||
assert.equal(mockSession.item?.durationMinutes, 120, 'mock session should inherit duration');
|
||||
assert.equal(Number(mockSession.item?.totalScore), 100, 'mock session should inherit total score');
|
||||
assert.ok(mockSession.item?.questionIds?.includes(ids.question), 'mock session should snapshot assembled questions');
|
||||
|
||||
const answer = await request('/api/learning/answers', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
@@ -251,6 +335,26 @@ async function testTenantIsolation() {
|
||||
query: { unitId: ids.vocabularyUnit },
|
||||
});
|
||||
assert.equal(partnerWordStats.item?.totalWords, 0, 'partner tenant must not see main tenant vocabulary words');
|
||||
|
||||
const partnerEntries = await request('/api/catalog/content-entries', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
query: { regionId: ids.region },
|
||||
});
|
||||
assert.ok(!partnerEntries.items?.some(item => item.id === ids.contentEntry), 'partner tenant must not see main tenant content entries');
|
||||
|
||||
const partnerCollections = await request('/api/catalog/question-collections', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
query: { nodeId: ids.contentNodeSchoolTarget },
|
||||
});
|
||||
assert.ok(!partnerCollections.items?.some(item => item.id === ids.questionCollection), 'partner tenant must not see main tenant collections');
|
||||
|
||||
const partnerBlueprintSession = await request('/api/learning/practice-sessions', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
method: 'POST',
|
||||
body: { userId: USER_ID, blueprintId: ids.practiceBlueprintMock },
|
||||
expectStatus: 404,
|
||||
});
|
||||
assert.equal(partnerBlueprintSession.code, 'PRACTICE_BLUEPRINT_NOT_FOUND', 'partner tenant must not assemble main tenant blueprint');
|
||||
}
|
||||
|
||||
async function testTenantContentAdmin() {
|
||||
@@ -261,6 +365,64 @@ async function testTenantContentAdmin() {
|
||||
});
|
||||
assert.equal(denied.code, 'TENANT_CONTENT_EDITOR_REQUIRED', 'student should not write tenant content');
|
||||
|
||||
const deniedEntry = await request('/api/tenant-content/content-entries', {
|
||||
method: 'PUT',
|
||||
body: { entryKey: 'student-denied', name: '学生不能配置入口' },
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(deniedEntry.code, 'TENANT_CONTENT_EDITOR_REQUIRED', 'student should not manage content navigation');
|
||||
|
||||
const entry = await request('/api/tenant-content/content-entries', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
entryKey: 'integration-practice',
|
||||
regionId: ids.region,
|
||||
name: '集成测试刷题入口',
|
||||
entryType: 'question_practice',
|
||||
route: '/practice/integration',
|
||||
layoutConfig: { tabs: ['all', 'paper', 'chapter', 'type'] },
|
||||
order: 11,
|
||||
},
|
||||
});
|
||||
assert.equal(entry.item?.entryKey, 'integration-practice', 'tenant admin should create content entry');
|
||||
|
||||
const rootNode = await request('/api/tenant-content/content-nodes', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
entryId: entry.item.id,
|
||||
nodeKey: 'integration-professional',
|
||||
regionId: ids.region,
|
||||
name: '专业课',
|
||||
nodeType: 'category',
|
||||
markerType: 'exam_track',
|
||||
markerConfig: { intentKey: 'professional' },
|
||||
isLeaf: false,
|
||||
order: 1,
|
||||
},
|
||||
});
|
||||
assert.equal(rootNode.item?.markerType, 'exam_track', 'tenant admin should create marked root node');
|
||||
|
||||
const childNode = await request('/api/tenant-content/content-nodes', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
entryId: entry.item.id,
|
||||
parentId: rootNode.item.id,
|
||||
nodeKey: 'integration-school-target',
|
||||
regionId: ids.region,
|
||||
name: '集成测试学院',
|
||||
nodeType: 'school',
|
||||
markerType: 'school',
|
||||
markerConfig: { salesIntent: true, schoolName: '集成测试学院' },
|
||||
isLeaf: true,
|
||||
order: 1,
|
||||
},
|
||||
});
|
||||
assert.equal(childNode.item?.parentId, rootNode.item.id, 'tenant admin should create child node');
|
||||
assert.ok(String(childNode.item?.path || '').includes(String(rootNode.item?.path || '')), 'child node should have hierarchical path');
|
||||
|
||||
const unit = await request('/api/tenant-content/vocabulary-units', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
@@ -304,6 +466,8 @@ async function testTenantContentAdmin() {
|
||||
body: {
|
||||
subjectId: '00000000-0000-0000-0000-000000000501',
|
||||
categoryId: '00000000-0000-0000-0000-000000000601',
|
||||
entryId: entry.item.id,
|
||||
contentNodeId: childNode.item.id,
|
||||
type: 'choice',
|
||||
typeLabel: '单选题',
|
||||
difficulty: 2,
|
||||
@@ -317,11 +481,71 @@ async function testTenantContentAdmin() {
|
||||
answerText: '4',
|
||||
explanation: '基础加法。',
|
||||
status: 'published',
|
||||
examMarkers: { schoolName: '集成测试学院', salesIntent: true },
|
||||
},
|
||||
});
|
||||
assert.ok(question.item?.id, 'tenant admin should create question');
|
||||
assert.equal(question.item?.currentVersion?.correctOptionIndex, 1, 'created question should have a version');
|
||||
|
||||
const collection = await request('/api/tenant-content/question-collections', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
entryId: entry.item.id,
|
||||
nodeId: childNode.item.id,
|
||||
regionId: ids.region,
|
||||
subjectId: ids.subject,
|
||||
categoryId: ids.category,
|
||||
questionBankId: '00000000-0000-0000-0000-000000000400',
|
||||
name: '集成测试题目列表',
|
||||
collectionType: 'manual',
|
||||
sourceType: 'manual_questions',
|
||||
durationMinutes: 90,
|
||||
totalScore: 100,
|
||||
questions: [{ questionId: question.item.id, sectionKey: 'choice', order: 1, score: 2 }],
|
||||
},
|
||||
});
|
||||
assert.equal(collection.item?.questionCount, 1, 'tenant admin should create question collection with items');
|
||||
|
||||
const blueprint = await request('/api/tenant-content/practice-blueprints', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'PUT',
|
||||
body: {
|
||||
entryId: entry.item.id,
|
||||
nodeId: childNode.item.id,
|
||||
collectionId: collection.item.id,
|
||||
regionId: ids.region,
|
||||
name: '集成测试全真模拟',
|
||||
mode: 'mock_exam',
|
||||
assemblyType: 'collection',
|
||||
questionLimit: 10,
|
||||
durationMinutes: 90,
|
||||
totalScore: 100,
|
||||
passScore: 60,
|
||||
sections: [{ key: 'choice', questionType: 'choice', questionCount: 10, scoreEach: 2 }],
|
||||
rules: { randomize: true },
|
||||
},
|
||||
});
|
||||
assert.equal(blueprint.item?.mode, 'mock_exam', 'tenant admin should create mock exam blueprint');
|
||||
|
||||
const adminNodes = await request('/api/tenant-content/content-nodes', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { entryId: entry.item.id, mode: 'flat' },
|
||||
});
|
||||
assert.ok(adminNodes.items?.some(item => item.id === childNode.item.id), 'tenant admin should list created navigation nodes');
|
||||
|
||||
const publicCreatedCollections = await request('/api/catalog/question-collections', {
|
||||
query: { nodeId: childNode.item.id },
|
||||
});
|
||||
assert.ok(publicCreatedCollections.items?.some(item => item.id === collection.item.id), 'catalog should expose created collection');
|
||||
|
||||
const createdBlueprintSession = await request('/api/learning/practice-sessions', {
|
||||
method: 'POST',
|
||||
body: { userId: USER_ID, blueprintId: blueprint.item.id },
|
||||
});
|
||||
assert.equal(createdBlueprintSession.item?.blueprintId, blueprint.item.id, 'created blueprint should assemble a practice session');
|
||||
assert.ok(createdBlueprintSession.item?.questionIds?.includes(question.item.id), 'created blueprint session should include created question');
|
||||
|
||||
const binding = await request('/api/tenant-content/question-videos', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
@@ -517,6 +741,9 @@ async function testTenantContentAssetsAndImports() {
|
||||
subjectId: ids.subject,
|
||||
categoryId: ids.category,
|
||||
regionId: ids.region,
|
||||
entryId: ids.contentEntry,
|
||||
contentNodeId: ids.contentNodeSchoolTarget,
|
||||
collectionId: ids.questionCollection,
|
||||
items: [
|
||||
{
|
||||
legacyId: 'integration-import-choice-001',
|
||||
@@ -568,11 +795,11 @@ async function testTenantContentAssetsAndImports() {
|
||||
assert.ok(jobs.items?.some(item => item.id === validPreview.job.id && item.status === 'completed'), 'import job list should include completed job');
|
||||
|
||||
const importedQuestions = await request('/api/catalog/questions', {
|
||||
query: { categoryId: ids.category, limit: 100 },
|
||||
query: { collectionId: ids.questionCollection, limit: 100 },
|
||||
});
|
||||
assert.ok(
|
||||
importedQuestions.items?.some(item => item.content === '批量导入题:企业级 SaaS 应优先使用哪种数据库?'),
|
||||
'catalog should expose imported question',
|
||||
'catalog should expose imported question through the new collection binding',
|
||||
);
|
||||
|
||||
const partnerImports = await request('/api/tenant-content/imports', {
|
||||
|
||||
@@ -14,6 +14,14 @@ const ids = {
|
||||
region: '00000000-0000-0000-0000-000000000301',
|
||||
subject: '00000000-0000-0000-0000-000000000501',
|
||||
category: '00000000-0000-0000-0000-000000000601',
|
||||
contentEntry: '00000000-0000-0000-0000-000000000611',
|
||||
contentNodeCulture: '00000000-0000-0000-0000-000000000612',
|
||||
contentNodeProfessional: '00000000-0000-0000-0000-000000000613',
|
||||
contentNodeSchoolTarget: '00000000-0000-0000-0000-000000000614',
|
||||
questionCollection: '00000000-0000-0000-0000-000000000615',
|
||||
practiceBlueprintSequential: '00000000-0000-0000-0000-000000000616',
|
||||
practiceBlueprintRandom: '00000000-0000-0000-0000-000000000617',
|
||||
practiceBlueprintMock: '00000000-0000-0000-0000-000000000618',
|
||||
questionBank: '00000000-0000-0000-0000-000000000400',
|
||||
question: '00000000-0000-0000-0000-000000000401',
|
||||
questionVersion: '00000000-0000-0000-0000-000000000402',
|
||||
@@ -238,20 +246,235 @@ async function main() {
|
||||
[ids.questionBank, tenantId, ids.region],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
insert into public.content_entries (
|
||||
id, tenant_id, region_id, legacy_id, entry_key, name, entry_type,
|
||||
icon, route, description, visibility, layout_config, sort_order, is_active, created_by, updated_by
|
||||
)
|
||||
values (
|
||||
$1, $2, $3, 'smoke-practice-entry', 'smoke-question-practice', '烟测刷题入口',
|
||||
'question_practice', 'book-open', '/practice', '用于验证新内容导航和组卷链路',
|
||||
'public', '{"tabs":["all","paper","chapter","type"]}'::jsonb, 1, true, $4, $4
|
||||
)
|
||||
on conflict (id)
|
||||
do update set region_id = excluded.region_id,
|
||||
name = excluded.name,
|
||||
entry_key = excluded.entry_key,
|
||||
entry_type = excluded.entry_type,
|
||||
layout_config = excluded.layout_config,
|
||||
is_active = true,
|
||||
updated_at = now()
|
||||
`,
|
||||
[ids.contentEntry, tenantId, ids.region, ids.tenantAdminUser],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
insert into public.content_nodes (
|
||||
id, tenant_id, entry_id, region_id, parent_id, legacy_id, node_key,
|
||||
name, node_type, marker_type, marker_config, path, depth, sort_order,
|
||||
is_active, is_selectable, is_leaf, metadata, created_by, updated_by
|
||||
)
|
||||
values
|
||||
(
|
||||
$1, $2, $5, $6, null, 'smoke-culture-node', 'culture',
|
||||
'文化课', 'category', 'exam_track', '{"intentKey":"culture"}'::jsonb,
|
||||
'n_000000000000000000000000000000612'::ltree, 0, 1,
|
||||
true, true, true, '{"businessMeaning":"公共课入口"}'::jsonb, $7, $7
|
||||
),
|
||||
(
|
||||
$3, $2, $5, $6, null, 'smoke-professional-node', 'professional',
|
||||
'专业课', 'category', 'exam_track', '{"intentKey":"professional"}'::jsonb,
|
||||
'n_000000000000000000000000000000613'::ltree, 0, 2,
|
||||
true, true, false, '{"businessMeaning":"院校自主命题入口"}'::jsonb, $7, $7
|
||||
),
|
||||
(
|
||||
$4, $2, $5, $6, $3, 'smoke-school-target-node', 'professional-school-a',
|
||||
'烟测学院专业课', 'school', 'school', '{"schoolName":"烟测学院","salesIntent":true}'::jsonb,
|
||||
'n_000000000000000000000000000000613.n_000000000000000000000000000000614'::ltree, 1, 1,
|
||||
true, true, true, '{"businessMeaning":"学生目标院校意向"}'::jsonb, $7, $7
|
||||
)
|
||||
on conflict (id)
|
||||
do update set name = excluded.name,
|
||||
parent_id = excluded.parent_id,
|
||||
node_type = excluded.node_type,
|
||||
marker_type = excluded.marker_type,
|
||||
marker_config = excluded.marker_config,
|
||||
path = excluded.path,
|
||||
depth = excluded.depth,
|
||||
is_leaf = excluded.is_leaf,
|
||||
metadata = excluded.metadata,
|
||||
updated_at = now()
|
||||
`,
|
||||
[
|
||||
ids.contentNodeCulture,
|
||||
tenantId,
|
||||
ids.contentNodeProfessional,
|
||||
ids.contentNodeSchoolTarget,
|
||||
ids.contentEntry,
|
||||
ids.region,
|
||||
ids.tenantAdminUser,
|
||||
],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
insert into public.question_collections (
|
||||
id, tenant_id, region_id, entry_id, node_id, subject_id, category_id,
|
||||
question_bank_id, legacy_id, name, collection_type, source_type,
|
||||
filters, question_count, total_score, duration_minutes, status,
|
||||
sort_order, metadata, created_by, updated_by
|
||||
)
|
||||
values (
|
||||
$1, $2, $3, $4, $5, $6, $7,
|
||||
$8, 'smoke-collection', '烟测学院专业课题目列表', 'manual', 'manual_questions',
|
||||
'{"tabs":["all","paper","chapter","type"]}'::jsonb, 0, 100, 120, 'active',
|
||||
1, '{"business":"supports sequential random mock exam"}'::jsonb, $9, $9
|
||||
)
|
||||
on conflict (id)
|
||||
do update set node_id = excluded.node_id,
|
||||
subject_id = excluded.subject_id,
|
||||
category_id = excluded.category_id,
|
||||
question_bank_id = excluded.question_bank_id,
|
||||
name = excluded.name,
|
||||
collection_type = excluded.collection_type,
|
||||
source_type = excluded.source_type,
|
||||
filters = excluded.filters,
|
||||
total_score = excluded.total_score,
|
||||
duration_minutes = excluded.duration_minutes,
|
||||
status = 'active',
|
||||
updated_at = now()
|
||||
`,
|
||||
[
|
||||
ids.questionCollection,
|
||||
tenantId,
|
||||
ids.region,
|
||||
ids.contentEntry,
|
||||
ids.contentNodeSchoolTarget,
|
||||
ids.subject,
|
||||
ids.category,
|
||||
ids.questionBank,
|
||||
ids.tenantAdminUser,
|
||||
],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
insert into public.questions (
|
||||
id, tenant_id, question_bank_id, subject_id, category_id,
|
||||
legacy_id, type, type_label, difficulty, status
|
||||
entry_id, content_node_id, primary_collection_id,
|
||||
legacy_id, type, type_label, difficulty, status, exam_markers
|
||||
)
|
||||
values (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8,
|
||||
'smoke-question', 'choice', '单选题', 1, 'published',
|
||||
'{"examTrack":"professional","school":"烟测学院"}'::jsonb
|
||||
)
|
||||
values ($1, $2, $3, $4, $5, 'smoke-question', 'choice', '单选题', 1, 'published')
|
||||
on conflict (id)
|
||||
do update set question_bank_id = excluded.question_bank_id,
|
||||
subject_id = excluded.subject_id,
|
||||
category_id = excluded.category_id,
|
||||
entry_id = excluded.entry_id,
|
||||
content_node_id = excluded.content_node_id,
|
||||
primary_collection_id = excluded.primary_collection_id,
|
||||
exam_markers = excluded.exam_markers,
|
||||
updated_at = now()
|
||||
`,
|
||||
[ids.question, tenantId, ids.questionBank, ids.subject, ids.category],
|
||||
[
|
||||
ids.question,
|
||||
tenantId,
|
||||
ids.questionBank,
|
||||
ids.subject,
|
||||
ids.category,
|
||||
ids.contentEntry,
|
||||
ids.contentNodeSchoolTarget,
|
||||
ids.questionCollection,
|
||||
],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
insert into public.question_collection_items (
|
||||
tenant_id, collection_id, question_id, section_key, sort_order, score, required, metadata
|
||||
)
|
||||
values ($1, $2, $3, 'choice', 1, 2, true, '{"source":"smoke-seed"}'::jsonb)
|
||||
on conflict (tenant_id, collection_id, question_id)
|
||||
do update set section_key = excluded.section_key,
|
||||
sort_order = excluded.sort_order,
|
||||
score = excluded.score,
|
||||
updated_at = now()
|
||||
`,
|
||||
[tenantId, ids.questionCollection, ids.question],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
update public.question_collections
|
||||
set question_count = (
|
||||
select count(*)
|
||||
from public.question_collection_items
|
||||
where tenant_id = $1 and collection_id = $2
|
||||
),
|
||||
updated_at = now()
|
||||
where tenant_id = $1 and id = $2
|
||||
`,
|
||||
[tenantId, ids.questionCollection],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
`
|
||||
insert into public.practice_blueprints (
|
||||
id, tenant_id, region_id, entry_id, node_id, collection_id,
|
||||
legacy_id, name, mode, assembly_type, question_limit,
|
||||
duration_minutes, total_score, pass_score, sections, rules,
|
||||
status, sort_order, created_by, updated_by
|
||||
)
|
||||
values
|
||||
(
|
||||
$1, $4, $5, $6, $7, $8,
|
||||
'smoke-sequential-blueprint', '烟测顺序刷题', 'sequential', 'collection', 20,
|
||||
null, null, null, '[]'::jsonb, '{"randomize":false}'::jsonb,
|
||||
'active', 1, $9, $9
|
||||
),
|
||||
(
|
||||
$2, $4, $5, $6, $7, $8,
|
||||
'smoke-random-blueprint', '烟测随机刷题', 'random', 'collection', 20,
|
||||
null, null, null, '[]'::jsonb, '{"randomize":true}'::jsonb,
|
||||
'active', 2, $9, $9
|
||||
),
|
||||
(
|
||||
$3, $4, $5, $6, $7, $8,
|
||||
'smoke-mock-blueprint', '烟测全真模拟', 'mock_exam', 'collection', 10,
|
||||
120, 100, 60,
|
||||
'[{"key":"choice","title":"单选题","questionType":"choice","questionCount":10,"scoreEach":2}]'::jsonb,
|
||||
'{"randomize":true,"showAnalysisAfterSubmit":false}'::jsonb,
|
||||
'active', 3, $9, $9
|
||||
)
|
||||
on conflict (id)
|
||||
do update set name = excluded.name,
|
||||
mode = excluded.mode,
|
||||
assembly_type = excluded.assembly_type,
|
||||
question_limit = excluded.question_limit,
|
||||
duration_minutes = excluded.duration_minutes,
|
||||
total_score = excluded.total_score,
|
||||
pass_score = excluded.pass_score,
|
||||
sections = excluded.sections,
|
||||
rules = excluded.rules,
|
||||
status = 'active',
|
||||
updated_at = now()
|
||||
`,
|
||||
[
|
||||
ids.practiceBlueprintSequential,
|
||||
ids.practiceBlueprintRandom,
|
||||
ids.practiceBlueprintMock,
|
||||
tenantId,
|
||||
ids.region,
|
||||
ids.contentEntry,
|
||||
ids.contentNodeSchoolTarget,
|
||||
ids.questionCollection,
|
||||
ids.tenantAdminUser,
|
||||
],
|
||||
);
|
||||
|
||||
await client.query(
|
||||
|
||||
Reference in New Issue
Block a user