From c7867f3030291bc658228626e84d1d2ee9a8d371 Mon Sep 17 00:00:00 2001
From: hz <1283027134@qq.com>
Date: Mon, 31 Aug 2026 14:06:52 +0800
Subject: [PATCH] =?UTF-8?q?feat:=E6=89=93=E5=8D=B0=E5=8A=9F=E8=83=BD?=
=?UTF-8?q?=EF=BC=8Cword=E8=BD=ACpdf?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
common/common-association/pom.xml | 8 +-
.../controller/QuotationPdfController.java | 83 +++
.../plugin/quotation/OpenPopupPlugin.java | 76 +++
.../association/service/QuotationPdfService.java | 22 +
.../service/impl/QuotationPdfServiceImpl.java | 568 +++++++++++++++++++++
5 files changed, 756 insertions(+), 1 deletion(-)
create mode 100644 common/common-association/src/main/java/apelet/association/controller/QuotationPdfController.java
create mode 100644 common/common-association/src/main/java/apelet/association/service/QuotationPdfService.java
create mode 100644 common/common-association/src/main/java/apelet/association/service/impl/QuotationPdfServiceImpl.java
diff --git a/common/common-association/pom.xml b/common/common-association/pom.xml
index 9e7b096..26e4512 100644
--- a/common/common-association/pom.xml
+++ b/common/common-association/pom.xml
@@ -74,7 +74,13 @@
poi-tl
1.12.2
-
+
+
+ e-iceblue
+ spire.doc.free
+ 14.3.1
+
+
diff --git a/common/common-association/src/main/java/apelet/association/controller/QuotationPdfController.java b/common/common-association/src/main/java/apelet/association/controller/QuotationPdfController.java
new file mode 100644
index 0000000..e0c5c3b
--- /dev/null
+++ b/common/common-association/src/main/java/apelet/association/controller/QuotationPdfController.java
@@ -0,0 +1,83 @@
+package apelet.association.controller;
+
+import apelet.association.service.QuotationPdfService;
+import apelet.common.core.exception.MyRuntimeException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 报价单 Word→PDF 下载接口。
+ *
+ *
触发链路:报价单列表页"打印PDF"按钮 → QuotationPdfPlugin 取当前行 id、
+ * 拼绝对下载地址 → 前端收到 DOWNLOAD_URL 事件跳转下载 → 本接口。
+ *
+ * 本类只做 HTTP 层:调 QuotationPdfService 拿 PDF 字节,设置响应头后写出。
+ * 业务逻辑(取附件模板→POI 渲染→LibreOffice 转 PDF)全部在 QuotationPdfService。
+ *
+ * 请求参数:
+ *
+ *
+ * @author hz
+ * @date 2026-08-19
+ */
+@RestController
+@RequestMapping("/api/quotationPdf")
+public class QuotationPdfController {
+
+ @Autowired
+ private QuotationPdfService quotationPdfService;
+
+ /**
+ * 下载报价单 PDF。
+ *
+ * 返回 application/pdf 附件流;业务或转换失败时抛 MyRuntimeException,
+ * 由全局异常处理器转成友好错误 JSON(不 500)。
+ */
+ @GetMapping("/downloadPdf")
+ public void downloadPdf(@RequestParam("id") String quotationId,
+ HttpServletResponse response) {
+ // 1. 生成 PDF 字节(查数据 → 取附件模板 → 渲染 → LibreOffice 转 PDF)
+ byte[] pdfBytes;
+ try {
+ pdfBytes = quotationPdfService.downloadPdf(quotationId);
+ } catch (MyRuntimeException e) {
+ // 业务异常(无附件/报价单不存在等):原样抛出,交给全局异常处理
+ throw e;
+ } catch (Exception e) {
+ // 其余异常:包一层,避免把内部堆栈直接抛给前端
+ throw new MyRuntimeException("导出失败: " + e.getMessage());
+ }
+
+ // 2. 设置响应头,输出 PDF 附件
+ try {
+ response.setContentType("application/pdf");
+ response.setCharacterEncoding("UTF-8");
+ // 中文文件名:URLEncoder 编码后走 RFC 5987 的 filename*=UTF-8'',兼容现代浏览器;
+ // 普通 filename 用 %20 代替空格,防止部分浏览器解析异常
+ String fileName = URLEncoder.encode("报价单.pdf", StandardCharsets.UTF_8.name())
+ .replaceAll("\\+", "%20");
+ response.setHeader("Content-Disposition",
+ "attachment; filename=\"" + fileName + "\"; filename*=UTF-8''" + fileName);
+ // 禁止浏览器缓存:每次打开都是最新 PDF,避免下载到旧的
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Cache-Control", "no-cache");
+ response.setDateHeader("Expires", 0);
+ OutputStream out = response.getOutputStream();
+ out.write(pdfBytes);
+ out.flush();
+ } catch (IOException e) {
+ throw new MyRuntimeException("输出 PDF 失败: " + e.getMessage());
+ }
+ }
+}
diff --git a/common/common-association/src/main/java/apelet/association/plugin/quotation/OpenPopupPlugin.java b/common/common-association/src/main/java/apelet/association/plugin/quotation/OpenPopupPlugin.java
index 8fb5886..fdc6325 100644
--- a/common/common-association/src/main/java/apelet/association/plugin/quotation/OpenPopupPlugin.java
+++ b/common/common-association/src/main/java/apelet/association/plugin/quotation/OpenPopupPlugin.java
@@ -4,10 +4,15 @@ package apelet.association.plugin.quotation;
import apelet.common.core.object.ObjectValue;
import apelet.common.online.abstractplugin.ListPlugin;
import apelet.common.online.model.ShowParameter;
+import apelet.common.online.model.constant.AttributeEnum;
import apelet.common.online.model.constant.ShowTypeEnum;
import apelet.common.online.model.constant.ViewStatus;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
/*
@@ -49,5 +54,76 @@ public class OpenPopupPlugin extends ListPlugin {
showParameter.setCustomParam(map);
super.showForm(showParameter);
}
+
+ if ("打印".equals(widgetVariableName)) {
+ // 1. 取当前行:优先从框架 rowData 穿透取(ReportListPlugin 同款);
+ // 拿不到再回退按钮事件传入的对象
+ Map row = getCurrentRow();
+ if ((row == null || row.get("id") == null) && objectValue != null) {
+ row = objectValue.getValues();
+ }
+ if (row == null || row.get("id") == null) {
+ this.showWarningMessage("请选择一条数据");
+ this.cancelOperate();
+ return;
+ }
+
+ // 2. 拼下载地址(优先绝对地址 scheme://host:port/contextPath;拿不到请求时退回相对路径)
+ // 签名图由报价单 managerperson 决定,不依赖当前登录用户
+ String url = buildUrl(String.valueOf(row.get("id")));
+
+ // 3. 事件:{url, type:"download"} → 前端下载该 URL
+ Map map = new HashMap<>();
+ map.put("url", url);
+ map.put("type", "download");
+ super.setWidgetAttribute("", AttributeEnum.DOWNLOAD_URL, map);
+ this.cancelOperate();
+ }
}
+
+ private String buildUrl(String quotationId) {
+ String base = buildBaseUrl();
+ return base + "/api/quotationPdf/downloadPdf?id=" + quotationId;
+ }
+
+ /**
+ * 取当前行数据。rowData 结构为 {@code {tableWidgetId: [行Map]}},
+ * 穿透外层 Map 找到第一个"值为 List