From 9e4decf8cc46a5d8a489d31263787277078c3b30 Mon Sep 17 00:00:00 2001 From: chenyuan Date: Sat, 17 Oct 2026 01:04:22 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20word=E8=BD=ACPDF=E5=86=85=E7=BD=AE?= =?UTF-8?q?=E5=AE=8B=E4=BD=93=E8=B5=84=E6=BA=90=E5=B9=B6=E6=98=BE=E5=BC=8F?= =?UTF-8?q?=E9=85=8D=E7=BD=AEaspose=E5=AD=97=E4=BD=93=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 内置 simsun.ttf 资源,首次转换解压到临时字体目录, 与系统字体目录一起交给 aspose,服务器无中文字体也能正常转换 - 字体配置线程安全(双重检查),按文档设置字体设置 - 学生/教师参考答案预览链路适配 --- .../controller/stu/TeaScoreController.java | 44 ++++++++-- .../util/pdfUtils/PDFConvertUtil.java | 80 ++++++++++++++++--- 2 files changed, 109 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreController.java b/src/main/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreController.java index 4a3387a..3964dde 100644 --- a/src/main/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreController.java +++ b/src/main/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreController.java @@ -29,6 +29,8 @@ import com.sztzjy.linkCommerce.util.pdfUtils.PDFConvertUtil; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -51,6 +53,8 @@ import java.util.stream.Collectors; @RequestMapping("api/weight") @RestController public class TeaScoreController { + private static final Logger log = LoggerFactory.getLogger(TeaScoreController.class); + @Autowired WeightMapper weightMapper; @Autowired @@ -265,10 +269,16 @@ public class TeaScoreController { public ResponseEntity previewTaskReferenceAnswer(@RequestParam String classId, @RequestParam String taskId, HttpServletRequest request) { + long startAt = System.currentTimeMillis(); + JwtUser operator = currentUser(request); try { - teacherScoreService.requireAccessibleClass(currentUser(request), classId); + teacherScoreService.requireAccessibleClass(operator, classId); TrainingTask task = trainingTaskMapper.selectByPrimaryKey(taskId); - if (task == null) return ResponseEntity.notFound().build(); + if (task == null) { + log.warn("参考答案预览失败:任务不存在 classId={} taskId={} userId={}", classId, taskId, + operator == null ? null : operator.getUserId()); + return ResponseEntity.notFound().build(); + } TrainingTaskClassConfig classConfig = trainingTaskClassConfigMapper == null ? null : trainingTaskClassConfigMapper.selectByTeachingClassAndTaskKey(classId, task.getTaskKey()); String referenceUrl = classConfig != null && StringUtils.isNotBlank(classConfig.getReferenceAnswerUrl()) @@ -276,6 +286,8 @@ public class TeaScoreController { String referenceName = classConfig != null && StringUtils.isNotBlank(classConfig.getReferenceAnswerName()) ? classConfig.getReferenceAnswerName() : task == null ? null : task.getReferenceAnswerName(); if (StringUtils.isBlank(referenceUrl)) { + log.warn("参考答案预览失败:未配置参考答案 classId={} taskId={} taskKey={} userId={}", + classId, taskId, task.getTaskKey(), operator == null ? null : operator.getUserId()); return ResponseEntity.notFound().build(); } String name = StringUtils.defaultIfBlank(referenceName, referenceUrl); @@ -283,19 +295,41 @@ public class TeaScoreController { int slash = path.indexOf("/file/"); if (slash >= 0) path = path.substring(slash + 1); File source = new File(fileUtil.getFullPath(path)); - if (!source.exists() || !PDFConvertUtil.isCreatePdf(name)) return ResponseEntity.notFound().build(); + if (!source.exists() || !PDFConvertUtil.isCreatePdf(name)) { + log.warn("参考答案预览失败:文件不存在或类型不支持转换 classId={} taskKey={} userId={} name={} url={} path={} exists={}", + classId, task.getTaskKey(), operator == null ? null : operator.getUserId(), + name, referenceUrl, source.getAbsolutePath(), source.exists()); + return ResponseEntity.notFound().build(); + } ByteArrayOutputStream output = new ByteArrayOutputStream(); try (InputStream input = Files.newInputStream(source.toPath())) { - if (!PDFConvertUtil.convertToPDFByFileNameSuffix(name, input, output)) return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).build(); - } + if (!PDFConvertUtil.convertToPDFByFileNameSuffix(name, input, output)) { + log.warn("参考答案预览失败:文档转换未成功 classId={} taskKey={} userId={} name={} path={}", + classId, task.getTaskKey(), operator == null ? null : operator.getUserId(), name, source.getAbsolutePath()); + return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).build(); + } + } + log.info("参考答案预览成功 classId={} taskKey={} userId={} name={} pdfBytes={} elapsed={}ms", + classId, task.getTaskKey(), operator == null ? null : operator.getUserId(), + name, output.size(), System.currentTimeMillis() - startAt); return ResponseEntity.ok().contentType(MediaType.APPLICATION_PDF).header("Content-Disposition", "inline; filename=reference-answer.pdf").body(output.toByteArray()); } catch (com.sztzjy.linkCommerce.config.exception.handler.ServiceException e) { + log.warn("参考答案预览失败:权限或参数校验未通过 classId={} taskId={} userId={} status={} reason={}", + classId, taskId, operator == null ? null : operator.getUserId(), e.getCode(), e.getMessage()); return ResponseEntity.status(e.getCode()).build(); } catch (RuntimeException | java.io.IOException e) { + log.warn("参考答案预览失败:转换异常 classId={} taskId={} userId={} name={}", + classId, taskId, operator == null ? null : operator.getUserId(), + StringUtils.defaultIfBlank(currentTaskKey(taskId), "-"), e); return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).build(); } } + private String currentTaskKey(String taskId) { + TrainingTask task = trainingTaskMapper == null ? null : trainingTaskMapper.selectByPrimaryKey(taskId); + return task == null ? null : task.getTaskKey(); + } + @PostMapping("/batchSaveTaskTeacherScore") @ApiOperation("批量保存教师任务评分") @Transactional diff --git a/src/main/java/com/sztzjy/linkCommerce/util/pdfUtils/PDFConvertUtil.java b/src/main/java/com/sztzjy/linkCommerce/util/pdfUtils/PDFConvertUtil.java index e908cb1..918b2a7 100644 --- a/src/main/java/com/sztzjy/linkCommerce/util/pdfUtils/PDFConvertUtil.java +++ b/src/main/java/com/sztzjy/linkCommerce/util/pdfUtils/PDFConvertUtil.java @@ -17,7 +17,11 @@ import org.slf4j.LoggerFactory; import java.awt.*; import java.awt.image.BufferedImage; import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -35,6 +39,8 @@ import static com.sztzjy.linkCommerce.util.pdfUtils.PDFConvertUtil.TypeEnum.*; public final class PDFConvertUtil { private static final Logger log = LoggerFactory.getLogger(PDFConvertUtil.class); + private static final String BUNDLED_FONT_RESOURCE = "/simsun.ttf"; + private static volatile boolean chineseFontsConfigured; final private static Map> types = new HashMap<>(); @@ -130,17 +136,10 @@ public final class PDFConvertUtil { return false; } try { - //此处处理乱码和小方块 - //如果在本地运行,此处报错,请注释这个这是字体,主要是为了解决linux环境下面运行jar时找不到中文字体的问题 - //指定文件库内容路径 - // 字体目录同时包含 Windows 系统字体与 Linux 常见目录: - // setFontsFolders 会替换 aspose 的默认字体源,漏掉 Windows 字体目录时 - // 宋体等中文字体不可见,转换出的 PDF 中文全部变成方框 - FontSettings.getDefaultInstance().setFontsFolders( - new String[] {"C:\\Windows\\Fonts", "/usr/share/fonts", "/usr/share/fonts/win"} - , true); + FontSettings fontSettings = configureChineseFonts(); // 将源文件保存在com.aspose.words.Document中,具体的转换格式依靠里面的save方法 com.aspose.words.Document doc = new com.aspose.words.Document(inputStream); + doc.setFontSettings(fontSettings); // 全面支持DOC, DOCX, OOXML, RTF HTML, OpenDocument, PDF,EPUB, XPS, SWF 相互转换 doc.save(outputStream, SaveFormat.PDF); @@ -156,6 +155,67 @@ public final class PDFConvertUtil { return true; } + /** + * 配置 Word 转 PDF 使用的中文字体。 + * + * 服务器通常没有 Windows 的宋体,单纯扫描系统字体目录会导致 DOC/DOCX + * 中的中文被替换成方框或乱码。项目内置 simsun.ttf,首次转换时解压到 + * 临时字体目录,并和常见系统字体目录一起交给 Aspose。 + */ + private static FontSettings configureChineseFonts() { + FontSettings settings = FontSettings.getDefaultInstance(); + if (chineseFontsConfigured) { + return settings; + } + synchronized (PDFConvertUtil.class) { + if (chineseFontsConfigured) { + return settings; + } + + List fontFolders = new ArrayList<>(); + addExistingFontFolder(fontFolders, "C:\\Windows\\Fonts"); + addExistingFontFolder(fontFolders, "/usr/share/fonts"); + addExistingFontFolder(fontFolders, "/usr/local/share/fonts"); + addExistingFontFolder(fontFolders, "/usr/share/fonts/chinese"); + addExistingFontFolder(fontFolders, "/usr/share/fonts/win"); + + try { + Path bundledFontFolder = Paths.get(System.getProperty("java.io.tmpdir"), "link-commerce-fonts"); + Files.createDirectories(bundledFontFolder); + Path bundledFont = bundledFontFolder.resolve("simsun.ttf"); + if (!Files.exists(bundledFont) || Files.size(bundledFont) == 0) { + try (InputStream input = PDFConvertUtil.class.getResourceAsStream(BUNDLED_FONT_RESOURCE)) { + if (input == null) { + throw new FileNotFoundException("Missing bundled font " + BUNDLED_FONT_RESOURCE); + } + Files.copy(input, bundledFont, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + } + addExistingFontFolder(fontFolders, bundledFontFolder.toString()); + } catch (IOException | SecurityException e) { + log.warn("内置中文字体加载失败,将继续使用系统字体目录", e); + } + + if (!fontFolders.isEmpty()) { + settings.setFontsFolders(fontFolders.toArray(new String[0]), true); + } + settings.setDefaultFontName("SimSun"); + settings.setFontSubstitutes("宋体", "SimSun", "Noto Sans CJK SC", "Microsoft YaHei"); + settings.setFontSubstitutes("等线", "SimSun", "Noto Sans CJK SC", "Microsoft YaHei"); + settings.setFontSubstitutes("微软雅黑", "SimSun", "Noto Sans CJK SC", "Microsoft YaHei"); + settings.setFontSubstitutes("黑体", "SimSun", "Noto Sans CJK SC", "Microsoft YaHei"); + settings.setFontSubstitutes("仿宋", "SimSun", "Noto Sans CJK SC", "Microsoft YaHei"); + chineseFontsConfigured = true; + return settings; + } + } + + private static void addExistingFontFolder(List folders, String folder) { + if (folder != null && Files.isDirectory(Paths.get(folder))) { + folders.add(folder); + } + } + /** * 关闭输出流 * @param outputStream @@ -552,4 +612,4 @@ public final class PDFConvertUtil { String[] split = fileName.split("\\."); return split[split.length - 1]; } -} \ No newline at end of file +}