From 7d0a559a8426f8c3b056bfacffaf6ce83f99f2e2 Mon Sep 17 00:00:00 2001 From: luotaiqian <1147642922@qq.com> Date: Thu, 20 Aug 2026 11:33:27 +0800 Subject: [PATCH] PaperBasicInfo --- safety-cedu-service/pom.xml | 7 ++ .../cedu/controller/PaperController.java | 26 ++++++ .../org/qinan/cedu/service/PaperService.java | 8 ++ .../cedu/service/impl/PaperServiceImpl.java | 81 +++++++++++++++++++ 4 files changed, 122 insertions(+) diff --git a/safety-cedu-service/pom.xml b/safety-cedu-service/pom.xml index 9da2609a..00e5ad1a 100644 --- a/safety-cedu-service/pom.xml +++ b/safety-cedu-service/pom.xml @@ -43,6 +43,13 @@ easyexcel + + + com.github.librepdf + openpdf + 1.3.34 + + org.springframework.boot diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/controller/PaperController.java b/safety-cedu-service/src/main/java/org/qinan/cedu/controller/PaperController.java index 3b43b48a..d791468c 100644 --- a/safety-cedu-service/src/main/java/org/qinan/cedu/controller/PaperController.java +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/controller/PaperController.java @@ -11,9 +11,16 @@ import org.qinan.cedu.service.PaperService; import org.qinan.cedu.service.QuestionService; import org.qinan.safetyeval.client.dto.PageResponse; import org.qinan.safetyeval.client.dto.SingleResponse; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + /** * 试卷管理 */ @@ -85,4 +92,23 @@ public class PaperController { PaperExamInfoVO pageInfo = paperService.paperExamInfo(paperId); return SingleResponse.success(pageInfo); } + + /** + * 下载试卷PDF:查询试卷考试信息(同paperExamInfo)后渲染为PDF文件流,浏览器直接访问即触发下载 + * + * @param paperId 试卷id + * @return PDF文件字节流(文件名为试卷名称) + */ + @ApiOperation("下载试卷PDF") + @GetMapping(value = "/paper/exam/download", produces = MediaType.APPLICATION_PDF_VALUE) + public ResponseEntity downloadPaperPdf(@RequestParam("paperId") @ApiParam(value = "试卷id", required = true) Long paperId) throws IOException { + PaperExamInfoVO examInfo = paperService.paperExamInfo(paperId); + byte[] pdfBytes = paperService.renderExamInfoPdf(examInfo); + // RFC5987编码文件名,避免中文试卷名称乱码 + String fileName = URLEncoder.encode(examInfo.getPaperBasicInfoVO().getPaperName() + ".pdf", StandardCharsets.UTF_8.name()); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename*=UTF-8''" + fileName) + .contentType(MediaType.APPLICATION_PDF) + .body(pdfBytes); + } } diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/service/PaperService.java b/safety-cedu-service/src/main/java/org/qinan/cedu/service/PaperService.java index eeb0560b..599f2fad 100644 --- a/safety-cedu-service/src/main/java/org/qinan/cedu/service/PaperService.java +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/service/PaperService.java @@ -107,4 +107,12 @@ public interface PaperService extends IService { * @return 试卷考试信息 */ PaperExamInfoVO paperExamInfo(Long paperId); + + /** + * 将试卷考试信息渲染为PDF字节数组(用于试卷下载) + * + * @param examInfo 试卷考试信息(试卷基础信息+试题列表含选项) + * @return PDF文件字节数组 + */ + byte[] renderExamInfoPdf(PaperExamInfoVO examInfo); } diff --git a/safety-cedu-service/src/main/java/org/qinan/cedu/service/impl/PaperServiceImpl.java b/safety-cedu-service/src/main/java/org/qinan/cedu/service/impl/PaperServiceImpl.java index 7739c14f..6d12c1be 100644 --- a/safety-cedu-service/src/main/java/org/qinan/cedu/service/impl/PaperServiceImpl.java +++ b/safety-cedu-service/src/main/java/org/qinan/cedu/service/impl/PaperServiceImpl.java @@ -8,6 +8,14 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.toolkit.Db; +import com.lowagie.text.Document; +import com.lowagie.text.DocumentException; +import com.lowagie.text.Element; +import com.lowagie.text.Font; +import com.lowagie.text.PageSize; +import com.lowagie.text.Paragraph; +import com.lowagie.text.pdf.BaseFont; +import com.lowagie.text.pdf.PdfWriter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.qinan.cedu.convertor.PaperConvertor; @@ -45,6 +53,8 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -372,4 +382,75 @@ public class PaperServiceImpl extends ServiceImpl implemen questionVOList.forEach(question -> question.setQuestionOptionVOList( optionMap.getOrDefault(Long.valueOf(question.getId()), new ArrayList<>()))); } + + @Override + public byte[] renderExamInfoPdf(PaperExamInfoVO examInfo) { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + Document document = new Document(PageSize.A4, 50, 50, 50, 50); + PdfWriter.getInstance(document, out); + document.open(); + // STSong-Light 为阅读器内置中文字体(cmaps已内置于openpdf jar),无需嵌入字体文件 + BaseFont baseFont = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED); + addPaperPdfHeader(document, baseFont, examInfo.getPaperBasicInfoVO()); + if (!CollectionUtils.isEmpty(examInfo.getQuestionVOList())) { + int questionNum = 1; + for (QuestionVO question : examInfo.getQuestionVOList()) { + addPdfQuestion(document, baseFont, questionNum++, question); + } + } + document.close(); + return out.toByteArray(); + } catch (DocumentException | IOException e) { + throw new IllegalStateException("试卷PDF渲染失败", e); + } + } + + /** + * 渲染试卷PDF头部:居中试卷名称 + 居中满分行 + * + * @param document PDF文档 + * @param baseFont 中文字体 + * @param basicInfo 试卷基础信息 + * @throws DocumentException 渲染异常 + */ + private void addPaperPdfHeader(Document document, BaseFont baseFont, PaperBasicInfoVO basicInfo) throws DocumentException { + Paragraph title = new Paragraph(basicInfo.getPaperName(), new Font(baseFont, 18, Font.BOLD)); + title.setAlignment(Element.ALIGN_CENTER); + title.setSpacingAfter(8f); + document.add(title); + String totalScore = basicInfo.getPaperTotalScore() == null ? "0" + : basicInfo.getPaperTotalScore().stripTrailingZeros().toPlainString(); + Paragraph scoreLine = new Paragraph("(满分:" + totalScore + " 分)", new Font(baseFont, 12, Font.NORMAL)); + scoreLine.setAlignment(Element.ALIGN_CENTER); + scoreLine.setSpacingAfter(10f); + document.add(scoreLine); + } + + /** + * 渲染单道试题:题干(题型+序号+分值)及其选项列表 + * + * @param document PDF文档 + * @param baseFont 中文字体 + * @param questionNum 题目序号(从1开始) + * @param question 试题信息 + * @throws DocumentException 渲染异常 + */ + private void addPdfQuestion(Document document, BaseFont baseFont, int questionNum, QuestionVO question) throws DocumentException { + QuestionTypeEnum typeEnum = QuestionTypeEnum.fromCode(question.getQuestionType()); + String typeLabel = typeEnum == null ? question.getQuestionType() : typeEnum.getDesc() + "题"; + String score = question.getScore() == null ? "0" : question.getScore().toPlainString(); + Paragraph questionLine = new Paragraph("(" + typeLabel + ")" + questionNum + "." + question.getTitle() + + "(题目分值:" + score + ")", new Font(baseFont, 10, Font.NORMAL)); + questionLine.setSpacingBefore(8f); + document.add(questionLine); + if (CollectionUtils.isEmpty(question.getQuestionOptionVOList())) { + return; + } + for (QuestionOptionVO option : question.getQuestionOptionVOList()) { + Paragraph optionLine = new Paragraph(option.getOptionKey() + ":" + option.getOptionText(), + new Font(baseFont, 10, Font.NORMAL)); + optionLine.setIndentationLeft(20f); + document.add(optionLine); + } + } }