From 62722a9ce73e67958e221d39a3e40b03bda8a09e Mon Sep 17 00:00:00 2001 From: cst61 Date: Sun, 24 May 2026 08:48:39 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=9F=E6=88=90=E6=8A=B5=E6=89=A3=E8=A1=A8?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LbDailyUserTradeReportExportSupport.java | 772 +++++++++++++++--- 1 file changed, 662 insertions(+), 110 deletions(-) diff --git a/src/main/java/com/rj/service/support/LbDailyUserTradeReportExportSupport.java b/src/main/java/com/rj/service/support/LbDailyUserTradeReportExportSupport.java index a8a8b18..94d3658 100644 --- a/src/main/java/com/rj/service/support/LbDailyUserTradeReportExportSupport.java +++ b/src/main/java/com/rj/service/support/LbDailyUserTradeReportExportSupport.java @@ -6,6 +6,7 @@ import jakarta.servlet.http.HttpServletResponse; import org.apache.poi.ss.usermodel.BorderStyle; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.ComparisonOperator; import org.apache.poi.ss.usermodel.ConditionalFormattingRule; import org.apache.poi.ss.usermodel.DataFormat; @@ -34,7 +35,6 @@ import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; import java.util.Map; @@ -43,14 +43,24 @@ import java.util.Map; */ public final class LbDailyUserTradeReportExportSupport { - /** 导出工作簿固定 7 个 sheet(顺序与业务模板一致)。 */ + /** 导出工作簿固定 8 个 sheet(顺序与业务模板一致)。 */ private static final String[] EXPORT_SHEET_NAMES = { - "总表", "抵扣表", "支付表", "11点表", "断货表", "临时表2", "结果表" + "总表", "抵扣表", "断货表", "11点表", "支付表", "支付表检查", "临时表2", "结果表" }; private static final int SUMMARY_SHEET_INDEX = 0; private static final int DEDUCTION_SHEET_INDEX = 1; + private static final int OUT_OF_STOCK_SHEET_INDEX = 2; private static final int ELEVEN_CLOCK_SHEET_INDEX = 3; + private static final int PAYMENT_SHEET_INDEX = 4; + private static final int PAYMENT_CHECK_SHEET_INDEX = 5; + + private static final int PAYMENT_COL_NAME = 0; + private static final int PAYMENT_COL_ACTUAL = 1; + /** 支付表:实际收付 < 0 的记录移至 E、F 列 */ + private static final int PAYMENT_NEGATIVE_COL_NAME = 4; + private static final int PAYMENT_NEGATIVE_COL_ACTUAL = 5; + private static final int PAYMENT_LAST_COL = PAYMENT_NEGATIVE_COL_ACTUAL; private static final int COL_NICKNAME = 0; private static final int COL_YESTODAY_BUY = 1; @@ -67,6 +77,8 @@ public final class LbDailyUserTradeReportExportSupport { private static final int COL_SEQUENCE_LEFT = 0; private static final String DATA_TYPE_REPORT_SUM = "report_sum"; + /** 支付表 A/B 列固定追加行姓名 */ + private static final String PAYMENT_AB_SPECIAL_NAME = "刘瑞华"; private LbDailyUserTradeReportExportSupport() { } @@ -84,11 +96,18 @@ public final class LbDailyUserTradeReportExportSupport { } DateHeaderLabels labels = DateHeaderLabels.of(reportDate); - fillSummarySheet(workbook.getSheetAt(SUMMARY_SHEET_INDEX), labels, rows, styles); + List exportRows = prepareExportRows(rows); + fillSummarySheet(workbook.getSheetAt(SUMMARY_SHEET_INDEX), labels, exportRows, styles); Map deductionMap = deductionByUserName == null ? Map.of() : deductionByUserName; - fillDeductionSheet(workbook.getSheetAt(DEDUCTION_SHEET_INDEX), labels, rows, styles, deductionMap, false, true); - // 11点表:与抵扣表相同的数据与计算逻辑,最左侧序号列,不含最后一行汇总 - fillDeductionSheet(workbook.getSheetAt(ELEVEN_CLOCK_SHEET_INDEX), labels, rows, styles, deductionMap, true, false); + fillDeductionSheet(workbook.getSheetAt(DEDUCTION_SHEET_INDEX), labels, exportRows, styles, deductionMap, false, true, false); + // 断货表:总表中当日买入金额为 0 的明细行,最左侧序号列 + fillOutOfStockSheet(workbook.getSheetAt(OUT_OF_STOCK_SHEET_INDEX), labels, exportRows, styles); + // 11点表:与抵扣表相同的数据与计算逻辑,最左侧序号列,不含最后一行汇总,数值四舍五入取整 + fillDeductionSheet(workbook.getSheetAt(ELEVEN_CLOCK_SHEET_INDEX), labels, exportRows, styles, deductionMap, true, false, true); + // 支付表:A/B 列含刘瑞华,按 B 列倒序;< 0 移至 E/F 列 + Sheet paymentSheet = workbook.getSheetAt(PAYMENT_SHEET_INDEX); + fillPaymentSheet(paymentSheet, exportRows, styles, deductionMap); + fillPaymentCheckSheet(workbook.getSheetAt(PAYMENT_CHECK_SHEET_INDEX), paymentSheet, styles); String filename = "当日汇总报表" + reportDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")) + ".xlsx"; String encoded = URLEncoder.encode(filename, StandardCharsets.UTF_8).replaceAll("\\+", "%20"); @@ -107,46 +126,444 @@ public final class LbDailyUserTradeReportExportSupport { private static void fillSummarySheet( Sheet sheet, DateHeaderLabels labels, - List rows, + List exportRows, SheetStyles styles) { - int r = writeHeaderRow(sheet, labels, styles, false); - sheet.createFreezePane(0, 1); - sheet.setAutoFilter(new CellRangeAddress(0, 0, 0, SUMMARY_LAST_COL)); - - for (LbDailyUserTradeReport item : rows) { - Row row = sheet.createRow(r++); - applyRowHeight(row, styles); - RowStylePair rowStyles = rowStyles(item, r, styles); - writeBaseDataCells(row, item, rowStyles, roundedInt(item.getDikouAmt()), false); - } - - applyDiffNegativeFormatting(sheet, r, false); - autoSizeColumns(sheet, SUMMARY_LAST_COL); + fillSummaryLikeSheet(sheet, labels, exportRows, styles, false, false); } - /** - * 抵扣表 / 11点表:先按总表写入各行;抵扣金额列改为 lb_deduction_amount 匹配值; - * 「实际收付」= 抵扣金额 + 原实际收付;明细按实际收付倒序。 - * 抵扣表最后一行汇总行的抵扣金额、实际收付为本列明细之和;11点表不输出汇总行,最左侧为序号列。 - */ - private static void fillDeductionSheet( + /** 断货表:复制总表中当日买入金额(dailyBuyAmt)为 0 的明细行,不含汇总行,最左侧序号列,数值四舍五入取整。 */ + private static void fillOutOfStockSheet( + Sheet sheet, + DateHeaderLabels labels, + List exportRows, + SheetStyles styles) { + fillSummaryLikeSheet(sheet, labels, filterZeroDailyBuyRows(exportRows), styles, true, true); + } + + private static List filterZeroDailyBuyRows(List exportRows) { + List filtered = new ArrayList<>(); + for (LbDailyUserTradeReport row : exportRows) { + if (DATA_TYPE_REPORT_SUM.equals(row.getDataType())) { + continue; + } + if (defaultZero(row.getDailyBuyAmt()).compareTo(BigDecimal.ZERO) == 0) { + filtered.add(row); + } + } + return filtered; + } + + private static void fillSummaryLikeSheet( Sheet sheet, DateHeaderLabels labels, List rows, SheetStyles styles, - Map deductionByUserName, - boolean sequenceOnLeft, - boolean includeSummaryRow) { + boolean roundValues, + boolean sequenceOnLeft) { int lastCol = sequenceOnLeft ? ELEVEN_CLOCK_LAST_COL : SUMMARY_LAST_COL; int r = writeHeaderRow(sheet, labels, styles, sequenceOnLeft); sheet.createFreezePane(0, 1); sheet.setAutoFilter(new CellRangeAddress(0, 0, 0, lastCol)); - long totalDikou = 0; - long totalActual = 0; + int sequence = 0; + for (LbDailyUserTradeReport item : rows) { + Row row = sheet.createRow(r++); + applyRowHeight(row, styles); + RowStylePair rowStyles = rowStyles(item, r, styles, roundValues); + if (sequenceOnLeft) { + sequence++; + createIntCell(row, COL_SEQUENCE_LEFT, sequence, rowStyles.sequenceStyle); + } + writeBaseDataCells(row, item, rowStyles, defaultZero(item.getDikouAmt()), sequenceOnLeft, roundValues); + } + + applyDiffNegativeFormatting(sheet, r, sequenceOnLeft); + autoSizeColumns(sheet, lastCol); + } + + /** + * 支付表:A/B 列含刘瑞华汇总行,与 |F 列总和| 平衡后按 B 列倒序;< 0 写入 E/F 列(升序)。 + */ + private static void fillPaymentSheet( + Sheet sheet, + List exportRows, + SheetStyles styles, + Map deductionByUserName) { + boolean roundValues = true; + int r = 0; + Row header = sheet.createRow(r++); + header.setHeightInPoints(20 * styles.rowHeightFactor); + createHeaderCell(header, PAYMENT_COL_NAME, "姓名", styles.headerStyle); + createHeaderCell(header, PAYMENT_COL_ACTUAL, "实际收付", styles.headerStyle); + createHeaderCell(header, PAYMENT_NEGATIVE_COL_NAME, "姓名", styles.headerStyle); + createHeaderCell(header, PAYMENT_NEGATIVE_COL_ACTUAL, "实际收付", styles.headerStyle); + + sheet.createFreezePane(0, 1); + sheet.setAutoFilter(new CellRangeAddress(0, 0, 0, PAYMENT_LAST_COL)); + + List abEntries = new ArrayList<>(); + List negativeRows = new ArrayList<>(); + for (LbDailyUserTradeReport item : elevenClockDetailRows(exportRows, deductionByUserName)) { + BigDecimal actual = computeActualReceiptsForDeduction(item, deductionByUserName, roundValues); + if (actual.compareTo(BigDecimal.ZERO) >= 0) { + abEntries.add(PaymentAbEntry.detail( + item.getNickname() == null ? "" : item.getNickname(), + actual, + item)); + } else { + negativeRows.add(item); + } + } + abEntries.add(PaymentAbEntry.special( + computeDeductionSheetSummaryActual(exportRows, deductionByUserName))); + balancePaymentSheetColumnTotals(abEntries, negativeRows, deductionByUserName, roundValues); + abEntries.sort((a, b) -> b.sortValue().compareTo(a.sortValue())); + + int positiveRow = 1; + for (PaymentAbEntry entry : abEntries) { + Row row = sheet.getRow(positiveRow); + if (row == null) { + row = sheet.createRow(positiveRow); + applyRowHeight(row, styles); + } + if (entry.isSpecialRow()) { + createTextCell(row, PAYMENT_COL_NAME, entry.name(), paymentTextStyle(positiveRow, styles)); + createPaymentActualCell( + row, PAYMENT_COL_ACTUAL, entry.actualAbs(), paymentActualStyle(positiveRow, styles)); + } else { + RowStylePair rowStyles = rowStyles(entry.detailRow(), positiveRow, styles, roundValues); + createTextCell(row, PAYMENT_COL_NAME, entry.name(), rowStyles.textStyle); + createPaymentActualCell( + row, PAYMENT_COL_ACTUAL, entry.actualAbs(), paymentActualStyle(positiveRow, styles)); + } + positiveRow++; + } + + negativeRows.sort((a, b) -> computeActualReceiptsForDeduction(a, deductionByUserName, roundValues) + .compareTo(computeActualReceiptsForDeduction(b, deductionByUserName, roundValues))); + + int negativeRow = 1; + for (LbDailyUserTradeReport item : negativeRows) { + Row row = sheet.getRow(negativeRow); + if (row == null) { + row = sheet.createRow(negativeRow); + applyRowHeight(row, styles); + } + RowStylePair rowStyles = rowStyles(item, negativeRow, styles, roundValues); + createTextCell( + row, + PAYMENT_NEGATIVE_COL_NAME, + item.getNickname() == null ? "" : item.getNickname(), + rowStyles.textStyle); + createPaymentActualCell( + row, + PAYMENT_NEGATIVE_COL_ACTUAL, + computeActualReceiptsForDeduction(item, deductionByUserName, roundValues), + paymentActualStyle(negativeRow, styles)); + negativeRow++; + } + + int nextRow = Math.max(positiveRow, negativeRow); + applyPaymentNegativeActualFormatting(sheet, nextRow); + autoSizeColumns(sheet, PAYMENT_LAST_COL); + } + + /** + * 支付表检查:完整复制支付表,末尾追加一行「求和」,B、F 列为上方数据行之和。 + */ + private static void fillPaymentCheckSheet(Sheet target, Sheet paymentSource, SheetStyles styles) { + copyPaymentSheetContent(paymentSource, target); + + BigDecimal sumB = BigDecimal.ZERO; + BigDecimal sumF = BigDecimal.ZERO; + int lastDataRow = paymentSource.getLastRowNum(); + for (int rowIdx = 1; rowIdx <= lastDataRow; rowIdx++) { + Row row = paymentSource.getRow(rowIdx); + if (row == null) { + continue; + } + sumB = sumB.add(readNumericCellValue(row.getCell(PAYMENT_COL_ACTUAL))); + sumF = sumF.add(readNumericCellValue(row.getCell(PAYMENT_NEGATIVE_COL_ACTUAL))); + } + + int sumRowIdx = lastDataRow + 1; + Row sumRow = target.createRow(sumRowIdx); + applyRowHeight(sumRow, styles); + createTextCell(sumRow, PAYMENT_COL_NAME, "求和", styles.summaryTextStyle); + createPaymentActualCell(sumRow, PAYMENT_COL_ACTUAL, sumB, styles.summaryRoundedAmountStyle); + createPaymentActualCell(sumRow, PAYMENT_NEGATIVE_COL_ACTUAL, sumF, styles.summaryRoundedAmountStyle); + + applyPaymentNegativeActualFormatting(target, sumRowIdx); + autoSizeColumns(target, PAYMENT_LAST_COL); + } + + private static void copyPaymentSheetContent(Sheet source, Sheet target) { + target.setDefaultRowHeight(source.getDefaultRowHeight()); + for (int col = 0; col <= PAYMENT_LAST_COL; col++) { + target.setColumnWidth(col, source.getColumnWidth(col)); + } + + for (int rowIdx = 0; rowIdx <= source.getLastRowNum(); rowIdx++) { + Row srcRow = source.getRow(rowIdx); + if (srcRow == null) { + continue; + } + Row destRow = target.createRow(rowIdx); + destRow.setHeight(srcRow.getHeight()); + for (int col = 0; col <= PAYMENT_LAST_COL; col++) { + Cell srcCell = srcRow.getCell(col); + if (srcCell == null) { + continue; + } + copyCellValueAndStyle(srcCell, destRow.createCell(col)); + } + } + + target.createFreezePane(0, 1); + target.setAutoFilter(new CellRangeAddress(0, 0, 0, PAYMENT_LAST_COL)); + } + + private static void copyCellValueAndStyle(Cell source, Cell target) { + target.setCellStyle(source.getCellStyle()); + switch (source.getCellType()) { + case STRING -> target.setCellValue(source.getStringCellValue()); + case NUMERIC -> target.setCellValue(source.getNumericCellValue()); + case BOOLEAN -> target.setCellValue(source.getBooleanCellValue()); + case FORMULA -> target.setCellFormula(source.getCellFormula()); + case BLANK -> target.setBlank(); + default -> { + } + } + } + + private static BigDecimal readNumericCellValue(Cell cell) { + if (cell == null || cell.getCellType() == CellType.BLANK) { + return BigDecimal.ZERO; + } + if (cell.getCellType() == CellType.NUMERIC || cell.getCellType() == CellType.FORMULA) { + return BigDecimal.valueOf(cell.getNumericCellValue()); + } + if (cell.getCellType() == CellType.STRING) { + String text = cell.getStringCellValue(); + if (text == null || text.isBlank()) { + return BigDecimal.ZERO; + } + try { + return new BigDecimal(text.trim()); + } catch (NumberFormatException ignored) { + return BigDecimal.ZERO; + } + } + return BigDecimal.ZERO; + } + + /** 抵扣表汇总行「实际收付」= 明细行(抵扣金额 + 原实际收付)之和,与 fillDeductionSheet 口径一致。 */ + private static BigDecimal computeDeductionSheetSummaryActual( + List exportRows, + Map deductionByUserName) { + BigDecimal totalActual = BigDecimal.ZERO; + for (LbDailyUserTradeReport item : orderRowsForDeductionSheet(exportRows, deductionByUserName, false)) { + if (DATA_TYPE_REPORT_SUM.equals(item.getDataType())) { + continue; + } + totalActual = totalActual.add( + computeActualReceiptsForDeduction(item, deductionByUserName, false)); + } + return totalActual; + } + + /** 支付表 B 列展示/排序值:实际收付绝对值四舍五入取整。 */ + private static BigDecimal paymentAbDisplayValue(BigDecimal actual) { + return roundHalfUp(defaultZero(actual).abs()); + } + + /** 支付表 F 列展示值:实际收付四舍五入取整(保留符号,与写入 F 列口径一致)。 */ + private static BigDecimal paymentFColumnDisplayValue(BigDecimal actual) { + return roundHalfUp(defaultZero(actual)); + } + + /** + * 支付表 B/F 列平衡:|F 列总和| 与 B 列总和不一致时,加减刘瑞华 B 列数值使两列总和相等。 + */ + private static void balancePaymentSheetColumnTotals( + List abEntries, + List negativeRows, + Map deductionByUserName, + boolean roundValues) { + BigDecimal fSum = BigDecimal.ZERO; + for (LbDailyUserTradeReport item : negativeRows) { + BigDecimal actual = computeActualReceiptsForDeduction(item, deductionByUserName, roundValues); + fSum = fSum.add(paymentFColumnDisplayValue(actual)); + } + BigDecimal fTotalAbs = fSum.abs(); + + BigDecimal bSum = BigDecimal.ZERO; + for (PaymentAbEntry entry : abEntries) { + bSum = bSum.add(entry.actualAbs()); + } + if (bSum.compareTo(fTotalAbs) == 0) { + return; + } + + BigDecimal diff = fTotalAbs.subtract(bSum); + for (int i = 0; i < abEntries.size(); i++) { + PaymentAbEntry entry = abEntries.get(i); + if (entry.isSpecialRow()) { + abEntries.set(i, entry.withActualAbs(entry.actualAbs().add(diff))); + return; + } + } + } + + private record PaymentAbEntry(String name, BigDecimal actualAbs, LbDailyUserTradeReport detailRow) { + static PaymentAbEntry detail(String name, BigDecimal actual, LbDailyUserTradeReport detailRow) { + return new PaymentAbEntry(name, paymentAbDisplayValue(actual), detailRow); + } + + static PaymentAbEntry special(BigDecimal deductionSummaryActual) { + return new PaymentAbEntry( + PAYMENT_AB_SPECIAL_NAME, + paymentAbDisplayValue(deductionSummaryActual), + null); + } + + boolean isSpecialRow() { + return detailRow == null; + } + + PaymentAbEntry withActualAbs(BigDecimal newActualAbs) { + return new PaymentAbEntry(name, newActualAbs, detailRow); + } + + BigDecimal sortValue() { + return actualAbs; + } + } + + private static CellStyle paymentActualDecimalStyle(int rowIndex, SheetStyles styles) { + return (rowIndex % 2 == 0) ? styles.zebraPaymentActualDecimalStyle : styles.paymentActualDecimalStyle; + } + + private static CellStyle paymentTextStyle(int rowIndex, SheetStyles styles) { + return (rowIndex % 2 == 0) ? styles.zebraTextStyle : styles.textStyle; + } + + /** 11点表 / 支付表共用的明细行:按实际收付倒序,不含汇总行。 */ + private static List elevenClockDetailRows( + List exportRows, + Map deductionByUserName) { + List rows = new ArrayList<>(); + for (LbDailyUserTradeReport row : orderRowsForDeductionSheet(exportRows, deductionByUserName, true)) { + if (!DATA_TYPE_REPORT_SUM.equals(row.getDataType())) { + rows.add(row); + } + } + return rows; + } + + private static void applyPaymentNegativeActualFormatting(Sheet sheet, int nextRowIndex) { + if (nextRowIndex > 1) { + SheetConditionalFormatting scf = sheet.getSheetConditionalFormatting(); + ConditionalFormattingRule negative = scf.createConditionalFormattingRule(ComparisonOperator.LT, "0"); + FontFormatting ff = negative.createFontFormatting(); + ff.setFontColorIndex(IndexedColors.DARK_RED.getIndex()); + ff.setFontStyle(false, false); + scf.addConditionalFormatting( + new CellRangeAddress[]{ + new CellRangeAddress( + 1, nextRowIndex - 1, PAYMENT_NEGATIVE_COL_ACTUAL, PAYMENT_NEGATIVE_COL_ACTUAL) + }, + negative); + } + } + + /** + * 导出行序:明细按差额倒序,汇总行固定在最后。 + * 若库中无 report_sum,则按明细字段聚合生成一条汇总行(与 calculateReportSum 口径一致)。 + */ + private static List prepareExportRows(List rows) { + if (rows == null || rows.isEmpty()) { + return List.of(); + } + List details = new ArrayList<>(); + LbDailyUserTradeReport summaryFromDb = null; + for (LbDailyUserTradeReport row : rows) { + if (DATA_TYPE_REPORT_SUM.equals(row.getDataType())) { + summaryFromDb = row; + } else { + details.add(row); + } + } + details.sort((a, b) -> { + BigDecimal da = a.getDiffAmt() == null ? BigDecimal.ZERO : a.getDiffAmt(); + BigDecimal db = b.getDiffAmt() == null ? BigDecimal.ZERO : b.getDiffAmt(); + return db.compareTo(da); + }); + LbDailyUserTradeReport summaryRow = + summaryFromDb != null ? summaryFromDb : (details.isEmpty() ? null : buildSummaryRowFromDetails(details)); + List ordered = new ArrayList<>(details.size() + (summaryRow == null ? 0 : 1)); + ordered.addAll(details); + if (summaryRow != null) { + ordered.add(summaryRow); + } + return ordered; + } + + private static LbDailyUserTradeReport buildSummaryRowFromDetails(List details) { + BigDecimal yestodayBuyAmt = BigDecimal.ZERO; + BigDecimal dailySellAmt = BigDecimal.ZERO; + BigDecimal dailyBuyAmt = BigDecimal.ZERO; + BigDecimal serviceAmt = BigDecimal.ZERO; + BigDecimal diffAmt = BigDecimal.ZERO; + BigDecimal dikouAmt = BigDecimal.ZERO; + for (LbDailyUserTradeReport row : details) { + yestodayBuyAmt = yestodayBuyAmt.add(defaultZero(row.getYestodayBuyAmt())); + dailySellAmt = dailySellAmt.add(defaultZero(row.getDailySellAmt())); + dailyBuyAmt = dailyBuyAmt.add(defaultZero(row.getDailyBuyAmt())); + serviceAmt = serviceAmt.add(defaultZero(row.getServiceAmt())); + diffAmt = diffAmt.add(defaultZero(row.getDiffAmt())); + dikouAmt = dikouAmt.add(defaultZero(row.getDikouAmt())); + } + LbDailyUserTradeReport summary = new LbDailyUserTradeReport(); + summary.setDataType(DATA_TYPE_REPORT_SUM); + summary.setYestodayBuyAmt(yestodayBuyAmt); + summary.setDailySellAmt(dailySellAmt); + summary.setDailyBuyAmt(dailyBuyAmt); + summary.setServiceAmt(serviceAmt); + summary.setDiffAmt(diffAmt); + summary.setDikouAmt(dikouAmt); + summary.setActualReceiptsPayments(diffAmt.subtract(serviceAmt)); + return summary; + } + + private static BigDecimal defaultZero(BigDecimal value) { + return value == null ? BigDecimal.ZERO : value; + } + + /** + * 抵扣表 / 11点表:先按总表写入各行;抵扣金额列改为 lb_deduction_amount 匹配值; + * 「实际收付」= 抵扣金额 + 原实际收付;明细按实际收付倒序。 + * 抵扣表最后一行汇总行的抵扣金额、实际收付为本列明细之和;11点表不输出汇总行,最左侧为序号列,数值四舍五入取整。 + */ + private static void fillDeductionSheet( + Sheet sheet, + DateHeaderLabels labels, + List exportRows, + SheetStyles styles, + Map deductionByUserName, + boolean sequenceOnLeft, + boolean includeSummaryRow, + boolean roundValues) { + int lastCol = sequenceOnLeft ? ELEVEN_CLOCK_LAST_COL : SUMMARY_LAST_COL; + int r = writeHeaderRow(sheet, labels, styles, sequenceOnLeft); + sheet.createFreezePane(0, 1); + sheet.setAutoFilter(new CellRangeAddress(0, 0, 0, lastCol)); + + BigDecimal totalDikou = BigDecimal.ZERO; + BigDecimal totalActual = BigDecimal.ZERO; int sequence = 0; - for (LbDailyUserTradeReport item : orderRowsForDeductionSheet(rows, deductionByUserName)) { + for (LbDailyUserTradeReport item : orderRowsForDeductionSheet(exportRows, deductionByUserName, roundValues)) { boolean isSummaryRow = DATA_TYPE_REPORT_SUM.equals(item.getDataType()); if (!includeSummaryRow && isSummaryRow) { continue; @@ -154,33 +571,31 @@ public final class LbDailyUserTradeReportExportSupport { Row row = sheet.createRow(r++); applyRowHeight(row, styles); - RowStylePair rowStyles = rowStyles(item, r, styles); + RowStylePair rowStyles = rowStyles(item, r, styles, roundValues); // 第1步:与总表一致(其它列含汇总行均沿用报表原值) - long dikouFromReport = roundedInt(item.getDikouAmt()); - long actualReceiptsPayments = roundedInt(item.getActualReceiptsPayments()); - writeBaseDataCells(row, item, rowStyles, dikouFromReport, sequenceOnLeft); + writeBaseDataCells(row, item, rowStyles, defaultZero(item.getDikouAmt()), sequenceOnLeft, roundValues); if (isSummaryRow) { // 汇总行:抵扣金额、实际收付 = 上方明细行之和 - createIntCell(row, dataCol(COL_DIKOU, sequenceOnLeft), totalDikou, rowStyles.numberStyle); - createIntCell(row, dataCol(COL_ACTUAL, sequenceOnLeft), totalActual, rowStyles.numberStyle); + createNumericCell(row, dataCol(COL_DIKOU, sequenceOnLeft), totalDikou, rowStyles, roundValues); + createNumericCell(row, dataCol(COL_ACTUAL, sequenceOnLeft), totalActual, rowStyles, roundValues); } else { if (sequenceOnLeft) { sequence++; - createIntCell(row, COL_SEQUENCE_LEFT, sequence, rowStyles.numberStyle); + createIntCell(row, COL_SEQUENCE_LEFT, sequence, rowStyles.sequenceStyle); } // 第2步:按昵称 + 报表日匹配 lb_deduction_amount,覆盖抵扣金额列 - long dikouFromDb = roundedInt(lookupDeductionAmt(item.getNickname(), deductionByUserName)); - createIntCell(row, dataCol(COL_DIKOU, sequenceOnLeft), dikouFromDb, rowStyles.numberStyle); + BigDecimal dikouFromDb = lookupDeductionAmt(item.getNickname(), deductionByUserName); + createNumericCell(row, dataCol(COL_DIKOU, sequenceOnLeft), dikouFromDb, rowStyles, roundValues); // 第3步:抵扣金额 + 原实际收付,覆盖「实际收付」列 - long dikouPlusActual = computeActualReceiptsForDeduction(item, deductionByUserName); - createIntCell(row, dataCol(COL_ACTUAL, sequenceOnLeft), dikouPlusActual, rowStyles.numberStyle); + BigDecimal dikouPlusActual = computeActualReceiptsForDeduction(item, deductionByUserName, roundValues); + createNumericCell(row, dataCol(COL_ACTUAL, sequenceOnLeft), dikouPlusActual, rowStyles, roundValues); - totalDikou += dikouFromDb; - totalActual += dikouPlusActual; + totalDikou = totalDikou.add(dikouFromDb); + totalActual = totalActual.add(dikouPlusActual); } } @@ -188,34 +603,40 @@ public final class LbDailyUserTradeReportExportSupport { autoSizeColumns(sheet, lastCol); } - /** 抵扣表:明细按计算后的实际收付倒序,汇总行保持在最后。 */ + /** 抵扣表:明细按计算后的实际收付倒序,汇总行保持在最后(exportRows 已保证末尾有汇总行)。 */ private static List orderRowsForDeductionSheet( - List rows, - Map deductionByUserName) { + List exportRows, + Map deductionByUserName, + boolean roundValues) { List details = new ArrayList<>(); - List summaries = new ArrayList<>(); - for (LbDailyUserTradeReport row : rows) { + LbDailyUserTradeReport summaryRow = null; + for (LbDailyUserTradeReport row : exportRows) { if (DATA_TYPE_REPORT_SUM.equals(row.getDataType())) { - summaries.add(row); + summaryRow = row; } else { details.add(row); } } - details.sort((a, b) -> Long.compare( - computeActualReceiptsForDeduction(b, deductionByUserName), - computeActualReceiptsForDeduction(a, deductionByUserName))); - List ordered = new ArrayList<>(details.size() + summaries.size()); + details.sort((a, b) -> computeActualReceiptsForDeduction(b, deductionByUserName, roundValues) + .compareTo(computeActualReceiptsForDeduction(a, deductionByUserName, roundValues))); + List ordered = new ArrayList<>(details.size() + (summaryRow == null ? 0 : 1)); ordered.addAll(details); - ordered.addAll(summaries); + if (summaryRow != null) { + ordered.add(summaryRow); + } return ordered; } - private static long computeActualReceiptsForDeduction( + private static BigDecimal computeActualReceiptsForDeduction( LbDailyUserTradeReport item, - Map deductionByUserName) { - long dikouFromDb = roundedInt(lookupDeductionAmt(item.getNickname(), deductionByUserName)); - long actualReceiptsPayments = roundedInt(item.getActualReceiptsPayments()); - return dikouFromDb + actualReceiptsPayments; + Map deductionByUserName, + boolean roundValues) { + BigDecimal dikou = lookupDeductionAmt(item.getNickname(), deductionByUserName); + BigDecimal actual = defaultZero(item.getActualReceiptsPayments()); + if (roundValues) { + return roundHalfUp(dikou).add(roundHalfUp(actual)); + } + return dikou.add(actual); } private static int dataCol(int col, boolean sequenceOnLeft) { @@ -248,28 +669,39 @@ public final class LbDailyUserTradeReportExportSupport { Row row, LbDailyUserTradeReport item, RowStylePair rowStyles, - long dikouAmt, - boolean sequenceOnLeft) { + BigDecimal dikouAmt, + boolean sequenceOnLeft, + boolean roundValues) { createTextCell( row, dataCol(COL_NICKNAME, sequenceOnLeft), item.getNickname() == null ? "" : item.getNickname(), rowStyles.textStyle); - createIntCell(row, dataCol(COL_YESTODAY_BUY, sequenceOnLeft), roundedInt(item.getYestodayBuyAmt()), rowStyles.numberStyle); - createIntCell(row, dataCol(COL_DAILY_SELL, sequenceOnLeft), roundedInt(item.getDailySellAmt()), rowStyles.numberStyle); - createIntCell(row, dataCol(COL_DAILY_BUY, sequenceOnLeft), roundedInt(item.getDailyBuyAmt()), rowStyles.numberStyle); - createIntCell(row, dataCol(COL_SERVICE, sequenceOnLeft), roundedInt(item.getServiceAmt()), rowStyles.numberStyle); - createIntCell(row, dataCol(COL_DIFF, sequenceOnLeft), roundedInt(item.getDiffAmt()), rowStyles.numberStyle); - createIntCell(row, dataCol(COL_ACTUAL, sequenceOnLeft), roundedInt(item.getActualReceiptsPayments()), rowStyles.numberStyle); - createIntCell(row, dataCol(COL_DIKOU, sequenceOnLeft), dikouAmt, rowStyles.numberStyle); + createNumericCell(row, dataCol(COL_YESTODAY_BUY, sequenceOnLeft), item.getYestodayBuyAmt(), rowStyles, roundValues); + createNumericCell(row, dataCol(COL_DAILY_SELL, sequenceOnLeft), item.getDailySellAmt(), rowStyles, roundValues); + createNumericCell(row, dataCol(COL_DAILY_BUY, sequenceOnLeft), item.getDailyBuyAmt(), rowStyles, roundValues); + createNumericCell(row, dataCol(COL_SERVICE, sequenceOnLeft), item.getServiceAmt(), rowStyles, roundValues); + createNumericCell(row, dataCol(COL_DIFF, sequenceOnLeft), item.getDiffAmt(), rowStyles, roundValues); + createNumericCell(row, dataCol(COL_ACTUAL, sequenceOnLeft), item.getActualReceiptsPayments(), rowStyles, roundValues); + createNumericCell(row, dataCol(COL_DIKOU, sequenceOnLeft), dikouAmt, rowStyles, roundValues); } - private static RowStylePair rowStyles(LbDailyUserTradeReport item, int rowIndex, SheetStyles styles) { + private static RowStylePair rowStyles(LbDailyUserTradeReport item, int rowIndex, SheetStyles styles, boolean roundValues) { boolean isSummary = DATA_TYPE_REPORT_SUM.equals(item.getDataType()); boolean zebra = (rowIndex % 2 == 0); CellStyle textStyle = isSummary ? styles.summaryTextStyle : (zebra ? styles.zebraTextStyle : styles.textStyle); - CellStyle numberStyle = isSummary ? styles.summaryIntStyle : (zebra ? styles.zebraIntStyle : styles.intStyle); - return new RowStylePair(textStyle, numberStyle); + CellStyle numberStyle; + if (roundValues) { + numberStyle = isSummary + ? styles.summaryRoundedAmountStyle + : (zebra ? styles.zebraRoundedAmountStyle : styles.roundedAmountStyle); + } else { + numberStyle = isSummary + ? styles.summaryAmountStyle + : (zebra ? styles.zebraAmountStyle : styles.amountStyle); + } + CellStyle sequenceStyle = isSummary ? styles.summarySequenceStyle : (zebra ? styles.zebraSequenceStyle : styles.sequenceStyle); + return new RowStylePair(textStyle, numberStyle, sequenceStyle); } private static BigDecimal lookupDeductionAmt(String nickname, Map deductionByUserName) { @@ -317,13 +749,6 @@ public final class LbDailyUserTradeReportExportSupport { } } - private static long roundedInt(BigDecimal value) { - if (value == null) { - return 0L; - } - return value.setScale(0, RoundingMode.HALF_UP).longValue(); - } - private static void createHeaderCell(Row row, int col, String text, CellStyle style) { Cell cell = row.createCell(col); cell.setCellValue(text); @@ -342,6 +767,37 @@ public final class LbDailyUserTradeReportExportSupport { cell.setCellStyle(style); } + private static void createDecimalCell(Row row, int col, BigDecimal value, CellStyle style) { + Cell cell = row.createCell(col); + cell.setCellValue(defaultZero(value).doubleValue()); + cell.setCellStyle(style); + } + + private static CellStyle paymentActualStyle(int rowIndex, SheetStyles styles) { + return (rowIndex % 2 == 0) ? styles.zebraPaymentActualStyle : styles.paymentActualStyle; + } + + private static void createPaymentActualCell(Row row, int col, BigDecimal value, CellStyle style) { + createIntCell(row, col, roundHalfUp(value).longValue(), style); + } + + private static void createNumericCell( + Row row, + int col, + BigDecimal value, + RowStylePair rowStyles, + boolean roundValues) { + if (roundValues) { + createIntCell(row, col, roundHalfUp(value).longValue(), rowStyles.numberStyle); + } else { + createDecimalCell(row, col, value, rowStyles.numberStyle); + } + } + + private static BigDecimal roundHalfUp(BigDecimal value) { + return defaultZero(value).setScale(0, RoundingMode.HALF_UP); + } + private record DateHeaderLabels(String yestodayBuyLabel, String dailySellLabel, String dailyBuyLabel) { static DateHeaderLabels of(LocalDate reportDate) { DateTimeFormatter mmdd = DateTimeFormatter.ofPattern("MM-dd"); @@ -351,7 +807,7 @@ public final class LbDailyUserTradeReportExportSupport { } } - private record RowStylePair(CellStyle textStyle, CellStyle numberStyle) { + private record RowStylePair(CellStyle textStyle, CellStyle numberStyle, CellStyle sequenceStyle) { } private static final class SheetStyles { @@ -359,26 +815,56 @@ public final class LbDailyUserTradeReportExportSupport { final CellStyle headerStyle; final CellStyle textStyle; final CellStyle zebraTextStyle; - final CellStyle intStyle; - final CellStyle zebraIntStyle; + final CellStyle amountStyle; + final CellStyle zebraAmountStyle; + final CellStyle roundedAmountStyle; + final CellStyle zebraRoundedAmountStyle; + final CellStyle paymentActualStyle; + final CellStyle zebraPaymentActualStyle; + final CellStyle paymentActualDecimalStyle; + final CellStyle zebraPaymentActualDecimalStyle; + final CellStyle sequenceStyle; + final CellStyle zebraSequenceStyle; final CellStyle summaryTextStyle; - final CellStyle summaryIntStyle; + final CellStyle summaryAmountStyle; + final CellStyle summaryRoundedAmountStyle; + final CellStyle summarySequenceStyle; private SheetStyles( CellStyle headerStyle, CellStyle textStyle, CellStyle zebraTextStyle, - CellStyle intStyle, - CellStyle zebraIntStyle, + CellStyle amountStyle, + CellStyle zebraAmountStyle, + CellStyle roundedAmountStyle, + CellStyle zebraRoundedAmountStyle, + CellStyle paymentActualStyle, + CellStyle zebraPaymentActualStyle, + CellStyle paymentActualDecimalStyle, + CellStyle zebraPaymentActualDecimalStyle, + CellStyle sequenceStyle, + CellStyle zebraSequenceStyle, CellStyle summaryTextStyle, - CellStyle summaryIntStyle) { + CellStyle summaryAmountStyle, + CellStyle summaryRoundedAmountStyle, + CellStyle summarySequenceStyle) { this.headerStyle = headerStyle; this.textStyle = textStyle; this.zebraTextStyle = zebraTextStyle; - this.intStyle = intStyle; - this.zebraIntStyle = zebraIntStyle; + this.amountStyle = amountStyle; + this.zebraAmountStyle = zebraAmountStyle; + this.roundedAmountStyle = roundedAmountStyle; + this.zebraRoundedAmountStyle = zebraRoundedAmountStyle; + this.paymentActualStyle = paymentActualStyle; + this.zebraPaymentActualStyle = zebraPaymentActualStyle; + this.paymentActualDecimalStyle = paymentActualDecimalStyle; + this.zebraPaymentActualDecimalStyle = zebraPaymentActualDecimalStyle; + this.sequenceStyle = sequenceStyle; + this.zebraSequenceStyle = zebraSequenceStyle; this.summaryTextStyle = summaryTextStyle; - this.summaryIntStyle = summaryIntStyle; + this.summaryAmountStyle = summaryAmountStyle; + this.summaryRoundedAmountStyle = summaryRoundedAmountStyle; + this.summarySequenceStyle = summarySequenceStyle; } static SheetStyles create(Workbook workbook) { @@ -410,22 +896,58 @@ public final class LbDailyUserTradeReportExportSupport { zebraTextStyle.cloneStyleFrom(textStyle); zebraTextStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); - CellStyle intStyle = workbook.createCellStyle(); - intStyle.setAlignment(HorizontalAlignment.RIGHT); - intStyle.setVerticalAlignment(VerticalAlignment.CENTER); - intStyle.setDataFormat(dataFormat.getFormat("#,##0")); - intStyle.setBorderTop(BorderStyle.THIN); - intStyle.setBorderBottom(BorderStyle.THIN); - intStyle.setBorderLeft(BorderStyle.THIN); - intStyle.setBorderRight(BorderStyle.THIN); + CellStyle amountStyle = workbook.createCellStyle(); + amountStyle.setAlignment(HorizontalAlignment.RIGHT); + amountStyle.setVerticalAlignment(VerticalAlignment.CENTER); + amountStyle.setDataFormat(dataFormat.getFormat("#,##0.##########")); + amountStyle.setBorderTop(BorderStyle.THIN); + amountStyle.setBorderBottom(BorderStyle.THIN); + amountStyle.setBorderLeft(BorderStyle.THIN); + amountStyle.setBorderRight(BorderStyle.THIN); - CellStyle zebraIntStyle = workbook.createCellStyle(); - zebraIntStyle.cloneStyleFrom(intStyle); - zebraIntStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + CellStyle zebraAmountStyle = workbook.createCellStyle(); + zebraAmountStyle.cloneStyleFrom(amountStyle); + zebraAmountStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + + CellStyle roundedAmountStyle = workbook.createCellStyle(); + roundedAmountStyle.cloneStyleFrom(amountStyle); + roundedAmountStyle.setDataFormat(dataFormat.getFormat("#,##0")); + + CellStyle zebraRoundedAmountStyle = workbook.createCellStyle(); + zebraRoundedAmountStyle.cloneStyleFrom(roundedAmountStyle); + zebraRoundedAmountStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + + CellStyle paymentActualStyle = workbook.createCellStyle(); + paymentActualStyle.cloneStyleFrom(roundedAmountStyle); + paymentActualStyle.setDataFormat(dataFormat.getFormat("0")); + + CellStyle zebraPaymentActualStyle = workbook.createCellStyle(); + zebraPaymentActualStyle.cloneStyleFrom(paymentActualStyle); + zebraPaymentActualStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + + CellStyle paymentActualDecimalStyle = workbook.createCellStyle(); + paymentActualDecimalStyle.cloneStyleFrom(paymentActualStyle); + paymentActualDecimalStyle.setDataFormat(dataFormat.getFormat("0.##########")); + + CellStyle zebraPaymentActualDecimalStyle = workbook.createCellStyle(); + zebraPaymentActualDecimalStyle.cloneStyleFrom(paymentActualDecimalStyle); + zebraPaymentActualDecimalStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + + CellStyle sequenceStyle = workbook.createCellStyle(); + sequenceStyle.cloneStyleFrom(amountStyle); + sequenceStyle.setDataFormat(dataFormat.getFormat("#,##0")); + + CellStyle zebraSequenceStyle = workbook.createCellStyle(); + zebraSequenceStyle.cloneStyleFrom(sequenceStyle); + zebraSequenceStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); XSSFColor zebraFill = new XSSFColor(new Color(242, 244, 248), null); ((XSSFCellStyle) zebraTextStyle).setFillForegroundColor(zebraFill); - ((XSSFCellStyle) zebraIntStyle).setFillForegroundColor(zebraFill); + ((XSSFCellStyle) zebraAmountStyle).setFillForegroundColor(zebraFill); + ((XSSFCellStyle) zebraRoundedAmountStyle).setFillForegroundColor(zebraFill); + ((XSSFCellStyle) zebraPaymentActualStyle).setFillForegroundColor(zebraFill); + ((XSSFCellStyle) zebraPaymentActualDecimalStyle).setFillForegroundColor(zebraFill); + ((XSSFCellStyle) zebraSequenceStyle).setFillForegroundColor(zebraFill); CellStyle summaryTextStyle = workbook.createCellStyle(); summaryTextStyle.cloneStyleFrom(textStyle); @@ -436,15 +958,45 @@ public final class LbDailyUserTradeReportExportSupport { summaryTextStyle.setFont(summaryFont); summaryTextStyle.setBorderTop(BorderStyle.THIN); - CellStyle summaryIntStyle = workbook.createCellStyle(); - summaryIntStyle.cloneStyleFrom(intStyle); - summaryIntStyle.setFillForegroundColor(IndexedColors.LEMON_CHIFFON.getIndex()); - summaryIntStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); - summaryIntStyle.setFont(summaryFont); - summaryIntStyle.setBorderTop(BorderStyle.THIN); + CellStyle summaryAmountStyle = workbook.createCellStyle(); + summaryAmountStyle.cloneStyleFrom(amountStyle); + summaryAmountStyle.setFillForegroundColor(IndexedColors.LEMON_CHIFFON.getIndex()); + summaryAmountStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + summaryAmountStyle.setFont(summaryFont); + summaryAmountStyle.setBorderTop(BorderStyle.THIN); + + CellStyle summaryRoundedAmountStyle = workbook.createCellStyle(); + summaryRoundedAmountStyle.cloneStyleFrom(roundedAmountStyle); + summaryRoundedAmountStyle.setFillForegroundColor(IndexedColors.LEMON_CHIFFON.getIndex()); + summaryRoundedAmountStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + summaryRoundedAmountStyle.setFont(summaryFont); + summaryRoundedAmountStyle.setBorderTop(BorderStyle.THIN); + + CellStyle summarySequenceStyle = workbook.createCellStyle(); + summarySequenceStyle.cloneStyleFrom(sequenceStyle); + summarySequenceStyle.setFillForegroundColor(IndexedColors.LEMON_CHIFFON.getIndex()); + summarySequenceStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + summarySequenceStyle.setFont(summaryFont); + summarySequenceStyle.setBorderTop(BorderStyle.THIN); return new SheetStyles( - headerStyle, textStyle, zebraTextStyle, intStyle, zebraIntStyle, summaryTextStyle, summaryIntStyle); + headerStyle, + textStyle, + zebraTextStyle, + amountStyle, + zebraAmountStyle, + roundedAmountStyle, + zebraRoundedAmountStyle, + paymentActualStyle, + zebraPaymentActualStyle, + paymentActualDecimalStyle, + zebraPaymentActualDecimalStyle, + sequenceStyle, + zebraSequenceStyle, + summaryTextStyle, + summaryAmountStyle, + summaryRoundedAmountStyle, + summarySequenceStyle); } } }