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