@@ -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 < LbDailyUserTradeReport > rows ,
Map < String , BigDecimal > 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 < String , BigDecimal > 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 < LbDailyUserTradeReport > 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 < LbDailyUserTradeReport > rows ,
SheetStyles styles ,
Map < String , BigDecimal > 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 < LbDailyUserTradeReport > orderRowsForDeductionSheet (
List < LbDailyUserTradeReport > rows ,
Map < String , BigDecimal > deductionByUserName ) {
List < LbDailyUserTradeReport > details = new ArrayList < > ( ) ;
List < LbDailyUserTradeReport > 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 < LbDailyUserTradeReport > ordered = new ArrayList < > ( details . size ( ) + summaries . size ( ) ) ;
ordered . addAll ( details ) ;
ordered . addAll ( summaries ) ;
return ordered ;
}
private static long computeActualReceiptsForDeduction (
LbDailyUserTradeReport item ,
Map < String , BigDecimal > 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 < String , BigDecimal > 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 ) ;
}
}
}