JAVA - 使用Apache POI 自定义报表字段手写导出(支持-合并单元格)


bash 复制代码
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
</dependency>
java 复制代码
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

//工具类导出(支持1单1商品,1单多商品)
public class ExcelExportUtil {

    public static String convertSpec(String spec) {
        JSONArray array = JSONUtil.parseArray(spec);
        return array.stream().map(obj -> {
            cn.hutool.json.JSONObject o = (cn.hutool.json.JSONObject) obj;
            return o.getStr("name") + ":" + o.getStr("value");
        }).collect(Collectors.joining(" "));
    }

    public static void putIfSelected(Map<String, Object> map, String field, Object value, List<String> exportFields) {
        if (exportFields.contains(field)) {
            map.put(field, value);
        }
    }


    //表头样式
    private static CellStyle createHeadStyle(Workbook workbook) {
        CellStyle style = workbook.createCellStyle();
        //背景色(浅灰)
        style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        //字体
        Font font = workbook.createFont();
        font.setFontName("微软雅黑");
        font.setFontHeightInPoints((short) 15);
        font.setBold(true);
        style.setFont(font);
        //对齐
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        //边框
        style.setBorderTop(BorderStyle.THIN);
        style.setBorderBottom(BorderStyle.THIN);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);
        return style;
    }

    //数据样式
    private static CellStyle createDataStyle(Workbook workbook) {
        CellStyle style = workbook.createCellStyle();
        Font font = workbook.createFont();
        font.setFontName("微软雅黑");
        font.setFontHeightInPoints((short) 11);
        style.setFont(font);
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        return style;
    }

    //初始化
    private static void initResponse(HttpServletResponse response, String sheetName) throws UnsupportedEncodingException {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(sheetName, "UTF-8") + ".xlsx");
    }


    //---------------------------售后导出-------------------------------
    //自定义字段普通导出
    public static void export(HttpServletResponse response, List<Map<String, Object>> data, List<String> fields, String sheetName) throws IOException {
        initResponse(response, sheetName);
        Workbook workbook = new XSSFWorkbook();
        Sheet sheet = workbook.createSheet(sheetName);
        writeSheet(workbook, sheet, data, fields, false);
        workbook.write(response.getOutputStream());
        workbook.close();
    }

    //---------------------------一键下单导出-------------------------------
    //自定义字段订单信息合并导出
    public static void exportOrder(HttpServletResponse response, List<Map<String, Object>> data, List<String> fields, String sheetName) throws IOException {
        initResponse(response, sheetName);
        Workbook workbook = new XSSFWorkbook();
        Sheet sheet = workbook.createSheet(sheetName);
        writeSheet(workbook, sheet, data, fields, true);
        workbook.write(response.getOutputStream());
        workbook.close();
    }

    //写sheet
    private static void writeSheet(Workbook workbook, Sheet sheet, List<Map<String, Object>> data, List<String> fields, boolean mergeOrder) {
        CellStyle headStyle = createHeadStyle(workbook);
        CellStyle dataStyle = createDataStyle(workbook);
        writeHeader(sheet, fields, headStyle);
        writeRows(sheet, data, fields, dataStyle, mergeOrder);
    }

    //创建表头
    private static void writeHeader(Sheet sheet, List<String> fields, CellStyle headStyle) {
        Row headRow = sheet.createRow(0);
        headRow.setHeightInPoints(30);
        for (int i = 0; i < fields.size(); i++) {
            Cell cell = headRow.createCell(i);
            cell.setCellValue(fields.get(i));
            cell.setCellStyle(headStyle);
            sheet.setColumnWidth(i, 20 * 256);
        }
    }

    //写数据
    private static void writeRows(Sheet sheet, List<Map<String, Object>> data, List<String> fields, CellStyle dataStyle, boolean mergeOrder) {
        int rowIndex = 1;
        int mergeStartRow = 1;
        String lastOrderId = "";
        for (Map<String, Object> rowData : data) {
            String currentOrderId = String.valueOf(rowData.getOrDefault("订单编号", ""));
            //合并逻辑只在 mergeOrder = true 时生效
            if (mergeOrder && !lastOrderId.equals(currentOrderId)) {
                if (!lastOrderId.isEmpty()) {
                    mergeOrderCells(sheet, mergeStartRow, rowIndex - 1, fields);
                }
                mergeStartRow = rowIndex;
            }
            Row row = sheet.createRow(rowIndex++);
            for (int i = 0; i < fields.size(); i++) {
                String field = fields.get(i);
                Object value = rowData.get(field);
                Cell cell = row.createCell(i);
                cell.setCellStyle(dataStyle);
                if (value == null) {
                    cell.setCellValue("");
                } else if (value instanceof Number) {
                    cell.setCellValue(((Number) value).doubleValue());
                } else {
                    cell.setCellValue(value.toString());
                }
            }
            lastOrderId = currentOrderId;
        }
        // 最后一个订单合并
        if (mergeOrder && !lastOrderId.isEmpty()) {
            mergeOrderCells(sheet, mergeStartRow, rowIndex - 1, fields);
        }
    }


    // ===== 辅助方法:合并订单字段的单元格 =====
    private static void mergeOrderCells(Sheet sheet, int startRow, int endRow, List<String> fields) {
        // 如果起始行大于或等于结束行,说明只有一行数据,不需要合并,直接返回
        if (startRow >= endRow) {
            return;
        }
        for (int i = 0; i < fields.size(); i++) {
            String fieldName = fields.get(i);
            // 如果是订单字段,则合并
            if (ORDER_FIELDS.contains(fieldName)) {
                sheet.addMergedRegion(new CellRangeAddress(startRow, endRow, i, i));
            }
        }
    }

    //定义合并的表头
    private static final Set<String> ORDER_FIELDS = Set.of(
            "订单编号", "店铺名称", "店铺ID", "成交时间", "订单状态", "实收款",
            "支付方式", "发货时间", "旗帜颜色", "用户留言", "店铺备注", "软件备注"
    );
}
java 复制代码
	@PostMapping("/export")
    public void export(HttpServletResponse response, @Valid @RequestBody OrderReq req) throws IOException {
        List<OrderVO> list = baseService.getOneClickOrderList(req);		//换成自己的查询列表
        Assert.isFalse(StringUtils.isEmpty(list), "导出失败,数据为空");
        List<Map<String, Object>> data = otherService.buildOrderExportData(list, req.getExportFields());
        ExcelExportUtil.exportOrder(response, data, req.getExportFields(), "oneClickOrder");
    }

