🐛 修复应收账单二维码位置

This commit is contained in:
caojia
2026-11-11 14:23:25 +08:00
parent 4ceb0bc830
commit 8cb2d03062
@@ -7,6 +7,7 @@ import java.io.OutputStream;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
@@ -121,7 +122,10 @@ public class ArBillServiceImpl implements IArBillService {
* Excel 行号从 1 开始;模板第 4 行是表头,明细从第 5 行开始写入。 * Excel 行号从 1 开始;模板第 4 行是表头,明细从第 5 行开始写入。
*/ */
private static final int TEMPLATE_DATA_START_ROW = 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 int TEMPLATE_COLUMN_COUNT = 21;
private static final double EXPORT_DETAIL_ROW_HEIGHT = 20.0; private static final double EXPORT_DETAIL_ROW_HEIGHT = 20.0;
/** 客户订单号、运单号位于 Excel 的 C、D 列,并保持相同列宽。 */ /** 客户订单号、运单号位于 Excel 的 C、D 列,并保持相同列宽。 */
@@ -158,7 +162,16 @@ public class ArBillServiceImpl implements IArBillService {
*/ */
private static final int TEMPLATE_DETAIL_STYLE_INDEX = 31; 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_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 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(
"<Relationship\\b(?=[^>]*\\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("<col\\b([^>]*)/\\s*>", Pattern.DOTALL); private static final Pattern TEMPLATE_COLUMN_PATTERN = Pattern.compile("<col\\b([^>]*)/\\s*>", Pattern.DOTALL);
private static final Pattern TEMPLATE_COLUMN_MIN_PATTERN = Pattern.compile("\\bmin\\s*=\\s*[\"'](\\d+)[\"']"); 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+)[\"']"); 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 { private void writeOoxmlWorkbook(ArBill bill, ArExportTemplate template, Path target) throws IOException {
// 样式表单独读取;不加载整张工作簿,且不依赖 styles.xml 和 sheet1.xml 在 ZIP 中的先后顺序。 // 样式表单独读取;不加载整张工作簿,且不依赖 styles.xml 和 sheet1.xml 在 ZIP 中的先后顺序。
StylesTable styles = readExportStyles(template); StylesTable styles = readExportStyles(template);
String drawingPartPath = readTemplateDrawingPartPath(template);
XSSFCellStyle detailNoStyle = styles.createCellStyle(); XSSFCellStyle detailNoStyle = styles.createCellStyle();
detailNoStyle.cloneStyleFrom(styles.getStyleAt(TEMPLATE_DETAIL_STYLE_INDEX)); detailNoStyle.cloneStyleFrom(styles.getStyleAt(TEMPLATE_DETAIL_STYLE_INDEX));
detailNoStyle.setDataFormat(BuiltinFormats.getBuiltinFormat("0")); detailNoStyle.setDataFormat(BuiltinFormats.getBuiltinFormat("0"));
@@ -1460,13 +1474,37 @@ public class ArBillServiceImpl implements IArBillService {
try (InputStream input = openTemplateStream(template); ZipInputStream templateZip = new ZipInputStream(input); try (InputStream input = openTemplateStream(template); ZipInputStream templateZip = new ZipInputStream(input);
OutputStream fileOutput = Files.newOutputStream(target); ZipOutputStream output = new ZipOutputStream(fileOutput)) { OutputStream fileOutput = Files.newOutputStream(target); ZipOutputStream output = new ZipOutputStream(fileOutput)) {
boolean replaced = false; boolean replaced = false;
Integer drawingRowOffset = null;
String pendingDrawingXml = null;
for (ZipEntry entry; (entry = templateZip.getNextEntry()) != null; ) { for (ZipEntry entry; (entry = templateZip.getNextEntry()) != null; ) {
ZipEntry outputEntry = new ZipEntry(entry.getName()); String entryName = entry.getName();
output.putNextEntry(outputEntry); 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())) { 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; 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); styles.writeTo(output);
} else if (!entry.isDirectory()) { } else if (!entry.isDirectory()) {
templateZip.transferTo(output); 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*</" + rowTag + "\\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 { private StylesTable readExportStyles(ArExportTemplate template) throws IOException {
try (InputStream input = openTemplateStream(template); ZipInputStream templateZip = new ZipInputStream(input)) { try (InputStream input = openTemplateStream(template); ZipInputStream templateZip = new ZipInputStream(input)) {
for (ZipEntry entry; (entry = templateZip.getNextEntry()) != null; ) { for (ZipEntry entry; (entry = templateZip.getNextEntry()) != null; ) {
@@ -1523,8 +1614,8 @@ public class ArBillServiceImpl implements IArBillService {
/** /**
* 以 XML 流方式写入表头、分页明细和银行信息,避免 POI 在大数据量下移动行。 * 以 XML 流方式写入表头、分页明细和银行信息,避免 POI 在大数据量下移动行。
*/ */
private void writeOoxmlSheet(String templateXml, ArBill bill, OutputStream output, private int writeOoxmlSheet(String templateXml, ArBill bill, OutputStream output,
ExportStyleIndexes styleIndexes) throws IOException { ExportStyleIndexes styleIndexes) throws IOException {
int sheetDataStart = templateXml.indexOf("<sheetData"); int sheetDataStart = templateXml.indexOf("<sheetData");
int sheetDataOpenEnd = templateXml.indexOf('>', sheetDataStart); int sheetDataOpenEnd = templateXml.indexOf('>', sheetDataStart);
int sheetDataEnd = templateXml.indexOf("</sheetData>", sheetDataOpenEnd); int sheetDataEnd = templateXml.indexOf("</sheetData>", sheetDataOpenEnd);
@@ -1562,10 +1653,12 @@ public class ArBillServiceImpl implements IArBillService {
lastDetailNo = details.get(details.size() - 1).getDetailNo(); lastDetailNo = details.get(details.size() - 1).getDetailNo();
} }
int footerRow = Math.max(rowIndex, TEMPLATE_FOOTER_START_ROW); int lastDetailRow = Math.max(rowIndex - 1, TEMPLATE_LAST_DETAIL_ROW);
writeOoxmlFooter(output, templateRows, footerRow, bill, styleIndexes.generalNumber()); int rowOffset = lastDetailRow - TEMPLATE_LAST_DETAIL_ROW;
String suffix = shiftFooterMerges(templateXml.substring(sheetDataEnd), footerRow - TEMPLATE_FOOTER_START_ROW); writeOoxmlFooter(output, templateRows, lastDetailRow, bill, styleIndexes.generalNumber());
String suffix = shiftFooterMerges(templateXml.substring(sheetDataEnd), rowOffset);
writeUtf8(output, suffix); writeUtf8(output, suffix);
return rowOffset;
} }
/** /**
@@ -1672,12 +1765,12 @@ public class ArBillServiceImpl implements IArBillService {
return rows; return rows;
} }
private void writeOoxmlFooter(OutputStream output, Map<Integer, String> templateRows, int footerRow, private void writeOoxmlFooter(OutputStream output, Map<Integer, String> templateRows, int lastDetailRow,
ArBill bill, int generalNumberStyleIndex) throws IOException { 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))"; String totalAmountFormula = "SUM(S" + TEMPLATE_DATA_START_ROW + ":INDEX(S:S,ROW()-1))";
for (int offset = 0; offset < 4; offset++) { 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; int targetRowNumber = sourceRowNumber + rowOffset;
String row = shiftRowXml(templateRows.get(sourceRowNumber), targetRowNumber); String row = shiftRowXml(templateRows.get(sourceRowNumber), targetRowNumber);
if (offset == 0) { if (offset == 0) {
@@ -1964,7 +2057,7 @@ public class ArBillServiceImpl implements IArBillService {
private String shiftFooterMerges(String xml, int rowOffset) { private String shiftFooterMerges(String xml, int rowOffset) {
Matcher matcher = MERGE_REF_PATTERN.matcher(xml); Matcher matcher = MERGE_REF_PATTERN.matcher(xml);
StringBuffer result = new StringBuffer(); StringBuffer result = new StringBuffer();
int sourceFooterStart = TEMPLATE_FOOTER_START_ROW + 1; int sourceFooterStart = TEMPLATE_LAST_DETAIL_ROW + 1;
int sourceFooterEnd = sourceFooterStart + 3; int sourceFooterEnd = sourceFooterStart + 3;
while (matcher.find()) { while (matcher.find()) {
int firstRow = Integer.parseInt(matcher.group(3)); int firstRow = Integer.parseInt(matcher.group(3));