🐛 修复应收账单二维码位置
This commit is contained in:
+107
-14
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user