Excel有一层表头和两层表头导出

Excel有一层表头和两层表头导出

  1. 需求, 列表页和导出功能如以下形式:
  2. 列表页展示, 表头前端根据后端返回接口处理
    后端返回数据格式:

    定义的类结构:
java 复制代码
@Data
public class ShelfActivityVO implements Serializable {
    private static final long serialVersionUID = 7533478359895121450L;

    @ApiModelProperty("货架名称")
    private String shelfName;

    @ApiModelProperty("功能模块")
    private String eventName;

    @ApiModelProperty("月份数据Map,key为月份(如2026-01),value为该月份的活跃详情")
    private Map<String, ShelfActivityDetailVO> monthDataMap;
}

@Data
public class ShelfActivityDetailVO implements Serializable {
    private static final long serialVersionUID = -4535299118819963786L;

    @ApiModelProperty("用户数")
    private Integer usersNumber;

    @ApiModelProperty("活跃用户数")
    private Integer activeUsersNumber;

    @ApiModelProperty("访问次数")
    private Integer visitsNumber;

    @ApiModelProperty("活跃占比")
    private BigDecimal activeProportion;
}

@ApiOperation("运营管理-货架运营-货架活跃度报表分页列表")
    @GetMapping(value = "/operationManagerCenter/shelfActivityPages")
    @PermissionData(pageComponent = "operations-management/shelf-operation/shelf-activity-report")
    public Result<IPage<ShelfActivityVO>> shelfActivityPages(ShelfActivityParam param) {
        return Result.OK(shelfOperationService.shelfActivityPages(param));
    }

 @Override
    public IPage<ShelfActivityVO> shelfActivityPages(ShelfActivityParam param) {
        // 1. 权限校验
        List<String> list = PdmQueryGenerator.getUapFieldAuthList(SQLPermissionFiledEnum.ORG_CODE.getRuleColumn());
        if (CollectionUtils.isEmpty(list)) {
            return new Page<>();
        } else if (!list.contains(CommonConstant.AUTH_UAP_ALL)) {
            param.setOrgCodes(StringUtil.joinList(list));
        }

        // 2. 处理功能模块参数
        String eventNames = param.getEventNames();
        if (StringUtils.isNotBlank(eventNames)) {
            String[] split = eventNames.split(",");
            param.setEventNameList(Arrays.asList(split));
        }

        // 3. 生成月份范围列表
        List<String> monthRangeList = generateMonthRangeList(param.getEventTimeStart(), param.getEventTimeEnd());
        param.setMonthRangeList(monthRangeList);

        // 4. 查询数据库
        List<BgbuShelfActivity> shelfActivityList = bgbuShelfActivityService.getBgbuShelfActivityList(param);
        if (CollectionUtils.isEmpty(shelfActivityList)) {
            return new Page<>(param.getPageNo(), param.getPageSize(), 0);
        }

        // 5. 按照 shelfName + eventName 分组,构建 Map<货架_功能模块, Map<月份, 活跃详情>>
        Map<String, Map<String, ShelfActivityDetailVO>> groupedData = new LinkedHashMap<>();

        // 当 roleCode 为空时,需要将销售、市场、咨询、其他角色的数据聚合
        String separator = "|||";
        if (StringUtils.isBlank(param.getRoleCode())) {
            // 先按 shelfName + eventName + monthRange 分组聚合
            Map<String, List<BgbuShelfActivity>> aggregatedMap = shelfActivityList.stream()
                    .collect(Collectors.groupingBy(
                            activity -> activity.getShelfName() + separator + activity.getEventName() + separator + activity.getMonthRange(),
                            LinkedHashMap::new,
                            Collectors.toList()
                    ));

            for (Map.Entry<String, List<BgbuShelfActivity>> entry : aggregatedMap.entrySet()) {
                List<BgbuShelfActivity> activities = entry.getValue();
                String[] keyParts = entry.getKey().split("\\|\\|\\|", 3);
                String shelfEventKey = keyParts[0] + separator + (keyParts.length > 1 ? keyParts[1] : "");
                String monthRange = keyParts.length > 2 ? keyParts[2] : "";

                Map<String, ShelfActivityDetailVO> monthDataMap = groupedData.computeIfAbsent(shelfEventKey, k -> new LinkedHashMap<>());

                // 聚合计算:将同一货架+功能模块+月份下的用户数、活跃用户数、访问次数相加
                int sumUsersNumber = 0;
                int sumActiveUsersNumber = 0;
                int sumVisitsNumber = 0;
                for (BgbuShelfActivity activity : activities) {
                    if (activity.getRoleUserNumber() != null) {
                        sumUsersNumber += activity.getRoleUserNumber();
                    }
                    if (activity.getActiveUserNumber() != null) {
                        sumActiveUsersNumber += activity.getActiveUserNumber();
                    }
                    if (activity.getVisitsNumber() != null) {
                        sumVisitsNumber += activity.getVisitsNumber();
                    }
                }

                ShelfActivityDetailVO detailVO = new ShelfActivityDetailVO();
                detailVO.setUsersNumber(sumUsersNumber);
                detailVO.setActiveUsersNumber(sumActiveUsersNumber);
                detailVO.setVisitsNumber(sumVisitsNumber);
                // 重新计算活跃占比
                if (sumUsersNumber > 0) {
                    BigDecimal proportion = new BigDecimal(sumActiveUsersNumber)
                            .divide(new BigDecimal(sumUsersNumber), 4, BigDecimal.ROUND_HALF_UP)
                            .multiply(new BigDecimal("100"))
                            .setScale(2, BigDecimal.ROUND_HALF_UP);
                    detailVO.setActiveProportion(proportion);
                } else {
                    detailVO.setActiveProportion(BigDecimal.ZERO);
                }
                monthDataMap.put(monthRange, detailVO);
            }
        } else {
            // roleCode 不为空时,按原有逻辑处理
            for (BgbuShelfActivity activity : shelfActivityList) {
                String key = activity.getShelfName() + separator + activity.getEventName();
                Map<String, ShelfActivityDetailVO> monthDataMap = groupedData.computeIfAbsent(key, k -> new LinkedHashMap<>());

                ShelfActivityDetailVO detailVO = new ShelfActivityDetailVO();
                detailVO.setUsersNumber(activity.getRoleUserNumber());
                detailVO.setActiveUsersNumber(activity.getActiveUserNumber());
                detailVO.setVisitsNumber(activity.getVisitsNumber());
                detailVO.setActiveProportion(activity.getActiveProportion());
                monthDataMap.put(activity.getMonthRange(), detailVO);
            }
        }

        // 6. 转换为 ShelfActivityVO 列表
        List<ShelfActivityVO> voList = new ArrayList<>();
        for (Map.Entry<String, Map<String, ShelfActivityDetailVO>> entry : groupedData.entrySet()) {
            String[] keyParts = entry.getKey().split("\\|\\|\\|", 2);
            ShelfActivityVO vo = new ShelfActivityVO();
            vo.setShelfName(keyParts[0]);
            vo.setEventName(keyParts.length > 1 ? keyParts[1] : "");

            // 确保所有月份都有数据(没有数据的月份填充空值)
            Map<String, ShelfActivityDetailVO> completeMonthData = new LinkedHashMap<>();
            for (String month : monthRangeList) {
                completeMonthData.put(month, entry.getValue().getOrDefault(month, createEmptyDetailVO()));
            }
            vo.setMonthDataMap(completeMonthData);
            voList.add(vo);
        }

        // 7. 手动分页
        int total = voList.size();
        int pageNo = param.getPageNo();
        int pageSize = param.getPageSize();
        int fromIndex = (pageNo - 1) * pageSize;
        int toIndex = Math.min(fromIndex + pageSize, total);

        List<ShelfActivityVO> pageList;
        if (fromIndex >= total) {
            pageList = new ArrayList<>();
        } else {
            pageList = voList.subList(fromIndex, toIndex);
        }

        // 8. 构建分页结果
        IPage<ShelfActivityVO> resultPage = new Page<>(pageNo, pageSize, total);
        resultPage.setRecords(pageList);
        return resultPage;
    }

