


触发场景描述
利用模板填充,先将数据垂直填充,其中有一个字段是列表,将其输出问一个中间excel,然后利用中间的excel作为模板去填充具体的list,发现第一列会不会被填充掉,先用垂直填充将{tmPsnInfoList.tag}替换为{dataRecord_xxxx.data},然后通过中间的excel作为输入模板,填充{tmPsnInfoList.tag},发现第一列的数据为{dataRecord_xxxx.data}而不是列表的具体值
触发Bug的代码
public void doExportMonthReport(TmMonthReportVO tmMonthReportVO, HttpServletResponse response, HttpServletRequest request) {
// 1. 从类路径读取模板文件
String templateFileName = "template/月报模板.xlsx";
InputStream templateInputStream = null;
File firstOutputFile = null;
File finalOutputFile = null;
ExcelWriter excelWriter = null;
try {
// 获取模板文件流
templateInputStream = this.getClass().getClassLoader().getResourceAsStream(templateFileName);
if (templateInputStream == null) {
throw new RuntimeException("模板文件不存在: " + templateFileName);
}
// 2. 创建临时中间文件
firstOutputFile = File.createTempFile("render_step1_", ".xlsx");
String firstOutputFilePath = firstOutputFile.getAbsolutePath();
// 第一步填充
assembleAttendanceData(tmMonthReportVO);
// 3. 第一次模板填充
try{
ExcelWriter firstExcelWriter = EasyExcel.write(firstOutputFilePath)
.withTemplate(templateInputStream)
.build();
WriteSheet writeSheet = EasyExcel.writerSheet().build();
Map<String, Object> renderMap = new HashMap<>();
renderMap.put("year", tmMonthReportVO.getYear());
renderMap.put("month", tmMonthReportVO.getMonth());
FillConfig hfillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();
FillConfig vfillConfig = FillConfig.builder().direction(WriteDirectionEnum.VERTICAL).build();
firstExcelWriter.fill(new FillWrapper("dayMapping", tmMonthReportVO.getDayMapping()), hfillConfig, writeSheet);
firstExcelWriter.fill(new FillWrapper("tmPsnInfoList", tmMonthReportVO.getTmPsnInfoList()), vfillConfig, writeSheet);
firstExcelWriter.fill(renderMap, writeSheet);
firstExcelWriter.finish();
} catch (RuntimeException e) {
throw new RuntimeException(e);
}
// 4. 创建最终输出临时文件
finalOutputFile = File.createTempFile("render_final_", ".xlsx");
String finalOutputFilePath = finalOutputFile.getAbsolutePath();
if (!firstOutputFile.exists() || firstOutputFile.length() == 0) {
throw new RuntimeException("第一次填充生成的文件无效");
}
// 5. 第二次模板填充
try {
ExcelWriter finalExcelWriter = EasyExcel.write(finalOutputFilePath)
.withTemplate(firstOutputFile)
.build();
WriteSheet writeSheet = EasyExcel.writerSheet().build();
Map<String, Object> otherMap = new HashMap<>();
for (Map<String, Object> map : tmMonthReportVO.getTmPsnInfoList()) {
String pkPsndoc = (String) map.getOrDefault("pkPsndoc", "");
List<Map<String, Object>> dataRecords = (List<Map<String, Object>>) map.get("dataRecord");
if (dataRecords != null && !dataRecords.isEmpty()) {
// 在第一次填充时占位符已经变为 "dataRecord_xxx.data",第二次填充时可以直接填充
otherMap.put("dataRecord_" + pkPsndoc + ".data", dataRecords.get(0).getOrDefault("data", ""));
// 正确填充第二次数据,填充到占位符 "dataRecord_xxx" 对应的地方
FillConfig fillConfigHorizontal = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();
finalExcelWriter.fill(new FillWrapper("dataRecord_" + pkPsndoc, dataRecords), fillConfigHorizontal, writeSheet);
}
}
finalExcelWriter.finish();
} catch (RuntimeException e) {
throw new RuntimeException(e);
}
// 6. 设置响应头,输出文件流
response.setContentType("application/octet-stream");
String fileName = URLEncoder.encode("月度考勤报表.xlsx", "UTF-8");
response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ";filename*=UTF-8''" + fileName);
try (InputStream inputStream = new FileInputStream(finalOutputFile);
OutputStream outputStream = response.getOutputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
}
} catch (Exception e) {
throw new RuntimeException("导出月报失败", e);
} finally {
// 7. 清理临时文件
try {
if (templateInputStream != null) {
templateInputStream.close();
}
if (firstOutputFile != null && firstOutputFile.exists()) {
firstOutputFile.delete();
}
if (finalOutputFile != null && finalOutputFile.exists()) {
finalOutputFile.delete();
}
} catch (IOException e) {
// 清理失败不影响主流程
e.printStackTrace();
}
}
}
private void assembleAttendanceData(TmMonthReportVO tmMonthReportVO) {
List<Map<String, List<Map<String, Map<String, String>>>>> tmPsnAttendanceList = tmMonthReportVO.getTmPsnAttendanceList();
for (Map<String, Object> map : tmMonthReportVO.getTmPsnInfoList()) {
String pkPsndoc = (String)map.getOrDefault("pkPsndoc","");
Optional<Map<String, List<Map<String, Map<String, String>>>>> attendanceData = tmPsnAttendanceList.stream().filter(map1 -> map1!=null && map1.containsKey(pkPsndoc)).findFirst();
List<Map<String,Object>> data= new LinkedList<>();
if (attendanceData.isPresent()){
Map<String, List<Map<String, Map<String, String>>>> record = attendanceData.get();
for (Map.Entry<String, List<Map<String, Map<String, String>>>> entry : record.entrySet()) {
List<Map<String, Map<String, String>>> attendenceRecord = entry.getValue();
for (Map<String, Map<String, String>> mapMap : attendenceRecord) {
for (Map.Entry<String, Map<String, String>> mapEntry : mapMap.entrySet()) {
Map<String, String> acutalRecord = mapEntry.getValue();
Map<String, Object> dataRecord = new HashMap<>();
String minTime = acutalRecord.get("minTime");
String maxTime = acutalRecord.get("maxTime");
dataRecord.put("data", minTime + "\n" + maxTime);
data.add(dataRecord);
}
}
}
}else {
int size = tmMonthReportVO.getDayMapping().size();
for (int i = 0; i < size; i++) {
Map<String, Object> dataRecord = new HashMap<>();
dataRecord.put("data", "");
data.add(dataRecord);
}
}
map.put("dataRecord", data);
map.put("tag", "{dataRecord_"+pkPsndoc+".data}");
}
}



