P0-1/P0-2/P1-1: dimensions form + E2E tests + PDF export

P0-1 Backend: dimensions column on template entity + validation
P0-1 Frontend: dimensions edit UI in TemplateManager
P0-2: routeAfterGrading unit tests (10 cases), service spec fix + certificate tests, jest-e2e.json
P1-1: proper PDF generation with embedded CJK font via pdf-lib low-level API
This commit is contained in:
Developer
2026-05-19 08:42:03 +08:00
parent 0b0a060967
commit 68371922ca
18 changed files with 663 additions and 72 deletions
@@ -6,6 +6,7 @@ import { AssessmentSession } from '../entities/assessment-session.entity';
import { AssessmentQuestion } from '../entities/assessment-question.entity';
import { AssessmentAnswer } from '../entities/assessment-answer.entity';
import { AssessmentCertificate } from '../entities/assessment-certificate.entity';
import { generateAssessmentPdf } from './pdf-generator';
@Injectable()
export class ExportService {
@@ -155,73 +156,55 @@ export class ExportService {
where: { questionId: In(questions.map((q) => q.id)) },
});
const content = this.generatePdfContent(session, questions, answers, certificate);
return Buffer.from(content, 'utf-8');
}
private generatePdfContent(
session: AssessmentSession,
questions: AssessmentQuestion[],
answers: AssessmentAnswer[],
certificate: AssessmentCertificate | null,
): string {
const lines: string[] = [];
lines.push('='.repeat(60));
lines.push(' 人才评估报告');
lines.push('='.repeat(60));
lines.push('');
lines.push(`评估ID: ${session.id}`);
lines.push(`用户: ${session.user?.displayName || session.user?.username || session.userId}`);
lines.push(`状态: ${session.status === 'COMPLETED' ? '已完成' : '进行中'}`);
lines.push(`最终分数: ${session.finalScore || '-'}`);
lines.push(`评估模板: ${session.template?.name || session.templateJson?.name || '-'}`);
lines.push(`评估时间: ${session.startedAt ? new Date(session.startedAt).toLocaleString() : '-'}`);
lines.push('');
if (certificate) {
lines.push('-'.repeat(60));
lines.push('证书信息');
lines.push('-'.repeat(60));
lines.push(`等级: ${certificate.level}`);
lines.push(`总分: ${certificate.totalScore}`);
lines.push(`是否通过: ${certificate.passed ? '是' : '否'}`);
lines.push(`颁发时间: ${certificate.issuedAt ? new Date(certificate.issuedAt).toLocaleString() : '-'}`);
lines.push('');
}
lines.push('-'.repeat(60));
lines.push('题目详情');
lines.push('-'.repeat(60));
const answerMap = new Map(answers.map((a) => [a.questionId, a]));
const detailLines: string[] = [];
for (let i = 0; i < questions.length; i++) {
const q = questions[i];
const a = answerMap.get(q.id);
lines.push('');
lines.push(`${i + 1}题:`);
lines.push(` 题目: ${q.questionText || '-'}`);
lines.push(` 用户回答: ${a?.userAnswer || '-'}`);
lines.push(` 得分: ${a?.score ?? '-'}`);
lines.push(` 反馈: ${a?.feedback || '-'}`);
lines.push(` 追问: ${a?.isFollowUp ? '是' : '否'}`);
detailLines.push(`Q${i + 1}: ${q.questionText || '-'}`);
detailLines.push(` A: ${a?.userAnswer || '-'}`);
detailLines.push(` Score: ${a?.score ?? '-'}`);
detailLines.push(` Feedback: ${a?.feedback || '-'}`);
}
if (session.finalReport) {
lines.push('');
lines.push('-'.repeat(60));
lines.push('综合评估报告');
lines.push('-'.repeat(60));
lines.push(session.finalReport);
const certificateLines: string[] = [];
if (certificate) {
certificateLines.push(`Level: ${certificate.level}`);
certificateLines.push(`Total Score: ${certificate.totalScore}`);
certificateLines.push(`Passed: ${certificate.passed ? 'Yes' : 'No'}`);
certificateLines.push(`Issued: ${certificate.issuedAt ? new Date(certificate.issuedAt).toLocaleString() : '-'}`);
}
lines.push('');
lines.push('='.repeat(60));
lines.push(' 报告结束');
lines.push('='.repeat(60));
const userName = session.user?.displayName || session.user?.username || session.userId;
return lines.join('\n');
return generateAssessmentPdf({
title: 'Assessment Report',
subtitle: `${userName}${new Date(session.createdAt).toLocaleDateString()}`,
sections: [
{
title: 'Summary',
lines: [
`Session: ${session.id}`,
`User: ${userName}`,
`Status: ${session.status}`,
`Score: ${session.finalScore ?? '-'}`,
`Template: ${session.template?.name || session.templateJson?.name || '-'}`,
],
},
...(certificate ? [{
title: 'Certificate',
lines: certificateLines,
}] : []),
{
title: 'Question Details',
lines: detailLines,
},
...(session.finalReport ? [{
title: 'Final Report',
lines: [session.finalReport],
}] : []),
],
});
}
}