导出功能处理:

java 复制代码
@ApiOperation("运营管理-货架运营-货架活跃度报表分页列表导出")
    @GetMapping(value = "/operationManagerCenter/exportShelfActivity")
    @PermissionData(pageComponent = "operations-management/shelf-operation/shelf-activity-report")
    @AutoLog(operateType = BizLogOperateTypeConstant.EXPORT, value = "货架活跃度报表数据导出")
    @DuplicateSubmit
    public Result<Boolean> exportShelfActivity(HttpServletResponse response, ShelfActivityParam param) {
        shelfOperationService.exportShelfActivity(response, param);
        return Result.OK(true);
    }

@Override
    public void exportShelfActivity(HttpServletResponse response, ShelfActivityParam param) {
        // 权限验证
        List<String> authList = PdmQueryGenerator.getUapFieldAuthList(SQLPermissionFiledEnum.ORG_CODE.getRuleColumn());
        if (CollectionUtils.isEmpty(authList)) {
            throw new CustomException("无数据权限");
        }
        if (!authList.contains(CommonConstant.AUTH_UAP_ALL)) {
            param.setOrgCodes(StringUtil.joinList(authList));
        }

        // 处理功能模块参数
        String eventNames = param.getEventNames();
        if (StringUtils.isNotBlank(eventNames)) {
            String[] split = eventNames.split(",");
            param.setEventNameList(Arrays.asList(split));
        }

        // 生成月份范围列表
        List<String> monthRangeList = generateMonthRangeList(param.getEventTimeStart(), param.getEventTimeEnd());
        param.setMonthRangeList(monthRangeList);

        // 查询全部数据(不分页)
        List<BgbuShelfActivity> shelfActivityList = bgbuShelfActivityService.getBgbuShelfActivityList(param);

        Workbook workbook = null;
        try {
            workbook = new XSSFWorkbook();
            Sheet sheet = workbook.createSheet("货架活跃度报表数据");

            // 创建表头样式
            CellStyle headerStyle = createHeaderStyle(workbook);
            CellStyle dataStyle = createDataStyle(workbook);

            // 固定列数
            int fixedColCount = 2;
            // 每个月份的子列数
            int subColCount = 4;
            // 子列标题
            String[] subHeaders = {"用户数", "活跃用户数", "访问次数", "活跃占比"};

            // 创建第一行(月份标题行)
            Row headerRow1 = sheet.createRow(0);
            // 创建第二行(子列标题行)
            Row headerRow2 = sheet.createRow(1);

            // 设置固定列标题(合并两行)
            Cell bgbuCell = headerRow1.createCell(0);
            bgbuCell.setCellValue("BGBU货架");
            bgbuCell.setCellStyle(headerStyle);
            sheet.addMergedRegion(new CellRangeAddress(0, 1, 0, 0));

            Cell funcCell = headerRow1.createCell(1);
            funcCell.setCellValue("功能模块");
            funcCell.setCellStyle(headerStyle);
            sheet.addMergedRegion(new CellRangeAddress(0, 1, 1, 1));

            // 设置第二行固定列的样式(合并后的单元格需要设置样式)
            Cell bgbuCell2 = headerRow2.createCell(0);
            bgbuCell2.setCellStyle(headerStyle);
            Cell funcCell2 = headerRow2.createCell(1);
            funcCell2.setCellStyle(headerStyle);

            // 创建月份列标题(动态生成)
            for (int i = 0; i < monthRangeList.size(); i++) {
                String monthStr = monthRangeList.get(i);
                // 转换月份格式:2026-02 -> 2026年2月
                String displayMonth = formatMonthDisplay(monthStr);

                int startCol = fixedColCount + i * subColCount;
                int endCol = startCol + subColCount - 1;

                // 设置月份标题(合并4个单元格)
                Cell monthCell = headerRow1.createCell(startCol);
                monthCell.setCellValue(displayMonth);
                monthCell.setCellStyle(headerStyle);
                sheet.addMergedRegion(new CellRangeAddress(0, 0, startCol, endCol));

                // 设置合并区域其他单元格的样式
                for (int j = 1; j < subColCount; j++) {
                    Cell mergedCell = headerRow1.createCell(startCol + j);
                    mergedCell.setCellStyle(headerStyle);
                }

                // 设置子列标题
                for (int j = 0; j < subColCount; j++) {
                    Cell subCell = headerRow2.createCell(startCol + j);
                    subCell.setCellValue(subHeaders[j]);
                    subCell.setCellStyle(headerStyle);
                }
            }

            // 按照 shelfName + eventName 分组数据
            Map<String, Map<String, ShelfActivityDetailVO>> groupedData = new LinkedHashMap<>();
            String separator = "|||";
            if (CollectionUtils.isNotEmpty(shelfActivityList)) {
                // 当 roleCode 为空时,需要将销售、市场、咨询、其他角色的数据聚合
                if (StringUtils.isBlank(param.getRoleCode())) {
                    // 先按 shelfName + eventName + monthRange 分组聚合
                    Map<String, List<BgbuShelfActivity>> aggregatedMap = shelfActivityList.stream()
                            .collect(Collectors.groupingBy(
                                    activity -> activity.getShelfName() + separator + activity.getEventName() + separator + activity.getMonthRange(),
                                    LinkedHashMap::new,
                                    Collectors.toList()
                            ));

                    for (Map.Entry<String, List<BgbuShelfActivity>> aggEntry : aggregatedMap.entrySet()) {
                        List<BgbuShelfActivity> activities = aggEntry.getValue();
                        String[] aggKeyParts = aggEntry.getKey().split("\\|\\|\\|", 3);
                        String shelfEventKey = aggKeyParts[0] + separator + (aggKeyParts.length > 1 ? aggKeyParts[1] : "");
                        String monthRange = aggKeyParts.length > 2 ? aggKeyParts[2] : "";

                        Map<String, ShelfActivityDetailVO> monthDataMap = groupedData.computeIfAbsent(shelfEventKey, k -> new LinkedHashMap<>());

                        // 聚合计算:将同一货架+功能模块+月份下的用户数、活跃用户数、访问次数相加
                        int sumUsersNumber = 0;
                        int sumActiveUsersNumber = 0;
                        int sumVisitsNumber = 0;
                        for (BgbuShelfActivity activity : activities) {
                            if (activity.getRoleUserNumber() != null) {
                                sumUsersNumber += activity.getRoleUserNumber();
                            }
                            if (activity.getActiveUserNumber() != null) {
                                sumActiveUsersNumber += activity.getActiveUserNumber();
                            }
                            if (activity.getVisitsNumber() != null) {
                                sumVisitsNumber += activity.getVisitsNumber();
                            }
                        }

                        ShelfActivityDetailVO detailVO = new ShelfActivityDetailVO();
                        detailVO.setUsersNumber(sumUsersNumber);
                        detailVO.setActiveUsersNumber(sumActiveUsersNumber);
                        detailVO.setVisitsNumber(sumVisitsNumber);
                        // 重新计算活跃占比
                        if (sumUsersNumber > 0) {
                            BigDecimal proportion = new BigDecimal(sumActiveUsersNumber)
                                    .divide(new BigDecimal(sumUsersNumber), 4, BigDecimal.ROUND_HALF_UP)
                                    .multiply(new BigDecimal("100"))
                                    .setScale(2, BigDecimal.ROUND_HALF_UP);
                            detailVO.setActiveProportion(proportion);
                        } else {
                            detailVO.setActiveProportion(BigDecimal.ZERO);
                        }
                        monthDataMap.put(monthRange, detailVO);
                    }
                } else {
                    // roleCode 不为空时,按原有逻辑处理
                    for (BgbuShelfActivity activity : shelfActivityList) {
                        String key = activity.getShelfName() + separator + activity.getEventName();
                        Map<String, ShelfActivityDetailVO> monthDataMap = groupedData.computeIfAbsent(key, k -> new LinkedHashMap<>());

                        ShelfActivityDetailVO detailVO = new ShelfActivityDetailVO();
                        detailVO.setUsersNumber(activity.getRoleUserNumber());
                        detailVO.setActiveUsersNumber(activity.getActiveUserNumber());
                        detailVO.setVisitsNumber(activity.getVisitsNumber());
                        if (null != activity.getActiveProportion()) {
                            detailVO.setActiveProportion(activity.getActiveProportion());
                        } else {
                            detailVO.setActiveProportion(activity.getActiveProportion());
                        }
                        monthDataMap.put(activity.getMonthRange(), detailVO);
                    }
                }
            }

            // 填充数据
            int rowIndex = 2;
            for (Map.Entry<String, Map<String, ShelfActivityDetailVO>> entry : groupedData.entrySet()) {
                Row dataRow = sheet.createRow(rowIndex);
                String[] keyParts = entry.getKey().split("\\|\\|\\|", 2);

                // 填充固定列
                Cell shelfNameCell = dataRow.createCell(0);
                shelfNameCell.setCellValue(keyParts[0]);
                shelfNameCell.setCellStyle(dataStyle);

                Cell eventNameCell = dataRow.createCell(1);
                eventNameCell.setCellValue(keyParts.length > 1 ? keyParts[1] : "");
                eventNameCell.setCellStyle(dataStyle);

                // 填充月份数据
                Map<String, ShelfActivityDetailVO> monthDataMap = entry.getValue();
                for (int i = 0; i < monthRangeList.size(); i++) {
                    String month = monthRangeList.get(i);
                    ShelfActivityDetailVO detailVO = monthDataMap.get(month);
                    int startCol = fixedColCount + i * subColCount;

                    if (detailVO != null) {
                        // 用户数
                        Cell usersCell = dataRow.createCell(startCol);
                        if (detailVO.getUsersNumber() != null) {
                            usersCell.setCellValue(detailVO.getUsersNumber());
                        } else {
                            usersCell.setCellValue("-");
                        }
                        usersCell.setCellStyle(dataStyle);

                        // 活跃用户数
                        Cell activeUsersCell = dataRow.createCell(startCol + 1);
                        if (detailVO.getActiveUsersNumber() != null) {
                            activeUsersCell.setCellValue(detailVO.getActiveUsersNumber());
                        } else {
                            activeUsersCell.setCellValue("-");
                        }
                        activeUsersCell.setCellStyle(dataStyle);

                        // 访问次数
                        Cell visitsCell = dataRow.createCell(startCol + 2);
                        if (detailVO.getVisitsNumber() != null) {
                            visitsCell.setCellValue(detailVO.getVisitsNumber());
                        } else {
                            visitsCell.setCellValue("-");
                        }
                        visitsCell.setCellStyle(dataStyle);

                        // 活跃占比
                        Cell proportionCell = dataRow.createCell(startCol + 3);
                        if (detailVO.getActiveProportion() != null) {
                            proportionCell.setCellValue(detailVO.getActiveProportion().toPlainString() + "%");
                        } else {
                            proportionCell.setCellValue("-");
                        }
                        proportionCell.setCellStyle(dataStyle);
                    } else {
                        // 无数据填充 "-"
                        for (int j = 0; j < subColCount; j++) {
                            Cell emptyCell = dataRow.createCell(startCol + j);
                            emptyCell.setCellValue("-");
                            emptyCell.setCellStyle(dataStyle);
                        }
                    }
                }
                rowIndex++;
            }

            // 设置列宽
            sheet.setColumnWidth(0, 15 * 256);
            sheet.setColumnWidth(1, 18 * 256);
            int totalCols = fixedColCount + monthRangeList.size() * subColCount;
            for (int i = fixedColCount; i < totalCols; i++) {
                sheet.setColumnWidth(i, 12 * 256);
            }

            // 写入响应流
            String encodedFileName = URLEncoder.encode("货架活跃度报表数据", "UTF-8").replaceAll("\\+", "%20");
            response.setHeader("Content-Disposition", "attachment;filename=" + encodedFileName + ".xlsx");
            response.setCharacterEncoding("UTF-8");
            workbook.write(response.getOutputStream());

        } catch (Exception e) {
            log.error("货架活跃度报表数据导出异常:", e);
            throw new CustomException("货架活跃度报表数据导出异常");
        } finally {
            if (workbook != null) {
                try {
                    workbook.close();
                } catch (Exception e) {
                    log.error("关闭工作簿异常:", e);
                }
            }
        }
    }

 /**
     * 生成月份范围列表
     *
     * @param startTime 开始时间
     * @param endTime   结束时间
     * @return 月份范围列表 (格式: yyyy-MM)
     */
    private List<String> generateMonthRangeList(Date startTime, Date endTime) {
        List<String> monthRangeList = new ArrayList<>();
        YearMonth start;
        YearMonth end;

        if (startTime == null && endTime == null) {
            // 默认查询最近6个月(不含当月)
            end = YearMonth.now().minusMonths(1);
            start = end.minusMonths(5);
        } else if (startTime != null && endTime != null) {
            // 指定了时间范围
            Calendar startCal = Calendar.getInstance();
            startCal.setTime(startTime);
            start = YearMonth.of(startCal.get(Calendar.YEAR), startCal.get(Calendar.MONTH) + 1);

            Calendar endCal = Calendar.getInstance();
            endCal.setTime(endTime);
            end = YearMonth.of(endCal.get(Calendar.YEAR), endCal.get(Calendar.MONTH) + 1);
        } else if (startTime != null) {
            // 只有开始时间,查询到当前月份
            Calendar startCal = Calendar.getInstance();
            startCal.setTime(startTime);
            start = YearMonth.of(startCal.get(Calendar.YEAR), startCal.get(Calendar.MONTH) + 1);
            end = YearMonth.now();
        } else {
            // 只有结束时间,向前查6个月
            Calendar endCal = Calendar.getInstance();
            endCal.setTime(endTime);
            end = YearMonth.of(endCal.get(Calendar.YEAR), endCal.get(Calendar.MONTH) + 1);
            start = end.minusMonths(5);
        }

        // 从最新月份向前排列
        YearMonth current = end;
        while (!current.isBefore(start)) {
            String monthStr = String.format("%d-%02d", current.getYear(), current.getMonthValue());
            monthRangeList.add(monthStr);
            current = current.minusMonths(1);
        }

        return monthRangeList;
    }
相关推荐
认真的小羽❅20 小时前
0-1手写通用的 Excel 导入/导出工具类
java·excel
catoop20 小时前
Excel 实战技巧:单元格相对引用 INDIRECT、ROW、COLUMN 函数
excel
Teable任意门互动1 天前
中小企业进销存实战:Teable多维表格从零搭建高效库存管理系统
开发语言·数据库·excel·飞书·开源软件
零零发聊技术1 天前
Excel 2016版的TextJoin函数为什么不能用?
excel·textjoin
catoop1 天前
Excel 实战技巧:动态单元格引用中使用 LET 函数优化 Excel 公式性能与可读性
excel
lengxuemo1 天前
Excel做正态分布图
学习·excel
白白白飘1 天前
【EXCEL】数据透视表学习
学习·excel
一晌小贪欢1 天前
PyQt5 + Pandas 打造常见的表格(Excel/CSV)读取与处理工具
python·qt·excel·pandas·python办公·excel处理
小鹿软件办公1 天前
如何用 Excel 宏原地批量修改单元格内容?
excel·excel重命名