189 lines
6.7 KiB
TypeScript
189 lines
6.7 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { Repository, In } from 'typeorm';
|
|
import * as XLSX from 'xlsx';
|
|
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 {
|
|
private readonly logger = new Logger(ExportService.name);
|
|
|
|
constructor(
|
|
@InjectRepository(AssessmentSession)
|
|
private sessionRepository: Repository<AssessmentSession>,
|
|
@InjectRepository(AssessmentQuestion)
|
|
private questionRepository: Repository<AssessmentQuestion>,
|
|
@InjectRepository(AssessmentAnswer)
|
|
private answerRepository: Repository<AssessmentAnswer>,
|
|
@InjectRepository(AssessmentCertificate)
|
|
private certificateRepository: Repository<AssessmentCertificate>,
|
|
) {}
|
|
|
|
async exportToExcel(sessionId: string): Promise<Buffer> {
|
|
const session = await this.sessionRepository.findOne({
|
|
where: { id: sessionId },
|
|
relations: ['template', 'user'],
|
|
});
|
|
|
|
if (!session) {
|
|
throw new Error('Session not found');
|
|
}
|
|
|
|
const questions = await this.questionRepository.find({
|
|
where: { sessionId },
|
|
order: { createdAt: 'ASC' },
|
|
});
|
|
|
|
const answers = await this.answerRepository.find({
|
|
where: { questionId: In(questions.map((q) => q.id)) },
|
|
});
|
|
|
|
const answerMap = new Map(answers.map((a) => [a.questionId, a]));
|
|
|
|
const summarySheet = [
|
|
['评估报告'],
|
|
['评估ID', session.id],
|
|
['用户', session.user?.displayName || session.user?.username || session.userId],
|
|
['状态', session.status],
|
|
['最终分数', session.finalScore || '-'],
|
|
['原始分数', session.originalScore || '-'],
|
|
['评估模板', session.template?.name || session.templateJson?.name || '-'],
|
|
['开始时间', session.startedAt ? new Date(session.startedAt).toLocaleString() : '-'],
|
|
['完成时间', session.updatedAt ? new Date(session.updatedAt).toLocaleString() : '-'],
|
|
['总用时(秒)', session.totalTimeLimit],
|
|
[''],
|
|
['维度分数'],
|
|
...this.extractDimensionScores(session),
|
|
];
|
|
|
|
const questionRows = [
|
|
['题号', '题目内容', '用户回答', '分数', '反馈', '是否追问'],
|
|
];
|
|
|
|
for (let i = 0; i < questions.length; i++) {
|
|
const q = questions[i];
|
|
const a = answerMap.get(q.id);
|
|
questionRows.push([
|
|
(i + 1).toString(),
|
|
q.questionText || '',
|
|
a?.userAnswer || '',
|
|
a?.score?.toString() || '',
|
|
a?.feedback || '',
|
|
a?.isFollowUp ? '是' : '否',
|
|
]);
|
|
}
|
|
|
|
const wb = XLSX.utils.book_new();
|
|
|
|
const summaryWs = XLSX.utils.aoa_to_sheet(summarySheet);
|
|
XLSX.utils.book_append_sheet(wb, summaryWs, '摘要');
|
|
|
|
const questionWs = XLSX.utils.aoa_to_sheet(questionRows);
|
|
XLSX.utils.book_append_sheet(wb, questionWs, '题目详情');
|
|
|
|
if (session.finalReport) {
|
|
const reportSheet = this.wrapTextToLines(session.finalReport, 80);
|
|
const reportWs = XLSX.utils.aoa_to_sheet(reportSheet);
|
|
XLSX.utils.book_append_sheet(wb, reportWs, '评估报告');
|
|
}
|
|
|
|
const buffer = XLSX.write(wb, { bookType: 'xlsx', type: 'buffer' });
|
|
return Buffer.from(buffer);
|
|
}
|
|
|
|
private extractDimensionScores(session: AssessmentSession): any[][] {
|
|
const scores = (session as any).dimensionScores;
|
|
if (!scores) return [['未找到维度分数']];
|
|
|
|
if (typeof scores === 'string') {
|
|
return [['维度分数', scores]];
|
|
}
|
|
|
|
return Object.entries(scores).map(([key, value]) => [key, value]);
|
|
}
|
|
|
|
private wrapTextToLines(text: string, maxWidth: number): string[][] {
|
|
const lines: string[] = [];
|
|
const paragraphs = text.split('\n');
|
|
|
|
for (const paragraph of paragraphs) {
|
|
if (paragraph.trim() === '') {
|
|
lines.push('');
|
|
continue;
|
|
}
|
|
|
|
const words = paragraph.split(' ');
|
|
let currentLine = '';
|
|
|
|
for (const word of words) {
|
|
if ((currentLine + ' ' + word).trim().length <= maxWidth) {
|
|
currentLine = (currentLine + ' ' + word).trim();
|
|
} else {
|
|
if (currentLine) lines.push(currentLine);
|
|
currentLine = word;
|
|
}
|
|
}
|
|
if (currentLine) lines.push(currentLine);
|
|
}
|
|
|
|
return lines.map((l) => [l]);
|
|
}
|
|
|
|
async exportToPdf(sessionId: string): Promise<Buffer> {
|
|
const session = await this.sessionRepository.findOne({
|
|
where: { id: sessionId },
|
|
relations: ['template', 'user'],
|
|
});
|
|
|
|
if (!session) {
|
|
throw new Error('Session not found');
|
|
}
|
|
|
|
const cert = await this.certificateRepository.findOne({
|
|
where: { sessionId },
|
|
});
|
|
|
|
const questions = (session.questions_json || []) as any[];
|
|
|
|
const userName = session.user?.displayName || session.user?.username || session.userId;
|
|
const templateName = session.template?.name || session.templateJson?.name || '-';
|
|
const dimensionScores = (session as any).dimensionScores || {};
|
|
|
|
let dimRows = '';
|
|
for (const [dim, score] of Object.entries(dimensionScores)) {
|
|
dimRows += `<tr><td>${dim}</td><td>${score}/10</td></tr>`;
|
|
}
|
|
|
|
let qRows = '';
|
|
questions.forEach((q: any, i: number) => {
|
|
qRows += `<tr><td>${i + 1}</td><td>${(q.questionText || '').substring(0, 80)}</td><td>${q.questionType || '-'}</td><td>${q.dimension || '-'}</td></tr>`;
|
|
});
|
|
|
|
const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Assessment Report</title>
|
|
<style>body{font-family:'Microsoft YaHei',sans-serif;max-width:800px;margin:40px auto;color:#333}
|
|
h1{font-size:24px}h2{font-size:18px;border-bottom:2px solid #4F46E5;padding-bottom:8px}
|
|
table{width:100%;border-collapse:collapse;margin:16px 0}
|
|
td,th{border:1px solid #ddd;padding:8px;text-align:left}
|
|
th{background:#F3F4F6}.score{font-size:36px;font-weight:bold;color:#4F46E5}
|
|
.pass{color:#059669}.fail{color:#DC2626}</style></head><body>
|
|
<h1>Assessment Report</h1>
|
|
<p>${userName} — ${new Date(session.createdAt).toLocaleDateString()}</p>
|
|
<p>Template: ${templateName}</p>
|
|
<h2>Result</h2>
|
|
<p class="score">${session.finalScore ?? '-'}/10</p>
|
|
<p class="${(session as any).passed ? 'pass' : 'fail'}">${(session as any).passed ? 'PASSED' : 'FAILED'}</p>
|
|
${cert ? `<p>Level: ${cert.level}</p>` : ''}
|
|
<h2>Dimension Scores</h2>
|
|
<table>${dimRows}</table>
|
|
<h2>Questions</h2>
|
|
<table><tr><th>#</th><th>Question</th><th>Type</th><th>Dimension</th></tr>${qRows}</table>
|
|
${session.finalReport ? `<h2>Mastery Report</h2><pre>${session.finalReport}</pre>` : ''}
|
|
</body></html>`;
|
|
|
|
return Buffer.from(html, 'utf-8');
|
|
}
|
|
} |