From c1ad7feec24e5fa107c72c4913c92e1173202b68 Mon Sep 17 00:00:00 2001 From: yexb <553699424@qq.com> Date: Wed, 1 Jul 2026 13:15:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai-report):=20=E6=96=B0=E5=A2=9E=E5=A7=94?= =?UTF-8?q?=E6=89=98=E5=8D=95=E4=BD=8D=E7=AE=A1=E7=90=86=E5=92=8C=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E5=AE=A1=E6=89=B9=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增委托单位管理模块,支持增删改查、导入导出功能 - 实现委托单位 Excel 模板下载和数据导入功能 - 添加报告审批抽象处理器,支持审批流程日志记录 - 统一文件存储路径配置,将各模块独立路径改为统一根目录 - 新增 PQDIF 文件名存储字段,完善文件管理功能 - 修复代码中的乱码问题和错误提示信息 - 优化系统常量定义和字典配置管理 --- AGENTS.md | 2 + ai-report/client-unit/pom.xml | 23 + .../component/ClientUnitUserNameResolver.java | 41 ++ .../controller/ClientUnitController.java | 112 +++ .../clientunit/mapper/ClientUnitMapper.java | 25 + .../pojo/constant/ClientUnitConst.java | 13 + .../pojo/param/ClientUnitParam.java | 55 ++ .../clientunit/pojo/po/ClientUnitPO.java | 39 + .../clientunit/pojo/vo/ClientUnitExcelVO.java | 26 + .../pojo/vo/ClientUnitImportResultVO.java | 19 + .../pojo/vo/ClientUnitUserNameVO.java | 9 + .../clientunit/pojo/vo/ClientUnitVO.java | 69 ++ .../service/IClientUnitService.java | 30 + .../service/impl/ClientUnitServiceImpl.java | 437 ++++++++++++ .../mapper/mapping/ClientUnitMapper.xml | 6 + ai-report/pom.xml | 1 + ai-report/report-approval/pom.xml | 56 ++ .../ReportApprovalUserNameResolver.java | 41 ++ .../controller/ReportApprovalController.java | 71 ++ .../AbstractReportApprovalHandler.java | 85 +++ .../handler/ReportApprovalHandler.java | 10 + .../ReportApprovalHandlerRegistry.java | 35 + .../handler/ReportModelApprovalHandler.java | 58 ++ .../handler/TestReportApprovalHandler.java | 59 ++ .../mapper/ReportApprovalMapper.java | 95 +++ .../pojo/constant/ReportApprovalConst.java | 12 + .../pojo/param/ReportApprovalParam.java | 42 ++ .../pojo/po/ReportApprovalLogPO.java | 48 ++ .../pojo/vo/ReportApprovalLogVO.java | 38 + .../pojo/vo/ReportApprovalPendingVO.java | 32 + .../service/ReportApprovalService.java | 17 + .../impl/ReportApprovalServiceImpl.java | 77 ++ .../vo/ReportApprovalVOTimeFormatTest.java | 27 + .../impl/ReportApprovalServiceImplTest.java | 75 ++ ai-report/report-model/pom.xml | 23 + .../ReportModelFileStorageService.java | 132 ++++ .../ReportModelUserNameResolver.java | 41 ++ .../controller/ReportModelController.java | 173 +++++ .../reportmodel/mapper/ReportModelMapper.java | 25 + .../pojo/constant/ReportModelConst.java | 19 + .../pojo/param/ReportModelParam.java | 58 ++ .../reportmodel/pojo/po/ReportModelPO.java | 45 ++ .../pojo/vo/ReportModelUserNameVO.java | 9 + .../reportmodel/pojo/vo/ReportModelVO.java | 108 +++ .../service/ReportModelService.java | 27 + .../service/impl/ReportModelServiceImpl.java | 279 ++++++++ .../ReportModelFileStorageServiceTest.java | 39 + .../ReportModelUserNameResolverTest.java | 30 + .../vo/ReportModelVODefaultDisplayTest.java | 33 + .../pojo/vo/ReportModelVOTimeFormatTest.java | 23 + .../impl/ReportModelServiceImplTest.java | 67 ++ ai-report/test-device/pom.xml | 29 + .../TestDeviceReportStorageService.java | 149 ++++ .../controller/TestDeviceController.java | 165 +++++ .../testdevice/mapper/TestDeviceMapper.java | 61 ++ .../pojo/constant/TestDeviceConst.java | 12 + .../pojo/param/TestDeviceParam.java | 49 ++ .../testdevice/pojo/po/TestDevicePO.java | 39 + .../testdevice/pojo/vo/TestDeviceExcelVO.java | 32 + .../pojo/vo/TestDeviceImportResultVO.java | 13 + .../pojo/vo/TestDeviceTypeNameVO.java | 9 + .../testdevice/pojo/vo/TestDeviceVO.java | 59 ++ .../testdevice/service/TestDeviceService.java | 32 + .../service/impl/TestDeviceServiceImpl.java | 666 ++++++++++++++++++ .../TestDeviceReportStorageServiceTest.java | 28 + .../controller/TestDeviceControllerTest.java | 45 ++ .../mapper/TestDeviceMapperTest.java | 31 + .../impl/TestDeviceServiceImplTest.java | 161 +++++ ai-report/test-report/pom.xml | 35 + .../controller/TestReportController.java | 111 +++ .../testreport/mapper/TestReportMapper.java | 72 ++ .../pojo/constant/TestReportConst.java | 16 + .../pojo/param/TestReportParam.java | 83 +++ .../testreport/pojo/po/TestReportPO.java | 53 ++ .../testreport/pojo/vo/TestReportExcelVO.java | 53 ++ .../testreport/pojo/vo/TestReportVO.java | 156 ++++ .../testreport/service/TestReportService.java | 31 + .../service/impl/TestReportServiceImpl.java | 621 ++++++++++++++++ .../testreport/pojo/vo/TestReportVOTest.java | 42 ++ .../impl/TestReportServiceImplTest.java | 318 +++++++++ comservice/pom.xml | 58 ++ .../filepreview/FilePreviewService.java | 116 +++ .../filepreview/FilePreviewServiceTest.java | 77 ++ entrance/pom.xml | 30 + entrance/src/main/resources/application.yml | 5 +- pom.xml | 1 + .../impl/SteadyChecksquareServiceImpl.java | 2 +- .../SteadyChecksquareServiceImplTest.java | 25 +- .../system/pojo/constant/DictConst.java | 16 +- .../component/PqdifFileStorageService.java | 9 +- .../controller/CsPqdifPathController.java | 1 + .../mapper/mapping/CsPqdifPathMapper.xml | 5 +- .../pojo/param/CsPqdifPathParam.java | 4 + .../parsepqdif/pojo/po/CsPqdifPathPO.java | 3 + .../pojo/vo/CsPqdifPathDetailVO.java | 3 + .../parsepqdif/pojo/vo/CsPqdifPathVO.java | 3 + .../service/impl/CsPqdifPathServiceImpl.java | 1 + .../PqdifFileStorageServiceTest.java | 28 + .../impl/CsPqdifPathServiceImplTest.java | 61 ++ 99 files changed, 6511 insertions(+), 24 deletions(-) create mode 100644 ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/component/ClientUnitUserNameResolver.java create mode 100644 ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/controller/ClientUnitController.java create mode 100644 ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/mapper/ClientUnitMapper.java create mode 100644 ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/constant/ClientUnitConst.java create mode 100644 ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/param/ClientUnitParam.java create mode 100644 ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/po/ClientUnitPO.java create mode 100644 ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/vo/ClientUnitExcelVO.java create mode 100644 ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/vo/ClientUnitImportResultVO.java create mode 100644 ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/vo/ClientUnitUserNameVO.java create mode 100644 ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/vo/ClientUnitVO.java create mode 100644 ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/service/IClientUnitService.java create mode 100644 ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/service/impl/ClientUnitServiceImpl.java create mode 100644 ai-report/client-unit/src/main/resources/com/njcn/gather/aireport/clientunit/mapper/mapping/ClientUnitMapper.xml create mode 100644 ai-report/report-approval/pom.xml create mode 100644 ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/component/ReportApprovalUserNameResolver.java create mode 100644 ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/controller/ReportApprovalController.java create mode 100644 ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/handler/AbstractReportApprovalHandler.java create mode 100644 ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/handler/ReportApprovalHandler.java create mode 100644 ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/handler/ReportApprovalHandlerRegistry.java create mode 100644 ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/handler/ReportModelApprovalHandler.java create mode 100644 ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/handler/TestReportApprovalHandler.java create mode 100644 ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/mapper/ReportApprovalMapper.java create mode 100644 ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/pojo/constant/ReportApprovalConst.java create mode 100644 ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/pojo/param/ReportApprovalParam.java create mode 100644 ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/pojo/po/ReportApprovalLogPO.java create mode 100644 ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/pojo/vo/ReportApprovalLogVO.java create mode 100644 ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/pojo/vo/ReportApprovalPendingVO.java create mode 100644 ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/service/ReportApprovalService.java create mode 100644 ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/service/impl/ReportApprovalServiceImpl.java create mode 100644 ai-report/report-approval/src/test/java/com/njcn/gather/aireport/reportapproval/pojo/vo/ReportApprovalVOTimeFormatTest.java create mode 100644 ai-report/report-approval/src/test/java/com/njcn/gather/aireport/reportapproval/service/impl/ReportApprovalServiceImplTest.java create mode 100644 ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/component/ReportModelFileStorageService.java create mode 100644 ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/component/ReportModelUserNameResolver.java create mode 100644 ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/controller/ReportModelController.java create mode 100644 ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/mapper/ReportModelMapper.java create mode 100644 ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/pojo/constant/ReportModelConst.java create mode 100644 ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/pojo/param/ReportModelParam.java create mode 100644 ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/pojo/po/ReportModelPO.java create mode 100644 ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/pojo/vo/ReportModelUserNameVO.java create mode 100644 ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/pojo/vo/ReportModelVO.java create mode 100644 ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/service/ReportModelService.java create mode 100644 ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/service/impl/ReportModelServiceImpl.java create mode 100644 ai-report/report-model/src/test/java/com/njcn/gather/aireport/reportmodel/component/ReportModelFileStorageServiceTest.java create mode 100644 ai-report/report-model/src/test/java/com/njcn/gather/aireport/reportmodel/component/ReportModelUserNameResolverTest.java create mode 100644 ai-report/report-model/src/test/java/com/njcn/gather/aireport/reportmodel/pojo/vo/ReportModelVODefaultDisplayTest.java create mode 100644 ai-report/report-model/src/test/java/com/njcn/gather/aireport/reportmodel/pojo/vo/ReportModelVOTimeFormatTest.java create mode 100644 ai-report/report-model/src/test/java/com/njcn/gather/aireport/reportmodel/service/impl/ReportModelServiceImplTest.java create mode 100644 ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/component/TestDeviceReportStorageService.java create mode 100644 ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/controller/TestDeviceController.java create mode 100644 ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/mapper/TestDeviceMapper.java create mode 100644 ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/constant/TestDeviceConst.java create mode 100644 ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/param/TestDeviceParam.java create mode 100644 ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/po/TestDevicePO.java create mode 100644 ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/vo/TestDeviceExcelVO.java create mode 100644 ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/vo/TestDeviceImportResultVO.java create mode 100644 ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/vo/TestDeviceTypeNameVO.java create mode 100644 ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/vo/TestDeviceVO.java create mode 100644 ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/service/TestDeviceService.java create mode 100644 ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/service/impl/TestDeviceServiceImpl.java create mode 100644 ai-report/test-device/src/test/java/com/njcn/gather/aireport/testdevice/component/TestDeviceReportStorageServiceTest.java create mode 100644 ai-report/test-device/src/test/java/com/njcn/gather/aireport/testdevice/controller/TestDeviceControllerTest.java create mode 100644 ai-report/test-device/src/test/java/com/njcn/gather/aireport/testdevice/mapper/TestDeviceMapperTest.java create mode 100644 ai-report/test-device/src/test/java/com/njcn/gather/aireport/testdevice/service/impl/TestDeviceServiceImplTest.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/controller/TestReportController.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/mapper/TestReportMapper.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/constant/TestReportConst.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/param/TestReportParam.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/po/TestReportPO.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportExcelVO.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportVO.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/service/TestReportService.java create mode 100644 ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/service/impl/TestReportServiceImpl.java create mode 100644 ai-report/test-report/src/test/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportVOTest.java create mode 100644 ai-report/test-report/src/test/java/com/njcn/gather/aireport/testreport/service/impl/TestReportServiceImplTest.java create mode 100644 comservice/pom.xml create mode 100644 comservice/src/main/java/com/njcn/gather/comservice/filepreview/FilePreviewService.java create mode 100644 comservice/src/test/java/com/njcn/gather/comservice/filepreview/FilePreviewServiceTest.java create mode 100644 tools/parse-pqdif/src/test/java/com/njcn/gather/tool/parsepqdif/component/PqdifFileStorageServiceTest.java create mode 100644 tools/parse-pqdif/src/test/java/com/njcn/gather/tool/parsepqdif/service/impl/CsPqdifPathServiceImplTest.java diff --git a/AGENTS.md b/AGENTS.md index eaf69db..c2eabc7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,7 +18,9 @@ - 只清理自己造成的问题:可以删除因本次修改而产生的未使用 `import`、变量或方法;不要删除仓库中原本就存在的死代码,除非用户明确要求。 - 先定义验证方式:执行方案里要写清楚“改哪里、怎么判断改对了”;默认通过代码路径、配置一致性和受影响范围检查进行验证。 - 除非用户明确要求,否则不执行任何 `mvn` 编译、打包、测试或其他构建命令。 +- 系统中所有无数据项默认显示 `-`;新增或修改列表、详情、导出、汇总等展示逻辑时,若字段无值,统一按该约定处理,避免空白、`null` 或其他占位写法并存。 - 所有导出或生成文件名统一追加日期,格式为“原文件名 + `_` + `yyyyMMdd` + 原扩展名”,例如 `暂态事件列表_20260516.xlsx`;新增下载导出、临时生成或落盘保存文件时,优先复用目标模块已有文件名工具,不要在业务代码中散落手写日期拼接。 +- 后端本地文件存储统一使用 `files.path` 作为根目录配置;各业务模块只能在该根目录下追加自己的固定子目录,例如 PQDIF 文件使用 `pqdif-files`,报告模板使用 `report-model`,检测设备校准报告使用 `test-device-report`,检测报告相关文件使用 `test-report`。不要再新增 `parse-pqdif.storage.path`、`report-model.storage.path`、`test-device.report.storage.path` 这类模块级根路径配置。 ## 项目结构与模块划分 `CN_Tool` 是一个 Maven 多模块后端项目,根目录的 [`pom.xml`](C:/code/gitea/cn_tool/CN_Tool/pom.xml) 聚合了 `entrance`、`system`、`user`、`detection` 和 `tools`。 diff --git a/ai-report/client-unit/pom.xml b/ai-report/client-unit/pom.xml index 1cb9a1e..6823884 100644 --- a/ai-report/client-unit/pom.xml +++ b/ai-report/client-unit/pom.xml @@ -11,6 +11,7 @@ client-unit + jar @@ -30,5 +31,27 @@ spingboot2.3.12 2.3.12 + + + com.njcn.gather + system + 1.0.0 + + + + org.apache.poi + poi-ooxml + 4.1.2 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-validation + diff --git a/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/component/ClientUnitUserNameResolver.java b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/component/ClientUnitUserNameResolver.java new file mode 100644 index 0000000..8942bb8 --- /dev/null +++ b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/component/ClientUnitUserNameResolver.java @@ -0,0 +1,41 @@ +package com.njcn.gather.aireport.clientunit.component; + +import cn.hutool.core.util.StrUtil; +import com.njcn.gather.aireport.clientunit.mapper.ClientUnitMapper; +import com.njcn.gather.aireport.clientunit.pojo.vo.ClientUnitUserNameVO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +public class ClientUnitUserNameResolver { + + private final ClientUnitMapper clientUnitMapper; + + public Map resolveUserNames(Collection userIds) { + if (userIds == null || userIds.isEmpty()) { + return Collections.emptyMap(); + } + Set validIds = userIds.stream() + .filter(StrUtil::isNotBlank) + .collect(Collectors.toCollection(LinkedHashSet::new)); + if (validIds.isEmpty()) { + return Collections.emptyMap(); + } + List users = clientUnitMapper.selectUserNamesByIds(validIds); + if (users == null || users.isEmpty()) { + return Collections.emptyMap(); + } + return users.stream() + .filter(user -> user != null && StrUtil.isNotBlank(user.getId()) && StrUtil.isNotBlank(user.getName())) + .collect(Collectors.toMap(ClientUnitUserNameVO::getId, ClientUnitUserNameVO::getName, (first, second) -> first)); + } +} diff --git a/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/controller/ClientUnitController.java b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/controller/ClientUnitController.java new file mode 100644 index 0000000..5b52cf5 --- /dev/null +++ b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/controller/ClientUnitController.java @@ -0,0 +1,112 @@ +package com.njcn.gather.aireport.clientunit.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.njcn.common.pojo.annotation.OperateInfo; +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.clientunit.pojo.param.ClientUnitParam; +import com.njcn.gather.aireport.clientunit.pojo.vo.ClientUnitImportResultVO; +import com.njcn.gather.aireport.clientunit.pojo.vo.ClientUnitVO; +import com.njcn.gather.aireport.clientunit.service.IClientUnitService; +import com.njcn.web.controller.BaseController; +import com.njcn.web.utils.HttpResultUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +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; + +/** + * 委托单位管理接口。 + */ +@Api(tags = "委托单位管理") +@RestController +@RequestMapping("/clientUnit") +@RequiredArgsConstructor +public class ClientUnitController extends BaseController { + + private final IClientUnitService clientUnitService; + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询委托单位列表") + @PostMapping("/list") + public HttpResult> list(@RequestBody(required = false) ClientUnitParam.QueryParam param) { + String methodDescribe = getMethodDescribe("list"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, + clientUnitService.listClientUnits(param), methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD) + @ApiOperation("新增委托单位") + @PostMapping("/add") + public HttpResult add(@RequestBody @Validated ClientUnitParam.AddParam param) { + String methodDescribe = getMethodDescribe("add"); + boolean result = clientUnitService.addClientUnit(param); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, + result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE) + @ApiOperation("编辑委托单位") + @PostMapping("/update") + public HttpResult update(@RequestBody @Validated ClientUnitParam.UpdateParam param) { + String methodDescribe = getMethodDescribe("update"); + boolean result = clientUnitService.updateClientUnit(param); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, + result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询委托单位详情") + @ApiImplicitParam(name = "id", value = "委托单位ID", required = true) + @GetMapping("/getById") + public HttpResult getById(@RequestParam("id") String id) { + String methodDescribe = getMethodDescribe("getById"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, + clientUnitService.getClientUnit(id), methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DELETE) + @ApiOperation("删除委托单位") + @PostMapping("/delete") + public HttpResult delete(@RequestBody List ids) { + String methodDescribe = getMethodDescribe("delete"); + boolean result = clientUnitService.deleteClientUnits(ids); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, + result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("导出委托单位") + @PostMapping("/export") + public void export(@RequestBody(required = false) ClientUnitParam.QueryParam param) { + clientUnitService.exportClientUnits(param); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("下载委托单位导入模板") + @GetMapping("/importTemplate") + public void importTemplate() { + clientUnitService.downloadImportTemplate(); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD) + @ApiOperation("导入委托单位") + @PostMapping(value = "/import", consumes = {"multipart/form-data"}) + public HttpResult importExcel(@RequestParam("file") MultipartFile file) { + String methodDescribe = getMethodDescribe("importExcel"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, + clientUnitService.importClientUnits(file), methodDescribe); + } +} diff --git a/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/mapper/ClientUnitMapper.java b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/mapper/ClientUnitMapper.java new file mode 100644 index 0000000..8ce7594 --- /dev/null +++ b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/mapper/ClientUnitMapper.java @@ -0,0 +1,25 @@ +package com.njcn.gather.aireport.clientunit.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njcn.gather.aireport.clientunit.pojo.po.ClientUnitPO; +import com.njcn.gather.aireport.clientunit.pojo.vo.ClientUnitUserNameVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface ClientUnitMapper extends BaseMapper { + + @Select({ + "" + }) + List selectUserNamesByIds(@Param("ids") Collection ids); +} diff --git a/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/constant/ClientUnitConst.java b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/constant/ClientUnitConst.java new file mode 100644 index 0000000..661db15 --- /dev/null +++ b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/constant/ClientUnitConst.java @@ -0,0 +1,13 @@ +package com.njcn.gather.aireport.clientunit.pojo.constant; + +/** + * 委托单位业务常量。 + */ +public final class ClientUnitConst { + + public static final int STATUS_DELETED = 0; + public static final int STATUS_NORMAL = 1; + + private ClientUnitConst() { + } +} diff --git a/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/param/ClientUnitParam.java b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/param/ClientUnitParam.java new file mode 100644 index 0000000..552e7c3 --- /dev/null +++ b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/param/ClientUnitParam.java @@ -0,0 +1,55 @@ +package com.njcn.gather.aireport.clientunit.pojo.param; + +import com.njcn.web.pojo.param.BaseParam; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +/** + * 委托单位请求参数。 + */ +public class ClientUnitParam { + + @Data + @EqualsAndHashCode(callSuper = true) + public static class QueryParam extends BaseParam { + @ApiModelProperty("单位名称关键字") + private String name; + + @ApiModelProperty("创建开始时间,yyyy-MM-dd HH:mm:ss") + private String searchBeginTime; + + @ApiModelProperty("创建结束时间,yyyy-MM-dd HH:mm:ss") + private String searchEndTime; + } + + @Data + public static class AddParam { + @ApiModelProperty("单位名称") + @NotBlank(message = "单位名称不能为空") + @Size(max = 32, message = "单位名称不能超过32个字符") + private String name; + + @ApiModelProperty("联系人") + @Size(max = 200, message = "联系人不能超过200个字符") + private String contactName; + + @ApiModelProperty("联系方式") + @Size(max = 32, message = "联系方式不能超过32个字符") + private String phonenumber; + + @ApiModelProperty("地址") + @Size(max = 128, message = "地址不能超过128个字符") + private String address; + } + + @Data + public static class UpdateParam extends AddParam { + @ApiModelProperty("委托单位ID") + @NotBlank(message = "委托单位ID不能为空") + private String id; + } +} diff --git a/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/po/ClientUnitPO.java b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/po/ClientUnitPO.java new file mode 100644 index 0000000..dd55e46 --- /dev/null +++ b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/po/ClientUnitPO.java @@ -0,0 +1,39 @@ +package com.njcn.gather.aireport.clientunit.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 委托单位记录。 + */ +@Data +@TableName("ai_clientunit") +public class ClientUnitPO implements Serializable { + private static final long serialVersionUID = 2517699918054153742L; + + @TableId("id") + private String id; + @TableField("name") + private String name; + @TableField("contact_name") + private String contactName; + @TableField("phonenumber") + private String phonenumber; + @TableField("address") + private String address; + @TableField("status") + private Integer status; + @TableField("create_by") + private String createBy; + @TableField("create_time") + private LocalDateTime createTime; + @TableField("update_by") + private String updateBy; + @TableField("update_time") + private LocalDateTime updateTime; +} diff --git a/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/vo/ClientUnitExcelVO.java b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/vo/ClientUnitExcelVO.java new file mode 100644 index 0000000..51ada5b --- /dev/null +++ b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/vo/ClientUnitExcelVO.java @@ -0,0 +1,26 @@ +package com.njcn.gather.aireport.clientunit.pojo.vo; + +import cn.afterturn.easypoi.excel.annotation.Excel; +import lombok.Data; + +import java.io.Serializable; + +/** + * 委托单位 Excel 行对象。 + */ +@Data +public class ClientUnitExcelVO implements Serializable { + private static final long serialVersionUID = 8533457988358235671L; + + @Excel(name = "单位名称", width = 25) + private String name; + + @Excel(name = "联系人", width = 20) + private String contactName; + + @Excel(name = "联系方式", width = 20) + private String phonenumber; + + @Excel(name = "地址", width = 35) + private String address; +} diff --git a/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/vo/ClientUnitImportResultVO.java b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/vo/ClientUnitImportResultVO.java new file mode 100644 index 0000000..e6fdabe --- /dev/null +++ b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/vo/ClientUnitImportResultVO.java @@ -0,0 +1,19 @@ +package com.njcn.gather.aireport.clientunit.pojo.vo; + +import lombok.Data; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 委托单位导入结果。 + */ +@Data +public class ClientUnitImportResultVO implements Serializable { + private static final long serialVersionUID = 509902603493571405L; + + private int successCount; + private int failCount; + private List failReasons = new ArrayList(); +} diff --git a/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/vo/ClientUnitUserNameVO.java b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/vo/ClientUnitUserNameVO.java new file mode 100644 index 0000000..754ff84 --- /dev/null +++ b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/vo/ClientUnitUserNameVO.java @@ -0,0 +1,9 @@ +package com.njcn.gather.aireport.clientunit.pojo.vo; + +import lombok.Data; + +@Data +public class ClientUnitUserNameVO { + private String id; + private String name; +} diff --git a/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/vo/ClientUnitVO.java b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/vo/ClientUnitVO.java new file mode 100644 index 0000000..c738581 --- /dev/null +++ b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/pojo/vo/ClientUnitVO.java @@ -0,0 +1,69 @@ +package com.njcn.gather.aireport.clientunit.pojo.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import lombok.Data; +import org.springframework.util.StringUtils; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 委托单位展示对象。 + */ +@Data +public class ClientUnitVO implements Serializable { + private static final long serialVersionUID = 8924649462626990262L; + private static final String DEFAULT_EMPTY_DISPLAY = "-"; + + private String id; + private String name; + private String contactName; + private String phonenumber; + private String address; + private String createBy; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + private LocalDateTime createTime; + private String updateBy; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + private LocalDateTime updateTime; + + public String getId() { + return defaultDisplay(id); + } + + public String getName() { + return defaultDisplay(name); + } + + public String getContactName() { + return defaultDisplay(contactName); + } + + public String getPhonenumber() { + return defaultDisplay(phonenumber); + } + + public String getAddress() { + return defaultDisplay(address); + } + + public String getCreateBy() { + return defaultDisplay(createBy); + } + + public String getUpdateBy() { + return defaultDisplay(updateBy); + } + + private String defaultDisplay(String value) { + return StringUtils.hasText(value) ? value : DEFAULT_EMPTY_DISPLAY; + } +} diff --git a/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/service/IClientUnitService.java b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/service/IClientUnitService.java new file mode 100644 index 0000000..2c5daa1 --- /dev/null +++ b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/service/IClientUnitService.java @@ -0,0 +1,30 @@ +package com.njcn.gather.aireport.clientunit.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.aireport.clientunit.pojo.param.ClientUnitParam; +import com.njcn.gather.aireport.clientunit.pojo.po.ClientUnitPO; +import com.njcn.gather.aireport.clientunit.pojo.vo.ClientUnitImportResultVO; +import com.njcn.gather.aireport.clientunit.pojo.vo.ClientUnitVO; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +public interface IClientUnitService extends IService { + + Page listClientUnits(ClientUnitParam.QueryParam param); + + boolean addClientUnit(ClientUnitParam.AddParam param); + + boolean updateClientUnit(ClientUnitParam.UpdateParam param); + + ClientUnitVO getClientUnit(String id); + + boolean deleteClientUnits(List ids); + + void exportClientUnits(ClientUnitParam.QueryParam param); + + void downloadImportTemplate(); + + ClientUnitImportResultVO importClientUnits(MultipartFile file); +} diff --git a/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/service/impl/ClientUnitServiceImpl.java b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/service/impl/ClientUnitServiceImpl.java new file mode 100644 index 0000000..e42d684 --- /dev/null +++ b/ai-report/client-unit/src/main/java/com/njcn/gather/aireport/clientunit/service/impl/ClientUnitServiceImpl.java @@ -0,0 +1,437 @@ +package com.njcn.gather.aireport.clientunit.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.aireport.clientunit.component.ClientUnitUserNameResolver; +import com.njcn.gather.aireport.clientunit.mapper.ClientUnitMapper; +import com.njcn.gather.aireport.clientunit.pojo.constant.ClientUnitConst; +import com.njcn.gather.aireport.clientunit.pojo.param.ClientUnitParam; +import com.njcn.gather.aireport.clientunit.pojo.po.ClientUnitPO; +import com.njcn.gather.aireport.clientunit.pojo.vo.ClientUnitExcelVO; +import com.njcn.gather.aireport.clientunit.pojo.vo.ClientUnitImportResultVO; +import com.njcn.gather.aireport.clientunit.pojo.vo.ClientUnitVO; +import com.njcn.gather.aireport.clientunit.service.IClientUnitService; +import com.njcn.gather.system.util.ExportFileNameUtil; +import com.njcn.web.factory.PageFactory; +import com.njcn.web.utils.ExcelUtil; +import com.njcn.web.utils.HttpServletUtil; +import com.njcn.web.utils.RequestUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.DataFormatter; +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.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.time.LocalDateTime; +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.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * 委托单位业务实现。 + */ +@Slf4j +@Service +public class ClientUnitServiceImpl extends ServiceImpl implements IClientUnitService { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final String[] TEMPLATE_HEADERS = new String[]{"单位名称", "联系人", "联系方式", "地址"}; + + private final ClientUnitUserNameResolver clientUnitUserNameResolver; + + public ClientUnitServiceImpl(ClientUnitUserNameResolver clientUnitUserNameResolver) { + this.clientUnitUserNameResolver = clientUnitUserNameResolver; + } + + @Override + public Page listClientUnits(ClientUnitParam.QueryParam param) { + ClientUnitParam.QueryParam query = param == null ? new ClientUnitParam.QueryParam() : param; + Page page = this.page( + new Page(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), + buildQueryWrapper(query)); + Page result = new Page(page.getCurrent(), page.getSize(), page.getTotal()); + Map userNameMap = resolveUserNameMap(page.getRecords()); + result.setRecords(page.getRecords().stream() + .map(clientUnit -> toVO(clientUnit, userNameMap)) + .collect(Collectors.toList())); + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean addClientUnit(ClientUnitParam.AddParam param) { + NormalizedClientUnit data = normalizeParam(param); + LocalDateTime now = LocalDateTime.now(); + String userId = resolveCurrentUserId(); + ClientUnitPO clientUnit = new ClientUnitPO(); + clientUnit.setId(UUID.randomUUID().toString()); + clientUnit.setName(data.getName()); + clientUnit.setContactName(data.getContactName()); + clientUnit.setPhonenumber(data.getPhonenumber()); + clientUnit.setAddress(data.getAddress()); + clientUnit.setStatus(ClientUnitConst.STATUS_NORMAL); + clientUnit.setCreateBy(userId); + clientUnit.setCreateTime(now); + clientUnit.setUpdateBy(userId); + clientUnit.setUpdateTime(now); + return this.save(clientUnit); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateClientUnit(ClientUnitParam.UpdateParam param) { + if (param == null || StrUtil.isBlank(param.getId())) { + throw new BusinessException(CommonResponseEnum.FAIL, "委托单位ID不能为空"); + } + ClientUnitPO clientUnit = requireNormal(param.getId()); + NormalizedClientUnit data = normalizeParam(param); + checkDuplicate(data.getName(), clientUnit.getId()); + clientUnit.setName(data.getName()); + clientUnit.setContactName(data.getContactName()); + clientUnit.setPhonenumber(data.getPhonenumber()); + clientUnit.setAddress(data.getAddress()); + clientUnit.setUpdateBy(resolveCurrentUserId()); + clientUnit.setUpdateTime(LocalDateTime.now()); + return this.updateById(clientUnit); + } + + @Override + public ClientUnitVO getClientUnit(String id) { + ClientUnitPO clientUnit = requireNormal(id); + return toVO(clientUnit, resolveUserNameMap(Arrays.asList(clientUnit))); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean deleteClientUnits(List ids) { + List validIds = normalizeIds(ids); + if (validIds.isEmpty()) { + throw new BusinessException(CommonResponseEnum.FAIL, "委托单位ID不能为空"); + } + return this.lambdaUpdate() + .set(ClientUnitPO::getStatus, ClientUnitConst.STATUS_DELETED) + .set(ClientUnitPO::getUpdateBy, resolveCurrentUserId()) + .set(ClientUnitPO::getUpdateTime, LocalDateTime.now()) + .in(ClientUnitPO::getId, validIds) + .eq(ClientUnitPO::getStatus, ClientUnitConst.STATUS_NORMAL) + .update(); + } + + @Override + public void exportClientUnits(ClientUnitParam.QueryParam param) { + List exportRows = this.list(buildQueryWrapper(param == null ? new ClientUnitParam.QueryParam() : param)) + .stream() + .map(this::toExcelVO) + .collect(Collectors.toList()); + ExcelUtil.exportExcel(ExportFileNameUtil.appendToday("委托单位列表.xlsx"), "委托单位列表", ClientUnitExcelVO.class, exportRows); + } + + @Override + public void downloadImportTemplate() { + HttpServletResponse response = HttpServletUtil.getResponse(); + try (XSSFWorkbook workbook = new XSSFWorkbook()) { + XSSFSheet sheet = workbook.createSheet("委托单位导入模板"); + writeTemplateHeader(workbook, sheet); + String fileName = URLEncoder.encode(ExportFileNameUtil.appendToday("委托单位导入模板.xlsx"), "UTF-8"); + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8"); + try (ServletOutputStream outputStream = response.getOutputStream()) { + workbook.write(outputStream); + outputStream.flush(); + } + } catch (IOException exception) { + log.error("下载委托单位导入模板失败", exception); + throw new BusinessException(CommonResponseEnum.FAIL, "下载委托单位导入模板失败"); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public ClientUnitImportResultVO importClientUnits(MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new BusinessException(CommonResponseEnum.FAIL, "导入文件不能为空"); + } + List rows = readImportRows(file); + ClientUnitImportResultVO result = new ClientUnitImportResultVO(); + if (rows.isEmpty()) { + result.setFailCount(0); + result.setSuccessCount(0); + return result; + } + LocalDateTime now = LocalDateTime.now(); + String userId = resolveCurrentUserId(); + Set fileKeys = new HashSet(); + List saveRows = new ArrayList(); + for (int i = 0; i < rows.size(); i++) { + int rowNumber = i + 2; + try { + NormalizedClientUnit data = normalizeExcelRow(rows.get(i)); + String duplicateKey = data.getName(); + if (!fileKeys.add(duplicateKey)) { + throw new BusinessException(CommonResponseEnum.FAIL, "文件内存在重复委托单位名称"); + } + checkDuplicate(data.getName(), null); + ClientUnitPO clientUnit = new ClientUnitPO(); + clientUnit.setId(UUID.randomUUID().toString()); + clientUnit.setName(data.getName()); + clientUnit.setContactName(data.getContactName()); + clientUnit.setPhonenumber(data.getPhonenumber()); + clientUnit.setAddress(data.getAddress()); + clientUnit.setStatus(ClientUnitConst.STATUS_NORMAL); + clientUnit.setCreateBy(userId); + clientUnit.setCreateTime(now); + clientUnit.setUpdateBy(userId); + clientUnit.setUpdateTime(now); + saveRows.add(clientUnit); + result.setSuccessCount(result.getSuccessCount() + 1); + } catch (BusinessException exception) { + result.setFailCount(result.getFailCount() + 1); + result.getFailReasons().add("第" + rowNumber + "行:" + exception.getMessage()); + } + } + if (!saveRows.isEmpty()) { + this.saveBatch(saveRows); + } + return result; + } + + private LambdaQueryWrapper buildQueryWrapper(ClientUnitParam.QueryParam query) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper(); + wrapper.eq(ClientUnitPO::getStatus, ClientUnitConst.STATUS_NORMAL) + .like(StrUtil.isNotBlank(query.getName()), ClientUnitPO::getName, query.getName()); + if (StrUtil.isNotBlank(query.getSearchBeginTime())) { + wrapper.ge(ClientUnitPO::getCreateTime, parseDateTime(query.getSearchBeginTime(), "开始时间格式不正确")); + } + if (StrUtil.isNotBlank(query.getSearchEndTime())) { + wrapper.le(ClientUnitPO::getCreateTime, parseDateTime(query.getSearchEndTime(), "结束时间格式不正确")); + } + wrapper.orderByDesc(ClientUnitPO::getCreateTime); + return wrapper; + } + + private ClientUnitPO requireNormal(String id) { + String normalizedId = trimToNull(id); + if (normalizedId == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "委托单位ID不能为空"); + } + ClientUnitPO clientUnit = this.lambdaQuery() + .eq(ClientUnitPO::getId, normalizedId) + .eq(ClientUnitPO::getStatus, ClientUnitConst.STATUS_NORMAL) + .one(); + if (clientUnit == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "委托单位不存在或已删除"); + } + return clientUnit; + } + + private void checkDuplicate(String name, String excludeId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper(); + wrapper.eq(ClientUnitPO::getName, name) + .eq(ClientUnitPO::getStatus, ClientUnitConst.STATUS_NORMAL) + .ne(StrUtil.isNotBlank(excludeId), ClientUnitPO::getId, excludeId); + if (this.count(wrapper) > 0) { + throw new BusinessException(CommonResponseEnum.FAIL, "委托单位名称已存在"); + } + } + + private NormalizedClientUnit normalizeParam(ClientUnitParam.AddParam param) { + if (param == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "委托单位参数不能为空"); + } + return normalizeFields(param.getName(), param.getContactName(), param.getPhonenumber(), param.getAddress()); + } + + private NormalizedClientUnit normalizeExcelRow(ClientUnitExcelVO row) { + if (row == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "空行"); + } + return normalizeFields(row.getName(), row.getContactName(), row.getPhonenumber(), row.getAddress()); + } + + private NormalizedClientUnit normalizeFields(String name, String contactName, String phonenumber, String address) { + NormalizedClientUnit data = new NormalizedClientUnit(); + data.setName(requireText(name, "单位名称不能为空", 32, "单位名称不能超过32个字符")); + data.setContactName(optionalText(contactName, 200, "联系人不能超过200个字符")); + data.setPhonenumber(optionalText(phonenumber, 32, "联系方式不能超过32个字符")); + data.setAddress(optionalText(address, 128, "地址不能超过128个字符")); + return data; + } + + private String requireText(String value, String blankMessage, int maxLength, String lengthMessage) { + String text = trimToNull(value); + if (text == null) { + throw new BusinessException(CommonResponseEnum.FAIL, blankMessage); + } + if (text.length() > maxLength) { + throw new BusinessException(CommonResponseEnum.FAIL, lengthMessage); + } + return text; + } + + private String optionalText(String value, int maxLength, String lengthMessage) { + String text = trimToNull(value); + if (text != null && text.length() > maxLength) { + throw new BusinessException(CommonResponseEnum.FAIL, lengthMessage); + } + return text; + } + + private List readImportRows(MultipartFile file) { + try (InputStream inputStream = file.getInputStream(); + XSSFWorkbook workbook = new XSSFWorkbook(inputStream)) { + XSSFSheet sheet = workbook.getNumberOfSheets() == 0 ? null : workbook.getSheetAt(0); + if (sheet == null || sheet.getLastRowNum() < 1) { + return Collections.emptyList(); + } + List rows = new ArrayList(); + for (int i = 1; i <= sheet.getLastRowNum(); i++) { + Row row = sheet.getRow(i); + if (isEmptyRow(row)) { + continue; + } + ClientUnitExcelVO excelVO = new ClientUnitExcelVO(); + excelVO.setName(readCell(row, 0)); + excelVO.setContactName(readCell(row, 1)); + excelVO.setPhonenumber(readCell(row, 2)); + excelVO.setAddress(readCell(row, 3)); + rows.add(excelVO); + } + return rows; + } catch (IOException exception) { + log.error("读取委托单位导入文件失败", exception); + throw new BusinessException(CommonResponseEnum.FAIL, "读取导入文件失败"); + } + } + + private void writeTemplateHeader(XSSFWorkbook workbook, XSSFSheet sheet) { + CellStyle style = workbook.createCellStyle(); + Font font = workbook.createFont(); + font.setBold(true); + style.setFont(font); + Row row = sheet.createRow(0); + for (int i = 0; i < TEMPLATE_HEADERS.length; i++) { + Cell cell = row.createCell(i); + cell.setCellValue(TEMPLATE_HEADERS[i]); + cell.setCellStyle(style); + sheet.setColumnWidth(i, 25 * 256); + } + } + + private boolean isEmptyRow(Row row) { + if (row == null) { + return true; + } + for (int i = 0; i < TEMPLATE_HEADERS.length; i++) { + if (trimToNull(readCell(row, i)) != null) { + return false; + } + } + return true; + } + + private String readCell(Row row, int index) { + Cell cell = row.getCell(index); + if (cell == null) { + return null; + } + return trimToNull(new DataFormatter().formatCellValue(cell)); + } + + private List normalizeIds(List ids) { + if (ids == null || ids.isEmpty()) { + return Collections.emptyList(); + } + List result = new ArrayList(); + for (String id : ids) { + String normalizedId = trimToNull(id); + if (normalizedId != null && !result.contains(normalizedId)) { + result.add(normalizedId); + } + } + return result; + } + + private LocalDateTime parseDateTime(String value, String errorMessage) { + try { + return LocalDateTime.parse(value.trim(), DATE_TIME_FORMATTER); + } catch (DateTimeParseException exception) { + throw new BusinessException(CommonResponseEnum.FAIL, errorMessage); + } + } + + private ClientUnitVO toVO(ClientUnitPO clientUnit, Map userNameMap) { + ClientUnitVO vo = new ClientUnitVO(); + BeanUtil.copyProperties(clientUnit, vo); + vo.setCreateBy(resolveUserName(clientUnit.getCreateBy(), userNameMap)); + vo.setUpdateBy(resolveUserName(clientUnit.getUpdateBy(), userNameMap)); + return vo; + } + + private ClientUnitExcelVO toExcelVO(ClientUnitPO clientUnit) { + ClientUnitExcelVO vo = new ClientUnitExcelVO(); + BeanUtil.copyProperties(clientUnit, vo); + return vo; + } + + private Map resolveUserNameMap(List clientUnits) { + List userIds = clientUnits.stream() + .flatMap(clientUnit -> Arrays.asList(clientUnit.getCreateBy(), clientUnit.getUpdateBy()).stream()) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toList()); + return clientUnitUserNameResolver.resolveUserNames(userIds); + } + + private String resolveUserName(String userId, Map userNameMap) { + if (StrUtil.isBlank(userId)) { + return null; + } + String userName = userNameMap == null ? null : userNameMap.get(userId); + return StrUtil.isNotBlank(userName) ? userName : userId; + } + + private String resolveCurrentUserId() { + String userId = RequestUtil.getUserId(); + return StrUtil.isBlank(userId) ? null : userId; + } + + private String trimToNull(String value) { + if (value == null) { + return null; + } + String text = value.trim(); + return text.isEmpty() ? null : text; + } + + @lombok.Data + private static class NormalizedClientUnit { + private String name; + private String contactName; + private String phonenumber; + private String address; + } +} 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 new file mode 100644 index 0000000..49589e5 --- /dev/null +++ b/ai-report/client-unit/src/main/resources/com/njcn/gather/aireport/clientunit/mapper/mapping/ClientUnitMapper.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/ai-report/pom.xml b/ai-report/pom.xml index 44c49b0..74d4b7b 100644 --- a/ai-report/pom.xml +++ b/ai-report/pom.xml @@ -20,6 +20,7 @@ client-unit test-device test-report + report-approval diff --git a/ai-report/report-approval/pom.xml b/ai-report/report-approval/pom.xml new file mode 100644 index 0000000..cc00940 --- /dev/null +++ b/ai-report/report-approval/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + + com.njcn.gather + ai-report + 1.0.0 + + + report-approval + jar + + + + com.njcn.gather + report-model + 1.0.0 + + + com.njcn.gather + test-report + 1.0.0 + + + com.njcn + njcn-common + 0.0.1 + + + com.njcn + mybatis-plus + 0.0.1 + + + com.njcn + spingboot2.3.12 + 2.3.12 + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-test + test + + + diff --git a/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/component/ReportApprovalUserNameResolver.java b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/component/ReportApprovalUserNameResolver.java new file mode 100644 index 0000000..932a89b --- /dev/null +++ b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/component/ReportApprovalUserNameResolver.java @@ -0,0 +1,41 @@ +package com.njcn.gather.aireport.reportapproval.component; + +import cn.hutool.core.util.StrUtil; +import com.njcn.gather.aireport.reportapproval.mapper.ReportApprovalMapper; +import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelUserNameVO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +public class ReportApprovalUserNameResolver { + + private final ReportApprovalMapper reportApprovalMapper; + + public Map resolveUserNames(Collection userIds) { + if (userIds == null || userIds.isEmpty()) { + return Collections.emptyMap(); + } + Set validIds = userIds.stream() + .filter(StrUtil::isNotBlank) + .collect(Collectors.toCollection(LinkedHashSet::new)); + if (validIds.isEmpty()) { + return Collections.emptyMap(); + } + List users = reportApprovalMapper.selectUserNamesByIds(validIds); + if (users == null || users.isEmpty()) { + return Collections.emptyMap(); + } + return users.stream() + .filter(user -> user != null && StrUtil.isNotBlank(user.getId()) && StrUtil.isNotBlank(user.getName())) + .collect(Collectors.toMap(ReportModelUserNameVO::getId, ReportModelUserNameVO::getName, (first, second) -> first)); + } +} diff --git a/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/controller/ReportApprovalController.java b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/controller/ReportApprovalController.java new file mode 100644 index 0000000..e874adf --- /dev/null +++ b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/controller/ReportApprovalController.java @@ -0,0 +1,71 @@ +package com.njcn.gather.aireport.reportapproval.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.njcn.common.pojo.annotation.OperateInfo; +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.reportapproval.pojo.param.ReportApprovalParam; +import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalLogVO; +import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalPendingVO; +import com.njcn.gather.aireport.reportapproval.service.ReportApprovalService; +import com.njcn.web.controller.BaseController; +import com.njcn.web.utils.HttpResultUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +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.RestController; + +@Api(tags = "报告审批管理") +@RestController +@RequestMapping("/api/report-approval") +@RequiredArgsConstructor +public class ReportApprovalController extends BaseController { + + private final ReportApprovalService reportApprovalService; + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询待审报告分页") + @GetMapping("/pending-page") + public HttpResult> pendingPage(@ModelAttribute ReportApprovalParam.PendingQueryParam param) { + String methodDescribe = getMethodDescribe("pendingPage"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, + reportApprovalService.pendingPage(param), methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE) + @ApiOperation("审批通过") + @PostMapping("/pass") + public HttpResult pass(@RequestBody @Validated ReportApprovalParam.AuditParam param) { + String methodDescribe = getMethodDescribe("pass"); + boolean result = reportApprovalService.pass(param); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, + result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE) + @ApiOperation("审批退回") + @PostMapping("/reject") + public HttpResult reject(@RequestBody @Validated ReportApprovalParam.AuditParam param) { + String methodDescribe = getMethodDescribe("reject"); + boolean result = reportApprovalService.reject(param); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, + result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询审批日志分页") + @GetMapping("/log-page") + public HttpResult> logPage(@ModelAttribute ReportApprovalParam.LogQueryParam param) { + String methodDescribe = getMethodDescribe("logPage"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, + reportApprovalService.logPage(param), methodDescribe); + } +} diff --git a/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/handler/AbstractReportApprovalHandler.java b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/handler/AbstractReportApprovalHandler.java new file mode 100644 index 0000000..e74b574 --- /dev/null +++ b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/handler/AbstractReportApprovalHandler.java @@ -0,0 +1,85 @@ +package com.njcn.gather.aireport.reportapproval.handler; + +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.reportapproval.component.ReportApprovalUserNameResolver; +import com.njcn.gather.aireport.reportapproval.mapper.ReportApprovalMapper; +import com.njcn.gather.aireport.reportapproval.pojo.constant.ReportApprovalConst; +import com.njcn.gather.aireport.reportapproval.pojo.param.ReportApprovalParam; +import com.njcn.gather.aireport.reportapproval.pojo.po.ReportApprovalLogPO; +import com.njcn.web.utils.RequestUtil; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Map; +import java.util.UUID; + +public abstract class AbstractReportApprovalHandler implements ReportApprovalHandler { + + private final ReportApprovalMapper reportApprovalMapper; + private final ReportApprovalUserNameResolver reportApprovalUserNameResolver; + + protected AbstractReportApprovalHandler(ReportApprovalMapper reportApprovalMapper, + ReportApprovalUserNameResolver reportApprovalUserNameResolver) { + this.reportApprovalMapper = reportApprovalMapper; + this.reportApprovalUserNameResolver = reportApprovalUserNameResolver; + } + + @Override + public boolean audit(ReportApprovalParam.AuditParam param, boolean passed) { + if (param == null || StrUtil.isBlank(param.getReportId())) { + throw new BusinessException(CommonResponseEnum.FAIL, "审批参数不能为空"); + } + String reason = trimToNull(param.getApprovalReason()); + if (reason == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "审批原因不能为空"); + } + return doAudit(param.getReportId(), reason, passed); + } + + protected abstract boolean doAudit(String reportId, String reason, boolean passed); + + protected void saveLog(String reportId, String reportName, String beforeState, String afterState, + String reason, String approverId, LocalDateTime approvalTime) { + ReportApprovalLogPO log = new ReportApprovalLogPO(); + log.setId(UUID.randomUUID().toString()); + log.setReportType(getReportType()); + log.setReportId(reportId); + log.setReportName(reportName); + log.setApprovalResult(afterState); + log.setApprovalReason(reason); + log.setBeforeState(beforeState); + log.setAfterState(afterState); + log.setApproverId(approverId); + log.setApproverName(resolveUserName(approverId)); + log.setApprovalTime(approvalTime); + log.setStatus(ReportApprovalConst.STATUS_NORMAL); + log.setCreateBy(approverId); + log.setCreateTime(approvalTime); + log.setUpdateBy(approverId); + log.setUpdateTime(approvalTime); + reportApprovalMapper.insert(log); + } + + protected String resolveCurrentUserId() { + String userId = RequestUtil.getUserId(); + return StrUtil.isBlank(userId) ? null : userId; + } + + private String resolveUserName(String userId) { + if (StrUtil.isBlank(userId)) { + return null; + } + Map userNameMap = reportApprovalUserNameResolver.resolveUserNames(Arrays.asList(userId)); + return userNameMap.containsKey(userId) ? userNameMap.get(userId) : userId; + } + + private String trimToNull(String value) { + if (value == null) { + return null; + } + String text = value.trim(); + return text.isEmpty() ? null : text; + } +} diff --git a/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/handler/ReportApprovalHandler.java b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/handler/ReportApprovalHandler.java new file mode 100644 index 0000000..189e6dc --- /dev/null +++ b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/handler/ReportApprovalHandler.java @@ -0,0 +1,10 @@ +package com.njcn.gather.aireport.reportapproval.handler; + +import com.njcn.gather.aireport.reportapproval.pojo.param.ReportApprovalParam; + +public interface ReportApprovalHandler { + + String getReportType(); + + boolean audit(ReportApprovalParam.AuditParam param, boolean passed); +} diff --git a/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/handler/ReportApprovalHandlerRegistry.java b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/handler/ReportApprovalHandlerRegistry.java new file mode 100644 index 0000000..24ff9f2 --- /dev/null +++ b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/handler/ReportApprovalHandlerRegistry.java @@ -0,0 +1,35 @@ +package com.njcn.gather.aireport.reportapproval.handler; + +import cn.hutool.core.util.StrUtil; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import org.springframework.stereotype.Component; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@Component +public class ReportApprovalHandlerRegistry { + + private final Map handlerMap = new LinkedHashMap(); + + public ReportApprovalHandlerRegistry(List handlers) { + if (handlers == null || handlers.isEmpty()) { + return; + } + for (ReportApprovalHandler handler : handlers) { + if (handler != null && StrUtil.isNotBlank(handler.getReportType())) { + handlerMap.put(handler.getReportType(), handler); + } + } + } + + public ReportApprovalHandler requireHandler(String reportType) { + ReportApprovalHandler handler = handlerMap.get(reportType); + if (handler == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "不支持的报告类型"); + } + return handler; + } +} diff --git a/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/handler/ReportModelApprovalHandler.java b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/handler/ReportModelApprovalHandler.java new file mode 100644 index 0000000..51edaee --- /dev/null +++ b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/handler/ReportModelApprovalHandler.java @@ -0,0 +1,58 @@ +package com.njcn.gather.aireport.reportapproval.handler; + +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.aireport.reportapproval.component.ReportApprovalUserNameResolver; +import com.njcn.gather.aireport.reportapproval.mapper.ReportApprovalMapper; +import com.njcn.gather.aireport.reportapproval.pojo.constant.ReportApprovalConst; +import com.njcn.gather.aireport.reportmodel.pojo.constant.ReportModelConst; +import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO; +import com.njcn.gather.aireport.reportmodel.service.ReportModelService; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +@Component +public class ReportModelApprovalHandler extends AbstractReportApprovalHandler { + + private final ReportModelService reportModelService; + + public ReportModelApprovalHandler(ReportApprovalMapper reportApprovalMapper, + ReportApprovalUserNameResolver reportApprovalUserNameResolver, + ReportModelService reportModelService) { + super(reportApprovalMapper, reportApprovalUserNameResolver); + this.reportModelService = reportModelService; + } + + @Override + public String getReportType() { + return ReportApprovalConst.REPORT_TYPE_REPORT_MODEL; + } + + @Override + protected boolean doAudit(String reportId, String reason, boolean passed) { + ReportModelPO reportModel = reportModelService.requireNormal(reportId); + if (!ReportModelConst.STATE_AUDITING.equals(reportModel.getState())) { + throw new BusinessException(CommonResponseEnum.FAIL, "仅待审状态的报告模板允许审批"); + } + String userId = resolveCurrentUserId(); + LocalDateTime now = LocalDateTime.now(); + String targetState = passed ? ReportModelConst.STATE_APPROVED : ReportModelConst.STATE_REJECTED; + boolean updated = reportModelService.lambdaUpdate() + .set(ReportModelPO::getState, targetState) + .set(ReportModelPO::getCheckId, userId) + .set(ReportModelPO::getCheckTime, now) + .set(ReportModelPO::getCheckResult, reason) + .set(ReportModelPO::getUpdateBy, userId) + .set(ReportModelPO::getUpdateTime, now) + .eq(ReportModelPO::getId, reportId) + .eq(ReportModelPO::getStatus, ReportModelConst.STATUS_NORMAL) + .eq(ReportModelPO::getState, ReportModelConst.STATE_AUDITING) + .update(); + if (!updated) { + throw new BusinessException(CommonResponseEnum.FAIL, "报告模板状态已变更,请刷新后重试"); + } + saveLog(reportModel.getId(), reportModel.getName(), ReportModelConst.STATE_AUDITING, targetState, reason, userId, now); + return true; + } +} diff --git a/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/handler/TestReportApprovalHandler.java b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/handler/TestReportApprovalHandler.java new file mode 100644 index 0000000..448e51f --- /dev/null +++ b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/handler/TestReportApprovalHandler.java @@ -0,0 +1,59 @@ +package com.njcn.gather.aireport.reportapproval.handler; + +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.aireport.reportapproval.component.ReportApprovalUserNameResolver; +import com.njcn.gather.aireport.reportapproval.mapper.ReportApprovalMapper; +import com.njcn.gather.aireport.reportapproval.pojo.constant.ReportApprovalConst; +import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst; +import com.njcn.gather.aireport.testreport.pojo.po.TestReportPO; +import com.njcn.gather.aireport.testreport.service.TestReportService; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Component +public class TestReportApprovalHandler extends AbstractReportApprovalHandler { + + private final TestReportService testReportService; + + public TestReportApprovalHandler(ReportApprovalMapper reportApprovalMapper, + ReportApprovalUserNameResolver reportApprovalUserNameResolver, + TestReportService testReportService) { + super(reportApprovalMapper, reportApprovalUserNameResolver); + this.testReportService = testReportService; + } + + @Override + public String getReportType() { + return ReportApprovalConst.REPORT_TYPE_TEST_REPORT; + } + + @Override + protected boolean doAudit(String reportId, String reason, boolean passed) { + TestReportPO testReport = testReportService.requireNormal(reportId); + if (!TestReportConst.STATE_AUDITING.equals(testReport.getState())) { + throw new BusinessException(CommonResponseEnum.FAIL, "仅待审状态的普测报告允许审批"); + } + String userId = resolveCurrentUserId(); + LocalDateTime now = LocalDateTime.now(); + String targetState = passed ? TestReportConst.STATE_APPROVED : TestReportConst.STATE_REJECTED; + boolean updated = testReportService.lambdaUpdate() + .set(TestReportPO::getState, targetState) + .set(TestReportPO::getCheckId, userId) + .set(TestReportPO::getCheckTime, LocalDate.now()) + .set(TestReportPO::getCheckResult, reason) + .set(TestReportPO::getUpdateBy, userId) + .set(TestReportPO::getUpdateTime, now) + .eq(TestReportPO::getId, reportId) + .eq(TestReportPO::getStatus, TestReportConst.STATUS_NORMAL) + .eq(TestReportPO::getState, TestReportConst.STATE_AUDITING) + .update(); + if (!updated) { + throw new BusinessException(CommonResponseEnum.FAIL, "普测报告状态已变更,请刷新后重试"); + } + saveLog(testReport.getId(), testReport.getNo(), TestReportConst.STATE_AUDITING, targetState, reason, userId, now); + return true; + } +} diff --git a/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/mapper/ReportApprovalMapper.java b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/mapper/ReportApprovalMapper.java new file mode 100644 index 0000000..363ec2f --- /dev/null +++ b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/mapper/ReportApprovalMapper.java @@ -0,0 +1,95 @@ +package com.njcn.gather.aireport.reportapproval.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.njcn.gather.aireport.reportapproval.pojo.param.ReportApprovalParam; +import com.njcn.gather.aireport.reportapproval.pojo.po.ReportApprovalLogPO; +import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalLogVO; +import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalPendingVO; +import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelUserNameVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface ReportApprovalMapper extends BaseMapper { + + @Select({ + "" + }) + List selectUserNamesByIds(@Param("ids") Collection ids); + + @Select({ + "" + }) + long countPendingReports(@Param("param") ReportApprovalParam.PendingQueryParam param); + + @Select({ + "" + }) + List selectPendingPage(@Param("offset") long offset, + @Param("size") long size, + @Param("param") ReportApprovalParam.PendingQueryParam param); + + @Select({ + "" + }) + Page selectLogPage(Page page, + @Param("param") ReportApprovalParam.LogQueryParam param); +} diff --git a/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/pojo/constant/ReportApprovalConst.java b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/pojo/constant/ReportApprovalConst.java new file mode 100644 index 0000000..2ff98e2 --- /dev/null +++ b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/pojo/constant/ReportApprovalConst.java @@ -0,0 +1,12 @@ +package com.njcn.gather.aireport.reportapproval.pojo.constant; + +public final class ReportApprovalConst { + + public static final String REPORT_TYPE_REPORT_MODEL = "REPORT_MODEL"; + public static final String REPORT_TYPE_TEST_REPORT = "TEST_REPORT"; + + public static final Integer STATUS_NORMAL = 1; + + private ReportApprovalConst() { + } +} diff --git a/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/pojo/param/ReportApprovalParam.java b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/pojo/param/ReportApprovalParam.java new file mode 100644 index 0000000..34426cf --- /dev/null +++ b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/pojo/param/ReportApprovalParam.java @@ -0,0 +1,42 @@ +package com.njcn.gather.aireport.reportapproval.pojo.param; + +import com.njcn.web.pojo.param.BaseParam; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +public class ReportApprovalParam { + + @Data + public static class AuditParam { + @ApiModelProperty("报告类型") + @NotBlank(message = "报告类型不能为空") + private String reportType; + + @ApiModelProperty("报告ID") + @NotBlank(message = "报告ID不能为空") + private String reportId; + + @ApiModelProperty("审批原因") + @NotBlank(message = "审批原因不能为空") + @Size(max = 128, message = "审批原因不能超过128个字符") + private String approvalReason; + } + + @Data + @EqualsAndHashCode(callSuper = true) + public static class PendingQueryParam extends BaseParam { + @ApiModelProperty("报告类型") + private String reportType; + } + + @Data + @EqualsAndHashCode(callSuper = true) + public static class LogQueryParam extends BaseParam { + @ApiModelProperty("报告类型") + private String reportType; + } +} diff --git a/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/pojo/po/ReportApprovalLogPO.java b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/pojo/po/ReportApprovalLogPO.java new file mode 100644 index 0000000..cdfc9bd --- /dev/null +++ b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/pojo/po/ReportApprovalLogPO.java @@ -0,0 +1,48 @@ +package com.njcn.gather.aireport.reportapproval.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +@Data +@TableName("ai_report_approval_log") +public class ReportApprovalLogPO implements Serializable { + private static final long serialVersionUID = 1900859716151337132L; + + @TableId("id") + private String id; + @TableField("report_type") + private String reportType; + @TableField("report_id") + private String reportId; + @TableField("report_name") + private String reportName; + @TableField("approval_result") + private String approvalResult; + @TableField("approval_reason") + private String approvalReason; + @TableField("before_state") + private String beforeState; + @TableField("after_state") + private String afterState; + @TableField("approver_id") + private String approverId; + @TableField("approver_name") + private String approverName; + @TableField("approval_time") + private LocalDateTime approvalTime; + @TableField("status") + private Integer status; + @TableField("create_by") + private String createBy; + @TableField("create_time") + private LocalDateTime createTime; + @TableField("update_by") + private String updateBy; + @TableField("update_time") + private LocalDateTime updateTime; +} diff --git a/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/pojo/vo/ReportApprovalLogVO.java b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/pojo/vo/ReportApprovalLogVO.java new file mode 100644 index 0000000..25505de --- /dev/null +++ b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/pojo/vo/ReportApprovalLogVO.java @@ -0,0 +1,38 @@ +package com.njcn.gather.aireport.reportapproval.pojo.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class ReportApprovalLogVO { + @ApiModelProperty("日志ID") + private String id; + @ApiModelProperty("报告类型") + private String reportType; + @ApiModelProperty("报告ID") + private String reportId; + @ApiModelProperty("报告名称") + private String reportName; + @ApiModelProperty("审批结果") + private String approvalResult; + @ApiModelProperty("审批原因") + private String approvalReason; + @ApiModelProperty("审批前状态") + private String beforeState; + @ApiModelProperty("审批后状态") + private String afterState; + @ApiModelProperty("审批人") + private String approverName; + @ApiModelProperty("审批时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + private LocalDateTime approvalTime; +} diff --git a/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/pojo/vo/ReportApprovalPendingVO.java b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/pojo/vo/ReportApprovalPendingVO.java new file mode 100644 index 0000000..7640892 --- /dev/null +++ b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/pojo/vo/ReportApprovalPendingVO.java @@ -0,0 +1,32 @@ +package com.njcn.gather.aireport.reportapproval.pojo.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class ReportApprovalPendingVO { + @ApiModelProperty("报告类型") + private String reportType; + @ApiModelProperty("报告ID") + private String reportId; + @ApiModelProperty("报告名称") + private String reportName; + @ApiModelProperty("报告状态") + private String state; + @ApiModelProperty("提交人ID") + private String submitterId; + @ApiModelProperty("提交人") + private String submitterName; + @ApiModelProperty("提交时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + private LocalDateTime submitTime; +} diff --git a/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/service/ReportApprovalService.java b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/service/ReportApprovalService.java new file mode 100644 index 0000000..df70e57 --- /dev/null +++ b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/service/ReportApprovalService.java @@ -0,0 +1,17 @@ +package com.njcn.gather.aireport.reportapproval.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.njcn.gather.aireport.reportapproval.pojo.param.ReportApprovalParam; +import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalLogVO; +import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalPendingVO; + +public interface ReportApprovalService { + + boolean pass(ReportApprovalParam.AuditParam param); + + boolean reject(ReportApprovalParam.AuditParam param); + + Page pendingPage(ReportApprovalParam.PendingQueryParam param); + + Page logPage(ReportApprovalParam.LogQueryParam param); +} diff --git a/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/service/impl/ReportApprovalServiceImpl.java b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/service/impl/ReportApprovalServiceImpl.java new file mode 100644 index 0000000..b6aed8e --- /dev/null +++ b/ai-report/report-approval/src/main/java/com/njcn/gather/aireport/reportapproval/service/impl/ReportApprovalServiceImpl.java @@ -0,0 +1,77 @@ +package com.njcn.gather.aireport.reportapproval.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.aireport.reportapproval.handler.ReportApprovalHandler; +import com.njcn.gather.aireport.reportapproval.handler.ReportApprovalHandlerRegistry; +import com.njcn.gather.aireport.reportapproval.mapper.ReportApprovalMapper; +import com.njcn.gather.aireport.reportapproval.pojo.param.ReportApprovalParam; +import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalLogVO; +import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalPendingVO; +import com.njcn.gather.aireport.reportapproval.service.ReportApprovalService; +import com.njcn.web.factory.PageFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collections; +import java.util.List; + +@Service +public class ReportApprovalServiceImpl implements ReportApprovalService { + + private final ReportApprovalMapper reportApprovalMapper; + private final ReportApprovalHandlerRegistry reportApprovalHandlerRegistry; + + public ReportApprovalServiceImpl(ReportApprovalMapper reportApprovalMapper, + ReportApprovalHandlerRegistry reportApprovalHandlerRegistry) { + this.reportApprovalMapper = reportApprovalMapper; + this.reportApprovalHandlerRegistry = reportApprovalHandlerRegistry; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean pass(ReportApprovalParam.AuditParam param) { + return audit(param, true); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean reject(ReportApprovalParam.AuditParam param) { + return audit(param, false); + } + + @Override + public Page pendingPage(ReportApprovalParam.PendingQueryParam param) { + ReportApprovalParam.PendingQueryParam query = param == null ? new ReportApprovalParam.PendingQueryParam() : param; + if (StrUtil.isNotBlank(query.getReportType())) { + reportApprovalHandlerRegistry.requireHandler(query.getReportType()); + } + long current = PageFactory.getPageNum(query); + long size = PageFactory.getPageSize(query); + long offset = Math.max(0L, (current - 1L) * size); + long total = reportApprovalMapper.countPendingReports(query); + List records = total <= 0 + ? Collections.emptyList() + : reportApprovalMapper.selectPendingPage(offset, size, query); + Page page = new Page(current, size, total); + page.setRecords(records); + return page; + } + + @Override + public Page logPage(ReportApprovalParam.LogQueryParam param) { + ReportApprovalParam.LogQueryParam query = param == null ? new ReportApprovalParam.LogQueryParam() : param; + return reportApprovalMapper.selectLogPage( + new Page(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), query); + } + + private boolean audit(ReportApprovalParam.AuditParam param, boolean passed) { + if (param == null || StrUtil.isBlank(param.getReportType()) || StrUtil.isBlank(param.getReportId())) { + throw new BusinessException(CommonResponseEnum.FAIL, "审批参数不能为空"); + } + ReportApprovalHandler handler = reportApprovalHandlerRegistry.requireHandler(param.getReportType()); + return handler.audit(param, passed); + } +} diff --git a/ai-report/report-approval/src/test/java/com/njcn/gather/aireport/reportapproval/pojo/vo/ReportApprovalVOTimeFormatTest.java b/ai-report/report-approval/src/test/java/com/njcn/gather/aireport/reportapproval/pojo/vo/ReportApprovalVOTimeFormatTest.java new file mode 100644 index 0000000..eabc7b4 --- /dev/null +++ b/ai-report/report-approval/src/test/java/com/njcn/gather/aireport/reportapproval/pojo/vo/ReportApprovalVOTimeFormatTest.java @@ -0,0 +1,27 @@ +package com.njcn.gather.aireport.reportapproval.pojo.vo; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Assert; +import org.junit.Test; + +import java.time.LocalDateTime; + +public class ReportApprovalVOTimeFormatTest { + + @Test + public void pendingAndLogTimeJsonUseStandardTwentyFourHourFormat() throws Exception { + ReportApprovalPendingVO pendingVO = new ReportApprovalPendingVO(); + pendingVO.setSubmitTime(LocalDateTime.of(2026, 6, 29, 10, 0, 0)); + ReportApprovalLogVO logVO = new ReportApprovalLogVO(); + logVO.setApprovalTime(LocalDateTime.of(2026, 6, 29, 18, 30, 45)); + + ObjectMapper objectMapper = new ObjectMapper(); + String pendingJson = objectMapper.writeValueAsString(pendingVO); + String logJson = objectMapper.writeValueAsString(logVO); + + Assert.assertTrue(pendingJson.contains("\"submitTime\":\"2026-06-29 10:00:00\"")); + Assert.assertFalse(pendingJson.contains("2026-06-29T10")); + Assert.assertTrue(logJson.contains("\"approvalTime\":\"2026-06-29 18:30:45\"")); + Assert.assertFalse(logJson.contains("2026-06-29T18")); + } +} diff --git a/ai-report/report-approval/src/test/java/com/njcn/gather/aireport/reportapproval/service/impl/ReportApprovalServiceImplTest.java b/ai-report/report-approval/src/test/java/com/njcn/gather/aireport/reportapproval/service/impl/ReportApprovalServiceImplTest.java new file mode 100644 index 0000000..048c9fc --- /dev/null +++ b/ai-report/report-approval/src/test/java/com/njcn/gather/aireport/reportapproval/service/impl/ReportApprovalServiceImplTest.java @@ -0,0 +1,75 @@ +package com.njcn.gather.aireport.reportapproval.service.impl; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.njcn.gather.aireport.reportapproval.handler.ReportApprovalHandler; +import com.njcn.gather.aireport.reportapproval.handler.ReportApprovalHandlerRegistry; +import com.njcn.gather.aireport.reportapproval.mapper.ReportApprovalMapper; +import com.njcn.gather.aireport.reportapproval.pojo.constant.ReportApprovalConst; +import com.njcn.gather.aireport.reportapproval.pojo.param.ReportApprovalParam; +import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalPendingVO; +import org.junit.Assert; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ReportApprovalServiceImplTest { + + @Test + public void pendingPageShouldUseDatabasePagingResult() { + ReportApprovalMapper mapper = mock(ReportApprovalMapper.class); + ReportApprovalHandlerRegistry registry = new ReportApprovalHandlerRegistry(Collections.emptyList()); + ReportApprovalServiceImpl service = new ReportApprovalServiceImpl(mapper, registry); + ReportApprovalParam.PendingQueryParam param = new ReportApprovalParam.PendingQueryParam(); + param.setPageNum(2); + param.setPageSize(5); + + ReportApprovalPendingVO record = new ReportApprovalPendingVO(); + record.setReportType(ReportApprovalConst.REPORT_TYPE_REPORT_MODEL); + record.setReportId("model-001"); + record.setReportName("模板A"); + record.setSubmitterId("user-001"); + record.setSubmitterName("张三"); + record.setSubmitTime(LocalDateTime.of(2026, 6, 29, 10, 0, 0)); + List records = Arrays.asList(record); + when(mapper.countPendingReports(param)).thenReturn(11L); + when(mapper.selectPendingPage(5L, 5L, param)).thenReturn(records); + + Page page = service.pendingPage(param); + + Assert.assertEquals(2L, page.getCurrent()); + Assert.assertEquals(5L, page.getSize()); + Assert.assertEquals(11L, page.getTotal()); + Assert.assertEquals(1, page.getRecords().size()); + Assert.assertEquals("user-001", page.getRecords().get(0).getSubmitterId()); + Assert.assertEquals("张三", page.getRecords().get(0).getSubmitterName()); + verify(mapper).countPendingReports(param); + verify(mapper).selectPendingPage(5L, 5L, param); + } + + @Test + public void passShouldDelegateToMatchedHandler() { + ReportApprovalMapper mapper = mock(ReportApprovalMapper.class); + ReportApprovalHandler handler = mock(ReportApprovalHandler.class); + when(handler.getReportType()).thenReturn(ReportApprovalConst.REPORT_TYPE_TEST_REPORT); + when(handler.audit(org.mockito.ArgumentMatchers.any(ReportApprovalParam.AuditParam.class), eq(true))).thenReturn(true); + ReportApprovalHandlerRegistry registry = new ReportApprovalHandlerRegistry(Arrays.asList(handler)); + ReportApprovalServiceImpl service = new ReportApprovalServiceImpl(mapper, registry); + ReportApprovalParam.AuditParam param = new ReportApprovalParam.AuditParam(); + param.setReportType(ReportApprovalConst.REPORT_TYPE_TEST_REPORT); + param.setReportId("report-001"); + param.setApprovalReason("通过"); + + boolean result = service.pass(param); + + Assert.assertTrue(result); + verify(handler).audit(param, true); + } +} diff --git a/ai-report/report-model/pom.xml b/ai-report/report-model/pom.xml index 4da2f27..1d6db0d 100644 --- a/ai-report/report-model/pom.xml +++ b/ai-report/report-model/pom.xml @@ -11,8 +11,15 @@ report-model + jar + + com.njcn.gather + comservice + 1.0.0 + + com.njcn njcn-common @@ -30,5 +37,21 @@ spingboot2.3.12 2.3.12 + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-test + test + diff --git a/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/component/ReportModelFileStorageService.java b/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/component/ReportModelFileStorageService.java new file mode 100644 index 0000000..b52b1a2 --- /dev/null +++ b/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/component/ReportModelFileStorageService.java @@ -0,0 +1,132 @@ +package com.njcn.gather.aireport.reportmodel.component; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +/** + * Local storage for uploaded report model template files. + */ +@Component +public class ReportModelFileStorageService { + + private static final String DEFAULT_STORAGE_DIR = "data/report-model"; + private static final String REPORT_MODEL_STORAGE_DIR = "report-model"; + private static final DateTimeFormatter DATE_DIR_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + + @Value("${files.path:}") + private String filesPath; + + public StoredFile save(MultipartFile templateFile) { + if (templateFile == null || templateFile.isEmpty()) { + throw new IllegalArgumentException("模板文件不能为空"); + } + try { + Path storageRoot = resolveStorageRoot(); + String dateDir = LocalDate.now().format(DATE_DIR_FORMATTER); + Path storageDir = storageRoot.resolve(dateDir).normalize(); + if (!storageDir.startsWith(storageRoot)) { + throw new IllegalArgumentException("模板文件存储路径不合法"); + } + Files.createDirectories(storageDir); + String sanitizedFileName = sanitizeFileName(templateFile.getOriginalFilename()); + String storedFileName = UUID.randomUUID().toString().replace("-", "") + "-" + sanitizedFileName; + Path target = storageDir.resolve(storedFileName).normalize(); + ensurePathInRoot(storageRoot, target); + try (InputStream inputStream = templateFile.getInputStream()) { + Files.copy(inputStream, target, StandardCopyOption.REPLACE_EXISTING); + } + String storedPath = storageRoot.relativize(target).toString().replace('\\', '/'); + return new StoredFile(storedPath, sanitizedFileName); + } catch (Exception exception) { + throw new IllegalArgumentException("保存模板文件失败: " + exception.getMessage(), exception); + } + } + + public Path resolveDownloadPath(String storedPath) { + if (trimToNull(storedPath) == null) { + throw new IllegalArgumentException("模板文件路径不能为空"); + } + Path storageRoot = resolveStorageRoot(); + Path target = storageRoot.resolve(storedPath).normalize(); + ensurePathInRoot(storageRoot, target); + return target; + } + + public void deleteQuietly(String storedPath) { + try { + if (trimToNull(storedPath) != null) { + Files.deleteIfExists(resolveDownloadPath(storedPath)); + } + } catch (Exception ignored) { + // Cleanup failure must not hide the original database error. + } + } + + public String resolveDownloadFileName(String storedPath) { + Path fileName = Paths.get(storedPath).getFileName(); + if (fileName == null) { + return "report-model"; + } + String name = fileName.toString(); + int separatorIndex = name.indexOf('-'); + return separatorIndex >= 0 && separatorIndex + 1 < name.length() ? name.substring(separatorIndex + 1) : name; + } + + private Path resolveStorageRoot() { + String configuredPath = trimToNull(filesPath); + Path path = configuredPath == null ? Paths.get(DEFAULT_STORAGE_DIR) : Paths.get(configuredPath, REPORT_MODEL_STORAGE_DIR); + return path.toAbsolutePath().normalize(); + } + + private void ensurePathInRoot(Path storageRoot, Path target) { + if (!target.startsWith(storageRoot)) { + throw new IllegalArgumentException("模板文件路径不合法"); + } + } + + private String sanitizeFileName(String originalFilename) { + String fileName = trimToNull(originalFilename); + if (fileName == null) { + return "unnamed"; + } + String normalizedName = Paths.get(fileName).getFileName().toString(); + String sanitized = normalizedName.replaceAll("[\\\\/:*?\"<>|]", "_"); + return trimToNull(sanitized) == null ? "unnamed" : sanitized; + } + + private String trimToNull(String value) { + if (value == null) { + return null; + } + String text = value.trim(); + return text.isEmpty() ? null : text; + } + + public static class StoredFile { + private final String path; + private final String fileName; + + public StoredFile(String path, String fileName) { + this.path = path; + this.fileName = fileName; + } + + public String getPath() { + return path; + } + + public String getFileName() { + return fileName; + } + } +} diff --git a/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/component/ReportModelUserNameResolver.java b/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/component/ReportModelUserNameResolver.java new file mode 100644 index 0000000..c8b57ce --- /dev/null +++ b/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/component/ReportModelUserNameResolver.java @@ -0,0 +1,41 @@ +package com.njcn.gather.aireport.reportmodel.component; + +import cn.hutool.core.util.StrUtil; +import com.njcn.gather.aireport.reportmodel.mapper.ReportModelMapper; +import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelUserNameVO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Component +@RequiredArgsConstructor +public class ReportModelUserNameResolver { + + private final ReportModelMapper reportModelMapper; + + public Map resolveUserNames(Collection userIds) { + if (userIds == null || userIds.isEmpty()) { + return Collections.emptyMap(); + } + Set validIds = userIds.stream() + .filter(StrUtil::isNotBlank) + .collect(Collectors.toCollection(LinkedHashSet::new)); + if (validIds.isEmpty()) { + return Collections.emptyMap(); + } + List users = reportModelMapper.selectUserNamesByIds(validIds); + if (users == null || users.isEmpty()) { + return Collections.emptyMap(); + } + return users.stream() + .filter(user -> user != null && StrUtil.isNotBlank(user.getId()) && StrUtil.isNotBlank(user.getName())) + .collect(Collectors.toMap(ReportModelUserNameVO::getId, ReportModelUserNameVO::getName, (first, second) -> first)); + } +} diff --git a/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/controller/ReportModelController.java b/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/controller/ReportModelController.java new file mode 100644 index 0000000..1723975 --- /dev/null +++ b/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/controller/ReportModelController.java @@ -0,0 +1,173 @@ +package com.njcn.gather.aireport.reportmodel.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.njcn.common.pojo.annotation.OperateInfo; +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.exception.BusinessException; +import com.njcn.common.pojo.response.HttpResult; +import com.njcn.gather.aireport.reportmodel.component.ReportModelFileStorageService; +import com.njcn.gather.aireport.reportmodel.pojo.param.ReportModelParam; +import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO; +import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelVO; +import com.njcn.gather.aireport.reportmodel.service.ReportModelService; +import com.njcn.gather.comservice.filepreview.FilePreviewService; +import com.njcn.web.controller.BaseController; +import com.njcn.web.utils.HttpResultUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +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.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +/** + * Report model management endpoints. + */ +@Api(tags = "报告模板管理") +@RestController +@RequestMapping("/api/report-model") +@RequiredArgsConstructor +public class ReportModelController extends BaseController { + + private final ReportModelService reportModelService; + private final ReportModelFileStorageService reportModelFileStorageService; + private final FilePreviewService filePreviewService; + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询报告模板列表") + @PostMapping("/list") + public HttpResult> list(@RequestBody(required = false) ReportModelParam.QueryParam param) { + String methodDescribe = getMethodDescribe("list"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, + reportModelService.listReportModels(param), methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD) + @ApiOperation("新增报告模板") + @PostMapping(value = "/add", consumes = {"multipart/form-data"}) + public HttpResult add(@RequestPart("request") @Validated ReportModelParam.AddParam param, + @RequestPart("templateFile") MultipartFile templateFile) { + String methodDescribe = getMethodDescribe("add"); + boolean result = reportModelService.addReportModel(param, templateFile); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, + result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE) + @ApiOperation("编辑报告模板") + @PostMapping(value = "/update", consumes = {"multipart/form-data"}) + public HttpResult update(@RequestPart("request") @Validated ReportModelParam.UpdateParam param, + @RequestPart(value = "templateFile", required = false) MultipartFile templateFile) { + String methodDescribe = getMethodDescribe("update"); + boolean result = reportModelService.updateReportModel(param, templateFile); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, + result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DELETE) + @ApiOperation("删除报告模板") + @PostMapping("/delete") + public HttpResult delete(@RequestBody List ids) { + String methodDescribe = getMethodDescribe("delete"); + boolean result = reportModelService.deleteReportModels(ids); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, + result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询报告模板详情") + @ApiImplicitParam(name = "id", value = "模板ID", required = true) + @GetMapping("/{id}") + public HttpResult detail(@PathVariable("id") String id) { + String methodDescribe = getMethodDescribe("detail"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, + reportModelService.getReportModel(id), methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("下载报告模板文件") + @ApiImplicitParam(name = "id", value = "模板ID", required = true) + @GetMapping("/{id}/download") + public ResponseEntity download(@PathVariable("id") String id) { + ReportModelPO model = reportModelService.requireNormal(id); + Path filePath = reportModelFileStorageService.resolveDownloadPath(model.getPath()); + if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) { + throw new BusinessException(CommonResponseEnum.FAIL, "模板文件不存在"); + } + try { + String fileName = model.getFileName(); + if (fileName == null || fileName.trim().isEmpty()) { + fileName = reportModelFileStorageService.resolveDownloadFileName(model.getPath()); + } + String encodedFileName = encodeFileName(fileName); + InputStream inputStream = Files.newInputStream(filePath); + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFileName) + .body(new InputStreamResource(inputStream)); + } catch (Exception exception) { + throw new BusinessException(CommonResponseEnum.FAIL, "读取模板文件失败"); + } + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("预览报告模板文件") + @ApiImplicitParam(name = "id", value = "模板ID", required = true) + @GetMapping("/{id}/preview") + public ResponseEntity preview(@PathVariable("id") String id) { + ReportModelPO model = reportModelService.requireNormal(id); + Path filePath = reportModelFileStorageService.resolveDownloadPath(model.getPath()); + if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) { + throw new BusinessException(CommonResponseEnum.FAIL, "模板文件不存在"); + } + String fileName = model.getFileName(); + if (fileName == null || fileName.trim().isEmpty()) { + fileName = reportModelFileStorageService.resolveDownloadFileName(model.getPath()); + } + FilePreviewService.PreviewContent previewContent = filePreviewService.preview(filePath, fileName); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, + "inline; filename*=UTF-8''" + encodeFileName(fileName)) + .header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate") + .contentType(MediaType.parseMediaType(previewContent.getContentType())) + .body(previewContent.getContent()); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE) + @ApiOperation("提交报告模板审核") + @PostMapping("/submit-audit") + public HttpResult submitAudit(@RequestBody @Validated ReportModelParam.SubmitAuditParam param) { + String methodDescribe = getMethodDescribe("submitAudit"); + boolean result = reportModelService.submitAudit(param); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, + result, methodDescribe); + } + + private String encodeFileName(String fileName) { + try { + return URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replace("+", "%20"); + } catch (Exception exception) { + throw new BusinessException(CommonResponseEnum.FAIL, "编码模板文件名失败"); + } + } +} diff --git a/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/mapper/ReportModelMapper.java b/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/mapper/ReportModelMapper.java new file mode 100644 index 0000000..22ab5cb --- /dev/null +++ b/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/mapper/ReportModelMapper.java @@ -0,0 +1,25 @@ +package com.njcn.gather.aireport.reportmodel.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO; +import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelUserNameVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface ReportModelMapper extends BaseMapper { + + @Select({ + "" + }) + List selectUserNamesByIds(@Param("ids") Collection ids); +} diff --git a/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/pojo/constant/ReportModelConst.java b/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/pojo/constant/ReportModelConst.java new file mode 100644 index 0000000..01751f8 --- /dev/null +++ b/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/pojo/constant/ReportModelConst.java @@ -0,0 +1,19 @@ +package com.njcn.gather.aireport.reportmodel.pojo.constant; + +/** + * Report model state constants. + */ +public final class ReportModelConst { + + private ReportModelConst() { + } + + public static final String STATE_EDITING = "01"; + public static final String STATE_AUDITING = "02"; + public static final String STATE_APPROVED = "03"; + public static final String STATE_REJECTED = "04"; + public static final String STATE_DELETED = "05"; + + public static final Integer STATUS_DELETED = 0; + public static final Integer STATUS_NORMAL = 1; +} diff --git a/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/pojo/param/ReportModelParam.java b/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/pojo/param/ReportModelParam.java new file mode 100644 index 0000000..6e928b3 --- /dev/null +++ b/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/pojo/param/ReportModelParam.java @@ -0,0 +1,58 @@ +package com.njcn.gather.aireport.reportmodel.pojo.param; + +import com.njcn.web.pojo.param.BaseParam; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +/** + * Report model request parameters. + */ +public class ReportModelParam { + + @Data + @EqualsAndHashCode(callSuper = true) + public static class QueryParam extends BaseParam { + @ApiModelProperty("Template name keyword") + private String name; + + @ApiModelProperty("Template state") + private String state; + + @ApiModelProperty("Create time begin, yyyy-MM-dd HH:mm:ss") + private String searchBeginTime; + + @ApiModelProperty("Create time end, yyyy-MM-dd HH:mm:ss") + private String searchEndTime; + } + + @Data + public static class AddParam { + @ApiModelProperty("Template name") + @NotBlank(message = "模板名称不能为空") + @Size(max = 32, message = "模板名称不能超过32个字符") + private String name; + } + + @Data + public static class UpdateParam { + @ApiModelProperty("Template ID") + @NotBlank(message = "模板ID不能为空") + private String id; + + @ApiModelProperty("Template name") + @NotBlank(message = "模板名称不能为空") + @Size(max = 32, message = "模板名称不能超过32个字符") + private String name; + } + + @Data + public static class SubmitAuditParam { + @ApiModelProperty("Template ID") + @NotBlank(message = "模板ID不能为空") + private String id; + } +} diff --git a/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/pojo/po/ReportModelPO.java b/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/pojo/po/ReportModelPO.java new file mode 100644 index 0000000..32c63c2 --- /dev/null +++ b/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/pojo/po/ReportModelPO.java @@ -0,0 +1,45 @@ +package com.njcn.gather.aireport.reportmodel.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * Report model template record. + */ +@Data +@TableName("ai_reportmodel") +public class ReportModelPO implements Serializable { + private static final long serialVersionUID = -1413572272542367267L; + + @TableId("id") + private String id; + @TableField("name") + private String name; + @TableField("state") + private String state; + @TableField("check_id") + private String checkId; + @TableField("check_time") + private LocalDateTime checkTime; + @TableField("check_result") + private String checkResult; + @TableField("path") + private String path; + @TableField("file_name") + private String fileName; + @TableField("status") + private Integer status; + @TableField("create_by") + private String createBy; + @TableField("create_time") + private LocalDateTime createTime; + @TableField("update_by") + private String updateBy; + @TableField("update_time") + private LocalDateTime updateTime; +} diff --git a/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/pojo/vo/ReportModelUserNameVO.java b/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/pojo/vo/ReportModelUserNameVO.java new file mode 100644 index 0000000..a275cca --- /dev/null +++ b/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/pojo/vo/ReportModelUserNameVO.java @@ -0,0 +1,9 @@ +package com.njcn.gather.aireport.reportmodel.pojo.vo; + +import lombok.Data; + +@Data +public class ReportModelUserNameVO { + private String id; + private String name; +} diff --git a/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/pojo/vo/ReportModelVO.java b/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/pojo/vo/ReportModelVO.java new file mode 100644 index 0000000..604181a --- /dev/null +++ b/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/pojo/vo/ReportModelVO.java @@ -0,0 +1,108 @@ +package com.njcn.gather.aireport.reportmodel.pojo.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; + +/** + * Report model view object. + */ +@Data +public class ReportModelVO { + private static final String DEFAULT_EMPTY_DISPLAY = "-"; + + @ApiModelProperty("Template ID") + private String id; + @ApiModelProperty("Template name") + private String name; + @ApiModelProperty("Template state") + private String state; + @ApiModelProperty("Auditor ID") + private String checkId; + @ApiModelProperty("Auditor display name") + private String checkerName; + @ApiModelProperty("Audit time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + private LocalDateTime checkTime; + @ApiModelProperty("Audit comment") + private String checkResult; + @ApiModelProperty("Template file path") + private String path; + @ApiModelProperty("Template document file name") + private String fileName; + @ApiModelProperty("Logical status") + private Integer status; + @ApiModelProperty("Creator display name") + private String createBy; + @ApiModelProperty("Create time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + private LocalDateTime createTime; + @ApiModelProperty("Editor display name") + private String updateBy; + @ApiModelProperty("Editor display name") + private String editorName; + @ApiModelProperty("Update time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + private LocalDateTime updateTime; + + public String getId() { + return defaultDisplay(id); + } + + public String getName() { + return defaultDisplay(name); + } + + public String getState() { + return defaultDisplay(state); + } + + public String getCheckId() { + return defaultDisplay(checkId); + } + + public String getCheckerName() { + return defaultDisplay(checkerName); + } + + public String getCheckResult() { + return defaultDisplay(checkResult); + } + + public String getPath() { + return defaultDisplay(path); + } + + public String getFileName() { + return defaultDisplay(fileName); + } + + public String getCreateBy() { + return defaultDisplay(createBy); + } + + public String getUpdateBy() { + return defaultDisplay(updateBy); + } + + public String getEditorName() { + return defaultDisplay(editorName); + } + + private String defaultDisplay(String value) { + return StringUtils.hasText(value) ? value : DEFAULT_EMPTY_DISPLAY; + } +} diff --git a/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/service/ReportModelService.java b/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/service/ReportModelService.java new file mode 100644 index 0000000..cd06f85 --- /dev/null +++ b/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/service/ReportModelService.java @@ -0,0 +1,27 @@ +package com.njcn.gather.aireport.reportmodel.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.aireport.reportmodel.pojo.param.ReportModelParam; +import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO; +import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelVO; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +public interface ReportModelService extends IService { + + Page listReportModels(ReportModelParam.QueryParam param); + + boolean addReportModel(ReportModelParam.AddParam param, MultipartFile templateFile); + + boolean updateReportModel(ReportModelParam.UpdateParam param, MultipartFile templateFile); + + boolean deleteReportModels(List ids); + + ReportModelVO getReportModel(String id); + + ReportModelPO requireNormal(String id); + + boolean submitAudit(ReportModelParam.SubmitAuditParam param); +} diff --git a/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/service/impl/ReportModelServiceImpl.java b/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/service/impl/ReportModelServiceImpl.java new file mode 100644 index 0000000..13070f7 --- /dev/null +++ b/ai-report/report-model/src/main/java/com/njcn/gather/aireport/reportmodel/service/impl/ReportModelServiceImpl.java @@ -0,0 +1,279 @@ +package com.njcn.gather.aireport.reportmodel.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +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.component.ReportModelUserNameResolver; +import com.njcn.gather.aireport.reportmodel.mapper.ReportModelMapper; +import com.njcn.gather.aireport.reportmodel.pojo.constant.ReportModelConst; +import com.njcn.gather.aireport.reportmodel.pojo.param.ReportModelParam; +import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO; +import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelVO; +import com.njcn.gather.aireport.reportmodel.service.ReportModelService; +import com.njcn.web.factory.PageFactory; +import com.njcn.web.utils.RequestUtil; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * Report model management service. + */ +@Service +public class ReportModelServiceImpl extends ServiceImpl implements ReportModelService { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + private final ReportModelFileStorageService reportModelFileStorageService; + private final ReportModelUserNameResolver reportModelUserNameResolver; + + public ReportModelServiceImpl(ReportModelFileStorageService reportModelFileStorageService, + ReportModelUserNameResolver reportModelUserNameResolver) { + this.reportModelFileStorageService = reportModelFileStorageService; + this.reportModelUserNameResolver = reportModelUserNameResolver; + } + + @Override + public Page listReportModels(ReportModelParam.QueryParam param) { + ReportModelParam.QueryParam query = param == null ? new ReportModelParam.QueryParam() : param; + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(ReportModelPO::getStatus, ReportModelConst.STATUS_NORMAL) + .like(StrUtil.isNotBlank(query.getName()), ReportModelPO::getName, query.getName()) + .eq(StrUtil.isNotBlank(query.getState()), ReportModelPO::getState, query.getState()); + if (StrUtil.isNotBlank(query.getSearchBeginTime())) { + wrapper.ge(ReportModelPO::getUpdateTime, parseDateTime(query.getSearchBeginTime(), "开始时间格式不正确")); + } + if (StrUtil.isNotBlank(query.getSearchEndTime())) { + wrapper.le(ReportModelPO::getUpdateTime, parseDateTime(query.getSearchEndTime(), "结束时间格式不正确")); + } + wrapper.orderByDesc(ReportModelPO::getCreateTime); + Page page = this.page(new Page<>(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), wrapper); + Page result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal()); + Map userNameMap = resolveUserNameMap(page.getRecords()); + result.setRecords(page.getRecords().stream().map(model -> toVO(model, userNameMap)).collect(Collectors.toList())); + return result; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean addReportModel(ReportModelParam.AddParam param, MultipartFile templateFile) { + String name = resolveName(param == null ? null : param.getName()); + checkNameUnique(name, null); + ReportModelFileStorageService.StoredFile storedFile = reportModelFileStorageService.save(templateFile); + try { + LocalDateTime now = LocalDateTime.now(); + String userId = resolveCurrentUserId(); + ReportModelPO model = new ReportModelPO(); + model.setId(UUID.randomUUID().toString()); + model.setName(name); + model.setState(ReportModelConst.STATE_EDITING); + model.setPath(storedFile.getPath()); + model.setFileName(storedFile.getFileName()); + model.setStatus(ReportModelConst.STATUS_NORMAL); + model.setCreateBy(userId); + model.setCreateTime(now); + model.setUpdateBy(userId); + model.setUpdateTime(now); + return this.save(model); + } catch (RuntimeException exception) { + reportModelFileStorageService.deleteQuietly(storedFile.getPath()); + throw exception; + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateReportModel(ReportModelParam.UpdateParam param, MultipartFile templateFile) { + if (param == null || StrUtil.isBlank(param.getId())) { + throw new BusinessException(CommonResponseEnum.FAIL, "模板ID不能为空"); + } + ReportModelPO model = requireNormal(param.getId()); + if (!allowEdit(model.getState())) { + throw new BusinessException(CommonResponseEnum.FAIL, "仅编制或退回状态的模板允许编辑"); + } + String name = resolveName(param.getName()); + checkNameUnique(name, model.getId()); + String newStoredPath = null; + if (templateFile != null && !templateFile.isEmpty()) { + ReportModelFileStorageService.StoredFile storedFile = reportModelFileStorageService.save(templateFile); + newStoredPath = storedFile.getPath(); + model.setPath(storedFile.getPath()); + model.setFileName(storedFile.getFileName()); + } + try { + model.setName(name); + model.setUpdateBy(resolveCurrentUserId()); + model.setUpdateTime(LocalDateTime.now()); + return this.updateById(model); + } catch (RuntimeException exception) { + reportModelFileStorageService.deleteQuietly(newStoredPath); + throw exception; + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean deleteReportModels(List ids) { + if (ids == null || ids.isEmpty()) { + throw new BusinessException(CommonResponseEnum.FAIL, "模板ID不能为空"); + } + List validIds = ids.stream().filter(StrUtil::isNotBlank).collect(Collectors.toList()); + if (validIds.isEmpty()) { + throw new BusinessException(CommonResponseEnum.FAIL, "模板ID不能为空"); + } + return this.lambdaUpdate() + .set(ReportModelPO::getStatus, ReportModelConst.STATUS_DELETED) + .set(ReportModelPO::getState, ReportModelConst.STATE_DELETED) + .set(ReportModelPO::getUpdateBy, resolveCurrentUserId()) + .set(ReportModelPO::getUpdateTime, LocalDateTime.now()) + .in(ReportModelPO::getId, validIds) + .eq(ReportModelPO::getStatus, ReportModelConst.STATUS_NORMAL) + .update(); + } + + @Override + public ReportModelVO getReportModel(String id) { + ReportModelPO model = requireNormal(id); + return toVO(model, resolveUserNameMap(Arrays.asList(model))); + } + + @Override + public ReportModelPO requireNormal(String id) { + if (StrUtil.isBlank(id)) { + throw new BusinessException(CommonResponseEnum.FAIL, "模板ID不能为空"); + } + ReportModelPO model = this.lambdaQuery() + .eq(ReportModelPO::getId, id) + .eq(ReportModelPO::getStatus, ReportModelConst.STATUS_NORMAL) + .one(); + if (model == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "模板不存在或已删除"); + } + return model; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean submitAudit(ReportModelParam.SubmitAuditParam param) { + if (param == null || StrUtil.isBlank(param.getId())) { + throw new BusinessException(CommonResponseEnum.FAIL, "模板ID不能为空"); + } + ReportModelPO model = requireNormal(param.getId()); + if (!ReportModelConst.STATE_EDITING.equals(model.getState())) { + throw new BusinessException(CommonResponseEnum.FAIL, "仅编制状态的模板允许提交审核"); + } + model.setState(ReportModelConst.STATE_AUDITING); + model.setCheckId(null); + model.setCheckTime(null); + model.setCheckResult(null); + model.setUpdateBy(resolveCurrentUserId()); + model.setUpdateTime(LocalDateTime.now()); + return this.updateById(model); + } + + private boolean allowEdit(String state) { + return ReportModelConst.STATE_EDITING.equals(state) || ReportModelConst.STATE_REJECTED.equals(state); + } + + private void checkNameUnique(String name, String excludeId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(ReportModelPO::getName, name) + .eq(ReportModelPO::getStatus, ReportModelConst.STATUS_NORMAL) + .ne(StrUtil.isNotBlank(excludeId), ReportModelPO::getId, excludeId); + if (this.count(wrapper) > 0) { + throw new BusinessException(CommonResponseEnum.FAIL, "模板名称已存在"); + } + } + + private String resolveName(String name) { + String result = trimToNull(name); + if (result == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "模板名称不能为空"); + } + if (result.length() > 32) { + throw new BusinessException(CommonResponseEnum.FAIL, "模板名称不能超过32个字符"); + } + return result; + } + + private LocalDateTime parseDateTime(String value, String errorMessage) { + if (StrUtil.isBlank(value)) { + return null; + } + try { + return LocalDateTime.parse(value.trim(), DATE_TIME_FORMATTER); + } catch (DateTimeParseException exception) { + throw new BusinessException(CommonResponseEnum.FAIL, errorMessage); + } + } + + private ReportModelVO toVO(ReportModelPO model) { + return toVO(model, resolveUserNameMap(Arrays.asList(model))); + } + + private ReportModelVO toVO(ReportModelPO model, Map userNameMap) { + ReportModelVO vo = new ReportModelVO(); + BeanUtil.copyProperties(model, vo); + vo.setCreateBy(resolveUserName(model.getCreateBy(), userNameMap)); + vo.setUpdateBy(resolveUserName(model.getUpdateBy(), userNameMap)); + vo.setEditorName(resolveEditorName(model, userNameMap)); + vo.setCheckerName(resolveUserName(model.getCheckId(), userNameMap)); + return vo; + } + + /** + * 列表“编辑人”优先取更新人,新增后若尚未产生独立更新记录,则回退到创建人。 + */ + private Map resolveUserNameMap(List models) { + List userIds = models.stream() + .flatMap(model -> Arrays.asList(model.getCreateBy(), model.getUpdateBy(), model.getCheckId()).stream()) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toList()); + return reportModelUserNameResolver.resolveUserNames(userIds); + } + + private String resolveEditorName(ReportModelPO model, Map userNameMap) { + if (model == null) { + return null; + } + if (StrUtil.isNotBlank(model.getUpdateBy())) { + return resolveUserName(model.getUpdateBy(), userNameMap); + } + return StrUtil.isNotBlank(model.getCreateBy()) ? resolveUserName(model.getCreateBy(), userNameMap) : null; + } + + private String resolveUserName(String userId, Map userNameMap) { + if (StrUtil.isBlank(userId)) { + return null; + } + String userName = userNameMap == null ? null : userNameMap.get(userId); + return StrUtil.isNotBlank(userName) ? userName : userId; + } + + private String resolveCurrentUserId() { + String userId = RequestUtil.getUserId(); + return StrUtil.isBlank(userId) ? null : userId; + } + + private String trimToNull(String value) { + if (value == null) { + return null; + } + String text = value.trim(); + return text.isEmpty() ? null : text; + } +} diff --git a/ai-report/report-model/src/test/java/com/njcn/gather/aireport/reportmodel/component/ReportModelFileStorageServiceTest.java b/ai-report/report-model/src/test/java/com/njcn/gather/aireport/reportmodel/component/ReportModelFileStorageServiceTest.java new file mode 100644 index 0000000..5e4c7b7 --- /dev/null +++ b/ai-report/report-model/src/test/java/com/njcn/gather/aireport/reportmodel/component/ReportModelFileStorageServiceTest.java @@ -0,0 +1,39 @@ +package com.njcn.gather.aireport.reportmodel.component; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.util.ReflectionTestUtils; + +import java.nio.file.Files; +import java.nio.file.Path; + +public class ReportModelFileStorageServiceTest { + + @Test + public void saveSanitizesFileNameAndReturnsRelativePath() throws Exception { + Path filesRoot = Files.createTempDirectory("files-root"); + ReportModelFileStorageService service = new ReportModelFileStorageService(); + ReflectionTestUtils.setField(service, "filesPath", filesRoot.toString()); + + ReportModelFileStorageService.StoredFile storedFile = service.save(new MockMultipartFile("templateFile", "../a:b.docx", + "application/octet-stream", "content".getBytes("UTF-8"))); + String storedPath = storedFile.getPath(); + + Assert.assertFalse(storedPath.contains("..")); + Assert.assertTrue(storedPath.endsWith("-a_b.docx")); + Assert.assertEquals("a_b.docx", storedFile.getFileName()); + Assert.assertTrue(service.resolveDownloadPath(storedPath) + .startsWith(filesRoot.resolve("report-model").toAbsolutePath().normalize())); + Assert.assertTrue(Files.exists(service.resolveDownloadPath(storedPath))); + } + + @Test(expected = IllegalArgumentException.class) + public void resolveDownloadPathRejectsPathOutsideStorageRoot() throws Exception { + Path filesRoot = Files.createTempDirectory("files-root"); + ReportModelFileStorageService service = new ReportModelFileStorageService(); + ReflectionTestUtils.setField(service, "filesPath", filesRoot.toString()); + + service.resolveDownloadPath("../outside.docx"); + } +} diff --git a/ai-report/report-model/src/test/java/com/njcn/gather/aireport/reportmodel/component/ReportModelUserNameResolverTest.java b/ai-report/report-model/src/test/java/com/njcn/gather/aireport/reportmodel/component/ReportModelUserNameResolverTest.java new file mode 100644 index 0000000..c4d18f1 --- /dev/null +++ b/ai-report/report-model/src/test/java/com/njcn/gather/aireport/reportmodel/component/ReportModelUserNameResolverTest.java @@ -0,0 +1,30 @@ +package com.njcn.gather.aireport.reportmodel.component; + +import com.njcn.gather.aireport.reportmodel.mapper.ReportModelMapper; +import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelUserNameVO; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ReportModelUserNameResolverTest { + + @Test + public void resolveUserNamesReturnsSysUserNameById() { + ReportModelMapper mapper = mock(ReportModelMapper.class); + ReportModelUserNameVO user = new ReportModelUserNameVO(); + user.setId("user-001"); + user.setName("张三"); + when(mapper.selectUserNamesByIds(anyCollection())).thenReturn(Arrays.asList(user)); + + ReportModelUserNameResolver resolver = new ReportModelUserNameResolver(mapper); + Map result = resolver.resolveUserNames(Arrays.asList("user-001")); + + Assert.assertEquals("张三", result.get("user-001")); + } +} diff --git a/ai-report/report-model/src/test/java/com/njcn/gather/aireport/reportmodel/pojo/vo/ReportModelVODefaultDisplayTest.java b/ai-report/report-model/src/test/java/com/njcn/gather/aireport/reportmodel/pojo/vo/ReportModelVODefaultDisplayTest.java new file mode 100644 index 0000000..9e16ad4 --- /dev/null +++ b/ai-report/report-model/src/test/java/com/njcn/gather/aireport/reportmodel/pojo/vo/ReportModelVODefaultDisplayTest.java @@ -0,0 +1,33 @@ +package com.njcn.gather.aireport.reportmodel.pojo.vo; + +import org.junit.Assert; +import org.junit.Test; + +public class ReportModelVODefaultDisplayTest { + + @Test + public void stringFieldsUseDashWhenValueMissing() { + ReportModelVO vo = new ReportModelVO(); + vo.setCheckerName(" "); + vo.setCheckResult(null); + vo.setEditorName(""); + vo.setUpdateBy(" "); + vo.setFileName(null); + + Assert.assertEquals("-", vo.getCheckerName()); + Assert.assertEquals("-", vo.getCheckResult()); + Assert.assertEquals("-", vo.getEditorName()); + Assert.assertEquals("-", vo.getUpdateBy()); + Assert.assertEquals("-", vo.getFileName()); + } + + @Test + public void stringFieldsKeepOriginalTextWhenValuePresent() { + ReportModelVO vo = new ReportModelVO(); + vo.setCheckerName("审核人A"); + vo.setCheckResult("审核通过"); + + Assert.assertEquals("审核人A", vo.getCheckerName()); + Assert.assertEquals("审核通过", vo.getCheckResult()); + } +} diff --git a/ai-report/report-model/src/test/java/com/njcn/gather/aireport/reportmodel/pojo/vo/ReportModelVOTimeFormatTest.java b/ai-report/report-model/src/test/java/com/njcn/gather/aireport/reportmodel/pojo/vo/ReportModelVOTimeFormatTest.java new file mode 100644 index 0000000..684e25a --- /dev/null +++ b/ai-report/report-model/src/test/java/com/njcn/gather/aireport/reportmodel/pojo/vo/ReportModelVOTimeFormatTest.java @@ -0,0 +1,23 @@ +package com.njcn.gather.aireport.reportmodel.pojo.vo; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Assert; +import org.junit.Test; + +import java.time.LocalDateTime; + +public class ReportModelVOTimeFormatTest { + + @Test + public void timeJsonUsesStandardTwentyFourHourFormat() throws Exception { + ReportModelVO vo = new ReportModelVO(); + vo.setCreateTime(LocalDateTime.of(2026, 6, 25, 13, 19, 25)); + vo.setUpdateTime(LocalDateTime.of(2026, 6, 25, 13, 20, 41)); + + String json = new ObjectMapper().writeValueAsString(vo); + + Assert.assertTrue(json.contains("\"createTime\":\"2026-06-25 13:19:25\"")); + Assert.assertTrue(json.contains("\"updateTime\":\"2026-06-25 13:20:41\"")); + Assert.assertFalse(json.contains("2026-06-25T13")); + } +} diff --git a/ai-report/report-model/src/test/java/com/njcn/gather/aireport/reportmodel/service/impl/ReportModelServiceImplTest.java b/ai-report/report-model/src/test/java/com/njcn/gather/aireport/reportmodel/service/impl/ReportModelServiceImplTest.java new file mode 100644 index 0000000..9734ef0 --- /dev/null +++ b/ai-report/report-model/src/test/java/com/njcn/gather/aireport/reportmodel/service/impl/ReportModelServiceImplTest.java @@ -0,0 +1,67 @@ +package com.njcn.gather.aireport.reportmodel.service.impl; + +import com.njcn.gather.aireport.reportmodel.component.ReportModelFileStorageService; +import com.njcn.gather.aireport.reportmodel.component.ReportModelUserNameResolver; +import com.njcn.gather.aireport.reportmodel.pojo.constant.ReportModelConst; +import com.njcn.gather.aireport.reportmodel.pojo.param.ReportModelParam; +import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO; +import org.junit.Assert; +import org.junit.Test; + +import java.time.LocalDateTime; + +import static org.mockito.Mockito.mock; + +public class ReportModelServiceImplTest { + + @Test + public void submitAuditShouldResetAuditFieldsWhenMoveToAuditing() { + ReportModelPO model = new ReportModelPO(); + model.setId("model-001"); + model.setState(ReportModelConst.STATE_EDITING); + model.setStatus(ReportModelConst.STATUS_NORMAL); + model.setCheckId("checker-001"); + model.setCheckTime(LocalDateTime.of(2026, 6, 26, 9, 30, 0)); + model.setCheckResult("已有审核意见"); + + TestableReportModelServiceImpl service = new TestableReportModelServiceImpl(model); + ReportModelParam.SubmitAuditParam param = new ReportModelParam.SubmitAuditParam(); + param.setId("model-001"); + + boolean result = service.submitAudit(param); + + Assert.assertTrue(result); + Assert.assertEquals(ReportModelConst.STATE_AUDITING, model.getState()); + Assert.assertNull(model.getCheckId()); + Assert.assertNull(model.getCheckTime()); + Assert.assertNull(model.getCheckResult()); + Assert.assertSame(model, service.getUpdatedModel()); + Assert.assertNotNull(model.getUpdateTime()); + } + + private static class TestableReportModelServiceImpl extends ReportModelServiceImpl { + + private final ReportModelPO model; + private ReportModelPO updatedModel; + + private TestableReportModelServiceImpl(ReportModelPO model) { + super(mock(ReportModelFileStorageService.class), mock(ReportModelUserNameResolver.class)); + this.model = model; + } + + @Override + public ReportModelPO requireNormal(String id) { + return model; + } + + @Override + public boolean updateById(ReportModelPO entity) { + this.updatedModel = entity; + return true; + } + + private ReportModelPO getUpdatedModel() { + return updatedModel; + } + } +} diff --git a/ai-report/test-device/pom.xml b/ai-report/test-device/pom.xml index 1d2d4ed..c6cd3cc 100644 --- a/ai-report/test-device/pom.xml +++ b/ai-report/test-device/pom.xml @@ -11,6 +11,7 @@ test-device + jar @@ -30,5 +31,33 @@ spingboot2.3.12 2.3.12 + + + com.njcn.gather + system + 1.0.0 + + + + org.apache.poi + poi-ooxml + 4.1.2 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-test + test + diff --git a/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/component/TestDeviceReportStorageService.java b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/component/TestDeviceReportStorageService.java new file mode 100644 index 0000000..6feb755 --- /dev/null +++ b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/component/TestDeviceReportStorageService.java @@ -0,0 +1,149 @@ +package com.njcn.gather.aireport.testdevice.component; + +import cn.hutool.core.util.StrUtil; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +@Component +public class TestDeviceReportStorageService { + + private static final String DEFAULT_STORAGE_DIR = "data/test-device-report"; + private static final String TEST_DEVICE_REPORT_STORAGE_DIR = "test-device-report"; + private static final DateTimeFormatter DATE_DIR_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + private static final long MAX_REPORT_SIZE = 10L * 1024L * 1024L; + private static final int MAX_ORIGINAL_NAME_LENGTH = 80; + + @Value("${files.path:}") + private String filesPath; + + public String save(MultipartFile reportFile) { + if (reportFile == null || reportFile.isEmpty()) { + throw new BusinessException(CommonResponseEnum.FAIL, "校准报告不能为空"); + } + validatePdf(reportFile.getOriginalFilename(), reportFile.getSize()); + try (InputStream inputStream = reportFile.getInputStream()) { + return savePdf(reportFile.getOriginalFilename(), inputStream, reportFile.getSize()); + } catch (IOException exception) { + throw new BusinessException(CommonResponseEnum.FAIL, "保存校准报告失败"); + } + } + + public String savePdf(String originalFilename, InputStream inputStream, long size) { + validatePdf(originalFilename, size); + try { + Path storageRoot = resolveStorageRoot(); + String dateDir = LocalDate.now().format(DATE_DIR_FORMATTER); + Path storageDir = storageRoot.resolve(dateDir).normalize(); + ensurePathInRoot(storageRoot, storageDir); + Files.createDirectories(storageDir); + + String fileName = UUID.randomUUID().toString().replace("-", "") + "-" + sanitizeFileName(originalFilename); + Path target = storageDir.resolve(fileName).normalize(); + ensurePathInRoot(storageRoot, target); + copyWithSizeLimit(inputStream, target); + return storageRoot.relativize(target).toString().replace('\\', '/'); + } catch (BusinessException exception) { + throw exception; + } catch (Exception exception) { + throw new BusinessException(CommonResponseEnum.FAIL, "保存校准报告失败"); + } + } + + public Path resolveDownloadPath(String storedPath) { + if (StrUtil.isBlank(storedPath)) { + throw new BusinessException(CommonResponseEnum.FAIL, "校准报告路径不能为空"); + } + Path storageRoot = resolveStorageRoot(); + Path target = storageRoot.resolve(storedPath).normalize(); + ensurePathInRoot(storageRoot, target); + return target; + } + + public String resolveDownloadFileName(String storedPath) { + Path fileName = Paths.get(storedPath).getFileName(); + if (fileName == null) { + return "校准报告.pdf"; + } + String name = fileName.toString(); + int separatorIndex = name.indexOf('-'); + return separatorIndex >= 0 && separatorIndex + 1 < name.length() ? name.substring(separatorIndex + 1) : name; + } + + public void deleteQuietly(String storedPath) { + try { + if (StrUtil.isNotBlank(storedPath)) { + Files.deleteIfExists(resolveDownloadPath(storedPath)); + } + } catch (Exception ignored) { + // 文件清理失败不能覆盖原始业务结果。 + } + } + + private void validatePdf(String originalFilename, long size) { + if (StrUtil.isBlank(originalFilename) || !originalFilename.toLowerCase().endsWith(".pdf")) { + throw new BusinessException(CommonResponseEnum.FAIL, "校准报告仅支持PDF文件"); + } + if (size > MAX_REPORT_SIZE) { + throw new BusinessException(CommonResponseEnum.FAIL, "校准报告不能超过10MB"); + } + } + + private void copyWithSizeLimit(InputStream inputStream, Path target) throws IOException { + Path tempFile = Files.createTempFile(target.getParent(), "upload-", ".tmp"); + long bytes = 0L; + byte[] buffer = new byte[8192]; + try { + int length; + try (InputStream source = inputStream; + OutputStream targetStream = Files.newOutputStream(tempFile)) { + while ((length = source.read(buffer)) != -1) { + bytes += length; + if (bytes > MAX_REPORT_SIZE) { + throw new BusinessException(CommonResponseEnum.FAIL, "校准报告不能超过10MB"); + } + targetStream.write(buffer, 0, length); + } + } + Files.move(tempFile, target, StandardCopyOption.REPLACE_EXISTING); + } finally { + Files.deleteIfExists(tempFile); + } + } + + private Path resolveStorageRoot() { + String configuredPath = StrUtil.isBlank(filesPath) ? null : filesPath.trim(); + Path path = configuredPath == null ? Paths.get(DEFAULT_STORAGE_DIR) : Paths.get(configuredPath, TEST_DEVICE_REPORT_STORAGE_DIR); + return path.toAbsolutePath().normalize(); + } + + private void ensurePathInRoot(Path storageRoot, Path target) { + if (!target.startsWith(storageRoot)) { + throw new BusinessException(CommonResponseEnum.FAIL, "校准报告路径不合法"); + } + } + + private String sanitizeFileName(String originalFilename) { + String fileName = Paths.get(originalFilename).getFileName().toString(); + String sanitized = fileName.replaceAll("[\\\\/:*?\"<>|]", "_"); + if (sanitized.length() > MAX_ORIGINAL_NAME_LENGTH) { + String suffix = sanitized.toLowerCase().endsWith(".pdf") ? ".pdf" : ""; + int keepLength = MAX_ORIGINAL_NAME_LENGTH - suffix.length(); + sanitized = sanitized.substring(0, keepLength) + suffix; + } + return StrUtil.isBlank(sanitized) ? "report.pdf" : sanitized; + } +} diff --git a/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/controller/TestDeviceController.java b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/controller/TestDeviceController.java new file mode 100644 index 0000000..b298346 --- /dev/null +++ b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/controller/TestDeviceController.java @@ -0,0 +1,165 @@ +package com.njcn.gather.aireport.testdevice.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.njcn.common.pojo.annotation.OperateInfo; +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.exception.BusinessException; +import com.njcn.common.pojo.response.HttpResult; +import com.njcn.gather.aireport.testdevice.component.TestDeviceReportStorageService; +import com.njcn.gather.aireport.testdevice.pojo.param.TestDeviceParam; +import com.njcn.gather.aireport.testdevice.pojo.po.TestDevicePO; +import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceImportResultVO; +import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceVO; +import com.njcn.gather.aireport.testdevice.service.TestDeviceService; +import com.njcn.web.controller.BaseController; +import com.njcn.web.utils.HttpResultUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +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.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +@Api(tags = "检测设备管理") +@RestController +@RequestMapping("/api/test-device") +@RequiredArgsConstructor +public class TestDeviceController extends BaseController { + + private final TestDeviceService testDeviceService; + private final TestDeviceReportStorageService reportStorageService; + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询检测设备列表") + @PostMapping("/list") + public HttpResult> list(@RequestBody(required = false) TestDeviceParam.QueryParam param) { + String methodDescribe = getMethodDescribe("list"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, + testDeviceService.listTestDevices(param), methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD) + @ApiOperation("新增检测设备") + @PostMapping(value = "/add", consumes = {"multipart/form-data"}) + public HttpResult add(@RequestPart("request") @Validated TestDeviceParam.AddParam param, + @RequestPart("reportFile") MultipartFile reportFile) { + String methodDescribe = getMethodDescribe("add"); + boolean result = testDeviceService.addTestDevice(param, reportFile); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, + result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE) + @ApiOperation("编辑检测设备") + @PostMapping(value = "/update", consumes = {"multipart/form-data"}) + public HttpResult update(@RequestPart("request") @Validated TestDeviceParam.UpdateParam param, + @RequestPart(value = "reportFile", required = false) MultipartFile reportFile) { + String methodDescribe = getMethodDescribe("update"); + boolean result = testDeviceService.updateTestDevice(param, reportFile); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, + result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DELETE) + @ApiOperation("删除检测设备") + @PostMapping("/delete") + public HttpResult delete(@RequestBody List ids) { + String methodDescribe = getMethodDescribe("delete"); + boolean result = testDeviceService.deleteTestDevices(ids); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, + result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询检测设备详情") + @ApiImplicitParam(name = "id", value = "检测设备ID", required = true) + @GetMapping("/{id}") + public HttpResult detail(@PathVariable("id") String id) { + String methodDescribe = getMethodDescribe("detail"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, + testDeviceService.getTestDevice(id), methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("下载检测设备导入模板") + @GetMapping("/template") + public void template() { + testDeviceService.downloadTemplate(); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD) + @ApiOperation("导入检测设备") + @PostMapping(value = "/import", consumes = {"multipart/form-data"}) + public HttpResult importExcel(@RequestPart("file") MultipartFile file, + @RequestPart("reportZip") MultipartFile reportZip) { + String methodDescribe = getMethodDescribe("importExcel"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, + testDeviceService.importTestDevices(file, reportZip), methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("导出检测设备") + @GetMapping("/export") + public void export(@ModelAttribute TestDeviceParam.QueryParam param) { + testDeviceService.exportTestDevices(param); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("下载检测设备校准报告") + @ApiImplicitParam(name = "id", value = "检测设备ID", required = true) + @GetMapping("/{id}/report") + public ResponseEntity report(@PathVariable("id") String id) { + return buildReportResponse(id, false); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("预览检测设备校准报告") + @ApiImplicitParam(name = "id", value = "检测设备ID", required = true) + @GetMapping("/{id}/report/preview") + public ResponseEntity previewReport(@PathVariable("id") String id) { + return buildReportResponse(id, true); + } + + private ResponseEntity buildReportResponse(String id, boolean preview) { + TestDevicePO device = testDeviceService.requireNormal(id); + if (device == null || device.getValidityReport() == null || device.getValidityReport().trim().isEmpty()) { + throw new BusinessException(CommonResponseEnum.FAIL, "校准报告文件不存在"); + } + Path filePath = reportStorageService.resolveDownloadPath(device.getValidityReport()); + if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) { + throw new BusinessException(CommonResponseEnum.FAIL, "校准报告文件不存在"); + } + try { + String fileName = reportStorageService.resolveDownloadFileName(device.getValidityReport()); + String encodedFileName = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20"); + InputStream inputStream = Files.newInputStream(filePath); + return ResponseEntity.ok() + .contentType(preview ? MediaType.APPLICATION_PDF : MediaType.APPLICATION_OCTET_STREAM) + .header(HttpHeaders.CONTENT_DISPOSITION, + (preview ? "inline" : "attachment") + "; filename*=UTF-8''" + encodedFileName) + .body(new InputStreamResource(inputStream)); + } catch (Exception exception) { + throw new BusinessException(CommonResponseEnum.FAIL, "读取校准报告失败"); + } + } +} diff --git a/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/mapper/TestDeviceMapper.java b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/mapper/TestDeviceMapper.java new file mode 100644 index 0000000..5149dd0 --- /dev/null +++ b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/mapper/TestDeviceMapper.java @@ -0,0 +1,61 @@ +package com.njcn.gather.aireport.testdevice.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.njcn.gather.aireport.testdevice.pojo.param.TestDeviceParam; +import com.njcn.gather.aireport.testdevice.pojo.po.TestDevicePO; +import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceTypeNameVO; +import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +@Mapper +public interface TestDeviceMapper extends BaseMapper { + + @Select({ + "" + }) + Page selectDevicePage(Page page, @Param("param") TestDeviceParam.QueryParam param); + + @Select({ + "select d.id, d.type, dt.name as typeName, d.no, d.validity_period as validityPeriod,", + "d.validity_report as validityReport, d.state, d.status, d.create_by as createBy,", + "creator.name as createByName, d.create_time as createTime, d.update_by as updateBy,", + "updater.name as updateByName, d.update_time as updateTime", + "from ai_testdevice d", + "left join cs_dev_type dt on d.type = dt.id", + "left join sys_user creator on d.create_by = creator.id", + "left join sys_user updater on d.update_by = updater.id", + "where d.id = #{id} and d.status = 1" + }) + TestDeviceVO selectDeviceDetail(@Param("id") String id); + + @Select("select count(1) from cs_dev_type where id = #{typeId} and State = 1") + int countNormalDeviceType(@Param("typeId") String typeId); + + @Select("select id, name from cs_dev_type where State = 1") + List selectNormalDeviceTypes(); +} diff --git a/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/constant/TestDeviceConst.java b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/constant/TestDeviceConst.java new file mode 100644 index 0000000..c2335d7 --- /dev/null +++ b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/constant/TestDeviceConst.java @@ -0,0 +1,12 @@ +package com.njcn.gather.aireport.testdevice.pojo.constant; + +public final class TestDeviceConst { + + public static final String STATE_RUNNING = "01"; + public static final String STATE_RETIRED = "02"; + public static final Integer STATUS_NORMAL = 1; + public static final Integer STATUS_DELETED = 0; + + private TestDeviceConst() { + } +} diff --git a/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/param/TestDeviceParam.java b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/param/TestDeviceParam.java new file mode 100644 index 0000000..1bff8cd --- /dev/null +++ b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/param/TestDeviceParam.java @@ -0,0 +1,49 @@ +package com.njcn.gather.aireport.testdevice.pojo.param; + +import com.njcn.web.pojo.param.BaseParam; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; + +public class TestDeviceParam { + + @Data + @EqualsAndHashCode(callSuper = true) + public static class QueryParam extends BaseParam { + @ApiModelProperty("关键字,匹配设备型号名称或设备编号") + private String keyword; + + @ApiModelProperty("创建开始时间,yyyy-MM-dd HH:mm:ss") + private String searchBeginTime; + + @ApiModelProperty("创建结束时间,yyyy-MM-dd HH:mm:ss") + private String searchEndTime; + } + + @Data + public static class AddParam { + @ApiModelProperty("设备型号ID") + @NotBlank(message = "设备型号不能为空") + private String type; + + @ApiModelProperty("设备编号") + @NotBlank(message = "设备编号不能为空") + private String no; + + @ApiModelProperty("校准有效期,yyyy-MM-dd") + @NotBlank(message = "校准有效期不能为空") + private String validityPeriod; + + @ApiModelProperty("设备状态,01-在运,02-退运") + private String state; + } + + @Data + public static class UpdateParam extends AddParam { + @ApiModelProperty("检测设备ID") + @NotBlank(message = "检测设备ID不能为空") + private String id; + } +} diff --git a/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/po/TestDevicePO.java b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/po/TestDevicePO.java new file mode 100644 index 0000000..0b07b02 --- /dev/null +++ b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/po/TestDevicePO.java @@ -0,0 +1,39 @@ +package com.njcn.gather.aireport.testdevice.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +@TableName("ai_testdevice") +public class TestDevicePO implements Serializable { + private static final long serialVersionUID = 4153967564253668210L; + + @TableId("id") + private String id; + @TableField("type") + private String type; + @TableField("no") + private String no; + @TableField("validity_period") + private LocalDate validityPeriod; + @TableField("validity_report") + private String validityReport; + @TableField("state") + private String state; + @TableField("status") + private Integer status; + @TableField("create_by") + private String createBy; + @TableField("create_time") + private LocalDateTime createTime; + @TableField("update_by") + private String updateBy; + @TableField("update_time") + private LocalDateTime updateTime; +} diff --git a/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/vo/TestDeviceExcelVO.java b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/vo/TestDeviceExcelVO.java new file mode 100644 index 0000000..994fc3a --- /dev/null +++ b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/vo/TestDeviceExcelVO.java @@ -0,0 +1,32 @@ +package com.njcn.gather.aireport.testdevice.pojo.vo; + +import cn.afterturn.easypoi.excel.annotation.Excel; +import lombok.Data; + +import java.io.Serializable; + +@Data +public class TestDeviceExcelVO implements Serializable { + private static final long serialVersionUID = 8111740807955262764L; + + @Excel(name = "设备型号", width = 25) + private String typeName; + + @Excel(name = "设备编号", width = 25) + private String no; + + @Excel(name = "校准有效期", width = 20) + private String validityPeriod; + + @Excel(name = "设备状态", width = 15) + private String stateName; + + @Excel(name = "校准报告文件名", width = 35) + private String validityReportName; + + @Excel(name = "创建时间", width = 25) + private String createTime; + + @Excel(name = "创建人", width = 20) + private String createByName; +} diff --git a/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/vo/TestDeviceImportResultVO.java b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/vo/TestDeviceImportResultVO.java new file mode 100644 index 0000000..1dc16b9 --- /dev/null +++ b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/vo/TestDeviceImportResultVO.java @@ -0,0 +1,13 @@ +package com.njcn.gather.aireport.testdevice.pojo.vo; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class TestDeviceImportResultVO { + private int successCount; + private int failCount; + private List failReasons = new ArrayList(); +} diff --git a/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/vo/TestDeviceTypeNameVO.java b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/vo/TestDeviceTypeNameVO.java new file mode 100644 index 0000000..ea9069f --- /dev/null +++ b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/vo/TestDeviceTypeNameVO.java @@ -0,0 +1,9 @@ +package com.njcn.gather.aireport.testdevice.pojo.vo; + +import lombok.Data; + +@Data +public class TestDeviceTypeNameVO { + private String id; + private String name; +} diff --git a/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/vo/TestDeviceVO.java b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/vo/TestDeviceVO.java new file mode 100644 index 0000000..39c4d19 --- /dev/null +++ b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/pojo/vo/TestDeviceVO.java @@ -0,0 +1,59 @@ +package com.njcn.gather.aireport.testdevice.pojo.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +public class TestDeviceVO { + @ApiModelProperty("检测设备ID") + private String id; + @ApiModelProperty("设备型号ID") + private String type; + @ApiModelProperty("设备型号名称") + private String typeName; + @ApiModelProperty("设备编号") + private String no; + @ApiModelProperty("校准有效期") + @JsonFormat(pattern = "yyyy-MM-dd") + @JsonDeserialize(using = LocalDateDeserializer.class) + @JsonSerialize(using = LocalDateSerializer.class) + private LocalDate validityPeriod; + @ApiModelProperty("校准报告存储路径") + private String validityReport; + @ApiModelProperty("校准报告文件名") + private String validityReportName; + @ApiModelProperty("校准报告下载地址") + private String validityReportUrl; + @ApiModelProperty("设备状态,01-在运,02-退役") + private String state; + @ApiModelProperty("逻辑状态") + private Integer status; + @ApiModelProperty("创建人ID") + private String createBy; + @ApiModelProperty("创建人显示名") + private String createByName; + @ApiModelProperty("创建时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + private LocalDateTime createTime; + @ApiModelProperty("更新人ID") + private String updateBy; + @ApiModelProperty("更新人显示名") + private String updateByName; + @ApiModelProperty("更新时间") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + private LocalDateTime updateTime; +} diff --git a/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/service/TestDeviceService.java b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/service/TestDeviceService.java new file mode 100644 index 0000000..56531cf --- /dev/null +++ b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/service/TestDeviceService.java @@ -0,0 +1,32 @@ +package com.njcn.gather.aireport.testdevice.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.njcn.gather.aireport.testdevice.pojo.param.TestDeviceParam; +import com.njcn.gather.aireport.testdevice.pojo.po.TestDevicePO; +import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceImportResultVO; +import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceVO; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +public interface TestDeviceService extends IService { + + Page listTestDevices(TestDeviceParam.QueryParam param); + + boolean addTestDevice(TestDeviceParam.AddParam param, MultipartFile reportFile); + + boolean updateTestDevice(TestDeviceParam.UpdateParam param, MultipartFile reportFile); + + boolean deleteTestDevices(List ids); + + TestDeviceVO getTestDevice(String id); + + TestDevicePO requireNormal(String id); + + void downloadTemplate(); + + TestDeviceImportResultVO importTestDevices(MultipartFile file, MultipartFile reportZip); + + void exportTestDevices(TestDeviceParam.QueryParam param); +} diff --git a/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/service/impl/TestDeviceServiceImpl.java b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/service/impl/TestDeviceServiceImpl.java new file mode 100644 index 0000000..63c7573 --- /dev/null +++ b/ai-report/test-device/src/main/java/com/njcn/gather/aireport/testdevice/service/impl/TestDeviceServiceImpl.java @@ -0,0 +1,666 @@ +package com.njcn.gather.aireport.testdevice.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.aireport.testdevice.component.TestDeviceReportStorageService; +import com.njcn.gather.aireport.testdevice.mapper.TestDeviceMapper; +import com.njcn.gather.aireport.testdevice.pojo.constant.TestDeviceConst; +import com.njcn.gather.aireport.testdevice.pojo.param.TestDeviceParam; +import com.njcn.gather.aireport.testdevice.pojo.po.TestDevicePO; +import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceExcelVO; +import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceImportResultVO; +import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceTypeNameVO; +import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceVO; +import com.njcn.gather.aireport.testdevice.service.TestDeviceService; +import com.njcn.gather.system.util.ExportFileNameUtil; +import com.njcn.web.factory.PageFactory; +import com.njcn.web.utils.ExcelUtil; +import com.njcn.web.utils.HttpServletUtil; +import com.njcn.web.utils.RequestUtil; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.DataFormatter; +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.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URLEncoder; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +@Slf4j +@Service +@RequiredArgsConstructor +public class TestDeviceServiceImpl extends ServiceImpl implements TestDeviceService { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private static final String[] TEMPLATE_HEADERS = new String[]{"设备型号", "设备编号", "校准有效期", "设备状态", "校准报告文件名"}; + private static final long MAX_IMPORT_REPORT_SIZE = 10L * 1024L * 1024L; + + private final TestDeviceReportStorageService reportStorageService; + + @Override + public Page listTestDevices(TestDeviceParam.QueryParam param) { + TestDeviceParam.QueryParam query = normalizeQueryParam(param); + Page page = this.baseMapper.selectDevicePage( + new Page(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), query); + page.getRecords().forEach(this::fillReportDisplayFields); + return page; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean addTestDevice(TestDeviceParam.AddParam param, MultipartFile reportFile) { + NormalizedDevice data = normalizeParam(param); + checkDeviceType(data.getType()); + checkNoUnique(data.getNo(), null); + String storedPath = reportStorageService.save(reportFile); + try { + LocalDateTime now = LocalDateTime.now(); + String userId = resolveCurrentUserId(); + TestDevicePO device = new TestDevicePO(); + device.setId(UUID.randomUUID().toString()); + device.setType(data.getType()); + device.setNo(data.getNo()); + device.setValidityPeriod(data.getValidityPeriod()); + device.setValidityReport(storedPath); + device.setState(data.getState()); + device.setStatus(TestDeviceConst.STATUS_NORMAL); + device.setCreateBy(userId); + device.setCreateTime(now); + device.setUpdateBy(userId); + device.setUpdateTime(now); + boolean saved = this.save(device); + if (!saved) { + reportStorageService.deleteQuietly(storedPath); + } + return saved; + } catch (RuntimeException exception) { + reportStorageService.deleteQuietly(storedPath); + throw exception; + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateTestDevice(TestDeviceParam.UpdateParam param, MultipartFile reportFile) { + if (param == null || StrUtil.isBlank(param.getId())) { + throw new BusinessException(CommonResponseEnum.FAIL, "检测设备ID不能为空"); + } + TestDevicePO device = requireNormal(param.getId()); + NormalizedDevice data = normalizeParam(param); + checkDeviceType(data.getType()); + checkNoUnique(data.getNo(), device.getId()); + String oldReport = device.getValidityReport(); + String newReport = null; + if (reportFile != null && !reportFile.isEmpty()) { + newReport = reportStorageService.save(reportFile); + device.setValidityReport(newReport); + } else if (StrUtil.isBlank(oldReport)) { + throw new BusinessException(CommonResponseEnum.FAIL, "校准报告不能为空"); + } + try { + device.setType(data.getType()); + device.setNo(data.getNo()); + device.setValidityPeriod(data.getValidityPeriod()); + device.setState(data.getState()); + device.setUpdateBy(resolveCurrentUserId()); + device.setUpdateTime(LocalDateTime.now()); + boolean updated = this.updateById(device); + if (!updated) { + reportStorageService.deleteQuietly(newReport); + return false; + } + if (StrUtil.isNotBlank(newReport)) { + reportStorageService.deleteQuietly(oldReport); + } + return true; + } catch (RuntimeException exception) { + reportStorageService.deleteQuietly(newReport); + throw exception; + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean deleteTestDevices(List ids) { + List validIds = normalizeIds(ids); + if (validIds.isEmpty()) { + throw new BusinessException(CommonResponseEnum.FAIL, "检测设备ID不能为空"); + } + return this.lambdaUpdate() + .set(TestDevicePO::getStatus, TestDeviceConst.STATUS_DELETED) + .set(TestDevicePO::getUpdateBy, resolveCurrentUserId()) + .set(TestDevicePO::getUpdateTime, LocalDateTime.now()) + .in(TestDevicePO::getId, validIds) + .eq(TestDevicePO::getStatus, TestDeviceConst.STATUS_NORMAL) + .update(); + } + + @Override + public TestDeviceVO getTestDevice(String id) { + TestDeviceVO vo = this.baseMapper.selectDeviceDetail(requireText(id, "检测设备ID不能为空")); + if (vo == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "检测设备不存在或已删除"); + } + fillReportDisplayFields(vo); + return vo; + } + + @Override + public TestDevicePO requireNormal(String id) { + String normalizedId = requireText(id, "检测设备ID不能为空"); + TestDevicePO device = this.lambdaQuery() + .eq(TestDevicePO::getId, normalizedId) + .eq(TestDevicePO::getStatus, TestDeviceConst.STATUS_NORMAL) + .one(); + if (device == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "检测设备不存在或已删除"); + } + return device; + } + + @Override + public void downloadTemplate() { + HttpServletResponse response = HttpServletUtil.getResponse(); + try (XSSFWorkbook workbook = new XSSFWorkbook()) { + XSSFSheet sheet = workbook.createSheet("检测设备导入模板"); + writeTemplateHeader(workbook, sheet); + String fileName = URLEncoder.encode(ExportFileNameUtil.appendToday("检测设备导入模板.xlsx"), "UTF-8"); + response.reset(); + response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8"); + try (ServletOutputStream outputStream = response.getOutputStream()) { + workbook.write(outputStream); + outputStream.flush(); + } + } catch (IOException exception) { + log.error("下载检测设备导入模板失败", exception); + throw new BusinessException(CommonResponseEnum.FAIL, "下载检测设备导入模板失败"); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public TestDeviceImportResultVO importTestDevices(MultipartFile file, MultipartFile reportZip) { + if (file == null || file.isEmpty()) { + throw new BusinessException(CommonResponseEnum.FAIL, "导入文件不能为空"); + } + if (reportZip == null || reportZip.isEmpty()) { + throw new BusinessException(CommonResponseEnum.FAIL, "校准报告压缩包不能为空"); + } + List rows = readImportRows(file); + TestDeviceImportResultVO result = new TestDeviceImportResultVO(); + if (rows.isEmpty()) { + return result; + } + + ImportArchive archive = readReportZip(reportZip); + try { + Map typeNameIdMap = loadTypeNameIdMap(); + List devices = validateImportRows(rows, archive, typeNameIdMap, result); + if (result.getFailCount() > 0) { + return result; + } + + List savedReports = new ArrayList(); + try { + LocalDateTime now = LocalDateTime.now(); + String userId = resolveCurrentUserId(); + List saveRows = new ArrayList(); + for (NormalizedImportDevice data : devices) { + String storedPath; + try (InputStream inputStream = Files.newInputStream(data.getReportPath())) { + storedPath = reportStorageService.savePdf(data.getReportName(), inputStream, Files.size(data.getReportPath())); + } catch (IOException exception) { + throw new BusinessException(CommonResponseEnum.FAIL, "读取校准报告压缩包失败"); + } + savedReports.add(storedPath); + + TestDevicePO device = new TestDevicePO(); + device.setId(UUID.randomUUID().toString()); + device.setType(data.getType()); + device.setNo(data.getNo()); + device.setValidityPeriod(data.getValidityPeriod()); + device.setValidityReport(storedPath); + device.setState(data.getState()); + device.setStatus(TestDeviceConst.STATUS_NORMAL); + device.setCreateBy(userId); + device.setCreateTime(now); + device.setUpdateBy(userId); + device.setUpdateTime(now); + saveRows.add(device); + } + + boolean saved = saveRows.isEmpty() || this.saveBatch(saveRows); + if (!saved) { + savedReports.forEach(reportStorageService::deleteQuietly); + return result; + } + result.setSuccessCount(saveRows.size()); + return result; + } catch (RuntimeException exception) { + savedReports.forEach(reportStorageService::deleteQuietly); + throw exception; + } + } finally { + archive.cleanup(); + } + } + + @Override + public void exportTestDevices(TestDeviceParam.QueryParam param) { + TestDeviceParam.QueryParam query = normalizeQueryParam(param); + Page page = this.baseMapper.selectDevicePage(new Page(1, Integer.MAX_VALUE), query); + List exportRows = page.getRecords().stream() + .peek(this::fillReportDisplayFields) + .map(this::toExcelVO) + .collect(Collectors.toList()); + ExcelUtil.exportExcel(ExportFileNameUtil.appendToday("检测设备列表.xlsx"), "检测设备列表", + TestDeviceExcelVO.class, exportRows); + } + + private TestDeviceParam.QueryParam normalizeQueryParam(TestDeviceParam.QueryParam param) { + TestDeviceParam.QueryParam query = param == null ? new TestDeviceParam.QueryParam() : param; + query.setKeyword(trimToNull(query.getKeyword())); + if (StrUtil.isNotBlank(query.getSearchBeginTime())) { + query.setSearchBeginTime(formatDateTime(parseDateTime(query.getSearchBeginTime(), "开始时间格式不正确"))); + } + if (StrUtil.isNotBlank(query.getSearchEndTime())) { + query.setSearchEndTime(formatDateTime(parseDateTime(query.getSearchEndTime(), "结束时间格式不正确"))); + } + return query; + } + + private NormalizedDevice normalizeParam(TestDeviceParam.AddParam param) { + if (param == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "检测设备参数不能为空"); + } + NormalizedDevice data = new NormalizedDevice(); + data.setType(requireText(param.getType(), "设备型号不能为空")); + data.setNo(requireText(param.getNo(), "设备编号不能为空", 64, "设备编号不能超过64个字符")); + data.setValidityPeriod(parseDate(requireText(param.getValidityPeriod(), "校准有效期不能为空"))); + data.setState(normalizeState(param.getState())); + return data; + } + + private List validateImportRows(List rows, ImportArchive archive, + Map typeNameIdMap, TestDeviceImportResultVO result) { + Set fileNos = new HashSet(); + List devices = new ArrayList(); + for (int i = 0; i < rows.size(); i++) { + int rowNumber = i + 2; + try { + TestDeviceExcelVO row = rows.get(i); + NormalizedImportDevice device = new NormalizedImportDevice(); + String typeName = requireText(row.getTypeName(), "设备型号不能为空"); + String typeId = typeNameIdMap.get(typeName); + if (StrUtil.isBlank(typeId)) { + throw new BusinessException(CommonResponseEnum.FAIL, "设备型号不存在或已删除"); + } + device.setType(typeId); + device.setNo(requireText(row.getNo(), "设备编号不能为空", 64, "设备编号不能超过64个字符")); + if (!fileNos.add(device.getNo())) { + throw new BusinessException(CommonResponseEnum.FAIL, "文件内存在重复设备编号"); + } + checkNoUnique(device.getNo(), null); + device.setValidityPeriod(parseDate(requireText(row.getValidityPeriod(), "校准有效期不能为空"))); + device.setState(normalizeState(row.getStateName())); + device.setReportName(requireText(row.getValidityReportName(), "校准报告文件名不能为空")); + Path reportPath = archive.getReportPath(device.getReportName()); + if (reportPath == null) { + throw new BusinessException(CommonResponseEnum.FAIL, "校准报告压缩包中未找到对应PDF"); + } + device.setReportPath(reportPath); + devices.add(device); + } catch (BusinessException exception) { + result.setFailCount(result.getFailCount() + 1); + result.getFailReasons().add("第" + rowNumber + "行:" + exception.getMessage()); + } + } + return devices; + } + + private List readImportRows(MultipartFile file) { + try (InputStream inputStream = file.getInputStream(); + XSSFWorkbook workbook = new XSSFWorkbook(inputStream)) { + XSSFSheet sheet = workbook.getNumberOfSheets() == 0 ? null : workbook.getSheetAt(0); + if (sheet == null || sheet.getLastRowNum() < 1) { + return Collections.emptyList(); + } + List rows = new ArrayList(); + for (int i = 1; i <= sheet.getLastRowNum(); i++) { + Row row = sheet.getRow(i); + if (isEmptyRow(row)) { + continue; + } + TestDeviceExcelVO excelVO = new TestDeviceExcelVO(); + excelVO.setTypeName(readCell(row, 0)); + excelVO.setNo(readCell(row, 1)); + excelVO.setValidityPeriod(readCell(row, 2)); + excelVO.setStateName(readCell(row, 3)); + excelVO.setValidityReportName(readCell(row, 4)); + rows.add(excelVO); + } + return rows; + } catch (IOException exception) { + log.error("读取检测设备导入文件失败", exception); + throw new BusinessException(CommonResponseEnum.FAIL, "读取导入文件失败"); + } + } + + ImportArchive readReportZip(MultipartFile reportZip) { + Path tempDir; + try { + tempDir = Files.createTempDirectory("test-device-import-"); + } catch (IOException exception) { + throw new BusinessException(CommonResponseEnum.FAIL, "读取校准报告压缩包失败"); + } + + Map files = new HashMap(); + try (ZipInputStream zipInputStream = new ZipInputStream(reportZip.getInputStream())) { + ZipEntry entry; + while ((entry = zipInputStream.getNextEntry()) != null) { + if (entry.isDirectory()) { + continue; + } + String fileName = Paths.get(entry.getName()).getFileName().toString(); + if (!fileName.toLowerCase().endsWith(".pdf")) { + throw new BusinessException(CommonResponseEnum.FAIL, "校准报告压缩包仅允许包含PDF文件"); + } + if (files.containsKey(fileName)) { + throw new BusinessException(CommonResponseEnum.FAIL, "校准报告压缩包中存在重复文件名"); + } + files.put(fileName, readZipEntryToFile(tempDir, fileName, zipInputStream)); + } + return new ImportArchive(tempDir, files); + } catch (BusinessException exception) { + deleteDirectoryQuietly(tempDir); + throw exception; + } catch (IOException exception) { + deleteDirectoryQuietly(tempDir); + log.error("读取校准报告压缩包失败", exception); + throw new BusinessException(CommonResponseEnum.FAIL, "读取校准报告压缩包失败"); + } + } + + private Path readZipEntryToFile(Path tempDir, String fileName, ZipInputStream zipInputStream) throws IOException { + Path tempFile = tempDir.resolve(UUID.randomUUID().toString().replace("-", "") + "-" + fileName).normalize(); + long bytes = 0L; + byte[] buffer = new byte[8192]; + try (OutputStream outputStream = Files.newOutputStream(tempFile)) { + int length; + while ((length = zipInputStream.read(buffer)) != -1) { + bytes += length; + if (bytes > MAX_IMPORT_REPORT_SIZE) { + throw new BusinessException(CommonResponseEnum.FAIL, "校准报告不能超过10MB"); + } + outputStream.write(buffer, 0, length); + } + } catch (BusinessException exception) { + Files.deleteIfExists(tempFile); + throw exception; + } catch (IOException exception) { + Files.deleteIfExists(tempFile); + throw exception; + } + return tempFile; + } + + private void deleteDirectoryQuietly(Path directory) { + if (directory == null || !Files.exists(directory)) { + return; + } + try (Stream pathStream = Files.walk(directory)) { + pathStream.sorted(Comparator.reverseOrder()).forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (IOException ignored) { + // 导入临时目录清理失败不影响主流程返回。 + } + }); + } catch (IOException ignored) { + // 导入临时目录清理失败不影响主流程返回。 + } + } + + private void writeTemplateHeader(XSSFWorkbook workbook, XSSFSheet sheet) { + CellStyle style = workbook.createCellStyle(); + Font font = workbook.createFont(); + font.setBold(true); + style.setFont(font); + Row row = sheet.createRow(0); + for (int i = 0; i < TEMPLATE_HEADERS.length; i++) { + Cell cell = row.createCell(i); + cell.setCellValue(TEMPLATE_HEADERS[i]); + cell.setCellStyle(style); + sheet.setColumnWidth(i, 25 * 256); + } + } + + private boolean isEmptyRow(Row row) { + if (row == null) { + return true; + } + for (int i = 0; i < TEMPLATE_HEADERS.length; i++) { + if (StrUtil.isNotBlank(readCell(row, i))) { + return false; + } + } + return true; + } + + private String readCell(Row row, int index) { + Cell cell = row.getCell(index); + return cell == null ? null : trimToNull(new DataFormatter().formatCellValue(cell)); + } + + private void fillReportDisplayFields(TestDeviceVO vo) { + if (vo == null || StrUtil.isBlank(vo.getValidityReport())) { + return; + } + vo.setValidityReportName(reportStorageService.resolveDownloadFileName(vo.getValidityReport())); + vo.setValidityReportUrl("/api/test-device/" + vo.getId() + "/report"); + } + + private TestDeviceExcelVO toExcelVO(TestDeviceVO vo) { + TestDeviceExcelVO excelVO = new TestDeviceExcelVO(); + excelVO.setTypeName(vo.getTypeName()); + excelVO.setNo(vo.getNo()); + excelVO.setValidityPeriod(vo.getValidityPeriod() == null ? null : DATE_FORMATTER.format(vo.getValidityPeriod())); + excelVO.setStateName(resolveStateName(vo.getState())); + excelVO.setValidityReportName(vo.getValidityReportName()); + excelVO.setCreateByName(StrUtil.isNotBlank(vo.getCreateByName()) ? vo.getCreateByName() : vo.getCreateBy()); + excelVO.setCreateTime(vo.getCreateTime() == null ? null : DATE_TIME_FORMATTER.format(vo.getCreateTime())); + return excelVO; + } + + private Map loadTypeNameIdMap() { + List types = this.baseMapper.selectNormalDeviceTypes(); + if (types == null || types.isEmpty()) { + return Collections.emptyMap(); + } + return types.stream() + .filter(type -> type != null && StrUtil.isNotBlank(type.getName()) && StrUtil.isNotBlank(type.getId())) + .collect(Collectors.toMap(TestDeviceTypeNameVO::getName, TestDeviceTypeNameVO::getId, (first, second) -> first)); + } + + private void checkDeviceType(String type) { + if (this.baseMapper.countNormalDeviceType(type) <= 0) { + throw new BusinessException(CommonResponseEnum.FAIL, "设备型号不存在或已删除"); + } + } + + private void checkNoUnique(String no, String excludeId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper(); + wrapper.eq(TestDevicePO::getNo, no) + .ne(StrUtil.isNotBlank(excludeId), TestDevicePO::getId, excludeId); + if (this.count(wrapper) > 0) { + throw new BusinessException(CommonResponseEnum.FAIL, "设备编号已存在"); + } + } + + private List normalizeIds(List ids) { + if (ids == null || ids.isEmpty()) { + return Collections.emptyList(); + } + List result = new ArrayList(); + for (String id : ids) { + String normalizedId = trimToNull(id); + if (normalizedId != null && !result.contains(normalizedId)) { + result.add(normalizedId); + } + } + return result; + } + + private LocalDate parseDate(String value) { + try { + return LocalDate.parse(value.trim(), DATE_FORMATTER); + } catch (DateTimeParseException exception) { + throw new BusinessException(CommonResponseEnum.FAIL, "校准有效期格式不正确"); + } + } + + private LocalDateTime parseDateTime(String value, String errorMessage) { + try { + return LocalDateTime.parse(value.trim(), DATE_TIME_FORMATTER); + } catch (DateTimeParseException exception) { + throw new BusinessException(CommonResponseEnum.FAIL, errorMessage); + } + } + + private String formatDateTime(LocalDateTime value) { + return DATE_TIME_FORMATTER.format(value); + } + + private String normalizeState(String state) { + String text = trimToNull(state); + if (text == null || "在运".equals(text) || TestDeviceConst.STATE_RUNNING.equals(text)) { + return TestDeviceConst.STATE_RUNNING; + } + if ("退运".equals(text) || TestDeviceConst.STATE_RETIRED.equals(text)) { + return TestDeviceConst.STATE_RETIRED; + } + throw new BusinessException(CommonResponseEnum.FAIL, "设备状态不正确"); + } + + private String resolveStateName(String state) { + if (TestDeviceConst.STATE_RUNNING.equals(state)) { + return "在运"; + } + if (TestDeviceConst.STATE_RETIRED.equals(state)) { + return "退运"; + } + return state; + } + + private String requireText(String value, String blankMessage) { + String text = trimToNull(value); + if (text == null) { + throw new BusinessException(CommonResponseEnum.FAIL, blankMessage); + } + return text; + } + + private String requireText(String value, String blankMessage, int maxLength, String lengthMessage) { + String text = requireText(value, blankMessage); + if (text.length() > maxLength) { + throw new BusinessException(CommonResponseEnum.FAIL, lengthMessage); + } + return text; + } + + private String resolveCurrentUserId() { + String userId = RequestUtil.getUserId(); + return StrUtil.isBlank(userId) ? null : userId; + } + + private String trimToNull(String value) { + if (value == null) { + return null; + } + String text = value.trim(); + return text.isEmpty() ? null : text; + } + + @Data + private static class NormalizedDevice { + private String type; + private String no; + private LocalDate validityPeriod; + private String state; + } + + @Data + private static class NormalizedImportDevice extends NormalizedDevice { + private String reportName; + private Path reportPath; + } + + static class ImportArchive { + private final Path tempDir; + private final Map reportFiles; + + ImportArchive(Path tempDir, Map reportFiles) { + this.tempDir = tempDir; + this.reportFiles = reportFiles; + } + + Path getReportPath(String fileName) { + return reportFiles.get(fileName); + } + + void cleanup() { + if (tempDir == null || !Files.exists(tempDir)) { + return; + } + try (Stream pathStream = Files.walk(tempDir)) { + pathStream.sorted(Comparator.reverseOrder()).forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (IOException ignored) { + // 导入临时文件清理失败不影响主流程返回。 + } + }); + } catch (IOException ignored) { + // 导入临时目录清理失败不影响主流程返回。 + } + } + } +} diff --git a/ai-report/test-device/src/test/java/com/njcn/gather/aireport/testdevice/component/TestDeviceReportStorageServiceTest.java b/ai-report/test-device/src/test/java/com/njcn/gather/aireport/testdevice/component/TestDeviceReportStorageServiceTest.java new file mode 100644 index 0000000..603ca6a --- /dev/null +++ b/ai-report/test-device/src/test/java/com/njcn/gather/aireport/testdevice/component/TestDeviceReportStorageServiceTest.java @@ -0,0 +1,28 @@ +package com.njcn.gather.aireport.testdevice.component; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.util.ReflectionTestUtils; + +import java.nio.file.Files; +import java.nio.file.Path; + +public class TestDeviceReportStorageServiceTest { + + @Test + public void saveUsesTestDeviceReportDirectoryUnderFilesRoot() throws Exception { + Path filesRoot = Files.createTempDirectory("files-root"); + TestDeviceReportStorageService service = new TestDeviceReportStorageService(); + ReflectionTestUtils.setField(service, "filesPath", filesRoot.toString()); + + String storedPath = service.save(new MockMultipartFile("reportFile", "../report.pdf", + "application/pdf", "%PDF-1.4".getBytes("UTF-8"))); + + Path expectedRoot = filesRoot.resolve("test-device-report").toAbsolutePath().normalize(); + Path actualPath = service.resolveDownloadPath(storedPath); + Assert.assertTrue(actualPath.startsWith(expectedRoot)); + Assert.assertTrue(storedPath.endsWith("-report.pdf")); + Assert.assertTrue(Files.exists(actualPath)); + } +} diff --git a/ai-report/test-device/src/test/java/com/njcn/gather/aireport/testdevice/controller/TestDeviceControllerTest.java b/ai-report/test-device/src/test/java/com/njcn/gather/aireport/testdevice/controller/TestDeviceControllerTest.java new file mode 100644 index 0000000..e006151 --- /dev/null +++ b/ai-report/test-device/src/test/java/com/njcn/gather/aireport/testdevice/controller/TestDeviceControllerTest.java @@ -0,0 +1,45 @@ +package com.njcn.gather.aireport.testdevice.controller; + +import com.njcn.gather.aireport.testdevice.component.TestDeviceReportStorageService; +import com.njcn.gather.aireport.testdevice.pojo.po.TestDevicePO; +import com.njcn.gather.aireport.testdevice.service.TestDeviceService; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.util.ReflectionTestUtils; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +class TestDeviceControllerTest { + + private final TestDeviceService testDeviceService = Mockito.mock(TestDeviceService.class); + private final TestDeviceReportStorageService reportStorageService = Mockito.mock(TestDeviceReportStorageService.class); + + @Test + void previewReportShouldReturnInlinePdfResponse() throws Exception { + TestDeviceController controller = new TestDeviceController(testDeviceService, reportStorageService); + TestDevicePO device = new TestDevicePO(); + device.setId("device-1"); + device.setValidityReport("20260626/report.pdf"); + Path tempFile = Files.createTempFile("test-device-preview-", ".pdf"); + Files.write(tempFile, "%PDF-1.4".getBytes(StandardCharsets.UTF_8)); + Mockito.when(testDeviceService.requireNormal("device-1")).thenReturn(device); + Mockito.when(reportStorageService.resolveDownloadPath("20260626/report.pdf")).thenReturn(tempFile); + Mockito.when(reportStorageService.resolveDownloadFileName("20260626/report.pdf")).thenReturn("report.pdf"); + + @SuppressWarnings("unchecked") + ResponseEntity response = (ResponseEntity) + ReflectionTestUtils.invokeMethod(controller, "buildReportResponse", "device-1", true); + + Assertions.assertNotNull(response); + Assertions.assertEquals(MediaType.APPLICATION_PDF, response.getHeaders().getContentType()); + Assertions.assertEquals("inline; filename*=UTF-8''report.pdf", + response.getHeaders().getFirst(HttpHeaders.CONTENT_DISPOSITION)); + } +} diff --git a/ai-report/test-device/src/test/java/com/njcn/gather/aireport/testdevice/mapper/TestDeviceMapperTest.java b/ai-report/test-device/src/test/java/com/njcn/gather/aireport/testdevice/mapper/TestDeviceMapperTest.java new file mode 100644 index 0000000..13dc910 --- /dev/null +++ b/ai-report/test-device/src/test/java/com/njcn/gather/aireport/testdevice/mapper/TestDeviceMapperTest.java @@ -0,0 +1,31 @@ +package com.njcn.gather.aireport.testdevice.mapper; + +import org.apache.ibatis.annotations.Select; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.util.Arrays; + +class TestDeviceMapperTest { + + @Test + void selectQueriesShouldContainUpdateUserNameMapping() throws Exception { + assertSelectContainsUpdateUserName("selectDevicePage"); + assertSelectContainsUpdateUserName("selectDeviceDetail"); + } + + private void assertSelectContainsUpdateUserName(String methodName) throws Exception { + Method method = Arrays.stream(TestDeviceMapper.class.getMethods()) + .filter(item -> item.getName().equals(methodName)) + .findFirst() + .orElseThrow(NoSuchMethodException::new); + Select select = method.getAnnotation(Select.class); + Assertions.assertNotNull(select); + String sql = String.join(" ", select.value()); + Assertions.assertTrue(sql.contains("updateByName"), + methodName + " should select updateByName"); + Assertions.assertTrue(sql.contains("d.update_by = updater.id"), + methodName + " should join update user"); + } +} diff --git a/ai-report/test-device/src/test/java/com/njcn/gather/aireport/testdevice/service/impl/TestDeviceServiceImplTest.java b/ai-report/test-device/src/test/java/com/njcn/gather/aireport/testdevice/service/impl/TestDeviceServiceImplTest.java new file mode 100644 index 0000000..9136f07 --- /dev/null +++ b/ai-report/test-device/src/test/java/com/njcn/gather/aireport/testdevice/service/impl/TestDeviceServiceImplTest.java @@ -0,0 +1,161 @@ +package com.njcn.gather.aireport.testdevice.service.impl; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.gather.aireport.testdevice.component.TestDeviceReportStorageService; +import com.njcn.gather.aireport.testdevice.mapper.TestDeviceMapper; +import com.njcn.gather.aireport.testdevice.pojo.param.TestDeviceParam; +import com.njcn.gather.aireport.testdevice.pojo.po.TestDevicePO; +import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceTypeNameVO; +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.mockito.Mockito; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.util.ReflectionTestUtils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.time.LocalDate; +import java.util.Collections; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +class TestDeviceServiceImplTest { + + private final TestDeviceMapper testDeviceMapper = Mockito.mock(TestDeviceMapper.class); + private final TestDeviceReportStorageService reportStorageService = Mockito.mock(TestDeviceReportStorageService.class); + + @Test + void addTestDeviceShouldRejectExistingDeletedNumber() { + TestableService service = buildService(); + service.countResult = 1; + Mockito.when(testDeviceMapper.countNormalDeviceType("type-1")).thenReturn(1); + + TestDeviceParam.AddParam param = new TestDeviceParam.AddParam(); + param.setType("type-1"); + param.setNo("TD-001"); + param.setValidityPeriod("2026-06-25"); + + BusinessException exception = Assertions.assertThrows(BusinessException.class, + () -> service.addTestDevice(param, new MockMultipartFile("reportFile", "report.pdf", + "application/pdf", "%PDF-1.4".getBytes()))); + + Assertions.assertEquals("设备编号已存在", exception.getMessage()); + Mockito.verifyNoInteractions(reportStorageService); + } + + @Test + void updateTestDeviceShouldDeleteNewReportWhenUpdateReturnsFalse() { + TestableService service = buildService(); + service.countResult = 0; + service.updateResult = false; + service.requiredDevice = buildDevice("device-1", "old/report.pdf"); + Mockito.when(testDeviceMapper.countNormalDeviceType("type-1")).thenReturn(1); + Mockito.when(reportStorageService.save(Mockito.any())).thenReturn("new/report.pdf"); + + TestDeviceParam.UpdateParam param = new TestDeviceParam.UpdateParam(); + param.setId("device-1"); + param.setType("type-1"); + param.setNo("TD-001"); + param.setValidityPeriod("2026-06-25"); + + boolean result = service.updateTestDevice(param, + new MockMultipartFile("reportFile", "report.pdf", "application/pdf", "%PDF-1.4".getBytes())); + + Assertions.assertFalse(result); + Mockito.verify(reportStorageService).deleteQuietly("new/report.pdf"); + Mockito.verify(reportStorageService, Mockito.never()).deleteQuietly("old/report.pdf"); + } + + @Test + void importTestDevicesShouldRejectOversizedPdfInZip() throws Exception { + TestableService service = buildService(); + Mockito.when(testDeviceMapper.selectNormalDeviceTypes()).thenReturn(Collections.singletonList(buildType("type-1", "示波器"))); + + BusinessException exception = Assertions.assertThrows(BusinessException.class, + () -> service.importTestDevices(buildExcelFile(), buildZipFile(10 * 1024 * 1024 + 1))); + + Assertions.assertEquals("校准报告不能超过10MB", exception.getMessage()); + } + + private TestableService buildService() { + TestableService service = new TestableService(reportStorageService); + ReflectionTestUtils.setField(service, "baseMapper", testDeviceMapper); + return service; + } + + private TestDevicePO buildDevice(String id, String reportPath) { + TestDevicePO device = new TestDevicePO(); + device.setId(id); + device.setType("type-1"); + device.setNo("TD-001"); + device.setValidityPeriod(LocalDate.of(2026, 6, 25)); + device.setValidityReport(reportPath); + return device; + } + + private TestDeviceTypeNameVO buildType(String id, String name) { + TestDeviceTypeNameVO type = new TestDeviceTypeNameVO(); + type.setId(id); + type.setName(name); + return type; + } + + private MockMultipartFile buildExcelFile() throws IOException { + try (XSSFWorkbook workbook = new XSSFWorkbook(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + XSSFSheet sheet = workbook.createSheet("导入"); + sheet.createRow(0).createCell(0).setCellValue("设备型号"); + sheet.getRow(0).createCell(1).setCellValue("设备编号"); + sheet.getRow(0).createCell(2).setCellValue("校准有效期"); + sheet.getRow(0).createCell(3).setCellValue("设备状态"); + sheet.getRow(0).createCell(4).setCellValue("校准报告文件名"); + sheet.createRow(1).createCell(0).setCellValue("示波器"); + sheet.getRow(1).createCell(1).setCellValue("TD-001"); + sheet.getRow(1).createCell(2).setCellValue("2026-06-25"); + sheet.getRow(1).createCell(3).setCellValue("在运"); + sheet.getRow(1).createCell(4).setCellValue("report.pdf"); + workbook.write(outputStream); + return new MockMultipartFile("file", "test-device.xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", outputStream.toByteArray()); + } + } + + private MockMultipartFile buildZipFile(int pdfSize) throws IOException { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) { + zipOutputStream.putNextEntry(new ZipEntry("report.pdf")); + zipOutputStream.write(new byte[pdfSize]); + zipOutputStream.closeEntry(); + zipOutputStream.finish(); + return new MockMultipartFile("reportZip", "report.zip", "application/zip", outputStream.toByteArray()); + } + } + + private static class TestableService extends TestDeviceServiceImpl { + private int countResult; + private boolean updateResult = true; + private TestDevicePO requiredDevice; + + private TestableService(TestDeviceReportStorageService reportStorageService) { + super(reportStorageService); + } + + @Override + public int count(Wrapper queryWrapper) { + return countResult; + } + + @Override + public boolean updateById(TestDevicePO entity) { + return updateResult; + } + + @Override + public TestDevicePO requireNormal(String id) { + return requiredDevice; + } + } +} diff --git a/ai-report/test-report/pom.xml b/ai-report/test-report/pom.xml index 95a21da..af8f158 100644 --- a/ai-report/test-report/pom.xml +++ b/ai-report/test-report/pom.xml @@ -11,8 +11,21 @@ test-report + jar + + com.njcn.gather + comservice + 1.0.0 + + + + com.njcn.gather + report-model + 1.0.0 + + com.njcn njcn-common @@ -30,5 +43,27 @@ spingboot2.3.12 2.3.12 + + + com.njcn.gather + system + 1.0.0 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-test + test + 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 new file mode 100644 index 0000000..c306828 --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/controller/TestReportController.java @@ -0,0 +1,111 @@ +package com.njcn.gather.aireport.testreport.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.njcn.common.pojo.annotation.OperateInfo; +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.TestReportParam; +import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO; +import com.njcn.gather.aireport.testreport.service.TestReportService; +import com.njcn.web.controller.BaseController; +import com.njcn.web.utils.HttpResultUtil; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiImplicitParam; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +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.RestController; + +import java.util.List; + +@Api(tags = "普测报告管理") +@RestController +@RequestMapping("/api/test-report") +@RequiredArgsConstructor +public class TestReportController extends BaseController { + + private final TestReportService testReportService; + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询普测报告列表") + @PostMapping("/list") + public HttpResult> list(@RequestBody(required = false) TestReportParam.QueryParam param) { + String methodDescribe = getMethodDescribe("list"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, + testReportService.listTestReports(param), methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD) + @ApiOperation("新增普测报告") + @PostMapping("/add") + public HttpResult add(@RequestBody @Validated TestReportParam.AddParam param) { + String methodDescribe = getMethodDescribe("add"); + boolean result = testReportService.addTestReport(param); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, + result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE) + @ApiOperation("编辑普测报告") + @PostMapping("/update") + public HttpResult update(@RequestBody @Validated TestReportParam.UpdateParam param) { + String methodDescribe = getMethodDescribe("update"); + boolean result = testReportService.updateTestReport(param); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, + result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DELETE) + @ApiOperation("删除普测报告") + @PostMapping("/delete") + public HttpResult delete(@RequestBody List ids) { + String methodDescribe = getMethodDescribe("delete"); + boolean result = testReportService.deleteTestReports(ids); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, + result, methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("查询普测报告详情") + @ApiImplicitParam(name = "id", value = "普测报告ID", required = true) + @GetMapping("/{id}") + public HttpResult detail(@PathVariable("id") String id) { + String methodDescribe = getMethodDescribe("detail"); + return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, + testReportService.getTestReport(id), methodDescribe); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("预览普测报告关联模板文件") + @ApiImplicitParam(name = "id", value = "普测报告ID", required = true) + @GetMapping("/{id}/preview") + public ResponseEntity preview(@PathVariable("id") String id) { + return testReportService.previewTemplate(id); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON) + @ApiOperation("导出普测报告") + @GetMapping("/export") + public void export(@ModelAttribute TestReportParam.QueryParam param) { + testReportService.exportTestReports(param); + } + + @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE) + @ApiOperation("提交普测报告审核") + @PostMapping("/submit-audit") + public HttpResult submitAudit(@RequestBody @Validated TestReportParam.SubmitAuditParam param) { + String methodDescribe = getMethodDescribe("submitAudit"); + boolean result = testReportService.submitAudit(param); + return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, + result, methodDescribe); + } +} 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 new file mode 100644 index 0000000..bddf674 --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/mapper/TestReportMapper.java @@ -0,0 +1,72 @@ +package com.njcn.gather.aireport.testreport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +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.TestReportVO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +@Mapper +public interface TestReportMapper extends BaseMapper { + + @Select({ + "" + }) + Page selectReportPage(Page page, @Param("param") TestReportParam.QueryParam param); + + @Select({ + "select r.id, r.no, r.client_unit_id as clientUnitId, cu.name as clientUnitName,", + "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.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", + "left join ai_clientunit cu on r.client_unit_id = cu.id", + "left join ai_reportmodel rm on r.report_model_id = rm.id", + "left join sys_user creator on r.create_by = creator.id", + "left join sys_user updater on r.update_by = updater.id", + "left join sys_user checker on r.check_id = checker.id", + "where r.id = #{id} and r.status = 1" + }) + TestReportVO selectReportDetail(@Param("id") String id); + + @Select("select count(1) from ai_clientunit where id = #{id} and status = 1") + int countNormalClientUnit(@Param("id") String id); + + @Select("select count(1) from ai_reportmodel where id = #{id} and status = 1") + int countNormalReportModel(@Param("id") String id); +} 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 new file mode 100644 index 0000000..ad12e11 --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/constant/TestReportConst.java @@ -0,0 +1,16 @@ +package com.njcn.gather.aireport.testreport.pojo.constant; + +public final class TestReportConst { + + public static final String STATE_EDITING = "01"; + public static final String STATE_AUDITING = "02"; + public static final String STATE_APPROVED = "03"; + public static final String STATE_REJECTED = "04"; + public static final String STATE_DELETED = "05"; + + public static final Integer STATUS_DELETED = 0; + public static final Integer STATUS_NORMAL = 1; + + private TestReportConst() { + } +} 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 new file mode 100644 index 0000000..755bbd1 --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/param/TestReportParam.java @@ -0,0 +1,83 @@ +package com.njcn.gather.aireport.testreport.pojo.param; + +import com.njcn.web.pojo.param.BaseParam; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +public class TestReportParam { + + @Data + @EqualsAndHashCode(callSuper = true) + public static class QueryParam extends BaseParam { + @ApiModelProperty("\u5173\u952e\u5b57\uff0c\u5339\u914d\u62a5\u544a\u7f16\u53f7\u3001\u5408\u540c\u7f16\u53f7\u3001\u59d4\u6258\u5355\u4f4d\u6216\u62a5\u544a\u6a21\u677f") + private String keyword; + + @ApiModelProperty("\u521b\u5efa\u5f00\u59cb\u65f6\u95f4\uff0cyyyy-MM-dd HH:mm:ss") + private String searchBeginTime; + + @ApiModelProperty("\u521b\u5efa\u7ed3\u675f\u65f6\u95f4\uff0cyyyy-MM-dd HH:mm:ss") + private String searchEndTime; + + @ApiModelProperty("\u62a5\u544a\u72b6\u6001") + private String state; + } + + @Data + public static class AddParam { + @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") + private String no; + + @ApiModelProperty("\u59d4\u6258\u5355\u4f4dID") + @NotBlank(message = "\u59d4\u6258\u5355\u4f4d\u4e0d\u80fd\u4e3a\u7a7a") + private String clientUnitId; + + @ApiModelProperty("\u62a5\u544a\u6a21\u677fID") + @NotBlank(message = "\u62a5\u544a\u6a21\u677f\u4e0d\u80fd\u4e3a\u7a7a") + private String reportModelId; + + @ApiModelProperty("\u68c0\u6d4b\u516c\u53f8ID") + @NotBlank(message = "\u68c0\u6d4b\u516c\u53f8\u4e0d\u80fd\u4e3a\u7a7a") + private String createUnit; + + @ApiModelProperty("\u5408\u540c\u7f16\u53f7") + @NotBlank(message = "\u5408\u540c\u7f16\u53f7\u4e0d\u80fd\u4e3a\u7a7a") + @Size(max = 32, message = "\u5408\u540c\u7f16\u53f7\u4e0d\u80fd\u8d85\u8fc732\u4e2a\u5b57\u7b26") + private String contractNumber; + + @ApiModelProperty("\u68c0\u6d4b\u4f9d\u636e\uff0cJSON\u5b57\u7b26\u4e32\u6570\u7ec4") + @NotBlank(message = "\u68c0\u6d4b\u4f9d\u636e\u4e0d\u80fd\u4e3a\u7a7a") + private String standard; + + @ApiModelProperty("\u53d7\u8d44\u6570\u636e\uff0cJSON\u5bf9\u8c61\u6216JSON\u6570\u7ec4") + @NotBlank(message = "\u53d7\u8d44\u6570\u636e\u4e0d\u80fd\u4e3a\u7a7a") + private String data; + + @ApiModelProperty("\u8054\u7cfb\u7535\u8bdd") + @Size(max = 32, message = "\u8054\u7cfb\u7535\u8bdd\u4e0d\u80fd\u8d85\u8fc732\u4e2a\u5b57\u7b26") + private String phonenumber; + } + + @Data + public static class UpdateParam extends AddParam { + @ApiModelProperty("\u666e\u6d4b\u62a5\u544aID") + @NotBlank(message = "\u666e\u6d4b\u62a5\u544aID\u4e0d\u80fd\u4e3a\u7a7a") + private String id; + } + + @Data + public static class SubmitAuditParam { + @ApiModelProperty("\u666e\u6d4b\u62a5\u544aID") + @NotBlank(message = "\u666e\u6d4b\u62a5\u544aID\u4e0d\u80fd\u4e3a\u7a7a") + private String id; + + @ApiModelProperty("\u5ba1\u6838\u610f\u89c1") + @Size(max = 128, message = "\u5ba1\u6838\u610f\u89c1\u4e0d\u80fd\u8d85\u8fc7128\u4e2a\u5b57\u7b26") + private String checkResult; + } +} 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 new file mode 100644 index 0000000..80a6efe --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/po/TestReportPO.java @@ -0,0 +1,53 @@ +package com.njcn.gather.aireport.testreport.pojo.po; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +@TableName("ai_testreport") +public class TestReportPO implements Serializable { + private static final long serialVersionUID = -3038286126414044160L; + + @TableId("id") + private String id; + @TableField("no") + private String no; + @TableField("client_unit_id") + private String clientUnitId; + @TableField("report_model_id") + private String reportModelId; + @TableField("create_unit") + private String createUnit; + @TableField("contract_number") + private String contractNumber; + @TableField("standard") + private String standard; + @TableField("data") + private String data; + @TableField("phonenumber") + private String phonenumber; + @TableField("state") + private String state; + @TableField("check_id") + private String checkId; + @TableField("check_time") + private LocalDate checkTime; + @TableField("check_result") + private String checkResult; + @TableField("status") + private Integer status; + @TableField("create_by") + private String createBy; + @TableField("create_time") + private LocalDateTime createTime; + @TableField("update_by") + private String updateBy; + @TableField("update_time") + private LocalDateTime updateTime; +} diff --git a/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportExcelVO.java b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportExcelVO.java new file mode 100644 index 0000000..c575a17 --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportExcelVO.java @@ -0,0 +1,53 @@ +package com.njcn.gather.aireport.testreport.pojo.vo; + +import cn.afterturn.easypoi.excel.annotation.Excel; +import lombok.Data; + +import java.io.Serializable; + +@Data +public class TestReportExcelVO implements Serializable { + private static final long serialVersionUID = -3010688465572795334L; + + @Excel(name = "\u666e\u6d4b\u62a5\u544a\u7f16\u53f7", width = 25) + private String no; + + @Excel(name = "\u59d4\u6258\u5355\u4f4d", width = 25) + private String clientUnitName; + + @Excel(name = "\u62a5\u544a\u6a21\u677f", width = 25) + private String reportModelName; + + @Excel(name = "\u68c0\u6d4b\u516c\u53f8", width = 20) + private String createUnit; + + @Excel(name = "\u5408\u540c\u7f16\u53f7", width = 20) + private String contractNumber; + + @Excel(name = "\u68c0\u6d4b\u4f9d\u636e", width = 35) + private String standard; + + @Excel(name = "\u53d7\u8d44\u6570\u636e", width = 35) + private String data; + + @Excel(name = "\u8054\u7cfb\u7535\u8bdd", width = 20) + private String phonenumber; + + @Excel(name = "\u5ba1\u6838\u4eba", width = 20) + private String checkerName; + + @Excel(name = "\u5ba1\u6838\u65f6\u95f4", width = 20) + private String checkTime; + + @Excel(name = "\u5ba1\u6838\u610f\u89c1", width = 30) + private String checkResult; + + @Excel(name = "\u62a5\u544a\u72b6\u6001", width = 15) + private String state; + + @Excel(name = "\u521b\u5efa\u65f6\u95f4", width = 25) + private String createTime; + + @Excel(name = "\u521b\u5efa\u4eba", width = 20) + private String createByName; +} 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 new file mode 100644 index 0000000..c979f10 --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportVO.java @@ -0,0 +1,156 @@ +package com.njcn.gather.aireport.testreport.pojo.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.util.StringUtils; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +public class TestReportVO { + private static final String DEFAULT_EMPTY_DISPLAY = "-"; + + @ApiModelProperty("\u666e\u6d4b\u62a5\u544aID") + private String id; + @ApiModelProperty("\u666e\u6d4b\u62a5\u544a\u7f16\u53f7") + private String no; + @ApiModelProperty("\u59d4\u6258\u5355\u4f4dID") + private String clientUnitId; + @ApiModelProperty("\u59d4\u6258\u5355\u4f4d\u540d\u79f0") + private String clientUnitName; + @ApiModelProperty("\u62a5\u544a\u6a21\u677fID") + private String reportModelId; + @ApiModelProperty("\u62a5\u544a\u6a21\u677f\u540d\u79f0") + private String reportModelName; + @ApiModelProperty("\u68c0\u6d4b\u516c\u53f8ID") + private String createUnit; + @ApiModelProperty("\u5408\u540c\u7f16\u53f7") + private String contractNumber; + @ApiModelProperty("\u68c0\u6d4b\u4f9d\u636e") + private String standard; + @ApiModelProperty("\u53d7\u8d44\u6570\u636e") + private String data; + @ApiModelProperty("\u8054\u7cfb\u7535\u8bdd") + private String phonenumber; + @ApiModelProperty("\u5ba1\u6838\u4ebaID") + private String checkId; + @ApiModelProperty("\u5ba1\u6838\u4eba\u540d\u79f0") + private String checkerName; + @ApiModelProperty("\u5ba1\u6838\u65f6\u95f4") + @JsonFormat(pattern = "yyyy-MM-dd") + @JsonDeserialize(using = LocalDateDeserializer.class) + @JsonSerialize(using = LocalDateSerializer.class) + private LocalDate checkTime; + @ApiModelProperty("\u5ba1\u6838\u610f\u89c1") + private String checkResult; + @ApiModelProperty("\u62a5\u544a\u72b6\u6001") + private String state; + @ApiModelProperty("\u903b\u8f91\u72b6\u6001") + private Integer status; + @ApiModelProperty("\u521b\u5efa\u4eba\u663e\u793a\u540d") + private String createBy; + @ApiModelProperty("\u521b\u5efa\u4eba\u540d\u79f0") + private String createByName; + @ApiModelProperty("\u521b\u5efa\u65f6\u95f4") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + private LocalDateTime createTime; + @ApiModelProperty("\u66f4\u65b0\u4eba\u663e\u793a\u540d") + private String updateBy; + @ApiModelProperty("\u66f4\u65b0\u4eba\u540d\u79f0") + private String updateByName; + @ApiModelProperty("\u66f4\u65b0\u65f6\u95f4") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + @JsonSerialize(using = LocalDateTimeSerializer.class) + private LocalDateTime updateTime; + + public String getId() { + return defaultDisplay(id); + } + + public String getNo() { + return defaultDisplay(no); + } + + public String getClientUnitId() { + return defaultDisplay(clientUnitId); + } + + public String getClientUnitName() { + return defaultDisplay(clientUnitName); + } + + public String getReportModelId() { + return defaultDisplay(reportModelId); + } + + public String getReportModelName() { + return defaultDisplay(reportModelName); + } + + public String getCreateUnit() { + return defaultDisplay(createUnit); + } + + public String getContractNumber() { + return defaultDisplay(contractNumber); + } + + public String getStandard() { + return defaultDisplay(standard); + } + + public String getData() { + return defaultDisplay(data); + } + + public String getPhonenumber() { + return defaultDisplay(phonenumber); + } + + public String getCheckId() { + return defaultDisplay(checkId); + } + + public String getCheckerName() { + return defaultDisplay(checkerName); + } + + public String getCheckResult() { + return defaultDisplay(checkResult); + } + + public String getState() { + return defaultDisplay(state); + } + + public String getCreateBy() { + return defaultDisplay(createBy); + } + + public String getCreateByName() { + return defaultDisplay(createByName); + } + + public String getUpdateBy() { + return defaultDisplay(updateBy); + } + + public String getUpdateByName() { + return defaultDisplay(updateByName); + } + + private String defaultDisplay(String value) { + return StringUtils.hasText(value) ? value : DEFAULT_EMPTY_DISPLAY; + } +} 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 new file mode 100644 index 0000000..a69fdd2 --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/service/TestReportService.java @@ -0,0 +1,31 @@ +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.TestReportParam; +import com.njcn.gather.aireport.testreport.pojo.po.TestReportPO; +import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO; +import org.springframework.http.ResponseEntity; + +import java.util.List; + +public interface TestReportService extends IService { + + Page listTestReports(TestReportParam.QueryParam param); + + boolean addTestReport(TestReportParam.AddParam param); + + boolean updateTestReport(TestReportParam.UpdateParam param); + + boolean deleteTestReports(List ids); + + TestReportVO getTestReport(String id); + + TestReportPO requireNormal(String id); + + boolean submitAudit(TestReportParam.SubmitAuditParam param); + + void exportTestReports(TestReportParam.QueryParam param); + + ResponseEntity previewTemplate(String id); +} 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 new file mode 100644 index 0000000..b3b48b9 --- /dev/null +++ b/ai-report/test-report/src/main/java/com/njcn/gather/aireport/testreport/service/impl/TestReportServiceImpl.java @@ -0,0 +1,621 @@ +package com.njcn.gather.aireport.testreport.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +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.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.mapper.TestReportMapper; +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.TestReportPO; +import com.njcn.gather.aireport.testreport.pojo.vo.TestReportExcelVO; +import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO; +import com.njcn.gather.aireport.testreport.service.TestReportService; +import com.njcn.gather.comservice.filepreview.FilePreviewService; +import com.njcn.gather.system.dictionary.pojo.po.DictData; +import com.njcn.gather.system.dictionary.pojo.po.DictType; +import com.njcn.gather.system.dictionary.service.IDictDataService; +import com.njcn.gather.system.dictionary.service.IDictTypeService; +import com.njcn.gather.system.pojo.constant.DictConst; +import com.njcn.gather.system.util.ExportFileNameUtil; +import com.njcn.web.factory.PageFactory; +import com.njcn.web.utils.ExcelUtil; +import com.njcn.web.utils.RequestUtil; +import lombok.Data; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +public class TestReportServiceImpl extends ServiceImpl implements TestReportService { + + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final Integer DICT_STATE_NORMAL = 1; + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private static final String MSG_ID_REQUIRED = "\u666e\u6d4b\u62a5\u544aID\u4e0d\u80fd\u4e3a\u7a7a"; + private static final String MSG_REPORT_NOT_FOUND = "\u666e\u6d4b\u62a5\u544a\u4e0d\u5b58\u5728\u6216\u5df2\u5220\u9664"; + private static final String MSG_ONLY_EDITABLE_CAN_UPDATE = + "\u4ec5\u7f16\u5236\u6216\u9000\u56de\u72b6\u6001\u7684\u666e\u6d4b\u62a5\u544a\u5141\u8bb8\u7f16\u8f91"; + private static final String MSG_ONLY_EDITABLE_CAN_AUDIT = + "\u4ec5\u7f16\u5236\u6216\u9000\u56de\u72b6\u6001\u7684\u666e\u6d4b\u62a5\u544a\u5141\u8bb8\u63d0\u4ea4\u5ba1\u6838"; + private static final String MSG_START_TIME_INVALID = "\u5f00\u59cb\u65f6\u95f4\u683c\u5f0f\u4e0d\u6b63\u786e"; + private static final String MSG_END_TIME_INVALID = "\u7ed3\u675f\u65f6\u95f4\u683c\u5f0f\u4e0d\u6b63\u786e"; + private static final String MSG_PARAM_REQUIRED = "\u666e\u6d4b\u62a5\u544a\u53c2\u6570\u4e0d\u80fd\u4e3a\u7a7a"; + private static final String MSG_NO_REQUIRED = "\u666e\u6d4b\u62a5\u544a\u7f16\u53f7\u4e0d\u80fd\u4e3a\u7a7a"; + private static final String MSG_NO_TOO_LONG = "\u666e\u6d4b\u62a5\u544a\u7f16\u53f7\u4e0d\u80fd\u8d85\u8fc732\u4e2a\u5b57\u7b26"; + private static final String MSG_CLIENT_UNIT_REQUIRED = "\u59d4\u6258\u5355\u4f4d\u4e0d\u80fd\u4e3a\u7a7a"; + private static final String MSG_REPORT_MODEL_REQUIRED = "\u62a5\u544a\u6a21\u677f\u4e0d\u80fd\u4e3a\u7a7a"; + private static final String MSG_CREATE_UNIT_REQUIRED = "\u68c0\u6d4b\u516c\u53f8\u4e0d\u80fd\u4e3a\u7a7a"; + private static final String MSG_CONTRACT_REQUIRED = "\u5408\u540c\u7f16\u53f7\u4e0d\u80fd\u4e3a\u7a7a"; + private static final String MSG_CONTRACT_TOO_LONG = "\u5408\u540c\u7f16\u53f7\u4e0d\u80fd\u8d85\u8fc732\u4e2a\u5b57\u7b26"; + private static final String MSG_STANDARD_REQUIRED = "\u68c0\u6d4b\u4f9d\u636e\u4e0d\u80fd\u4e3a\u7a7a"; + private static final String MSG_DATA_REQUIRED = "\u53d7\u8d44\u6570\u636e\u4e0d\u80fd\u4e3a\u7a7a"; + private static final String MSG_DATA_INVALID = "\u53d7\u8d44\u6570\u636e\u683c\u5f0f\u4e0d\u6b63\u786e"; + private static final String MSG_PHONE_TOO_LONG = "\u8054\u7cfb\u7535\u8bdd\u4e0d\u80fd\u8d85\u8fc732\u4e2a\u5b57\u7b26"; + private static final String MSG_NO_EXISTS = "\u666e\u6d4b\u62a5\u544a\u7f16\u53f7\u5df2\u5b58\u5728"; + private static final String MSG_COMPANY_NOT_FOUND = "\u68c0\u6d4b\u516c\u53f8\u4e0d\u5b58\u5728\u6216\u5df2\u5220\u9664"; + private static final String MSG_STANDARD_INVALID = "\u6d4b\u8bd5\u4f9d\u636e\u683c\u5f0f\u4e0d\u6b63\u786e"; + private static final String MSG_STANDARD_NOT_FOUND = "\u6d4b\u8bd5\u4f9d\u636e\u4e0d\u5b58\u5728\u6216\u5df2\u5220\u9664"; + private static final String MSG_CLIENT_UNIT_NOT_FOUND = "\u59d4\u6258\u5355\u4f4d\u4e0d\u5b58\u5728\u6216\u5df2\u5220\u9664"; + private static final String MSG_REPORT_MODEL_NOT_FOUND = "\u62a5\u544a\u6a21\u677f\u4e0d\u5b58\u5728\u6216\u5df2\u5220\u9664"; + private static final String MSG_PREVIEW_NOT_READY = "\u62a5\u544a\u6a21\u677f\u9884\u89c8\u670d\u52a1\u672a\u521d\u59cb\u5316"; + private static final String MSG_TEMPLATE_FILE_NOT_FOUND = "\u62a5\u544a\u6a21\u677f\u6587\u4ef6\u4e0d\u5b58\u5728"; + private static final String MSG_ENCODE_FILE_NAME_FAILED = "\u7f16\u7801\u62a5\u544a\u6a21\u677f\u6587\u4ef6\u540d\u5931\u8d25"; + + private final ReportModelService reportModelService; + private final ReportModelFileStorageService reportModelFileStorageService; + private final FilePreviewService filePreviewService; + private final IDictTypeService dictTypeService; + private final IDictDataService dictDataService; + + public TestReportServiceImpl(ReportModelService reportModelService, + ReportModelFileStorageService reportModelFileStorageService, + FilePreviewService filePreviewService, + IDictTypeService dictTypeService, + IDictDataService dictDataService) { + this.reportModelService = reportModelService; + this.reportModelFileStorageService = reportModelFileStorageService; + this.filePreviewService = filePreviewService; + this.dictTypeService = dictTypeService; + this.dictDataService = dictDataService; + } + + @Override + public Page listTestReports(TestReportParam.QueryParam param) { + TestReportParam.QueryParam query = normalizeQueryParam(param); + Page page = this.baseMapper.selectReportPage( + new Page(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), query); + fillDisplayUserNames(page.getRecords(), buildUserNameMap(page.getRecords())); + fillDisplayFields(page.getRecords()); + return page; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean addTestReport(TestReportParam.AddParam param) { + NormalizedReport data = normalizeParam(param); + checkNoUnique(data.getNo(), null); + checkClientUnit(data.getClientUnitId()); + checkReportModel(data.getReportModelId()); + checkCreateUnit(data.getCreateUnit()); + checkStandard(data.getStandard()); + + LocalDateTime now = LocalDateTime.now(); + String userId = resolveCurrentUserId(); + TestReportPO report = new TestReportPO(); + report.setId(UUID.randomUUID().toString()); + applyBusinessFields(report, data); + report.setState(TestReportConst.STATE_EDITING); + report.setStatus(TestReportConst.STATUS_NORMAL); + report.setCreateBy(userId); + report.setCreateTime(now); + report.setUpdateBy(userId); + report.setUpdateTime(now); + return this.save(report); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateTestReport(TestReportParam.UpdateParam param) { + if (param == null || StrUtil.isBlank(param.getId())) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_ID_REQUIRED); + } + TestReportPO report = requireNormal(param.getId()); + if (!allowEdit(report.getState())) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_ONLY_EDITABLE_CAN_UPDATE); + } + NormalizedReport data = normalizeParam(param); + checkNoUnique(data.getNo(), report.getId()); + checkClientUnit(data.getClientUnitId()); + checkReportModel(data.getReportModelId()); + checkCreateUnit(data.getCreateUnit()); + checkStandard(data.getStandard()); + + applyBusinessFields(report, data); + report.setUpdateBy(resolveCurrentUserId()); + report.setUpdateTime(LocalDateTime.now()); + return this.updateById(report); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean deleteTestReports(List ids) { + List validIds = normalizeIds(ids); + if (validIds.isEmpty()) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_ID_REQUIRED); + } + return this.lambdaUpdate() + .set(TestReportPO::getState, TestReportConst.STATE_DELETED) + .set(TestReportPO::getStatus, TestReportConst.STATUS_DELETED) + .set(TestReportPO::getUpdateBy, resolveCurrentUserId()) + .set(TestReportPO::getUpdateTime, LocalDateTime.now()) + .in(TestReportPO::getId, validIds) + .eq(TestReportPO::getStatus, TestReportConst.STATUS_NORMAL) + .update(); + } + + @Override + public TestReportVO getTestReport(String id) { + TestReportVO report = this.baseMapper.selectReportDetail(requireText(id, MSG_ID_REQUIRED)); + if (report == null) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_REPORT_NOT_FOUND); + } + fillDisplayUserNames(report, buildUserNameMap(Collections.singletonList(report))); + fillDisplayFields(Collections.singletonList(report)); + return report; + } + + @Override + public TestReportPO requireNormal(String id) { + String normalizedId = requireText(id, MSG_ID_REQUIRED); + TestReportPO report = this.lambdaQuery() + .eq(TestReportPO::getId, normalizedId) + .eq(TestReportPO::getStatus, TestReportConst.STATUS_NORMAL) + .one(); + if (report == null) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_REPORT_NOT_FOUND); + } + return report; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean submitAudit(TestReportParam.SubmitAuditParam param) { + if (param == null || StrUtil.isBlank(param.getId())) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_ID_REQUIRED); + } + TestReportPO report = requireNormal(param.getId()); + if (!allowEdit(report.getState())) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_ONLY_EDITABLE_CAN_AUDIT); + } + report.setCheckId(null); + report.setCheckTime(null); + report.setCheckResult(null); + report.setState(TestReportConst.STATE_AUDITING); + report.setUpdateBy(resolveCurrentUserId()); + report.setUpdateTime(LocalDateTime.now()); + return this.updateById(report); + } + + @Override + public void exportTestReports(TestReportParam.QueryParam param) { + TestReportParam.QueryParam query = normalizeQueryParam(param); + Page page = this.baseMapper.selectReportPage(new Page(1, Integer.MAX_VALUE), query); + fillDisplayUserNames(page.getRecords(), buildUserNameMap(page.getRecords())); + fillDisplayFields(page.getRecords()); + List exportRows = page.getRecords().stream() + .map(this::toExcelVO) + .collect(Collectors.toList()); + ExcelUtil.exportExcel(ExportFileNameUtil.appendToday("\u666e\u6d4b\u62a5\u544a\u5217\u8868.xlsx"), + "\u666e\u6d4b\u62a5\u544a\u5217\u8868", TestReportExcelVO.class, exportRows); + } + + @Override + public ResponseEntity previewTemplate(String id) { + TestReportPO report = requireNormal(id); + ReportModelPO model = requireReportModel(report.getReportModelId()); + Path filePath = reportModelFileStorageService.resolveDownloadPath(model.getPath()); + if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_TEMPLATE_FILE_NOT_FOUND); + } + String fileName = resolvePreviewFileName(model); + FilePreviewService.PreviewContent previewContent = filePreviewService.preview(filePath, fileName); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename*=UTF-8''" + encodeFileName(fileName)) + .header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate") + .contentType(MediaType.parseMediaType(previewContent.getContentType())) + .body(previewContent.getContent()); + } + + private TestReportParam.QueryParam normalizeQueryParam(TestReportParam.QueryParam param) { + TestReportParam.QueryParam query = param == null ? new TestReportParam.QueryParam() : param; + query.setKeyword(trimToNull(query.getKeyword())); + if (StrUtil.isNotBlank(query.getSearchBeginTime())) { + query.setSearchBeginTime(formatDateTime(parseDateTime(query.getSearchBeginTime(), MSG_START_TIME_INVALID))); + } + if (StrUtil.isNotBlank(query.getSearchEndTime())) { + query.setSearchEndTime(formatDateTime(parseDateTime(query.getSearchEndTime(), MSG_END_TIME_INVALID))); + } + return query; + } + + private NormalizedReport normalizeParam(TestReportParam.AddParam param) { + if (param == null) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_PARAM_REQUIRED); + } + NormalizedReport data = new NormalizedReport(); + data.setNo(requireText(param.getNo(), MSG_NO_REQUIRED, 32, MSG_NO_TOO_LONG)); + data.setClientUnitId(requireText(param.getClientUnitId(), MSG_CLIENT_UNIT_REQUIRED)); + data.setReportModelId(requireText(param.getReportModelId(), MSG_REPORT_MODEL_REQUIRED)); + data.setCreateUnit(requireText(param.getCreateUnit(), MSG_CREATE_UNIT_REQUIRED)); + data.setContractNumber(requireText(param.getContractNumber(), MSG_CONTRACT_REQUIRED, 32, MSG_CONTRACT_TOO_LONG)); + data.setStandard(requireText(param.getStandard(), MSG_STANDARD_REQUIRED)); + data.setData(requireJsonObjectOrArrayText(param.getData(), MSG_DATA_REQUIRED, MSG_DATA_INVALID)); + data.setPhonenumber(trimToNull(param.getPhonenumber())); + if (data.getPhonenumber() != null && data.getPhonenumber().length() > 32) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_PHONE_TOO_LONG); + } + return data; + } + + private void applyBusinessFields(TestReportPO report, NormalizedReport data) { + report.setNo(data.getNo()); + report.setClientUnitId(data.getClientUnitId()); + report.setReportModelId(data.getReportModelId()); + report.setCreateUnit(data.getCreateUnit()); + report.setContractNumber(data.getContractNumber()); + report.setStandard(data.getStandard()); + report.setData(data.getData()); + report.setPhonenumber(data.getPhonenumber()); + } + + private boolean allowEdit(String state) { + return TestReportConst.STATE_EDITING.equals(state) || TestReportConst.STATE_REJECTED.equals(state); + } + + private void checkNoUnique(String no, String excludeId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper(); + wrapper.eq(TestReportPO::getNo, no) + .eq(TestReportPO::getStatus, TestReportConst.STATUS_NORMAL) + .ne(StrUtil.isNotBlank(excludeId), TestReportPO::getId, excludeId); + if (this.count(wrapper) > 0) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_NO_EXISTS); + } + } + + private TestReportExcelVO toExcelVO(TestReportVO vo) { + TestReportExcelVO excelVO = new TestReportExcelVO(); + excelVO.setNo(vo.getNo()); + excelVO.setClientUnitName(StrUtil.isNotBlank(vo.getClientUnitName()) ? vo.getClientUnitName() : vo.getClientUnitId()); + excelVO.setReportModelName(StrUtil.isNotBlank(vo.getReportModelName()) ? vo.getReportModelName() : vo.getReportModelId()); + excelVO.setCreateUnit(vo.getCreateUnit()); + excelVO.setContractNumber(vo.getContractNumber()); + excelVO.setStandard(vo.getStandard()); + excelVO.setData(vo.getData()); + excelVO.setPhonenumber(vo.getPhonenumber()); + excelVO.setCheckerName(StrUtil.isNotBlank(vo.getCheckerName()) ? vo.getCheckerName() : vo.getCheckId()); + excelVO.setCheckTime(vo.getCheckTime() == null ? null : vo.getCheckTime().toString()); + excelVO.setCheckResult(vo.getCheckResult()); + excelVO.setState(vo.getState()); + excelVO.setCreateTime(vo.getCreateTime() == null ? null : DATE_TIME_FORMATTER.format(vo.getCreateTime())); + excelVO.setCreateByName(StrUtil.isNotBlank(vo.getCreateByName()) ? vo.getCreateByName() : vo.getCreateBy()); + return excelVO; + } + + void fillDisplayUserNames(List reports, Map userNameMap) { + if (reports == null || reports.isEmpty()) { + return; + } + for (TestReportVO report : reports) { + fillDisplayUserNames(report, userNameMap); + } + } + + void fillDisplayUserNames(TestReportVO report, Map userNameMap) { + if (report == null) { + return; + } + report.setCreateBy(resolveDisplayUserName(report.getCreateBy(), userNameMap)); + report.setUpdateBy(resolveDisplayUserName(report.getUpdateBy(), userNameMap)); + } + + void fillDisplayFields(List reports) { + if (reports == null || reports.isEmpty()) { + return; + } + Map companyNameMap = buildDictNameMap(DictConst.TEST_REPORT_COMPANY_DICT_CODE); + Map standardNameMap = buildDictNameMap(DictConst.TEST_REPORT_STANDARD_DICT_CODE); + for (TestReportVO report : reports) { + if (report == null) { + continue; + } + report.setCreateUnit(resolveDictDisplayValue(report.getCreateUnit(), companyNameMap)); + report.setStandard(resolveStandardDisplayValue(report.getStandard(), standardNameMap)); + } + } + + private Map buildUserNameMap(Collection reports) { + if (reports == null || reports.isEmpty()) { + return Collections.emptyMap(); + } + Map userNameMap = new LinkedHashMap(); + for (TestReportVO report : reports) { + if (report == null) { + continue; + } + putUserName(userNameMap, report.getCreateBy(), report.getCreateByName()); + putUserName(userNameMap, report.getUpdateBy(), report.getUpdateByName()); + putUserName(userNameMap, report.getCheckId(), report.getCheckerName()); + } + return userNameMap; + } + + private Map buildDictNameMap(String typeCode) { + if (dictTypeService == null || dictDataService == null || StrUtil.isBlank(typeCode)) { + return Collections.emptyMap(); + } + DictType dictType = dictTypeService.getByCode(typeCode); + if (dictType == null || StrUtil.isBlank(dictType.getId()) || !DICT_STATE_NORMAL.equals(dictType.getState())) { + return Collections.emptyMap(); + } + List dictDataList = dictDataService.listDictDataByTypeId(dictType.getId()); + if (dictDataList == null || dictDataList.isEmpty()) { + return Collections.emptyMap(); + } + Map result = new LinkedHashMap(); + for (DictData dictData : dictDataList) { + if (dictData == null || StrUtil.isBlank(dictData.getId()) || StrUtil.isBlank(dictData.getName()) + || !DICT_STATE_NORMAL.equals(dictData.getState())) { + continue; + } + result.put(dictData.getId(), dictData.getName()); + } + return result; + } + + private void putUserName(Map userNameMap, String userId, String userName) { + String validUserId = trimToNull(userId); + String validUserName = trimToNull(userName); + if (validUserId == null || validUserName == null || "-".equals(validUserName)) { + return; + } + userNameMap.put(validUserId, validUserName); + } + + private String resolveDisplayUserName(String userId, Map userNameMap) { + String validUserId = trimToNull(userId); + if (validUserId == null) { + return null; + } + if (userNameMap == null || userNameMap.isEmpty()) { + return validUserId; + } + String userName = trimToNull(userNameMap.get(validUserId)); + return userName == null ? validUserId : userName; + } + + private String resolveDictDisplayValue(String rawValue, Map dictNameMap) { + String validValue = trimToNull(rawValue); + if (validValue == null) { + return null; + } + if (dictNameMap == null || dictNameMap.isEmpty()) { + return validValue; + } + String displayValue = trimToNull(dictNameMap.get(validValue)); + return displayValue == null ? validValue : displayValue; + } + + private String resolveStandardDisplayValue(String rawValue, Map dictNameMap) { + String validValue = trimToNull(rawValue); + if (validValue == null) { + return null; + } + List standardIds = parseJsonArray(validValue); + if (standardIds.isEmpty()) { + return validValue; + } + List displayValues = new ArrayList(); + for (String standardId : standardIds) { + String displayValue = resolveDictDisplayValue(standardId, dictNameMap); + if (displayValue != null) { + displayValues.add(displayValue); + } + } + if (displayValues.isEmpty()) { + return validValue; + } + return String.join(", ", displayValues); + } + + private List parseJsonArray(String jsonArrayText) { + String text = trimToNull(jsonArrayText); + if (text == null) { + return Collections.emptyList(); + } + try { + List parsedItems = OBJECT_MAPPER.readValue(text, new TypeReference>() { + }); + if (parsedItems == null || parsedItems.isEmpty()) { + return Collections.emptyList(); + } + List result = new ArrayList(); + for (String item : parsedItems) { + String normalizedItem = trimToNull(item); + if (normalizedItem != null) { + result.add(normalizedItem); + } + } + return result; + } catch (Exception exception) { + return Collections.emptyList(); + } + } + + private String requireJsonObjectOrArrayText(String value, String blankMessage, String invalidMessage) { + String text = requireText(value, blankMessage); + try { + JsonNode jsonNode = OBJECT_MAPPER.readTree(text); + if (jsonNode == null || !(jsonNode.isObject() || jsonNode.isArray())) { + throw new BusinessException(CommonResponseEnum.FAIL, invalidMessage); + } + return text; + } catch (BusinessException exception) { + throw exception; + } catch (Exception exception) { + throw new BusinessException(CommonResponseEnum.FAIL, invalidMessage); + } + } + + private void checkCreateUnit(String createUnit) { + Map companyNameMap = buildDictNameMap(DictConst.TEST_REPORT_COMPANY_DICT_CODE); + if (!companyNameMap.containsKey(createUnit)) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_COMPANY_NOT_FOUND); + } + } + + private void checkStandard(String standard) { + List standardIds = parseJsonArray(standard); + if (standardIds.isEmpty()) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_STANDARD_INVALID); + } + Map standardNameMap = buildDictNameMap(DictConst.TEST_REPORT_STANDARD_DICT_CODE); + for (String standardId : standardIds) { + if (!standardNameMap.containsKey(standardId)) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_STANDARD_NOT_FOUND); + } + } + } + + private void checkClientUnit(String id) { + if (this.baseMapper.countNormalClientUnit(id) <= 0) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_CLIENT_UNIT_NOT_FOUND); + } + } + + private void checkReportModel(String id) { + if (this.baseMapper.countNormalReportModel(id) <= 0) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_REPORT_MODEL_NOT_FOUND); + } + } + + private ReportModelPO requireReportModel(String id) { + if (reportModelService == null || reportModelFileStorageService == null || filePreviewService == null) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_PREVIEW_NOT_READY); + } + return reportModelService.requireNormal(id); + } + + private String resolvePreviewFileName(ReportModelPO model) { + String fileName = trimToNull(model.getFileName()); + if (fileName != null) { + return fileName; + } + return reportModelFileStorageService.resolveDownloadFileName(model.getPath()); + } + + private List normalizeIds(List ids) { + if (ids == null || ids.isEmpty()) { + return Collections.emptyList(); + } + List result = new ArrayList(); + for (String id : ids) { + String normalizedId = trimToNull(id); + if (normalizedId != null && !result.contains(normalizedId)) { + result.add(normalizedId); + } + } + return result; + } + + private LocalDateTime parseDateTime(String value, String errorMessage) { + try { + return LocalDateTime.parse(value.trim(), DATE_TIME_FORMATTER); + } catch (DateTimeParseException exception) { + throw new BusinessException(CommonResponseEnum.FAIL, errorMessage); + } + } + + private String formatDateTime(LocalDateTime value) { + return DATE_TIME_FORMATTER.format(value); + } + + private String requireText(String value, String blankMessage) { + String text = trimToNull(value); + if (text == null) { + throw new BusinessException(CommonResponseEnum.FAIL, blankMessage); + } + return text; + } + + private String requireText(String value, String blankMessage, int maxLength, String lengthMessage) { + String text = requireText(value, blankMessage); + if (text.length() > maxLength) { + throw new BusinessException(CommonResponseEnum.FAIL, lengthMessage); + } + return text; + } + + private String resolveCurrentUserId() { + String userId = RequestUtil.getUserId(); + return StrUtil.isBlank(userId) ? null : userId; + } + + private String trimToNull(String value) { + if (value == null) { + return null; + } + String text = value.trim(); + if (text.isEmpty() || "-".equals(text)) { + return null; + } + return text; + } + + private String encodeFileName(String fileName) { + try { + return URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replace("+", "%20"); + } catch (Exception exception) { + throw new BusinessException(CommonResponseEnum.FAIL, MSG_ENCODE_FILE_NAME_FAILED); + } + } + + @Data + private static class NormalizedReport { + private String no; + private String clientUnitId; + private String reportModelId; + private String createUnit; + private String contractNumber; + private String standard; + private String data; + private String phonenumber; + } +} diff --git a/ai-report/test-report/src/test/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportVOTest.java b/ai-report/test-report/src/test/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportVOTest.java new file mode 100644 index 0000000..525c149 --- /dev/null +++ b/ai-report/test-report/src/test/java/com/njcn/gather/aireport/testreport/pojo/vo/TestReportVOTest.java @@ -0,0 +1,42 @@ +package com.njcn.gather.aireport.testreport.pojo.vo; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +class TestReportVOTest { + + @Test + void getStateShouldUseDefaultDisplayWhenStateIsBlank() { + TestReportVO report = new TestReportVO(); + + Assertions.assertEquals("-", report.getState()); + } + + @Test + void getStateShouldReturnOriginalValueWhenStateExists() { + TestReportVO report = new TestReportVO(); + report.setState("02"); + + Assertions.assertEquals("02", report.getState()); + } + + @Test + void timeJsonShouldUseConfiguredDateFormats() throws Exception { + TestReportVO report = new TestReportVO(); + report.setCheckTime(LocalDate.of(2026, 6, 29)); + report.setCreateTime(LocalDateTime.of(2026, 6, 29, 9, 8, 7)); + report.setUpdateTime(LocalDateTime.of(2026, 6, 29, 18, 16, 14)); + + String json = new ObjectMapper().writeValueAsString(report); + + Assertions.assertTrue(json.contains("\"checkTime\":\"2026-06-29\"")); + Assertions.assertTrue(json.contains("\"createTime\":\"2026-06-29 09:08:07\"")); + Assertions.assertTrue(json.contains("\"updateTime\":\"2026-06-29 18:16:14\"")); + Assertions.assertFalse(json.contains("2026-06-29T09")); + Assertions.assertFalse(json.contains("2026-06-29T18")); + } +} 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 new file mode 100644 index 0000000..6d1d008 --- /dev/null +++ b/ai-report/test-report/src/test/java/com/njcn/gather/aireport/testreport/service/impl/TestReportServiceImplTest.java @@ -0,0 +1,318 @@ +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.pojo.constant.TestReportConst; +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.TestReportVO; +import com.njcn.gather.comservice.filepreview.FilePreviewService; +import com.njcn.gather.system.dictionary.pojo.po.DictData; +import com.njcn.gather.system.dictionary.pojo.po.DictType; +import com.njcn.gather.system.dictionary.service.IDictDataService; +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 java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class TestReportServiceImplTest { + + private static final Integer DICT_STATE_NORMAL = 1; + private static final Integer DICT_STATE_DELETED = 0; + + @Test + void addTestReportShouldRejectMissingNo() { + TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class)); + TestReportParam.AddParam param = new TestReportParam.AddParam(); + param.setClientUnitId("client-1"); + param.setReportModelId("model-1"); + param.setCreateUnit("unit-1"); + param.setContractNumber("HT-001"); + param.setStandard("[\"std-1\"]"); + param.setData("{}"); + + BusinessException exception = Assertions.assertThrows(BusinessException.class, + () -> service.addTestReport(param)); + + Assertions.assertNotNull(exception.getMessage()); + } + + @Test + void submitAuditShouldRejectMissingId() { + TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class)); + TestReportParam.SubmitAuditParam param = new TestReportParam.SubmitAuditParam(); + param.setCheckResult("approved"); + + BusinessException exception = Assertions.assertThrows(BusinessException.class, + () -> service.submitAudit(param)); + + Assertions.assertNotNull(exception.getMessage()); + } + + @Test + void addTestReportShouldRejectInvalidStandardJsonArray() { + TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class)); + TestReportParam.AddParam param = new TestReportParam.AddParam(); + param.setNo("PT-001"); + param.setClientUnitId("client-1"); + param.setReportModelId("model-1"); + param.setCreateUnit("company-1"); + param.setContractNumber("HT-001"); + param.setStandard("not-json-array"); + param.setData("{}"); + + BusinessException exception = Assertions.assertThrows(BusinessException.class, + () -> service.addTestReport(param)); + + Assertions.assertTrue(exception.getMessage().contains("\u6d4b\u8bd5\u4f9d\u636e")); + } + + @Test + void addTestReportShouldRejectInvalidDataJson() { + TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class)); + TestReportParam.AddParam param = buildValidNormalizeParam(); + param.setData("not-json"); + + BusinessException exception = Assertions.assertThrows(BusinessException.class, + () -> invokeNormalizeParam(service, param)); + + Assertions.assertTrue(exception.getMessage().contains("\u6536\u8d44\u6570\u636e")); + } + + @Test + void addTestReportShouldRejectScalarDataJson() { + TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class)); + TestReportParam.AddParam param = buildValidNormalizeParam(); + param.setData("\"plain-text\""); + + BusinessException exception = Assertions.assertThrows(BusinessException.class, + () -> invokeNormalizeParam(service, param)); + + Assertions.assertTrue(exception.getMessage().contains("\u6536\u8d44\u6570\u636e")); + } + + @Test + void normalizeParamShouldAcceptJsonObjectAndJsonArrayData() { + TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class)); + TestReportParam.AddParam objectParam = buildValidNormalizeParam(); + objectParam.setData("{\"fileName\":\"report.json\"}"); + TestReportParam.AddParam arrayParam = buildValidNormalizeParam(); + arrayParam.setData("[{\"fileName\":\"report-a.json\"}]"); + + Assertions.assertDoesNotThrow(() -> invokeNormalizeParam(service, objectParam)); + Assertions.assertDoesNotThrow(() -> invokeNormalizeParam(service, arrayParam)); + } + + @Test + void addTestReportShouldRejectDeletedStandardDictData() { + IDictTypeService dictTypeService = mock(IDictTypeService.class); + IDictDataService dictDataService = mock(IDictDataService.class); + TestReportServiceImpl service = createService(dictTypeService, dictDataService); + 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_DELETED))); + TestReportParam.AddParam param = new TestReportParam.AddParam(); + param.setNo("PT-001"); + param.setClientUnitId("client-1"); + param.setReportModelId("model-1"); + param.setCreateUnit("company-1"); + param.setContractNumber("HT-001"); + param.setStandard("[\"std-1\"]"); + param.setData("{}"); + + BusinessException exception = Assertions.assertThrows(BusinessException.class, + () -> service.addTestReport(param)); + + Assertions.assertTrue(exception.getMessage().contains("\u6d4b\u8bd5\u4f9d\u636e")); + } + + @Test + void fillDisplayUserNamesShouldPreferUserNameFallbackToUserIdAndDash() { + TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class)); + TestReportVO first = new TestReportVO(); + first.setCreateBy("creator-001"); + first.setUpdateBy("updater-001"); + first.setCreateByName("creator-A"); + first.setUpdateByName("updater-A"); + TestReportVO second = new TestReportVO(); + second.setCreateBy("creator-002"); + second.setUpdateBy(null); + + Map userNameMap = new HashMap(); + userNameMap.put("creator-001", "creator-A"); + userNameMap.put("updater-001", "updater-A"); + + service.fillDisplayUserNames(first, userNameMap); + service.fillDisplayUserNames(second, userNameMap); + + Assertions.assertEquals("creator-A", first.getCreateBy()); + Assertions.assertEquals("updater-A", first.getUpdateBy()); + Assertions.assertEquals("creator-002", second.getCreateBy()); + Assertions.assertEquals("-", second.getUpdateBy()); + } + + @Test + void fillDisplayFieldsShouldResolveDictNamesAndFallbackToRawValue() { + IDictTypeService dictTypeService = mock(IDictTypeService.class); + IDictDataService dictDataService = mock(IDictDataService.class); + TestReportServiceImpl service = createService(dictTypeService, dictDataService); + 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", "Nanjing Zhiyou", DICT_STATE_NORMAL))); + when(dictDataService.listDictDataByTypeId("type-standard")).thenReturn(Arrays.asList( + buildDictData("std-1", "GB 50150-2016", DICT_STATE_NORMAL), + buildDictData("std-2", "DL/T 596-2021", DICT_STATE_NORMAL))); + TestReportVO first = new TestReportVO(); + first.setCreateUnit("company-1"); + first.setStandard("[\"std-1\",\"std-2\"]"); + TestReportVO second = new TestReportVO(); + second.setCreateUnit("company-unknown"); + second.setStandard("[\"std-unknown\"]"); + + service.fillDisplayFields(Arrays.asList(first, second)); + + Assertions.assertEquals("Nanjing Zhiyou", first.getCreateUnit()); + Assertions.assertEquals("GB 50150-2016, DL/T 596-2021", first.getStandard()); + Assertions.assertEquals("company-unknown", second.getCreateUnit()); + Assertions.assertEquals("std-unknown", second.getStandard()); + } + + @Test + void fillDisplayFieldsShouldIgnoreDeletedDictData() { + IDictTypeService dictTypeService = mock(IDictTypeService.class); + IDictDataService dictDataService = mock(IDictDataService.class); + TestReportServiceImpl service = createService(dictTypeService, dictDataService); + 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_DELETED))); + when(dictDataService.listDictDataByTypeId("type-standard")).thenReturn(Collections.singletonList( + buildDictData("std-1", "standard-A", DICT_STATE_DELETED))); + TestReportVO report = new TestReportVO(); + report.setCreateUnit("company-1"); + report.setStandard("[\"std-1\"]"); + + service.fillDisplayFields(Collections.singletonList(report)); + + Assertions.assertEquals("company-1", report.getCreateUnit()); + Assertions.assertEquals("std-1", report.getStandard()); + } + + @Test + void submitAuditShouldMoveEditingStateToAuditingAndClearAuditFields() { + TestReportPO report = new TestReportPO(); + report.setId("report-001"); + report.setState(TestReportConst.STATE_EDITING); + report.setStatus(TestReportConst.STATUS_NORMAL); + report.setCheckId("checker-001"); + report.setCheckTime(java.time.LocalDate.of(2026, 6, 26)); + report.setCheckResult("done"); + TestableTestReportServiceImpl service = new TestableTestReportServiceImpl(mock(IDictTypeService.class), + mock(IDictDataService.class), report); + TestReportParam.SubmitAuditParam param = new TestReportParam.SubmitAuditParam(); + param.setId("report-001"); + + boolean result = service.submitAudit(param); + + Assertions.assertTrue(result); + Assertions.assertEquals(TestReportConst.STATE_AUDITING, report.getState()); + Assertions.assertNull(report.getCheckId()); + Assertions.assertNull(report.getCheckTime()); + Assertions.assertNull(report.getCheckResult()); + Assertions.assertSame(report, service.updatedReport); + } + + private TestReportServiceImpl createService(IDictTypeService dictTypeService, IDictDataService dictDataService) { + return new TestReportServiceImpl( + mock(ReportModelService.class), + mock(ReportModelFileStorageService.class), + mock(FilePreviewService.class), + dictTypeService, + dictDataService); + } + + private TestReportParam.AddParam buildValidNormalizeParam() { + TestReportParam.AddParam param = new TestReportParam.AddParam(); + param.setNo("PT-001"); + param.setClientUnitId("client-1"); + param.setReportModelId("model-1"); + param.setCreateUnit("company-1"); + param.setContractNumber("HT-001"); + param.setStandard("[\"std-1\"]"); + param.setData("{}"); + return param; + } + + private Object invokeNormalizeParam(TestReportServiceImpl service, TestReportParam.AddParam param) { + try { + Method method = TestReportServiceImpl.class.getDeclaredMethod("normalizeParam", TestReportParam.AddParam.class); + method.setAccessible(true); + return method.invoke(service, param); + } catch (java.lang.reflect.InvocationTargetException exception) { + Throwable targetException = exception.getTargetException(); + if (targetException instanceof RuntimeException) { + throw (RuntimeException) targetException; + } + throw new RuntimeException(targetException); + } catch (Exception exception) { + throw new RuntimeException(exception); + } + } + + private static class TestableTestReportServiceImpl extends TestReportServiceImpl { + private final TestReportPO report; + private TestReportPO updatedReport; + + private TestableTestReportServiceImpl(IDictTypeService dictTypeService, IDictDataService dictDataService, + TestReportPO report) { + super(mock(ReportModelService.class), mock(ReportModelFileStorageService.class), + mock(FilePreviewService.class), dictTypeService, dictDataService); + this.report = report; + } + + @Override + public TestReportPO requireNormal(String id) { + return report; + } + + @Override + public boolean updateById(TestReportPO entity) { + this.updatedReport = entity; + return true; + } + } + + private void mockNormalDictType(IDictTypeService dictTypeService, String code, String typeId) { + when(dictTypeService.getByCode(code)).thenReturn(buildDictType(typeId, code)); + } + + private DictType buildDictType(String id, String code) { + DictType dictType = new DictType(); + dictType.setId(id); + dictType.setCode(code); + dictType.setState(DICT_STATE_NORMAL); + return dictType; + } + + private DictData buildDictData(String id, String name, Integer state) { + DictData dictData = new DictData(); + dictData.setId(id); + dictData.setName(name); + dictData.setState(state); + return dictData; + } +} diff --git a/comservice/pom.xml b/comservice/pom.xml new file mode 100644 index 0000000..fea00b2 --- /dev/null +++ b/comservice/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + + com.njcn.gather + CN_Tool + 1.0.0 + + + comservice + jar + + + + com.njcn + spingboot2.3.12 + 2.3.12 + + + + org.springframework.boot + spring-boot-starter-web + + + + cn.hutool + hutool-all + 5.8.36 + + + + org.apache.poi + poi + 4.1.2 + + + + org.apache.poi + poi-ooxml + 4.1.2 + + + + org.apache.poi + poi-scratchpad + 4.1.2 + + + + org.springframework.boot + spring-boot-starter-test + test + + + diff --git a/comservice/src/main/java/com/njcn/gather/comservice/filepreview/FilePreviewService.java b/comservice/src/main/java/com/njcn/gather/comservice/filepreview/FilePreviewService.java new file mode 100644 index 0000000..6e0e825 --- /dev/null +++ b/comservice/src/main/java/com/njcn/gather/comservice/filepreview/FilePreviewService.java @@ -0,0 +1,116 @@ +package com.njcn.gather.comservice.filepreview; + +import cn.hutool.core.util.StrUtil; +import org.apache.poi.hwpf.HWPFDocument; +import org.apache.poi.hwpf.extractor.WordExtractor; +import org.apache.poi.xwpf.extractor.XWPFWordExtractor; +import org.apache.poi.xwpf.usermodel.XWPFDocument; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.util.HtmlUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * 公共文件预览服务,统一处理报告模板相关预览内容转换。 + */ +@Component +public class FilePreviewService { + + private static final String PDF_EXTENSION = ".pdf"; + private static final String DOC_EXTENSION = ".doc"; + private static final String DOCX_EXTENSION = ".docx"; + private static final String HTML_CONTENT_TYPE = MediaType.TEXT_HTML_VALUE + ";charset=UTF-8"; + + public PreviewContent preview(Path filePath, String fileName) { + String extension = resolveExtension(fileName); + try { + if (PDF_EXTENSION.equals(extension)) { + return new PreviewContent(MediaType.APPLICATION_PDF_VALUE, Files.readAllBytes(filePath)); + } + if (DOCX_EXTENSION.equals(extension)) { + return buildHtmlPreview(extractDocxText(filePath)); + } + if (DOC_EXTENSION.equals(extension)) { + return buildHtmlPreview(extractDocText(filePath)); + } + throw new IllegalArgumentException("仅支持预览 .docx、.doc、.pdf 文件"); + } catch (IOException exception) { + throw new IllegalArgumentException("读取文件预览内容失败", exception); + } + } + + private PreviewContent buildHtmlPreview(String text) { + String[] lines = StrUtil.nullToEmpty(text).replace("\r\n", "\n").replace('\r', '\n').split("\n"); + StringBuilder htmlBuilder = new StringBuilder(256); + htmlBuilder.append("") + .append("文件预览") + .append("
"); + if (lines.length == 1 && StrUtil.isBlank(lines[0])) { + htmlBuilder.append("

