EasyExcel双表头导出实战
1:需求

页面展示效果及导出效果如下:

列表展示和导出按照用户勾选的字段配置
2:实现
2.1 数据库设计:
字段配置表:

field_json存储json, key为字段变量, value为字段名称
示例:
java
[
{"key":"quotationInfo","title":"报价单信息","expanded":true,
"fields":[{"key":"orderId","label":"订单ID","checked":true}, {"key":"orgName","label":"一级组织","checked":true},{"key":"quotationName","label":"报价 单名称","checked":true}]
},
{"key":"businessOpportunity","title":"商机信息","expanded":false,"fields":[{"key":"nicheName","label":"商机名称","checked":true},{"key":"nicheNum","label":"商机编码","checked":true},{"key":"nicheStageDescription","label":"商机进程","checked":true}]}
]
列表也展示处理:
- 展示的两行表头是前端调用后端提供的根据域账号获取field_json进行展示
- 后端只需要提供一下字段的分页接口

导出处理:
用的是easyexcel的双表头方法, 自定义字段导出是前端传字段

java
@ApiOperation("运营管理-货架运营-CPQ运营报表-产品报价表导出")
@PostMapping(value = "/operationManagerCenter/exportQuotationSheetStatisticsReport")
@AutoLog(operateType = BizLogOperateTypeConstant.EXPORT, value = "CPQ运营报表-产品报价表导出")
@DuplicateSubmit
public Result<Boolean> exportQuotationSheetStatisticsReport(HttpServletResponse response, @RequestBody ProductQuotationExportParam param) {
quotationSheetStatisticsReportService.exportQuotationSheetStatisticsReport(response, param);
return Result.OK(true);
}
@Data
public class ProductQuotationExportParam {
private static final long serialVersionUID = -6240257453459481055L;
@ApiModelProperty("导出字段列表,为空则导出全部字段")
private List<String> exportFields;
}
@Override
public void exportQuotationSheetStatisticsReport(HttpServletResponse response, ProductQuotationExportParam param) {
List<QuotationSheetStatisticsReportVO> dataList = quotationSheetStatisticsReportMapper.quotationSheetStatisticsReportList(param);
List<String> exportFields = param.getExportFields();
boolean exportAll = CollectionUtils.isEmpty(exportFields);
List<String> fieldNames = exportAll ? ALL_FIELD_NAMES :
exportFields.stream().filter(FIELD_HEADER_MAP::containsKey).collect(Collectors.toList());
if (fieldNames.isEmpty()) {
fieldNames = ALL_FIELD_NAMES;
}
List<List<String>> headers = buildTwoLevelHeaders(fieldNames);
List<List<Object>> rows = buildRows(dataList, fieldNames);
try {
EasyExcelUtil.dynamicHeaderWrite(headers, rows, "CPQ运营报表-产品报价表.xlsx", "产品报价表", response);
} catch (Exception e) {
log.error("CPQ运营报表-产品报价表导出异常:", e);
throw new RuntimeException("导出异常", e);
}
}
/**
* 构建两层动态表头:第一层为分组名称,第二层为字段名称
* EasyExcel 中 head 的每个 List<String> 代表一列的多级表头,相邻列相同的上级会自动合并
*/
private List<List<String>> buildTwoLevelHeaders(List<String> fieldNames) {
List<List<String>> headers = new ArrayList<>();
for (String fieldName : fieldNames) {
List<String> head = new ArrayList<>();
String groupName = FIELD_GROUP_MAP.getOrDefault(fieldName, "");
String headerName = FIELD_HEADER_MAP.getOrDefault(fieldName, fieldName);
head.add(groupName);
head.add(headerName);
headers.add(head);
}
return headers;
}
/**
* 构建数据行,通过反射获取字段值
*/
private List<List<Object>> buildRows(List<QuotationSheetStatisticsReportVO> dataList, List<String> fieldNames) {
Map<String, Field> fieldCache = new HashMap<>();
for (String fieldName : fieldNames) {
try {
Field field = QuotationSheetStatisticsReportVO.class.getDeclaredField(fieldName);
field.setAccessible(true);
fieldCache.put(fieldName, field);
} catch (NoSuchFieldException e) {
log.warn("字段不存在: {}", fieldName);
}
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
List<List<Object>> rows = new ArrayList<>();
for (QuotationSheetStatisticsReportVO vo : dataList) {
List<Object> row = new ArrayList<>();
for (String fieldName : fieldNames) {
Field field = fieldCache.get(fieldName);
if (field == null) {
row.add("");
continue;
}
try {
Object value = field.get(vo);
if (value == null) {
row.add("");
} else if (value instanceof Date) {
row.add(sdf.format((Date) value));
} else if (value instanceof BigDecimal) {
row.add(((BigDecimal) value).toPlainString());
} else {
row.add(value.toString());
}
} catch (IllegalAccessException e) {
log.warn("获取字段值异常: {}", fieldName, e);
row.add("");
}
}
rows.add(row);
}
return rows;
}
/**
* 字段名 -> 中文表头映射(有序),基于 ProductQuotationInfoVO 的 @ExportFieldTitle 注解构建
*/
private static final LinkedHashMap<String, String> FIELD_HEADER_MAP = new LinkedHashMap<>();
/**
* 字段名 -> 所属分组名称映射
*/
private static final LinkedHashMap<String, String> FIELD_GROUP_MAP = new LinkedHashMap<>();
/**
* 所有可导出字段名列表(有序,排除id)
*/
private static final List<String> ALL_FIELD_NAMES;
static {
Field[] fields = QuotationSheetStatisticsReportVO.class.getDeclaredFields();
for (Field field : fields) {
if ("serialVersionUID".equals(field.getName()) || "id".equals(field.getName())) {
continue;
}
ExportFieldTitle annotation = field.getAnnotation(ExportFieldTitle.class);
if (annotation != null) {
FIELD_HEADER_MAP.put(field.getName(), annotation.value());
} else {
FIELD_HEADER_MAP.put(field.getName(), field.getName());
}
}
ALL_FIELD_NAMES = new ArrayList<>(FIELD_HEADER_MAP.keySet());
initFieldGroupMap();
}
/**
* 初始化字段分组映射,对应前端字段配置面板的6个分组
*/
private static void initFieldGroupMap() {
// ==================== 报价单信息 ====================
String quotationGroup = "报价单信息";
for (String f : Arrays.asList(
"orderId", "orgCode", "orgName", "quotationName"
)) {
FIELD_GROUP_MAP.put(f, quotationGroup);
}
// ==================== 商机信息 ====================
String nicheGroup = "商机信息";
for (String f : Arrays.asList(
"nicheName", "nicheNum", "nicheStageCode"
)) {
FIELD_GROUP_MAP.put(f, nicheGroup);
}
}
public static <T> void dynamicHeaderWrite(List<List<String>> head, List<T> list, String fileName, String sheetName, HttpServletResponse servletResponse) throws Exception {
ExcelWriter excelWriter = null;
OutputStream outputStream = null;
try {
outputStream = servletResponse.getOutputStream();
excelWriter = EasyExcel.write(outputStream).head(head).registerConverter(new LongStringConverter()).registerWriteHandler(new CustomWidthStyleStrategy()).build();
WriteSheet writeSheet = EasyExcel.writerSheet(sheetName).build();
excelWriter.write(list, writeSheet);
servletResponse.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
servletResponse.setHeader("Content-Disposition", "attachment;fileName=" + new String(fileName.getBytes("gbk"), StandardCharsets.ISO_8859_1));
} finally {
if (excelWriter != null) {
excelWriter.finish();
}
if (null != outputStream) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public class CustomWidthStyleStrategy extends AbstractColumnWidthStyleStrategy {
private Map<Integer, Map<Integer, Integer>> CACHE = new HashMap<>();
/**
* 设置列宽
*
* @param writeSheetHolder writeSheetHolder
* @param cellDataList cellDataList
* @param cell cell
* @param head head
* @param relativeRowIndex relativeRowIndex
* @param isHead isHead
*/
@Override
protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
boolean needSetWidth = isHead || !CollectionUtils.isEmpty(cellDataList);
if (needSetWidth) {
Map<Integer, Integer> maxColumnWidthMap = CACHE.get(writeSheetHolder.getSheetNo());
if (maxColumnWidthMap == null) {
maxColumnWidthMap = new HashMap<>();
CACHE.put(writeSheetHolder.getSheetNo(), maxColumnWidthMap);
}
Integer columnWidth = 30;
Integer maxColumnWidth = maxColumnWidthMap.get(cell.getColumnIndex());
if (maxColumnWidth == null || columnWidth > maxColumnWidth) {
maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth);
writeSheetHolder.getSheet().setColumnWidth(cell.getColumnIndex(), columnWidth * 256);
}
}
}
}