Excel有一层表头和两层表头导出
- 需求, 列表页和导出功能如以下形式:

- 列表页展示, 表头前端根据后端返回接口处理
后端返回数据格式:

定义的类结构:
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;
}