forked from wangziqi/gongxue-base
feat: import scorelines and videos
This commit is contained in:
@@ -3012,6 +3012,195 @@ async function testTenantContentAssetsAndImports() {
|
||||
'catalog should expose imported handbook entry content',
|
||||
);
|
||||
|
||||
const invalidScorelinePreview = await request('/api/tenant-content/imports/preview/scoreline', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
regionId: ids.region,
|
||||
items: [{ kind: 'record', year: 2026, score: 180 }],
|
||||
},
|
||||
});
|
||||
assert.ok(
|
||||
invalidScorelinePreview.issues?.some(issue => issue.code === 'SCORELINE_RECORD_SCHOOL_REQUIRED'),
|
||||
'invalid scoreline preview should validate school reference',
|
||||
);
|
||||
|
||||
const scorelinePreview = await request('/api/tenant-content/imports/preview/scoreline', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
sourceName: 'scoreline-batch-import.json',
|
||||
regionId: ids.region,
|
||||
fields: [
|
||||
{
|
||||
legacyId: 'integration-scoreline-field-min',
|
||||
fieldKey: 'minScore',
|
||||
fieldName: '最低分',
|
||||
fieldType: 'number',
|
||||
unit: '分',
|
||||
isFilter: true,
|
||||
isTrend: true,
|
||||
order: 1,
|
||||
},
|
||||
],
|
||||
schools: [
|
||||
{
|
||||
legacyId: 'integration-scoreline-school-a',
|
||||
name: '集成测试学院',
|
||||
shortName: '集测学院',
|
||||
type: 'public',
|
||||
isHot: true,
|
||||
},
|
||||
],
|
||||
majors: [
|
||||
{
|
||||
legacyId: 'integration-scoreline-major-a',
|
||||
schoolLegacyId: 'integration-scoreline-school-a',
|
||||
name: '软件工程',
|
||||
hasRestriction: false,
|
||||
},
|
||||
],
|
||||
records: [
|
||||
{
|
||||
legacyId: 'integration-scoreline-record-a-2025',
|
||||
schoolLegacyId: 'integration-scoreline-school-a',
|
||||
majorLegacyId: 'integration-scoreline-major-a',
|
||||
schoolName: '集成测试学院',
|
||||
majorName: '软件工程',
|
||||
year: 2025,
|
||||
fieldValues: { minScore: 188, planCount: 60 },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
assert.equal(scorelinePreview.job?.errorCount, 0, 'valid scoreline preview should have no errors');
|
||||
assert.equal(scorelinePreview.job?.validCount, 4, 'scoreline preview should normalize fields, schools, majors, and records');
|
||||
|
||||
const scorelineImport = await request('/api/tenant-content/imports/scoreline', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: { previewJobId: scorelinePreview.job.id },
|
||||
});
|
||||
assert.equal(scorelineImport.item?.status, 'completed', 'scoreline import should complete');
|
||||
assert.ok(
|
||||
(scorelineImport.item?.insertedCount || 0) + (scorelineImport.item?.updatedCount || 0) + (scorelineImport.item?.skippedCount || 0) >= 4,
|
||||
'scoreline import should process all valid rows',
|
||||
);
|
||||
|
||||
const importedScorelineRecords = await request('/api/scoreline/records', {
|
||||
query: { regionId: ids.region, year: 2025, pageSize: 50 },
|
||||
});
|
||||
assert.ok(
|
||||
importedScorelineRecords.items?.some(item => item.schoolName === '集成测试学院' && item.fieldValues?.minScore === 188),
|
||||
'public scoreline records should expose imported record',
|
||||
);
|
||||
|
||||
const mixedScorelinePreview = await request('/api/tenant-content/imports/preview/scoreline', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
sourceName: 'scoreline-mixed-items.json',
|
||||
regionId: ids.region,
|
||||
items: [
|
||||
{
|
||||
kind: 'record',
|
||||
legacyId: 'integration-scoreline-mixed-record-2026',
|
||||
schoolLegacyId: 'integration-scoreline-mixed-school',
|
||||
majorLegacyId: 'integration-scoreline-mixed-major',
|
||||
year: 2026,
|
||||
minScore: 211,
|
||||
},
|
||||
{
|
||||
kind: 'major',
|
||||
legacyId: 'integration-scoreline-mixed-major',
|
||||
schoolLegacyId: 'integration-scoreline-mixed-school',
|
||||
name: '混排专业',
|
||||
},
|
||||
{
|
||||
kind: 'school',
|
||||
legacyId: 'integration-scoreline-mixed-school',
|
||||
name: '混排测试学院',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
assert.equal(mixedScorelinePreview.job?.errorCount, 0, 'mixed scoreline preview should accept out-of-order items');
|
||||
|
||||
const mixedScorelineImport = await request('/api/tenant-content/imports/scoreline', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: { previewJobId: mixedScorelinePreview.job.id },
|
||||
});
|
||||
assert.equal(mixedScorelineImport.item?.status, 'completed', 'mixed scoreline import should order dependent rows');
|
||||
|
||||
const mixedScorelineRecords = await request('/api/scoreline/records', {
|
||||
query: { regionId: ids.region, year: 2026, pageSize: 50 },
|
||||
});
|
||||
assert.ok(
|
||||
mixedScorelineRecords.items?.some(item => item.schoolName === '混排测试学院' && item.fieldValues?.minScore === 211),
|
||||
'mixed scoreline import should create record after school and major',
|
||||
);
|
||||
|
||||
const invalidVideoPreview = await request('/api/tenant-content/imports/preview/videos', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
videos: [{ title: '缺少题目绑定', bindings: [{ videoType: 'specific' }] }],
|
||||
},
|
||||
});
|
||||
assert.ok(
|
||||
invalidVideoPreview.issues?.some(issue => issue.code === 'VIDEO_BINDING_QUESTION_REQUIRED'),
|
||||
'invalid video preview should validate question binding',
|
||||
);
|
||||
|
||||
const videoPreview = await request('/api/tenant-content/imports/preview/videos', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
sourceName: 'video-batch-import.json',
|
||||
videos: [
|
||||
{
|
||||
legacyId: 'integration-video-import-001',
|
||||
title: '批量导入视频精讲',
|
||||
description: '通过内容导入管线导入并绑定题目',
|
||||
videoUrl: 'https://example.test/videos/imported.mp4',
|
||||
thumbnailUrl: 'https://example.test/videos/imported.jpg',
|
||||
durationSeconds: 120,
|
||||
knowledgeTags: ['批量导入', '视频精讲'],
|
||||
isGeneral: false,
|
||||
subjectId: ids.subject,
|
||||
accessMode: 'free',
|
||||
bindings: [
|
||||
{
|
||||
legacyId: 'integration-video-binding-001',
|
||||
questionId: ids.question,
|
||||
videoType: 'specific',
|
||||
order: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
assert.equal(videoPreview.job?.errorCount, 0, 'valid video preview should have no errors');
|
||||
|
||||
const videoImport = await request('/api/tenant-content/imports/videos', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: { previewJobId: videoPreview.job.id },
|
||||
});
|
||||
assert.equal(videoImport.item?.status, 'completed', 'video import should complete');
|
||||
assert.ok(
|
||||
(videoImport.item?.insertedCount || 0) + (videoImport.item?.updatedCount || 0) + (videoImport.item?.skippedCount || 0) >= 2,
|
||||
'video import should process video and question binding',
|
||||
);
|
||||
|
||||
const questionVideosAfterImport = await request(`/api/questions/${ids.question}/videos`);
|
||||
assert.ok(
|
||||
questionVideosAfterImport.videos?.some(item => item.legacyId === 'integration-video-import-001' && item.title === '批量导入视频精讲'),
|
||||
'question video API should expose imported video binding',
|
||||
);
|
||||
|
||||
const importJobs = await request('/api/tenant-content/imports', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { importType: 'vocabulary', limit: 10 },
|
||||
|
||||
@@ -41,6 +41,11 @@ async function runWorkerOnce() {
|
||||
return output;
|
||||
}
|
||||
|
||||
function countFromWorkerOutput(output, name) {
|
||||
const match = output.match(new RegExp(`${name}=(\\d+)`));
|
||||
return match ? Number(match[1]) : 0;
|
||||
}
|
||||
|
||||
async function cleanup(pool) {
|
||||
await pool.query(
|
||||
`
|
||||
@@ -109,8 +114,8 @@ async function main() {
|
||||
seeded = true;
|
||||
|
||||
const output = await runWorkerOnce();
|
||||
assert.match(output, /verified=1/, 'worker should verify one asset');
|
||||
assert.match(output, /failed=1/, 'worker should fail one mismatched asset');
|
||||
assert.ok(countFromWorkerOutput(output, 'verified') >= 1, 'worker should verify at least one asset');
|
||||
assert.ok(countFromWorkerOutput(output, 'failed') >= 1, 'worker should fail at least one mismatched asset');
|
||||
|
||||
const assets = await pool.query(
|
||||
`
|
||||
|
||||
Reference in New Issue
Block a user