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));
}