28 Commits

Author SHA1 Message Date
c1ad7feec2 feat(ai-report): 新增委托单位管理和报告审批功能
- 新增委托单位管理模块,支持增删改查、导入导出功能
- 实现委托单位 Excel 模板下载和数据导入功能
- 添加报告审批抽象处理器,支持审批流程日志记录
- 统一文件存储路径配置,将各模块独立路径改为统一根目录
- 新增 PQDIF 文件名存储字段,完善文件管理功能
- 修复代码中的乱码问题和错误提示信息
- 优化系统常量定义和字典配置管理
2026-07-01 13:15:57 +08:00
0ea686d3cb feat(icd): 实现标准ICD新增时自动设置自身为参照源功能
- 在添加标准ICD路径时自动填充referenceIcdId字段为自身ID
- 添加fillSelfReferenceForStandardIcd方法处理标准类型ICD的自参照逻辑
- 增加isStandardType方法判断ICD类型是否为标准类型
- 添加单元测试验证手动录入标准ICD和上游解析标准ICD的自参照功能
- 修改应用配置文件中的文件存储路径
- 新增ai-report模块及其子模块的Maven配置
- 移除数据校验服务中已废弃的单个监测点ID字段,统一使用监测点ID列表
- 更新相关实体类、参数类和返回类的数据结构定义
- 修正测试代码中的字段引用和字符编码问题
2026-06-25 07:59:08 +08:00
周宇 蔡
646b5f3283 fix(mapping): 修复配置类型判断逻辑
- 将SDI描述中的AB、BC、CA检测改为fullKey中线电压总有效值检测
- 确保正确的路径前缀添加逻辑
- 修复了条件判断的准确性问题
2026-06-23 16:44:32 +08:00
周宇 蔡
e92ed85647 Merge branch 'dev-czy' 2026-06-23 10:43:01 +08:00
周宇 蔡
5bd709560a refactor(icd-mapping): 重构ICD到XML转换服务以支持配置类型参数
- 移除generateFromIcd方法中的索引选择验证逻辑
- 在fillXmlContent方法中添加configType参数传递
- 在JsonToXmlConversionService中添加configType参数支持
- 删除convertFromJson方法的旧实现
- 更新buildXmlContentFromJson方法签名以包含configType参数
- 在applyRulesFromMapping和extractMetricsFromMapping方法中传递configType参数
- 添加针对PMS3.0配置类型的特殊处理逻辑
- 修复模板中线电压基波有效值描述文本错误
2026-06-23 10:42:01 +08:00
2b56da2134 feat(parse-pqdif): 重构PQDIF文件存储及解析功能
- 引入PqdifFileStorageService组件实现PQDIF原始文件本地存储
- 修改application.yml添加parse-pqdif.storage.path配置项
- 更新CsPqdifPath相关实体类将pqdifContent改为filePath存储路径
- 重构CsPqdifPathController中的文件上传处理逻辑
- 新增PqdifRecognizeResultVO支持PQDIF二次解析结果结构
- 实现ParsePqdifService接口支持可识别数据解析功能
- 添加PqdifSecondParseComponent进行底层解析结果转换
- 新增桌面PQDIF文件测试程序便于调试验证
- 为CsIcdPath增加referenceIcdId字段及相应测试用例
2026-06-23 08:23:15 +08:00
97b1334714 feat(icd): 完善ICD映射管理功能
- 在AuthGlobalFilter中添加稳态检验相关接口的免认证路径
- 修改CsDevTypeMapper.xml移除icdPath字段返回避免数据冗余
- 在CsIcdPathController中新增查询参照ICD列表和ICD校验详情接口
- 更新CsIcdPathMapper添加selectReferenceIcdPathList等方法实现
- 移除CsIcdPath相关实体和参数中的path字段简化数据结构
- 扩展ICD类型定义支持手动录入和上游解析的标准/非标准分类
- 重构激活标准ICD逻辑支持不同类型间的正确转换
- 新增ICD一致性校验排除规则跳过特定描述的DOI项检查
- 优化报告映射规则应用逻辑提升校验准确性
- 添加去除重复DOI项功能确保数据唯一性
2026-06-18 16:33:40 +08:00
7fb4c8e78a feat(mms-mapping): 添加ICD校验结果保存功能支持文件上传
- 在CsIcdPathController中添加POST接口支持multipart表单上传ICD文件
- 实现saveIcdCheckResultWithFile方法处理文件上传和校验结果保存
- 添加fillIcdFile方法处理ICD文件内容填充到参数对象
- 在CsIcdPathPO中将Icd_Content字段重命名为Icd并使用JsonNodeTypeHandler处理JSON转换
- 更新resultMap配置使用新的字段映射关系
- 修改ICD一致性校验服务的日志记录和校验逻辑
- 移除自动修正映射差异的功能,只保留一致性检查
- 优化测试用例验证ICD校验结果保存和文件上传功能
2026-06-16 13:25:12 +08:00
fd6e5097d7 feat(mms-mapping): 添加ICD一致性校验功能并重构设备类型管理
- 在MappingController中新增ICD一致性校验接口checkIcdJsonConsistency
- 添加IcdConsistencyCheckService服务实现ICD映射JSON一致性校验逻辑
- 添加IcdConsistencyCheckRequest和IcdConsistencyCheckResponse相关数据传输对象
- 在CsIcdPathPO中新增icdContent字段存储ICD内容字节数组
- 在CsIcdPathMapper中新增selectIcdPathList方法支持关键词搜索
- 移除设备类型相关的控制器、服务接口及实现类(MmsDeviceTypeController等)
- 更新.gitignore文件排除特定jar包路径
- 在pom.xml中添加device-types模块依赖和JNA库依赖
- 更新README.md文档添加device-types模块说明
- 重命名steady-DataView为steady-dataView模块名统一格式
2026-06-15 08:38:19 +08:00
周宇 蔡
1edee2bf12 fix(mms-mapping): 修复XML资源配置加载和JSON映射提取问题
- 删除冗余注释,优化代码可读性
- 增强extractMappingJson方法中的类型安全处理,支持字符串和对象类型的mappingJson字段
- 添加对象类型转换逻辑,确保返回正确格式的JSON字符串
2026-06-12 13:55:48 +08:00
b7b18dc325 调整parse函数解析失败的异常处理。现在不会直接抛出异常了。 2026-06-12 10:58:49 +08:00
a5a09022f2 添加了pqdif解析的基础功能,开放了一个基础解析接口。 2026-06-12 10:42:18 +08:00
4aca8ca2c4 Merge remote-tracking branch 'origin/main' 2026-06-12 10:41:11 +08:00
362bbf536f 添加了pqdif解析的基础功能,开放了一个基础解析接口。 2026-06-12 10:40:59 +08:00
周宇 蔡
89ec9e1fa3 Merge branch 'dev-czy' 2026-06-12 09:51:41 +08:00
周宇 蔡
1343d235c8 refactor(mms-mapping): 更新江苏配置模板并增强JSON到XML转换服务
- 将JiangSu_Config1.xml和JiangSu_Config2.xml中的Value标签注释格式统一
- 在JsonToXmlConversionService中改进规则匹配统计信息显示
- 添加XML模板中Value标签数量统计功能
- 实现匹配成功和未匹配Value标签的计数统计
- 增加未匹配标签的详细描述列表输出
- 新增countXmlValueTags方法用于统计XML中Value标签总数
- 新增countMatchedValueTags方法用于统计已匹配的Value标签数
- 新增findUnmatchedValueTags方法用于查找未匹配的Value标签描述
2026-06-12 09:50:51 +08:00
周宇 蔡
0cecf2d7a2 Merge branch 'dev-czy' 2026-06-12 08:53:25 +08:00
周宇 蔡
c0cf4de315 feat(mapping): 添加配置文件类型参数支持
- 在 generateXmlFromJson 方法中添加 configType 参数以支持不同配置文件
- 新增 loadXmlResources(Integer configType) 方法用于加载指定类型的XML资源
- 更新 JsonToXmlRequest 类添加 configType 属性定义
- 修改 MappingController 中的 getXmlFromJson 方法以传递配置类型参数
- 扩展 RuleBasedXmlMappingService 的 loadDefaultXmlFile 方法支持配置类型选择
- 在调试类 JsonToXmlDebugRunner 中设置默认使用配置类型2进行测试
2026-06-12 08:52:36 +08:00
212b69060c refactor(steady): 重构数据校验功能并新增PQDIF解析预留模块
- 将数据校验中的缺失率相关字段替换为数据完整性字段
- 新增数据校验任务删除功能及相应测试
- 在tools模块中添加parse-pqdif子模块作为PQDIF文件解析预留
- 更新README文档以反映新的模块结构和依赖关系
- 优化数据校验统计汇总逻辑和测试覆盖
- 在entrance模块中集成parse-pqdif依赖
- 重构数据校验服务层实现和数据对象映射
2026-06-12 08:41:11 +08:00
周宇 蔡
f7154db93d Merge branch 'dev-new' 2026-06-11 18:13:29 +08:00
周宇 蔡
557022d346 Merge branch 'main' of http://www.pqmcc.com:3000/ClientApps/CN_Tool 2026-06-11 18:09:02 +08:00
周宇 蔡
e43ab264e0 feat(mms-mapping): 添加江苏配置模板支持谐波数据
- 新增 JiangSu_Config1.xml 配置文件
- 配置历史稳态数据类型包括短时闪变、长时闪变、暂态数据
- 配置实时稳态数据类型支持电压、电流、功率等参数
- 设置谐波数据解析规则和通配符配置
- 配置电压电流功率等电气量的95值、平均值、最大值、最小值统计
- 添加谐波含有率、相角等谐波类数据配置
- 配置暂态事件解析规则和录波文件时间格式
- 设置 Kafka 消息发送相关参数和数据系数转换规则
2026-06-11 18:04:25 +08:00
1c979e248a feat(steady-checksquare): 新增数据校验功能模块
- 添加数据校验历史记录查询接口
- 实现数据校验任务创建功能
- 新增数据校验详情查询接口
- 添加谐波奇偶关系异常检测规则
- 实现数据校验明细数据结构
- 添加数据校验编号生成工具
- 优化InfluxDB查询组件并增加缓存机制
- 添加数据校验常量定义
- 实现数据校验值生成器中的派生字段处理逻辑
- 新增数据校验相关的VO、PO、DTO类
- 添加数据校验组件单元测试
2026-06-11 11:09:12 +08:00
36962221f5 feat(dbms): 增加数据库备份任务停止重启功能和MySQL支持
- 添加了备份任务停止和重启接口及实现
- 实现了对MySQL数据库的支持,包括数据库名配置
- 重构了数据库连接和备份操作的SPI架构
- 优化了备份文件删除逻辑,支持目录递归删除
- 增加了连接名称唯一性校验
- 完善了备份任务状态管理和错误处理机制
- 更新了数据库连接参数验证逻辑
2026-06-09 13:14:43 +08:00
周宇 蔡
24bdaa1ae9 Merge branch 'dev-czy'
# Conflicts:
#	tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/IcdToXmlResponseConverter.java
#	tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/JsonToXmlConversionService.java
#	tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/RuleBasedXmlMappingService.java
#	tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/bo/IcdToXmlGenerateResult.java
#	tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/IcdToXmlResponse.java
#	tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/service/impl/IcdToXmlTaskAppService.java
2026-06-01 08:54:13 +08:00
周宇 蔡
58ca8b0c23 refactor(mms-mapping): 重构ICD到XML转换服务优化代码结构
- 移除IcdToXmlGenerateResult中未使用的iedName、ldInst、indexAnalysis、savedPath字段
- 注释掉相应的getter/setter方法减少代码冗余
- 重构IcdToXmlTaskAppService中generateFromIcd方法的业务逻辑
- 优化JsonToXmlConversionService的转换流程提高性能
- 添加详细的中文注释说明各个方法的功能和实现逻辑
- 调整规则匹配和XML生成的核心算法提升匹配准确性
- 修改未匹配规则的错误提示信息增加详细指标信息
2026-05-27 08:45:36 +08:00
周宇 蔡
8a92ff3be0 feat(mapping): 优化ICD到XML转换服务支持未匹配规则详情
- 实现convertFromJsonWithResult方法返回完整的转换结果
- 添加未匹配规则详细信息到生成结果的问题列表中
- 新增ConversionResult、RuleMatchingResult和UnmatchedRuleDetail数据结构
- 扩展应用规则逻辑以跟踪和报告未匹配的规则变体
- 重构buildXmlFromMapping方法以支持结果详情返回
- 更新控制台日志输出以显示规则匹配统计信息
2026-05-06 15:43:19 +08:00
周宇 蔡
ddadf26837 feat(mms-mapping): test中添加XML字符串返回功能并优化JSON转XML性能测试
- 在IcdToXmlGenerateResult和IcdToXmlResponse中新增mappingXml字段存储生成的XML字符串
- 修改IcdToXmlResponseConverter将XML内容从savedPath改为mappingXml字段
- 更新IcdToXmlTaskAppService将XML转换结果从保存路径改为直接返回XML内容
- 重构JsonToXmlConversionService移除临时文件创建,直接返回XML字符串
- 在JsonToXmlConversionService中添加性能监控日志输出
- 新增JsonToXmlDebugRunner用于本地调试JSON转XML功能
2026-05-06 14:31:18 +08:00
288 changed files with 21004 additions and 1351 deletions

