EasyExcel自定义字段并且双表头导出实战

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}]}
]

列表也展示处理:

  1. 展示的两行表头是前端调用后端提供的根据域账号获取field_json进行展示
  2. 后端只需要提供一下字段的分页接口
    导出处理:
    用的是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);
            }
        }
    }
}
相关推荐
yuyu_03042 小时前
Spring Boot在Windows开机自启动
windows·spring boot·后端
骇客野人2 小时前
Java springboot里注解大全和使用指南
java·开发语言·spring boot
用户8307196840822 小时前
Spring Boot 启动报错:OpenFeign 隐性循环依赖,排查了整整一下午
java·spring boot·spring cloud
星辰_mya2 小时前
@SpringBootApplication 与 SPI 机制的终极解密
java·spring boot·spring
qiuyuyiyang2 小时前
SpringBoot中如何手动开启事务
java·spring boot·spring
aisifang002 小时前
SpringBoot Maven 项目 pom 中的 plugin 插件用法整理
spring boot·后端·maven
yashuk2 小时前
SpringBoot中自定义Starter
java·spring boot·后端
独断万古他化3 小时前
【抽奖系统开发实战】Spring Boot 活动模块设计:事务保障、缓存优化与列表展示
java·spring boot·redis·后端·缓存·mvc
prince053 小时前
SpringBoot + 多级缓存(Caffeine + Redis + 空值缓存):防穿透、防雪崩、低延迟三合一
spring boot·redis·缓存