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

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.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(
"<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_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*</" + 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 {
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("<sheetData");
int sheetDataOpenEnd = templateXml.indexOf('>', sheetDataStart);
int sheetDataEnd = templateXml.indexOf("</sheetData>", 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<Integer, String> templateRows, int footerRow,
private void writeOoxmlFooter(OutputStream output, Map<Integer, String> 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));