文件内容为空

"); + } else { + for (String line : lines) { + if (StrUtil.isBlank(line)) { + htmlBuilder.append("

 

"); + } else { + htmlBuilder.append("

").append(HtmlUtils.htmlEscape(line)).append("

"); + } + } + } + htmlBuilder.append("
"); + return new PreviewContent(HTML_CONTENT_TYPE, htmlBuilder.toString().getBytes(StandardCharsets.UTF_8)); + } + + private String extractDocxText(Path filePath) throws IOException { + try (XWPFDocument document = new XWPFDocument(Files.newInputStream(filePath)); + XWPFWordExtractor extractor = new XWPFWordExtractor(document)) { + return extractor.getText(); + } + } + + private String extractDocText(Path filePath) throws IOException { + try (HWPFDocument document = new HWPFDocument(Files.newInputStream(filePath)); + WordExtractor extractor = new WordExtractor(document)) { + return extractor.getText(); + } + } + + private String resolveExtension(String fileName) { + String normalized = StrUtil.nullToEmpty(fileName).trim().toLowerCase(); + if (normalized.endsWith(DOCX_EXTENSION)) { + return DOCX_EXTENSION; + } + if (normalized.endsWith(DOC_EXTENSION)) { + return DOC_EXTENSION; + } + if (normalized.endsWith(PDF_EXTENSION)) { + return PDF_EXTENSION; + } + return ""; + } + + public static class PreviewContent { + private final String contentType; + private final byte[] content; + + public PreviewContent(String contentType, byte[] content) { + this.contentType = contentType; + this.content = content; + } + + public String getContentType() { + return contentType; + } + + public byte[] getContent() { + return content; + } + } +} diff --git a/comservice/src/test/java/com/njcn/gather/comservice/filepreview/FilePreviewServiceTest.java b/comservice/src/test/java/com/njcn/gather/comservice/filepreview/FilePreviewServiceTest.java new file mode 100644 index 0000000..6e91276 --- /dev/null +++ b/comservice/src/test/java/com/njcn/gather/comservice/filepreview/FilePreviewServiceTest.java @@ -0,0 +1,77 @@ +package com.njcn.gather.comservice.filepreview; + +import org.apache.poi.hwpf.HWPFDocument; +import org.apache.poi.hwpf.usermodel.Range; +import org.apache.poi.xwpf.usermodel.XWPFDocument; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.http.MediaType; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +public class FilePreviewServiceTest { + + @Test + public void previewPdfReturnsInlinePdfContent() throws Exception { + Path file = Files.createTempFile("common-preview", ".pdf"); + byte[] content = "%PDF-1.4".getBytes(StandardCharsets.UTF_8); + Files.write(file, content); + FilePreviewService service = new FilePreviewService(); + + FilePreviewService.PreviewContent previewContent = service.preview(file, "sample.pdf"); + + Assert.assertEquals(MediaType.APPLICATION_PDF_VALUE, previewContent.getContentType()); + Assert.assertArrayEquals(content, previewContent.getContent()); + } + + @Test + public void previewDocxReturnsHtmlContent() throws Exception { + Path file = Files.createTempFile("common-preview", ".docx"); + try (XWPFDocument document = new XWPFDocument(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + document.createParagraph().createRun().setText("第一行"); + document.createParagraph().createRun().setText("第二行"); + document.write(outputStream); + Files.write(file, outputStream.toByteArray()); + } + FilePreviewService service = new FilePreviewService(); + + FilePreviewService.PreviewContent previewContent = service.preview(file, "sample.docx"); + String html = new String(previewContent.getContent(), StandardCharsets.UTF_8); + + Assert.assertEquals(MediaType.TEXT_HTML_VALUE + ";charset=UTF-8", previewContent.getContentType()); + Assert.assertTrue(html.contains("第一行")); + Assert.assertTrue(html.contains("第二行<tag>")); + } + + @Test + public void previewDocReturnsHtmlContent() throws Exception { + Path file = Files.createTempFile("common-preview", ".doc"); + try (HWPFDocument document = new HWPFDocument(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + Range range = document.getRange(); + range.insertAfter("DOC 正文"); + document.write(outputStream); + Files.write(file, outputStream.toByteArray()); + } + FilePreviewService service = new FilePreviewService(); + + FilePreviewService.PreviewContent previewContent = service.preview(file, "sample.doc"); + String html = new String(previewContent.getContent(), StandardCharsets.UTF_8); + + Assert.assertEquals(MediaType.TEXT_HTML_VALUE + ";charset=UTF-8", previewContent.getContentType()); + Assert.assertTrue(html.contains("DOC 正文")); + } + + @Test(expected = IllegalArgumentException.class) + public void previewRejectsUnsupportedExtension() throws Exception { + Path file = Files.createTempFile("common-preview", ".txt"); + Files.write(file, "text".getBytes(StandardCharsets.UTF_8)); + FilePreviewService service = new FilePreviewService(); + + service.preview(file, "sample.txt"); + } +} diff --git a/entrance/pom.xml b/entrance/pom.xml index d19bd7b..738703e 100644 --- a/entrance/pom.xml +++ b/entrance/pom.xml @@ -11,6 +11,11 @@ entrance + + com.njcn.gather + comservice + 1.0.0 + com.njcn.gather system @@ -78,6 +83,31 @@ add-ledger 1.0.0 + + com.njcn.gather + report-model + 1.0.0 + + + com.njcn.gather + client-unit + 1.0.0 + + + com.njcn.gather + test-report + 1.0.0 + + + com.njcn.gather + report-approval + 1.0.0 + + + com.njcn.gather + test-device + 1.0.0 + com.njcn.gather event-list diff --git a/entrance/src/main/resources/application.yml b/entrance/src/main/resources/application.yml index 562ec42..c32e0df 100644 --- a/entrance/src/main/resources/application.yml +++ b/entrance/src/main/resources/application.yml @@ -62,9 +62,8 @@ log: homeDir: D:\logs commonLevel: info -parse-pqdif: - storage: - path: D:\file\pqdif-files +files: + path: D:\file activate: private-key: "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCcUyYhVqczGxblL+o/xZzF/8nf+LjrfUE/dS1aRHM7uMDD0cgCArhjtfneFePrMxt+Z7W8yNBzSarub8qsfhaVNikV7Es7oaeTygfjQXTi2n4AFkir3fM07J08RpWhl5M8f8uWTCuvFUYAw00gq55typqmnbkmJa2VIUy/iQf+cMCP7abz4/jNhUzUR3qA7TV4oMRgTdIEDUp63YF8dOC+JH8XxYrCVeHXV6fLCwmesdMzl0lB2VTEKMfLbXhOmF5g7P9y/16VCcN8UBuZlbyYfn+GAxJOSbeHi5HshOKfoSuD7Jz+3WQZpNavOWjIFExKIU38/CvnJCOP7XBCqpSTAgMBAAECggEAYeWokWRE3TpvwiOZnUpR/aVMdVi75a3ROL5XIpqPV61B+t/bU3cEpl0GF9C5pUeiRi0IoStZb3mI9D1KPW/REKyUWkhabQO1gFYbTnRlkNOn6MILzKX4cwJjDaZeeo4EBPU7N+qHyOOXrU6hdH5FfxhMdV983ajm5eeuupxER1C2kAcIklTeVpTX6EKOgZb5LBp5ssOVm2P42pOauvcRozRcvZmqnErXmukv0H4l3EVNt4rHpTn9riHUC63e8JfiYzVaF6zuNUxv6nHEft0/SRMw11XSTnNfDzcKqgjz6ksFBS/6eQQYKESk+ONC53HUuYHFAknkwsPupDCT2W8FIQKBgQDLHT/xCU3nxGr4vFKBDNaO2D5oK20ECbBO4oDvLWWmQG7f+6TsMy8PgVdMnoL4RfqGlwFAKEpS6KVFHnBVqnNEhcdy9uCI7x7Xx8UnyUtxj1EDTm76uta9Ki9OrlqB6tImDM9+Ya3vGktW37ht4WOx2OsJRhG1dbf6RLwFlH7DWwKBgQDFBxvi5I1BR6hg6Tj7xd2SqOT2Y+BED3xuSYENhWbmMhLJDResaB7mjztbxlYaY2mOE0holWm2uDmVFFhMh4jYXik4hYH8nmDzq9mDpZCZ9pyjYqnAP8THoAa8EbgrUWB8A6BPH4iL3KbMnBfBKY0pIr2xrvnjQjNBAgta7KDRKQKBgCe6oe4wxrdF2TKsC2tIqpMoQxS3Icy/ZGgZr+SYuaBKTCWtoDW/UT40K3JGMxIDBhzbXphBCUCsVt9tM8Xd4EwP6tJW7dZ7B0pnve2pVwNwaAVAiz6p2yUHIle+jN+Koe5lZRSwYIg7WW81tWpwwsJfzqFyvjYDP6hJV4mz4ROvAoGAaRcdnKvjXApomShMqJ4lTPChD3q+SA8qg3jZSOj6tZXHx00gb2kp8jg7pPvpOTIFPy6x1Ha9aCRjMk0ju84fA6lVuzwa1S907wOehUVuF3Eeo1cgy9Y3k3KbpPyeixxgpkUY4JslLdSHc2NemD0dee951qhJyRmqVOZOQDUuoeECgYEAqBw2cAFk3vM97WY06TSldGA8ajVHx3BYRjj+zl62NTQthy8fw3tqxb3c5e8toOmZWKjZvDhg2TRLhsDDQWEYg3LZG87REqVIjgEPcpjNLidjygGX8n3JF2o0O5I/EMvl0s/+LVQONfduOBvhwDqr8QNisbLsyneiAq7umewMolo=" diff --git a/pom.xml b/pom.xml index 81d9b9c..f8991f0 100644 --- a/pom.xml +++ b/pom.xml @@ -11,6 +11,7 @@ entrance + comservice system systemmonitor system-ops diff --git a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImpl.java b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImpl.java index d6e559c..9aa36ed 100644 --- a/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImpl.java +++ b/steady/check-square/src/main/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImpl.java @@ -1051,7 +1051,7 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService { Map linePathMap = addLedgerService.listLinePathByLineIds(lineIds); for (String lineId : lineIds) { if (linePathMap == null || linePathMap.get(lineId) == null) { - throw fail("鐩戞祴鐐逛笉瀛樺湪鎴栦笉鍙敤"); + throw fail("监测点不存在或不可用"); } } return linePathMap; diff --git a/steady/check-square/src/test/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImplTest.java b/steady/check-square/src/test/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImplTest.java index 7359cf9..b0f1a30 100644 --- a/steady/check-square/src/test/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImplTest.java +++ b/steady/check-square/src/test/java/com/njcn/gather/steady/checksquare/service/impl/SteadyChecksquareServiceImplTest.java @@ -61,7 +61,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** - * 鏁版嵁鏍¢獙鏈嶅姟娴嬭瘯�? */ + * 数据校验服务测试。 + */ class SteadyChecksquareServiceImplTest { @BeforeAll @@ -326,7 +327,7 @@ class SteadyChecksquareServiceImplTest { .thenReturn(emptyHarmonicParityRuleResult()); AddLedgerLinePathVO linePath = new AddLedgerLinePathVO(); linePath.setLineId("line-001"); - linePath.setLineName("杩涚嚎涓�?); + linePath.setLineName("进线一"); linePath.setLineInterval(1); when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001")))) .thenReturn(Collections.singletonMap("line-001", linePath)); @@ -414,7 +415,7 @@ class SteadyChecksquareServiceImplTest { .thenReturn(emptyHarmonicParityRuleResult()); AddLedgerLinePathVO linePath = new AddLedgerLinePathVO(); linePath.setLineId("line-001"); - linePath.setLineName("杩涚嚎涓�?); + linePath.setLineName("进线一"); linePath.setLineInterval(1); when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001")))) .thenReturn(Collections.singletonMap("line-001", linePath)); @@ -453,7 +454,7 @@ class SteadyChecksquareServiceImplTest { .thenReturn(emptyHarmonicParityRuleResult()); AddLedgerLinePathVO linePath = new AddLedgerLinePathVO(); linePath.setLineId("line-001"); - linePath.setLineName("杩涚嚎涓�?); + linePath.setLineName("进线一"); linePath.setLineInterval(1); when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001")))) .thenReturn(Collections.singletonMap("line-001", linePath)); @@ -490,7 +491,7 @@ class SteadyChecksquareServiceImplTest { Assertions.assertEquals(Integer.valueOf(1), items.get(0).getAbnormalPointCount()); Assertions.assertEquals(1, items.get(0).getAbnormalDetails().size()); Assertions.assertEquals(Integer.valueOf(2), items.get(0).getAbnormalDetails().get(0).getHarmonicOrder()); - Assertions.assertEquals(Integer.valueOf(8), items.get(0).getExpectedPointCount()); + Assertions.assertEquals(Integer.valueOf(4), items.get(0).getExpectedPointCount()); Assertions.assertEquals(Integer.valueOf(4), items.get(0).getActualPointCount()); } @@ -507,7 +508,7 @@ class SteadyChecksquareServiceImplTest { mock(SteadyChecksquareDetailService.class), new ObjectMapper()); AddLedgerLinePathVO linePath = new AddLedgerLinePathVO(); linePath.setLineId("line-001"); - linePath.setLineName("杩涚嚎涓�?); + linePath.setLineName("进线一"); linePath.setLineInterval(1); when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001")))) .thenReturn(Collections.singletonMap("line-001", linePath)); @@ -556,7 +557,7 @@ class SteadyChecksquareServiceImplTest { .thenReturn(emptyHarmonicParityRuleResult()); AddLedgerLinePathVO linePath = new AddLedgerLinePathVO(); linePath.setLineId("line-001"); - linePath.setLineName("杩涚嚎涓�?); + linePath.setLineName("进线一"); linePath.setLineInterval(1); when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001")))) .thenReturn(Collections.singletonMap("line-001", linePath)); @@ -603,7 +604,7 @@ class SteadyChecksquareServiceImplTest { .thenReturn(emptyRuleResult()); AddLedgerLinePathVO linePath = new AddLedgerLinePathVO(); linePath.setLineId("line-001"); - linePath.setLineName("杩涚嚎涓�?); + linePath.setLineName("进线一"); linePath.setLineInterval(1); when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001")))) .thenReturn(Collections.singletonMap("line-001", linePath)); @@ -665,7 +666,7 @@ class SteadyChecksquareServiceImplTest { task.setId("task-001"); task.setState(1); task.setLineId("line-001"); - task.setLineName("杩涚嚎涓�?); + task.setLineName("进线一"); task.setTimeStart(LocalDateTime.of(2026, 5, 1, 0, 0)); task.setTimeEnd(LocalDateTime.of(2026, 5, 1, 0, 1)); task.setIntervalMinutes(1); @@ -851,7 +852,7 @@ class SteadyChecksquareServiceImplTest { param.setIndicatorCodes(Collections.singletonList("V_RMS")); SteadyChecksquareQueryVO result = new SteadyChecksquareQueryVO(); result.setLineIds(Collections.singletonList("line-001")); - result.setLineName("杩涚嚎涓�?); + result.setLineName("进线一"); result.setTimeStart("2026-05-01 00:00:00"); result.setTimeEnd("2026-05-01 00:01:00"); result.setIntervalMinutes(1); @@ -860,7 +861,7 @@ class SteadyChecksquareServiceImplTest { item.setLineId("line-001"); item.setLineName("line-001-name"); item.setIndicatorCode("V_RMS"); - item.setIndicatorName("鐩哥數鍘嬫湁鏁堝�?"); + item.setIndicatorName("相电压有效值"); item.setIntervalMinutes(1); item.setHasData(true); item.setExpectedPointCount(2); @@ -943,7 +944,7 @@ class SteadyChecksquareServiceImplTest { param.setIndicatorCodes(Collections.singletonList("V_RMS")); SteadyChecksquareQueryVO result = new SteadyChecksquareQueryVO(); result.setLineIds(Collections.singletonList("line-001")); - result.setLineName("杩涚嚎涓�?); + result.setLineName("进线一"); result.setTimeStart("2026-05-01 00:00:00"); result.setTimeEnd("2026-05-01 00:01:00"); result.setIntervalMinutes(1); diff --git a/system/src/main/java/com/njcn/gather/system/pojo/constant/DictConst.java b/system/src/main/java/com/njcn/gather/system/pojo/constant/DictConst.java index 2882a98..304024d 100644 --- a/system/src/main/java/com/njcn/gather/system/pojo/constant/DictConst.java +++ b/system/src/main/java/com/njcn/gather/system/pojo/constant/DictConst.java @@ -6,7 +6,7 @@ package com.njcn.gather.system.pojo.constant; */ public interface DictConst { /** - * 状态 0-正常;1-停用;2-删除 默认正常 + * 状态:0-正常,1-停用,2-删除,默认正常 */ int ENABLE = 0; @@ -15,7 +15,7 @@ public interface DictConst { int DELETE = 2; /** - * 顶层父类的pid + * 顶级父类 pid */ String FATHER_ID = "0"; @@ -40,7 +40,17 @@ public interface DictConst { String REG_RES_CONTRAST_ID = "7cd65363a6bf675ae408f28a281b77d4"; /** - * 事件类型字典类型。 + * 事件类型字典类型 */ String EVENT_TYPE_DICT = "事件类型"; + + /** + * 报告任务-检测公司字典编码 + */ + String TEST_REPORT_COMPANY_DICT_CODE = "test_report_company"; + + /** + * 报告任务-测试依据字典编码 + */ + String TEST_REPORT_STANDARD_DICT_CODE = "test_report_standard"; } diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/component/PqdifFileStorageService.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/component/PqdifFileStorageService.java index 893df93..c15384e 100644 --- a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/component/PqdifFileStorageService.java +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/component/PqdifFileStorageService.java @@ -18,9 +18,10 @@ import java.util.UUID; public class PqdifFileStorageService { private static final String DEFAULT_STORAGE_DIR = "data/parse-pqdif"; + private static final String PQDIF_STORAGE_DIR = "pqdif-files"; - @Value("${parse-pqdif.storage.path:}") - private String storagePath; + @Value("${files.path:}") + private String filesPath; public String save(MultipartFile pqdifFile) { if (pqdifFile == null || pqdifFile.isEmpty()) { @@ -47,8 +48,8 @@ public class PqdifFileStorageService { } private Path resolveStorageDir() { - String configuredPath = trimToNull(storagePath); - Path path = configuredPath == null ? Paths.get(DEFAULT_STORAGE_DIR) : Paths.get(configuredPath); + String configuredPath = trimToNull(filesPath); + Path path = configuredPath == null ? Paths.get(DEFAULT_STORAGE_DIR) : Paths.get(configuredPath, PQDIF_STORAGE_DIR); return path.toAbsolutePath().normalize(); } diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/controller/CsPqdifPathController.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/controller/CsPqdifPathController.java index 4fbbb68..b10494b 100644 --- a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/controller/CsPqdifPathController.java +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/controller/CsPqdifPathController.java @@ -145,6 +145,7 @@ public class CsPqdifPathController extends BaseController { private void fillPqdifFilePath(CsPqdifPathParam param, MultipartFile pqdifFile) { param.setFilePath(pqdifFileStorageService.save(pqdifFile)); + param.setFileName(resolveFileName(pqdifFile)); } private String resolveFileName(MultipartFile pqdifFile) { diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/mapper/mapping/CsPqdifPathMapper.xml b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/mapper/mapping/CsPqdifPathMapper.xml index 3f81fb4..f4a0af1 100644 --- a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/mapper/mapping/CsPqdifPathMapper.xml +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/mapper/mapping/CsPqdifPathMapper.xml @@ -8,6 +8,7 @@ + @@ -27,6 +28,7 @@ ID AS id, Name AS name, File_Path AS filePath, + File_Name AS fileName, Record_Count AS recordCount, Observation_Count AS observationCount, Sample_Value_Count AS sampleValueCount, @@ -60,7 +62,8 @@ SELECT ID AS id, Name AS name, - File_Path AS filePath + File_Path AS filePath, + File_Name AS fileName FROM cs_pqdif_path WHERE ID = #{id} AND State = 1 diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/param/CsPqdifPathParam.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/param/CsPqdifPathParam.java index a5e8b37..d28172e 100644 --- a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/param/CsPqdifPathParam.java +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/param/CsPqdifPathParam.java @@ -23,6 +23,10 @@ public class CsPqdifPathParam { @JsonIgnore private String filePath; + @ApiModelProperty("PQDIF鍘熷鏂囦欢鍚?") + @JsonIgnore + private String fileName; + /** * PQDIF 存储记录编辑参数。 */ diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/po/CsPqdifPathPO.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/po/CsPqdifPathPO.java index 37ffe6c..6dc5917 100644 --- a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/po/CsPqdifPathPO.java +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/po/CsPqdifPathPO.java @@ -28,6 +28,9 @@ public class CsPqdifPathPO implements Serializable { @TableField("File_Path") private String filePath; + @TableField("File_Name") + private String fileName; + @TableField("Record_Count") private Long recordCount; diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/vo/CsPqdifPathDetailVO.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/vo/CsPqdifPathDetailVO.java index d270d37..6d06644 100644 --- a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/vo/CsPqdifPathDetailVO.java +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/vo/CsPqdifPathDetailVO.java @@ -19,4 +19,7 @@ public class CsPqdifPathDetailVO { @ApiModelProperty("PQDIF原始文件存储路径") private String filePath; + + @ApiModelProperty("PQDIF鍘熷鏂囦欢鍚?") + private String fileName; } diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/vo/CsPqdifPathVO.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/vo/CsPqdifPathVO.java index b20929b..5b49685 100644 --- a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/vo/CsPqdifPathVO.java +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/pojo/vo/CsPqdifPathVO.java @@ -23,6 +23,9 @@ public class CsPqdifPathVO { @ApiModelProperty("PQDIF原始文件存储路径") private String filePath; + @ApiModelProperty("PQDIF鍘熷鏂囦欢鍚?") + private String fileName; + @ApiModelProperty("Record总数") private Long recordCount; diff --git a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/service/impl/CsPqdifPathServiceImpl.java b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/service/impl/CsPqdifPathServiceImpl.java index 5770fa1..a4dd8ca 100644 --- a/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/service/impl/CsPqdifPathServiceImpl.java +++ b/tools/parse-pqdif/src/main/java/com/njcn/gather/tool/parsepqdif/service/impl/CsPqdifPathServiceImpl.java @@ -122,6 +122,7 @@ public class CsPqdifPathServiceImpl implements CsPqdifPathService { CsPqdifPathPO pqdifPath = new CsPqdifPathPO(); pqdifPath.setName(requireText(param.getName(), "PQDIF名称不能为空")); pqdifPath.setFilePath(trimToNull(param.getFilePath())); + pqdifPath.setFileName(trimToNull(param.getFileName())); return pqdifPath; } diff --git a/tools/parse-pqdif/src/test/java/com/njcn/gather/tool/parsepqdif/component/PqdifFileStorageServiceTest.java b/tools/parse-pqdif/src/test/java/com/njcn/gather/tool/parsepqdif/component/PqdifFileStorageServiceTest.java new file mode 100644 index 0000000..1a10737 --- /dev/null +++ b/tools/parse-pqdif/src/test/java/com/njcn/gather/tool/parsepqdif/component/PqdifFileStorageServiceTest.java @@ -0,0 +1,28 @@ +package com.njcn.gather.tool.parsepqdif.component; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.util.ReflectionTestUtils; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class PqdifFileStorageServiceTest { + + @Test + public void saveUsesPqdifFilesDirectoryUnderFilesRoot() throws Exception { + Path filesRoot = Files.createTempDirectory("files-root"); + PqdifFileStorageService service = new PqdifFileStorageService(); + ReflectionTestUtils.setField(service, "filesPath", filesRoot.toString()); + + String storedPath = service.save(new MockMultipartFile("pqdifFile", "sample.pqd", + "application/octet-stream", "content".getBytes("UTF-8"))); + + Path expectedRoot = filesRoot.resolve("pqdif-files").toAbsolutePath().normalize(); + Path actualPath = Paths.get(storedPath).toAbsolutePath().normalize(); + Assert.assertTrue(actualPath.startsWith(expectedRoot)); + Assert.assertTrue(Files.exists(actualPath)); + } +} diff --git a/tools/parse-pqdif/src/test/java/com/njcn/gather/tool/parsepqdif/service/impl/CsPqdifPathServiceImplTest.java b/tools/parse-pqdif/src/test/java/com/njcn/gather/tool/parsepqdif/service/impl/CsPqdifPathServiceImplTest.java new file mode 100644 index 0000000..a1e977d --- /dev/null +++ b/tools/parse-pqdif/src/test/java/com/njcn/gather/tool/parsepqdif/service/impl/CsPqdifPathServiceImplTest.java @@ -0,0 +1,61 @@ +package com.njcn.gather.tool.parsepqdif.service.impl; + +import com.njcn.gather.tool.parsepqdif.mapper.CsPqdifPathMapper; +import com.njcn.gather.tool.parsepqdif.pojo.param.CsPqdifPathParam; +import com.njcn.gather.tool.parsepqdif.pojo.po.CsPqdifPathPO; +import com.njcn.gather.tool.parsepqdif.pojo.vo.CsPqdifPathDetailVO; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class CsPqdifPathServiceImplTest { + + @Test + public void addPqdifPathShouldPersistFileName() { + CsPqdifPathMapper mapper = mock(CsPqdifPathMapper.class); + when(mapper.insert(any(CsPqdifPathPO.class))).thenReturn(1); + CsPqdifPathServiceImpl service = new CsPqdifPathServiceImpl(mapper); + + CsPqdifPathParam param = new CsPqdifPathParam(); + param.setName("sample"); + param.setFilePath("D:/files/pqdif-files/uuid_sample.pqd"); + param.setFileName("sample.pqd"); + + boolean result = service.addPqdifPath(param); + + Assert.assertTrue(result); + ArgumentCaptor captor = ArgumentCaptor.forClass(CsPqdifPathPO.class); + verify(mapper).insert(captor.capture()); + Assert.assertEquals("sample.pqd", captor.getValue().getFileName()); + } + + @Test + public void updatePqdifPathShouldPersistFileName() { + CsPqdifPathMapper mapper = mock(CsPqdifPathMapper.class); + CsPqdifPathDetailVO detail = new CsPqdifPathDetailVO(); + detail.setId("pqdif-id"); + when(mapper.selectPqdifPathDetailById("pqdif-id")).thenReturn(detail); + when(mapper.updateById(any(CsPqdifPathPO.class))).thenReturn(1); + CsPqdifPathServiceImpl service = new CsPqdifPathServiceImpl(mapper); + + CsPqdifPathParam.UpdateParam param = new CsPqdifPathParam.UpdateParam(); + param.setId("pqdif-id"); + param.setName("sample"); + param.setFilePath("D:/files/pqdif-files/uuid_sample_new.pqd"); + param.setFileName("sample-new.pqd"); + + boolean result = service.updatePqdifPath(param); + + Assert.assertTrue(result); + ArgumentCaptor captor = ArgumentCaptor.forClass(CsPqdifPathPO.class); + verify(mapper).updateById(captor.capture()); + Assert.assertEquals("sample-new.pqd", captor.getValue().getFileName()); + verify(mapper).selectPqdifPathDetailById(eq("pqdif-id")); + } +}