//业务层,整合数据
public List<Map<String, Object>> buildOrderExportData(List<OrderVO> list, List<String> exportFields) {
        List<Map<String, Object>> result = new ArrayList<>();
        for (OrderVO vo : list) {
            for (ProductVO p : vo.getProductVOS()) {
                //-----------------------订单信息---------------------
                Map<String, Object> map = new HashMap<>();
                ExcelExportUtil.putIfSelected(map, "订单编号", vo.getOrderId(), exportFields);
                ExcelExportUtil.putIfSelected(map, "店铺名称", vo.getShopName(), exportFields);
                ExcelExportUtil.putIfSelected(map, "店铺ID", vo.getShopId(), exportFields);
                ExcelExportUtil.putIfSelected(map, "成交时间", vo.getPayTime(), exportFields);
                ExcelExportUtil.putIfSelected(map, "订单状态", vo.getOrderStatusDesc(), exportFields);
                ExcelExportUtil.putIfSelected(map, "实收款", vo.getActualReceiveAmount(), exportFields);
                ExcelExportUtil.putIfSelected(map, "支付方式", TagConstants.getDesc(vo.getPayType()), exportFields);
                ExcelExportUtil.putIfSelected(map, "发货时间", vo.getShipTimeStr(), exportFields);
                ExcelExportUtil.putIfSelected(map, "旗帜颜色", TagConstants.getStarsDesc(vo.getSellerRemarkStars()), exportFields);
                ExcelExportUtil.putIfSelected(map, "用户留言", vo.getBuyerWords(), exportFields);
                ExcelExportUtil.putIfSelected(map, "店铺备注", vo.getSellerWords(), exportFields);
                ExcelExportUtil.putIfSelected(map, "软件备注", vo.getLocalRemark(), exportFields);
                //-----------------------商品信息---------------------
                String afterSaleStatus = p.getAfterSaleStatus() == 0 ?
                        p.getProductOrderStatusDesc() : AfterSaleConstants.getAfterSaleStatusDesc(p.getAfterSaleStatus());
                ExcelExportUtil.putIfSelected(map, "商品标题", p.getProductName(), exportFields);
                ExcelExportUtil.putIfSelected(map, "商品ID", p.getProductId(), exportFields);
                ExcelExportUtil.putIfSelected(map, "商品单价", p.getProductOrderAmount(), exportFields);
                ExcelExportUtil.putIfSelected(map, "商品数量", p.getItemNum(), exportFields);
                ExcelExportUtil.putIfSelected(map, "售后状态", afterSaleStatus, exportFields);
                ExcelExportUtil.putIfSelected(map, "商品实收", p.getProductPayAmount(), exportFields);
                ExcelExportUtil.putIfSelected(map, "运费", p.getPostAmount(), exportFields);
                ExcelExportUtil.putIfSelected(map, "运费优惠", p.getPostInsuranceAmount(), exportFields);
                ExcelExportUtil.putIfSelected(map, "规格", ExcelExportUtil.convertSpec(p.getSpec()), exportFields);
                ExcelExportUtil.putIfSelected(map, "商品编码", p.getCode(), exportFields);
                if (StringUtils.isNotEmpty(p.getAlProductVOList())) {
                    AlProductVO item = p.getAlProductVOList().get(0);
                    ExcelExportUtil.putIfSelected(map, "物流名称", item.getLogisticsCompany(), exportFields);
                    ExcelExportUtil.putIfSelected(map, "物流单号", item.getBillNo(), exportFields);
                }
                result.add(map);
            }
        }
        return result;
    }
相关推荐
折哥的程序人生 · 物流技术专研1 小时前
Java面试85题图解版 · 特别篇:2026后端高频面试题复盘(算法底层逻辑+高并发架构设计全解析,附Java实战代码)
java·网络·数据库·算法·面试
xxie1237941 小时前
return与print
开发语言·python
秋91 小时前
从 Python 后端工程师转型 AI Engineer(AI 工程化)的完整补课清单(2026实战版)
开发语言·人工智能·python
一条泥憨鱼1 小时前
【Redis】数据类型和常用命令
java·数据库·redis·后端·缓存
云烟成雨TD1 小时前
Spring AI Alibaba 1.x 系列【78】沙箱(Sandbox)
java·人工智能·spring
程序员二叉2 小时前
【Java】 异常高频面试题精讲 | 易错点+对比总结
java·开发语言·面试
周航宇JoeZhou2 小时前
JB3-9-SpringAI(二)
java·ai·agent·多智能体·调度·智能体·观察
好家伙VCC2 小时前
Web Components主题热切换方案揭秘
java·前端
慕木沐2 小时前
Google ADK Java 1.0版本 核心机制与实战 Demo
java·开发语言·python
BBWEYY终身尊贵会员2 小时前
教培小机构小程序开发:从技术选型、系统架构到表结构与接口设计的完整实践
apache