diff --git a/bonus-business/src/main/java/com/bonus/business/controller/tool/ResourceSummaryExcelExporter.java b/bonus-business/src/main/java/com/bonus/business/controller/tool/ResourceSummaryExcelExporter.java index 30f69b1..6bb6e1e 100644 --- a/bonus-business/src/main/java/com/bonus/business/controller/tool/ResourceSummaryExcelExporter.java +++ b/bonus-business/src/main/java/com/bonus/business/controller/tool/ResourceSummaryExcelExporter.java @@ -1,5 +1,6 @@ package com.bonus.business.controller.tool; +import com.bonus.digital.dao.BusinessTypeVo; import com.bonus.digital.dao.ResourceSummaryVo; import org.apache.poi.ss.usermodel.*; import org.apache.poi.ss.util.CellRangeAddress; @@ -17,17 +18,22 @@ import java.util.*; import java.util.stream.Collectors; /** - * 月计划资源统计分析Excel导出工具类 + * 月计划资源统计分析Excel导出工具类(适配动态业务类型) + * 修复点: + * 1. 运检站A列合并行数多一行问题 + * 2. 单元格边框缺失问题 + * 3. 第四行C列表头改为“工日” + * 4. 第五行B列清空(移除“总工日”文本) */ public class ResourceSummaryExcelExporter { private static final Logger log = LoggerFactory.getLogger(ResourceSummaryExcelExporter.class); // 常量定义 - private static final int STATION_ROW_TOTAL = 14; // 单个运检站总行数 - private static final int A_MERGE_START = 3; // A列合并起始行偏移(第4行) - private static final int A_MERGE_END = 13; // A列合并结束行偏移(第14行) - private static final int MAX_COLUMN = 5; // 最大列索引(F列) - private static final int REMARK_COL_INDEX = 5; // 备注列索引(F列) + private static final int STATION_ROW_BASE = 5; // 单个运检站基础行数(标题+人员信息+子表头+总工日+分包用工) + private static final int A_MERGE_START = 3; // A列合并起始行偏移(第4行) + private static final int MAX_COLUMN = 5; // 最大列索引(F列) + private static final int REMARK_COL_INDEX = 5; // 备注列索引(F列) + private static final int A_MERGE_ROW_REDUCE = 1; // A列合并行数减少1行(修复多合并问题) // 固定文本常量 private static final String SHEET_PREFIX = "月资源统计分析表"; @@ -35,10 +41,13 @@ public class ResourceSummaryExcelExporter { private static final String SUMMARY_TEXT = "汇总"; private static final String RESOURCE_STAT_TITLE = "资源统计分析"; private static final String LONG_TERM_SUPPORT_REMARK = "长期支援不用统计"; + private static final String TOTAL_WORKDAY_TEXT = "总工日"; + private static final String UNARRANGE_TEXT = "未安排"; + private static final String SUBCONTRACT_TEXT = "分包用工"; + private static final String WORKDAY_TEXT = "工日"; // 修正后的C列子表头 // 列宽配置(单位:字符,POI中1字符≈256单位) - // 核心调整:C列从12→20,D列从10→15,保证表头文字完整显示 - private static final int[] COLUMN_WIDTHS = {20, 15, 20, 15, 10, 18}; // A-F列宽(调整后) + private static final int[] COLUMN_WIDTHS = {20, 15, 20, 15, 10, 18}; // A-F列宽 // 字体配置 private static final String FONT_NAME = "微软雅黑"; private static final short FONT_SIZE = 11; @@ -107,7 +116,7 @@ public class ResourceSummaryExcelExporter { Workbook workbook = new XSSFWorkbook(); Sheet sheet = workbook.createSheet(sheetName); initStyles(workbook); // 初始化样式(含不同数值格式) - setColumnWidths(sheet); // 设置列宽(已调整C/D列) + setColumnWidths(sheet); // 设置列宽 // 写入数据 int currentRow = 0; @@ -140,7 +149,7 @@ public class ResourceSummaryExcelExporter { } /** - * 写入单个运检站/汇总的数据 + * 写入单个运检站/汇总的数据(适配动态类型) * @param sheet 工作表 * @param vo 数据VO * @param name 运检站/汇总名称 @@ -149,10 +158,17 @@ public class ResourceSummaryExcelExporter { */ private int writeStationData(Sheet sheet, ResourceSummaryVo vo, String name, int startRow) { if (vo == null) { - return startRow + STATION_ROW_TOTAL; + // 计算默认行数(基础行数+至少1行动态类型) + return startRow + STATION_ROW_BASE + 1; } int rowIdx = startRow; + // 获取动态业务类型列表 + List dynamicTypeList = vo.getBusinessTypeList(); + // 单个运检站总行数 = 基础行数 + 动态类型行数 + int stationTotalRows = STATION_ROW_BASE + (dynamicTypeList == null ? 0 : dynamicTypeList.size()); + // A列合并结束行偏移(修复多合并1行问题) + int aMergeEndOffset = A_MERGE_START + (stationTotalRows - 1) - A_MERGE_ROW_REDUCE; // 1. 标题行(第1行):合并A-F列,内容"资源统计分析" Row titleRow = createRow(sheet, rowIdx++); @@ -170,10 +186,10 @@ public class ResourceSummaryExcelExporter { setCellValueAndStyle(personHeaderRow, 2, "长期借调、支援人数", headerStyle); // D列:实际在站人数(表头样式) setCellValueAndStyle(personHeaderRow, 3, "实际在站人数", headerStyle); - // A/E/F列:空(A列样式兜底,F列内容样式) + // A/E/F列:空(填充样式保证边框) setCellValueAndStyle(personHeaderRow, 0, "", headerStyle); setCellValueAndStyle(personHeaderRow, 4, "", headerStyle); - setCellValueAndStyle(personHeaderRow, REMARK_COL_INDEX, "", contentStyle); + setCellValueAndStyle(personHeaderRow, REMARK_COL_INDEX, "", headerStyle); // 3. 人员数据行(第3行):B/C/D列填充对应数据 Row personDataRow = createRow(sheet, rowIdx++); @@ -186,87 +202,105 @@ public class ResourceSummaryExcelExporter { // D列:实际在站人数(actualStationNum) int actualStationNum = vo.getActualStationNum() == null ? 0 : vo.getActualStationNum(); setCellValueAndStyle(personDataRow, 3, actualStationNum, contentStyle); - // A/E/F列:空 + // A/E/F列:空(填充样式保证边框) setCellValueAndStyle(personDataRow, 0, "", contentStyle); setCellValueAndStyle(personDataRow, 4, "", contentStyle); setCellValueAndStyle(personDataRow, REMARK_COL_INDEX, "", contentStyle); - // 4. 子表头行(第4行):类型/总工日/比值/人均/备注 + // 4. 子表头行(第4行):类型/工日/比值/人均/备注(修复C列表头) Row subHeaderRow = createRow(sheet, rowIdx++); + setCellValueAndStyle(subHeaderRow, 0, "", headerStyle); // A列空(填充样式) setCellValueAndStyle(subHeaderRow, 1, "类型", headerStyle); - setCellValueAndStyle(subHeaderRow, 2, "总工日", headerStyle); + setCellValueAndStyle(subHeaderRow, 2, WORKDAY_TEXT, headerStyle); // 修正为“工日” setCellValueAndStyle(subHeaderRow, 3, "比值", headerStyle); setCellValueAndStyle(subHeaderRow, 4, "人均", headerStyle); setCellValueAndStyle(subHeaderRow, REMARK_COL_INDEX, "备注", headerStyle); - fillRowStyle(subHeaderRow, headerStyle); + fillRowStyle(subHeaderRow, headerStyle); // 强制填充整行样式 - // 5. 总工日行(第5行):C列填充totalWorkday + // 5. 总工日行(第5行):B列清空,C列填充totalWorkday(修复点) Row totalWorkdayRow = createRow(sheet, rowIdx++); - // A/B/D/E/F列:空 + // A列:空(填充样式) setCellValueAndStyle(totalWorkdayRow, 0, "", contentStyle); + // B列:清空(移除原“总工日”文本) setCellValueAndStyle(totalWorkdayRow, 1, "", contentStyle); - setCellValueAndStyle(totalWorkdayRow, 3, "", contentStyle); - setCellValueAndStyle(totalWorkdayRow, 4, "", contentStyle); - setCellValueAndStyle(totalWorkdayRow, REMARK_COL_INDEX, "", contentStyle); // C列:总工日数据(totalWorkday) int totalWorkday = vo.getTotalWorkday() == null ? 0 : vo.getTotalWorkday(); setCellValueAndStyle(totalWorkdayRow, 2, totalWorkday, workdayStyle); + // D/E/F列:空(填充样式) + setCellValueAndStyle(totalWorkdayRow, 3, "", contentStyle); + setCellValueAndStyle(totalWorkdayRow, 4, "", contentStyle); + setCellValueAndStyle(totalWorkdayRow, REMARK_COL_INDEX, "", contentStyle); - // 6. 类型行(第6-13行:8类数据) - String[] types = {"休假", "培训", "运行", "运行(视频)", "检修", "值班", "其他", "未安排"}; - Integer[] workdays = { - vo.getRestWorkday(), vo.getTrainWorkday(), vo.getRunWorkday(), vo.getRunVideoWorkday(), - vo.getMaintainWorkday(), vo.getDutyWorkday(), vo.getOtherWorkday(), vo.getUnarrangeWorkday() - }; - BigDecimal[] ratios = { - vo.getRestRatio(), vo.getTrainRatio(), vo.getRunRatio(), vo.getRunVideoRatio(), - vo.getMaintainRatio(), vo.getDutyRatio(), vo.getOtherRatio(), vo.getUnarrangeRatio() - }; - BigDecimal[] avgs = { - vo.getRestAvg(), vo.getTrainAvg(), vo.getRunAvg(), vo.getRunVideoAvg(), - vo.getMaintainAvg(), vo.getDutyAvg(), vo.getOtherAvg(), vo.getUnarrangeAvg() - }; - - for (int i = 0; i < types.length; i++) { - Row typeRow = createRow(sheet, rowIdx++); - // B列:类型名称 - setCellValueAndStyle(typeRow, 1, types[i], contentStyle); - // C列:总工日(整数格式) - int workday = workdays[i] == null ? 0 : workdays[i]; - setCellValueAndStyle(typeRow, 2, workday, workdayStyle); - // D列:比值(保留2位小数) - BigDecimal ratio = ratios[i] == null ? BigDecimal.ZERO : ratios[i].setScale(2, BigDecimal.ROUND_HALF_UP); - setCellValueAndStyle(typeRow, 3, ratio, ratioStyle); - // E列:人均(保留1位小数) - BigDecimal avg = avgs[i] == null ? BigDecimal.ZERO : avgs[i].setScale(1, BigDecimal.ROUND_HALF_UP); - setCellValueAndStyle(typeRow, 4, avg, avgStyle); - // F列:备注(仅"其他"行显示) - if (i == 6) { // "其他"行 - setCellValueAndStyle(typeRow, REMARK_COL_INDEX, LONG_TERM_SUPPORT_REMARK, contentStyle); - } else { - setCellValueAndStyle(typeRow, REMARK_COL_INDEX, "", contentStyle); + // 6. 动态类型行(替换原固定类型行) + if (dynamicTypeList != null && !dynamicTypeList.isEmpty()) { + for (BusinessTypeVo typeVo : dynamicTypeList) { + Row typeRow = createRow(sheet, rowIdx++); + // 跳过未安排(单独处理) + if (UNARRANGE_TEXT.equals(typeVo.getTypeName())) { + continue; + } + // A列:空(填充样式) + setCellValueAndStyle(typeRow, 0, "", contentStyle); + // B列:类型名称 + String typeName = Optional.ofNullable(typeVo.getTypeName()).orElse(""); + setCellValueAndStyle(typeRow, 1, typeName, contentStyle); + // C列:总工日(整数格式) + int workday = typeVo.getWorkday() == null ? 0 : typeVo.getWorkday(); + setCellValueAndStyle(typeRow, 2, workday, workdayStyle); + // D列:比值(保留2位小数) + BigDecimal ratio = Optional.ofNullable(typeVo.getRatio()).orElse(BigDecimal.ZERO) + .setScale(2, BigDecimal.ROUND_HALF_UP); + setCellValueAndStyle(typeRow, 3, ratio, ratioStyle); + // E列:人均(保留1位小数) + BigDecimal avg = Optional.ofNullable(typeVo.getAvg()).orElse(BigDecimal.ZERO) + .setScale(1, BigDecimal.ROUND_HALF_UP); + setCellValueAndStyle(typeRow, 4, avg, avgStyle); + // F列:备注("其他"类型显示备注) + if (LONG_TERM_SUPPORT_REMARK.contains(typeName) || "其他".equals(typeName)) { + setCellValueAndStyle(typeRow, REMARK_COL_INDEX, LONG_TERM_SUPPORT_REMARK, contentStyle); + } else { + setCellValueAndStyle(typeRow, REMARK_COL_INDEX, "", contentStyle); + } } - // A列:空 - setCellValueAndStyle(typeRow, 0, "", contentStyle); } - // 7. 分包用工行(第14行):C列填充subcontractWorkday,字体不加粗 + // 7. 未安排行(固定行,单独处理) + Row unarrangeRow = createRow(sheet, rowIdx++); + // A列:空(填充样式) + setCellValueAndStyle(unarrangeRow, 0, "", contentStyle); + // B列:未安排 + setCellValueAndStyle(unarrangeRow, 1, UNARRANGE_TEXT, contentStyle); + // C列:未安排工日 + int unarrangeWorkday = vo.getUnarrangeWorkday() == null ? 0 : vo.getUnarrangeWorkday(); + setCellValueAndStyle(unarrangeRow, 2, unarrangeWorkday, workdayStyle); + // D列:未安排比值 + BigDecimal unarrangeRatio = Optional.ofNullable(vo.getUnarrangeRatio()).orElse(BigDecimal.ZERO) + .setScale(2, BigDecimal.ROUND_HALF_UP); + setCellValueAndStyle(unarrangeRow, 3, unarrangeRatio, ratioStyle); + // E列:未安排人均 + BigDecimal unarrangeAvg = Optional.ofNullable(vo.getUnarrangeAvg()).orElse(BigDecimal.ZERO) + .setScale(1, BigDecimal.ROUND_HALF_UP); + setCellValueAndStyle(unarrangeRow, 4, unarrangeAvg, avgStyle); + // F列:空备注(填充样式) + setCellValueAndStyle(unarrangeRow, REMARK_COL_INDEX, "", contentStyle); + + // 8. 分包用工行(固定行) Row subcontractRow = createRow(sheet, rowIdx++); - // A列:空 + // A列:空(填充样式) setCellValueAndStyle(subcontractRow, 0, "", contentStyle); - // B列:分包用工(内容样式,不加粗) - setCellValueAndStyle(subcontractRow, 1, "分包用工", contentStyle); + // B列:分包用工 + setCellValueAndStyle(subcontractRow, 1, SUBCONTRACT_TEXT, contentStyle); // C列:分包用工总工日(subcontractWorkday) int subWorkday = vo.getSubcontractWorkday() == null ? 0 : vo.getSubcontractWorkday(); setCellValueAndStyle(subcontractRow, 2, subWorkday, workdayStyle); - // D/E/F列:空 + // D/E/F列:空(填充样式) setCellValueAndStyle(subcontractRow, 3, "", contentStyle); setCellValueAndStyle(subcontractRow, 4, "", contentStyle); setCellValueAndStyle(subcontractRow, REMARK_COL_INDEX, "", contentStyle); - // A列合并(第4-14行)并填充运检站名称 + // A列合并(修复多合并1行问题)并填充运检站名称 int aMergeFirstRow = startRow + A_MERGE_START; - int aMergeLastRow = startRow + A_MERGE_END; + int aMergeLastRow = startRow + aMergeEndOffset; sheet.addMergedRegion(new CellRangeAddress(aMergeFirstRow, aMergeLastRow, 0, 0)); Row aMergeRow = sheet.getRow(aMergeFirstRow); Cell aMergeCell = createCell(aMergeRow, 0); @@ -301,21 +335,21 @@ public class ResourceSummaryExcelExporter { // 基础内容样式:普通+居中+全边框 contentStyle = createBaseCellStyle(workbook, normalFont); - // 工日列样式:整数格式 + // 工日列样式:整数格式 + 全边框 workdayStyle = createBaseCellStyle(workbook, normalFont); workdayStyle.setDataFormat(workbook.createDataFormat().getFormat("0")); - // 比值列样式:保留2位小数 + // 比值列样式:保留2位小数 + 全边框 ratioStyle = createBaseCellStyle(workbook, normalFont); ratioStyle.setDataFormat(workbook.createDataFormat().getFormat("0.00")); - // 人均列样式:保留1位小数 + // 人均列样式:保留1位小数 + 全边框 avgStyle = createBaseCellStyle(workbook, normalFont); avgStyle.setDataFormat(workbook.createDataFormat().getFormat("0.0")); } /** - * 创建基础单元格样式 + * 创建基础单元格样式(保证全边框) */ private CellStyle createBaseCellStyle(Workbook workbook, Font font) { CellStyle style = workbook.createCellStyle(); @@ -324,7 +358,7 @@ public class ResourceSummaryExcelExporter { // 对齐方式:水平+垂直居中 style.setAlignment(HorizontalAlignment.CENTER); style.setVerticalAlignment(VerticalAlignment.CENTER); - // 边框:全边框+黑色 + // 边框:全边框+黑色(确保边框完整) style.setBorderTop(BorderStyle.THIN); style.setBorderBottom(BorderStyle.THIN); style.setBorderLeft(BorderStyle.THIN); @@ -339,7 +373,7 @@ public class ResourceSummaryExcelExporter { } /** - * 设置列宽(核心调整:C/D列加宽) + * 设置列宽 */ private void setColumnWidths(Sheet sheet) { for (int i = 0; i < COLUMN_WIDTHS.length; i++) { @@ -376,7 +410,7 @@ public class ResourceSummaryExcelExporter { if (row == null) { row = sheet.createRow(rowIdx); } - // 设置行高 + // 设置行高(保证单元格显示完整) row.setHeightInPoints(20); return row; } diff --git a/bonus-business/src/main/java/com/bonus/digital/dao/BusinessTypeVo.java b/bonus-business/src/main/java/com/bonus/digital/dao/BusinessTypeVo.java new file mode 100644 index 0000000..d46cb12 --- /dev/null +++ b/bonus-business/src/main/java/com/bonus/digital/dao/BusinessTypeVo.java @@ -0,0 +1,44 @@ +package com.bonus.digital.dao; + +import lombok.Data; +import java.math.BigDecimal; + +/** + * 动态业务类型Vo(存储每个运检站实际存在的业务类型数据) + */ +@Data +public class BusinessTypeVo { + // 类型名称(如:休假、培训、运行(视频)等) + private String typeName; + // 总天数 + private Integer totalDays; + // 参与人数 + private Integer personCount; + // 工日 + private Integer workday; + // 比值(workday/totalWorkday) + private BigDecimal ratio; + // 人均(workday/actualStationNum) + private BigDecimal avg; + // 以下两个字段用于计算比值和人均,无需前端返回,仅作中间计算 + private Integer totalWorkday; + private Integer actualStationNum; + + /** + * 计算比值和人均(初始化时自动计算) + */ + public void calculateRatioAndAvg() { + // 计算比值(保留4位小数,分母为0时返回0) + this.ratio = BigDecimal.ZERO; + if (this.totalWorkday != null && this.totalWorkday > 0 && this.workday != null) { + this.ratio = new BigDecimal(this.workday) + .divide(new BigDecimal(this.totalWorkday), 4, BigDecimal.ROUND_HALF_UP); + } + // 计算人均(保留3位小数,分母为0时返回0) + this.avg = BigDecimal.ZERO; + if (this.actualStationNum != null && this.actualStationNum > 0 && this.workday != null) { + this.avg = new BigDecimal(this.workday) + .divide(new BigDecimal(this.actualStationNum), 3, BigDecimal.ROUND_HALF_UP); + } + } +} diff --git a/bonus-business/src/main/java/com/bonus/digital/dao/ResourceSummaryExcelVo.java b/bonus-business/src/main/java/com/bonus/digital/dao/ResourceSummaryExcelVo.java index d5e97cb..52f9d0e 100644 --- a/bonus-business/src/main/java/com/bonus/digital/dao/ResourceSummaryExcelVo.java +++ b/bonus-business/src/main/java/com/bonus/digital/dao/ResourceSummaryExcelVo.java @@ -17,32 +17,32 @@ public class ResourceSummaryExcelVo { @ColumnWidth(15) private String inspectionStationName; - // B列:编制人数/类型(二选一,用ignore避免重复映射) + // B列:编制人数/类型 @ExcelProperty(value = "编制人数/类型", index = 1) @ColumnWidth(18) private Integer compileNum; @ExcelIgnore // 关键:标记为忽略,避免EasyExcel分配到G列 private String type; - // C列:长期借调、支援人数/工日(二选一) + // C列:长期借调、支援人数/工日 @ExcelProperty(value = "长期借调、支援人数/工日", index = 2) @ColumnWidth(18) private Integer secondmentNum; @ExcelIgnore // 关键:标记为忽略 private Integer workday; - // D列:实际在站人数/比值(二选一) + // D列:实际在站人数/比值 @ExcelProperty(value = "实际在站人数/比值", index = 3) @ColumnWidth(12) private Integer actualStationNum; @ExcelIgnore // 关键:标记为忽略 private BigDecimal ratio; - // E列:人均/分包用工(二选一) + // E列:人均/分包用工 @ExcelProperty(value = "人均/分包用工", index = 4) @ColumnWidth(12) private BigDecimal avg; - @ExcelIgnore // 关键:标记为忽略 + @ExcelIgnore // 标记为忽略 private Integer subcontractWorkday; // F列:备注 diff --git a/bonus-business/src/main/java/com/bonus/digital/dao/ResourceSummaryVo.java b/bonus-business/src/main/java/com/bonus/digital/dao/ResourceSummaryVo.java index 3470b2f..4a70172 100644 --- a/bonus-business/src/main/java/com/bonus/digital/dao/ResourceSummaryVo.java +++ b/bonus-business/src/main/java/com/bonus/digital/dao/ResourceSummaryVo.java @@ -2,6 +2,8 @@ package com.bonus.digital.dao; import lombok.Data; import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; @Data public class ResourceSummaryVo { @@ -15,74 +17,53 @@ public class ResourceSummaryVo { private Integer actualStationNum; // 实际在站人数 private Integer totalWorkday; // 总工日 - // 休假 - private Integer restTotalDays; // 休假总天数 - private Integer restPersonCount; // 休假参与人数 - private Integer restWorkday; - private BigDecimal restRatio; - private BigDecimal restAvg; - - // 培训 - private Integer trainTotalDays; - private Integer trainPersonCount; - private Integer trainWorkday; - private BigDecimal trainRatio; - private BigDecimal trainAvg; - - // 运行 - private Integer runTotalDays; - private Integer runPersonCount; - private Integer runWorkday; - private BigDecimal runRatio; - private BigDecimal runAvg; - - // 运行(视频) - private Integer runVideoTotalDays; - private Integer runVideoPersonCount; - private Integer runVideoWorkday; - private BigDecimal runVideoRatio; - private BigDecimal runVideoAvg; - - // 检修 - private Integer maintainTotalDays; - private Integer maintainPersonCount; - private Integer maintainWorkday; - private BigDecimal maintainRatio; - private BigDecimal maintainAvg; - - // 值班 - private Integer dutyTotalDays; - private Integer dutyPersonCount; - private Integer dutyWorkday; - private BigDecimal dutyRatio; - private BigDecimal dutyAvg; - - // 抢修 - private Integer repairTotalDays; - private Integer repairPersonCount; - private Integer repairWorkday; - private BigDecimal repairRatio; - private BigDecimal repairAvg; - - // 学习 - private Integer studyTotalDays; - private Integer studyPersonCount; - private Integer studyWorkday; - private BigDecimal studyRatio; - private BigDecimal studyAvg; - - // 其他 - private Integer otherTotalDays; - private Integer otherPersonCount; - private Integer otherWorkday; - private BigDecimal otherRatio; - private BigDecimal otherAvg; - - // 未安排 + // 未安排(固定展示) private Integer unarrangeWorkday; private BigDecimal unarrangeRatio; private BigDecimal unarrangeAvg; - // 分包用工 + // 分包用工(固定展示) private Integer subcontractWorkday; + + // 动态业务类型列表(替换原type_json为type_str,存储拼接字符串) + private String typeStr; // 数据库返回的拼接字符串,格式:类型1|类型2|... + private transient List businessTypeList; // 转换后的类型列表,不持久化 + + /** + * 获取动态业务类型列表(自动解析拼接字符串) + * @return 业务类型列表 + */ + public List getBusinessTypeList() { + if (businessTypeList == null) { + businessTypeList = new ArrayList<>(); + // 解析拼接字符串 + if (typeStr != null && !typeStr.isEmpty()) { + // 按|分割多个类型 + String[] typeArray = typeStr.split("\\|"); + for (String singleTypeStr : typeArray) { + if (singleTypeStr.isEmpty()) { + continue; + } + // 按,分割单个类型的字段 + String[] fieldArray = singleTypeStr.split(","); + // 字段顺序:typeName,totalDays,personCount,workday,totalWorkday,actualStationNum + if (fieldArray.length != 6) { + continue; // 字段异常,跳过该类型 + } + BusinessTypeVo typeVo = new BusinessTypeVo(); + // 赋值字段(注意类型转换) + typeVo.setTypeName(fieldArray[0].replace(",", ",")); // 还原转义的逗号 + typeVo.setTotalDays(Integer.parseInt(fieldArray[1])); + typeVo.setPersonCount(Integer.parseInt(fieldArray[2])); + typeVo.setWorkday(Integer.parseInt(fieldArray[3])); + typeVo.setTotalWorkday(Integer.parseInt(fieldArray[4])); + typeVo.setActualStationNum(Integer.parseInt(fieldArray[5])); + // 计算比值和人均 + typeVo.calculateRatioAndAvg(); + businessTypeList.add(typeVo); + } + } + } + return businessTypeList; + } } diff --git a/bonus-business/src/main/java/com/bonus/digital/service/impl/MonthlyPlanServiceImpl.java b/bonus-business/src/main/java/com/bonus/digital/service/impl/MonthlyPlanServiceImpl.java index f54aada..03478ab 100644 --- a/bonus-business/src/main/java/com/bonus/digital/service/impl/MonthlyPlanServiceImpl.java +++ b/bonus-business/src/main/java/com/bonus/digital/service/impl/MonthlyPlanServiceImpl.java @@ -15,6 +15,7 @@ import javax.annotation.Resource; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.*; +import java.util.stream.Collectors; /** * @author 马三炮 @@ -202,108 +203,175 @@ public class MonthlyPlanServiceImpl implements MonthlyPlanService { return exportList; } - /** - * 导出资源统计汇总数据(适配Excel导出,含各运检站+汇总行) - * @param monthlyPlanVo 筛选条件(核心:monthlyPlan 统计月份) - * @return 资源统计Vo列表(最后一条为汇总数据) - */ @Override public List exportResourceSummary(MonthlyPlanVo monthlyPlanVo) { - // 1. 入参:用MonthlyPlanVo传递筛选条件,调用Mapper查询ResourceSummaryVo(统计结果) - // (Mapper中会从MonthlyPlanVo取monthlyPlan/inspectionStationId等筛选值) + // 1. 入参校验与查询数据 + if (monthlyPlanVo == null || monthlyPlanVo.getMonthlyPlan() == null) { + log.warn("资源统计查询入参无效:MonthlyPlanVo或统计月份为空"); + return Collections.emptyList(); + } + List stationResourceList = monthlyPlanMapper.getResourceSummary(monthlyPlanVo); - if (stationResourceList.isEmpty()) { + if (stationResourceList == null || stationResourceList.isEmpty()) { log.info("资源统计查询无数据,筛选条件:月份={},运检站ID={}", monthlyPlanVo.getMonthlyPlan(), monthlyPlanVo.getInspectionStationId()); return Collections.emptyList(); } + log.info("资源统计查询成功,运检站原始数据量:{}", stationResourceList.size()); - // 2. 计算汇总行(仅用ResourceSummaryVo,全程不混用) - ResourceSummaryVo summaryResourceVo = new ResourceSummaryVo(); - summaryResourceVo.setInspectionStationName("汇总"); // 汇总行名称 + // 2. 数据去重(按运检站名称去重,避免重复数据) + List distinctStationList = stationResourceList.stream() + .filter(Objects::nonNull) + .filter(vo -> Objects.nonNull(vo.getInspectionStationName())) + .collect(Collectors.collectingAndThen( + Collectors.toMap( + ResourceSummaryVo::getInspectionStationName, + vo -> vo, + (existing, newVo) -> { + log.warn("运检站名称重复,保留第一条数据:{}", existing.getInspectionStationName()); + return existing; + } + ), + map -> new ArrayList<>(map.values()) + )); + log.info("运检站数据去重后数量:{}", distinctStationList.size()); - // 2.1 人员基础数据累加(纯ResourceSummaryVo字段) - summaryResourceVo.setCompileNum(stationResourceList.stream().mapToInt(ResourceSummaryVo::getCompileNum).sum()); - summaryResourceVo.setSecondmentNum(stationResourceList.stream().mapToInt(ResourceSummaryVo::getSecondmentNum).sum()); - summaryResourceVo.setActualStationNum(stationResourceList.stream().mapToInt(ResourceSummaryVo::getActualStationNum).sum()); - summaryResourceVo.setTotalWorkday(stationResourceList.stream().mapToInt(ResourceSummaryVo::getTotalWorkday).sum()); + // 3. 构建汇总行数据 + ResourceSummaryVo summaryResourceVo = buildSummaryResourceVo(distinctStationList); - // 2.2 各类型中间字段累加(TotalDays/PersonCount,纯ResourceSummaryVo) - // 休假 - summaryResourceVo.setRestTotalDays(stationResourceList.stream().mapToInt(ResourceSummaryVo::getRestTotalDays).sum()); - summaryResourceVo.setRestPersonCount(stationResourceList.stream().mapToInt(ResourceSummaryVo::getRestPersonCount).sum()); - summaryResourceVo.setRestWorkday(stationResourceList.stream().mapToInt(ResourceSummaryVo::getRestWorkday).sum()); - // 培训 - summaryResourceVo.setTrainTotalDays(stationResourceList.stream().mapToInt(ResourceSummaryVo::getTrainTotalDays).sum()); - summaryResourceVo.setTrainPersonCount(stationResourceList.stream().mapToInt(ResourceSummaryVo::getTrainPersonCount).sum()); - summaryResourceVo.setTrainWorkday(stationResourceList.stream().mapToInt(ResourceSummaryVo::getTrainWorkday).sum()); - // 运行 - summaryResourceVo.setRunTotalDays(stationResourceList.stream().mapToInt(ResourceSummaryVo::getRunTotalDays).sum()); - summaryResourceVo.setRunPersonCount(stationResourceList.stream().mapToInt(ResourceSummaryVo::getRunPersonCount).sum()); - summaryResourceVo.setRunWorkday(stationResourceList.stream().mapToInt(ResourceSummaryVo::getRunWorkday).sum()); - // 运行(视频) - summaryResourceVo.setRunVideoTotalDays(stationResourceList.stream().mapToInt(ResourceSummaryVo::getRunVideoTotalDays).sum()); - summaryResourceVo.setRunVideoPersonCount(stationResourceList.stream().mapToInt(ResourceSummaryVo::getRunVideoPersonCount).sum()); - summaryResourceVo.setRunVideoWorkday(stationResourceList.stream().mapToInt(ResourceSummaryVo::getRunVideoWorkday).sum()); - // 检修(原维护) - summaryResourceVo.setMaintainTotalDays(stationResourceList.stream().mapToInt(ResourceSummaryVo::getMaintainTotalDays).sum()); - summaryResourceVo.setMaintainPersonCount(stationResourceList.stream().mapToInt(ResourceSummaryVo::getMaintainPersonCount).sum()); - summaryResourceVo.setMaintainWorkday(stationResourceList.stream().mapToInt(ResourceSummaryVo::getMaintainWorkday).sum()); - // 值班 - summaryResourceVo.setDutyTotalDays(stationResourceList.stream().mapToInt(ResourceSummaryVo::getDutyTotalDays).sum()); - summaryResourceVo.setDutyPersonCount(stationResourceList.stream().mapToInt(ResourceSummaryVo::getDutyPersonCount).sum()); - summaryResourceVo.setDutyWorkday(stationResourceList.stream().mapToInt(ResourceSummaryVo::getDutyWorkday).sum()); - // 抢修 - summaryResourceVo.setRepairTotalDays(stationResourceList.stream().mapToInt(ResourceSummaryVo::getRepairTotalDays).sum()); - summaryResourceVo.setRepairPersonCount(stationResourceList.stream().mapToInt(ResourceSummaryVo::getRepairPersonCount).sum()); - summaryResourceVo.setRepairWorkday(stationResourceList.stream().mapToInt(ResourceSummaryVo::getRepairWorkday).sum()); - // 学习 - summaryResourceVo.setStudyTotalDays(stationResourceList.stream().mapToInt(ResourceSummaryVo::getStudyTotalDays).sum()); - summaryResourceVo.setStudyPersonCount(stationResourceList.stream().mapToInt(ResourceSummaryVo::getStudyPersonCount).sum()); - summaryResourceVo.setStudyWorkday(stationResourceList.stream().mapToInt(ResourceSummaryVo::getStudyWorkday).sum()); - // 其他 - summaryResourceVo.setOtherTotalDays(stationResourceList.stream().mapToInt(ResourceSummaryVo::getOtherTotalDays).sum()); - summaryResourceVo.setOtherPersonCount(stationResourceList.stream().mapToInt(ResourceSummaryVo::getOtherPersonCount).sum()); - summaryResourceVo.setOtherWorkday(stationResourceList.stream().mapToInt(ResourceSummaryVo::getOtherWorkday).sum()); - // 分包用工 - summaryResourceVo.setSubcontractWorkday(stationResourceList.stream().mapToInt(ResourceSummaryVo::getSubcontractWorkday).sum()); - - // 2.3 未安排工日计算(纯ResourceSummaryVo字段) - int unarrangeWorkday = summaryResourceVo.getTotalWorkday() - - (summaryResourceVo.getRestWorkday() + summaryResourceVo.getTrainWorkday() + summaryResourceVo.getRunWorkday() - + summaryResourceVo.getRunVideoWorkday() + summaryResourceVo.getMaintainWorkday() + summaryResourceVo.getDutyWorkday() - + summaryResourceVo.getRepairWorkday() + summaryResourceVo.getStudyWorkday()); - summaryResourceVo.setUnarrangeWorkday(unarrangeWorkday); - - // 2.4 比值/人均计算(纯ResourceSummaryVo字段,复用通用方法) - summaryResourceVo.setRestRatio(calcRatio(summaryResourceVo.getRestWorkday(), summaryResourceVo.getTotalWorkday())); - summaryResourceVo.setRestAvg(calcAvg(summaryResourceVo.getRestWorkday(), summaryResourceVo.getActualStationNum())); - summaryResourceVo.setTrainRatio(calcRatio(summaryResourceVo.getTrainWorkday(), summaryResourceVo.getTotalWorkday())); - summaryResourceVo.setTrainAvg(calcAvg(summaryResourceVo.getTrainWorkday(), summaryResourceVo.getActualStationNum())); - summaryResourceVo.setRunRatio(calcRatio(summaryResourceVo.getRunWorkday(), summaryResourceVo.getTotalWorkday())); - summaryResourceVo.setRunAvg(calcAvg(summaryResourceVo.getRunWorkday(), summaryResourceVo.getActualStationNum())); - summaryResourceVo.setRunVideoRatio(calcRatio(summaryResourceVo.getRunVideoWorkday(), summaryResourceVo.getTotalWorkday())); - summaryResourceVo.setRunVideoAvg(calcAvg(summaryResourceVo.getRunVideoWorkday(), summaryResourceVo.getActualStationNum())); - summaryResourceVo.setMaintainRatio(calcRatio(summaryResourceVo.getMaintainWorkday(), summaryResourceVo.getTotalWorkday())); - summaryResourceVo.setMaintainAvg(calcAvg(summaryResourceVo.getMaintainWorkday(), summaryResourceVo.getActualStationNum())); - summaryResourceVo.setDutyRatio(calcRatio(summaryResourceVo.getDutyWorkday(), summaryResourceVo.getTotalWorkday())); - summaryResourceVo.setDutyAvg(calcAvg(summaryResourceVo.getDutyWorkday(), summaryResourceVo.getActualStationNum())); - summaryResourceVo.setRepairRatio(calcRatio(summaryResourceVo.getRepairWorkday(), summaryResourceVo.getTotalWorkday())); - summaryResourceVo.setRepairAvg(calcAvg(summaryResourceVo.getRepairWorkday(), summaryResourceVo.getActualStationNum())); - summaryResourceVo.setStudyRatio(calcRatio(summaryResourceVo.getStudyWorkday(), summaryResourceVo.getTotalWorkday())); - summaryResourceVo.setStudyAvg(calcAvg(summaryResourceVo.getStudyWorkday(), summaryResourceVo.getActualStationNum())); - summaryResourceVo.setOtherRatio(calcRatio(summaryResourceVo.getOtherWorkday(), summaryResourceVo.getTotalWorkday())); - summaryResourceVo.setOtherAvg(calcAvg(summaryResourceVo.getOtherWorkday(), summaryResourceVo.getActualStationNum())); - summaryResourceVo.setUnarrangeRatio(calcRatio(summaryResourceVo.getUnarrangeWorkday(), summaryResourceVo.getTotalWorkday())); - summaryResourceVo.setUnarrangeAvg(calcAvg(summaryResourceVo.getUnarrangeWorkday(), summaryResourceVo.getActualStationNum())); - - // 3. 汇总行加入结果列表(纯ResourceSummaryVo列表) - List finalResourceList = new ArrayList<>(stationResourceList); + // 4. 组装最终结果列表(运检站数据+汇总数据) + List finalResourceList = new ArrayList<>(distinctStationList); finalResourceList.add(summaryResourceVo); + log.info("资源统计汇总完成,最终数据量:{}(运检站{}个+汇总1条)", + finalResourceList.size(), distinctStationList.size()); return finalResourceList; } + /** + * 构建汇总行数据(独立方法,降低耦合) + * @param distinctStationList 去重后的运检站数据列表 + * @return 汇总行ResourceSummaryVo + */ + private ResourceSummaryVo buildSummaryResourceVo(List distinctStationList) { + ResourceSummaryVo summaryVo = new ResourceSummaryVo(); + summaryVo.setInspectionStationName("汇总"); + + // 3.1 人员基础数据累加 + int totalCompileNum = distinctStationList.stream() + .mapToInt(vo -> Optional.ofNullable(vo.getCompileNum()).orElse(0)) + .sum(); + int totalSecondmentNum = distinctStationList.stream() + .mapToInt(vo -> Optional.ofNullable(vo.getSecondmentNum()).orElse(0)) + .sum(); + int totalActualStationNum = distinctStationList.stream() + .mapToInt(vo -> Optional.ofNullable(vo.getActualStationNum()).orElse(0)) + .sum(); + int totalWorkday = distinctStationList.stream() + .mapToInt(vo -> Optional.ofNullable(vo.getTotalWorkday()).orElse(0)) + .sum(); + + summaryVo.setCompileNum(totalCompileNum); + summaryVo.setSecondmentNum(totalSecondmentNum); + summaryVo.setActualStationNum(totalActualStationNum); + summaryVo.setTotalWorkday(totalWorkday); + // 3.2 分包用工累加 + int totalSubcontractWorkday = distinctStationList.stream() + .mapToInt(vo -> Optional.ofNullable(vo.getSubcontractWorkday()).orElse(0)) + .sum(); + summaryVo.setSubcontractWorkday(totalSubcontractWorkday); + + // 3.3 动态业务类型汇总(按类型名称分组累加) + Map summaryTypeMap = aggregateDynamicTypeData(distinctStationList); + List summaryTypeList = new ArrayList<>(summaryTypeMap.values()); + + // 3.4 处理汇总类型的比值与人均计算 + summaryTypeList.forEach(typeVo -> { + typeVo.setTotalWorkday(totalWorkday); + typeVo.setActualStationNum(totalActualStationNum); + typeVo.calculateRatioAndAvg(); + }); + + // 3.5 未安排工日计算(动态扣除所有有效类型工日,避免负数) + int totalDynamicWorkday = summaryTypeList.stream() + .mapToInt(typeVo -> Optional.ofNullable(typeVo.getWorkday()).orElse(0)) + .sum(); + int unarrangeWorkday = Math.max(totalWorkday - totalDynamicWorkday, 0); + summaryVo.setUnarrangeWorkday(unarrangeWorkday); + // 3.6 未安排字段比值/人均计算 + summaryVo.setUnarrangeRatio(calcRatio(unarrangeWorkday, totalWorkday)); + summaryVo.setUnarrangeAvg(calcAvg(unarrangeWorkday, totalActualStationNum)); + + String summaryTypeStr = buildDynamicTypeStr(summaryTypeList); + summaryVo.setTypeStr(summaryTypeStr); + + return summaryVo; + } + + /** + * 聚合所有运检站的动态类型数据(按类型名称分组) + * @param stationList 运检站数据列表 + * @return 分组后的汇总类型Map + */ + private Map aggregateDynamicTypeData(List stationList) { + return stationList.stream() + .filter(Objects::nonNull) + .map(ResourceSummaryVo::getBusinessTypeList) // 获取每个运检站的动态类型列表 + .filter(Objects::nonNull) + .flatMap(List::stream) // 扁平化所有类型 + .filter(typeVo -> Objects.nonNull(typeVo.getTypeName())) + .filter(typeVo -> !"未安排".equals(typeVo.getTypeName())) // 排除未安排(单独处理) + .collect(Collectors.toMap( + BusinessTypeVo::getTypeName, // 按类型名称分组 + typeVo -> { + // 新建汇总类型对象,避免修改原数据 + BusinessTypeVo newTypeVo = new BusinessTypeVo(); + newTypeVo.setTypeName(typeVo.getTypeName()); + newTypeVo.setTotalDays(Optional.ofNullable(typeVo.getTotalDays()).orElse(0)); + newTypeVo.setPersonCount(Optional.ofNullable(typeVo.getPersonCount()).orElse(0)); + newTypeVo.setWorkday(Optional.ofNullable(typeVo.getWorkday()).orElse(0)); + return newTypeVo; + }, + (existingType, newType) -> { + // 累加数据 + existingType.setTotalDays(existingType.getTotalDays() + Optional.ofNullable(newType.getTotalDays()).orElse(0)); + existingType.setPersonCount(existingType.getPersonCount() + Optional.ofNullable(newType.getPersonCount()).orElse(0)); + existingType.setWorkday(existingType.getWorkday() + Optional.ofNullable(newType.getWorkday()).orElse(0)); + return existingType; + } + )); + } + + /** + * 构建动态类型拼接字符串 + * @param typeList 动态类型列表 + * @return 拼接后的字符串(格式:类型1|类型2|...;单个类型:名称,总天数,人数,工日,总工日,在站人数) + */ + private String buildDynamicTypeStr(List typeList) { + if (typeList == null || typeList.isEmpty()) { + return ""; + } + + return typeList.stream() + .map(typeVo -> { + // 转义类型名称中的逗号,避免分隔符冲突 + String typeName = Optional.ofNullable(typeVo.getTypeName()).orElse("").replace(",", ","); + int totalDays = Optional.ofNullable(typeVo.getTotalDays()).orElse(0); + int personCount = Optional.ofNullable(typeVo.getPersonCount()).orElse(0); + int workday = Optional.ofNullable(typeVo.getWorkday()).orElse(0); + int totalWorkday = Optional.ofNullable(typeVo.getTotalWorkday()).orElse(0); + int actualStationNum = Optional.ofNullable(typeVo.getActualStationNum()).orElse(0); + return String.join(",", typeName, + String.valueOf(totalDays), + String.valueOf(personCount), + String.valueOf(workday), + String.valueOf(totalWorkday), + String.valueOf(actualStationNum)); + }) + .collect(Collectors.joining("|")); + } + + /** * 获取月计划详情 diff --git a/bonus-business/src/main/resources/mapper/MonthPlanMapper.xml b/bonus-business/src/main/resources/mapper/MonthPlanMapper.xml index 3c00248..714032d 100644 --- a/bonus-business/src/main/resources/mapper/MonthPlanMapper.xml +++ b/bonus-business/src/main/resources/mapper/MonthPlanMapper.xml @@ -196,105 +196,38 @@ +