From 8f005fcd233296fdfdffdde1e2abff4d66a07b1a Mon Sep 17 00:00:00 2001 From: cst61 Date: Thu, 21 May 2026 22:57:14 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=BC=E5=87=BA=E6=8A=A5=E8=A1=A8=2011?= =?UTF-8?q?=E7=82=B9=E6=8A=A5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LbDailyUserTradeReportController.java | 236 +-------- .../ILbDailyUserTradeReportService.java | 7 + .../rj/service/ILbDeductionAmountService.java | 7 + .../LbDailyUserTradeReportServiceImpl.java | 59 +++ .../impl/LbDeductionAmountServiceImpl.java | 25 + .../LbDailyUserTradeReportExportSupport.java | 450 ++++++++++++++++++ 6 files changed, 549 insertions(+), 235 deletions(-) create mode 100644 src/main/java/com/rj/service/support/LbDailyUserTradeReportExportSupport.java diff --git a/src/main/java/com/rj/controller/LbDailyUserTradeReportController.java b/src/main/java/com/rj/controller/LbDailyUserTradeReportController.java index 422ccfd..cecae8b 100644 --- a/src/main/java/com/rj/controller/LbDailyUserTradeReportController.java +++ b/src/main/java/com/rj/controller/LbDailyUserTradeReportController.java @@ -8,43 +8,16 @@ import com.rj.service.ILbDailyUserTradeReportService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.ServletOutputStream; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import org.apache.poi.ss.usermodel.Row; -import org.apache.poi.ss.usermodel.Sheet; -import org.apache.poi.ss.usermodel.Workbook; -import org.apache.poi.ss.usermodel.Cell; -import org.apache.poi.ss.usermodel.CellStyle; -import org.apache.poi.ss.usermodel.DataFormat; -import org.apache.poi.ss.usermodel.FillPatternType; -import org.apache.poi.ss.usermodel.HorizontalAlignment; -import org.apache.poi.ss.usermodel.IndexedColors; -import org.apache.poi.ss.usermodel.VerticalAlignment; -import org.apache.poi.ss.usermodel.BorderStyle; -import org.apache.poi.ss.usermodel.Font; -import org.apache.poi.ss.usermodel.SheetConditionalFormatting; -import org.apache.poi.ss.usermodel.ComparisonOperator; -import org.apache.poi.ss.usermodel.ConditionalFormattingRule; -import org.apache.poi.ss.usermodel.FontFormatting; -import org.apache.poi.ss.util.CellRangeAddress; -import org.apache.poi.xssf.usermodel.XSSFCellStyle; -import org.apache.poi.xssf.usermodel.XSSFColor; -import org.apache.poi.xssf.usermodel.XSSFWorkbook; -import java.awt.Color; import java.math.BigDecimal; -import java.math.RoundingMode; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; -import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -658,213 +631,6 @@ public class LbDailyUserTradeReportController { @Parameter(description = "租户ID", required = true) @RequestParam String tenantId, HttpServletResponse response) { - try { - if (tenantId == null || tenantId.trim().isEmpty()) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "tenantId不能为空"); - return; - } - if (reportDate == null || reportDate.trim().isEmpty()) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "reportDate不能为空"); - return; - } - - LocalDate parsedReportDate; - try { - parsedReportDate = LocalDate.parse(reportDate.trim()); - } catch (Exception e) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "报表日期格式错误,请使用 yyyy-MM-dd"); - return; - } - - LocalDateTime start = parsedReportDate.atStartOfDay(); - LocalDateTime end = parsedReportDate.plusDays(1).atStartOfDay(); - - LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(LbDailyUserTradeReport::getTenantId, tenantId.trim()) - .ge(LbDailyUserTradeReport::getReportDate, start) - .lt(LbDailyUserTradeReport::getReportDate, end) - .orderByDesc(LbDailyUserTradeReport::getCreatedAt); - - List rows = lbDailyUserTradeReportService.list(queryWrapper); - rows.sort( - Comparator - .comparingInt((LbDailyUserTradeReport o) -> "report_sum".equals(o.getDataType()) ? 1 : 0) - .thenComparing((a, b) -> { - BigDecimal da = a.getDiffAmt() == null ? BigDecimal.ZERO : a.getDiffAmt(); - BigDecimal db = b.getDiffAmt() == null ? BigDecimal.ZERO : b.getDiffAmt(); - return db.compareTo(da); - }) - ); - - try (Workbook workbook = new XSSFWorkbook()) { - Sheet sheet = workbook.createSheet("report"); - - DateTimeFormatter mmdd = DateTimeFormatter.ofPattern("MM-dd"); - String d0 = parsedReportDate.format(mmdd); - String d1 = parsedReportDate.minusDays(1).format(mmdd); - - float rowHeightFactor = 1.3f; - sheet.setDefaultRowHeightInPoints(sheet.getDefaultRowHeightInPoints() * rowHeightFactor); - - DataFormat dataFormat = workbook.createDataFormat(); - - CellStyle headerStyle = workbook.createCellStyle(); - Font headerFont = workbook.createFont(); - headerFont.setBold(true); - headerFont.setColor(IndexedColors.DARK_BLUE.getIndex()); - headerStyle.setFont(headerFont); - headerStyle.setAlignment(HorizontalAlignment.CENTER); - headerStyle.setVerticalAlignment(VerticalAlignment.CENTER); - headerStyle.setFillForegroundColor(IndexedColors.PALE_BLUE.getIndex()); - headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); - headerStyle.setBorderTop(BorderStyle.THIN); - headerStyle.setBorderBottom(BorderStyle.THIN); - headerStyle.setBorderLeft(BorderStyle.THIN); - headerStyle.setBorderRight(BorderStyle.THIN); - - CellStyle textStyle = workbook.createCellStyle(); - textStyle.setAlignment(HorizontalAlignment.LEFT); - textStyle.setVerticalAlignment(VerticalAlignment.CENTER); - textStyle.setBorderTop(BorderStyle.THIN); - textStyle.setBorderBottom(BorderStyle.THIN); - textStyle.setBorderLeft(BorderStyle.THIN); - textStyle.setBorderRight(BorderStyle.THIN); - - CellStyle zebraTextStyle = workbook.createCellStyle(); - 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 zebraIntStyle = workbook.createCellStyle(); - zebraIntStyle.cloneStyleFrom(intStyle); - zebraIntStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); - - // 更淡的斑马纹底色(接近白色) - XSSFColor zebraFill = new XSSFColor(new Color(242, 244, 248), null); - ((XSSFCellStyle) zebraTextStyle).setFillForegroundColor(zebraFill); - ((XSSFCellStyle) zebraIntStyle).setFillForegroundColor(zebraFill); - - CellStyle summaryTextStyle = workbook.createCellStyle(); - summaryTextStyle.cloneStyleFrom(textStyle); - summaryTextStyle.setFillForegroundColor(IndexedColors.LEMON_CHIFFON.getIndex()); - summaryTextStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); - Font summaryFont = workbook.createFont(); - summaryFont.setBold(true); - 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); - - int r = 0; - Row header = sheet.createRow(r++); - header.setHeightInPoints(20 * rowHeightFactor); - createHeaderCell(header, 0, "昵称", headerStyle); - createHeaderCell(header, 1, d1 + "买货", headerStyle); - createHeaderCell(header, 2, d0 + "卖货", headerStyle); - createHeaderCell(header, 3, d0 + "买货", headerStyle); - createHeaderCell(header, 4, "服务费", headerStyle); - createHeaderCell(header, 5, "差额", headerStyle); - createHeaderCell(header, 6, "应收应付", headerStyle); - createHeaderCell(header, 7, "抵扣金额", headerStyle); - - sheet.createFreezePane(0, 1); - sheet.setAutoFilter(new CellRangeAddress(0, 0, 0, 7)); - - for (LbDailyUserTradeReport item : rows) { - Row row = sheet.createRow(r++); - row.setHeightInPoints(row.getHeightInPoints() * rowHeightFactor); - boolean isSummary = "report_sum".equals(item.getDataType()); - boolean zebra = (r % 2 == 0); - - CellStyle tStyle = isSummary ? summaryTextStyle : (zebra ? zebraTextStyle : textStyle); - CellStyle nStyle = isSummary ? summaryIntStyle : (zebra ? zebraIntStyle : intStyle); - - createTextCell(row, 0, item.getNickname() == null ? "" : item.getNickname(), tStyle); - createIntCell(row, 1, roundedInt(item.getYestodayBuyAmt()), nStyle); - createIntCell(row, 2, roundedInt(item.getDailySellAmt()), nStyle); - createIntCell(row, 3, roundedInt(item.getDailyBuyAmt()), nStyle); - createIntCell(row, 4, roundedInt(item.getServiceAmt()), nStyle); - createIntCell(row, 5, roundedInt(item.getDiffAmt()), nStyle); - createIntCell(row, 6, roundedInt(item.getActualReceiptsPayments()), nStyle); - createIntCell(row, 7, roundedInt(item.getDikouAmt()), nStyle); - } - - if (r > 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, r - 1, 5, 5)}, negative); - } - - for (int i = 0; i <= 7; i++) { - sheet.autoSizeColumn(i); - int currentWidth = sheet.getColumnWidth(i); - int widened = (int) Math.min(255 * 256, Math.round(currentWidth * 1.5)); - sheet.setColumnWidth(i, widened); - } - - String filename = "当日汇总报表" + parsedReportDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")) + ".xlsx"; - String encoded = URLEncoder.encode(filename, StandardCharsets.UTF_8).replaceAll("\\+", "%20"); - - response.setCharacterEncoding(StandardCharsets.UTF_8.name()); - response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); - response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded); - - try (ServletOutputStream os = response.getOutputStream()) { - workbook.write(os); - os.flush(); - } - } - } catch (Exception e) { - try { - response.reset(); - response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - response.setCharacterEncoding(StandardCharsets.UTF_8.name()); - response.setContentType("text/plain;charset=UTF-8"); - response.getWriter().write("导出异常:" + e.getMessage()); - } catch (Exception ignored) { - // ignore - } - } - } - - 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); - cell.setCellStyle(style); - } - - private static void createTextCell(Row row, int col, String text, CellStyle style) { - Cell cell = row.createCell(col); - cell.setCellValue(text); - cell.setCellStyle(style); - } - - private static void createIntCell(Row row, int col, long value, CellStyle style) { - Cell cell = row.createCell(col); - cell.setCellValue((double) value); - cell.setCellStyle(style); + lbDailyUserTradeReportService.exportByDateAndTenant(reportDate, tenantId, response); } } diff --git a/src/main/java/com/rj/service/ILbDailyUserTradeReportService.java b/src/main/java/com/rj/service/ILbDailyUserTradeReportService.java index 9fae00c..bb44e1c 100644 --- a/src/main/java/com/rj/service/ILbDailyUserTradeReportService.java +++ b/src/main/java/com/rj/service/ILbDailyUserTradeReportService.java @@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.rj.entity.LbDailyUserTrade; import com.rj.entity.LbDailyUserTradeReport; +import jakarta.servlet.http.HttpServletResponse; + import java.time.LocalDate; import java.util.List; import java.util.Map; @@ -33,4 +35,9 @@ public interface ILbDailyUserTradeReportService extends IService listUserTradeAmountWithTeamByDateRangeAndTenant( LocalDate startDate, LocalDate endDate, String tenantId); + + /** + * 按报表日期与租户导出 Excel。 + */ + void exportByDateAndTenant(String reportDate, String tenantId, HttpServletResponse response); } diff --git a/src/main/java/com/rj/service/ILbDeductionAmountService.java b/src/main/java/com/rj/service/ILbDeductionAmountService.java index 49c033c..35e44a5 100644 --- a/src/main/java/com/rj/service/ILbDeductionAmountService.java +++ b/src/main/java/com/rj/service/ILbDeductionAmountService.java @@ -3,6 +3,8 @@ package com.rj.service; import com.baomidou.mybatisplus.extension.service.IService; import com.rj.entity.LbDeductionAmount; +import java.math.BigDecimal; +import java.time.LocalDate; import java.util.Map; public interface ILbDeductionAmountService extends IService { @@ -25,4 +27,9 @@ public interface ILbDeductionAmountService extends IService { * 根据自然语言文本调用大模型解析为 {@link LbDeductionAmount} 并落库。 */ Map parseFromTextByLlmAndSave(String tenantId, String text); + + /** + * 按租户、报表日期汇总抵扣金额:key 为 user_name(trim),value 为同日 create_time 记录的 dikou_amt 之和。 + */ + Map sumDikouAmtByUserNameForReportDate(String tenantId, LocalDate reportDate); } diff --git a/src/main/java/com/rj/service/impl/LbDailyUserTradeReportServiceImpl.java b/src/main/java/com/rj/service/impl/LbDailyUserTradeReportServiceImpl.java index d87d0a9..6c3f104 100644 --- a/src/main/java/com/rj/service/impl/LbDailyUserTradeReportServiceImpl.java +++ b/src/main/java/com/rj/service/impl/LbDailyUserTradeReportServiceImpl.java @@ -6,12 +6,15 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.rj.entity.LbDailyUserTrade; import com.rj.entity.LbDailyUserTradeReport; import com.rj.entity.LbDepartmentUser; +import com.rj.service.ILbDeductionAmountService; import com.rj.mapper.LbDailyUserTradeMapper; import com.rj.mapper.LbDailyUserTradeReportMapper; import com.rj.service.ILbDailyUserTradeReportService; import com.rj.service.ILbDepartmentUserService; +import com.rj.service.support.LbDailyUserTradeReportExportSupport; import com.rj.service.support.LbDailyUserTradeReportWriteSupport; import com.rj.tenant.TenantContextHolder; +import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -53,6 +56,8 @@ public class LbDailyUserTradeReportServiceImpl extends ServiceImpl pageDailyUserTradeByUserIdAndDateRange( @@ -498,6 +503,60 @@ public class LbDailyUserTradeReportServiceImpl extends ServiceImpl rows = listReportsForExport(tenantIdTrim, parsedReportDate); + Map deductionByUserName = + lbDeductionAmountService.sumDikouAmtByUserNameForReportDate(tenantIdTrim, parsedReportDate); + LbDailyUserTradeReportExportSupport.writeExcel( + parsedReportDate, rows, deductionByUserName, response); + } catch (Exception e) { + LbDailyUserTradeReportExportSupport.writeExportError(response, e); + } + } + + private List listReportsForExport(String tenantId, LocalDate reportDate) { + LocalDateTime start = reportDate.atStartOfDay(); + LocalDateTime end = reportDate.plusDays(1).atStartOfDay(); + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(LbDailyUserTradeReport::getTenantId, tenantId) + .ge(LbDailyUserTradeReport::getReportDate, start) + .lt(LbDailyUserTradeReport::getReportDate, end) + .orderByDesc(LbDailyUserTradeReport::getCreatedAt); + + List rows = list(queryWrapper); + rows.sort( + Comparator + .comparingInt((LbDailyUserTradeReport o) -> DATA_TYPE_REPORT_SUM.equals(o.getDataType()) ? 1 : 0) + .thenComparing((a, b) -> { + BigDecimal da = a.getDiffAmt() == null ? BigDecimal.ZERO : a.getDiffAmt(); + BigDecimal db = b.getDiffAmt() == null ? BigDecimal.ZERO : b.getDiffAmt(); + return db.compareTo(da); + }) + ); + return rows; + } + private static final class DetailRepairStat { final boolean repaired; final int updatedCount; diff --git a/src/main/java/com/rj/service/impl/LbDeductionAmountServiceImpl.java b/src/main/java/com/rj/service/impl/LbDeductionAmountServiceImpl.java index 9f9a691..3443f64 100644 --- a/src/main/java/com/rj/service/impl/LbDeductionAmountServiceImpl.java +++ b/src/main/java/com/rj/service/impl/LbDeductionAmountServiceImpl.java @@ -25,6 +25,7 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; import java.util.regex.Matcher; @@ -429,6 +430,30 @@ public class LbDeductionAmountServiceImpl } } + @Override + public Map sumDikouAmtByUserNameForReportDate(String tenantId, LocalDate reportDate) { + Map byUserName = new HashMap<>(); + if (tenantId == null || tenantId.trim().isEmpty() || reportDate == null) { + return byUserName; + } + LocalDateTime start = reportDate.atStartOfDay(); + LocalDateTime end = reportDate.plusDays(1).atStartOfDay(); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(LbDeductionAmount::getTenantId, tenantId.trim()) + .ge(LbDeductionAmount::getCreateTime, start) + .lt(LbDeductionAmount::getCreateTime, end); + List records = list(queryWrapper); + for (LbDeductionAmount record : records) { + if (record.getUserName() == null || record.getUserName().trim().isEmpty()) { + continue; + } + String key = record.getUserName().trim(); + BigDecimal amt = record.getDikouAmt() == null ? BigDecimal.ZERO : record.getDikouAmt(); + byUserName.merge(key, amt, BigDecimal::add); + } + return byUserName; + } + private static LocalDateTime parseDateTime(String text) { if (text == null || text.trim().isEmpty()) { return null; diff --git a/src/main/java/com/rj/service/support/LbDailyUserTradeReportExportSupport.java b/src/main/java/com/rj/service/support/LbDailyUserTradeReportExportSupport.java new file mode 100644 index 0000000..a8a8b18 --- /dev/null +++ b/src/main/java/com/rj/service/support/LbDailyUserTradeReportExportSupport.java @@ -0,0 +1,450 @@ +package com.rj.service.support; + +import com.rj.entity.LbDailyUserTradeReport; +import jakarta.servlet.ServletOutputStream; +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.ComparisonOperator; +import org.apache.poi.ss.usermodel.ConditionalFormattingRule; +import org.apache.poi.ss.usermodel.DataFormat; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.FontFormatting; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.SheetConditionalFormatting; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.xssf.usermodel.XSSFCellStyle; +import org.apache.poi.xssf.usermodel.XSSFColor; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.http.HttpHeaders; + +import java.awt.Color; +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.net.URLEncoder; +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; + +/** + * 当日用户交易报表 Excel 导出(POI 样式与写入逻辑)。 + */ +public final class LbDailyUserTradeReportExportSupport { + + /** 导出工作簿固定 7 个 sheet(顺序与业务模板一致)。 */ + private static final String[] EXPORT_SHEET_NAMES = { + "总表", "抵扣表", "支付表", "11点表", "断货表", "临时表2", "结果表" + }; + + private static final int SUMMARY_SHEET_INDEX = 0; + private static final int DEDUCTION_SHEET_INDEX = 1; + private static final int ELEVEN_CLOCK_SHEET_INDEX = 3; + + private static final int COL_NICKNAME = 0; + private static final int COL_YESTODAY_BUY = 1; + private static final int COL_DAILY_SELL = 2; + private static final int COL_DAILY_BUY = 3; + private static final int COL_SERVICE = 4; + private static final int COL_DIFF = 5; + private static final int COL_ACTUAL = 6; + private static final int COL_DIKOU = 7; + + private static final int SUMMARY_LAST_COL = COL_DIKOU; + /** 11点表:最左侧序号列,其余列右移一列 */ + private static final int ELEVEN_CLOCK_LAST_COL = COL_DIKOU + 1; + private static final int COL_SEQUENCE_LEFT = 0; + + private static final String DATA_TYPE_REPORT_SUM = "report_sum"; + + private LbDailyUserTradeReportExportSupport() { + } + + public static void writeExcel( + LocalDate reportDate, + List rows, + Map deductionByUserName, + HttpServletResponse response) throws IOException { + try (Workbook workbook = new XSSFWorkbook()) { + SheetStyles styles = SheetStyles.create(workbook); + for (String sheetName : EXPORT_SHEET_NAMES) { + Sheet sheet = workbook.createSheet(sheetName); + sheet.setDefaultRowHeightInPoints(sheet.getDefaultRowHeightInPoints() * styles.rowHeightFactor); + } + + DateHeaderLabels labels = DateHeaderLabels.of(reportDate); + fillSummarySheet(workbook.getSheetAt(SUMMARY_SHEET_INDEX), labels, rows, 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); + + String filename = "当日汇总报表" + reportDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")) + ".xlsx"; + String encoded = URLEncoder.encode(filename, StandardCharsets.UTF_8).replaceAll("\\+", "%20"); + + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encoded); + + try (ServletOutputStream os = response.getOutputStream()) { + workbook.write(os); + os.flush(); + } + } + } + + private static void fillSummarySheet( + Sheet sheet, + DateHeaderLabels labels, + List rows, + 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); + } + + /** + * 抵扣表 / 11点表:先按总表写入各行;抵扣金额列改为 lb_deduction_amount 匹配值; + * 「实际收付」= 抵扣金额 + 原实际收付;明细按实际收付倒序。 + * 抵扣表最后一行汇总行的抵扣金额、实际收付为本列明细之和;11点表不输出汇总行,最左侧为序号列。 + */ + private static void fillDeductionSheet( + Sheet sheet, + DateHeaderLabels labels, + List rows, + SheetStyles styles, + Map deductionByUserName, + boolean sequenceOnLeft, + boolean includeSummaryRow) { + 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 : orderRowsForDeductionSheet(rows, deductionByUserName)) { + boolean isSummaryRow = DATA_TYPE_REPORT_SUM.equals(item.getDataType()); + if (!includeSummaryRow && isSummaryRow) { + continue; + } + + Row row = sheet.createRow(r++); + applyRowHeight(row, styles); + RowStylePair rowStyles = rowStyles(item, r, styles); + + // 第1步:与总表一致(其它列含汇总行均沿用报表原值) + long dikouFromReport = roundedInt(item.getDikouAmt()); + long actualReceiptsPayments = roundedInt(item.getActualReceiptsPayments()); + writeBaseDataCells(row, item, rowStyles, dikouFromReport, sequenceOnLeft); + + if (isSummaryRow) { + // 汇总行:抵扣金额、实际收付 = 上方明细行之和 + createIntCell(row, dataCol(COL_DIKOU, sequenceOnLeft), totalDikou, rowStyles.numberStyle); + createIntCell(row, dataCol(COL_ACTUAL, sequenceOnLeft), totalActual, rowStyles.numberStyle); + } else { + if (sequenceOnLeft) { + sequence++; + createIntCell(row, COL_SEQUENCE_LEFT, sequence, rowStyles.numberStyle); + } + + // 第2步:按昵称 + 报表日匹配 lb_deduction_amount,覆盖抵扣金额列 + long dikouFromDb = roundedInt(lookupDeductionAmt(item.getNickname(), deductionByUserName)); + createIntCell(row, dataCol(COL_DIKOU, sequenceOnLeft), dikouFromDb, rowStyles.numberStyle); + + // 第3步:抵扣金额 + 原实际收付,覆盖「实际收付」列 + long dikouPlusActual = computeActualReceiptsForDeduction(item, deductionByUserName); + createIntCell(row, dataCol(COL_ACTUAL, sequenceOnLeft), dikouPlusActual, rowStyles.numberStyle); + + totalDikou += dikouFromDb; + totalActual += dikouPlusActual; + } + } + + applyDiffNegativeFormatting(sheet, r, sequenceOnLeft); + autoSizeColumns(sheet, lastCol); + } + + /** 抵扣表:明细按计算后的实际收付倒序,汇总行保持在最后。 */ + private static List orderRowsForDeductionSheet( + List rows, + Map deductionByUserName) { + List details = new ArrayList<>(); + List summaries = new ArrayList<>(); + for (LbDailyUserTradeReport row : rows) { + if (DATA_TYPE_REPORT_SUM.equals(row.getDataType())) { + summaries.add(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()); + ordered.addAll(details); + ordered.addAll(summaries); + return ordered; + } + + private static long computeActualReceiptsForDeduction( + LbDailyUserTradeReport item, + Map deductionByUserName) { + long dikouFromDb = roundedInt(lookupDeductionAmt(item.getNickname(), deductionByUserName)); + long actualReceiptsPayments = roundedInt(item.getActualReceiptsPayments()); + return dikouFromDb + actualReceiptsPayments; + } + + private static int dataCol(int col, boolean sequenceOnLeft) { + return sequenceOnLeft ? col + 1 : col; + } + + private static int writeHeaderRow( + Sheet sheet, + DateHeaderLabels labels, + SheetStyles styles, + boolean sequenceOnLeft) { + int r = 0; + Row header = sheet.createRow(r++); + header.setHeightInPoints(20 * styles.rowHeightFactor); + if (sequenceOnLeft) { + createHeaderCell(header, COL_SEQUENCE_LEFT, "序号", styles.headerStyle); + } + createHeaderCell(header, dataCol(COL_NICKNAME, sequenceOnLeft), "昵称", styles.headerStyle); + createHeaderCell(header, dataCol(COL_YESTODAY_BUY, sequenceOnLeft), labels.yestodayBuyLabel, styles.headerStyle); + createHeaderCell(header, dataCol(COL_DAILY_SELL, sequenceOnLeft), labels.dailySellLabel, styles.headerStyle); + createHeaderCell(header, dataCol(COL_DAILY_BUY, sequenceOnLeft), labels.dailyBuyLabel, styles.headerStyle); + createHeaderCell(header, dataCol(COL_SERVICE, sequenceOnLeft), "服务费", styles.headerStyle); + createHeaderCell(header, dataCol(COL_DIFF, sequenceOnLeft), "差额", styles.headerStyle); + createHeaderCell(header, dataCol(COL_ACTUAL, sequenceOnLeft), "实际收付", styles.headerStyle); + createHeaderCell(header, dataCol(COL_DIKOU, sequenceOnLeft), "抵扣金额", styles.headerStyle); + return r; + } + + private static void writeBaseDataCells( + Row row, + LbDailyUserTradeReport item, + RowStylePair rowStyles, + long dikouAmt, + boolean sequenceOnLeft) { + 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); + } + + private static RowStylePair rowStyles(LbDailyUserTradeReport item, int rowIndex, SheetStyles styles) { + 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); + } + + private static BigDecimal lookupDeductionAmt(String nickname, Map deductionByUserName) { + if (nickname == null || nickname.trim().isEmpty()) { + return BigDecimal.ZERO; + } + return deductionByUserName.getOrDefault(nickname.trim(), BigDecimal.ZERO); + } + + private static void applyRowHeight(Row row, SheetStyles styles) { + row.setHeightInPoints(row.getHeightInPoints() * styles.rowHeightFactor); + } + + private static void applyDiffNegativeFormatting(Sheet sheet, int nextRowIndex, boolean sequenceOnLeft) { + if (nextRowIndex > 1) { + int diffCol = dataCol(COL_DIFF, sequenceOnLeft); + 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, diffCol, diffCol)}, negative); + } + } + + private static void autoSizeColumns(Sheet sheet, int lastCol) { + for (int i = 0; i <= lastCol; i++) { + sheet.autoSizeColumn(i); + int currentWidth = sheet.getColumnWidth(i); + int widened = (int) Math.min(255 * 256, Math.round(currentWidth * 1.5)); + sheet.setColumnWidth(i, widened); + } + } + + public static void writeExportError(HttpServletResponse response, Exception e) { + try { + response.reset(); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + response.setContentType("text/plain;charset=UTF-8"); + response.getWriter().write("导出异常:" + e.getMessage()); + } catch (Exception ignored) { + // ignore + } + } + + 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); + cell.setCellStyle(style); + } + + private static void createTextCell(Row row, int col, String text, CellStyle style) { + Cell cell = row.createCell(col); + cell.setCellValue(text); + cell.setCellStyle(style); + } + + private static void createIntCell(Row row, int col, long value, CellStyle style) { + Cell cell = row.createCell(col); + cell.setCellValue((double) value); + cell.setCellStyle(style); + } + + private record DateHeaderLabels(String yestodayBuyLabel, String dailySellLabel, String dailyBuyLabel) { + static DateHeaderLabels of(LocalDate reportDate) { + DateTimeFormatter mmdd = DateTimeFormatter.ofPattern("MM-dd"); + String d0 = reportDate.format(mmdd); + String d1 = reportDate.minusDays(1).format(mmdd); + return new DateHeaderLabels(d1 + "买货", d0 + "卖货", d0 + "买货"); + } + } + + private record RowStylePair(CellStyle textStyle, CellStyle numberStyle) { + } + + private static final class SheetStyles { + final float rowHeightFactor = 1.3f; + final CellStyle headerStyle; + final CellStyle textStyle; + final CellStyle zebraTextStyle; + final CellStyle intStyle; + final CellStyle zebraIntStyle; + final CellStyle summaryTextStyle; + final CellStyle summaryIntStyle; + + private SheetStyles( + CellStyle headerStyle, + CellStyle textStyle, + CellStyle zebraTextStyle, + CellStyle intStyle, + CellStyle zebraIntStyle, + CellStyle summaryTextStyle, + CellStyle summaryIntStyle) { + this.headerStyle = headerStyle; + this.textStyle = textStyle; + this.zebraTextStyle = zebraTextStyle; + this.intStyle = intStyle; + this.zebraIntStyle = zebraIntStyle; + this.summaryTextStyle = summaryTextStyle; + this.summaryIntStyle = summaryIntStyle; + } + + static SheetStyles create(Workbook workbook) { + DataFormat dataFormat = workbook.createDataFormat(); + + CellStyle headerStyle = workbook.createCellStyle(); + Font headerFont = workbook.createFont(); + headerFont.setBold(true); + headerFont.setColor(IndexedColors.DARK_BLUE.getIndex()); + headerStyle.setFont(headerFont); + headerStyle.setAlignment(HorizontalAlignment.CENTER); + headerStyle.setVerticalAlignment(VerticalAlignment.CENTER); + headerStyle.setFillForegroundColor(IndexedColors.PALE_BLUE.getIndex()); + headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + headerStyle.setBorderTop(BorderStyle.THIN); + headerStyle.setBorderBottom(BorderStyle.THIN); + headerStyle.setBorderLeft(BorderStyle.THIN); + headerStyle.setBorderRight(BorderStyle.THIN); + + CellStyle textStyle = workbook.createCellStyle(); + textStyle.setAlignment(HorizontalAlignment.LEFT); + textStyle.setVerticalAlignment(VerticalAlignment.CENTER); + textStyle.setBorderTop(BorderStyle.THIN); + textStyle.setBorderBottom(BorderStyle.THIN); + textStyle.setBorderLeft(BorderStyle.THIN); + textStyle.setBorderRight(BorderStyle.THIN); + + CellStyle zebraTextStyle = workbook.createCellStyle(); + 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 zebraIntStyle = workbook.createCellStyle(); + zebraIntStyle.cloneStyleFrom(intStyle); + zebraIntStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + + XSSFColor zebraFill = new XSSFColor(new Color(242, 244, 248), null); + ((XSSFCellStyle) zebraTextStyle).setFillForegroundColor(zebraFill); + ((XSSFCellStyle) zebraIntStyle).setFillForegroundColor(zebraFill); + + CellStyle summaryTextStyle = workbook.createCellStyle(); + summaryTextStyle.cloneStyleFrom(textStyle); + summaryTextStyle.setFillForegroundColor(IndexedColors.LEMON_CHIFFON.getIndex()); + summaryTextStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + Font summaryFont = workbook.createFont(); + summaryFont.setBold(true); + 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); + + return new SheetStyles( + headerStyle, textStyle, zebraTextStyle, intStyle, zebraIntStyle, summaryTextStyle, summaryIntStyle); + } + } +}