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, @InjectRepository(AssessmentQuestion) private questionRepository: Repository, @InjectRepository(AssessmentAnswer) private answerRepository: Repository, @InjectRepository(AssessmentCertificate) private certificateRepository: Repository, ) {} async exportToExcel(sessionId: string): Promise { 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 { 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 += `${dim}${score}/10`; } let qRows = ''; questions.forEach((q: any, i: number) => { qRows += `${i + 1}${(q.questionText || '').substring(0, 80)}${q.questionType || '-'}${q.dimension || '-'}`; }); const html = `Assessment Report

Assessment Report

${userName} — ${new Date(session.createdAt).toLocaleDateString()}

Template: ${templateName}

Result

${session.finalScore ?? '-'}/10

${(session as any).passed ? 'PASSED' : 'FAILED'}

${cert ? `

Level: ${cert.level}

` : ''}

Dimension Scores

${dimRows}

Questions

${qRows}
#QuestionTypeDimension
${session.finalReport ? `

Mastery Report

${session.finalReport}
` : ''} `; return Buffer.from(html, 'utf-8'); } }