🐛 修复账单导出数值精度显示

This commit is contained in:
caojia
2026-10-21 14:57:33 +08:00
parent b8e2a8e31a
commit c04191c316
@@ -19,6 +19,7 @@ import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
@@ -120,6 +121,8 @@ public class ArBillServiceImpl implements IArBillService {
private static final int TRACKING_NO_COLUMN_INDEX = 4;
private static final int FORMULA_COLUMN_INDEX = 16;
private static final int FEE_TYPE_COLUMN_INDEX = 17;
/** 数值明细列(Excel 列号):重量、尺寸、单价、处理费和应收金额。 */
private static final Set<Integer> GENERAL_NUMERIC_EXPORT_COLUMNS = Set.of(8, 9, 10, 11, 12, 13, 14, 15, 19);
private static final double MIN_CUSTOMER_ORDER_NO_COLUMN_WIDTH = 16.0;
private static final double MIN_TRACKING_NO_COLUMN_WIDTH = MIN_CUSTOMER_ORDER_NO_COLUMN_WIDTH;
private static final double MIN_FORMULA_COLUMN_WIDTH = 17.0;
@@ -158,6 +161,8 @@ public class ArBillServiceImpl implements IArBillService {
"<c\\b([^>]*?)(?:/\\s*>|>(.*?)</c>)", Pattern.DOTALL);
private static final Pattern CELL_REF_PATTERN = Pattern.compile(
"\\br\\s*=\\s*[\\\"']([A-Z]+\\d+)[\\\"']", Pattern.CASE_INSENSITIVE);
private static final Pattern CELL_STYLE_PATTERN = Pattern.compile(
"\\bs\\s*=\\s*[\\\"'](\\d+)[\\\"']", Pattern.CASE_INSENSITIVE);
private static final Pattern CELL_ROW_REF_PATTERN = Pattern.compile(
"(<c\\b[^>]*\\br=\")([A-Z]+)\\d+(\")");
private static final Pattern ROW_REF_PATTERN = Pattern.compile(
@@ -166,6 +171,13 @@ public class ArBillServiceImpl implements IArBillService {
private static final Pattern MERGE_REF_PATTERN = Pattern.compile(
"(<mergeCell\\b[^>]*\\bref=\")([A-Z]+)(\\d+)(?::([A-Z]+)(\\d+))?(\")");
private record ExportStyleIndexes(int failedDetail, int detailNo, int failedDetailNo,
int customerOrderNo, int failedCustomerOrderNo,
int trackingNo, int failedTrackingNo,
int generalNumber, int failedGeneralNumber,
int formula, int failedFormula) {
}
@Autowired
private ArBillMapper billMapper;
@Autowired
@@ -1337,6 +1349,14 @@ public class ArBillServiceImpl implements IArBillService {
failedDetailStyle.setFillForegroundColor(IndexedColors.YELLOW.getIndex());
failedDetailStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
int failedDetailStyleIndex = Short.toUnsignedInt(failedDetailStyle.getIndex());
XSSFCellStyle customerOrderNoStyle = styles.createCellStyle();
customerOrderNoStyle.cloneStyleFrom(styles.getStyleAt(TEMPLATE_DETAIL_STYLE_INDEX));
customerOrderNoStyle.setDataFormat(BuiltinFormats.getBuiltinFormat("@"));
int customerOrderNoStyleIndex = Short.toUnsignedInt(customerOrderNoStyle.getIndex());
XSSFCellStyle failedCustomerOrderNoStyle = styles.createCellStyle();
failedCustomerOrderNoStyle.cloneStyleFrom(failedDetailStyle);
failedCustomerOrderNoStyle.setDataFormat(BuiltinFormats.getBuiltinFormat("@"));
int failedCustomerOrderNoStyleIndex = Short.toUnsignedInt(failedCustomerOrderNoStyle.getIndex());
XSSFCellStyle trackingNoStyle = styles.createCellStyle();
trackingNoStyle.cloneStyleFrom(styles.getStyleAt(TEMPLATE_DETAIL_STYLE_INDEX));
trackingNoStyle.setDataFormat(BuiltinFormats.getBuiltinFormat("@"));
@@ -1345,6 +1365,14 @@ public class ArBillServiceImpl implements IArBillService {
failedTrackingNoStyle.cloneStyleFrom(failedDetailStyle);
failedTrackingNoStyle.setDataFormat(BuiltinFormats.getBuiltinFormat("@"));
int failedTrackingNoStyleIndex = Short.toUnsignedInt(failedTrackingNoStyle.getIndex());
XSSFCellStyle generalNumberStyle = styles.createCellStyle();
generalNumberStyle.cloneStyleFrom(styles.getStyleAt(TEMPLATE_DETAIL_STYLE_INDEX));
generalNumberStyle.setDataFormat(BuiltinFormats.getBuiltinFormat("General"));
int generalNumberStyleIndex = Short.toUnsignedInt(generalNumberStyle.getIndex());
XSSFCellStyle failedGeneralNumberStyle = styles.createCellStyle();
failedGeneralNumberStyle.cloneStyleFrom(failedDetailStyle);
failedGeneralNumberStyle.setDataFormat(BuiltinFormats.getBuiltinFormat("General"));
int failedGeneralNumberStyleIndex = Short.toUnsignedInt(failedGeneralNumberStyle.getIndex());
XSSFCellStyle failedDetailNoStyle = styles.createCellStyle();
failedDetailNoStyle.cloneStyleFrom(failedDetailStyle);
failedDetailNoStyle.setDataFormat(BuiltinFormats.getBuiltinFormat("0"));
@@ -1357,6 +1385,12 @@ public class ArBillServiceImpl implements IArBillService {
failedFormulaStyle.cloneStyleFrom(failedDetailStyle);
failedFormulaStyle.setShrinkToFit(true);
int failedFormulaStyleIndex = Short.toUnsignedInt(failedFormulaStyle.getIndex());
ExportStyleIndexes exportStyleIndexes = new ExportStyleIndexes(
failedDetailStyleIndex, detailNoStyleIndex, failedDetailNoStyleIndex,
customerOrderNoStyleIndex, failedCustomerOrderNoStyleIndex,
trackingNoStyleIndex, failedTrackingNoStyleIndex,
generalNumberStyleIndex, failedGeneralNumberStyleIndex,
formulaStyleIndex, failedFormulaStyleIndex);
try (InputStream input = openTemplateStream(template); ZipInputStream templateZip = new ZipInputStream(input);
OutputStream fileOutput = Files.newOutputStream(target); ZipOutputStream output = new ZipOutputStream(fileOutput)) {
boolean replaced = false;
@@ -1364,9 +1398,7 @@ public class ArBillServiceImpl implements IArBillService {
ZipEntry outputEntry = new ZipEntry(entry.getName());
output.putNextEntry(outputEntry);
if (TEMPLATE_SHEET_XML.equals(entry.getName())) {
writeOoxmlSheet(readUtf8(templateZip), bill, output, failedDetailStyleIndex,
trackingNoStyleIndex, failedTrackingNoStyleIndex,
detailNoStyleIndex, failedDetailNoStyleIndex, formulaStyleIndex, failedFormulaStyleIndex);
writeOoxmlSheet(readUtf8(templateZip), bill, output, exportStyleIndexes);
replaced = true;
} else if (TEMPLATE_STYLES_XML.equals(entry.getName())) {
styles.writeTo(output);
@@ -1425,10 +1457,8 @@ public class ArBillServiceImpl implements IArBillService {
/**
* 以 XML 流方式写入表头、分页明细和银行信息,避免 POI 在大数据量下移动行。
*/
private void writeOoxmlSheet(String templateXml, ArBill bill, OutputStream output, int failedDetailStyleIndex,
int trackingNoStyleIndex, int failedTrackingNoStyleIndex,
int detailNoStyleIndex, int failedDetailNoStyleIndex,
int formulaStyleIndex, int failedFormulaStyleIndex) throws IOException {
private void 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);
@@ -1461,15 +1491,13 @@ public class ArBillServiceImpl implements IArBillService {
}
attachFees(details);
for (ArBillDetail detail : details) {
writeOoxmlDetailRow(output, rowIndex++, detail, failedDetailStyleIndex,
trackingNoStyleIndex, failedTrackingNoStyleIndex,
detailNoStyleIndex, failedDetailNoStyleIndex, formulaStyleIndex, failedFormulaStyleIndex);
writeOoxmlDetailRow(output, rowIndex++, detail, styleIndexes);
}
lastDetailNo = details.get(details.size() - 1).getDetailNo();
}
int footerRow = Math.max(rowIndex, TEMPLATE_FOOTER_START_ROW);
writeOoxmlFooter(output, templateRows, footerRow, bill);
writeOoxmlFooter(output, templateRows, footerRow, bill, styleIndexes.generalNumber());
String suffix = shiftFooterMerges(templateXml.substring(sheetDataEnd), footerRow - TEMPLATE_FOOTER_START_ROW);
writeUtf8(output, suffix);
}
@@ -1578,8 +1606,8 @@ public class ArBillServiceImpl implements IArBillService {
return rows;
}
private void writeOoxmlFooter(OutputStream output, Map<Integer, String> templateRows, int footerRow, ArBill bill)
throws IOException {
private void writeOoxmlFooter(OutputStream output, Map<Integer, String> templateRows, int footerRow,
ArBill bill, int generalNumberStyleIndex) throws IOException {
int rowOffset = footerRow - TEMPLATE_FOOTER_START_ROW;
String totalAmountFormula = "SUM(S" + TEMPLATE_DATA_START_ROW + ":INDEX(S:S,ROW()-1))";
for (int offset = 0; offset < 4; offset++) {
@@ -1590,19 +1618,20 @@ public class ArBillServiceImpl implements IArBillService {
row = replaceCellXml(row, "B" + targetRowNumber, "银行信息", false);
row = replaceCellXml(row, "P" + targetRowNumber, "本期应收金额", false);
row = replaceFormulaCellXml(row, "S" + targetRowNumber, totalAmountFormula,
nvl(bill.getTotalAmount()));
nvl(bill.getTotalAmount()), generalNumberStyleIndex);
} else if (offset == 1) {
row = replaceCellXml(row, "B" + targetRowNumber, "开户名", false);
row = replaceCellXml(row, "C" + targetRowNumber, bill.getBankAccountName(), false);
row = replaceCellXml(row, "P" + targetRowNumber, "本期已核销金额", false);
row = replaceCellXml(row, "S" + targetRowNumber, bill.getWrittenOffAmount(), true);
row = replaceCellXml(row, "S" + targetRowNumber, bill.getWrittenOffAmount(), true,
generalNumberStyleIndex);
} else if (offset == 2) {
row = replaceCellXml(row, "B" + targetRowNumber, "账号", false);
row = replaceCellXml(row, "C" + targetRowNumber, exportBankAccountNo(bill.getBankAccountNo()), false);
row = replaceCellXml(row, "P" + targetRowNumber, "本期未核销金额", false);
String unwrittenAmountFormula = "S" + (targetRowNumber - 2) + "-S" + (targetRowNumber - 1);
row = replaceFormulaCellXml(row, "S" + targetRowNumber, unwrittenAmountFormula,
nvl(bill.getUnwrittenAmount()));
nvl(bill.getUnwrittenAmount()), generalNumberStyleIndex);
} else {
row = replaceCellXml(row, "B" + targetRowNumber, "开户行", false);
row = replaceCellXml(row, "C" + targetRowNumber, bill.getBankName(), false);
@@ -1611,23 +1640,21 @@ public class ArBillServiceImpl implements IArBillService {
}
}
private void writeOoxmlDetailRow(OutputStream output, int rowNumber, ArBillDetail detail, int failedDetailStyleIndex,
int trackingNoStyleIndex, int failedTrackingNoStyleIndex,
int detailNoStyleIndex, int failedDetailNoStyleIndex,
int formulaStyleIndex, int failedFormulaStyleIndex) throws IOException {
private void writeOoxmlDetailRow(OutputStream output, int rowNumber, ArBillDetail detail,
ExportStyleIndexes styleIndexes) throws IOException {
String failureDescription = exportBillingFailDescription(detail.getBillingFailStatus());
boolean failedDetail = !failureDescription.isEmpty();
int styleIndex = failedDetail ? failedDetailStyleIndex : TEMPLATE_DETAIL_STYLE_INDEX;
int sequenceStyleIndex = failedDetail ? failedDetailNoStyleIndex : detailNoStyleIndex;
int styleIndex = failedDetail ? styleIndexes.failedDetail() : TEMPLATE_DETAIL_STYLE_INDEX;
int sequenceStyleIndex = failedDetail ? styleIndexes.failedDetailNo() : styleIndexes.detailNo();
Object[] values = {
detail.getDetailNo(),
detail.getReceiveTime() == null ? "" : new SimpleDateFormat("yyyy-MM-dd").format(detail.getReceiveTime()),
detail.getOrderNo(), detail.getTrackingNo(),
StringUtils.isEmpty(detail.getProductName()) ? detail.getProductCode() : detail.getProductName(),
StringUtils.isEmpty(detail.getCountryName()) ? detail.getCountryCode() : detail.getCountryName(),
detail.getDestPostcode(), decimal(detail.getReceiveWeight(), 3), decimal(detail.getPkgLength(), 2),
decimal(detail.getPkgHeight(), 2), decimal(detail.getPkgWidth(), 2), decimal(detail.getVolumeWeight(), 3),
decimal(detail.getChargeWeight(), 3), exportUnitPrice(detail), exportProcessingFee(detail),
detail.getDestPostcode(), detail.getReceiveWeight(), detail.getPkgLength(),
detail.getPkgHeight(), detail.getPkgWidth(), detail.getVolumeWeight(),
detail.getChargeWeight(), exportUnitPrice(detail), exportProcessingFee(detail),
exportFormula(detail), exportFeeType(), detail.getCurrency(), detail.getTotalAmount(), exportRemark(detail),
failureDescription
};
@@ -1635,13 +1662,18 @@ public class ArBillServiceImpl implements IArBillService {
row.append("<row r=\"").append(rowNumber).append("\" ht=\"")
.append(EXPORT_DETAIL_ROW_HEIGHT).append("\" customHeight=\"true\" spans=\"1:21\">");
for (int index = 0; index < TEMPLATE_COLUMN_COUNT; index++) {
int excelColumnIndex = index + 1;
int cellStyleIndex = styleIndex;
if (index == 0) {
cellStyleIndex = sequenceStyleIndex;
} else if (index == TRACKING_NO_COLUMN_INDEX - 1) {
cellStyleIndex = failedDetail ? failedTrackingNoStyleIndex : trackingNoStyleIndex;
} else if (index == FORMULA_COLUMN_INDEX - 1) {
cellStyleIndex = failedDetail ? failedFormulaStyleIndex : formulaStyleIndex;
} else if (excelColumnIndex == CUSTOMER_ORDER_NO_COLUMN_INDEX) {
cellStyleIndex = failedDetail ? styleIndexes.failedCustomerOrderNo() : styleIndexes.customerOrderNo();
} else if (excelColumnIndex == TRACKING_NO_COLUMN_INDEX) {
cellStyleIndex = failedDetail ? styleIndexes.failedTrackingNo() : styleIndexes.trackingNo();
} else if (excelColumnIndex == FORMULA_COLUMN_INDEX) {
cellStyleIndex = failedDetail ? styleIndexes.failedFormula() : styleIndexes.formula();
} else if (GENERAL_NUMERIC_EXPORT_COLUMNS.contains(excelColumnIndex)) {
cellStyleIndex = failedDetail ? styleIndexes.failedGeneralNumber() : styleIndexes.generalNumber();
}
appendOoxmlCell(row, (char) ('A' + index) + String.valueOf(rowNumber), values[index], cellStyleIndex);
}
@@ -1699,13 +1731,28 @@ public class ArBillServiceImpl implements IArBillService {
return replaceCellXml(rowXml, reference, value, numeric, null);
}
private String replaceCellXml(String rowXml, String reference, Object value, boolean numeric, int styleIndex)
throws IOException {
return replaceCellXml(rowXml, reference, value, numeric, null, styleIndex);
}
private String replaceFormulaCellXml(String rowXml, String reference, String formula, Number cachedValue)
throws IOException {
return replaceCellXml(rowXml, reference, cachedValue, true, formula);
}
private String replaceFormulaCellXml(String rowXml, String reference, String formula, Number cachedValue,
int styleIndex) throws IOException {
return replaceCellXml(rowXml, reference, cachedValue, true, formula, styleIndex);
}
private String replaceCellXml(String rowXml, String reference, Object value, boolean numeric, String formula)
throws IOException {
return replaceCellXml(rowXml, reference, value, numeric, formula, null);
}
private String replaceCellXml(String rowXml, String reference, Object value, boolean numeric, String formula,
Integer styleIndex) throws IOException {
if (rowXml == null) {
throw new IOException("应收账单模板缺少行: " + reference);
}
@@ -1722,6 +1769,9 @@ public class ArBillServiceImpl implements IArBillService {
found = true;
String normalizedAttributes = attributes.replaceAll("(?i)\\s+t\\s*=\\s*[\\\"'][^\\\"']*[\\\"']", "")
.replaceAll("\\s*/\\s*$", "");
if (styleIndex != null) {
normalizedAttributes = replaceCellStyle(normalizedAttributes, styleIndex);
}
StringBuilder cell = new StringBuilder("<c").append(normalizedAttributes)
.append(" t=\"").append(numeric ? "n" : "inlineStr").append("\">");
if (numeric) {
@@ -1742,17 +1792,25 @@ public class ArBillServiceImpl implements IArBillService {
}
matcher.appendTail(result);
if (!found) {
return insertMissingCellXml(rowXml, reference, value, numeric, formula);
return insertMissingCellXml(rowXml, reference, value, numeric, formula, styleIndex);
}
return result.toString();
}
private String replaceCellStyle(String attributes, int styleIndex) {
Matcher matcher = CELL_STYLE_PATTERN.matcher(attributes);
if (!matcher.find()) {
return attributes + " s=\"" + styleIndex + "\"";
}
return matcher.replaceFirst(Matcher.quoteReplacement("s=\"" + styleIndex + "\""));
}
/**
* 兼容不同 Excel/WPS 版本保存后被省略的空单元格,按列顺序补回动态单元格。
* C3/P3 使用模板中对应的样式,避免客户名称或账单周期单元格变成无边框样式。
*/
private String insertMissingCellXml(String rowXml, String reference, Object value, boolean numeric, String formula)
throws IOException {
private String insertMissingCellXml(String rowXml, String reference, Object value, boolean numeric, String formula,
Integer styleIndex) throws IOException {
int rowEnd = rowXml.lastIndexOf("</row>");
if (rowEnd < 0) {
throw new IOException("应收账单模板缺少行结束节点: " + reference);
@@ -1768,7 +1826,7 @@ public class ArBillServiceImpl implements IArBillService {
}
}
String style = fallbackCellStyle(reference);
String style = styleIndex == null ? fallbackCellStyle(reference) : String.valueOf(styleIndex);
StringBuilder cell = new StringBuilder("<c r=\"").append(reference).append("\"");
if (!StringUtils.isEmpty(style)) {
cell.append(" s=\"").append(style).append("\"");
@@ -2027,10 +2085,6 @@ public class ArBillServiceImpl implements IArBillService {
return result.toString();
}
private BigDecimal decimal(BigDecimal value, int scale) {
return value == null ? null : value.setScale(scale, java.math.RoundingMode.HALF_UP);
}
private BigDecimal nvl(BigDecimal value) {
return value == null ? BigDecimal.ZERO : value;
}