From 8cb2d030626abe721ba63439bdf6bc334fa5fc4f Mon Sep 17 00:00:00 2001 From: caojia <1301869380@qq.com> Date: Wed, 11 Nov 2026 14:23:25 +0800 Subject: [PATCH] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E5=BA=94=E6=94=B6?= =?UTF-8?q?=E8=B4=A6=E5=8D=95=E4=BA=8C=E7=BB=B4=E7=A0=81=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/ArBillServiceImpl.java | 121 ++++++++++++++++-- 1 file changed, 107 insertions(+), 14 deletions(-) diff --git a/bms-back/bms-system/src/main/java/com/bms/receivable/service/impl/ArBillServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/receivable/service/impl/ArBillServiceImpl.java index e0431a02..78cf656d 100644 --- a/bms-back/bms-system/src/main/java/com/bms/receivable/service/impl/ArBillServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/receivable/service/impl/ArBillServiceImpl.java @@ -7,6 +7,7 @@ import java.io.OutputStream; import java.math.BigDecimal; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; @@ -121,7 +122,10 @@ public class ArBillServiceImpl implements IArBillService { * Excel 行号从 1 开始;模板第 4 行是表头,明细从第 5 行开始写入。 */ private static final int TEMPLATE_DATA_START_ROW = 5; - private static final int TEMPLATE_FOOTER_START_ROW = 5; + /** 模板的明细区只有第 5 行;超出的明细行插入在页脚前。 */ + private static final int TEMPLATE_LAST_DETAIL_ROW = 5; + /** DrawingML 行号从 0 开始,插入点是模板第 5 行之后。 */ + private static final int TEMPLATE_DRAWING_INSERT_ROW_INDEX = 5; private static final int TEMPLATE_COLUMN_COUNT = 21; private static final double EXPORT_DETAIL_ROW_HEIGHT = 20.0; /** 客户订单号、运单号位于 Excel 的 C、D 列,并保持相同列宽。 */ @@ -158,7 +162,16 @@ public class ArBillServiceImpl implements IArBillService { */ private static final int TEMPLATE_DETAIL_STYLE_INDEX = 31; private static final String TEMPLATE_SHEET_XML = "xl/worksheets/sheet1.xml"; + private static final String TEMPLATE_SHEET_RELATIONSHIPS_XML = "xl/worksheets/_rels/sheet1.xml.rels"; private static final String TEMPLATE_STYLES_XML = "xl/styles.xml"; + private static final String SPREADSHEET_DRAWING_NAMESPACE = + "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"; + private static final Pattern DRAWING_RELATIONSHIP_PATTERN = Pattern.compile( + "]*\\bType\\s*=\\s*[\\\"'][^\\\"']*/drawing[\\\"'])(?=[^>]*\\bTarget\\s*=\\s*[\\\"']([^\\\"']+)[\\\"'])[^>]*/?>", + Pattern.DOTALL); + private static final Pattern DRAWING_NAMESPACE_PREFIX_PATTERN = Pattern.compile( + "xmlns:([A-Za-z_][\\w.-]*)\\s*=\\s*[\\\"']" + + Pattern.quote(SPREADSHEET_DRAWING_NAMESPACE) + "[\\\"']"); private static final Pattern TEMPLATE_COLUMN_PATTERN = Pattern.compile("]*)/\\s*>", Pattern.DOTALL); private static final Pattern TEMPLATE_COLUMN_MIN_PATTERN = Pattern.compile("\\bmin\\s*=\\s*[\"'](\\d+)[\"']"); private static final Pattern TEMPLATE_COLUMN_MAX_PATTERN = Pattern.compile("\\bmax\\s*=\\s*[\"'](\\d+)[\"']"); @@ -1406,6 +1419,7 @@ public class ArBillServiceImpl implements IArBillService { private void writeOoxmlWorkbook(ArBill bill, ArExportTemplate template, Path target) throws IOException { // 样式表单独读取;不加载整张工作簿,且不依赖 styles.xml 和 sheet1.xml 在 ZIP 中的先后顺序。 StylesTable styles = readExportStyles(template); + String drawingPartPath = readTemplateDrawingPartPath(template); XSSFCellStyle detailNoStyle = styles.createCellStyle(); detailNoStyle.cloneStyleFrom(styles.getStyleAt(TEMPLATE_DETAIL_STYLE_INDEX)); detailNoStyle.setDataFormat(BuiltinFormats.getBuiltinFormat("0")); @@ -1460,13 +1474,37 @@ public class ArBillServiceImpl implements IArBillService { try (InputStream input = openTemplateStream(template); ZipInputStream templateZip = new ZipInputStream(input); OutputStream fileOutput = Files.newOutputStream(target); ZipOutputStream output = new ZipOutputStream(fileOutput)) { boolean replaced = false; + Integer drawingRowOffset = null; + String pendingDrawingXml = null; for (ZipEntry entry; (entry = templateZip.getNextEntry()) != null; ) { - ZipEntry outputEntry = new ZipEntry(entry.getName()); - output.putNextEntry(outputEntry); + String entryName = entry.getName(); + if (!StringUtils.isEmpty(drawingPartPath) && drawingPartPath.equals(entryName)) { + String drawingXml = readUtf8(templateZip); + if (drawingRowOffset == null) { + // ZIP 包内绘图可能先于工作表出现,先缓存小型绘图 XML,待明细行数确定后再写入。 + pendingDrawingXml = drawingXml; + } else { + writeDrawingEntry(output, entryName, drawingXml, drawingRowOffset.intValue()); + } + templateZip.closeEntry(); + continue; + } if (TEMPLATE_SHEET_XML.equals(entry.getName())) { - writeOoxmlSheet(readUtf8(templateZip), bill, output, exportStyleIndexes); + output.putNextEntry(new ZipEntry(entryName)); + int rowOffset = writeOoxmlSheet(readUtf8(templateZip), bill, output, exportStyleIndexes); + output.closeEntry(); replaced = true; - } else if (TEMPLATE_STYLES_XML.equals(entry.getName())) { + drawingRowOffset = Integer.valueOf(rowOffset); + if (pendingDrawingXml != null) { + writeDrawingEntry(output, drawingPartPath, pendingDrawingXml, rowOffset); + pendingDrawingXml = null; + } + templateZip.closeEntry(); + continue; + } + + output.putNextEntry(new ZipEntry(entryName)); + if (TEMPLATE_STYLES_XML.equals(entryName)) { styles.writeTo(output); } else if (!entry.isDirectory()) { templateZip.transferTo(output); @@ -1480,6 +1518,59 @@ public class ArBillServiceImpl implements IArBillService { } } + private String readTemplateDrawingPartPath(ArExportTemplate template) throws IOException { + try (InputStream input = openTemplateStream(template); ZipInputStream templateZip = new ZipInputStream(input)) { + for (ZipEntry entry; (entry = templateZip.getNextEntry()) != null; ) { + if (TEMPLATE_SHEET_RELATIONSHIPS_XML.equals(entry.getName())) { + String relationshipsXml = readUtf8(templateZip); + Matcher matcher = DRAWING_RELATIONSHIP_PATTERN.matcher(relationshipsXml); + if (matcher.find()) { + String target = matcher.group(1); + if (target.startsWith("/")) { + return target.substring(1); + } + Path sheetDirectory = Paths.get(TEMPLATE_SHEET_XML).getParent(); + return sheetDirectory.resolve(target).normalize().toString().replace('\\', '/'); + } + return null; + } + templateZip.closeEntry(); + } + } + return null; + } + + private void writeDrawingEntry(ZipOutputStream output, String entryName, String drawingXml, + int rowOffset) throws IOException { + output.putNextEntry(new ZipEntry(entryName)); + writeUtf8(output, shiftDrawingAnchorRows(drawingXml, rowOffset)); + output.closeEntry(); + } + + /** + * 明细超出模板预留行时,同步下移插入点之后的图片锚点,保持二维码与页脚的相对位置。 + */ + private String shiftDrawingAnchorRows(String drawingXml, int rowOffset) { + if (rowOffset <= 0) { + return drawingXml; + } + Matcher namespaceMatcher = DRAWING_NAMESPACE_PREFIX_PATTERN.matcher(drawingXml); + String rowTag = namespaceMatcher.find() + ? Pattern.quote(namespaceMatcher.group(1)) + ":row" + : "row"; + Pattern rowPattern = Pattern.compile("(<" + rowTag + "\\s*>\\s*)(\\d+)(\\s*)"); + Matcher rowMatcher = rowPattern.matcher(drawingXml); + StringBuffer shifted = new StringBuffer(); + while (rowMatcher.find()) { + int rowIndex = Integer.parseInt(rowMatcher.group(2)); + int targetRowIndex = rowIndex >= TEMPLATE_DRAWING_INSERT_ROW_INDEX ? rowIndex + rowOffset : rowIndex; + rowMatcher.appendReplacement(shifted, Matcher.quoteReplacement( + rowMatcher.group(1) + targetRowIndex + rowMatcher.group(3))); + } + rowMatcher.appendTail(shifted); + return shifted.toString(); + } + private StylesTable readExportStyles(ArExportTemplate template) throws IOException { try (InputStream input = openTemplateStream(template); ZipInputStream templateZip = new ZipInputStream(input)) { for (ZipEntry entry; (entry = templateZip.getNextEntry()) != null; ) { @@ -1523,8 +1614,8 @@ public class ArBillServiceImpl implements IArBillService { /** * 以 XML 流方式写入表头、分页明细和银行信息,避免 POI 在大数据量下移动行。 */ - private void writeOoxmlSheet(String templateXml, ArBill bill, OutputStream output, - ExportStyleIndexes styleIndexes) throws IOException { + private int writeOoxmlSheet(String templateXml, ArBill bill, OutputStream output, + ExportStyleIndexes styleIndexes) throws IOException { int sheetDataStart = templateXml.indexOf("', sheetDataStart); int sheetDataEnd = templateXml.indexOf("", sheetDataOpenEnd); @@ -1562,10 +1653,12 @@ public class ArBillServiceImpl implements IArBillService { lastDetailNo = details.get(details.size() - 1).getDetailNo(); } - int footerRow = Math.max(rowIndex, TEMPLATE_FOOTER_START_ROW); - writeOoxmlFooter(output, templateRows, footerRow, bill, styleIndexes.generalNumber()); - String suffix = shiftFooterMerges(templateXml.substring(sheetDataEnd), footerRow - TEMPLATE_FOOTER_START_ROW); + int lastDetailRow = Math.max(rowIndex - 1, TEMPLATE_LAST_DETAIL_ROW); + int rowOffset = lastDetailRow - TEMPLATE_LAST_DETAIL_ROW; + writeOoxmlFooter(output, templateRows, lastDetailRow, bill, styleIndexes.generalNumber()); + String suffix = shiftFooterMerges(templateXml.substring(sheetDataEnd), rowOffset); writeUtf8(output, suffix); + return rowOffset; } /** @@ -1672,12 +1765,12 @@ public class ArBillServiceImpl implements IArBillService { return rows; } - private void writeOoxmlFooter(OutputStream output, Map templateRows, int footerRow, + private void writeOoxmlFooter(OutputStream output, Map templateRows, int lastDetailRow, ArBill bill, int generalNumberStyleIndex) throws IOException { - int rowOffset = footerRow - TEMPLATE_FOOTER_START_ROW; + int rowOffset = lastDetailRow - TEMPLATE_LAST_DETAIL_ROW; String totalAmountFormula = "SUM(S" + TEMPLATE_DATA_START_ROW + ":INDEX(S:S,ROW()-1))"; for (int offset = 0; offset < 4; offset++) { - int sourceRowNumber = TEMPLATE_FOOTER_START_ROW + offset + 1; + int sourceRowNumber = TEMPLATE_LAST_DETAIL_ROW + offset + 1; int targetRowNumber = sourceRowNumber + rowOffset; String row = shiftRowXml(templateRows.get(sourceRowNumber), targetRowNumber); if (offset == 0) { @@ -1964,7 +2057,7 @@ public class ArBillServiceImpl implements IArBillService { private String shiftFooterMerges(String xml, int rowOffset) { Matcher matcher = MERGE_REF_PATTERN.matcher(xml); StringBuffer result = new StringBuffer(); - int sourceFooterStart = TEMPLATE_FOOTER_START_ROW + 1; + int sourceFooterStart = TEMPLATE_LAST_DETAIL_ROW + 1; int sourceFooterEnd = sourceFooterStart + 3; while (matcher.find()) { int firstRow = Integer.parseInt(matcher.group(3));