diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/PerformanceExcelCellConstants.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/PerformanceExcelCellConstants.java index 96fbcbf..434ddfc 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/PerformanceExcelCellConstants.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/constant/PerformanceExcelCellConstants.java @@ -25,4 +25,11 @@ public final class PerformanceExcelCellConstants { "K6", "K7", "K8", "K9", "K10", "L6", "L7", "L8", "L9", "L10" ); + + public static final List MERGED_EXPORT_CELL_ADDRESSES = List.of( + "I6", "I7", "I8", "I9", + "J6", "J7", "J8", "J9", "J10", + "K6", "K7", "K8", "K9", "K10", + "L6", "L7", "L8", "L9", "L10" + ); } diff --git a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceSheetServiceImpl.java b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceSheetServiceImpl.java index 6e9660b..d096ede 100644 --- a/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceSheetServiceImpl.java +++ b/rdms-project/rdms-project-boot/src/main/java/com/njcn/rdms/module/project/service/performance/PerformanceSheetServiceImpl.java @@ -93,9 +93,8 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.zip.CRC32; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception; import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; @@ -105,7 +104,8 @@ import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil public class PerformanceSheetServiceImpl implements PerformanceSheetService { private static final String EXCEL_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; - private static final String ZIP_CONTENT_TYPE = "application/zip"; + private static final Pattern ASSESSED_EMPLOYEE_TEXT_PATTERN = Pattern.compile("(被考核人\\s*[::]\\s*)(.+)"); + private static final long CURRENT_TEMPLATE_CACHE_KEY = -1L; @Resource private PerformanceSheetMapper performanceSheetMapper; @@ -263,7 +263,7 @@ public class PerformanceSheetServiceImpl implements PerformanceSheetService { for (Long id : ids) { sheets.add(validateReadableSheet(id)); } - return zipSheetsWithMergedWorkbook("绩效表_" + LocalDate.now() + ".zip", sheets); + return buildMergedWorkbookDownload(sheets); } @Override @@ -282,7 +282,7 @@ public class PerformanceSheetServiceImpl implements PerformanceSheetService { employeeIds = List.of(Objects.requireNonNull(SecurityFrameworkUtils.getLoginUserId())); } List sheets = performanceSheetMapper.selectExportList(reqVO, employeeIds); - return zipSheetsWithMergedWorkbook("绩效表_" + LocalDate.now() + ".zip", sheets); + return buildMergedWorkbookDownload(sheets); } @Override @@ -530,69 +530,40 @@ public class PerformanceSheetServiceImpl implements PerformanceSheetService { return respVO; } - private PerformanceDownloadFile zipSheetsWithMergedWorkbook(String filename, List sheets) { - try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) { - Map filenameCounts = new HashMap<>(); - Map fileContents = loadFileContents(sheets); - if (sheets != null && !sheets.isEmpty()) { - byte[] mergedWorkbookContent = buildMergedWorkbook(sheets, fileContents); - putStoredZipEntry(zipOutputStream, buildMergedWorkbookEntryName(sheets), mergedWorkbookContent); - } - for (PerformanceSheetDO sheet : sheets) { - byte[] content = fileContents.get(sheet); - String entryName = uniqueFilename(defaultDownloadName(sheet), filenameCounts); - putStoredZipEntry(zipOutputStream, entryName, content); - } - zipOutputStream.finish(); - return new PerformanceDownloadFile(filename, ZIP_CONTENT_TYPE, outputStream.toByteArray()); + private PerformanceDownloadFile buildMergedWorkbookDownload(List sheets) { + try { + return new PerformanceDownloadFile(buildMergedWorkbookDownloadName(sheets), EXCEL_CONTENT_TYPE, + buildMergedWorkbook(sheets)); } catch (Exception ex) { - throw new IllegalStateException("打包绩效表失败", ex); + throw new IllegalStateException("导出绩效汇总 Excel 失败", ex); } } - private Map loadFileContents(List sheets) { - Map fileContents = new IdentityHashMap<>(); - if (sheets == null || sheets.isEmpty()) { - return fileContents; - } - for (PerformanceSheetDO sheet : sheets) { - fileContents.put(sheet, readFileContent(sheet)); - } - return fileContents; - } - - private void putStoredZipEntry(ZipOutputStream zipOutputStream, String entryName, byte[] content) throws Exception { - CRC32 crc32 = new CRC32(); - crc32.update(content); - ZipEntry zipEntry = new ZipEntry(entryName); - zipEntry.setMethod(ZipEntry.STORED); - zipEntry.setSize(content.length); - zipEntry.setCompressedSize(content.length); - zipEntry.setCrc(crc32.getValue()); - zipOutputStream.putNextEntry(zipEntry); - zipOutputStream.write(content); - zipOutputStream.closeEntry(); - } - - private byte[] buildMergedWorkbook(List sheets, Map fileContents) { + private byte[] buildMergedWorkbook(List sheets) { try (XSSFWorkbook targetWorkbook = new XSSFWorkbook(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - Set usedSheetNames = new HashSet<>(); - for (PerformanceSheetDO performanceSheet : sheets) { - byte[] sourceBytes = fileContents.get(performanceSheet); - try (ByteArrayInputStream inputStream = new ByteArrayInputStream(sourceBytes); - Workbook sourceWorkbook = WorkbookFactory.create(inputStream)) { - if (!(sourceWorkbook instanceof XSSFWorkbook)) { - throw new IllegalStateException("绩效汇总导出仅支持 .xlsx 文件"); - } - if (sourceWorkbook.getNumberOfSheets() <= 0) { - continue; - } - Sheet sourceSheet = sourceWorkbook.getSheetAt(0); + if (sheets == null || sheets.isEmpty()) { + Sheet emptySheet = targetWorkbook.createSheet("绩效表汇总"); + Row row = emptySheet.createRow(0); + row.createCell(0).setCellValue("暂无绩效数据"); + } else { + Set usedSheetNames = new HashSet<>(); + Map templateCache = new HashMap<>(); + Map templateContentCache = new HashMap<>(); + for (PerformanceSheetDO performanceSheet : sheets) { String targetSheetName = sanitizeSheetName(buildMergedSheetName(performanceSheet), usedSheetNames); Sheet targetSheet = targetWorkbook.createSheet(targetSheetName); - copySheet(sourceWorkbook, sourceSheet, targetWorkbook, targetSheet); + try { + PerformanceTemplateDO template = resolveTemplateForMergedExport(performanceSheet, templateCache); + copyTemplateSheetToTarget(targetWorkbook, targetSheet, template, templateContentCache); + } catch (Exception ex) { + log.warn("复制绩效汇总模板失败,改为空白页继续导出。sheetId={}, employeeId={}, periodMonth={}, sourceFileId={}, sourceFileName={}, reason={}", + performanceSheet.getId(), performanceSheet.getEmployeeId(), performanceSheet.getPeriodMonth(), + performanceSheet.getFileId(), performanceSheet.getFileName(), ex.getMessage(), ex); + writeFallbackSheetHeader(targetSheet, performanceSheet); + } + fillAssessedEmployeeName(targetSheet, performanceSheet.getEmployeeName()); + writeMergedExportCellValues(targetSheet, readMergedExportCellValuesSafely(performanceSheet)); } } targetWorkbook.setForceFormulaRecalculation(true); @@ -603,7 +574,10 @@ public class PerformanceSheetServiceImpl implements PerformanceSheetService { } } - private String buildMergedWorkbookEntryName(List sheets) { + private String buildMergedWorkbookDownloadName(List sheets) { + if (sheets == null || sheets.isEmpty()) { + return "绩效表汇总_" + LocalDate.now() + ".xlsx"; + } Set periodMonths = new LinkedHashSet<>(); for (PerformanceSheetDO sheet : sheets) { if (sheet != null && StringUtils.hasText(sheet.getPeriodMonth())) { @@ -611,9 +585,181 @@ public class PerformanceSheetServiceImpl implements PerformanceSheetService { } } if (periodMonths.size() == 1) { - return "0_绩效表汇总_" + periodMonths.iterator().next() + "月.xlsx"; + return "绩效表汇总_" + periodMonths.iterator().next() + "月.xlsx"; } - return "0_绩效表汇总.xlsx"; + return "绩效表汇总_" + LocalDate.now() + ".xlsx"; + } + + private PerformanceTemplateDO resolveTemplateForMergedExport(PerformanceSheetDO performanceSheet, + Map templateCache) { + PerformanceTemplateDO template = null; + Long templateId = performanceSheet.getTemplateId(); + if (templateId != null) { + template = templateCache.get(templateId); + if (!templateCache.containsKey(templateId)) { + template = performanceTemplateMapper.selectById(templateId); + templateCache.put(templateId, template); + } + } + if (template == null) { + template = templateCache.get(CURRENT_TEMPLATE_CACHE_KEY); + if (!templateCache.containsKey(CURRENT_TEMPLATE_CACHE_KEY)) { + template = performanceTemplateMapper.selectCurrent(); + templateCache.put(CURRENT_TEMPLATE_CACHE_KEY, template); + } + if (template != null && templateId != null) { + log.warn("绩效汇总导出未找到原模板,已回退当前启用模板。sheetId={}, templateId={}", + performanceSheet.getId(), templateId); + } + } + if (template == null) { + throw exception(ErrorCodeConstants.PERFORMANCE_TEMPLATE_CURRENT_NOT_EXISTS); + } + if (template.getFileId() == null) { + throw exception(ErrorCodeConstants.PERFORMANCE_FILE_NOT_EXISTS); + } + return template; + } + + private void copyTemplateSheetToTarget(Workbook targetWorkbook, Sheet targetSheet, PerformanceTemplateDO template, + Map templateContentCache) throws Exception { + byte[] templateContent = templateContentCache.get(template.getId()); + if (templateContent == null) { + templateContent = readTemplateFileContent(template); + templateContentCache.put(template.getId(), templateContent); + } + try (ByteArrayInputStream inputStream = new ByteArrayInputStream(templateContent); + Workbook templateWorkbook = WorkbookFactory.create(inputStream)) { + if (templateWorkbook.getNumberOfSheets() <= 0) { + return; + } + copySheet(templateWorkbook, templateWorkbook.getSheetAt(0), targetWorkbook, targetSheet); + } + } + + private void writeFallbackSheetHeader(Sheet targetSheet, PerformanceSheetDO performanceSheet) { + Row firstRow = targetSheet.getRow(0); + if (firstRow == null) { + firstRow = targetSheet.createRow(0); + } + firstRow.createCell(0).setCellValue("被考核人:" + defaultText(performanceSheet.getEmployeeName())); + Row secondRow = targetSheet.getRow(1); + if (secondRow == null) { + secondRow = targetSheet.createRow(1); + } + secondRow.createCell(0).setCellValue("绩效月份:" + defaultText(performanceSheet.getPeriodMonth())); + Row fourthRow = targetSheet.getRow(3); + if (fourthRow == null) { + fourthRow = targetSheet.createRow(3); + } + fourthRow.createCell(0).setCellValue("说明:模板复制失败,本页仅保留汇总字段。"); + } + + private void fillAssessedEmployeeName(Sheet targetSheet, String employeeName) { + if (!StringUtils.hasText(employeeName)) { + return; + } + int firstRowNum = targetSheet.getFirstRowNum(); + int lastRowNum = targetSheet.getLastRowNum(); + for (int rowIndex = firstRowNum; rowIndex <= lastRowNum; rowIndex++) { + Row row = targetSheet.getRow(rowIndex); + if (row == null) { + continue; + } + short firstCellNum = row.getFirstCellNum(); + short lastCellNum = row.getLastCellNum(); + if (firstCellNum < 0 || lastCellNum < 0) { + continue; + } + for (int cellIndex = firstCellNum; cellIndex < lastCellNum; cellIndex++) { + Cell cell = row.getCell(cellIndex); + if (cell == null || cell.getCellType() != org.apache.poi.ss.usermodel.CellType.STRING) { + continue; + } + String cellValue = cell.getStringCellValue(); + if (!StringUtils.hasText(cellValue)) { + continue; + } + Matcher matcher = ASSESSED_EMPLOYEE_TEXT_PATTERN.matcher(cellValue); + if (!matcher.find()) { + continue; + } + cell.setCellValue(matcher.replaceFirst("$1" + Matcher.quoteReplacement(defaultText(employeeName)))); + } + } + } + + private byte[] readTemplateFileContent(PerformanceTemplateDO template) { + CommonResult result = fileApi.getFileContent(template.getFileId()); + byte[] content = result == null ? null : result.getCheckedData(); + if (content == null || content.length == 0) { + throw exception(ErrorCodeConstants.PERFORMANCE_FILE_NOT_EXISTS); + } + return content; + } + + private Map readMergedExportCellValues(PerformanceSheetDO performanceSheet) throws Exception { + return readSelectedCellValues(readFileContent(performanceSheet), PerformanceExcelCellConstants.MERGED_EXPORT_CELL_ADDRESSES); + } + + private Map readMergedExportCellValuesSafely(PerformanceSheetDO performanceSheet) { + try { + return readMergedExportCellValues(performanceSheet); + } catch (Exception ex) { + log.warn("读取绩效汇总导出单元格失败,已跳过该绩效表的数据回填。sheetId={}, employeeId={}, periodMonth={}, sourceFileId={}, sourceFileName={}, reason={}", + performanceSheet.getId(), performanceSheet.getEmployeeId(), performanceSheet.getPeriodMonth(), + performanceSheet.getFileId(), performanceSheet.getFileName(), ex.getMessage(), ex); + return Map.of(); + } + } + + private void writeMergedExportCellValues(Sheet targetSheet, Map cellValues) { + if (cellValues == null || cellValues.isEmpty()) { + return; + } + for (Map.Entry entry : cellValues.entrySet()) { + String cellAddress = entry.getKey(); + String cellValue = entry.getValue(); + if (!StringUtils.hasText(cellAddress) || cellValue == null) { + continue; + } + CellReference reference = new CellReference(cellAddress); + Row row = targetSheet.getRow(reference.getRow()); + if (row == null) { + row = targetSheet.createRow(reference.getRow()); + } + Cell cell = row.getCell(reference.getCol()); + if (cell == null) { + cell = row.createCell(reference.getCol()); + } + if (cell.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) { + cell.removeFormula(); + } + BigDecimal scoreValue = parseScoreCellValue(cellAddress, cellValue); + if (scoreValue != null) { + cell.setCellValue(scoreValue.doubleValue()); + } else { + cell.setCellValue(cellValue); + } + } + } + + private BigDecimal parseScoreCellValue(String cellAddress, String cellValue) { + if (!isScoreCellAddress(cellAddress) || !StringUtils.hasText(cellValue)) { + return null; + } + String normalized = cellValue.trim().replace(",", ""); + try { + return new BigDecimal(normalized); + } catch (NumberFormatException ex) { + return null; + } + } + + private boolean isScoreCellAddress(String cellAddress) { + return PerformanceExcelCellConstants.ACTUAL_SCORE_CELLS.contains(cellAddress) + || PerformanceExcelCellConstants.BASE_SCORE_CELLS.contains(cellAddress) + || PerformanceExcelCellConstants.EXTRA_SCORE_CELLS.contains(cellAddress); } private String buildMergedSheetName(PerformanceSheetDO sheet) { @@ -936,20 +1082,11 @@ public class PerformanceSheetServiceImpl implements PerformanceSheetService { return content; } - private String uniqueFilename(String filename, Map filenameCounts) { - Integer count = filenameCounts.merge(filename, 1, Integer::sum); - if (count == 1) { - return filename; - } - int dotIndex = filename.lastIndexOf('.'); - if (dotIndex <= 0) { - return filename + "_" + count; - } - return filename.substring(0, dotIndex) + "_" + count + filename.substring(dotIndex); + private Map readPrefillCellValues(PerformanceSheetDO sheet) throws Exception { + return readSelectedCellValues(readFileContent(sheet), PerformanceExcelCellConstants.PREFILL_CELL_ADDRESSES); } - private Map readPrefillCellValues(PerformanceSheetDO sheet) throws Exception { - byte[] content = readFileContent(sheet); + private Map readSelectedCellValues(byte[] content, List cellAddresses) throws Exception { try (ByteArrayInputStream inputStream = new ByteArrayInputStream(content); Workbook workbook = WorkbookFactory.create(inputStream)) { if (workbook.getNumberOfSheets() <= 0) { @@ -959,7 +1096,7 @@ public class PerformanceSheetServiceImpl implements PerformanceSheetService { FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator(); DataFormatter formatter = new DataFormatter(); Map cellValues = new LinkedHashMap<>(); - for (String cellAddress : PerformanceExcelCellConstants.PREFILL_CELL_ADDRESSES) { + for (String cellAddress : cellAddresses) { String value = readCellDisplayValue(sourceSheet, evaluator, formatter, cellAddress); if (value != null) { cellValues.put(cellAddress, value);