From f5f828cd05e7e22c046bf1a1d982f8d8dded678a Mon Sep 17 00:00:00 2001 From: yexb <553699424@qq.com> Date: Fri, 3 Jul 2026 16:05:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(test-report):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=8A=A5=E5=91=8A=E5=8F=B0=E8=B4=A6=E5=AF=BC?= =?UTF-8?q?=E5=85=A5=E4=B8=8E=E5=88=86=E7=BB=84=E7=94=9F=E6=88=90=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在.gitignore中添加各模块SQL资源目录忽略规则 - 从client-unit模块移除空的MyBatis映射文件 - 在test-report模块pom.xml中添加client-unit、test-device依赖及poi-ooxml库 - 扩展TestReportConst类添加报告生成状态、存储路径等常量定义 - 增强TestReportController实现台账模板下载、数据导入、分组管理等REST接口 - 修改TestReportMapper查询语句添加报告生成状态和数量统计字段 - 扩展TestReportParam.AddParam添加可选ID参数支持前端指定ID - 在TestReportPO实体类中增加reportGenerateState字段存储生成状态 - 完善TestReportService接口定义台账导入、分组管理等业务方法 - 实现TestReportServiceImpl服务类中的台账导入、分组生成核心逻辑 - 添加报告ID解析、导入验证、文件处理等辅助方法实现 --- .gitignore | 5 + .../mapper/mapping/ClientUnitMapper.xml | 6 - ai-report/test-report/pom.xml | 18 + .../TestReportLedgerExcelParser.java | 317 ++++++++++++ .../TestReportLedgerImportService.java | 477 ++++++++++++++++++ .../TestReportLedgerTemplateService.java | 64 +++ .../component/TestReportPointExcelParser.java | 108 ++++ .../component/TestReportStorageService.java | 88 ++++ .../controller/TestReportController.java | 72 +++ .../mapper/TestReportGroupReportMapper.java | 9 + .../testreport/mapper/TestReportMapper.java | 6 + .../mapper/TestReportPointMapper.java | 9 + .../pojo/constant/TestReportConst.java | 25 + .../pojo/param/TestReportGroupSaveParam.java | 29 ++ .../pojo/param/TestReportParam.java | 3 + .../pojo/po/TestReportGroupReportPO.java | 30 ++ .../testreport/pojo/po/TestReportPO.java | 2 + .../testreport/pojo/po/TestReportPointPO.java | 41 ++ .../pojo/vo/TestReportGroupReportVO.java | 19 + .../vo/TestReportLedgerImportResultVO.java | 25 + .../vo/TestReportLedgerImportStageVO.java | 11 + .../vo/TestReportPointImportResultVO.java | 14 + .../testreport/pojo/vo/TestReportPointVO.java | 25 + .../testreport/pojo/vo/TestReportVO.java | 6 + .../testreport/service/TestReportService.java | 19 + .../service/impl/TestReportServiceImpl.java | 460 ++++++++++++++++- .../mapping/TestReportGroupReportMapper.xml | 4 + .../mapper/mapping/TestReportPointMapper.xml | 4 + .../TestReportLedgerExcelParserTest.java | 153 ++++++ .../TestReportLedgerImportServiceTest.java | 104 ++++ .../TestReportLedgerTemplateServiceTest.java | 34 ++ .../impl/TestReportServiceImplTest.java | 176 ++++++- 32 files changed, 2349 insertions(+), 14 deletions(-) delete mode 100644 ai-report/client-unit/src/main/resources/com/njcn/gather/aireport/clientunit/mapper/mapping/ClientUnitMapper.xml create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerExcelParser.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerImportService.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerTemplateService.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/component/TestReportPointExcelParser.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/component/TestReportStorageService.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/mapper/TestReportGroupReportMapper.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/mapper/TestReportPointMapper.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/param/TestReportGroupSaveParam.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/po/TestReportGroupReportPO.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/po/TestReportPointPO.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportGroupReportVO.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportLedgerImportResultVO.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportLedgerImportStageVO.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportPointImportResultVO.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportPointVO.java create mode 100644 ai-report/test-report/src/main/resources/com/njcn/gather/aireport/testreport/mapper/mapping/TestReportGroupReportMapper.xml create mode 100644 ai-report/test-report/src/main/resources/com/njcn/gather/aireport/testreport/mapper/mapping/TestReportPointMapper.xml create mode 100644 ai-report/test-report/src/test/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerExcelParserTest.java create mode 100644 ai-report/test-report/src/test/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerImportServiceTest.java create mode 100644 ai-report/test-report/src/test/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerTemplateServiceTest.java diff --git a/.gitignore b/.gitignore index 61636bd..e6e606a 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,8 @@ rebel.xml /.fastRequest/collections/Root/Default Group/directory.json /.fastRequest/collections/Root/directory.json /.fastRequest/config/fastRequestCurrentProjectConfig.json +/ai-report/report-approval/src/main/resources/sql/report-approval/ +/ai-report/test-report/src/main/resources/sql/test-report/ +/ai-report/client-unit/src/main/resources/sql/client-unit/ +/ai-report/report-model/src/main/resources/sql/report-model/ +/ai-report/test-device/src/main/resources/sql/test-device/ diff --git a/ai-report/client-unit/src/main/resources/com/njcn/gather/aireport/clientunit/mapper/mapping/ClientUnitMapper.xml b/ai-report/client-unit/src/main/resources/com/njcn/gather/aireport/clientunit/mapper/mapping/ClientUnitMapper.xml deleted file mode 100644 index 49589e5..0000000 --- a/ai-report/client-unit/src/main/resources/com/njcn/gather/aireport/clientunit/mapper/mapping/ClientUnitMapper.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/ai-report/test-report/pom.xml b/ai-report/test-report/pom.xml index af8f158..04ea811 100644 --- a/ai-report/test-report/pom.xml +++ b/ai-report/test-report/pom.xml @@ -26,6 +26,18 @@ 1.0.0 + + com.njcn.gather + client-unit + 1.0.0 + + + + com.njcn.gather + test-device + 1.0.0 + + com.njcn njcn-common @@ -50,6 +62,12 @@ 1.0.0 + + org.apache.poi + poi-ooxml + 4.1.2 + + org.springframework.boot spring-boot-starter-web diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerExcelParser.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerExcelParser.java new file mode 100644 index 0000000..1d1f8f3 --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerExcelParser.java @@ -0,0 +1,317 @@ +package com.njcn.gather.aireport.testreport.component; + +import cn.hutool.core.util.StrUtil; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst; +import lombok.Data; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Component +public class TestReportLedgerExcelParser { + + private static final DataFormatter FORMATTER = new DataFormatter(); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private static final List POINT_HEADERS = Arrays.asList( + "分组号", "变电站", "监测点名称", "检测设备编号", "委托单位名称", "Excel附件名称", "Word附件名称"); + private static final List DEVICE_HEADERS = Arrays.asList( + "设备型号", "设备编号", "标准有效期", "设备状态", "标准报告文件名"); + private static final List CLIENT_HEADERS = Arrays.asList( + "单位名称", "联系人", "联系方式", "地址"); + + public ParsedLedger parse(MultipartFile summaryFile, Map attachments) { + String fileName = summaryFile == null ? null : StrUtil.trimToNull(summaryFile.getOriginalFilename()); + if (!TestReportConst.LEDGER_SUMMARY_FILE_NAME.equals(fileName)) { + throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:台账汇总文件名必须为台账信息汇总.xlsx"); + } + try (InputStream inputStream = summaryFile.getInputStream(); + Workbook workbook = WorkbookFactory.create(inputStream)) { + ParsedLedger ledger = new ParsedLedger(); + ledger.setSummaryFileName(fileName); + ledger.setPoints(parsePointSheet(requireSheet(workbook, TestReportConst.LEDGER_POINT_SHEET_NAME), attachments)); + ledger.setDevices(parseDeviceSheet(workbook.getSheet(TestReportConst.LEDGER_DEVICE_SHEET_NAME))); + ledger.setClients(parseClientSheet(workbook.getSheet(TestReportConst.LEDGER_CLIENT_SHEET_NAME))); + validateReferencedAttachments(ledger.getPoints(), attachments); + return ledger; + } catch (BusinessException exception) { + throw exception; + } catch (IOException exception) { + throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:读取台账汇总excel失败"); + } catch (Exception exception) { + throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:解析台账汇总excel失败"); + } + } + + private Sheet requireSheet(Workbook workbook, String sheetName) { + Sheet sheet = workbook.getSheet(sheetName); + if (sheet == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:缺少" + sheetName + "sheet"); + } + return sheet; + } + + private List parsePointSheet(Sheet sheet, Map attachments) { + validateHeaders(sheet, POINT_HEADERS, "监测点信息"); + List points = new ArrayList(); + Set pointKeys = new HashSet(); + Set excelNames = new HashSet(); + Set wordNames = new HashSet(); + for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) { + Row row = sheet.getRow(rowIndex); + if (isEmptyRow(row, POINT_HEADERS.size())) { + continue; + } + ParsedPointRow point = new ParsedPointRow(); + point.setRowNo(rowIndex + 1); + point.setGroupNo(parsePositiveInteger(optionalCell(row, 0), "第" + (rowIndex + 1) + "行分组号必须为正整数")); + point.setSubstationName(requireCell(row, 1, rowIndex, "变电站")); + point.setPointName(requireCell(row, 2, rowIndex, "监测点名称")); + point.setTestDeviceNo(optionalCell(row, 3)); + point.setClientUnitName(optionalCell(row, 4)); + point.setExcelAttachmentName(requireCell(row, 5, rowIndex, "Excel附件名称")); + point.setWordAttachmentName(requireCell(row, 6, rowIndex, "Word附件名称")); + validateExtension(point.getExcelAttachmentName(), Arrays.asList(".xls", ".xlsx"), + "第" + (rowIndex + 1) + "行Excel附件扩展名仅允许xls/xlsx"); + validateExtension(point.getWordAttachmentName(), Arrays.asList(".doc", ".docx"), + "第" + (rowIndex + 1) + "行Word附件扩展名仅允许doc/docx"); + if (!attachments.containsKey(point.getExcelAttachmentName())) { + throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:第" + (rowIndex + 1) + "行缺少Excel附件"); + } + if (!attachments.containsKey(point.getWordAttachmentName())) { + throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:第" + (rowIndex + 1) + "行缺少Word附件"); + } + String pointKey = point.getSubstationName() + "|" + point.getPointName(); + if (!pointKeys.add(pointKey)) { + throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:监测点信息中存在重复点位"); + } + if (!excelNames.add(point.getExcelAttachmentName())) { + throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:监测点信息中存在重复Excel附件名称"); + } + if (!wordNames.add(point.getWordAttachmentName())) { + throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:监测点信息中存在重复Word附件名称"); + } + points.add(point); + } + if (points.isEmpty()) { + throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:监测点信息sheet不能为空"); + } + return points; + } + + private List parseDeviceSheet(Sheet sheet) { + if (sheet == null) { + return Collections.emptyList(); + } + validateHeaders(sheet, DEVICE_HEADERS, "检测设备"); + List devices = new ArrayList(); + Set deviceNos = new HashSet(); + for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) { + Row row = sheet.getRow(rowIndex); + if (isEmptyRow(row, DEVICE_HEADERS.size())) { + continue; + } + ParsedDeviceRow device = new ParsedDeviceRow(); + device.setRowNo(rowIndex + 1); + device.setTypeName(requireCell(row, 0, rowIndex, "设备型号")); + device.setNo(requireCell(row, 1, rowIndex, "设备编号")); + device.setValidityPeriod(parseDate(requireCell(row, 2, rowIndex, "标准有效期"), rowIndex)); + device.setStateName(requireCell(row, 3, rowIndex, "设备状态")); + device.setValidityReportName(requireCell(row, 4, rowIndex, "标准报告文件名")); + if (!deviceNos.add(device.getNo())) { + throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:检测设备sheet中存在重复设备编号"); + } + devices.add(device); + } + return devices; + } + + private List parseClientSheet(Sheet sheet) { + if (sheet == null) { + return Collections.emptyList(); + } + validateHeaders(sheet, CLIENT_HEADERS, "委托单位"); + List clients = new ArrayList(); + Set clientNames = new HashSet(); + for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) { + Row row = sheet.getRow(rowIndex); + if (isEmptyRow(row, CLIENT_HEADERS.size())) { + continue; + } + ParsedClientRow client = new ParsedClientRow(); + client.setRowNo(rowIndex + 1); + client.setName(requireCell(row, 0, rowIndex, "单位名称")); + client.setContactName(optionalCell(row, 1)); + client.setPhonenumber(optionalCell(row, 2)); + client.setAddress(optionalCell(row, 3)); + if (!clientNames.add(client.getName())) { + throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:委托单位sheet中存在重复单位名称"); + } + clients.add(client); + } + return clients; + } + + private void validateReferencedAttachments(List points, Map attachments) { + Set referencedNames = new HashSet(); + for (ParsedPointRow point : points) { + referencedNames.add(point.getExcelAttachmentName()); + referencedNames.add(point.getWordAttachmentName()); + } + for (String attachmentName : attachments.keySet()) { + if (!referencedNames.contains(attachmentName)) { + throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:存在未在监测点信息sheet中引用的附件 " + attachmentName); + } + } + } + + private void validateHeaders(Sheet sheet, List expectedHeaders, String sheetName) { + Row headerRow = sheet.getRow(0); + if (headerRow == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:" + sheetName + "sheet缺少表头"); + } + for (int i = 0; i < expectedHeaders.size(); i++) { + String actualHeader = optionalCell(headerRow, i); + if (!expectedHeaders.get(i).equals(actualHeader)) { + throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:" + sheetName + "sheet表头不符合模板要求"); + } + } + } + + private boolean isEmptyRow(Row row, int cellCount) { + if (row == null) { + return true; + } + for (int i = 0; i < cellCount; i++) { + if (StrUtil.isNotBlank(optionalCell(row, i))) { + return false; + } + } + return true; + } + + private String requireCell(Row row, int cellIndex, int rowIndex, String fieldName) { + String value = optionalCell(row, cellIndex); + if (StrUtil.isBlank(value)) { + throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:第" + (rowIndex + 1) + "行缺少" + fieldName); + } + return value; + } + + private String optionalCell(Row row, int cellIndex) { + if (row == null) { + return null; + } + Cell cell = row.getCell(cellIndex); + return cell == null ? null : StrUtil.trimToNull(FORMATTER.formatCellValue(cell)); + } + + private Integer parsePositiveInteger(String value, String message) { + if (StrUtil.isBlank(value)) { + return null; + } + try { + int parsed = Integer.parseInt(value); + if (parsed <= 0) { + throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:" + message); + } + return parsed; + } catch (NumberFormatException exception) { + throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:" + message); + } + } + + private void validateExtension(String fileName, List validExtensions, String message) { + String lowerFileName = fileName.toLowerCase(); + for (String validExtension : validExtensions) { + if (lowerFileName.endsWith(validExtension)) { + return; + } + } + throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:" + message); + } + + private LocalDate parseDate(String value, int rowIndex) { + try { + return LocalDate.parse(value, DATE_FORMATTER); + } catch (DateTimeParseException exception) { + throw new BusinessException(CommonResponseEnum.FAIL, + "校验文件阶段:检测设备sheet第" + (rowIndex + 1) + "行标准有效期格式必须为yyyy-MM-dd"); + } + } + + @Data + public static class ParsedLedger { + private String summaryFileName; + private List points = new ArrayList(); + private List devices = new ArrayList(); + private List clients = new ArrayList(); + + public Map toDeviceMap() { + Map result = new HashMap(); + for (ParsedDeviceRow device : devices) { + result.put(device.getNo(), device); + } + return result; + } + + public Map toClientMap() { + Map result = new HashMap(); + for (ParsedClientRow client : clients) { + result.put(client.getName(), client); + } + return result; + } + } + + @Data + public static class ParsedPointRow { + private Integer rowNo; + private Integer groupNo; + private String substationName; + private String pointName; + private String testDeviceNo; + private String clientUnitName; + private String excelAttachmentName; + private String wordAttachmentName; + } + + @Data + public static class ParsedDeviceRow { + private Integer rowNo; + private String typeName; + private String no; + private LocalDate validityPeriod; + private String stateName; + private String validityReportName; + } + + @Data + public static class ParsedClientRow { + private Integer rowNo; + private String name; + private String contactName; + private String phonenumber; + private String address; + } +} diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerImportService.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerImportService.java new file mode 100644 index 0000000..fcc092b --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerImportService.java @@ -0,0 +1,477 @@ +package com.njcn.gather.aireport.testreport.component; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.aireport.clientunit.mapper.ClientUnitMapper; +import com.njcn.gather.aireport.clientunit.pojo.constant.ClientUnitConst; +import com.njcn.gather.aireport.clientunit.pojo.po.ClientUnitPO; +import com.njcn.gather.aireport.testdevice.mapper.TestDeviceMapper; +import com.njcn.gather.aireport.testdevice.pojo.constant.TestDeviceConst; +import com.njcn.gather.aireport.testdevice.pojo.po.TestDevicePO; +import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceTypeNameVO; +import com.njcn.gather.aireport.testreport.mapper.TestReportGroupReportMapper; +import com.njcn.gather.aireport.testreport.mapper.TestReportMapper; +import com.njcn.gather.aireport.testreport.mapper.TestReportPointMapper; +import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst; +import com.njcn.gather.aireport.testreport.pojo.po.TestReportGroupReportPO; +import com.njcn.gather.aireport.testreport.pojo.po.TestReportPO; +import com.njcn.gather.aireport.testreport.pojo.po.TestReportPointPO; +import com.njcn.gather.aireport.testreport.pojo.vo.TestReportLedgerImportResultVO; +import com.njcn.gather.aireport.testreport.pojo.vo.TestReportLedgerImportStageVO; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@Component +public class TestReportLedgerImportService { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private final TestReportMapper testReportMapper; + private final TestReportPointMapper testReportPointMapper; + private final TestReportGroupReportMapper testReportGroupReportMapper; + private final TestDeviceMapper testDeviceMapper; + private final ClientUnitMapper clientUnitMapper; + private final TestReportLedgerExcelParser testReportLedgerExcelParser; + private final TestReportStorageService testReportStorageService; + + public TestReportLedgerImportService(TestReportMapper testReportMapper, + TestReportPointMapper testReportPointMapper, + TestReportGroupReportMapper testReportGroupReportMapper, + TestDeviceMapper testDeviceMapper, + ClientUnitMapper clientUnitMapper, + TestReportLedgerExcelParser testReportLedgerExcelParser, + TestReportStorageService testReportStorageService) { + this.testReportMapper = testReportMapper; + this.testReportPointMapper = testReportPointMapper; + this.testReportGroupReportMapper = testReportGroupReportMapper; + this.testDeviceMapper = testDeviceMapper; + this.clientUnitMapper = clientUnitMapper; + this.testReportLedgerExcelParser = testReportLedgerExcelParser; + this.testReportStorageService = testReportStorageService; + } + + public TestReportLedgerImportResultVO importLedger(TestReportPO report, MultipartFile[] files, String userId) { + Map fileMap = normalizeFileMap(files); + MultipartFile summaryFile = fileMap.remove(TestReportConst.LEDGER_SUMMARY_FILE_NAME); + if (summaryFile == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:缺少台账信息汇总.xlsx"); + } + TestReportLedgerExcelParser.ParsedLedger ledger = testReportLedgerExcelParser.parse(summaryFile, fileMap); + + Map dbDeviceMap = loadExistingDevices(collectDeviceNos(ledger)); + Map dbClientMap = loadExistingClients(collectClientNames(ledger)); + Map sheetDeviceMap = ledger.toDeviceMap(); + Map sheetClientMap = ledger.toClientMap(); + validatePointReferences(ledger.getPoints(), dbDeviceMap, dbClientMap, sheetDeviceMap, sheetClientMap); + + BaseDataSummary baseDataSummary = maintainBaseData(ledger, dbDeviceMap, dbClientMap, userId); + List oldPoints = loadNormalPoints(report.getId()); + List oldGroupReports = loadNormalGroupReports(report.getId()); + deleteOldFiles(oldPoints, oldGroupReports, report.getData()); + removeOldRows(report.getId()); + + String summaryStoragePath = testReportStorageService.saveLedgerSummaryExcel(report.getId(), summaryFile); + int groupCount = saveNewPoints(report.getId(), ledger.getPoints(), fileMap, userId); + LocalDateTime now = LocalDateTime.now(); + report.setData(buildSnapshotJson(ledger, summaryStoragePath, baseDataSummary, groupCount)); + report.setReportGenerateState(TestReportConst.REPORT_GENERATE_STATE_PENDING); + report.setUpdateBy(userId); + report.setUpdateTime(now); + testReportMapper.updateById(report); + + return buildSuccessResult(ledger, baseDataSummary, fileMap.size() + 1, groupCount); + } + + private Map normalizeFileMap(MultipartFile[] files) { + if (files == null || files.length == 0) { + throw new BusinessException(CommonResponseEnum.FAIL, "选择文件阶段:导入文件不能为空"); + } + Map fileMap = new LinkedHashMap(); + for (MultipartFile file : files) { + if (file == null || file.isEmpty()) { + continue; + } + String fileName = normalizeFileName(file.getOriginalFilename()); + if (fileName == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "选择文件阶段:文件名不能为空"); + } + if (fileMap.containsKey(fileName)) { + throw new BusinessException(CommonResponseEnum.FAIL, "选择文件阶段:存在重复文件名 " + fileName); + } + fileMap.put(fileName, file); + } + if (fileMap.isEmpty()) { + throw new BusinessException(CommonResponseEnum.FAIL, "选择文件阶段:导入文件不能为空"); + } + return fileMap; + } + + private void validatePointReferences(List points, + Map dbDeviceMap, + Map dbClientMap, + Map sheetDeviceMap, + Map sheetClientMap) { + for (TestReportLedgerExcelParser.ParsedPointRow point : points) { + if (StrUtil.isNotBlank(point.getTestDeviceNo()) + && !dbDeviceMap.containsKey(point.getTestDeviceNo()) + && !sheetDeviceMap.containsKey(point.getTestDeviceNo())) { + throw new BusinessException(CommonResponseEnum.FAIL, + "校验文件阶段:第" + point.getRowNo() + "行检测设备编号在数据库和检测设备sheet中都不存在"); + } + if (StrUtil.isNotBlank(point.getClientUnitName()) + && !dbClientMap.containsKey(point.getClientUnitName()) + && !sheetClientMap.containsKey(point.getClientUnitName())) { + throw new BusinessException(CommonResponseEnum.FAIL, + "校验文件阶段:第" + point.getRowNo() + "行委托单位名称在数据库和委托单位sheet中都不存在"); + } + } + } + + private BaseDataSummary maintainBaseData(TestReportLedgerExcelParser.ParsedLedger ledger, + Map dbDeviceMap, + Map dbClientMap, + String userId) { + BaseDataSummary summary = new BaseDataSummary(); + Map typeNameIdMap = loadDeviceTypeNameIdMap(); + LocalDateTime now = LocalDateTime.now(); + for (TestReportLedgerExcelParser.ParsedDeviceRow deviceRow : ledger.getDevices()) { + TestDevicePO existingDevice = dbDeviceMap.get(deviceRow.getNo()); + if (existingDevice != null) { + summary.setDeviceReuseCount(summary.getDeviceReuseCount() + 1); + continue; + } + String typeId = typeNameIdMap.get(deviceRow.getTypeName()); + if (StrUtil.isBlank(typeId)) { + throw new BusinessException(CommonResponseEnum.FAIL, + "基础资料维护阶段:检测设备sheet第" + deviceRow.getRowNo() + "行设备型号不存在"); + } + TestDevicePO device = new TestDevicePO(); + device.setId(UUID.randomUUID().toString()); + device.setType(typeId); + device.setNo(deviceRow.getNo()); + device.setValidityPeriod(deviceRow.getValidityPeriod()); + device.setValidityReport(deviceRow.getValidityReportName()); + device.setState(resolveDeviceState(deviceRow.getStateName(), deviceRow.getRowNo())); + device.setStatus(TestDeviceConst.STATUS_NORMAL); + device.setCreateBy(userId); + device.setCreateTime(now); + device.setUpdateBy(userId); + device.setUpdateTime(now); + testDeviceMapper.insert(device); + dbDeviceMap.put(device.getNo(), device); + summary.setDeviceAddedCount(summary.getDeviceAddedCount() + 1); + } + + for (TestReportLedgerExcelParser.ParsedClientRow clientRow : ledger.getClients()) { + ClientUnitPO existingClient = dbClientMap.get(clientRow.getName()); + if (existingClient != null) { + summary.setClientReuseCount(summary.getClientReuseCount() + 1); + continue; + } + ClientUnitPO client = new ClientUnitPO(); + client.setId(UUID.randomUUID().toString()); + client.setName(clientRow.getName()); + client.setContactName(defaultEmpty(clientRow.getContactName())); + client.setPhonenumber(defaultEmpty(clientRow.getPhonenumber())); + client.setAddress(clientRow.getAddress()); + client.setStatus(ClientUnitConst.STATUS_NORMAL); + client.setCreateBy(userId); + client.setCreateTime(now); + client.setUpdateBy(userId); + client.setUpdateTime(now); + clientUnitMapper.insert(client); + dbClientMap.put(client.getName(), client); + summary.setClientAddedCount(summary.getClientAddedCount() + 1); + } + return summary; + } + + private int saveNewPoints(String reportId, List points, + Map fileMap, String userId) { + LocalDateTime now = LocalDateTime.now(); + Set groupNos = new HashSet(); + int sortNo = 1; + for (TestReportLedgerExcelParser.ParsedPointRow pointRow : points) { + MultipartFile excelFile = fileMap.get(pointRow.getExcelAttachmentName()); + MultipartFile wordFile = fileMap.get(pointRow.getWordAttachmentName()); + TestReportPointPO point = new TestReportPointPO(); + point.setId(UUID.randomUUID().toString()); + point.setReportId(reportId); + point.setGroupNo(pointRow.getGroupNo()); + point.setSubstationName(pointRow.getSubstationName()); + point.setPointName(pointRow.getPointName()); + point.setTestDeviceNo(pointRow.getTestDeviceNo()); + point.setClientUnitName(pointRow.getClientUnitName()); + point.setExcelAttachmentName(pointRow.getExcelAttachmentName()); + point.setExcelStoragePath(testReportStorageService.savePointExcelAttachment(reportId, excelFile)); + point.setWordAttachmentName(pointRow.getWordAttachmentName()); + point.setWordStoragePath(testReportStorageService.savePointWordAttachment(reportId, wordFile)); + point.setSortNo(sortNo++); + point.setDeleted(TestReportConst.DELETED_NO); + point.setCreateBy(userId); + point.setCreateTime(now); + point.setUpdateBy(userId); + point.setUpdateTime(now); + testReportPointMapper.insert(point); + if (pointRow.getGroupNo() != null) { + groupNos.add(pointRow.getGroupNo()); + } + } + return groupNos.size(); + } + + private void deleteOldFiles(List oldPoints, List oldGroupReports, String data) { + for (TestReportPointPO point : oldPoints) { + testReportStorageService.deleteQuietly(point.getExcelStoragePath()); + testReportStorageService.deleteQuietly(point.getWordStoragePath()); + } + for (TestReportGroupReportPO groupReport : oldGroupReports) { + testReportStorageService.deleteQuietly(groupReport.getReportStoragePath()); + } + testReportStorageService.deleteQuietly(resolveSummaryStoragePath(data)); + } + + private void removeOldRows(String reportId) { + LambdaQueryWrapper pointWrapper = new LambdaQueryWrapper(); + pointWrapper.eq(TestReportPointPO::getReportId, reportId); + testReportPointMapper.delete(pointWrapper); + + LambdaQueryWrapper groupReportWrapper = new LambdaQueryWrapper(); + groupReportWrapper.eq(TestReportGroupReportPO::getReportId, reportId); + testReportGroupReportMapper.delete(groupReportWrapper); + } + + private String buildSnapshotJson(TestReportLedgerExcelParser.ParsedLedger ledger, String summaryStoragePath, + BaseDataSummary summary, int groupCount) { + ObjectNode root = OBJECT_MAPPER.createObjectNode(); + root.put("version", "2.0"); + root.put("importMode", "summary-excel-with-base-data"); + root.put("summaryFileName", ledger.getSummaryFileName()); + root.put("summaryStoragePath", summaryStoragePath); + + ObjectNode sheetSummary = root.putObject("sheetSummary"); + sheetSummary.put("pointSheet", TestReportConst.LEDGER_POINT_SHEET_NAME); + sheetSummary.put("deviceSheetPresent", !ledger.getDevices().isEmpty()); + sheetSummary.put("clientSheetPresent", !ledger.getClients().isEmpty()); + + root.put("rowCount", ledger.getPoints().size()); + root.put("pointCount", ledger.getPoints().size()); + root.put("groupCount", groupCount); + + ArrayNode groupSummary = root.putArray("groupSummary"); + Map groupCounter = ledger.getPoints().stream() + .collect(Collectors.groupingBy(TestReportLedgerExcelParser.ParsedPointRow::getGroupNo, + LinkedHashMap::new, Collectors.counting())); + for (Map.Entry entry : groupCounter.entrySet()) { + ObjectNode groupNode = groupSummary.addObject(); + if (entry.getKey() == null) { + groupNode.putNull("groupNo"); + } else { + groupNode.put("groupNo", entry.getKey()); + } + groupNode.put("count", entry.getValue()); + } + + ObjectNode baseDataSummary = root.putObject("baseDataSummary"); + baseDataSummary.put("deviceAddedCount", summary.getDeviceAddedCount()); + baseDataSummary.put("deviceReuseCount", summary.getDeviceReuseCount()); + baseDataSummary.put("clientAddedCount", summary.getClientAddedCount()); + baseDataSummary.put("clientReuseCount", summary.getClientReuseCount()); + return root.toString(); + } + + private TestReportLedgerImportResultVO buildSuccessResult(TestReportLedgerExcelParser.ParsedLedger ledger, + BaseDataSummary summary, int totalFileCount, int groupCount) { + TestReportLedgerImportResultVO result = new TestReportLedgerImportResultVO(); + result.setCurrentStage("完成"); + result.setSuccess(Boolean.TRUE); + result.setSummaryFileName(ledger.getSummaryFileName()); + result.setTotalFileCount(totalFileCount); + result.setPointCount(ledger.getPoints().size()); + result.setExcelAttachmentCount(ledger.getPoints().size()); + result.setWordAttachmentCount(ledger.getPoints().size()); + result.setDeviceAddedCount(summary.getDeviceAddedCount()); + result.setDeviceReuseCount(summary.getDeviceReuseCount()); + result.setClientAddedCount(summary.getClientAddedCount()); + result.setClientReuseCount(summary.getClientReuseCount()); + result.setGroupCount(groupCount); + result.getStages().add(buildStage("选择文件", "文件选择完成")); + result.getStages().add(buildStage("校验文件", "文件校验通过")); + result.getStages().add(buildStage("文件上传", "文件上传完成")); + result.getStages().add(buildStage("基础资料维护", "基础资料维护完成")); + result.getStages().add(buildStage("完成", "导入完成")); + return result; + } + + private TestReportLedgerImportStageVO buildStage(String stage, String message) { + TestReportLedgerImportStageVO stageVO = new TestReportLedgerImportStageVO(); + stageVO.setStage(stage); + stageVO.setSuccess(Boolean.TRUE); + stageVO.setMessage(message); + return stageVO; + } + + private String resolveSummaryStoragePath(String data) { + if (StrUtil.isBlank(data)) { + return null; + } + try { + String summaryStoragePath = OBJECT_MAPPER.readTree(data).path("summaryStoragePath").asText(); + return StrUtil.isBlank(summaryStoragePath) ? null : summaryStoragePath; + } catch (Exception exception) { + return null; + } + } + + private List loadNormalPoints(String reportId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper(); + wrapper.eq(TestReportPointPO::getReportId, reportId) + .eq(TestReportPointPO::getDeleted, TestReportConst.DELETED_NO); + return testReportPointMapper.selectList(wrapper); + } + + private List loadNormalGroupReports(String reportId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper(); + wrapper.eq(TestReportGroupReportPO::getReportId, reportId) + .eq(TestReportGroupReportPO::getDeleted, TestReportConst.DELETED_NO); + return testReportGroupReportMapper.selectList(wrapper); + } + + private Map loadExistingDevices(Collection deviceNos) { + if (deviceNos.isEmpty()) { + return new HashMap(); + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper(); + wrapper.in(TestDevicePO::getNo, deviceNos) + .eq(TestDevicePO::getStatus, TestDeviceConst.STATUS_NORMAL); + return testDeviceMapper.selectList(wrapper).stream() + .collect(Collectors.toMap(TestDevicePO::getNo, item -> item, (first, second) -> first, HashMap::new)); + } + + private Map loadExistingClients(Collection clientNames) { + if (clientNames.isEmpty()) { + return new HashMap(); + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper(); + wrapper.in(ClientUnitPO::getName, clientNames) + .eq(ClientUnitPO::getStatus, ClientUnitConst.STATUS_NORMAL); + return clientUnitMapper.selectList(wrapper).stream() + .collect(Collectors.toMap(ClientUnitPO::getName, item -> item, (first, second) -> first, HashMap::new)); + } + + private Collection collectDeviceNos(TestReportLedgerExcelParser.ParsedLedger ledger) { + Set deviceNos = new HashSet(); + for (TestReportLedgerExcelParser.ParsedPointRow point : ledger.getPoints()) { + if (StrUtil.isNotBlank(point.getTestDeviceNo())) { + deviceNos.add(point.getTestDeviceNo()); + } + } + for (TestReportLedgerExcelParser.ParsedDeviceRow device : ledger.getDevices()) { + deviceNos.add(device.getNo()); + } + return deviceNos; + } + + private Collection collectClientNames(TestReportLedgerExcelParser.ParsedLedger ledger) { + Set clientNames = new HashSet(); + for (TestReportLedgerExcelParser.ParsedPointRow point : ledger.getPoints()) { + if (StrUtil.isNotBlank(point.getClientUnitName())) { + clientNames.add(point.getClientUnitName()); + } + } + for (TestReportLedgerExcelParser.ParsedClientRow client : ledger.getClients()) { + clientNames.add(client.getName()); + } + return clientNames; + } + + private Map loadDeviceTypeNameIdMap() { + List typeRows = testDeviceMapper.selectNormalDeviceTypes(); + if (typeRows == null || typeRows.isEmpty()) { + return Collections.emptyMap(); + } + Map result = new HashMap(); + for (TestDeviceTypeNameVO typeRow : typeRows) { + if (typeRow != null && StrUtil.isNotBlank(typeRow.getName()) && StrUtil.isNotBlank(typeRow.getId())) { + result.put(typeRow.getName(), typeRow.getId()); + } + } + return result; + } + + private String resolveDeviceState(String stateName, Integer rowNo) { + String normalizedState = normalizeFileName(stateName); + if ("01".equals(normalizedState) || "在运".equals(normalizedState)) { + return TestDeviceConst.STATE_RUNNING; + } + if ("02".equals(normalizedState) || "退运".equals(normalizedState)) { + return TestDeviceConst.STATE_RETIRED; + } + throw new BusinessException(CommonResponseEnum.FAIL, + "基础资料维护阶段:检测设备sheet第" + rowNo + "行设备状态不正确"); + } + + private String defaultEmpty(String value) { + return value == null ? "" : value; + } + + private String normalizeFileName(String fileName) { + return fileName == null ? null : fileName.trim(); + } + + private static class BaseDataSummary { + private int deviceAddedCount; + private int deviceReuseCount; + private int clientAddedCount; + private int clientReuseCount; + + public int getDeviceAddedCount() { + return deviceAddedCount; + } + + public void setDeviceAddedCount(int deviceAddedCount) { + this.deviceAddedCount = deviceAddedCount; + } + + public int getDeviceReuseCount() { + return deviceReuseCount; + } + + public void setDeviceReuseCount(int deviceReuseCount) { + this.deviceReuseCount = deviceReuseCount; + } + + public int getClientAddedCount() { + return clientAddedCount; + } + + public void setClientAddedCount(int clientAddedCount) { + this.clientAddedCount = clientAddedCount; + } + + public int getClientReuseCount() { + return clientReuseCount; + } + + public void setClientReuseCount(int clientReuseCount) { + this.clientReuseCount = clientReuseCount; + } + } +} diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerTemplateService.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerTemplateService.java new file mode 100644 index 0000000..55ccb79 --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerTemplateService.java @@ -0,0 +1,64 @@ +package com.njcn.gather.aireport.testreport.component; + +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.stereotype.Component; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +@Component +public class TestReportLedgerTemplateService { + + public byte[] buildTemplate() { + try (XSSFWorkbook workbook = new XSSFWorkbook(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + writeGuideSheet(workbook.createSheet(TestReportConst.LEDGER_GUIDE_SHEET_NAME)); + writeHeaders(workbook, workbook.createSheet(TestReportConst.LEDGER_POINT_SHEET_NAME), + Arrays.asList("分组号", "变电站", "监测点名称", "检测设备编号", "委托单位名称", "Excel附件名称", "Word附件名称")); + writeHeaders(workbook, workbook.createSheet(TestReportConst.LEDGER_DEVICE_SHEET_NAME), + Arrays.asList("设备型号", "设备编号", "标准有效期", "设备状态", "标准报告文件名")); + writeHeaders(workbook, workbook.createSheet(TestReportConst.LEDGER_CLIENT_SHEET_NAME), + Arrays.asList("单位名称", "联系人", "联系方式", "地址")); + workbook.write(outputStream); + return outputStream.toByteArray(); + } catch (IOException exception) { + throw new BusinessException(CommonResponseEnum.FAIL, "下载模板失败"); + } + } + + private void writeGuideSheet(XSSFSheet sheet) { + Row row0 = sheet.createRow(0); + row0.createCell(0).setCellValue("1. 汇总文件名固定为台账信息汇总.xlsx"); + Row row1 = sheet.createRow(1); + row1.createCell(0).setCellValue("2. 监测点信息sheet必填,检测设备/委托单位sheet按需填写"); + Row row2 = sheet.createRow(2); + row2.createCell(0).setCellValue("3. 每个监测点必须同时填写Excel附件名称和Word附件名称"); + Row row3 = sheet.createRow(3); + row3.createCell(0).setCellValue("4. 检测设备编号、委托单位名称优先匹配数据库,未命中时再匹配本次sheet"); + sheet.setColumnWidth(0, 120 * 256); + } + + private void writeHeaders(XSSFWorkbook workbook, XSSFSheet sheet, List headers) { + CellStyle style = workbook.createCellStyle(); + Font font = workbook.createFont(); + font.setBold(true); + style.setFont(font); + Row headerRow = sheet.createRow(0); + for (int i = 0; i < headers.size(); i++) { + Cell cell = headerRow.createCell(i); + cell.setCellValue(headers.get(i)); + cell.setCellStyle(style); + sheet.setColumnWidth(i, 24 * 256); + } + } +} diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/component/TestReportPointExcelParser.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/component/TestReportPointExcelParser.java new file mode 100644 index 0000000..7cf9e50 --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/component/TestReportPointExcelParser.java @@ -0,0 +1,108 @@ +package com.njcn.gather.aireport.testreport.component; + +import cn.hutool.core.util.StrUtil; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import lombok.Data; +import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedHashMap; +import java.util.Map; + +@Component +public class TestReportPointExcelParser { + + private static final String NODE_INFO_SHEET_NAME = "节点信息"; + + public ParsedPoint parse(String fileName, InputStream inputStream) { + try (Workbook workbook = WorkbookFactory.create(inputStream)) { + Sheet sheet = workbook.getSheet(NODE_INFO_SHEET_NAME); + if (sheet == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "原始Excel缺少节点信息sheet"); + } + return parseNodeInfoSheet(fileName, sheet); + } catch (BusinessException exception) { + throw exception; + } catch (IOException exception) { + throw new BusinessException(CommonResponseEnum.FAIL, "读取原始Excel失败"); + } catch (Exception exception) { + throw new BusinessException(CommonResponseEnum.FAIL, "解析原始Excel失败"); + } + } + + private ParsedPoint parseNodeInfoSheet(String fileName, Sheet sheet) { + Map keyValueMap = new LinkedHashMap(); + DataFormatter formatter = new DataFormatter(); + for (Row row : sheet) { + short lastCellNum = row.getLastCellNum(); + if (lastCellNum < 2) { + continue; + } + for (int i = 0; i < lastCellNum - 1; i++) { + String key = trim(formatter.formatCellValue(row.getCell(i))); + String value = trim(formatter.formatCellValue(row.getCell(i + 1))); + if (StrUtil.isNotBlank(key) && StrUtil.isNotBlank(value) && !keyValueMap.containsKey(key)) { + keyValueMap.put(key, value); + } + } + } + + ParsedPoint point = new ParsedPoint(); + point.setExcelAttachmentName(fileName); + point.setSubstationName(requireAny(keyValueMap, "变电站", "厂站名称", "变电站名称")); + point.setPointName(requireAny(keyValueMap, "监测点名称", "节点名称", "监测点")); + point.setMonitorTime(optionalAny(keyValueMap, "监测时间", "录波时间", "开始时间")); + point.setVoltageLevel(optionalAny(keyValueMap, "电压等级", "电压")); + point.setPtRatio(optionalAny(keyValueMap, "PT变比", "PT")); + point.setCtRatio(optionalAny(keyValueMap, "CT变比", "CT")); + point.setBaseShortCircuitCapacity(optionalAny(keyValueMap, "基准短路容量")); + point.setMinShortCircuitCapacity(optionalAny(keyValueMap, "最小短路容量")); + point.setProtocolCapacity(optionalAny(keyValueMap, "用户协议容量")); + point.setPowerSupplyCapacity(optionalAny(keyValueMap, "供电设备容量")); + return point; + } + + private String requireAny(Map keyValueMap, String... candidateKeys) { + String value = optionalAny(keyValueMap, candidateKeys); + if (StrUtil.isBlank(value)) { + throw new BusinessException(CommonResponseEnum.FAIL, "节点信息sheet缺少必要字段"); + } + return value; + } + + private String optionalAny(Map keyValueMap, String... candidateKeys) { + for (String candidateKey : candidateKeys) { + String value = keyValueMap.get(candidateKey); + if (StrUtil.isNotBlank(value)) { + return value; + } + } + return null; + } + + private String trim(String value) { + return StrUtil.trimToNull(value); + } + + @Data + public static class ParsedPoint { + private String excelAttachmentName; + private String substationName; + private String pointName; + private String monitorTime; + private String voltageLevel; + private String ptRatio; + private String ctRatio; + private String baseShortCircuitCapacity; + private String minShortCircuitCapacity; + private String protocolCapacity; + private String powerSupplyCapacity; + } +} diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/component/TestReportStorageService.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/component/TestReportStorageService.java new file mode 100644 index 0000000..3d71570 --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/component/TestReportStorageService.java @@ -0,0 +1,88 @@ +package com.njcn.gather.aireport.testreport.component; + +import cn.hutool.core.util.StrUtil; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.UUID; + +@Component +public class TestReportStorageService { + + @Value("${files.path}") + private String filesRootPath; + + public String saveSourceExcel(String reportId, MultipartFile file) { + return saveMultipartFile(reportId, TestReportConst.STORAGE_SOURCE_EXCEL_DIR, file, "保存原始Excel失败"); + } + + public String saveLedgerSummaryExcel(String reportId, MultipartFile file) { + return saveMultipartFile(reportId, TestReportConst.STORAGE_LEDGER_SUMMARY_DIR, file, "保存台账汇总Excel失败"); + } + + public String savePointExcelAttachment(String reportId, MultipartFile file) { + return saveMultipartFile(reportId, TestReportConst.STORAGE_POINT_EXCEL_DIR, file, "保存监测点Excel附件失败"); + } + + public String savePointWordAttachment(String reportId, MultipartFile file) { + return saveMultipartFile(reportId, TestReportConst.STORAGE_POINT_WORD_DIR, file, "保存监测点Word附件失败"); + } + + private String saveMultipartFile(String reportId, String childDir, MultipartFile file, String errorMessage) { + String originalFileName = file == null ? null : file.getOriginalFilename(); + if (file == null || file.isEmpty() || StrUtil.isBlank(originalFileName)) { + throw new BusinessException(CommonResponseEnum.FAIL, "文件不能为空"); + } + String fileName = UUID.randomUUID().toString().replace("-", "") + "-" + Paths.get(originalFileName).getFileName(); + Path targetPath = resolveReportDir(reportId, childDir).resolve(fileName); + try { + Files.createDirectories(targetPath.getParent()); + file.transferTo(targetPath.toFile()); + } catch (IOException exception) { + throw new BusinessException(CommonResponseEnum.FAIL, errorMessage); + } + return buildRelativePath(reportId, childDir, fileName); + } + + public String saveGeneratedPlaceholder(String reportId, String fileName, byte[] content) { + Path targetPath = resolveReportDir(reportId, TestReportConst.STORAGE_GENERATED_REPORT_DIR).resolve(fileName); + try { + Files.createDirectories(targetPath.getParent()); + Files.write(targetPath, content); + } catch (IOException exception) { + throw new BusinessException(CommonResponseEnum.FAIL, "保存生成报告失败"); + } + return buildRelativePath(reportId, TestReportConst.STORAGE_GENERATED_REPORT_DIR, fileName); + } + + public Path resolveBusinessPath(String relativePath) { + return Paths.get(filesRootPath, TestReportConst.STORAGE_DIR).resolve(relativePath).normalize(); + } + + public void deleteQuietly(String relativePath) { + if (StrUtil.isBlank(relativePath)) { + return; + } + try { + Files.deleteIfExists(resolveBusinessPath(relativePath)); + } catch (IOException ignored) { + // 文件清理失败不影响主流程 + } + } + + private Path resolveReportDir(String reportId, String childDir) { + return Paths.get(filesRootPath, TestReportConst.STORAGE_DIR, reportId, childDir).normalize(); + } + + private String buildRelativePath(String reportId, String childDir, String fileName) { + return reportId + "/" + childDir + "/" + fileName; + } +} diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/controller/TestReportController.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/controller/TestReportController.java index c306828..781bc21 100644 --- a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/controller/TestReportController.java +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/controller/TestReportController.java @@ -6,7 +6,11 @@ import com.njcn.common.pojo.constant.OperateType; import com.njcn.common.pojo.enums.common.LogEnum; import com.njcn.common.pojo.enums.response.CommonResponseEnum; import com.njcn.common.pojo.response.HttpResult; +import com.njcn.gather.aireport.testreport.pojo.param.TestReportGroupSaveParam; import com.njcn.gather.aireport.testreport.pojo.param.TestReportParam; +import com.njcn.gather.aireport.testreport.pojo.vo.TestReportGroupReportVO; +import com.njcn.gather.aireport.testreport.pojo.vo.TestReportLedgerImportResultVO; +import com.njcn.gather.aireport.testreport.pojo.vo.TestReportPointVO; import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO; import com.njcn.gather.aireport.testreport.service.TestReportService; import com.njcn.web.controller.BaseController; @@ -23,7 +27,9 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -92,6 +98,13 @@ public class TestReportController extends BaseController { return testReportService.previewTemplate(id); } + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("导出台账导入模板") + @GetMapping("/ledger/template") + public ResponseEntity downloadLedgerTemplate() { + return testReportService.downloadLedgerTemplate(); + } + @OperateInfo(info = LogEnum.BUSINESS_COMMON) @ApiOperation("导出普测报告") @GetMapping("/export") @@ -108,4 +121,63 @@ public class TestReportController extends BaseController { return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, result, methodDescribe); } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE) + @ApiOperation("导入台账汇总与监测点附件") + @PostMapping("/{id}/ledger/import") + public HttpResult importLedger(@PathVariable("id") String id, + @RequestParam("files") MultipartFile[] files) { + String methodDescribe = getMethodDescribe("importLedger"); + TestReportLedgerImportResultVO result = testReportService.importLedger(id, files); + return HttpResultUtil.assembleCommonResponseResult(Boolean.TRUE.equals(result.getSuccess()) + ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, + result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询监测点列表") + @GetMapping("/{id}/point/list") + public HttpResult> listPoints(@PathVariable("id") String id) { + String methodDescribe = getMethodDescribe("listPoints"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, + testReportService.listPoints(id), methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE) + @ApiOperation("保存监测点分组") + @PostMapping("/{id}/group/save") + public HttpResult saveGroups(@PathVariable("id") String id, + @RequestBody @Validated TestReportGroupSaveParam param) { + String methodDescribe = getMethodDescribe("saveGroups"); + boolean result = testReportService.saveGroups(id, param); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, + result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询分组报告结果") + @GetMapping("/{id}/group-report/list") + public HttpResult> listGroupReports(@PathVariable("id") String id) { + String methodDescribe = getMethodDescribe("listGroupReports"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, + testReportService.listGroupReports(id), methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE) + @ApiOperation("按分组生成报告") + @PostMapping("/{id}/generate") + public HttpResult generateGroupedReports(@PathVariable("id") String id) { + String methodDescribe = getMethodDescribe("generateGroupedReports"); + boolean result = testReportService.generateGroupedReports(id); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, + result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("下载分组报告") + @ApiImplicitParam(name = "groupReportId", value = "分组报告结果ID", required = true) + @GetMapping("/group-report/{groupReportId}/download") + public ResponseEntity downloadGroupReport(@PathVariable("groupReportId") String groupReportId) { + return testReportService.downloadGroupReport(groupReportId); + } } diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/mapper/TestReportGroupReportMapper.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/mapper/TestReportGroupReportMapper.java new file mode 100644 index 0000000..99d214f --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/mapper/TestReportGroupReportMapper.java @@ -0,0 +1,9 @@ +package com.njcn.gather.aireport.testreport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njcn.gather.aireport.testreport.pojo.po.TestReportGroupReportPO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TestReportGroupReportMapper extends BaseMapper { +} diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/mapper/TestReportMapper.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/mapper/TestReportMapper.java index bddf674..e5978fe 100644 --- a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/mapper/TestReportMapper.java +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/mapper/TestReportMapper.java @@ -18,6 +18,9 @@ public interface TestReportMapper extends BaseMapper { "r.report_model_id as reportModelId, rm.name as reportModelName, r.create_unit as createUnit,", "r.contract_number as contractNumber, r.standard, r.data, r.phonenumber, r.check_id as checkId,", "checker.name as checkerName, r.check_time as checkTime, r.check_result as checkResult, r.state,", + "r.report_generate_state as reportGenerateState,", + "(select count(1) from ai_testreport_point p where p.report_id = r.id and p.deleted = 0) as pointCount,", + "(select count(1) from ai_testreport_group_report gr where gr.report_id = r.id and gr.deleted = 0) as groupCount,", "r.status, r.create_by as createBy, creator.name as createByName, r.create_time as createTime,", "r.update_by as updateBy, updater.name as updateByName, r.update_time as updateTime", "from ai_testreport r", @@ -52,6 +55,9 @@ public interface TestReportMapper extends BaseMapper { "r.report_model_id as reportModelId, rm.name as reportModelName, r.create_unit as createUnit,", "r.contract_number as contractNumber, r.standard, r.data, r.phonenumber, r.check_id as checkId,", "checker.name as checkerName, r.check_time as checkTime, r.check_result as checkResult, r.state,", + "r.report_generate_state as reportGenerateState,", + "(select count(1) from ai_testreport_point p where p.report_id = r.id and p.deleted = 0) as pointCount,", + "(select count(1) from ai_testreport_group_report gr where gr.report_id = r.id and gr.deleted = 0) as groupCount,", "r.status, r.create_by as createBy, creator.name as createByName, r.create_time as createTime,", "r.update_by as updateBy, updater.name as updateByName, r.update_time as updateTime", "from ai_testreport r", diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/mapper/TestReportPointMapper.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/mapper/TestReportPointMapper.java new file mode 100644 index 0000000..a019844 --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/mapper/TestReportPointMapper.java @@ -0,0 +1,9 @@ +package com.njcn.gather.aireport.testreport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njcn.gather.aireport.testreport.pojo.po.TestReportPointPO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TestReportPointMapper extends BaseMapper { +} diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/constant/TestReportConst.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/constant/TestReportConst.java index ad12e11..118b262 100644 --- a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/constant/TestReportConst.java +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/constant/TestReportConst.java @@ -11,6 +11,31 @@ public final class TestReportConst { public static final Integer STATUS_DELETED = 0; public static final Integer STATUS_NORMAL = 1; + public static final Integer REPORT_GENERATE_STATE_PENDING = 0; + public static final Integer REPORT_GENERATE_STATE_GENERATING = 1; + public static final Integer REPORT_GENERATE_STATE_FINISHED = 2; + + public static final Integer GROUP_REPORT_GENERATE_STATE_PENDING = 0; + public static final Integer GROUP_REPORT_GENERATE_STATE_GENERATING = 1; + public static final Integer GROUP_REPORT_GENERATE_STATE_SUCCESS = 2; + public static final Integer GROUP_REPORT_GENERATE_STATE_FAILED = 3; + + public static final Integer DELETED_NO = 0; + public static final Integer DELETED_YES = 1; + + public static final String STORAGE_DIR = "test-report"; + public static final String STORAGE_SOURCE_EXCEL_DIR = "source-excel"; + public static final String STORAGE_LEDGER_SUMMARY_DIR = "ledger-summary"; + public static final String STORAGE_POINT_EXCEL_DIR = "point-excel"; + public static final String STORAGE_POINT_WORD_DIR = "point-word"; + public static final String STORAGE_GENERATED_REPORT_DIR = "generated-report"; + + public static final String LEDGER_SUMMARY_FILE_NAME = "台账信息汇总.xlsx"; + public static final String LEDGER_GUIDE_SHEET_NAME = "填写说明"; + public static final String LEDGER_POINT_SHEET_NAME = "监测点信息"; + public static final String LEDGER_DEVICE_SHEET_NAME = "检测设备"; + public static final String LEDGER_CLIENT_SHEET_NAME = "委托单位"; + private TestReportConst() { } } diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/param/TestReportGroupSaveParam.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/param/TestReportGroupSaveParam.java new file mode 100644 index 0000000..cfeaa6f --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/param/TestReportGroupSaveParam.java @@ -0,0 +1,29 @@ +package com.njcn.gather.aireport.testreport.pojo.param; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +@Data +public class TestReportGroupSaveParam { + + @ApiModelProperty("分组列表") + @Valid + @NotEmpty(message = "分组列表不能为空") + private List groups; + + @Data + public static class GroupItem { + @ApiModelProperty("组号") + @NotNull(message = "组号不能为空") + private Integer groupNo; + + @ApiModelProperty("监测点ID列表") + @NotEmpty(message = "监测点列表不能为空") + private List pointIds; + } +} diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/param/TestReportParam.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/param/TestReportParam.java index 755bbd1..3170f8e 100644 --- a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/param/TestReportParam.java +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/param/TestReportParam.java @@ -28,6 +28,9 @@ public class TestReportParam { @Data public static class AddParam { + @ApiModelProperty("\u666e\u6d4b\u62a5\u544aID\uff0c\u4e0d\u4f20\u65f6\u7531\u540e\u7aef\u751f\u6210") + private String id; + @ApiModelProperty("\u666e\u6d4b\u62a5\u544a\u7f16\u53f7") @NotBlank(message = "\u666e\u6d4b\u62a5\u544a\u7f16\u53f7\u4e0d\u80fd\u4e3a\u7a7a") @Size(max = 32, message = "\u666e\u6d4b\u62a5\u544a\u7f16\u53f7\u4e0d\u80fd\u8d85\u8fc732\u4e2a\u5b57\u7b26") diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/po/TestReportGroupReportPO.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/po/TestReportGroupReportPO.java new file mode 100644 index 0000000..e0fef9a --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/po/TestReportGroupReportPO.java @@ -0,0 +1,30 @@ +package com.njcn.gather.aireport.testreport.pojo.po; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("ai_testreport_group_report") +public class TestReportGroupReportPO { + + @TableId(type = IdType.INPUT) + private String id; + private String reportId; + private Integer groupNo; + private String reportName; + private String reportFileName; + private String reportStoragePath; + private Integer generateState; + private String generateMessage; + private LocalDateTime generateTime; + private Integer sortNo; + private Integer deleted; + private String createBy; + private LocalDateTime createTime; + private String updateBy; + private LocalDateTime updateTime; +} diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/po/TestReportPO.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/po/TestReportPO.java index 80a6efe..571c984 100644 --- a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/po/TestReportPO.java +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/po/TestReportPO.java @@ -30,6 +30,8 @@ public class TestReportPO implements Serializable { private String standard; @TableField("data") private String data; + @TableField("report_generate_state") + private Integer reportGenerateState; @TableField("phonenumber") private String phonenumber; @TableField("state") diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/po/TestReportPointPO.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/po/TestReportPointPO.java new file mode 100644 index 0000000..b4b89a3 --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/po/TestReportPointPO.java @@ -0,0 +1,41 @@ +package com.njcn.gather.aireport.testreport.pojo.po; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("ai_testreport_point") +public class TestReportPointPO { + + @TableId(type = IdType.INPUT) + private String id; + private String reportId; + private Integer groupNo; + private String substationName; + private String deviceName; + private String pointName; + private String testDeviceNo; + private String clientUnitName; + private String monitorTime; + private String voltageLevel; + private String ptRatio; + private String ctRatio; + private String baseShortCircuitCapacity; + private String minShortCircuitCapacity; + private String protocolCapacity; + private String powerSupplyCapacity; + private String excelAttachmentName; + private String excelStoragePath; + private String wordAttachmentName; + private String wordStoragePath; + private Integer sortNo; + private Integer deleted; + private String createBy; + private LocalDateTime createTime; + private String updateBy; + private LocalDateTime updateTime; +} diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportGroupReportVO.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportGroupReportVO.java new file mode 100644 index 0000000..84dc616 --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportGroupReportVO.java @@ -0,0 +1,19 @@ +package com.njcn.gather.aireport.testreport.pojo.vo; + +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class TestReportGroupReportVO { + + private String id; + private Integer groupNo; + private String reportName; + private String reportFileName; + private String reportStoragePath; + private Integer generateState; + private String generateMessage; + private LocalDateTime generateTime; + private Integer sortNo; +} diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportLedgerImportResultVO.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportLedgerImportResultVO.java new file mode 100644 index 0000000..4a158ff --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportLedgerImportResultVO.java @@ -0,0 +1,25 @@ +package com.njcn.gather.aireport.testreport.pojo.vo; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class TestReportLedgerImportResultVO { + + private String currentStage; + private Boolean success; + private String summaryFileName; + private Integer totalFileCount; + private Integer pointCount; + private Integer excelAttachmentCount; + private Integer wordAttachmentCount; + private Integer deviceAddedCount; + private Integer deviceReuseCount; + private Integer clientAddedCount; + private Integer clientReuseCount; + private Integer groupCount; + private List failReasons = new ArrayList(); + private List stages = new ArrayList(); +} diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportLedgerImportStageVO.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportLedgerImportStageVO.java new file mode 100644 index 0000000..003b560 --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportLedgerImportStageVO.java @@ -0,0 +1,11 @@ +package com.njcn.gather.aireport.testreport.pojo.vo; + +import lombok.Data; + +@Data +public class TestReportLedgerImportStageVO { + + private String stage; + private Boolean success; + private String message; +} diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportPointImportResultVO.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportPointImportResultVO.java new file mode 100644 index 0000000..4968b93 --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportPointImportResultVO.java @@ -0,0 +1,14 @@ +package com.njcn.gather.aireport.testreport.pojo.vo; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class TestReportPointImportResultVO { + + private Integer fileCount; + private Integer pointCount; + private List failReasons = new ArrayList(); +} diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportPointVO.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportPointVO.java new file mode 100644 index 0000000..401e08b --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportPointVO.java @@ -0,0 +1,25 @@ +package com.njcn.gather.aireport.testreport.pojo.vo; + +import lombok.Data; + +@Data +public class TestReportPointVO { + + private String id; + private Integer groupNo; + private String substationName; + private String deviceName; + private String pointName; + private String testDeviceNo; + private String clientUnitName; + private String monitorTime; + private String voltageLevel; + private String ptRatio; + private String ctRatio; + private String baseShortCircuitCapacity; + private String minShortCircuitCapacity; + private String protocolCapacity; + private String powerSupplyCapacity; + private String excelAttachmentName; + private String wordAttachmentName; +} diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportVO.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportVO.java index c979f10..b30e332 100644 --- a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportVO.java +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportVO.java @@ -53,6 +53,12 @@ public class TestReportVO { private String checkResult; @ApiModelProperty("\u62a5\u544a\u72b6\u6001") private String state; + @ApiModelProperty("\u62a5\u544a\u751f\u6210\u72b6\u6001") + private Integer reportGenerateState; + @ApiModelProperty("\u76d1\u6d4b\u70b9\u6570\u91cf") + private Integer pointCount; + @ApiModelProperty("\u5206\u7ec4\u6570\u91cf") + private Integer groupCount; @ApiModelProperty("\u903b\u8f91\u72b6\u6001") private Integer status; @ApiModelProperty("\u521b\u5efa\u4eba\u663e\u793a\u540d") diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/service/TestReportService.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/service/TestReportService.java index a69fdd2..3489f56 100644 --- a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/service/TestReportService.java +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/service/TestReportService.java @@ -2,10 +2,15 @@ package com.njcn.gather.aireport.testreport.service; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.aireport.testreport.pojo.param.TestReportGroupSaveParam; import com.njcn.gather.aireport.testreport.pojo.param.TestReportParam; import com.njcn.gather.aireport.testreport.pojo.po.TestReportPO; +import com.njcn.gather.aireport.testreport.pojo.vo.TestReportGroupReportVO; +import com.njcn.gather.aireport.testreport.pojo.vo.TestReportLedgerImportResultVO; +import com.njcn.gather.aireport.testreport.pojo.vo.TestReportPointVO; import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO; import org.springframework.http.ResponseEntity; +import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -28,4 +33,18 @@ public interface TestReportService extends IService { void exportTestReports(TestReportParam.QueryParam param); ResponseEntity previewTemplate(String id); + + TestReportLedgerImportResultVO importLedger(String id, MultipartFile[] files); + + ResponseEntity downloadLedgerTemplate(); + + List listPoints(String id); + + boolean saveGroups(String id, TestReportGroupSaveParam param); + + List listGroupReports(String id); + + boolean generateGroupedReports(String id); + + ResponseEntity downloadGroupReport(String groupReportId); } diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/service/impl/TestReportServiceImpl.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/service/impl/TestReportServiceImpl.java index b3b48b9..94fa09e 100644 --- a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/service/impl/TestReportServiceImpl.java +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/service/impl/TestReportServiceImpl.java @@ -6,17 +6,30 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.njcn.common.pojo.enums.response.CommonResponseEnum; import com.njcn.common.pojo.exception.BusinessException; import com.njcn.gather.aireport.reportmodel.component.ReportModelFileStorageService; import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO; import com.njcn.gather.aireport.reportmodel.service.ReportModelService; +import com.njcn.gather.aireport.testreport.component.TestReportPointExcelParser; +import com.njcn.gather.aireport.testreport.component.TestReportLedgerImportService; +import com.njcn.gather.aireport.testreport.component.TestReportLedgerTemplateService; +import com.njcn.gather.aireport.testreport.component.TestReportStorageService; +import com.njcn.gather.aireport.testreport.mapper.TestReportGroupReportMapper; import com.njcn.gather.aireport.testreport.mapper.TestReportMapper; +import com.njcn.gather.aireport.testreport.mapper.TestReportPointMapper; import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst; +import com.njcn.gather.aireport.testreport.pojo.param.TestReportGroupSaveParam; import com.njcn.gather.aireport.testreport.pojo.param.TestReportParam; +import com.njcn.gather.aireport.testreport.pojo.po.TestReportGroupReportPO; import com.njcn.gather.aireport.testreport.pojo.po.TestReportPO; +import com.njcn.gather.aireport.testreport.pojo.po.TestReportPointPO; import com.njcn.gather.aireport.testreport.pojo.vo.TestReportExcelVO; +import com.njcn.gather.aireport.testreport.pojo.vo.TestReportGroupReportVO; +import com.njcn.gather.aireport.testreport.pojo.vo.TestReportLedgerImportResultVO; +import com.njcn.gather.aireport.testreport.pojo.vo.TestReportPointVO; import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO; import com.njcn.gather.aireport.testreport.service.TestReportService; import com.njcn.gather.comservice.filepreview.FilePreviewService; @@ -35,7 +48,9 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -49,6 +64,8 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.HashSet; +import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -88,23 +105,50 @@ public class TestReportServiceImpl extends ServiceImpl downloadLedgerTemplate() { + byte[] content = testReportLedgerTemplateService.buildTemplate(); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename*=UTF-8''" + encodeFileName(TestReportConst.LEDGER_SUMMARY_FILE_NAME)) + .contentType(MediaType.parseMediaType( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")) + .body(content); + } + + @Override + public List listPoints(String id) { + requireNormal(id); + return loadNormalPoints(id).stream().map(this::toPointVO).collect(Collectors.toList()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean saveGroups(String id, TestReportGroupSaveParam param) { + TestReportPO report = requireNormal(id); + ensureImportable(report); + List points = loadNormalPoints(id); + validateGroupingPayload(points, param); + String userId = resolveCurrentUserId(); + LocalDateTime now = LocalDateTime.now(); + Map pointGroupMap = buildPointGroupMap(param); + for (TestReportPointPO point : points) { + point.setGroupNo(pointGroupMap.get(point.getId())); + point.setUpdateBy(userId); + point.setUpdateTime(now); + testReportPointMapper.updateById(point); + } + rebuildGroupReports(report, param, userId, now); + report.setData(updateImportSnapshotGroupCount(report.getData(), param.getGroups().size())); + report.setUpdateBy(userId); + report.setUpdateTime(now); + return this.updateById(report); + } + + @Override + public List listGroupReports(String id) { + requireNormal(id); + return loadNormalGroupReports(id).stream().map(this::toGroupReportVO).collect(Collectors.toList()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean generateGroupedReports(String id) { + TestReportPO report = requireNormal(id); + ensureImportable(report); + List groupReports = loadNormalGroupReports(id); + if (groupReports.isEmpty()) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_GROUPS_NOT_READY); + } + report.setReportGenerateState(TestReportConst.REPORT_GENERATE_STATE_GENERATING); + report.setUpdateBy(resolveCurrentUserId()); + report.setUpdateTime(LocalDateTime.now()); + this.updateById(report); + try { + for (TestReportGroupReportPO groupReport : groupReports) { + generateSingleGroupReport(report, groupReport); + } + report.setReportGenerateState(TestReportConst.REPORT_GENERATE_STATE_FINISHED); + report.setUpdateTime(LocalDateTime.now()); + return this.updateById(report); + } catch (RuntimeException exception) { + report.setReportGenerateState(TestReportConst.REPORT_GENERATE_STATE_PENDING); + report.setUpdateTime(LocalDateTime.now()); + this.updateById(report); + throw exception; + } + } + + @Override + public ResponseEntity downloadGroupReport(String groupReportId) { + TestReportGroupReportPO groupReport = requireNormalGroupReport(groupReportId); + if (StrUtil.isBlank(groupReport.getReportStoragePath())) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_TEMPLATE_FILE_NOT_FOUND); + } + try { + Path filePath = testReportStorageService.resolveBusinessPath(groupReport.getReportStoragePath()); + byte[] bytes = Files.readAllBytes(filePath); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename*=UTF-8''" + encodeFileName(groupReport.getReportFileName())) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(bytes); + } catch (Exception exception) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_TEMPLATE_FILE_NOT_FOUND); + } + } + private TestReportParam.QueryParam normalizeQueryParam(TestReportParam.QueryParam param) { TestReportParam.QueryParam query = param == null ? new TestReportParam.QueryParam() : param; query.setKeyword(trimToNull(query.getKeyword())); @@ -296,10 +442,320 @@ public class TestReportServiceImpl extends ServiceImpl normalizeImportFiles(MultipartFile[] files) { + if (files == null || files.length == 0) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_IMPORT_FILES_REQUIRED); + } + List result = new ArrayList(); + Set fileNames = new HashSet(); + for (MultipartFile file : files) { + if (file == null || file.isEmpty()) { + continue; + } + String originalFileName = trimToNull(file.getOriginalFilename()); + if (originalFileName == null) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_IMPORT_FILES_REQUIRED); + } + String normalizedFileName = originalFileName.toLowerCase(); + if (!normalizedFileName.endsWith(".xls") && !normalizedFileName.endsWith(".xlsx")) { + throw new BusinessException(CommonResponseEnum.FAIL, "原始Excel仅允许xls或xlsx格式"); + } + if (!fileNames.add(originalFileName)) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_IMPORT_FILE_NAME_DUPLICATE); + } + result.add(file); + } + if (result.isEmpty()) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_IMPORT_FILES_REQUIRED); + } + return result; + } + + private List parseAllPoints(List files) { + List result = new ArrayList(); + for (MultipartFile file : files) { + try { + result.add(testReportPointExcelParser.parse(file.getOriginalFilename(), file.getInputStream())); + } catch (IOException exception) { + throw new BusinessException(CommonResponseEnum.FAIL, "读取原始Excel失败"); + } + } + return result; + } + + private void overwriteReportPoints(TestReportPO report, List files, + List parsedPoints) { + List oldPoints = loadNormalPoints(report.getId()); + List oldGroupReports = loadNormalGroupReports(report.getId()); + deletePointFiles(oldPoints); + deleteGroupReportFiles(oldGroupReports); + removeOldRows(report.getId()); + + LocalDateTime now = LocalDateTime.now(); + String userId = resolveCurrentUserId(); + for (int i = 0; i < files.size(); i++) { + MultipartFile file = files.get(i); + TestReportPointExcelParser.ParsedPoint parsedPoint = parsedPoints.get(i); + String storagePath = testReportStorageService.saveSourceExcel(report.getId(), file); + TestReportPointPO point = new TestReportPointPO(); + point.setId(UUID.randomUUID().toString()); + point.setReportId(report.getId()); + point.setGroupNo(null); + point.setSubstationName(parsedPoint.getSubstationName()); + point.setPointName(parsedPoint.getPointName()); + point.setMonitorTime(parsedPoint.getMonitorTime()); + point.setVoltageLevel(parsedPoint.getVoltageLevel()); + point.setPtRatio(parsedPoint.getPtRatio()); + point.setCtRatio(parsedPoint.getCtRatio()); + point.setBaseShortCircuitCapacity(parsedPoint.getBaseShortCircuitCapacity()); + point.setMinShortCircuitCapacity(parsedPoint.getMinShortCircuitCapacity()); + point.setProtocolCapacity(parsedPoint.getProtocolCapacity()); + point.setPowerSupplyCapacity(parsedPoint.getPowerSupplyCapacity()); + point.setExcelAttachmentName(parsedPoint.getExcelAttachmentName()); + point.setExcelStoragePath(storagePath); + point.setSortNo(i + 1); + point.setDeleted(TestReportConst.DELETED_NO); + point.setCreateBy(userId); + point.setCreateTime(now); + point.setUpdateBy(userId); + point.setUpdateTime(now); + testReportPointMapper.insert(point); + } + report.setData(buildImportSnapshotJson(parsedPoints.size(), 0)); + report.setReportGenerateState(TestReportConst.REPORT_GENERATE_STATE_PENDING); + report.setUpdateBy(userId); + report.setUpdateTime(now); + this.updateById(report); + } + + private void deletePointFiles(List oldPoints) { + for (TestReportPointPO oldPoint : oldPoints) { + testReportStorageService.deleteQuietly(oldPoint.getExcelStoragePath()); + } + } + + private void deleteGroupReportFiles(List oldGroupReports) { + for (TestReportGroupReportPO oldGroupReport : oldGroupReports) { + testReportStorageService.deleteQuietly(oldGroupReport.getReportStoragePath()); + } + } + + private void removeOldRows(String reportId) { + LambdaQueryWrapper pointWrapper = new LambdaQueryWrapper(); + pointWrapper.eq(TestReportPointPO::getReportId, reportId); + testReportPointMapper.delete(pointWrapper); + + LambdaQueryWrapper groupReportWrapper = new LambdaQueryWrapper(); + groupReportWrapper.eq(TestReportGroupReportPO::getReportId, reportId); + testReportGroupReportMapper.delete(groupReportWrapper); + } + + private String buildImportSnapshotJson(int pointCount, int groupCount) { + ObjectNode root = OBJECT_MAPPER.createObjectNode(); + root.put("version", "1.0"); + root.put("importMode", "multi-source-excel-auto-parse"); + root.put("fileCount", pointCount); + root.put("pointCount", pointCount); + root.put("groupCount", groupCount); + return root.toString(); + } + + private String updateImportSnapshotGroupCount(String data, int groupCount) { + try { + JsonNode jsonNode = StrUtil.isBlank(data) ? OBJECT_MAPPER.createObjectNode() : OBJECT_MAPPER.readTree(data); + ObjectNode root = jsonNode instanceof ObjectNode ? (ObjectNode) jsonNode : OBJECT_MAPPER.createObjectNode(); + if (!root.has("version")) { + root.put("version", "1.0"); + } + if (!root.has("importMode")) { + root.put("importMode", "multi-source-excel-auto-parse"); + } + root.put("groupCount", groupCount); + return root.toString(); + } catch (Exception exception) { + return buildImportSnapshotJson(loadNormalPointsCountFromData(data), groupCount); + } + } + + private int loadNormalPointsCountFromData(String data) { + try { + if (StrUtil.isBlank(data)) { + return 0; + } + JsonNode jsonNode = OBJECT_MAPPER.readTree(data); + JsonNode pointCountNode = jsonNode.get("pointCount"); + return pointCountNode == null ? 0 : pointCountNode.asInt(0); + } catch (Exception exception) { + return 0; + } + } + + private List loadNormalPoints(String reportId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper(); + wrapper.eq(TestReportPointPO::getReportId, reportId) + .eq(TestReportPointPO::getDeleted, TestReportConst.DELETED_NO) + .orderByAsc(TestReportPointPO::getSortNo) + .orderByAsc(TestReportPointPO::getCreateTime); + return testReportPointMapper.selectList(wrapper); + } + + private List loadNormalGroupReports(String reportId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper(); + wrapper.eq(TestReportGroupReportPO::getReportId, reportId) + .eq(TestReportGroupReportPO::getDeleted, TestReportConst.DELETED_NO) + .orderByAsc(TestReportGroupReportPO::getSortNo) + .orderByAsc(TestReportGroupReportPO::getGroupNo); + return testReportGroupReportMapper.selectList(wrapper); + } + + private TestReportGroupReportPO requireNormalGroupReport(String groupReportId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper(); + wrapper.eq(TestReportGroupReportPO::getId, requireText(groupReportId, MSG_ID_REQUIRED)) + .eq(TestReportGroupReportPO::getDeleted, TestReportConst.DELETED_NO); + TestReportGroupReportPO groupReport = testReportGroupReportMapper.selectOne(wrapper); + if (groupReport == null) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_GROUP_REPORT_NOT_FOUND); + } + return groupReport; + } + + private void validateGroupingPayload(List points, TestReportGroupSaveParam param) { + if (param == null || param.getGroups() == null || param.getGroups().isEmpty()) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_GROUPS_REQUIRED); + } + Set pointIds = points.stream().map(TestReportPointPO::getId).collect(Collectors.toSet()); + Set payloadPointIds = new HashSet(); + for (TestReportGroupSaveParam.GroupItem group : param.getGroups()) { + if (group.getGroupNo() == null || group.getGroupNo() <= 0) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_GROUP_NO_INVALID); + } + for (String pointId : group.getPointIds()) { + if (!payloadPointIds.add(pointId)) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_GROUP_POINT_DUPLICATE); + } + } + } + if (!payloadPointIds.equals(pointIds)) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_GROUP_POINT_MISSING); + } + } + + private Map buildPointGroupMap(TestReportGroupSaveParam param) { + Map result = new LinkedHashMap(); + for (TestReportGroupSaveParam.GroupItem group : param.getGroups()) { + for (String pointId : group.getPointIds()) { + result.put(pointId, group.getGroupNo()); + } + } + return result; + } + + private void rebuildGroupReports(TestReportPO report, TestReportGroupSaveParam param, String userId, LocalDateTime now) { + deleteGroupReportFiles(loadNormalGroupReports(report.getId())); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper(); + wrapper.eq(TestReportGroupReportPO::getReportId, report.getId()); + testReportGroupReportMapper.delete(wrapper); + int sortNo = 1; + for (TestReportGroupSaveParam.GroupItem group : param.getGroups()) { + TestReportGroupReportPO groupReport = new TestReportGroupReportPO(); + groupReport.setId(UUID.randomUUID().toString()); + groupReport.setReportId(report.getId()); + groupReport.setGroupNo(group.getGroupNo()); + groupReport.setGenerateState(TestReportConst.GROUP_REPORT_GENERATE_STATE_PENDING); + groupReport.setSortNo(sortNo++); + groupReport.setDeleted(TestReportConst.DELETED_NO); + groupReport.setCreateBy(userId); + groupReport.setCreateTime(now); + groupReport.setUpdateBy(userId); + groupReport.setUpdateTime(now); + testReportGroupReportMapper.insert(groupReport); + } + } + + private void generateSingleGroupReport(TestReportPO report, TestReportGroupReportPO groupReport) { + try { + groupReport.setGenerateState(TestReportConst.GROUP_REPORT_GENERATE_STATE_GENERATING); + groupReport.setGenerateMessage(null); + groupReport.setUpdateBy(resolveCurrentUserId()); + groupReport.setUpdateTime(LocalDateTime.now()); + testReportGroupReportMapper.updateById(groupReport); + + String reportName = report.getNo() + "-第" + groupReport.getGroupNo() + "组检测报告"; + String reportFileName = ExportFileNameUtil.appendToday(reportName + ".docx"); + String relativePath = testReportStorageService.saveGeneratedPlaceholder(report.getId(), reportFileName, + ("group:" + groupReport.getGroupNo()).getBytes(StandardCharsets.UTF_8)); + groupReport.setReportName(reportName); + groupReport.setReportFileName(reportFileName); + groupReport.setReportStoragePath(relativePath); + groupReport.setGenerateState(TestReportConst.GROUP_REPORT_GENERATE_STATE_SUCCESS); + groupReport.setGenerateTime(LocalDateTime.now()); + groupReport.setUpdateTime(LocalDateTime.now()); + testReportGroupReportMapper.updateById(groupReport); + } catch (RuntimeException exception) { + groupReport.setGenerateState(TestReportConst.GROUP_REPORT_GENERATE_STATE_FAILED); + groupReport.setGenerateMessage(exception.getMessage()); + groupReport.setUpdateTime(LocalDateTime.now()); + testReportGroupReportMapper.updateById(groupReport); + throw exception; + } + } + + private TestReportPointVO toPointVO(TestReportPointPO point) { + TestReportPointVO vo = new TestReportPointVO(); + vo.setId(point.getId()); + vo.setGroupNo(point.getGroupNo()); + vo.setSubstationName(defaultDash(point.getSubstationName())); + vo.setDeviceName(defaultDash(point.getDeviceName())); + vo.setPointName(defaultDash(point.getPointName())); + vo.setTestDeviceNo(defaultDash(point.getTestDeviceNo())); + vo.setClientUnitName(defaultDash(point.getClientUnitName())); + vo.setMonitorTime(defaultDash(point.getMonitorTime())); + vo.setVoltageLevel(defaultDash(point.getVoltageLevel())); + vo.setPtRatio(defaultDash(point.getPtRatio())); + vo.setCtRatio(defaultDash(point.getCtRatio())); + vo.setBaseShortCircuitCapacity(defaultDash(point.getBaseShortCircuitCapacity())); + vo.setMinShortCircuitCapacity(defaultDash(point.getMinShortCircuitCapacity())); + vo.setProtocolCapacity(defaultDash(point.getProtocolCapacity())); + vo.setPowerSupplyCapacity(defaultDash(point.getPowerSupplyCapacity())); + vo.setExcelAttachmentName(defaultDash(point.getExcelAttachmentName())); + vo.setWordAttachmentName(defaultDash(point.getWordAttachmentName())); + return vo; + } + + private TestReportGroupReportVO toGroupReportVO(TestReportGroupReportPO groupReport) { + TestReportGroupReportVO vo = new TestReportGroupReportVO(); + vo.setId(groupReport.getId()); + vo.setGroupNo(groupReport.getGroupNo()); + vo.setReportName(defaultDash(groupReport.getReportName())); + vo.setReportFileName(defaultDash(groupReport.getReportFileName())); + vo.setReportStoragePath(defaultDash(groupReport.getReportStoragePath())); + vo.setGenerateState(groupReport.getGenerateState()); + vo.setGenerateMessage(defaultDash(groupReport.getGenerateMessage())); + vo.setGenerateTime(groupReport.getGenerateTime()); + vo.setSortNo(groupReport.getSortNo()); + return vo; + } + + private String defaultDash(String value) { + return StrUtil.isBlank(value) ? "-" : value; + } + private void checkNoUnique(String no, String excludeId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper(); wrapper.eq(TestReportPO::getNo, no) diff --git a/ai-report/test-report/src/main/resources/com/njcn/gather/aireport/testreport/mapper/mapping/TestReportGroupReportMapper.xml b/ai-report/test-report/src/main/resources/com/njcn/gather/aireport/testreport/mapper/mapping/TestReportGroupReportMapper.xml new file mode 100644 index 0000000..d6b63fd --- /dev/null +++ b/ai-report/test-report/src/main/resources/com/njcn/gather/aireport/testreport/mapper/mapping/TestReportGroupReportMapper.xml @@ -0,0 +1,4 @@ + + + + diff --git a/ai-report/test-report/src/main/resources/com/njcn/gather/aireport/testreport/mapper/mapping/TestReportPointMapper.xml b/ai-report/test-report/src/main/resources/com/njcn/gather/aireport/testreport/mapper/mapping/TestReportPointMapper.xml new file mode 100644 index 0000000..fe82e3c --- /dev/null +++ b/ai-report/test-report/src/main/resources/com/njcn/gather/aireport/testreport/mapper/mapping/TestReportPointMapper.xml @@ -0,0 +1,4 @@ + + + + diff --git a/ai-report/test-report/src/test/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerExcelParserTest.java b/ai-report/test-report/src/test/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerExcelParserTest.java new file mode 100644 index 0000000..d36a1fa --- /dev/null +++ b/ai-report/test-report/src/test/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerExcelParserTest.java @@ -0,0 +1,153 @@ +package com.njcn.gather.aireport.testreport.component; + +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import java.io.ByteArrayOutputStream; +import java.util.LinkedHashMap; +import java.util.Map; + +class TestReportLedgerExcelParserTest { + + @Test + void shouldParseValidLedgerWorkbook() throws Exception { + TestReportLedgerExcelParser parser = new TestReportLedgerExcelParser(); + MockMultipartFile summaryFile = new MockMultipartFile("files", TestReportConst.LEDGER_SUMMARY_FILE_NAME, + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buildWorkbookBytes(true)); + Map attachments = new LinkedHashMap(); + attachments.put("point-a.xlsx", new MockMultipartFile("files", "point-a.xlsx", "application/octet-stream", new byte[]{1})); + attachments.put("point-a.docx", new MockMultipartFile("files", "point-a.docx", "application/octet-stream", new byte[]{1})); + + TestReportLedgerExcelParser.ParsedLedger ledger = parser.parse(summaryFile, attachments); + + Assertions.assertEquals(1, ledger.getPoints().size()); + Assertions.assertEquals(1, ledger.getDevices().size()); + Assertions.assertEquals(1, ledger.getClients().size()); + Assertions.assertEquals("点位A", ledger.getPoints().get(0).getPointName()); + } + + @Test + void shouldAllowSummaryFileNameWithSurroundingSpaces() throws Exception { + TestReportLedgerExcelParser parser = new TestReportLedgerExcelParser(); + MockMultipartFile summaryFile = new MockMultipartFile("files", " " + TestReportConst.LEDGER_SUMMARY_FILE_NAME + " ", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buildWorkbookBytes(true)); + Map attachments = new LinkedHashMap(); + attachments.put("point-a.xlsx", new MockMultipartFile("files", "point-a.xlsx", "application/octet-stream", new byte[]{1})); + attachments.put("point-a.docx", new MockMultipartFile("files", "point-a.docx", "application/octet-stream", new byte[]{1})); + + TestReportLedgerExcelParser.ParsedLedger ledger = parser.parse(summaryFile, attachments); + + Assertions.assertEquals(TestReportConst.LEDGER_SUMMARY_FILE_NAME, ledger.getSummaryFileName()); + Assertions.assertEquals(1, ledger.getPoints().size()); + } + + @Test + void shouldRejectMissingPointSheet() throws Exception { + TestReportLedgerExcelParser parser = new TestReportLedgerExcelParser(); + MockMultipartFile summaryFile = new MockMultipartFile("files", TestReportConst.LEDGER_SUMMARY_FILE_NAME, + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buildWorkbookBytes(false)); + + BusinessException exception = Assertions.assertThrows(BusinessException.class, + () -> parser.parse(summaryFile, new LinkedHashMap())); + + Assertions.assertTrue(exception.getMessage().contains("监测点信息")); + } + + @Test + void shouldRejectDuplicatePointBySubstationDeviceAndPointName() throws Exception { + TestReportLedgerExcelParser parser = new TestReportLedgerExcelParser(); + MockMultipartFile summaryFile = new MockMultipartFile("files", TestReportConst.LEDGER_SUMMARY_FILE_NAME, + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buildDuplicatePointWorkbookBytes()); + Map attachments = new LinkedHashMap(); + attachments.put("point-a.xlsx", new MockMultipartFile("files", "point-a.xlsx", "application/octet-stream", new byte[]{1})); + attachments.put("point-a.docx", new MockMultipartFile("files", "point-a.docx", "application/octet-stream", new byte[]{1})); + attachments.put("point-b.xlsx", new MockMultipartFile("files", "point-b.xlsx", "application/octet-stream", new byte[]{1})); + attachments.put("point-b.docx", new MockMultipartFile("files", "point-b.docx", "application/octet-stream", new byte[]{1})); + + BusinessException exception = Assertions.assertThrows(BusinessException.class, + () -> parser.parse(summaryFile, attachments)); + + Assertions.assertTrue(exception.getMessage().contains("重复点位")); + } + + private byte[] buildWorkbookBytes(boolean includePointSheet) throws Exception { + try (XSSFWorkbook workbook = new XSSFWorkbook(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + workbook.createSheet(TestReportConst.LEDGER_GUIDE_SHEET_NAME); + if (includePointSheet) { + XSSFSheet pointSheet = workbook.createSheet(TestReportConst.LEDGER_POINT_SHEET_NAME); + pointSheet.createRow(0).createCell(0).setCellValue("分组号"); + pointSheet.getRow(0).createCell(1).setCellValue("变电站"); + pointSheet.getRow(0).createCell(2).setCellValue("监测点名称"); + pointSheet.getRow(0).createCell(3).setCellValue("检测设备编号"); + pointSheet.getRow(0).createCell(4).setCellValue("委托单位名称"); + pointSheet.getRow(0).createCell(5).setCellValue("Excel附件名称"); + pointSheet.getRow(0).createCell(6).setCellValue("Word附件名称"); + pointSheet.createRow(1).createCell(0).setCellValue("1"); + pointSheet.getRow(1).createCell(1).setCellValue("站点A"); + pointSheet.getRow(1).createCell(2).setCellValue("点位A"); + pointSheet.getRow(1).createCell(3).setCellValue("DEV-001"); + pointSheet.getRow(1).createCell(4).setCellValue("单位A"); + pointSheet.getRow(1).createCell(5).setCellValue("point-a.xlsx"); + pointSheet.getRow(1).createCell(6).setCellValue("point-a.docx"); + } + XSSFSheet deviceSheet = workbook.createSheet(TestReportConst.LEDGER_DEVICE_SHEET_NAME); + deviceSheet.createRow(0).createCell(0).setCellValue("设备型号"); + deviceSheet.getRow(0).createCell(1).setCellValue("设备编号"); + deviceSheet.getRow(0).createCell(2).setCellValue("标准有效期"); + deviceSheet.getRow(0).createCell(3).setCellValue("设备状态"); + deviceSheet.getRow(0).createCell(4).setCellValue("标准报告文件名"); + deviceSheet.createRow(1).createCell(0).setCellValue("型号A"); + deviceSheet.getRow(1).createCell(1).setCellValue("DEV-001"); + deviceSheet.getRow(1).createCell(2).setCellValue("2026-07-01"); + deviceSheet.getRow(1).createCell(3).setCellValue("在运"); + deviceSheet.getRow(1).createCell(4).setCellValue("report-a.pdf"); + XSSFSheet clientSheet = workbook.createSheet(TestReportConst.LEDGER_CLIENT_SHEET_NAME); + clientSheet.createRow(0).createCell(0).setCellValue("单位名称"); + clientSheet.getRow(0).createCell(1).setCellValue("联系人"); + clientSheet.getRow(0).createCell(2).setCellValue("联系方式"); + clientSheet.getRow(0).createCell(3).setCellValue("地址"); + clientSheet.createRow(1).createCell(0).setCellValue("单位A"); + clientSheet.getRow(1).createCell(1).setCellValue("张三"); + clientSheet.getRow(1).createCell(2).setCellValue("13800000000"); + clientSheet.getRow(1).createCell(3).setCellValue("南京"); + workbook.write(outputStream); + return outputStream.toByteArray(); + } + } + + private byte[] buildDuplicatePointWorkbookBytes() throws Exception { + try (XSSFWorkbook workbook = new XSSFWorkbook(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + workbook.createSheet(TestReportConst.LEDGER_GUIDE_SHEET_NAME); + XSSFSheet pointSheet = workbook.createSheet(TestReportConst.LEDGER_POINT_SHEET_NAME); + pointSheet.createRow(0).createCell(0).setCellValue("分组号"); + pointSheet.getRow(0).createCell(1).setCellValue("变电站"); + pointSheet.getRow(0).createCell(2).setCellValue("监测点名称"); + pointSheet.getRow(0).createCell(3).setCellValue("检测设备编号"); + pointSheet.getRow(0).createCell(4).setCellValue("委托单位名称"); + pointSheet.getRow(0).createCell(5).setCellValue("Excel附件名称"); + pointSheet.getRow(0).createCell(6).setCellValue("Word附件名称"); + pointSheet.createRow(1).createCell(0).setCellValue("1"); + pointSheet.getRow(1).createCell(1).setCellValue("站点A"); + pointSheet.getRow(1).createCell(2).setCellValue("点位A"); + pointSheet.getRow(1).createCell(3).setCellValue("DEV-001"); + pointSheet.getRow(1).createCell(4).setCellValue("单位A"); + pointSheet.getRow(1).createCell(5).setCellValue("point-a.xlsx"); + pointSheet.getRow(1).createCell(6).setCellValue("point-a.docx"); + pointSheet.createRow(2).createCell(0).setCellValue("2"); + pointSheet.getRow(2).createCell(1).setCellValue("站点A"); + pointSheet.getRow(2).createCell(2).setCellValue("点位A"); + pointSheet.getRow(2).createCell(3).setCellValue("DEV-002"); + pointSheet.getRow(2).createCell(4).setCellValue("单位B"); + pointSheet.getRow(2).createCell(5).setCellValue("point-b.xlsx"); + pointSheet.getRow(2).createCell(6).setCellValue("point-b.docx"); + workbook.write(outputStream); + return outputStream.toByteArray(); + } + } +} diff --git a/ai-report/test-report/src/test/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerImportServiceTest.java b/ai-report/test-report/src/test/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerImportServiceTest.java new file mode 100644 index 0000000..b8b8c1b --- /dev/null +++ b/ai-report/test-report/src/test/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerImportServiceTest.java @@ -0,0 +1,104 @@ +package com.njcn.gather.aireport.testreport.component; + +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.aireport.clientunit.mapper.ClientUnitMapper; +import com.njcn.gather.aireport.testdevice.mapper.TestDeviceMapper; +import com.njcn.gather.aireport.testdevice.pojo.constant.TestDeviceConst; +import com.njcn.gather.aireport.testreport.mapper.TestReportGroupReportMapper; +import com.njcn.gather.aireport.testreport.mapper.TestReportMapper; +import com.njcn.gather.aireport.testreport.mapper.TestReportPointMapper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map; + +import static org.mockito.Mockito.mock; + +class TestReportLedgerImportServiceTest { + + @Test + void normalizeFileMapShouldRejectEmptyFiles() { + TestReportLedgerImportService service = createService(); + + BusinessException exception = Assertions.assertThrows(BusinessException.class, + () -> invokeNormalizeFileMap(service, new MultipartFile[0])); + + Assertions.assertEquals("选择文件阶段:导入文件不能为空", exception.getMessage()); + } + + @Test + void resolveDeviceStateShouldSupportCodesAndNames() { + TestReportLedgerImportService service = createService(); + + Assertions.assertEquals(TestDeviceConst.STATE_RUNNING, invokeResolveDeviceState(service, "01", 2)); + Assertions.assertEquals(TestDeviceConst.STATE_RUNNING, invokeResolveDeviceState(service, "在运", 2)); + Assertions.assertEquals(TestDeviceConst.STATE_RETIRED, invokeResolveDeviceState(service, "02", 3)); + Assertions.assertEquals(TestDeviceConst.STATE_RETIRED, invokeResolveDeviceState(service, "退运", 3)); + } + + @Test + void resolveDeviceStateShouldRejectUnexpectedState() { + TestReportLedgerImportService service = createService(); + + BusinessException exception = Assertions.assertThrows(BusinessException.class, + () -> invokeResolveDeviceState(service, "停用", 4)); + + Assertions.assertEquals("基础资料维护阶段:检测设备sheet第4行设备状态不正确", exception.getMessage()); + } + + private TestReportLedgerImportService createService() { + return new TestReportLedgerImportService( + mock(TestReportMapper.class), + mock(TestReportPointMapper.class), + mock(TestReportGroupReportMapper.class), + mock(TestDeviceMapper.class), + mock(ClientUnitMapper.class), + mock(TestReportLedgerExcelParser.class), + mock(TestReportStorageService.class)); + } + + @SuppressWarnings("unchecked") + private Map invokeNormalizeFileMap(TestReportLedgerImportService service, MultipartFile[] files) { + return invokePrivateMethod(service, "normalizeFileMap", new Class[]{MultipartFile[].class}, new Object[]{files}); + } + + private String invokeResolveDeviceState(TestReportLedgerImportService service, String stateName, Integer rowNo) { + return (String) invokePrivateMethod(service, "resolveDeviceState", + new Class[]{String.class, Integer.class}, new Object[]{stateName, rowNo}); + } + + private Object invokePrivateMethod(TestReportLedgerImportService service, String methodName, + Class[] parameterTypes, Object[] args) { + try { + Method method = TestReportLedgerImportService.class.getDeclaredMethod(methodName, parameterTypes); + method.setAccessible(true); + return method.invoke(service, args); + } catch (InvocationTargetException exception) { + Throwable targetException = exception.getTargetException(); + if (targetException instanceof RuntimeException) { + throw (RuntimeException) targetException; + } + throw new RuntimeException(targetException); + } catch (Exception exception) { + throw new RuntimeException(exception); + } + } + + @Test + void normalizeFileMapShouldSkipEmptyFileItemsAndKeepTrimmedNames() { + TestReportLedgerImportService service = createService(); + MockMultipartFile validFile = new MockMultipartFile("files", " point-a.xlsx ", "application/octet-stream", + new byte[]{1}); + MockMultipartFile emptyFile = new MockMultipartFile("files", "empty.xlsx", "application/octet-stream", + new byte[0]); + + Map result = invokeNormalizeFileMap(service, new MultipartFile[]{emptyFile, validFile}); + + Assertions.assertEquals(1, result.size()); + Assertions.assertSame(validFile, result.get("point-a.xlsx")); + } +} diff --git a/ai-report/test-report/src/test/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerTemplateServiceTest.java b/ai-report/test-report/src/test/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerTemplateServiceTest.java new file mode 100644 index 0000000..814c039 --- /dev/null +++ b/ai-report/test-report/src/test/java/com/njcn/gather/aireport/testreport/component/TestReportLedgerTemplateServiceTest.java @@ -0,0 +1,34 @@ +package com.njcn.gather.aireport.testreport.component; + +import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.util.Arrays; +import java.util.List; + +class TestReportLedgerTemplateServiceTest { + + @Test + void shouldBuildTemplateWithRequiredSheets() throws Exception { + TestReportLedgerTemplateService service = new TestReportLedgerTemplateService(); + + byte[] content = service.buildTemplate(); + + try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(content))) { + Assertions.assertNotNull(workbook.getSheet(TestReportConst.LEDGER_GUIDE_SHEET_NAME)); + Assertions.assertNotNull(workbook.getSheet(TestReportConst.LEDGER_POINT_SHEET_NAME)); + Assertions.assertNotNull(workbook.getSheet(TestReportConst.LEDGER_DEVICE_SHEET_NAME)); + Assertions.assertNotNull(workbook.getSheet(TestReportConst.LEDGER_CLIENT_SHEET_NAME)); + List expectedHeaders = Arrays.asList( + "分组号", "变电站", "监测点名称", "检测设备编号", "委托单位名称", "Excel附件名称", "Word附件名称"); + for (int i = 0; i < expectedHeaders.size(); i++) { + Assertions.assertEquals(expectedHeaders.get(i), + workbook.getSheet(TestReportConst.LEDGER_POINT_SHEET_NAME).getRow(0).getCell(i).getStringCellValue()); + } + Assertions.assertNull(workbook.getSheet(TestReportConst.LEDGER_POINT_SHEET_NAME).getRow(0).getCell(expectedHeaders.size())); + } + } +} diff --git a/ai-report/test-report/src/test/java/com/njcn/gather/aireport/testreport/service/impl/TestReportServiceImplTest.java b/ai-report/test-report/src/test/java/com/njcn/gather/aireport/testreport/service/impl/TestReportServiceImplTest.java index 6d1d008..ff1f508 100644 --- a/ai-report/test-report/src/test/java/com/njcn/gather/aireport/testreport/service/impl/TestReportServiceImplTest.java +++ b/ai-report/test-report/src/test/java/com/njcn/gather/aireport/testreport/service/impl/TestReportServiceImplTest.java @@ -3,9 +3,19 @@ package com.njcn.gather.aireport.testreport.service.impl; import com.njcn.common.pojo.exception.BusinessException; import com.njcn.gather.aireport.reportmodel.component.ReportModelFileStorageService; import com.njcn.gather.aireport.reportmodel.service.ReportModelService; +import com.njcn.gather.aireport.testreport.component.TestReportLedgerImportService; +import com.njcn.gather.aireport.testreport.component.TestReportLedgerTemplateService; +import com.njcn.gather.aireport.testreport.component.TestReportPointExcelParser; +import com.njcn.gather.aireport.testreport.component.TestReportStorageService; +import com.njcn.gather.aireport.testreport.mapper.TestReportGroupReportMapper; +import com.njcn.gather.aireport.testreport.mapper.TestReportMapper; +import com.njcn.gather.aireport.testreport.mapper.TestReportPointMapper; import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst; import com.njcn.gather.aireport.testreport.pojo.param.TestReportParam; +import com.njcn.gather.aireport.testreport.pojo.po.TestReportGroupReportPO; import com.njcn.gather.aireport.testreport.pojo.po.TestReportPO; +import com.njcn.gather.aireport.testreport.pojo.po.TestReportPointPO; +import com.njcn.gather.aireport.testreport.pojo.vo.TestReportPointVO; import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO; import com.njcn.gather.comservice.filepreview.FilePreviewService; import com.njcn.gather.system.dictionary.pojo.po.DictData; @@ -15,12 +25,18 @@ import com.njcn.gather.system.dictionary.service.IDictTypeService; import com.njcn.gather.system.pojo.constant.DictConst; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; +import java.io.InputStream; import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.UUID; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -30,6 +46,79 @@ class TestReportServiceImplTest { private static final Integer DICT_STATE_NORMAL = 1; private static final Integer DICT_STATE_DELETED = 0; + @Test + void constantsShouldExposeGenerateStates() { + Assertions.assertEquals(Integer.valueOf(0), TestReportConst.REPORT_GENERATE_STATE_PENDING); + Assertions.assertEquals(Integer.valueOf(1), TestReportConst.REPORT_GENERATE_STATE_GENERATING); + Assertions.assertEquals(Integer.valueOf(2), TestReportConst.REPORT_GENERATE_STATE_FINISHED); + Assertions.assertEquals(Integer.valueOf(3), TestReportConst.GROUP_REPORT_GENERATE_STATE_FAILED); + } + + @Test + void pointPoShouldStoreParsedFields() { + TestReportPointPO point = new TestReportPointPO(); + point.setSubstationName("220kV城南站"); + point.setPointName("1号监测点"); + point.setVoltageLevel("10kV"); + + Assertions.assertEquals("220kV城南站", point.getSubstationName()); + Assertions.assertEquals("1号监测点", point.getPointName()); + Assertions.assertEquals("10kV", point.getVoltageLevel()); + } + + @Test + void groupReportPoShouldStoreResultFields() { + TestReportGroupReportPO groupReport = new TestReportGroupReportPO(); + groupReport.setGroupNo(1); + groupReport.setReportName("第1组检测报告"); + groupReport.setGenerateState(TestReportConst.GROUP_REPORT_GENERATE_STATE_PENDING); + + Assertions.assertEquals(Integer.valueOf(1), groupReport.getGroupNo()); + Assertions.assertEquals("第1组检测报告", groupReport.getReportName()); + Assertions.assertEquals(TestReportConst.GROUP_REPORT_GENERATE_STATE_PENDING, groupReport.getGenerateState()); + } + + @Test + void serviceShouldExposeGroupedApis() throws Exception { + Assertions.assertNotNull(TestReportServiceImpl.class.getMethod("listTestReports", TestReportParam.QueryParam.class)); + Assertions.assertNotNull(TestReportServiceImpl.class.getMethod("importLedger", String.class, MultipartFile[].class)); + Assertions.assertNotNull(TestReportServiceImpl.class.getMethod("listPoints", String.class)); + Assertions.assertNotNull(TestReportServiceImpl.class.getMethod("listGroupReports", String.class)); + } + + @Test + void pointVoShouldExposeDeviceName() throws Exception { + Assertions.assertNotNull(TestReportPointVO.class.getMethod("getDeviceName")); + } + + @Test + void parserShouldReadNodeInfoSheetFromSampleXls() throws Exception { + TestReportPointExcelParser parser = new TestReportPointExcelParser(); + try (InputStream inputStream = Files.newInputStream( + Paths.get("ai-report/test-report/src/恒谊_20260610110840_202606101108_202606110957.xls"))) { + TestReportPointExcelParser.ParsedPoint point = parser.parse( + "恒谊_20260610110840_202606101108_202606110957.xls", inputStream); + Assertions.assertNotNull(point.getPointName()); + Assertions.assertNotNull(point.getSubstationName()); + } + } + + @Test + void importLedgerShouldRejectGeneratingTask() { + TestReportPO report = new TestReportPO(); + report.setId("report-001"); + report.setStatus(TestReportConst.STATUS_NORMAL); + report.setReportGenerateState(TestReportConst.REPORT_GENERATE_STATE_GENERATING); + TestableTestReportServiceImpl service = new TestableTestReportServiceImpl(mock(IDictTypeService.class), + mock(IDictDataService.class), report); + MockMultipartFile file = new MockMultipartFile("files", "sample.xls", "application/vnd.ms-excel", new byte[]{1}); + + BusinessException exception = Assertions.assertThrows(BusinessException.class, + () -> service.importLedger("report-001", new MultipartFile[]{file})); + + Assertions.assertTrue(exception.getMessage().contains("未生成")); + } + @Test void addTestReportShouldRejectMissingNo() { TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class)); @@ -74,7 +163,33 @@ class TestReportServiceImplTest { BusinessException exception = Assertions.assertThrows(BusinessException.class, () -> service.addTestReport(param)); - Assertions.assertTrue(exception.getMessage().contains("\u6d4b\u8bd5\u4f9d\u636e")); + Assertions.assertTrue(exception.getMessage().contains("测试依据")); + } + + @Test + void addTestReportShouldPreferFrontendIdWhenProvided() { + TestableAddTestReportServiceImpl service = createAddService(); + TestReportParam.AddParam param = buildValidNormalizeParam(); + param.setId("report-frontend-001"); + + boolean result = service.addTestReport(param); + + Assertions.assertTrue(result); + Assertions.assertNotNull(service.savedReport); + Assertions.assertEquals("report-frontend-001", service.savedReport.getId()); + } + + @Test + void addTestReportShouldGenerateUuidWhenFrontendIdMissing() { + TestableAddTestReportServiceImpl service = createAddService(); + TestReportParam.AddParam param = buildValidNormalizeParam(); + + boolean result = service.addTestReport(param); + + Assertions.assertTrue(result); + Assertions.assertNotNull(service.savedReport); + Assertions.assertNotNull(service.savedReport.getId()); + Assertions.assertDoesNotThrow(() -> UUID.fromString(service.savedReport.getId())); } @Test @@ -86,7 +201,7 @@ class TestReportServiceImplTest { BusinessException exception = Assertions.assertThrows(BusinessException.class, () -> invokeNormalizeParam(service, param)); - Assertions.assertTrue(exception.getMessage().contains("\u6536\u8d44\u6570\u636e")); + Assertions.assertTrue(exception.getMessage().contains("受资数据")); } @Test @@ -98,7 +213,7 @@ class TestReportServiceImplTest { BusinessException exception = Assertions.assertThrows(BusinessException.class, () -> invokeNormalizeParam(service, param)); - Assertions.assertTrue(exception.getMessage().contains("\u6536\u8d44\u6570\u636e")); + Assertions.assertTrue(exception.getMessage().contains("受资数据")); } @Test @@ -136,7 +251,7 @@ class TestReportServiceImplTest { BusinessException exception = Assertions.assertThrows(BusinessException.class, () -> service.addTestReport(param)); - Assertions.assertTrue(exception.getMessage().contains("\u6d4b\u8bd5\u4f9d\u636e")); + Assertions.assertTrue(exception.getMessage().contains("测试依据")); } @Test @@ -242,7 +357,28 @@ class TestReportServiceImplTest { mock(ReportModelFileStorageService.class), mock(FilePreviewService.class), dictTypeService, - dictDataService); + dictDataService, + mock(TestReportPointMapper.class), + mock(TestReportGroupReportMapper.class), + mock(TestReportPointExcelParser.class), + mock(TestReportStorageService.class), + mock(TestReportLedgerImportService.class), + mock(TestReportLedgerTemplateService.class)); + } + + private TestableAddTestReportServiceImpl createAddService() { + IDictTypeService dictTypeService = mock(IDictTypeService.class); + IDictDataService dictDataService = mock(IDictDataService.class); + TestReportMapper testReportMapper = mock(TestReportMapper.class); + mockNormalDictType(dictTypeService, DictConst.TEST_REPORT_COMPANY_DICT_CODE, "type-company"); + mockNormalDictType(dictTypeService, DictConst.TEST_REPORT_STANDARD_DICT_CODE, "type-standard"); + when(dictDataService.listDictDataByTypeId("type-company")).thenReturn(Collections.singletonList( + buildDictData("company-1", "company-A", DICT_STATE_NORMAL))); + when(dictDataService.listDictDataByTypeId("type-standard")).thenReturn(Collections.singletonList( + buildDictData("std-1", "standard-A", DICT_STATE_NORMAL))); + when(testReportMapper.countNormalClientUnit("client-1")).thenReturn(1); + when(testReportMapper.countNormalReportModel("model-1")).thenReturn(1); + return new TestableAddTestReportServiceImpl(dictTypeService, dictDataService, testReportMapper); } private TestReportParam.AddParam buildValidNormalizeParam() { @@ -280,7 +416,10 @@ class TestReportServiceImplTest { private TestableTestReportServiceImpl(IDictTypeService dictTypeService, IDictDataService dictDataService, TestReportPO report) { super(mock(ReportModelService.class), mock(ReportModelFileStorageService.class), - mock(FilePreviewService.class), dictTypeService, dictDataService); + mock(FilePreviewService.class), dictTypeService, dictDataService, + mock(TestReportPointMapper.class), mock(TestReportGroupReportMapper.class), + mock(TestReportPointExcelParser.class), mock(TestReportStorageService.class), + mock(TestReportLedgerImportService.class), mock(TestReportLedgerTemplateService.class)); this.report = report; } @@ -296,6 +435,31 @@ class TestReportServiceImplTest { } } + private static class TestableAddTestReportServiceImpl extends TestReportServiceImpl { + private TestReportPO savedReport; + + private TestableAddTestReportServiceImpl(IDictTypeService dictTypeService, IDictDataService dictDataService, + TestReportMapper testReportMapper) { + super(mock(ReportModelService.class), mock(ReportModelFileStorageService.class), + mock(FilePreviewService.class), dictTypeService, dictDataService, + mock(TestReportPointMapper.class), mock(TestReportGroupReportMapper.class), + mock(TestReportPointExcelParser.class), mock(TestReportStorageService.class), + mock(TestReportLedgerImportService.class), mock(TestReportLedgerTemplateService.class)); + this.baseMapper = testReportMapper; + } + + @Override + public long count(com.baomidou.mybatisplus.core.conditions.Wrapper queryWrapper) { + return 0; + } + + @Override + public boolean save(TestReportPO entity) { + this.savedReport = entity; + return true; + } + } + private void mockNormalDictType(IDictTypeService dictTypeService, String code, String typeId) { when(dictTypeService.getByCode(code)).thenReturn(buildDictType(typeId, code)); }