触发场景描述
利用模板填充,先将数据垂直填充,其中有一个字段是列表,将其输出问一个中间excel,然后利用中间的excel作为模板去填充具体的list,发现第一列会不会被填充掉,先用垂直填充将{tmPsnInfoList.tag}替换为{dataRecord_xxxx.data},然后通过中间的excel作为输入模板,填充{tmPsnInfoList.tag},发现第一列的数据为{dataRecord_xxxx.data}而不是列表的具体值
触发Bug的代码
public void doExportMonthReport(TmMonthReportVO tmMonthReportVO, HttpServletResponse response, HttpServletRequest request) {
// 1. 从类路径读取模板文件
String templateFileName = "template/月报模板.xlsx";
InputStream templateInputStream = null;
File firstOutputFile = null;
File finalOutputFile = null;
ExcelWriter excelWriter = null;
}
private void assembleAttendanceData(TmMonthReportVO tmMonthReportVO) {
List<Map<String, List<Map<String, Map<String, String>>>>> tmPsnAttendanceList = tmMonthReportVO.getTmPsnAttendanceList();
for (Map<String, Object> map : tmMonthReportVO.getTmPsnInfoList()) {
String pkPsndoc = (String)map.getOrDefault("pkPsndoc","");
Optional<Map<String, List<Map<String, Map<String, String>>>>> attendanceData = tmPsnAttendanceList.stream().filter(map1 -> map1!=null && map1.containsKey(pkPsndoc)).findFirst();
List<Map<String,Object>> data= new LinkedList<>();
if (attendanceData.isPresent()){
Map<String, List<Map<String, Map<String, String>>>> record = attendanceData.get();
for (Map.Entry<String, List<Map<String, Map<String, String>>>> entry : record.entrySet()) {
List<Map<String, Map<String, String>>> attendenceRecord = entry.getValue();
for (Map<String, Map<String, String>> mapMap : attendenceRecord) {
for (Map.Entry<String, Map<String, String>> mapEntry : mapMap.entrySet()) {
Map<String, String> acutalRecord = mapEntry.getValue();
Map<String, Object> dataRecord = new HashMap<>();
String minTime = acutalRecord.get("minTime");
String maxTime = acutalRecord.get("maxTime");
dataRecord.put("data", minTime + "\n" + maxTime);
data.add(dataRecord);
}
}
}
}else {
int size = tmMonthReportVO.getDayMapping().size();
for (int i = 0; i < size; i++) {
Map<String, Object> dataRecord = new HashMap<>();
dataRecord.put("data", "");
data.add(dataRecord);
}
}
map.put("dataRecord", data);
map.put("tag", "{dataRecord_"+pkPsndoc+".data}");
}
}