3
.gitignore vendored
View File

@@ -5,6 +5,8 @@
target/
logs/
docs/
.codex-tmp/
.docs/
# Log file
*.log
@@ -21,6 +23,7 @@ docs/
*.ear
*.tar.gz
*.rar
!tools/parse-pqdif/lib/pqdif-native-basic-bridge-1.0.0-jar-with-dependencies.jar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*

View File

@@ -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`

View File

@@ -34,7 +34,9 @@ CN_Tool 是一个基于 Spring Boot 的多模块后端聚合工程,当前仓
- `activate-tool`
- `add-data`
- `add-ledger`
- `device-types`
- `mms-mapping`
- `parse-pqdif`
- `wave-tool`
## 启动入口
@@ -43,7 +45,7 @@ CN_Tool 是一个基于 Spring Boot 的多模块后端聚合工程,当前仓
- `entrance/src/main/java/com/njcn/gather/EntranceApplication.java`
`entrance` 模块聚合了 `system``disk-monitor``dbms``deploy``user``detection``activate-tool``add-data``add-ledger``wave-tool``mms-mapping`,是当前运行时主入口。
`entrance` 模块聚合了 `system``disk-monitor``dbms``deploy``user``detection``activate-tool``add-data``add-ledger``wave-tool``device-types``mms-mapping``parse-pqdif`,是当前运行时主入口。
## 技术基线
@@ -98,8 +100,12 @@ P0 已补齐基线文档,建议按以下顺序阅读:
- 当前提供电能质量 13 张表批量补数、任务状态查询和模板规则查询能力
- `tools/add-ledger`
- 当前为数据台账工具预留空模块
- `tools/device-types`
- 负责设备类型维护、ICD 校验结果保存和 PQDIF 校验预留入口
- `tools/mms-mapping`
- 负责 ICD 文件解析与 MMS 映射数据生成能力
- `tools/parse-pqdif`
- 当前为 PQDIF 文件解析能力预留空方法骨架
- `tools/wave-tool`
- 负责波形文本解析与查看数据组装能力

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.njcn.gather</groupId>
<artifactId>ai-report</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>client-unit</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>njcn-common</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>mybatis-plus</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>spingboot2.3.12</artifactId>
<version>2.3.12</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>system</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -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<String, String> resolveUserNames(Collection<String> userIds) {
if (userIds == null || userIds.isEmpty()) {
return Collections.emptyMap();
}
Set<String> validIds = userIds.stream()
.filter(StrUtil::isNotBlank)
.collect(Collectors.toCollection(LinkedHashSet::new));
if (validIds.isEmpty()) {
return Collections.emptyMap();
}
List<ClientUnitUserNameVO> 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));
}
}

View File

@@ -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<Page<ClientUnitVO>> 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<Boolean> 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<Boolean> 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<ClientUnitVO> 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<Boolean> delete(@RequestBody List<String> 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<ClientUnitImportResultVO> importExcel(@RequestParam("file") MultipartFile file) {
String methodDescribe = getMethodDescribe("importExcel");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
clientUnitService.importClientUnits(file), methodDescribe);
}
}

View File

@@ -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<ClientUnitPO> {
@Select({
"<script>",
"select id, name from sys_user where id in",
"<foreach collection='ids' item='id' open='(' separator=',' close=')'>",
"#{id}",
"</foreach>",
"</script>"
})
List<ClientUnitUserNameVO> selectUserNamesByIds(@Param("ids") Collection<String> ids);
}

View File

@@ -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() {
}
}

View File

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

View File

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

View File

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

View File

@@ -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<String> failReasons = new ArrayList<String>();
}

View File

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

View File

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

View File

@@ -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<ClientUnitPO> {
Page<ClientUnitVO> listClientUnits(ClientUnitParam.QueryParam param);
boolean addClientUnit(ClientUnitParam.AddParam param);
boolean updateClientUnit(ClientUnitParam.UpdateParam param);
ClientUnitVO getClientUnit(String id);
boolean deleteClientUnits(List<String> ids);
void exportClientUnits(ClientUnitParam.QueryParam param);
void downloadImportTemplate();
ClientUnitImportResultVO importClientUnits(MultipartFile file);
}

View File

@@ -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<ClientUnitMapper, ClientUnitPO> 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<ClientUnitVO> listClientUnits(ClientUnitParam.QueryParam param) {
ClientUnitParam.QueryParam query = param == null ? new ClientUnitParam.QueryParam() : param;
Page<ClientUnitPO> page = this.page(
new Page<ClientUnitPO>(PageFactory.getPageNum(query), PageFactory.getPageSize(query)),
buildQueryWrapper(query));
Page<ClientUnitVO> result = new Page<ClientUnitVO>(page.getCurrent(), page.getSize(), page.getTotal());
Map<String, String> 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<String> ids) {
List<String> 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<ClientUnitExcelVO> 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<ClientUnitExcelVO> 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<String> fileKeys = new HashSet<String>();
List<ClientUnitPO> saveRows = new ArrayList<ClientUnitPO>();
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<ClientUnitPO> buildQueryWrapper(ClientUnitParam.QueryParam query) {
LambdaQueryWrapper<ClientUnitPO> wrapper = new LambdaQueryWrapper<ClientUnitPO>();
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<ClientUnitPO> wrapper = new LambdaQueryWrapper<ClientUnitPO>();
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<ClientUnitExcelVO> 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<ClientUnitExcelVO> rows = new ArrayList<ClientUnitExcelVO>();
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<String> normalizeIds(List<String> ids) {
if (ids == null || ids.isEmpty()) {
return Collections.emptyList();
}
List<String> result = new ArrayList<String>();
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<String, String> 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<String, String> resolveUserNameMap(List<ClientUnitPO> clientUnits) {
List<String> 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<String, String> 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;
}
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.njcn.gather.aireport.clientunit.mapper.ClientUnitMapper">
</mapper>

26
ai-report/pom.xml Normal file
View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.njcn.gather</groupId>
<artifactId>CN_Tool</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>ai-report</artifactId>
<packaging>pom</packaging>
<name>ai-report</name>
<description>AI report capability aggregator.</description>
<modules>
<module>report-model</module>
<module>client-unit</module>
<module>test-device</module>
<module>test-report</module>
<module>report-approval</module>
</modules>
</project>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.njcn.gather</groupId>
<artifactId>ai-report</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>report-approval</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>report-model</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>test-report</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>njcn-common</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>mybatis-plus</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>spingboot2.3.12</artifactId>
<version>2.3.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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<String, String> resolveUserNames(Collection<String> userIds) {
if (userIds == null || userIds.isEmpty()) {
return Collections.emptyMap();
}
Set<String> validIds = userIds.stream()
.filter(StrUtil::isNotBlank)
.collect(Collectors.toCollection(LinkedHashSet::new));
if (validIds.isEmpty()) {
return Collections.emptyMap();
}
List<ReportModelUserNameVO> 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));
}
}

View File

@@ -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<Page<ReportApprovalPendingVO>> 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<Boolean> 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<Boolean> 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<Page<ReportApprovalLogVO>> logPage(@ModelAttribute ReportApprovalParam.LogQueryParam param) {
String methodDescribe = getMethodDescribe("logPage");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
reportApprovalService.logPage(param), methodDescribe);
}
}

View File

@@ -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<String, String> 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;
}
}

View File

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

View File

@@ -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<String, ReportApprovalHandler> handlerMap = new LinkedHashMap<String, ReportApprovalHandler>();
public ReportApprovalHandlerRegistry(List<ReportApprovalHandler> 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;
}
}

View File

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

View File

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

View File

@@ -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<ReportApprovalLogPO> {
@Select({
"<script>",
"select id, name from sys_user where id in",
"<foreach collection='ids' item='id' open='(' separator=',' close=')'>",
"#{id}",
"</foreach>",
"</script>"
})
List<ReportModelUserNameVO> selectUserNamesByIds(@Param("ids") Collection<String> ids);
@Select({
"<script>",
"select count(1) from (",
"<if test='param.reportType == null or param.reportType == \"\" or param.reportType == \"REPORT_MODEL\"'>",
"select model.id",
"from ai_reportmodel model",
"where model.status = 1 and model.state = '02'",
"</if>",
"<if test='param.reportType == null or param.reportType == \"\"'>",
"union all",
"</if>",
"<if test='param.reportType == null or param.reportType == \"\" or param.reportType == \"TEST_REPORT\"'>",
"select report.id",
"from ai_testreport report",
"where report.status = 1 and report.state = '02'",
"</if>",
") pending",
"</script>"
})
long countPendingReports(@Param("param") ReportApprovalParam.PendingQueryParam param);
@Select({
"<script>",
"select * from (",
"<if test='param.reportType == null or param.reportType == \"\" or param.reportType == \"REPORT_MODEL\"'>",
"select 'REPORT_MODEL' as reportType, model.id as reportId, model.name as reportName, model.state,",
"model.update_by as submitterId, user.name as submitterName, model.update_time as submitTime",
"from ai_reportmodel model",
"left join sys_user user on user.id = model.update_by",
"where model.status = 1 and model.state = '02'",
"</if>",
"<if test='param.reportType == null or param.reportType == \"\"'>",
"union all",
"</if>",
"<if test='param.reportType == null or param.reportType == \"\" or param.reportType == \"TEST_REPORT\"'>",
"select 'TEST_REPORT' as reportType, report.id as reportId, report.no as reportName, report.state,",
"report.update_by as submitterId, user.name as submitterName, report.update_time as submitTime",
"from ai_testreport report",
"left join sys_user user on user.id = report.update_by",
"where report.status = 1 and report.state = '02'",
"</if>",
") pending",
"order by pending.submitTime desc",
"limit #{offset}, #{size}",
"</script>"
})
List<ReportApprovalPendingVO> selectPendingPage(@Param("offset") long offset,
@Param("size") long size,
@Param("param") ReportApprovalParam.PendingQueryParam param);
@Select({
"<script>",
"select id, report_type as reportType, report_id as reportId, report_name as reportName,",
"approval_result as approvalResult, approval_reason as approvalReason, before_state as beforeState,",
"after_state as afterState, approver_name as approverName, approval_time as approvalTime",
"from ai_report_approval_log",
"where status = 1",
"<if test='param.reportType != null and param.reportType != \"\"'>",
"and report_type = #{param.reportType}",
"</if>",
"order by approval_time desc, create_time desc",
"</script>"
})
Page<ReportApprovalLogVO> selectLogPage(Page<ReportApprovalLogVO> page,
@Param("param") ReportApprovalParam.LogQueryParam param);
}

View File

@@ -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() {
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<ReportApprovalPendingVO> pendingPage(ReportApprovalParam.PendingQueryParam param);
Page<ReportApprovalLogVO> logPage(ReportApprovalParam.LogQueryParam param);
}

View File

@@ -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<ReportApprovalPendingVO> 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<ReportApprovalPendingVO> records = total <= 0
? Collections.<ReportApprovalPendingVO>emptyList()
: reportApprovalMapper.selectPendingPage(offset, size, query);
Page<ReportApprovalPendingVO> page = new Page<ReportApprovalPendingVO>(current, size, total);
page.setRecords(records);
return page;
}
@Override
public Page<ReportApprovalLogVO> logPage(ReportApprovalParam.LogQueryParam param) {
ReportApprovalParam.LogQueryParam query = param == null ? new ReportApprovalParam.LogQueryParam() : param;
return reportApprovalMapper.selectLogPage(
new Page<ReportApprovalLogVO>(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);
}
}

View File

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

View File

@@ -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.<ReportApprovalHandler>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<ReportApprovalPendingVO> records = Arrays.asList(record);
when(mapper.countPendingReports(param)).thenReturn(11L);
when(mapper.selectPendingPage(5L, 5L, param)).thenReturn(records);
Page<ReportApprovalPendingVO> 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);
}
}

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.njcn.gather</groupId>
<artifactId>ai-report</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>report-model</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>comservice</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>njcn-common</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>mybatis-plus</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>spingboot2.3.12</artifactId>
<version>2.3.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

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

View File

@@ -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<String, String> resolveUserNames(Collection<String> userIds) {
if (userIds == null || userIds.isEmpty()) {
return Collections.emptyMap();
}
Set<String> validIds = userIds.stream()
.filter(StrUtil::isNotBlank)
.collect(Collectors.toCollection(LinkedHashSet::new));
if (validIds.isEmpty()) {
return Collections.emptyMap();
}
List<ReportModelUserNameVO> 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));
}
}

View File

@@ -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<Page<ReportModelVO>> 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<Boolean> 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<Boolean> 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<Boolean> delete(@RequestBody List<String> 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<ReportModelVO> 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<InputStreamResource> 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<byte[]> 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<Boolean> 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, "编码模板文件名失败");
}
}
}

View File

@@ -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<ReportModelPO> {
@Select({
"<script>",
"select id, name from sys_user where id in",
"<foreach collection='ids' item='id' open='(' separator=',' close=')'>",
"#{id}",
"</foreach>",
"</script>"
})
List<ReportModelUserNameVO> selectUserNamesByIds(@Param("ids") Collection<String> ids);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<ReportModelPO> {
Page<ReportModelVO> listReportModels(ReportModelParam.QueryParam param);
boolean addReportModel(ReportModelParam.AddParam param, MultipartFile templateFile);
boolean updateReportModel(ReportModelParam.UpdateParam param, MultipartFile templateFile);
boolean deleteReportModels(List<String> ids);
ReportModelVO getReportModel(String id);
ReportModelPO requireNormal(String id);
boolean submitAudit(ReportModelParam.SubmitAuditParam param);
}

View File

@@ -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<ReportModelMapper, ReportModelPO> 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<ReportModelVO> listReportModels(ReportModelParam.QueryParam param) {
ReportModelParam.QueryParam query = param == null ? new ReportModelParam.QueryParam() : param;
LambdaQueryWrapper<ReportModelPO> 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<ReportModelPO> page = this.page(new Page<>(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), wrapper);
Page<ReportModelVO> result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
Map<String, String> 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<String> ids) {
if (ids == null || ids.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "模板ID不能为空");
}
List<String> 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<ReportModelPO> 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<String, String> 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<String, String> resolveUserNameMap(List<ReportModelPO> models) {
List<String> 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<String, String> 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<String, String> 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;
}
}

View File

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

View File

@@ -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<String, String> result = resolver.resolveUserNames(Arrays.asList("user-001"));
Assert.assertEquals("张三", result.get("user-001"));
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.njcn.gather</groupId>
<artifactId>ai-report</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>test-device</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>njcn-common</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>mybatis-plus</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>spingboot2.3.12</artifactId>
<version>2.3.12</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>system</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

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

View File

@@ -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<Page<TestDeviceVO>> 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<Boolean> 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<Boolean> 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<Boolean> delete(@RequestBody List<String> 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<TestDeviceVO> 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<TestDeviceImportResultVO> 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<InputStreamResource> 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<InputStreamResource> previewReport(@PathVariable("id") String id) {
return buildReportResponse(id, true);
}
private ResponseEntity<InputStreamResource> 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, "读取校准报告失败");
}
}
}

View File

@@ -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<TestDevicePO> {
@Select({
"<script>",
"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.status = 1",
"<if test='param.keyword != null and param.keyword != \"\"'>",
"and (d.no like concat('%', #{param.keyword}, '%') or dt.name like concat('%', #{param.keyword}, '%'))",
"</if>",
"<if test='param.searchBeginTime != null and param.searchBeginTime != \"\"'>",
"and d.create_time &gt;= #{param.searchBeginTime}",
"</if>",
"<if test='param.searchEndTime != null and param.searchEndTime != \"\"'>",
"and d.create_time &lt;= #{param.searchEndTime}",
"</if>",
"order by d.create_time desc",
"</script>"
})
Page<TestDeviceVO> selectDevicePage(Page<TestDeviceVO> 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<TestDeviceTypeNameVO> selectNormalDeviceTypes();
}

View File

@@ -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() {
}
}

View File

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

View File

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

View File

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

View File

@@ -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<String> failReasons = new ArrayList<String>();
}

View File

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

View File

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

View File

@@ -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<TestDevicePO> {
Page<TestDeviceVO> listTestDevices(TestDeviceParam.QueryParam param);
boolean addTestDevice(TestDeviceParam.AddParam param, MultipartFile reportFile);
boolean updateTestDevice(TestDeviceParam.UpdateParam param, MultipartFile reportFile);
boolean deleteTestDevices(List<String> ids);
TestDeviceVO getTestDevice(String id);
TestDevicePO requireNormal(String id);
void downloadTemplate();
TestDeviceImportResultVO importTestDevices(MultipartFile file, MultipartFile reportZip);
void exportTestDevices(TestDeviceParam.QueryParam param);
}

View File

@@ -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<TestDeviceMapper, TestDevicePO> 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<TestDeviceVO> listTestDevices(TestDeviceParam.QueryParam param) {
TestDeviceParam.QueryParam query = normalizeQueryParam(param);
Page<TestDeviceVO> page = this.baseMapper.selectDevicePage(
new Page<TestDeviceVO>(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<String> ids) {
List<String> 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<TestDeviceExcelVO> rows = readImportRows(file);
TestDeviceImportResultVO result = new TestDeviceImportResultVO();
if (rows.isEmpty()) {
return result;
}
ImportArchive archive = readReportZip(reportZip);
try {
Map<String, String> typeNameIdMap = loadTypeNameIdMap();
List<NormalizedImportDevice> devices = validateImportRows(rows, archive, typeNameIdMap, result);
if (result.getFailCount() > 0) {
return result;
}
List<String> savedReports = new ArrayList<String>();
try {
LocalDateTime now = LocalDateTime.now();
String userId = resolveCurrentUserId();
List<TestDevicePO> saveRows = new ArrayList<TestDevicePO>();
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<TestDeviceVO> page = this.baseMapper.selectDevicePage(new Page<TestDeviceVO>(1, Integer.MAX_VALUE), query);
List<TestDeviceExcelVO> 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<NormalizedImportDevice> validateImportRows(List<TestDeviceExcelVO> rows, ImportArchive archive,
Map<String, String> typeNameIdMap, TestDeviceImportResultVO result) {
Set<String> fileNos = new HashSet<String>();
List<NormalizedImportDevice> devices = new ArrayList<NormalizedImportDevice>();
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<TestDeviceExcelVO> 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<TestDeviceExcelVO> rows = new ArrayList<TestDeviceExcelVO>();
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<String, Path> files = new HashMap<String, Path>();
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<Path> 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<String, String> loadTypeNameIdMap() {
List<TestDeviceTypeNameVO> 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<TestDevicePO> wrapper = new LambdaQueryWrapper<TestDevicePO>();
wrapper.eq(TestDevicePO::getNo, no)
.ne(StrUtil.isNotBlank(excludeId), TestDevicePO::getId, excludeId);
if (this.count(wrapper) > 0) {
throw new BusinessException(CommonResponseEnum.FAIL, "设备编号已存在");
}
}
private List<String> normalizeIds(List<String> ids) {
if (ids == null || ids.isEmpty()) {
return Collections.emptyList();
}
List<String> result = new ArrayList<String>();
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<String, Path> reportFiles;
ImportArchive(Path tempDir, Map<String, Path> 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<Path> pathStream = Files.walk(tempDir)) {
pathStream.sorted(Comparator.reverseOrder()).forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (IOException ignored) {
// 导入临时文件清理失败不影响主流程返回。
}
});
} catch (IOException ignored) {
// 导入临时目录清理失败不影响主流程返回。
}
}
}
}

View File

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

View File

@@ -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<InputStreamResource> response = (ResponseEntity<InputStreamResource>)
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));
}
}

View File

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

View File

@@ -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<TestDevicePO> queryWrapper) {
return countResult;
}
@Override
public boolean updateById(TestDevicePO entity) {
return updateResult;
}
@Override
public TestDevicePO requireNormal(String id) {
return requiredDevice;
}
}
}

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.njcn.gather</groupId>
<artifactId>ai-report</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>test-report</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>comservice</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>report-model</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>njcn-common</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>mybatis-plus</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>spingboot2.3.12</artifactId>
<version>2.3.12</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>system</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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<Page<TestReportVO>> 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<Boolean> 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<Boolean> 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<Boolean> delete(@RequestBody List<String> 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<TestReportVO> 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<byte[]> 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<Boolean> 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);
}
}

View File

@@ -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<TestReportPO> {
@Select({
"<script>",
"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.status = 1",
"<if test='param.keyword != null and param.keyword != \"\"'>",
"and (r.no like concat('%', #{param.keyword}, '%')",
"or r.contract_number like concat('%', #{param.keyword}, '%')",
"or cu.name like concat('%', #{param.keyword}, '%')",
"or rm.name like concat('%', #{param.keyword}, '%'))",
"</if>",
"<if test='param.searchBeginTime != null and param.searchBeginTime != \"\"'>",
"and r.create_time &gt;= #{param.searchBeginTime}",
"</if>",
"<if test='param.searchEndTime != null and param.searchEndTime != \"\"'>",
"and r.create_time &lt;= #{param.searchEndTime}",
"</if>",
"<if test='param.state != null and param.state != \"\"'>",
"and r.state = #{param.state}",
"</if>",
"order by r.create_time desc",
"</script>"
})
Page<TestReportVO> selectReportPage(Page<TestReportVO> 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);
}

View File

@@ -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() {
}
}

View File

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

Some files were not shown because too many files have changed in this diff Show More