🐛 修复账单导出数值精度显示
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user