forked from wangziqi/gongxue-base
feat: support daily practice exports
This commit is contained in:
@@ -94,7 +94,11 @@ export interface BuiltExportPayload {
|
||||
sectionCount: number;
|
||||
totalScore: number;
|
||||
durationMinutes: number | null;
|
||||
cardCount?: number;
|
||||
issue?: string;
|
||||
date?: string;
|
||||
};
|
||||
dailyPractice?: DailyPracticeMetadata;
|
||||
sections: Array<{
|
||||
key: string;
|
||||
title: string;
|
||||
@@ -117,6 +121,38 @@ export interface BuiltExportPayload {
|
||||
};
|
||||
}
|
||||
|
||||
interface DailyPracticeBrand {
|
||||
name: string;
|
||||
english: string;
|
||||
slogan: string;
|
||||
ctaLine: string;
|
||||
}
|
||||
|
||||
interface DailyPracticeMetadata {
|
||||
issue: string;
|
||||
date: string;
|
||||
theme: string;
|
||||
cardFormat: string;
|
||||
brand: DailyPracticeBrand;
|
||||
showAnswer: boolean;
|
||||
centerSlot: {
|
||||
type: 'cta' | 'image';
|
||||
title: string;
|
||||
subtitle: string;
|
||||
imageUrl: string | null;
|
||||
};
|
||||
slots: Array<{
|
||||
slot: number;
|
||||
kind: 'question' | 'center';
|
||||
questionId?: unknown;
|
||||
subjectName?: unknown;
|
||||
typeLabel?: unknown;
|
||||
order?: unknown;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
function choose(value: unknown, allowed: string[], fallback: string, code: string) {
|
||||
const candidate = nullableString(value) || fallback;
|
||||
if (!allowed.includes(candidate)) {
|
||||
@@ -139,6 +175,11 @@ function exportLimit(value: unknown) {
|
||||
return Math.max(1, Math.min(Math.trunc(parsed), 5000));
|
||||
}
|
||||
|
||||
function exportLimitForType(exportType: string, value: unknown) {
|
||||
const limit = exportLimit(value);
|
||||
return exportType === 'daily_practice' ? Math.min(limit, 8) : limit;
|
||||
}
|
||||
|
||||
function isBinaryExportFormat(format: string) {
|
||||
return BINARY_EXPORT_FORMATS.includes(format);
|
||||
}
|
||||
@@ -231,6 +272,72 @@ function groupBySection(questions: ReturnType<typeof formatQuestion>[]) {
|
||||
}));
|
||||
}
|
||||
|
||||
function todayDateString() {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function objectValue(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
}
|
||||
|
||||
function stringValue(value: unknown, fallback: string) {
|
||||
return typeof value === 'string' && value.trim() ? value.trim().slice(0, 120) : fallback;
|
||||
}
|
||||
|
||||
function dailyPracticeBrand(options: Record<string, unknown>): DailyPracticeBrand {
|
||||
const brand = objectValue(options.brand);
|
||||
return {
|
||||
name: stringValue(brand.name, '恭学教育'),
|
||||
english: stringValue(brand.english, 'GONGXUE EDU'),
|
||||
slogan: stringValue(brand.slogan, '专注高职升本'),
|
||||
ctaLine: stringValue(brand.ctaLine, '每日一练 · 精选八题 · 稳步上岸'),
|
||||
};
|
||||
}
|
||||
|
||||
function dailyPracticeMetadata(input: {
|
||||
questions: ReturnType<typeof formatQuestion>[];
|
||||
options: Record<string, unknown>;
|
||||
includeAnswers: boolean;
|
||||
}): DailyPracticeMetadata {
|
||||
const issue = stringValue(input.options.issue, stringValue(input.options.title, '每日一练'));
|
||||
const date = stringValue(input.options.date, todayDateString());
|
||||
const center = objectValue(input.options.centerSlot);
|
||||
const brand = dailyPracticeBrand(input.options);
|
||||
const questionSlots = [0, 1, 2, 3, 5, 6, 7, 8];
|
||||
const slots: DailyPracticeMetadata['slots'] = [];
|
||||
input.questions.slice(0, 8).forEach((question, index) => {
|
||||
slots.push({
|
||||
slot: questionSlots[index],
|
||||
kind: 'question',
|
||||
questionId: question.id,
|
||||
subjectName: question.subjectName,
|
||||
typeLabel: question.typeLabel,
|
||||
order: index + 1,
|
||||
});
|
||||
});
|
||||
slots.splice(Math.min(4, slots.length), 0, {
|
||||
slot: 4,
|
||||
kind: 'center',
|
||||
title: stringValue(center.title, brand.name),
|
||||
subtitle: stringValue(center.subtitle, brand.ctaLine),
|
||||
});
|
||||
return {
|
||||
issue,
|
||||
date,
|
||||
theme: stringValue(input.options.theme, 'default'),
|
||||
cardFormat: stringValue(input.options.cardFormat, '1:1'),
|
||||
brand,
|
||||
showAnswer: input.includeAnswers,
|
||||
centerSlot: {
|
||||
type: stringValue(center.type, 'cta') === 'image' ? 'image' : 'cta',
|
||||
title: stringValue(center.title, brand.name),
|
||||
subtitle: stringValue(center.subtitle, brand.ctaLine),
|
||||
imageUrl: typeof center.imageUrl === 'string' && center.imageUrl.trim() ? center.imageUrl.trim() : null,
|
||||
},
|
||||
slots,
|
||||
};
|
||||
}
|
||||
|
||||
function buildExportPayload(input: {
|
||||
auth: TenantContentAuth;
|
||||
jobId: string;
|
||||
@@ -268,7 +375,23 @@ function buildExportPayload(input: {
|
||||
sectionCount: sections.length,
|
||||
totalScore: sections.reduce((sum, section) => sum + section.totalScore, 0),
|
||||
durationMinutes: input.scope.durationMinutes,
|
||||
...(input.exportType === 'daily_practice'
|
||||
? {
|
||||
cardCount: Math.min(input.questions.length, 8),
|
||||
issue: stringValue(input.options.issue, stringValue(input.options.title, '每日一练')),
|
||||
date: stringValue(input.options.date, todayDateString()),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
...(input.exportType === 'daily_practice'
|
||||
? {
|
||||
dailyPractice: dailyPracticeMetadata({
|
||||
questions: input.questions,
|
||||
options: input.options,
|
||||
includeAnswers: input.includeAnswers,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
sections,
|
||||
questions: input.questions,
|
||||
};
|
||||
@@ -284,7 +407,7 @@ function buildExportPayload(input: {
|
||||
},
|
||||
],
|
||||
renderHints: {
|
||||
pdfLayout: input.exportType === 'paper' ? 'paper' : 'practice',
|
||||
pdfLayout: input.exportType === 'daily_practice' ? 'daily_practice' : input.exportType === 'paper' ? 'paper' : 'practice',
|
||||
pageSize: 'A4',
|
||||
answerPlacement: input.includeAnswers ? 'inline_or_appendix' : 'hidden',
|
||||
frontendRenderer: 'apps/taro admin export renderer',
|
||||
@@ -606,11 +729,16 @@ export async function createQuestionExportRoute(ctx: RequestContext) {
|
||||
const scopeType = choose(body.scopeType, SCOPE_TYPES, 'collection', 'INVALID_EXPORT_SCOPE') as ExportScope['scopeType'];
|
||||
const scopeId = requiredString(body, 'scopeId');
|
||||
const format = choose(body.format, EXPORT_FORMATS, 'json', 'INVALID_EXPORT_FORMAT');
|
||||
const exportType = choose(body.exportType, EXPORT_TYPES, format === 'paper_json' ? 'paper' : 'questions', 'INVALID_EXPORT_TYPE');
|
||||
const exportType = choose(
|
||||
body.exportType,
|
||||
EXPORT_TYPES,
|
||||
format === 'paper_json' || isBinaryExportFormat(format) ? 'paper' : 'questions',
|
||||
'INVALID_EXPORT_TYPE',
|
||||
);
|
||||
const includeAnswers = boolValue(body.includeAnswers, true);
|
||||
const includeExplanations = boolValue(body.includeExplanations, includeAnswers);
|
||||
const includeVideoRefs = boolValue(body.includeVideoRefs, false);
|
||||
const limit = exportLimit(body.limit);
|
||||
const limit = exportLimitForType(exportType, body.limit);
|
||||
const options = body.options && typeof body.options === 'object' && !Array.isArray(body.options) ? body.options as Record<string, unknown> : {};
|
||||
|
||||
const result = await transaction(async client => {
|
||||
|
||||
Reference in New Issue
Block a user