From 5b2c6dd94c25430ea2599eec691a00970fe4c201 Mon Sep 17 00:00:00 2001 From: dk <1260500659@qq.com> Date: Thu, 9 Jul 2026 16:23:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(performance):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=BB=A9=E6=95=88=E8=A1=A8=E5=90=88=E5=B9=B6=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=20-=20=E7=A7=BB=E9=99=A4ZIP=E5=8E=8B?= =?UTF-8?q?=E7=BC=A9=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=B9=E4=B8=BA=E7=9B=B4?= =?UTF-8?q?=E6=8E=A5=E8=BF=94=E5=9B=9E=E5=90=88=E5=B9=B6=E7=9A=84Excel?= =?UTF-8?q?=E6=96=87=E4=BB=B6=20-=20=E6=96=B0=E5=A2=9EMERGED=5FEXPORT=5FCE?= =?UTF-8?q?LL=5FADDRESSES=E5=B8=B8=E9=87=8F=E5=AE=9A=E4=B9=89=E5=90=88?= =?UTF-8?q?=E5=B9=B6=E5=AF=BC=E5=87=BA=E5=8D=95=E5=85=83=E6=A0=BC=E5=9C=B0?= =?UTF-8?q?=E5=9D=80=20-=20=E6=B7=BB=E5=8A=A0=E6=AD=A3=E5=88=99=E8=A1=A8?= =?UTF-8?q?=E8=BE=BE=E5=BC=8F=E7=94=A8=E4=BA=8E=E5=8C=B9=E9=85=8D=E5=92=8C?= =?UTF-8?q?=E6=9B=BF=E6=8D=A2=E8=A2=AB=E8=80=83=E6=A0=B8=E4=BA=BA=E5=A7=93?= =?UTF-8?q?=E5=90=8D=20-=20=E5=AE=9E=E7=8E=B0=E5=9F=BA=E4=BA=8E=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E7=9A=84=E5=90=88=E5=B9=B6=E5=AF=BC=E5=87=BA=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=EF=BC=8C=E6=94=AF=E6=8C=81=E6=A8=A1=E6=9D=BF=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E6=9C=BA=E5=88=B6=20-=20=E6=B7=BB=E5=8A=A0=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E5=A4=B1=E8=B4=A5=E6=97=B6=E7=9A=84=E9=99=8D=E7=BA=A7?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=92=8C=E7=A9=BA=E7=99=BD=E9=A1=B5=E7=94=9F?= =?UTF-8?q?=E6=88=90=20-=20=E5=AE=9E=E7=8E=B0=E8=A2=AB=E8=80=83=E6=A0=B8?= =?UTF-8?q?=E4=BA=BA=E5=A7=93=E5=90=8D=E7=9A=84=E8=87=AA=E5=8A=A8=E5=A1=AB?= =?UTF-8?q?=E5=85=85=E5=92=8C=E5=8D=95=E5=85=83=E6=A0=BC=E5=80=BC=E7=9A=84?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E8=AF=BB=E5=8F=96=20-=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E5=B7=A5=E4=BD=9C=E7=B0=BF=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E7=A7=BB=E9=99=A4=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E7=9A=84=E6=96=87=E4=BB=B6=E5=86=85=E5=AE=B9=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=20-=20=E6=94=B9=E8=BF=9B=E4=B8=8B=E8=BD=BD=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E8=A7=84=E5=88=99=EF=BC=8C=E5=8E=BB=E9=99=A4?= =?UTF-8?q?=E7=BC=96=E5=8F=B7=E5=89=8D=E7=BC=80=E5=B9=B6=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PerformanceExcelCellConstants.java | 7 + .../PerformanceSheetServiceImpl.java | 291 +++++++++++++----- 2 files changed, 221 insertions(+), 77 deletions(-) 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);