diff --git a/bms-back/bms-system/src/main/java/com/bms/ar/service/impl/ArBillServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/ar/service/impl/ArBillServiceImpl.java index 7bf53c84..67c74020 100644 --- a/bms-back/bms-system/src/main/java/com/bms/ar/service/impl/ArBillServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/ar/service/impl/ArBillServiceImpl.java @@ -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 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 { "]*?)(?:/\\s*>|>(.*?))", 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( "(]*\\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( "(]*\\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("', sheetDataStart); int sheetDataEnd = templateXml.indexOf("", 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 templateRows, int footerRow, ArBill bill) - throws IOException { + private void writeOoxmlFooter(OutputStream output, Map 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(""); 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(""); 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(""); 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("