From f32ff8cb6bd0d07a35f8db3a88c826936db647c0 Mon Sep 17 00:00:00 2001 From: hayu <1604366271@qq.com> Date: Mon, 15 Sep 2025 19:19:37 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../material/common/utils/WordParserUtil.java | 499 ++++++++++++++++++ .../controller/ProAuthorizeController.java | 35 ++ .../service/ProAuthorizeService.java | 7 + .../service/impl/ProAuthorizeServiceImpl.java | 39 +- .../resources/template/授权委托书模板.doc | Bin 0 -> 23133 bytes 5 files changed, 576 insertions(+), 4 deletions(-) create mode 100644 bonus-modules/bonus-material/src/main/java/com/bonus/material/common/utils/WordParserUtil.java create mode 100644 bonus-modules/bonus-material/src/main/resources/template/授权委托书模板.doc diff --git a/bonus-modules/bonus-material/src/main/java/com/bonus/material/common/utils/WordParserUtil.java b/bonus-modules/bonus-material/src/main/java/com/bonus/material/common/utils/WordParserUtil.java new file mode 100644 index 00000000..ae0cd958 --- /dev/null +++ b/bonus-modules/bonus-material/src/main/java/com/bonus/material/common/utils/WordParserUtil.java @@ -0,0 +1,499 @@ +package com.bonus.material.common.utils; + +import com.bonus.common.core.web.domain.AjaxResult; +import com.bonus.material.materialStation.domain.ProAuthorizeDetails; +import com.bonus.system.api.RemoteFileService; +import org.apache.poi.hwpf.HWPFDocument; +import org.apache.poi.hwpf.usermodel.Picture; +import org.apache.poi.openxml4j.opc.OPCPackage; +import org.apache.poi.poifs.filesystem.POIFSFileSystem; +import org.apache.poi.xwpf.usermodel.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import java.io.*; +import java.net.URL; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.file.*; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Word 解析工具类(兼容本地路径与 URL,支持 doc/docx 文字与图片提取) + * 依赖:Apache POI (poi-ooxml, poi-scratchpad) + * + * 使用: + * List list = WordParserUtil.parseWordFile(multipartFile, filePathUrlOrLocalPath, sysFileService); + * + * 注意:sysFileService.upload(...) 应返回 AjaxResult 或类似结构,且包含 data.url 字段(你项目的上传逻辑)。 + */ +public class WordParserUtil { + + private static final Logger logger = LoggerFactory.getLogger(WordParserUtil.class); + + // 当 filePath 为 URL 时,先下载到本地临时文件再解析 + private static final String TEMP_PREFIX = "word_parse_"; + + // 身份证号正则(15 或 18,最后一位可能是 Xx) + private static final Pattern ID_PATTERN = Pattern.compile("(\\d{15}|\\d{17}[0-9Xx])"); + + /** + * 统一入口 + * + * @param file 前端上传的 MultipartFile(原始文件) + * @param filePath sysFileService.upload 返回的文件访问路径(可以是 http(s) URL 或 本地路径) + * @param sysFileService 你项目的文件上传服务实例(用于上传解析出的图片) + */ + public static Map parseWordFile(MultipartFile file,String fileName, String filePath, RemoteFileService sysFileService) throws Exception { + Map result = new HashMap<>(); + + List receivers = new ArrayList<>(); + try { + String originalName = file.getOriginalFilename(); + if (originalName == null) { + originalName = "upload.docx"; + } + + // 根据扩展名选择解析 + String lower = originalName.toLowerCase(); + File localFile; + if (filePath.startsWith("http://") || filePath.startsWith("https://")) { + localFile = downloadToLocal(filePath); + } else { + Path p = Paths.get(filePath); + if (Files.exists(p)) { + localFile = p.toFile(); + } else { + localFile = writeMultipartToTemp(file); + } + } + + if (lower.endsWith(".docx")) { + try (FileInputStream fis2 = new FileInputStream(localFile)) { + receivers = parseDocx(fis2, sysFileService); + } + } else { + throw new IllegalArgumentException("文件格式不对,请下载模版(仅支持docx)"); + } + + result.put("success", true); + result.put("receivers", receivers); // 识别出来的委托人 + result.put("filePath", filePath); // 文档路径,始终返回 + result.put("fileName", fileName); + } catch (Exception e) { + result.put("success", false); + result.put("message", "文档解析失败"); + result.put("filePath", filePath); + result.put("fileName", fileName); + } + + return result; + } + + + + /* ================= helper: 下载/写临时文件 ================= */ + + private static File downloadToLocal(String fileUrl) throws IOException { + // 对 URL 的文件名部分做编码(处理中文/空格) + String encoded = encodeChineseInUrl(fileUrl); + URL url = new URL(encoded); + + Path tempDir = Files.createTempDirectory(TEMP_PREFIX); + String fileName = fileUrl.substring(fileUrl.lastIndexOf('/') + 1); + Path target = tempDir.resolve(fileName); + + try (InputStream in = url.openStream()) { + Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + logger.error("下载远程文件失败: {}, encodedUrl={}", fileUrl, encoded, e); + throw e; + } + return target.toFile(); + } + + private static String encodeChineseInUrl(String url) throws UnsupportedEncodingException { + int idx = url.lastIndexOf('/'); + if (idx < 0) { + return URLEncoder.encode(url, "UTF-8").replace("+", "%20"); + } + String prefix = url.substring(0, idx + 1); + String fileName = url.substring(idx + 1); + return prefix + URLEncoder.encode(fileName, "UTF-8").replace("+", "%20"); + } + + private static File writeMultipartToTemp(MultipartFile file) throws IOException { + Path tempDir = Files.createTempDirectory(TEMP_PREFIX); + String fileName = file.getOriginalFilename() == null ? UUID.randomUUID().toString() + ".docx" : file.getOriginalFilename(); + Path target = tempDir.resolve(fileName); + try (InputStream in = file.getInputStream()) { + Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING); + } + return target.toFile(); + } + + /* ================= 文本抽取(鲁棒) ================= */ + + // 归一化文本:去零宽字符、合并空白等 + private static String normalizeText(String text) { + if (text == null) { + return ""; + } + text = text.replace('\u00A0', ' '); // NBSP + text = text.replaceAll("[\\u200B-\\u200F\\uFEFF]", ""); + text = text.replaceAll("[\\p{Cntrl}&&[^\r\n\t]]", " "); + text = text.replaceAll("\\s+", " "); + return text.trim(); + } + + // 鲁棒提取姓名+身份证对 + private static List extractReceivers(String text) { + List list = new ArrayList<>(); + if (text == null || text.trim().isEmpty()) { + return list; + } + + String norm = normalizeText(text); + + // 支持: (孙贤红)身份证号123... / 孙贤红 身份证号123... / 被授权人姓名:孙贤红 身份证号... + Pattern p1 = Pattern.compile("[\\((\\[]?\\s*([\\p{IsHan}·•・]{2,10})\\s*[\\))\\]]?\\s*身份证号\\s*(\\d{15}|\\d{17}[0-9Xx])"); + Matcher m1 = p1.matcher(norm); + while (m1.find()) { + ProAuthorizeDetails d = new ProAuthorizeDetails(); + d.setName(m1.group(1).trim()); + d.setIdNumber(m1.group(2).trim()); + list.add(d); + } + + // 若 p1 未找到,再试带“被授权人姓名/委托人”等标签的模式 + if (list.isEmpty()) { + Pattern p2 = Pattern.compile("(被授权人姓名|委托人姓名|委托人|被授权人)[::]?\\s*([\\p{IsHan}·•・]{2,10}).{0,50}?身份证号\\s*(\\d{15}|\\d{17}[0-9Xx])"); + Matcher m2 = p2.matcher(norm); + while (m2.find()) { + ProAuthorizeDetails d = new ProAuthorizeDetails(); + d.setName(m2.group(2).trim()); + d.setIdNumber(m2.group(3).trim()); + list.add(d); + } + } + + return list; + } + + /* ================= 上下文判定(正面/背面) ================= */ + + private static boolean contextSuggestsFront(String ctx) { + if (ctx == null) { + return false; + } + String lower = ctx.toLowerCase(); + return lower.contains("正面") || lower.contains("头像") || lower.contains("人像") || lower.contains("正 "); + } + + private static boolean contextSuggestsBack(String ctx) { + if (ctx == null) { + return false; + } + String lower = ctx.toLowerCase(); + return lower.contains("背面") || lower.contains("国徽") || lower.contains("背 "); + } + + /* ================= 图片上传(调用 sysFileService.upload) ================= */ + + // 你项目的 sysFileService.upload(MultipartFile) 应返回 AjaxResult,data 中包含 url 字段 + private static String saveImageWithSysFileService(byte[] data, String ext, RemoteFileService sysFileService) { + try { + String fileName = UUID.randomUUID().toString().replace("-", "") + "." + (ext == null ? "jpg" : ext); + MultipartFile multipartFile = new MockMultipartFile(fileName, fileName, "image/" + (ext == null ? "jpeg" : ext), data); + + AjaxResult uploadRes = sysFileService.upload(multipartFile); + if (uploadRes != null && uploadRes.isSuccess()) { + Object d = uploadRes.get("data"); + if (d instanceof Map) { + Map map = (Map) d; + Object url = map.get("url"); + if (url != null) { + return url.toString(); + } + } + } + } catch (Exception e) { + logger.warn("图片上传到 sysFileService 失败: {}", e.getMessage()); + } + return null; + } + + /* ================= docx 解析(文字 + 图片) ================= */ + + private static List parseDocx(InputStream inputStream, RemoteFileService sysFileService) throws Exception { + List receivers = new ArrayList<>(); + XWPFDocument doc = new XWPFDocument(OPCPackage.open(inputStream)); + + // 1) 提取全文文本并识别委托人姓名/身份证 + StringBuilder sb = new StringBuilder(); + for (XWPFParagraph p : doc.getParagraphs()) { + sb.append(p.getText()).append(" "); + } + for (XWPFTable t : doc.getTables()) { + for (XWPFTableRow r : t.getRows()) { + for (XWPFTableCell c : r.getTableCells()) { + sb.append(c.getText()).append(" "); + } + } + } + String fullText = sb.toString(); + receivers = extractReceivers(fullText); // 你已有的鲁棒方法 + + // 2) 遍历文档结构,收集图片 & 标识(注意保持全局 paraIndex) + List images = new ArrayList<>(); + List markers = new ArrayList<>(); + int globalParaIndex = 0; + int tableIdx = 0; + int globalImageCounter = 0; + + List bodyElements = doc.getBodyElements(); + for (IBodyElement be : bodyElements) { + if (be.getElementType() == BodyElementType.PARAGRAPH) { + XWPFParagraph p = (XWPFParagraph) be; + String text = normalizeText(p.getText()); + String cellKey = "body"; + // 标识:只匹配委托人相关 + if (text.contains("委托人") && (text.contains("正面") || text.contains("背面"))) { + Marker mk = new Marker(); + mk.isFront = text.contains("正面"); + mk.paraIndex = globalParaIndex; + mk.cellKey = cellKey; + mk.text = text; + markers.add(mk); + } + + // 图片(段落内) + if (p.getRuns() != null) { + for (XWPFRun run : p.getRuns()) { + List pics = run.getEmbeddedPictures(); + if (pics != null && !pics.isEmpty()) { + for (XWPFPicture pic : pics) { + XWPFPictureData pd = pic.getPictureData(); + ImgHolder ih = new ImgHolder(); + ih.data = pd.getData(); + ih.ext = pd.suggestFileExtension(); + ih.cellKey = cellKey; + ih.paraIndex = globalParaIndex; + ih.url = null; + ih.imageIndex = globalImageCounter++; + images.add(ih); + } + } + } + } + globalParaIndex++; + } else if (be.getElementType() == BodyElementType.TABLE) { + XWPFTable table = (XWPFTable) be; + for (int r = 0; r < table.getRows().size(); r++) { + XWPFTableRow row = table.getRows().get(r); + List cells = row.getTableCells(); + for (int c = 0; c < cells.size(); c++) { + XWPFTableCell cell = cells.get(c); + String cellKey = "t" + tableIdx + "_r" + r + "_c" + c; + for (XWPFParagraph p : cell.getParagraphs()) { + String text = normalizeText(p.getText()); + if (text.contains("委托人") && (text.contains("正面") || text.contains("背面"))) { + Marker mk = new Marker(); + mk.isFront = text.contains("正面"); + mk.paraIndex = globalParaIndex; + mk.cellKey = cellKey; + mk.text = text; + markers.add(mk); + } + + if (p.getRuns() != null) { + for (XWPFRun run : p.getRuns()) { + List pics = run.getEmbeddedPictures(); + if (pics != null && !pics.isEmpty()) { + for (XWPFPicture pic : pics) { + XWPFPictureData pd = pic.getPictureData(); + ImgHolder ih = new ImgHolder(); + ih.data = pd.getData(); + ih.ext = pd.suggestFileExtension(); + ih.cellKey = cellKey; + ih.paraIndex = globalParaIndex; + ih.url = null; + ih.imageIndex = globalImageCounter++; + images.add(ih); + } + } + } + } + globalParaIndex++; + } + } + } + tableIdx++; + } + } + + // 如果没有任何标识(markers),尽量 fallback:不做这里的自动索引分配(后面会尝试按剩余图片补位) + if (markers.isEmpty()) { + // 直接尝试按“模板索引规则”作为兜底(例如单人取 images[0]/images[2],两人取 [2,4] & [0,1] 等) + // 但首先还是希望尽量靠标识。下面的代码会在分配后进行补位。 + } + + // 3) 按标识顺序分配图片给委托人(只关注委托人) + Set usedImageIndices = new HashSet<>(); + int delegateIdx = 0; // 当前分配到第几个委托人(0-based) + + for (Marker mk : markers) { + // 只处理“委托人”的标识(忽略“法人代表”或其它) + if (mk.text == null || !mk.text.contains("委托人")) { + continue; + } + if (receivers.isEmpty()) { + break; + } + + int targetReceiver = Math.min(delegateIdx, receivers.size() - 1); + + // 找到最佳图片候选:优先同 cell、paraIndex >= marker.paraIndex、未被用过 + ImgHolder found = null; + for (ImgHolder ih : images) { + if (usedImageIndices.contains(ih.imageIndex)) { + continue; + } + if (!Objects.equals(ih.cellKey, mk.cellKey)) { + continue; + } + if (ih.paraIndex >= mk.paraIndex) { found = ih; break; } + } + // 如果未找到同 cell 的,则找全局 paraIndex >= marker.paraIndex + if (found == null) { + for (ImgHolder ih : images) { + if (usedImageIndices.contains(ih.imageIndex)) { + continue; + } + if (ih.paraIndex >= mk.paraIndex) { found = ih; break; } + } + } + // 最后兜底:任意未使用图片 + if (found == null) { + for (ImgHolder ih : images) { + if (!usedImageIndices.contains(ih.imageIndex)) { found = ih; break; } + } + } + + if (found != null) { + // 上传(惰性):若还未上传(url==null)则上传一次 + if (found.url == null) { + String uploaded = saveImageWithSysFileService(found.data, found.ext, sysFileService); + found.url = uploaded; + } + if (found.url != null) { + ProAuthorizeDetails p = receivers.get(targetReceiver); + if (mk.isFront) { + p.setFrontUrl(found.url); + } else { + p.setBackUrl(found.url); + } + } + usedImageIndices.add(found.imageIndex); + } + + // 如果这是“背面”,则才认为一组委托人完成,delegateIdx++(开始下一人) + if (!mk.isFront) { + delegateIdx++; + } + } + + // 4) 补位:若某些委托人缺少 front/back,用剩余图片补上(不覆盖已有) + if (!receivers.isEmpty() && !images.isEmpty()) { + for (int i = 0; i < receivers.size(); i++) { + ProAuthorizeDetails p = receivers.get(i); + if ((p.getFrontUrl() == null || p.getFrontUrl().isEmpty()) || + (p.getBackUrl() == null || p.getBackUrl().isEmpty())) { + + for (ImgHolder ih : images) { + if (usedImageIndices.contains(ih.imageIndex)) { + continue; + } + // 上传并分配 + if (ih.url == null) { + ih.url = saveImageWithSysFileService(ih.data, ih.ext, sysFileService); + } + if (ih.url == null) { + continue; + } + if (p.getFrontUrl() == null || p.getFrontUrl().isEmpty()) { + p.setFrontUrl(ih.url); + usedImageIndices.add(ih.imageIndex); + } else if (p.getBackUrl() == null || p.getBackUrl().isEmpty()) { + p.setBackUrl(ih.url); + usedImageIndices.add(ih.imageIndex); + } + if ((p.getFrontUrl() != null && !p.getFrontUrl().isEmpty()) && + (p.getBackUrl() != null && !p.getBackUrl().isEmpty())) { + break; + } + } + } + } + } + + return receivers; + } + + + + private static int handleParagraph(XWPFParagraph p, List receivers, + RemoteFileService sysFileService, int personIndex) { + String text = normalizeText(p.getText()); + + if (text.contains("身份证(正面)") || text.contains("身份证(背面)")) { + for (XWPFRun run : p.getRuns()) { + for (XWPFPicture pic : run.getEmbeddedPictures()) { + XWPFPictureData pd = pic.getPictureData(); + String url = saveImageWithSysFileService(pd.getData(), pd.suggestFileExtension(), sysFileService); + if (url == null) { + continue; + } + + if (text.contains("委托人")) { + // 委托人 + if (!receivers.isEmpty()) { + ProAuthorizeDetails person = receivers.get(Math.min(personIndex, receivers.size() - 1)); + if (text.contains("正面")) { + person.setFrontUrl(url); + } else { + person.setBackUrl(url); + personIndex++; // 背面处理完 → 下一个人 + } + } + } + } + } + } + return personIndex; + } + + + + private static class ImgHolder { + byte[] data; + String ext; + String cellKey; // 如 "body" 或 "t0_r1_c2" + int paraIndex; + String url; // 上传后返回的 url(惰性赋值) + int imageIndex; // 全局顺序索引(可选,用于调试) + } + + private static class Marker { + boolean isFront; // true = 正面, false = 背面 + int paraIndex; + String cellKey; + String text; // 原始文本,用于调试 + } + + +} diff --git a/bonus-modules/bonus-material/src/main/java/com/bonus/material/materialStation/controller/ProAuthorizeController.java b/bonus-modules/bonus-material/src/main/java/com/bonus/material/materialStation/controller/ProAuthorizeController.java index ed96d404..73e06fce 100644 --- a/bonus-modules/bonus-material/src/main/java/com/bonus/material/materialStation/controller/ProAuthorizeController.java +++ b/bonus-modules/bonus-material/src/main/java/com/bonus/material/materialStation/controller/ProAuthorizeController.java @@ -11,6 +11,7 @@ import com.bonus.common.core.web.page.TableDataInfo; import com.bonus.common.log.annotation.SysLog; import com.bonus.common.log.enums.OperaType; import com.bonus.material.common.annotation.PreventRepeatSubmit; +import com.bonus.material.common.utils.WordParserUtil; import com.bonus.material.materialStation.domain.ProAuthorizeDetails; import com.bonus.material.materialStation.domain.ProAuthorizeInfo; import com.bonus.material.materialStation.service.ProAuthorizeService; @@ -19,13 +20,17 @@ import com.bonus.system.api.RemoteFileService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; import javax.validation.constraints.NotNull; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; /** @@ -81,6 +86,26 @@ public class ProAuthorizeController extends BaseController { } } + @PostMapping("/parseWord") + public AjaxResult parseWord(@RequestParam("file") MultipartFile file) { + try { + AjaxResult result = sysFileService.upload(file); + if (!result.isSuccess()) { + return AjaxResult.error("文件上传失败"); + } + Map jsonObject = (Map) result.get("data"); + String fileName = file.getOriginalFilename(); + String filePath = jsonObject.get("url").toString(); + Map data = WordParserUtil.parseWordFile(file,fileName, filePath, sysFileService); + return AjaxResult.success(data); + } catch (Exception e) { + logger.error("解析失败", e); + return AjaxResult.error("解析失败,请按模版填写内容"); + } + } + + + /** * 授权提交 */ @@ -138,5 +163,15 @@ public class ProAuthorizeController extends BaseController { return AjaxResult.success(ListPagingUtil.paging(pageIndex, pageSize, new ArrayList<>())); } } + + /** + * 授权委托书模版下载 + */ + @ApiOperation(value = "授权委托书模版下载") + @PostMapping("/downLoad") + public void downLoadExcelFile(){ + HttpServletResponse resp = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getResponse(); + service.downLoadTemplate(resp); + } } diff --git a/bonus-modules/bonus-material/src/main/java/com/bonus/material/materialStation/service/ProAuthorizeService.java b/bonus-modules/bonus-material/src/main/java/com/bonus/material/materialStation/service/ProAuthorizeService.java index 27c23283..58276f52 100644 --- a/bonus-modules/bonus-material/src/main/java/com/bonus/material/materialStation/service/ProAuthorizeService.java +++ b/bonus-modules/bonus-material/src/main/java/com/bonus/material/materialStation/service/ProAuthorizeService.java @@ -9,6 +9,7 @@ import com.bonus.material.materialStation.domain.ProAuthorizeInfo; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.multipart.MultipartFile; +import javax.servlet.http.HttpServletResponse; import java.util.List; /** @@ -57,4 +58,10 @@ public interface ProAuthorizeService { int updateAuthorizeInfoSign(ProAuthorizeDetails bean); List getAuthorList(ProAuthorizeInfo bean); + + /** + * 授权委托书模版下载 + * @param resp + */ + void downLoadTemplate(HttpServletResponse resp); } diff --git a/bonus-modules/bonus-material/src/main/java/com/bonus/material/materialStation/service/impl/ProAuthorizeServiceImpl.java b/bonus-modules/bonus-material/src/main/java/com/bonus/material/materialStation/service/impl/ProAuthorizeServiceImpl.java index 243c5852..dc0554c5 100644 --- a/bonus-modules/bonus-material/src/main/java/com/bonus/material/materialStation/service/impl/ProAuthorizeServiceImpl.java +++ b/bonus-modules/bonus-material/src/main/java/com/bonus/material/materialStation/service/impl/ProAuthorizeServiceImpl.java @@ -14,6 +14,7 @@ import com.bonus.material.materialStation.mapper.ProAuthorizeMapper; import com.bonus.material.materialStation.service.ProAuthorizeService; import lombok.extern.slf4j.Slf4j; import okhttp3.*; +import org.apache.commons.io.IOUtils; import org.hibernate.validator.internal.util.StringHelper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -22,10 +23,8 @@ import org.springframework.util.CollectionUtils; import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.*; import java.net.HttpURLConnection; import java.net.URI; import java.net.URL; @@ -283,6 +282,38 @@ public class ProAuthorizeServiceImpl implements ProAuthorizeService { } } + @Override + public void downLoadTemplate(HttpServletResponse response) { + // 模板名称 + String templateName = "授权委托书模板.doc"; + try ( + InputStream input = this.getClass().getClassLoader().getResourceAsStream("template/授权委托书模板.doc"); + OutputStream out = response.getOutputStream() + ) { + if (input == null) { + throw new FileNotFoundException("模板文件不存在: " + templateName); + } + // 设置响应头 + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/msword"); + response.setHeader( + "Content-Disposition", + "attachment;filename=" + new String(templateName.getBytes("UTF-8"), "ISO-8859-1") + ); + response.setHeader("Access-Control-Expose-Headers", "Content-Disposition"); + // 缓冲区传输 + byte[] buffer = new byte[1024]; + int bytesToRead; + while ((bytesToRead = input.read(buffer)) != -1) { + out.write(buffer, 0, bytesToRead); + } + out.flush(); + } catch (IOException e) { + log.error("下载模板失败: {}", e.getMessage(), e); + } + } + + /** * 判断关键字是否包含在item中 * @param item diff --git a/bonus-modules/bonus-material/src/main/resources/template/授权委托书模板.doc b/bonus-modules/bonus-material/src/main/resources/template/授权委托书模板.doc new file mode 100644 index 0000000000000000000000000000000000000000..250162b627790f09f0ea93286b214323196bb931 GIT binary patch literal 23133 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 N`pjGGC&u{B{|6CTMiBr2 literal 0 HcmV?d00001 From d2e2fe1b1a491d52549354058712977fad4bdb17 Mon Sep 17 00:00:00 2001 From: hayu <1604366271@qq.com> Date: Mon, 15 Sep 2025 19:21:02 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../material/repair/RepairAuditDetailsMapper.xml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/bonus-modules/bonus-material/src/main/resources/mapper/material/repair/RepairAuditDetailsMapper.xml b/bonus-modules/bonus-material/src/main/resources/mapper/material/repair/RepairAuditDetailsMapper.xml index a942b6d8..f2efa20d 100644 --- a/bonus-modules/bonus-material/src/main/resources/mapper/material/repair/RepairAuditDetailsMapper.xml +++ b/bonus-modules/bonus-material/src/main/resources/mapper/material/repair/RepairAuditDetailsMapper.xml @@ -340,6 +340,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" tk.create_time AS createTime, tk.remark, tk.CODE AS repairNum, + rad.audit_time as auditTime, GROUP_CONCAT(DISTINCT mt4.type_id) AS firstId FROM tm_task tk @@ -402,6 +403,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" tk.create_time AS createTime, tk.remark, tk.CODE AS repairNum, + rad.audit_time as auditTime, GROUP_CONCAT(DISTINCT mt4.type_id) AS firstId FROM tm_task tk @@ -449,9 +451,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" GROUP BY tk.CODE ) AS combined_results - ORDER BY - taskStatus, - createTime DESC; -- 统一在外部排序 + + + ORDER BY auditTime DESC + + + ORDER BY + taskStatus, + createTime DESC + +