Compare commits
36 Commits
2026-05
...
9b71dfdce3
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b71dfdce3 | |||
| 46ae986db2 | |||
|
|
6fc0d58fe4 | ||
|
|
42911e35bb | ||
|
|
cbafe85c4a | ||
| d8ae012bd2 | |||
|
|
fa1a285e04 | ||
| f5f828cd05 | |||
| c1ad7feec2 | |||
| 0ea686d3cb | |||
|
|
646b5f3283 | ||
|
|
e92ed85647 | ||
|
|
5bd709560a | ||
| 2b56da2134 | |||
| 97b1334714 | |||
| 7fb4c8e78a | |||
| fd6e5097d7 | |||
|
|
1edee2bf12 | ||
| b7b18dc325 | |||
| a5a09022f2 | |||
| 4aca8ca2c4 | |||
| 362bbf536f | |||
|
|
89ec9e1fa3 | ||
|
|
1343d235c8 | ||
|
|
0cecf2d7a2 | ||
|
|
c0cf4de315 | ||
| 212b69060c | |||
|
|
f7154db93d | ||
|
|
557022d346 | ||
|
|
e43ab264e0 | ||
| 1c979e248a | |||
| 36962221f5 | |||
|
|
24bdaa1ae9 | ||
|
|
58ca8b0c23 | ||
|
|
8a92ff3be0 | ||
|
|
ddadf26837 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -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*
|
||||
@@ -49,3 +52,8 @@ rebel.xml
|
||||
/.fastRequest/collections/Root/Default Group/directory.json
|
||||
/.fastRequest/collections/Root/directory.json
|
||||
/.fastRequest/config/fastRequestCurrentProjectConfig.json
|
||||
/ai-report/report-approval/src/main/resources/sql/report-approval/
|
||||
/ai-report/test-report/src/main/resources/sql/test-report/
|
||||
/ai-report/client-unit/src/main/resources/sql/client-unit/
|
||||
/ai-report/report-model/src/main/resources/sql/report-model/
|
||||
/ai-report/test-device/src/main/resources/sql/test-device/
|
||||
|
||||
@@ -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`。
|
||||
|
||||
@@ -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`
|
||||
- 负责波形文本解析与查看数据组装能力
|
||||
|
||||
|
||||
57
ai-report/client-unit/pom.xml
Normal file
57
ai-report/client-unit/pom.xml
Normal 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>
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
26
ai-report/pom.xml
Normal file
26
ai-report/pom.xml
Normal 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>
|
||||
56
ai-report/report-approval/pom.xml
Normal file
56
ai-report/report-approval/pom.xml
Normal 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>
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
57
ai-report/report-model/pom.xml
Normal file
57
ai-report/report-model/pom.xml
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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, "编码模板文件名失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
ai-report/test-device/pom.xml
Normal file
63
ai-report/test-device/pom.xml
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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, "读取校准报告失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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 >= #{param.searchBeginTime}",
|
||||
"</if>",
|
||||
"<if test='param.searchEndTime != null and param.searchEndTime != \"\"'>",
|
||||
"and d.create_time <= #{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();
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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) {
|
||||
// 导入临时目录清理失败不影响主流程返回。
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
87
ai-report/test-report/pom.xml
Normal file
87
ai-report/test-report/pom.xml
Normal file
@@ -0,0 +1,87 @@
|
||||
<?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.gather</groupId>
|
||||
<artifactId>client-unit</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.njcn.gather</groupId>
|
||||
<artifactId>test-device</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.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>
|
||||
@@ -0,0 +1,382 @@
|
||||
package com.njcn.gather.aireport.testreport.component;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
|
||||
import com.njcn.common.pojo.exception.BusinessException;
|
||||
import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst;
|
||||
import lombok.Data;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.DataFormatter;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Component
|
||||
public class TestReportLedgerExcelParser {
|
||||
|
||||
private static final DataFormatter FORMATTER = new DataFormatter();
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
private static final List<String> REQUIRED_SHEETS = Arrays.asList(
|
||||
TestReportConst.LEDGER_POINT_SHEET_NAME);
|
||||
private static final List<String> OPTIONAL_SHEETS = Arrays.asList(
|
||||
TestReportConst.LEDGER_GUIDE_SHEET_NAME,
|
||||
TestReportConst.LEDGER_DEVICE_SHEET_NAME,
|
||||
TestReportConst.LEDGER_CLIENT_SHEET_NAME);
|
||||
private static final List<String> POINT_HEADERS = Arrays.asList(
|
||||
"分组号", "变电站", "监测点名称", "检测设备编号", "委托单位名称", "Excel附件名称", "Word附件名称");
|
||||
private static final List<String> DEVICE_HEADERS = Arrays.asList(
|
||||
"设备型号", "设备编号", "标准有效期", "设备状态", "标准报告文件名");
|
||||
private static final List<String> CLIENT_HEADERS = Arrays.asList(
|
||||
"单位名称", "联系人", "联系方式", "地址");
|
||||
|
||||
public ParsedLedger parse(MultipartFile summaryFile, Map<String, MultipartFile> attachments) {
|
||||
String fileName = summaryFile == null ? null : StrUtil.trimToNull(summaryFile.getOriginalFilename());
|
||||
Set<String> attachmentNames = attachments == null ? Collections.<String>emptySet() : attachments.keySet();
|
||||
try (InputStream inputStream = summaryFile.getInputStream()) {
|
||||
return parse(fileName, inputStream, attachmentNames);
|
||||
} catch (IOException exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:读取台账汇总excel失败");
|
||||
}
|
||||
}
|
||||
|
||||
public ParsedLedger parse(String fileName, InputStream inputStream, Set<String> attachmentNames) {
|
||||
ParsedLedger ledger = parseForValidate(fileName, inputStream);
|
||||
ensureRequiredSheetsPresent(ledger);
|
||||
validateReferencedAttachments(ledger.getPoints(), attachmentNames);
|
||||
return ledger;
|
||||
}
|
||||
|
||||
public ParsedLedger parseForValidate(String fileName, InputStream inputStream) {
|
||||
String normalizedFileName = StrUtil.trimToNull(fileName);
|
||||
if (!TestReportConst.LEDGER_SUMMARY_FILE_NAME.equals(normalizedFileName)) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:台账汇总文件名必须为台账信息汇总.xlsx");
|
||||
}
|
||||
try (Workbook workbook = WorkbookFactory.create(inputStream)) {
|
||||
ParsedLedger ledger = new ParsedLedger();
|
||||
ledger.setSummaryFileName(TestReportConst.LEDGER_SUMMARY_FILE_NAME);
|
||||
ledger.getRequiredSheets().addAll(REQUIRED_SHEETS);
|
||||
collectSheets(ledger, workbook);
|
||||
|
||||
Sheet pointSheet = workbook.getSheet(TestReportConst.LEDGER_POINT_SHEET_NAME);
|
||||
if (pointSheet != null) {
|
||||
ledger.setPoints(parsePointSheet(pointSheet));
|
||||
}
|
||||
// 检测设备/委托单位 sheet 按需填写,仅在实际存在时才做结构校验和解析。
|
||||
Sheet deviceSheet = workbook.getSheet(TestReportConst.LEDGER_DEVICE_SHEET_NAME);
|
||||
if (deviceSheet != null) {
|
||||
ledger.setDevices(parseDeviceSheet(deviceSheet));
|
||||
}
|
||||
Sheet clientSheet = workbook.getSheet(TestReportConst.LEDGER_CLIENT_SHEET_NAME);
|
||||
if (clientSheet != null) {
|
||||
ledger.setClients(parseClientSheet(clientSheet));
|
||||
}
|
||||
return ledger;
|
||||
} catch (BusinessException exception) {
|
||||
throw exception;
|
||||
} catch (IOException exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:读取台账汇总excel失败");
|
||||
} catch (Exception exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:解析台账汇总excel失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void collectSheets(ParsedLedger ledger, Workbook workbook) {
|
||||
for (String sheetName : REQUIRED_SHEETS) {
|
||||
Sheet sheet = workbook.getSheet(sheetName);
|
||||
if (sheet == null) {
|
||||
ledger.getMissingSheets().add(sheetName);
|
||||
continue;
|
||||
}
|
||||
ledger.getExistingSheets().add(sheetName);
|
||||
}
|
||||
for (String sheetName : OPTIONAL_SHEETS) {
|
||||
if (workbook.getSheet(sheetName) != null) {
|
||||
ledger.getExistingSheets().add(sheetName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureRequiredSheetsPresent(ParsedLedger ledger) {
|
||||
if (!ledger.getMissingSheets().isEmpty()) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL,
|
||||
"校验文件阶段:缺少" + String.join("、", ledger.getMissingSheets()) + "sheet");
|
||||
}
|
||||
}
|
||||
|
||||
private List<ParsedPointRow> parsePointSheet(Sheet sheet) {
|
||||
validateHeaders(sheet, POINT_HEADERS, "监测点信息");
|
||||
List<ParsedPointRow> points = new ArrayList<ParsedPointRow>();
|
||||
Set<String> pointKeys = new HashSet<String>();
|
||||
Set<String> excelNames = new HashSet<String>();
|
||||
Set<String> wordNames = new HashSet<String>();
|
||||
for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
|
||||
Row row = sheet.getRow(rowIndex);
|
||||
if (isEmptyRow(row, POINT_HEADERS.size())) {
|
||||
continue;
|
||||
}
|
||||
ParsedPointRow point = new ParsedPointRow();
|
||||
point.setRowNo(rowIndex + 1);
|
||||
point.setGroupNo(parsePositiveInteger(optionalCell(row, 0), "第" + (rowIndex + 1) + "行分组号必须为正整数"));
|
||||
point.setSubstationName(requireCell(row, 1, rowIndex, "变电站"));
|
||||
point.setPointName(requireCell(row, 2, rowIndex, "监测点名称"));
|
||||
point.setTestDeviceNo(optionalCell(row, 3));
|
||||
point.setClientUnitName(optionalCell(row, 4));
|
||||
point.setExcelAttachmentName(requireCell(row, 5, rowIndex, "Excel附件名称"));
|
||||
point.setWordAttachmentName(requireCell(row, 6, rowIndex, "Word附件名称"));
|
||||
validateExtension(point.getExcelAttachmentName(), Arrays.asList(".xls", ".xlsx"),
|
||||
"第" + (rowIndex + 1) + "行Excel附件扩展名仅允许xls/xlsx");
|
||||
validateExtension(point.getWordAttachmentName(), Arrays.asList(".doc", ".docx"),
|
||||
"第" + (rowIndex + 1) + "行Word附件扩展名仅允许doc/docx");
|
||||
String pointKey = point.getSubstationName() + "|" + point.getPointName();
|
||||
if (!pointKeys.add(pointKey)) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:监测点信息中存在重复点位");
|
||||
}
|
||||
if (!excelNames.add(point.getExcelAttachmentName())) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:监测点信息中存在重复Excel附件名称");
|
||||
}
|
||||
if (!wordNames.add(point.getWordAttachmentName())) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:监测点信息中存在重复Word附件名称");
|
||||
}
|
||||
points.add(point);
|
||||
}
|
||||
if (points.isEmpty()) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:监测点信息sheet不能为空");
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
private List<ParsedDeviceRow> parseDeviceSheet(Sheet sheet) {
|
||||
validateHeaders(sheet, DEVICE_HEADERS, "检测设备");
|
||||
List<ParsedDeviceRow> devices = new ArrayList<ParsedDeviceRow>();
|
||||
Set<String> deviceNos = new HashSet<String>();
|
||||
for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
|
||||
Row row = sheet.getRow(rowIndex);
|
||||
if (isEmptyRow(row, DEVICE_HEADERS.size())) {
|
||||
continue;
|
||||
}
|
||||
ParsedDeviceRow device = new ParsedDeviceRow();
|
||||
device.setRowNo(rowIndex + 1);
|
||||
device.setTypeName(optionalCell(row, 0));
|
||||
device.setNo(optionalCell(row, 1));
|
||||
device.setValidityPeriod(parseOptionalDate(optionalCell(row, 2), rowIndex));
|
||||
device.setStateName(optionalCell(row, 3));
|
||||
device.setValidityReportName(optionalCell(row, 4));
|
||||
if (StrUtil.isBlank(device.getNo())) {
|
||||
continue;
|
||||
}
|
||||
if (!deviceNos.add(device.getNo())) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:检测设备sheet中存在重复设备编号");
|
||||
}
|
||||
devices.add(device);
|
||||
}
|
||||
return devices;
|
||||
}
|
||||
|
||||
private List<ParsedClientRow> parseClientSheet(Sheet sheet) {
|
||||
validateHeaders(sheet, CLIENT_HEADERS, "委托单位");
|
||||
List<ParsedClientRow> clients = new ArrayList<ParsedClientRow>();
|
||||
Set<String> clientNames = new HashSet<String>();
|
||||
for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
|
||||
Row row = sheet.getRow(rowIndex);
|
||||
if (isEmptyRow(row, CLIENT_HEADERS.size())) {
|
||||
continue;
|
||||
}
|
||||
ParsedClientRow client = new ParsedClientRow();
|
||||
client.setRowNo(rowIndex + 1);
|
||||
client.setName(optionalCell(row, 0));
|
||||
client.setContactName(optionalCell(row, 1));
|
||||
client.setPhonenumber(optionalCell(row, 2));
|
||||
client.setAddress(optionalCell(row, 3));
|
||||
if (StrUtil.isBlank(client.getName())) {
|
||||
continue;
|
||||
}
|
||||
if (!clientNames.add(client.getName())) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:委托单位sheet中存在重复单位名称");
|
||||
}
|
||||
clients.add(client);
|
||||
}
|
||||
return clients;
|
||||
}
|
||||
|
||||
private void validateReferencedAttachments(List<ParsedPointRow> points, Set<String> attachmentNames) {
|
||||
Set<String> referencedNames = new HashSet<String>();
|
||||
for (ParsedPointRow point : points) {
|
||||
if (StrUtil.isNotBlank(point.getExcelAttachmentName())) {
|
||||
referencedNames.add(point.getExcelAttachmentName());
|
||||
}
|
||||
if (StrUtil.isNotBlank(point.getWordAttachmentName())) {
|
||||
referencedNames.add(point.getWordAttachmentName());
|
||||
}
|
||||
}
|
||||
for (ParsedPointRow point : points) {
|
||||
if (!attachmentNames.contains(point.getExcelAttachmentName())) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:第" + point.getRowNo() + "行缺少Excel附件");
|
||||
}
|
||||
if (!attachmentNames.contains(point.getWordAttachmentName())) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:第" + point.getRowNo() + "行缺少Word附件");
|
||||
}
|
||||
}
|
||||
for (String attachmentName : attachmentNames) {
|
||||
if (!referencedNames.contains(attachmentName)) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:存在未在监测点信息sheet中引用的附件 " + attachmentName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateHeaders(Sheet sheet, List<String> expectedHeaders, String sheetName) {
|
||||
Row headerRow = sheet.getRow(0);
|
||||
if (headerRow == null) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:" + sheetName + "sheet缺少表头");
|
||||
}
|
||||
for (int i = 0; i < expectedHeaders.size(); i++) {
|
||||
String actualHeader = optionalCell(headerRow, i);
|
||||
if (!expectedHeaders.get(i).equals(actualHeader)) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:" + sheetName + "sheet表头不符合模板要求");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isEmptyRow(Row row, int cellCount) {
|
||||
if (row == null) {
|
||||
return true;
|
||||
}
|
||||
for (int i = 0; i < cellCount; i++) {
|
||||
if (StrUtil.isNotBlank(optionalCell(row, i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private String requireCell(Row row, int cellIndex, int rowIndex, String fieldName) {
|
||||
String value = optionalCell(row, cellIndex);
|
||||
if (StrUtil.isBlank(value)) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:第" + (rowIndex + 1) + "行缺少" + fieldName);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private String optionalCell(Row row, int cellIndex) {
|
||||
if (row == null) {
|
||||
return null;
|
||||
}
|
||||
Cell cell = row.getCell(cellIndex);
|
||||
return cell == null ? null : StrUtil.trimToNull(FORMATTER.formatCellValue(cell));
|
||||
}
|
||||
|
||||
private Integer parsePositiveInteger(String value, String message) {
|
||||
if (StrUtil.isBlank(value)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
int parsed = Integer.parseInt(value);
|
||||
if (parsed <= 0) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:" + message);
|
||||
}
|
||||
return parsed;
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:" + message);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateExtension(String fileName, List<String> validExtensions, String message) {
|
||||
String lowerFileName = fileName.toLowerCase();
|
||||
for (String validExtension : validExtensions) {
|
||||
if (lowerFileName.endsWith(validExtension)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:" + message);
|
||||
}
|
||||
|
||||
private LocalDate parseDate(String value, int rowIndex) {
|
||||
try {
|
||||
return LocalDate.parse(value, DATE_FORMATTER);
|
||||
} catch (DateTimeParseException exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL,
|
||||
"校验文件阶段:检测设备sheet第" + (rowIndex + 1) + "行标准有效期格式必须为yyyy-MM-dd");
|
||||
}
|
||||
}
|
||||
|
||||
private LocalDate parseOptionalDate(String value, int rowIndex) {
|
||||
if (StrUtil.isBlank(value)) {
|
||||
return null;
|
||||
}
|
||||
return parseDate(value, rowIndex);
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ParsedLedger {
|
||||
private String summaryFileName;
|
||||
private List<String> requiredSheets = new ArrayList<String>();
|
||||
private List<String> existingSheets = new ArrayList<String>();
|
||||
private List<String> missingSheets = new ArrayList<String>();
|
||||
private List<ParsedPointRow> points = new ArrayList<ParsedPointRow>();
|
||||
private List<ParsedDeviceRow> devices = new ArrayList<ParsedDeviceRow>();
|
||||
private List<ParsedClientRow> clients = new ArrayList<ParsedClientRow>();
|
||||
|
||||
public Map<String, ParsedDeviceRow> toDeviceMap() {
|
||||
Map<String, ParsedDeviceRow> result = new HashMap<String, ParsedDeviceRow>();
|
||||
for (ParsedDeviceRow device : devices) {
|
||||
result.put(device.getNo(), device);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<String, ParsedClientRow> toClientMap() {
|
||||
Map<String, ParsedClientRow> result = new HashMap<String, ParsedClientRow>();
|
||||
for (ParsedClientRow client : clients) {
|
||||
result.put(client.getName(), client);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ParsedPointRow {
|
||||
private Integer rowNo;
|
||||
private Integer groupNo;
|
||||
private String substationName;
|
||||
private String pointName;
|
||||
private String testDeviceNo;
|
||||
private String clientUnitName;
|
||||
private String excelAttachmentName;
|
||||
private String wordAttachmentName;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ParsedDeviceRow {
|
||||
private Integer rowNo;
|
||||
private String typeName;
|
||||
private String no;
|
||||
private LocalDate validityPeriod;
|
||||
private String stateName;
|
||||
private String validityReportName;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ParsedClientRow {
|
||||
private Integer rowNo;
|
||||
private String name;
|
||||
private String contactName;
|
||||
private String phonenumber;
|
||||
private String address;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,879 @@
|
||||
package com.njcn.gather.aireport.testreport.component;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
|
||||
import com.njcn.common.pojo.exception.BusinessException;
|
||||
import com.njcn.gather.aireport.clientunit.mapper.ClientUnitMapper;
|
||||
import com.njcn.gather.aireport.clientunit.pojo.constant.ClientUnitConst;
|
||||
import com.njcn.gather.aireport.clientunit.pojo.po.ClientUnitPO;
|
||||
import com.njcn.gather.aireport.testdevice.mapper.TestDeviceMapper;
|
||||
import com.njcn.gather.aireport.testdevice.pojo.constant.TestDeviceConst;
|
||||
import com.njcn.gather.aireport.testdevice.pojo.po.TestDevicePO;
|
||||
import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceTypeNameVO;
|
||||
import com.njcn.gather.aireport.testreport.mapper.TestReportGroupReportMapper;
|
||||
import com.njcn.gather.aireport.testreport.mapper.TestReportMapper;
|
||||
import com.njcn.gather.aireport.testreport.mapper.TestReportPointMapper;
|
||||
import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst;
|
||||
import com.njcn.gather.aireport.testreport.pojo.po.TestReportGroupReportPO;
|
||||
import com.njcn.gather.aireport.testreport.pojo.po.TestReportPO;
|
||||
import com.njcn.gather.aireport.testreport.pojo.po.TestReportPointPO;
|
||||
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportLedgerImportResultVO;
|
||||
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportLedgerImportStageVO;
|
||||
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportLedgerValidateAttachmentVO;
|
||||
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportLedgerValidatePointVO;
|
||||
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportLedgerValidateResultVO;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
public class TestReportLedgerImportService {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
private static final String BATCH_STATUS_VALIDATED = "validated";
|
||||
|
||||
private final TestReportMapper testReportMapper;
|
||||
private final TestReportPointMapper testReportPointMapper;
|
||||
private final TestReportGroupReportMapper testReportGroupReportMapper;
|
||||
private final TestDeviceMapper testDeviceMapper;
|
||||
private final ClientUnitMapper clientUnitMapper;
|
||||
private final TestReportLedgerExcelParser testReportLedgerExcelParser;
|
||||
private final TestReportStorageService testReportStorageService;
|
||||
|
||||
public TestReportLedgerImportService(TestReportMapper testReportMapper,
|
||||
TestReportPointMapper testReportPointMapper,
|
||||
TestReportGroupReportMapper testReportGroupReportMapper,
|
||||
TestDeviceMapper testDeviceMapper,
|
||||
ClientUnitMapper clientUnitMapper,
|
||||
TestReportLedgerExcelParser testReportLedgerExcelParser,
|
||||
TestReportStorageService testReportStorageService) {
|
||||
this.testReportMapper = testReportMapper;
|
||||
this.testReportPointMapper = testReportPointMapper;
|
||||
this.testReportGroupReportMapper = testReportGroupReportMapper;
|
||||
this.testDeviceMapper = testDeviceMapper;
|
||||
this.clientUnitMapper = clientUnitMapper;
|
||||
this.testReportLedgerExcelParser = testReportLedgerExcelParser;
|
||||
this.testReportStorageService = testReportStorageService;
|
||||
}
|
||||
|
||||
public TestReportLedgerValidateResultVO validateLedgerSession(String sessionId, MultipartFile[] files) {
|
||||
Map<String, MultipartFile> fileMap = normalizeFileMap(files);
|
||||
TestReportStorageService.UploadedBatch uploadedBatch = testReportStorageService.saveValidateSessionFiles(sessionId, fileMap);
|
||||
List<String> sessionFileNames = testReportStorageService.listValidateSessionFileNames(sessionId);
|
||||
Set<String> sessionFileNameSet = new HashSet<String>(sessionFileNames);
|
||||
TestReportLedgerValidateResultVO result = new TestReportLedgerValidateResultVO();
|
||||
result.setSessionId(sessionId);
|
||||
result.setTempStoragePath(uploadedBatch.getTempStoragePath());
|
||||
result.setUploadedFileCount(sessionFileNames.size());
|
||||
result.setCanContinueUpload(Boolean.TRUE);
|
||||
result.setSummaryFileName(TestReportConst.LEDGER_SUMMARY_FILE_NAME);
|
||||
result.setSummaryFilePresent(sessionFileNameSet.contains(TestReportConst.LEDGER_SUMMARY_FILE_NAME));
|
||||
if (!Boolean.TRUE.equals(result.getSummaryFilePresent())) {
|
||||
result.setSuccess(Boolean.FALSE);
|
||||
result.getFailReasons().add("鏍¢獙鏂囦欢闃舵锛氱己灏戝彴璐︿俊鎭眹鎬?xlsx");
|
||||
persistValidateSessionResult(sessionId, result);
|
||||
return result;
|
||||
}
|
||||
try (InputStream inputStream = Files.newInputStream(
|
||||
testReportStorageService.resolveValidateSessionFile(sessionId, TestReportConst.LEDGER_SUMMARY_FILE_NAME))) {
|
||||
TestReportLedgerExcelParser.ParsedLedger ledger = testReportLedgerExcelParser.parseForValidate(
|
||||
TestReportConst.LEDGER_SUMMARY_FILE_NAME, inputStream);
|
||||
Map<String, TestDevicePO> dbDeviceMap = loadExistingDevices(collectDeviceNos(ledger));
|
||||
Map<String, ClientUnitPO> dbClientMap = loadExistingClients(collectClientNames(ledger));
|
||||
result.getRequiredSheets().addAll(ledger.getRequiredSheets());
|
||||
result.getExistingSheets().addAll(ledger.getExistingSheets());
|
||||
result.getMissingSheets().addAll(ledger.getMissingSheets());
|
||||
fillPointValidationResult(result, ledger, sessionFileNameSet, dbDeviceMap, dbClientMap);
|
||||
result.getOrphanAttachmentNames().addAll(resolveOrphanAttachmentNames(ledger, sessionFileNameSet));
|
||||
result.setSuccess(result.getMissingSheets().isEmpty() && result.getMissingAttachmentCount() != null
|
||||
&& result.getMissingAttachmentCount() == 0 && !hasReferenceErrors(result.getPoints()));
|
||||
if (!result.getMissingSheets().isEmpty()) {
|
||||
result.getFailReasons().add("鏍¢獙鏂囦欢闃舵锛歋heet涓嶄笉榻? " + String.join("銆?", result.getMissingSheets()));
|
||||
}
|
||||
if (result.getMissingAttachmentCount() != null && result.getMissingAttachmentCount() > 0) {
|
||||
result.getFailReasons().add("鏍¢獙鏂囦欢闃舵锛氬瓨鍦ㄧ己澶遍檮浠讹紝鍙户缁笂浼犲悗鍐嶆鏍¢獙");
|
||||
}
|
||||
appendReferenceFailReasons(result);
|
||||
persistValidateSessionResult(sessionId, result);
|
||||
return result;
|
||||
} catch (BusinessException exception) {
|
||||
throw exception;
|
||||
} catch (Exception exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "鏍¢獙鏂囦欢闃舵锛氳鍙栧彴璐︽眹鎬籩xcel澶辫触");
|
||||
}
|
||||
}
|
||||
|
||||
public TestReportLedgerImportResultVO validateLedger(TestReportPO report, MultipartFile[] files, String userId) {
|
||||
Map<String, MultipartFile> fileMap = normalizeFileMap(files);
|
||||
String batchId = UUID.randomUUID().toString();
|
||||
TestReportStorageService.UploadedBatch uploadedBatch = testReportStorageService.saveTempBatchFiles(report.getId(), batchId, fileMap);
|
||||
try {
|
||||
MultipartFile summaryFile = fileMap.get(TestReportConst.LEDGER_SUMMARY_FILE_NAME);
|
||||
if (summaryFile == null) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:缺少台账信息汇总.xlsx");
|
||||
}
|
||||
TestReportLedgerExcelParser.ParsedLedger ledger = parseLedgerFromBatch(report.getId(), batchId, fileMap.keySet());
|
||||
Map<String, TestDevicePO> dbDeviceMap = loadExistingDevices(collectDeviceNos(ledger));
|
||||
Map<String, ClientUnitPO> dbClientMap = loadExistingClients(collectClientNames(ledger));
|
||||
validatePointReferences(ledger.getPoints(), dbDeviceMap, dbClientMap, ledger.toDeviceMap(), ledger.toClientMap());
|
||||
writeValidatedBatchMeta(report.getId(), batchId, uploadedBatch, ledger, userId);
|
||||
return buildValidateSuccessResult(batchId, uploadedBatch.getTempStoragePath(), ledger, fileMap.size());
|
||||
} catch (RuntimeException exception) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
public TestReportLedgerImportResultVO importLedger(TestReportPO report, String batchId, String userId) {
|
||||
BatchMeta batchMeta = loadValidatedBatchMeta(report.getId(), batchId);
|
||||
TestReportLedgerExcelParser.ParsedLedger ledger = parseLedgerFromBatch(report.getId(), batchId, batchMeta.toFileNameSet());
|
||||
|
||||
Map<String, TestDevicePO> dbDeviceMap = loadExistingDevices(collectDeviceNos(ledger));
|
||||
Map<String, ClientUnitPO> dbClientMap = loadExistingClients(collectClientNames(ledger));
|
||||
BaseDataSummary baseDataSummary = maintainBaseData(ledger, dbDeviceMap, dbClientMap, userId);
|
||||
|
||||
List<TestReportPointPO> oldPoints = loadNormalPoints(report.getId());
|
||||
List<TestReportGroupReportPO> oldGroupReports = loadNormalGroupReports(report.getId());
|
||||
deleteOldFiles(oldPoints, oldGroupReports, report.getData());
|
||||
removeOldRows(report.getId());
|
||||
|
||||
String summaryStoragePath = testReportStorageService.saveLedgerSummaryFromTemp(report.getId(), batchId,
|
||||
batchMeta.getSummaryFileName());
|
||||
int groupCount = saveNewPoints(report.getId(), batchId, ledger.getPoints(), userId);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
report.setData(buildSnapshotJson(ledger, summaryStoragePath, baseDataSummary, groupCount));
|
||||
report.setReportGenerateState(TestReportConst.REPORT_GENERATE_STATE_PENDING);
|
||||
report.setUpdateBy(userId);
|
||||
report.setUpdateTime(now);
|
||||
testReportMapper.updateById(report);
|
||||
testReportStorageService.deleteTempBatchQuietly(report.getId(), batchId);
|
||||
return buildImportSuccessResult(batchId, batchMeta.getTempStoragePath(), ledger, baseDataSummary,
|
||||
batchMeta.getUploadedFileCount(), groupCount);
|
||||
}
|
||||
|
||||
private TestReportLedgerExcelParser.ParsedLedger parseLedgerFromBatch(String reportId, String batchId, Collection<String> fileNames) {
|
||||
try (InputStream inputStream = java.nio.file.Files.newInputStream(
|
||||
testReportStorageService.resolveTempBatchFile(reportId, batchId, TestReportConst.LEDGER_SUMMARY_FILE_NAME))) {
|
||||
return testReportLedgerExcelParser.parse(TestReportConst.LEDGER_SUMMARY_FILE_NAME, inputStream,
|
||||
buildAttachmentNameSet(fileNames));
|
||||
} catch (java.io.IOException exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:读取台账汇总excel失败");
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> buildAttachmentNameSet(Collection<String> fileNames) {
|
||||
Set<String> attachmentNames = new HashSet<String>();
|
||||
if (fileNames == null || fileNames.isEmpty()) {
|
||||
return attachmentNames;
|
||||
}
|
||||
for (String fileName : fileNames) {
|
||||
if (StrUtil.isBlank(fileName) || TestReportConst.LEDGER_SUMMARY_FILE_NAME.equals(fileName)) {
|
||||
continue;
|
||||
}
|
||||
attachmentNames.add(fileName);
|
||||
}
|
||||
return attachmentNames;
|
||||
}
|
||||
|
||||
private void fillPointValidationResult(TestReportLedgerValidateResultVO result,
|
||||
TestReportLedgerExcelParser.ParsedLedger ledger,
|
||||
Set<String> sessionFileNameSet,
|
||||
Map<String, TestDevicePO> dbDeviceMap,
|
||||
Map<String, ClientUnitPO> dbClientMap) {
|
||||
int missingAttachmentCount = 0;
|
||||
Map<String, TestReportLedgerExcelParser.ParsedDeviceRow> sheetDeviceMap = ledger.toDeviceMap();
|
||||
Map<String, TestReportLedgerExcelParser.ParsedClientRow> sheetClientMap = ledger.toClientMap();
|
||||
for (TestReportLedgerExcelParser.ParsedPointRow pointRow : ledger.getPoints()) {
|
||||
TestReportLedgerValidatePointVO pointVO = new TestReportLedgerValidatePointVO();
|
||||
pointVO.setRowNo(pointRow.getRowNo());
|
||||
pointVO.setSubstationName(pointRow.getSubstationName());
|
||||
pointVO.setPointName(pointRow.getPointName());
|
||||
pointVO.setPointKey(pointRow.getSubstationName() + "|" + pointRow.getPointName());
|
||||
|
||||
TestReportLedgerValidateAttachmentVO excelAttachment = buildAttachmentRow(
|
||||
pointRow.getExcelAttachmentName(), "excel", sessionFileNameSet.contains(pointRow.getExcelAttachmentName()));
|
||||
TestReportLedgerValidateAttachmentVO wordAttachment = buildAttachmentRow(
|
||||
pointRow.getWordAttachmentName(), "word", sessionFileNameSet.contains(pointRow.getWordAttachmentName()));
|
||||
|
||||
pointVO.getAttachments().add(excelAttachment);
|
||||
pointVO.getAttachments().add(wordAttachment);
|
||||
|
||||
int pointMissingCount = 0;
|
||||
if (Boolean.TRUE.equals(excelAttachment.getMissing())) {
|
||||
pointMissingCount++;
|
||||
}
|
||||
if (Boolean.TRUE.equals(wordAttachment.getMissing())) {
|
||||
pointMissingCount++;
|
||||
}
|
||||
pointVO.setMissingAttachmentCount(pointMissingCount);
|
||||
fillReferenceValidationResult(pointVO, pointRow, dbDeviceMap, dbClientMap, sheetDeviceMap, sheetClientMap);
|
||||
pointVO.setComplete(pointMissingCount == 0
|
||||
&& !Boolean.FALSE.equals(pointVO.getTestDeviceNoValid())
|
||||
&& !Boolean.FALSE.equals(pointVO.getClientUnitNameValid()));
|
||||
missingAttachmentCount += pointMissingCount;
|
||||
result.getPoints().add(pointVO);
|
||||
}
|
||||
result.setPointCount(result.getPoints().size());
|
||||
result.setMissingAttachmentCount(missingAttachmentCount);
|
||||
}
|
||||
|
||||
private void fillReferenceValidationResult(TestReportLedgerValidatePointVO pointVO,
|
||||
TestReportLedgerExcelParser.ParsedPointRow pointRow,
|
||||
Map<String, TestDevicePO> dbDeviceMap,
|
||||
Map<String, ClientUnitPO> dbClientMap,
|
||||
Map<String, TestReportLedgerExcelParser.ParsedDeviceRow> sheetDeviceMap,
|
||||
Map<String, TestReportLedgerExcelParser.ParsedClientRow> sheetClientMap) {
|
||||
if (StrUtil.isBlank(pointRow.getTestDeviceNo())) {
|
||||
pointVO.setTestDeviceNoValid(Boolean.TRUE);
|
||||
pointVO.setTestDeviceNoMessage("检测设备编号为空,跳过校验");
|
||||
} else if (dbDeviceMap.containsKey(pointRow.getTestDeviceNo()) || sheetDeviceMap.containsKey(pointRow.getTestDeviceNo())) {
|
||||
pointVO.setTestDeviceNoValid(Boolean.TRUE);
|
||||
pointVO.setTestDeviceNoMessage("检测设备编号校验通过");
|
||||
} else {
|
||||
pointVO.setTestDeviceNoValid(Boolean.FALSE);
|
||||
pointVO.setTestDeviceNoMessage("第" + pointRow.getRowNo() + "行检测设备编号在数据库和检测设备sheet中都不存在");
|
||||
}
|
||||
if (StrUtil.isBlank(pointRow.getClientUnitName())) {
|
||||
pointVO.setClientUnitNameValid(Boolean.TRUE);
|
||||
pointVO.setClientUnitNameMessage("委托单位名称为空,跳过校验");
|
||||
} else if (dbClientMap.containsKey(pointRow.getClientUnitName()) || sheetClientMap.containsKey(pointRow.getClientUnitName())) {
|
||||
pointVO.setClientUnitNameValid(Boolean.TRUE);
|
||||
pointVO.setClientUnitNameMessage("委托单位名称校验通过");
|
||||
} else {
|
||||
pointVO.setClientUnitNameValid(Boolean.FALSE);
|
||||
pointVO.setClientUnitNameMessage("第" + pointRow.getRowNo() + "行委托单位名称在数据库和委托单位sheet中都不存在");
|
||||
}
|
||||
}
|
||||
|
||||
private TestReportLedgerValidateAttachmentVO buildAttachmentRow(String attachmentName, String attachmentType,
|
||||
boolean uploaded) {
|
||||
TestReportLedgerValidateAttachmentVO attachmentVO = new TestReportLedgerValidateAttachmentVO();
|
||||
attachmentVO.setAttachmentName(attachmentName);
|
||||
attachmentVO.setAttachmentType(attachmentType);
|
||||
attachmentVO.setReferenced(Boolean.TRUE);
|
||||
attachmentVO.setUploaded(uploaded);
|
||||
attachmentVO.setMissing(!uploaded);
|
||||
attachmentVO.setReuploadAllowed(Boolean.TRUE);
|
||||
attachmentVO.setMessage(uploaded ? "宸蹭笂浼?" : "缂哄皯闄勪欢锛屽彲缁х画涓婁紶");
|
||||
return attachmentVO;
|
||||
}
|
||||
|
||||
private List<String> resolveOrphanAttachmentNames(TestReportLedgerExcelParser.ParsedLedger ledger, Set<String> sessionFileNameSet) {
|
||||
Set<String> referencedNames = new HashSet<String>();
|
||||
for (TestReportLedgerExcelParser.ParsedPointRow pointRow : ledger.getPoints()) {
|
||||
if (StrUtil.isNotBlank(pointRow.getExcelAttachmentName())) {
|
||||
referencedNames.add(pointRow.getExcelAttachmentName());
|
||||
}
|
||||
if (StrUtil.isNotBlank(pointRow.getWordAttachmentName())) {
|
||||
referencedNames.add(pointRow.getWordAttachmentName());
|
||||
}
|
||||
}
|
||||
List<String> orphanAttachmentNames = new java.util.ArrayList<String>();
|
||||
for (String fileName : sessionFileNameSet) {
|
||||
if (TestReportConst.LEDGER_SUMMARY_FILE_NAME.equals(fileName)) {
|
||||
continue;
|
||||
}
|
||||
if (!referencedNames.contains(fileName)) {
|
||||
orphanAttachmentNames.add(fileName);
|
||||
}
|
||||
}
|
||||
return orphanAttachmentNames;
|
||||
}
|
||||
|
||||
private void persistValidateSessionResult(String sessionId, TestReportLedgerValidateResultVO result) {
|
||||
try {
|
||||
testReportStorageService.writeValidateSessionResult(sessionId, OBJECT_MAPPER.writeValueAsString(result));
|
||||
} catch (Exception exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "鏍¢獙鏂囦欢闃舵锛氫繚瀛樻牎楠岀粨鏋滃け璐?");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasReferenceErrors(List<TestReportLedgerValidatePointVO> points) {
|
||||
if (points == null || points.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (TestReportLedgerValidatePointVO point : points) {
|
||||
if (point == null) {
|
||||
continue;
|
||||
}
|
||||
if (Boolean.FALSE.equals(point.getTestDeviceNoValid()) || Boolean.FALSE.equals(point.getClientUnitNameValid())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void appendReferenceFailReasons(TestReportLedgerValidateResultVO result) {
|
||||
if (result == null || result.getPoints() == null || result.getPoints().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
boolean hasDeviceError = false;
|
||||
boolean hasClientError = false;
|
||||
for (TestReportLedgerValidatePointVO point : result.getPoints()) {
|
||||
if (point == null) {
|
||||
continue;
|
||||
}
|
||||
if (Boolean.FALSE.equals(point.getTestDeviceNoValid())) {
|
||||
hasDeviceError = true;
|
||||
}
|
||||
if (Boolean.FALSE.equals(point.getClientUnitNameValid())) {
|
||||
hasClientError = true;
|
||||
}
|
||||
}
|
||||
if (hasDeviceError) {
|
||||
result.getFailReasons().add("监测点信息中存在检测设备编号未匹配到数据库或检测设备sheet的记录");
|
||||
}
|
||||
if (hasClientError) {
|
||||
result.getFailReasons().add("监测点信息中存在委托单位名称未匹配到数据库或委托单位sheet的记录");
|
||||
}
|
||||
}
|
||||
|
||||
private void writeValidatedBatchMeta(String reportId, String batchId, TestReportStorageService.UploadedBatch uploadedBatch,
|
||||
TestReportLedgerExcelParser.ParsedLedger ledger, String userId) {
|
||||
BatchMeta batchMeta = new BatchMeta();
|
||||
batchMeta.setBatchId(batchId);
|
||||
batchMeta.setReportId(reportId);
|
||||
batchMeta.setStatus(BATCH_STATUS_VALIDATED);
|
||||
batchMeta.setSummaryFileName(ledger.getSummaryFileName());
|
||||
batchMeta.setTempStoragePath(uploadedBatch.getTempStoragePath());
|
||||
batchMeta.setUploadedFileCount(uploadedBatch.getStoredFileMap().size());
|
||||
batchMeta.setUploadedFileNames(uploadedBatch.getStoredFileMap().keySet().stream().collect(Collectors.toList()));
|
||||
batchMeta.setValidatedBy(userId);
|
||||
batchMeta.setValidatedTime(LocalDateTime.now().toString());
|
||||
try {
|
||||
testReportStorageService.writeTempBatchMeta(reportId, batchId, OBJECT_MAPPER.writeValueAsString(batchMeta));
|
||||
} catch (Exception exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:保存批次元数据失败");
|
||||
}
|
||||
}
|
||||
|
||||
private BatchMeta loadValidatedBatchMeta(String reportId, String batchId) {
|
||||
if (StrUtil.isBlank(batchId)) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "导入批次ID不能为空");
|
||||
}
|
||||
String metaText = testReportStorageService.readTempBatchMeta(reportId, batchId);
|
||||
if (StrUtil.isBlank(metaText)) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "导入批次不存在或未通过校验");
|
||||
}
|
||||
try {
|
||||
BatchMeta batchMeta = OBJECT_MAPPER.readValue(metaText, BatchMeta.class);
|
||||
if (!reportId.equals(batchMeta.getReportId()) || !BATCH_STATUS_VALIDATED.equals(batchMeta.getStatus())) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "导入批次不存在或未通过校验");
|
||||
}
|
||||
return batchMeta;
|
||||
} catch (BusinessException exception) {
|
||||
throw exception;
|
||||
} catch (Exception exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "读取导入批次信息失败");
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, MultipartFile> normalizeFileMap(MultipartFile[] files) {
|
||||
if (files == null || files.length == 0) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "选择文件阶段:导入文件不能为空");
|
||||
}
|
||||
Map<String, MultipartFile> fileMap = new LinkedHashMap<String, MultipartFile>();
|
||||
for (MultipartFile file : files) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String fileName = normalizeFileName(file.getOriginalFilename());
|
||||
if (fileName == null) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "选择文件阶段:文件名不能为空");
|
||||
}
|
||||
if (fileMap.containsKey(fileName)) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "选择文件阶段:存在重复文件名 " + fileName);
|
||||
}
|
||||
fileMap.put(fileName, file);
|
||||
}
|
||||
if (fileMap.isEmpty()) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "选择文件阶段:导入文件不能为空");
|
||||
}
|
||||
return fileMap;
|
||||
}
|
||||
|
||||
private void validatePointReferences(List<TestReportLedgerExcelParser.ParsedPointRow> points,
|
||||
Map<String, TestDevicePO> dbDeviceMap,
|
||||
Map<String, ClientUnitPO> dbClientMap,
|
||||
Map<String, TestReportLedgerExcelParser.ParsedDeviceRow> sheetDeviceMap,
|
||||
Map<String, TestReportLedgerExcelParser.ParsedClientRow> sheetClientMap) {
|
||||
for (TestReportLedgerExcelParser.ParsedPointRow point : points) {
|
||||
if (StrUtil.isNotBlank(point.getTestDeviceNo())
|
||||
&& !dbDeviceMap.containsKey(point.getTestDeviceNo())
|
||||
&& !sheetDeviceMap.containsKey(point.getTestDeviceNo())) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL,
|
||||
"校验文件阶段:第" + point.getRowNo() + "行检测设备编号在数据库和检测设备sheet中都不存在");
|
||||
}
|
||||
if (StrUtil.isNotBlank(point.getClientUnitName())
|
||||
&& !dbClientMap.containsKey(point.getClientUnitName())
|
||||
&& !sheetClientMap.containsKey(point.getClientUnitName())) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL,
|
||||
"校验文件阶段:第" + point.getRowNo() + "行委托单位名称在数据库和委托单位sheet中都不存在");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private BaseDataSummary maintainBaseData(TestReportLedgerExcelParser.ParsedLedger ledger,
|
||||
Map<String, TestDevicePO> dbDeviceMap,
|
||||
Map<String, ClientUnitPO> dbClientMap,
|
||||
String userId) {
|
||||
BaseDataSummary summary = new BaseDataSummary();
|
||||
Map<String, String> typeNameIdMap = loadDeviceTypeNameIdMap();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (TestReportLedgerExcelParser.ParsedDeviceRow deviceRow : ledger.getDevices()) {
|
||||
TestDevicePO existingDevice = dbDeviceMap.get(deviceRow.getNo());
|
||||
if (existingDevice != null) {
|
||||
summary.setDeviceReuseCount(summary.getDeviceReuseCount() + 1);
|
||||
continue;
|
||||
}
|
||||
String typeName = normalizeFileName(deviceRow.getTypeName());
|
||||
String typeId = StrUtil.isBlank(typeName) ? null : typeNameIdMap.get(typeName);
|
||||
if (StrUtil.isNotBlank(typeName) && StrUtil.isBlank(typeId)) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL,
|
||||
"基础信息维护阶段:检测设备sheet第" + deviceRow.getRowNo() + "行设备型号不存在");
|
||||
}
|
||||
TestDevicePO device = new TestDevicePO();
|
||||
device.setId(UUID.randomUUID().toString());
|
||||
device.setType(typeId);
|
||||
device.setNo(deviceRow.getNo());
|
||||
device.setValidityPeriod(deviceRow.getValidityPeriod());
|
||||
device.setValidityReport(deviceRow.getValidityReportName());
|
||||
device.setState(resolveDeviceState(deviceRow.getStateName(), deviceRow.getRowNo()));
|
||||
device.setStatus(TestDeviceConst.STATUS_NORMAL);
|
||||
device.setCreateBy(userId);
|
||||
device.setCreateTime(now);
|
||||
device.setUpdateBy(userId);
|
||||
device.setUpdateTime(now);
|
||||
testDeviceMapper.insert(device);
|
||||
dbDeviceMap.put(device.getNo(), device);
|
||||
summary.setDeviceAddedCount(summary.getDeviceAddedCount() + 1);
|
||||
}
|
||||
|
||||
for (TestReportLedgerExcelParser.ParsedClientRow clientRow : ledger.getClients()) {
|
||||
ClientUnitPO existingClient = dbClientMap.get(clientRow.getName());
|
||||
if (existingClient != null) {
|
||||
summary.setClientReuseCount(summary.getClientReuseCount() + 1);
|
||||
continue;
|
||||
}
|
||||
ClientUnitPO client = new ClientUnitPO();
|
||||
client.setId(UUID.randomUUID().toString());
|
||||
client.setName(clientRow.getName());
|
||||
client.setContactName(defaultEmpty(clientRow.getContactName()));
|
||||
client.setPhonenumber(defaultEmpty(clientRow.getPhonenumber()));
|
||||
client.setAddress(clientRow.getAddress());
|
||||
client.setStatus(ClientUnitConst.STATUS_NORMAL);
|
||||
client.setCreateBy(userId);
|
||||
client.setCreateTime(now);
|
||||
client.setUpdateBy(userId);
|
||||
client.setUpdateTime(now);
|
||||
clientUnitMapper.insert(client);
|
||||
dbClientMap.put(client.getName(), client);
|
||||
summary.setClientAddedCount(summary.getClientAddedCount() + 1);
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
private int saveNewPoints(String reportId, String batchId, List<TestReportLedgerExcelParser.ParsedPointRow> points,
|
||||
String userId) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
Set<Integer> groupNos = new HashSet<Integer>();
|
||||
int sortNo = 1;
|
||||
for (TestReportLedgerExcelParser.ParsedPointRow pointRow : points) {
|
||||
TestReportPointPO point = new TestReportPointPO();
|
||||
point.setId(UUID.randomUUID().toString());
|
||||
point.setReportId(reportId);
|
||||
point.setGroupNo(pointRow.getGroupNo());
|
||||
point.setSubstationName(pointRow.getSubstationName());
|
||||
point.setPointName(pointRow.getPointName());
|
||||
point.setTestDeviceNo(pointRow.getTestDeviceNo());
|
||||
point.setClientUnitName(pointRow.getClientUnitName());
|
||||
point.setExcelAttachmentName(pointRow.getExcelAttachmentName());
|
||||
point.setExcelStoragePath(testReportStorageService.savePointExcelFromTemp(reportId, batchId,
|
||||
pointRow.getExcelAttachmentName()));
|
||||
point.setWordAttachmentName(pointRow.getWordAttachmentName());
|
||||
point.setWordStoragePath(testReportStorageService.savePointWordFromTemp(reportId, batchId,
|
||||
pointRow.getWordAttachmentName()));
|
||||
point.setSortNo(sortNo++);
|
||||
point.setDeleted(TestReportConst.DELETED_NO);
|
||||
point.setCreateBy(userId);
|
||||
point.setCreateTime(now);
|
||||
point.setUpdateBy(userId);
|
||||
point.setUpdateTime(now);
|
||||
testReportPointMapper.insert(point);
|
||||
if (pointRow.getGroupNo() != null) {
|
||||
groupNos.add(pointRow.getGroupNo());
|
||||
}
|
||||
}
|
||||
return groupNos.size();
|
||||
}
|
||||
|
||||
private void deleteOldFiles(List<TestReportPointPO> oldPoints, List<TestReportGroupReportPO> oldGroupReports, String data) {
|
||||
for (TestReportPointPO point : oldPoints) {
|
||||
testReportStorageService.deleteQuietly(point.getExcelStoragePath());
|
||||
testReportStorageService.deleteQuietly(point.getWordStoragePath());
|
||||
}
|
||||
for (TestReportGroupReportPO groupReport : oldGroupReports) {
|
||||
testReportStorageService.deleteQuietly(groupReport.getReportStoragePath());
|
||||
}
|
||||
testReportStorageService.deleteQuietly(resolveSummaryStoragePath(data));
|
||||
}
|
||||
|
||||
private void removeOldRows(String reportId) {
|
||||
LambdaQueryWrapper<TestReportPointPO> pointWrapper = new LambdaQueryWrapper<TestReportPointPO>();
|
||||
pointWrapper.eq(TestReportPointPO::getReportId, reportId);
|
||||
testReportPointMapper.delete(pointWrapper);
|
||||
|
||||
LambdaQueryWrapper<TestReportGroupReportPO> groupReportWrapper = new LambdaQueryWrapper<TestReportGroupReportPO>();
|
||||
groupReportWrapper.eq(TestReportGroupReportPO::getReportId, reportId);
|
||||
testReportGroupReportMapper.delete(groupReportWrapper);
|
||||
}
|
||||
|
||||
private String buildSnapshotJson(TestReportLedgerExcelParser.ParsedLedger ledger, String summaryStoragePath,
|
||||
BaseDataSummary summary, int groupCount) {
|
||||
ObjectNode root = OBJECT_MAPPER.createObjectNode();
|
||||
root.put("version", "2.0");
|
||||
root.put("importMode", "summary-excel-with-base-data");
|
||||
root.put("summaryFileName", ledger.getSummaryFileName());
|
||||
root.put("summaryStoragePath", summaryStoragePath);
|
||||
|
||||
ObjectNode sheetSummary = root.putObject("sheetSummary");
|
||||
sheetSummary.put("pointSheet", TestReportConst.LEDGER_POINT_SHEET_NAME);
|
||||
sheetSummary.put("deviceSheetPresent", ledger.getExistingSheets().contains(TestReportConst.LEDGER_DEVICE_SHEET_NAME));
|
||||
sheetSummary.put("clientSheetPresent", ledger.getExistingSheets().contains(TestReportConst.LEDGER_CLIENT_SHEET_NAME));
|
||||
|
||||
root.put("rowCount", ledger.getPoints().size());
|
||||
root.put("pointCount", ledger.getPoints().size());
|
||||
root.put("groupCount", groupCount);
|
||||
|
||||
ArrayNode groupSummary = root.putArray("groupSummary");
|
||||
Map<Integer, Long> groupCounter = ledger.getPoints().stream()
|
||||
.collect(Collectors.groupingBy(TestReportLedgerExcelParser.ParsedPointRow::getGroupNo,
|
||||
LinkedHashMap::new, Collectors.counting()));
|
||||
for (Map.Entry<Integer, Long> entry : groupCounter.entrySet()) {
|
||||
ObjectNode groupNode = groupSummary.addObject();
|
||||
if (entry.getKey() == null) {
|
||||
groupNode.putNull("groupNo");
|
||||
} else {
|
||||
groupNode.put("groupNo", entry.getKey());
|
||||
}
|
||||
groupNode.put("count", entry.getValue());
|
||||
}
|
||||
|
||||
ObjectNode baseDataSummary = root.putObject("baseDataSummary");
|
||||
baseDataSummary.put("deviceAddedCount", summary.getDeviceAddedCount());
|
||||
baseDataSummary.put("deviceReuseCount", summary.getDeviceReuseCount());
|
||||
baseDataSummary.put("clientAddedCount", summary.getClientAddedCount());
|
||||
baseDataSummary.put("clientReuseCount", summary.getClientReuseCount());
|
||||
return root.toString();
|
||||
}
|
||||
|
||||
private TestReportLedgerImportResultVO buildValidateSuccessResult(String batchId, String tempStoragePath,
|
||||
TestReportLedgerExcelParser.ParsedLedger ledger,
|
||||
int uploadedFileCount) {
|
||||
TestReportLedgerImportResultVO result = new TestReportLedgerImportResultVO();
|
||||
result.setBatchId(batchId);
|
||||
result.setCurrentStage("文件校验");
|
||||
result.setSuccess(Boolean.TRUE);
|
||||
result.setSummaryFileName(ledger.getSummaryFileName());
|
||||
result.setTempStoragePath(tempStoragePath);
|
||||
result.setUploadedFileCount(uploadedFileCount);
|
||||
result.setTotalFileCount(uploadedFileCount);
|
||||
result.setPointCount(ledger.getPoints().size());
|
||||
result.setExcelAttachmentCount(ledger.getPoints().size());
|
||||
result.setWordAttachmentCount(ledger.getPoints().size());
|
||||
result.getStages().add(buildStage("文件选择", "文件选择完成"));
|
||||
result.getStages().add(buildStage("文件校验", "文件校验通过"));
|
||||
return result;
|
||||
}
|
||||
|
||||
private TestReportLedgerImportResultVO buildImportSuccessResult(String batchId, String tempStoragePath,
|
||||
TestReportLedgerExcelParser.ParsedLedger ledger,
|
||||
BaseDataSummary summary, int uploadedFileCount,
|
||||
int groupCount) {
|
||||
TestReportLedgerImportResultVO result = new TestReportLedgerImportResultVO();
|
||||
result.setBatchId(batchId);
|
||||
result.setCurrentStage("完成");
|
||||
result.setSuccess(Boolean.TRUE);
|
||||
result.setSummaryFileName(ledger.getSummaryFileName());
|
||||
result.setTempStoragePath(tempStoragePath);
|
||||
result.setUploadedFileCount(uploadedFileCount);
|
||||
result.setTotalFileCount(uploadedFileCount);
|
||||
result.setPointCount(ledger.getPoints().size());
|
||||
result.setExcelAttachmentCount(ledger.getPoints().size());
|
||||
result.setWordAttachmentCount(ledger.getPoints().size());
|
||||
result.setDeviceAddedCount(summary.getDeviceAddedCount());
|
||||
result.setDeviceReuseCount(summary.getDeviceReuseCount());
|
||||
result.setClientAddedCount(summary.getClientAddedCount());
|
||||
result.setClientReuseCount(summary.getClientReuseCount());
|
||||
result.setGroupCount(groupCount);
|
||||
result.getStages().add(buildStage("文件选择", "文件选择完成"));
|
||||
result.getStages().add(buildStage("文件校验", "已复用已校验批次"));
|
||||
result.getStages().add(buildStage("文件导入", "正式文件和点位数据已导入"));
|
||||
result.getStages().add(buildStage("基础信息维护", "基础信息维护完成"));
|
||||
result.getStages().add(buildStage("完成", "导入完成"));
|
||||
return result;
|
||||
}
|
||||
|
||||
private TestReportLedgerImportStageVO buildStage(String stage, String message) {
|
||||
TestReportLedgerImportStageVO stageVO = new TestReportLedgerImportStageVO();
|
||||
stageVO.setStage(stage);
|
||||
stageVO.setSuccess(Boolean.TRUE);
|
||||
stageVO.setMessage(message);
|
||||
return stageVO;
|
||||
}
|
||||
|
||||
private String resolveSummaryStoragePath(String data) {
|
||||
if (StrUtil.isBlank(data)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String summaryStoragePath = OBJECT_MAPPER.readTree(data).path("summaryStoragePath").asText();
|
||||
return StrUtil.isBlank(summaryStoragePath) ? null : summaryStoragePath;
|
||||
} catch (Exception exception) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<TestReportPointPO> loadNormalPoints(String reportId) {
|
||||
LambdaQueryWrapper<TestReportPointPO> wrapper = new LambdaQueryWrapper<TestReportPointPO>();
|
||||
wrapper.eq(TestReportPointPO::getReportId, reportId)
|
||||
.eq(TestReportPointPO::getDeleted, TestReportConst.DELETED_NO);
|
||||
return testReportPointMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
private List<TestReportGroupReportPO> loadNormalGroupReports(String reportId) {
|
||||
LambdaQueryWrapper<TestReportGroupReportPO> wrapper = new LambdaQueryWrapper<TestReportGroupReportPO>();
|
||||
wrapper.eq(TestReportGroupReportPO::getReportId, reportId)
|
||||
.eq(TestReportGroupReportPO::getDeleted, TestReportConst.DELETED_NO);
|
||||
return testReportGroupReportMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
private Map<String, TestDevicePO> loadExistingDevices(Collection<String> deviceNos) {
|
||||
if (deviceNos.isEmpty()) {
|
||||
return new HashMap<String, TestDevicePO>();
|
||||
}
|
||||
LambdaQueryWrapper<TestDevicePO> wrapper = new LambdaQueryWrapper<TestDevicePO>();
|
||||
wrapper.in(TestDevicePO::getNo, deviceNos)
|
||||
.eq(TestDevicePO::getStatus, TestDeviceConst.STATUS_NORMAL);
|
||||
return testDeviceMapper.selectList(wrapper).stream()
|
||||
.collect(Collectors.toMap(TestDevicePO::getNo, item -> item, (first, second) -> first, HashMap::new));
|
||||
}
|
||||
|
||||
private Map<String, ClientUnitPO> loadExistingClients(Collection<String> clientNames) {
|
||||
if (clientNames.isEmpty()) {
|
||||
return new HashMap<String, ClientUnitPO>();
|
||||
}
|
||||
LambdaQueryWrapper<ClientUnitPO> wrapper = new LambdaQueryWrapper<ClientUnitPO>();
|
||||
wrapper.in(ClientUnitPO::getName, clientNames)
|
||||
.eq(ClientUnitPO::getStatus, ClientUnitConst.STATUS_NORMAL);
|
||||
return clientUnitMapper.selectList(wrapper).stream()
|
||||
.collect(Collectors.toMap(ClientUnitPO::getName, item -> item, (first, second) -> first, HashMap::new));
|
||||
}
|
||||
|
||||
private Collection<String> collectDeviceNos(TestReportLedgerExcelParser.ParsedLedger ledger) {
|
||||
Set<String> deviceNos = new HashSet<String>();
|
||||
for (TestReportLedgerExcelParser.ParsedPointRow point : ledger.getPoints()) {
|
||||
if (StrUtil.isNotBlank(point.getTestDeviceNo())) {
|
||||
deviceNos.add(point.getTestDeviceNo());
|
||||
}
|
||||
}
|
||||
for (TestReportLedgerExcelParser.ParsedDeviceRow device : ledger.getDevices()) {
|
||||
deviceNos.add(device.getNo());
|
||||
}
|
||||
return deviceNos;
|
||||
}
|
||||
|
||||
private Collection<String> collectClientNames(TestReportLedgerExcelParser.ParsedLedger ledger) {
|
||||
Set<String> clientNames = new HashSet<String>();
|
||||
for (TestReportLedgerExcelParser.ParsedPointRow point : ledger.getPoints()) {
|
||||
if (StrUtil.isNotBlank(point.getClientUnitName())) {
|
||||
clientNames.add(point.getClientUnitName());
|
||||
}
|
||||
}
|
||||
for (TestReportLedgerExcelParser.ParsedClientRow client : ledger.getClients()) {
|
||||
clientNames.add(client.getName());
|
||||
}
|
||||
return clientNames;
|
||||
}
|
||||
|
||||
private Map<String, String> loadDeviceTypeNameIdMap() {
|
||||
List<TestDeviceTypeNameVO> typeRows = testDeviceMapper.selectNormalDeviceTypes();
|
||||
if (typeRows == null || typeRows.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, String> result = new HashMap<String, String>();
|
||||
for (TestDeviceTypeNameVO typeRow : typeRows) {
|
||||
if (typeRow != null && StrUtil.isNotBlank(typeRow.getName()) && StrUtil.isNotBlank(typeRow.getId())) {
|
||||
result.put(typeRow.getName(), typeRow.getId());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String resolveDeviceState(String stateName, Integer rowNo) {
|
||||
String normalizedState = normalizeFileName(stateName);
|
||||
if (normalizedState == null) {
|
||||
return null;
|
||||
}
|
||||
if ("01".equals(normalizedState) || "在运".equals(normalizedState)) {
|
||||
return TestDeviceConst.STATE_RUNNING;
|
||||
}
|
||||
if ("02".equals(normalizedState) || "退运".equals(normalizedState)) {
|
||||
return TestDeviceConst.STATE_RETIRED;
|
||||
}
|
||||
throw new BusinessException(CommonResponseEnum.FAIL,
|
||||
"基础信息维护阶段:检测设备sheet第" + rowNo + "行设备状态不正确");
|
||||
}
|
||||
|
||||
private String defaultEmpty(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
private String normalizeFileName(String fileName) {
|
||||
String normalized = fileName == null ? null : fileName.trim();
|
||||
return StrUtil.isBlank(normalized) ? null : normalized;
|
||||
}
|
||||
|
||||
private static class BaseDataSummary {
|
||||
private int deviceAddedCount;
|
||||
private int deviceReuseCount;
|
||||
private int clientAddedCount;
|
||||
private int clientReuseCount;
|
||||
|
||||
public int getDeviceAddedCount() {
|
||||
return deviceAddedCount;
|
||||
}
|
||||
|
||||
public void setDeviceAddedCount(int deviceAddedCount) {
|
||||
this.deviceAddedCount = deviceAddedCount;
|
||||
}
|
||||
|
||||
public int getDeviceReuseCount() {
|
||||
return deviceReuseCount;
|
||||
}
|
||||
|
||||
public void setDeviceReuseCount(int deviceReuseCount) {
|
||||
this.deviceReuseCount = deviceReuseCount;
|
||||
}
|
||||
|
||||
public int getClientAddedCount() {
|
||||
return clientAddedCount;
|
||||
}
|
||||
|
||||
public void setClientAddedCount(int clientAddedCount) {
|
||||
this.clientAddedCount = clientAddedCount;
|
||||
}
|
||||
|
||||
public int getClientReuseCount() {
|
||||
return clientReuseCount;
|
||||
}
|
||||
|
||||
public void setClientReuseCount(int clientReuseCount) {
|
||||
this.clientReuseCount = clientReuseCount;
|
||||
}
|
||||
}
|
||||
|
||||
public static class BatchMeta {
|
||||
private String batchId;
|
||||
private String reportId;
|
||||
private String status;
|
||||
private String summaryFileName;
|
||||
private String tempStoragePath;
|
||||
private Integer uploadedFileCount;
|
||||
private List<String> uploadedFileNames;
|
||||
private String validatedBy;
|
||||
private String validatedTime;
|
||||
|
||||
public Set<String> toFileNameSet() {
|
||||
return uploadedFileNames == null ? Collections.<String>emptySet() : new HashSet<String>(uploadedFileNames);
|
||||
}
|
||||
|
||||
public String getBatchId() {
|
||||
return batchId;
|
||||
}
|
||||
|
||||
public void setBatchId(String batchId) {
|
||||
this.batchId = batchId;
|
||||
}
|
||||
|
||||
public String getReportId() {
|
||||
return reportId;
|
||||
}
|
||||
|
||||
public void setReportId(String reportId) {
|
||||
this.reportId = reportId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getSummaryFileName() {
|
||||
return summaryFileName;
|
||||
}
|
||||
|
||||
public void setSummaryFileName(String summaryFileName) {
|
||||
this.summaryFileName = summaryFileName;
|
||||
}
|
||||
|
||||
public String getTempStoragePath() {
|
||||
return tempStoragePath;
|
||||
}
|
||||
|
||||
public void setTempStoragePath(String tempStoragePath) {
|
||||
this.tempStoragePath = tempStoragePath;
|
||||
}
|
||||
|
||||
public Integer getUploadedFileCount() {
|
||||
return uploadedFileCount;
|
||||
}
|
||||
|
||||
public void setUploadedFileCount(Integer uploadedFileCount) {
|
||||
this.uploadedFileCount = uploadedFileCount;
|
||||
}
|
||||
|
||||
public List<String> getUploadedFileNames() {
|
||||
return uploadedFileNames;
|
||||
}
|
||||
|
||||
public void setUploadedFileNames(List<String> uploadedFileNames) {
|
||||
this.uploadedFileNames = uploadedFileNames;
|
||||
}
|
||||
|
||||
public String getValidatedBy() {
|
||||
return validatedBy;
|
||||
}
|
||||
|
||||
public void setValidatedBy(String validatedBy) {
|
||||
this.validatedBy = validatedBy;
|
||||
}
|
||||
|
||||
public String getValidatedTime() {
|
||||
return validatedTime;
|
||||
}
|
||||
|
||||
public void setValidatedTime(String validatedTime) {
|
||||
this.validatedTime = validatedTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.njcn.gather.aireport.testreport.component;
|
||||
|
||||
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
|
||||
import com.njcn.common.pojo.exception.BusinessException;
|
||||
import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.CellStyle;
|
||||
import org.apache.poi.ss.usermodel.Font;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.xssf.usermodel.XSSFSheet;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class TestReportLedgerTemplateService {
|
||||
|
||||
public byte[] buildTemplate() {
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook();
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
writeGuideSheet(workbook.createSheet(TestReportConst.LEDGER_GUIDE_SHEET_NAME));
|
||||
writeHeaders(workbook, workbook.createSheet(TestReportConst.LEDGER_POINT_SHEET_NAME),
|
||||
Arrays.asList("分组号", "变电站", "监测点名称", "检测设备编号", "委托单位名称", "Excel附件名称", "Word附件名称"));
|
||||
writeHeaders(workbook, workbook.createSheet(TestReportConst.LEDGER_DEVICE_SHEET_NAME),
|
||||
Arrays.asList("设备型号", "设备编号", "标准有效期", "设备状态", "标准报告文件名"));
|
||||
writeHeaders(workbook, workbook.createSheet(TestReportConst.LEDGER_CLIENT_SHEET_NAME),
|
||||
Arrays.asList("单位名称", "联系人", "联系方式", "地址"));
|
||||
workbook.write(outputStream);
|
||||
return outputStream.toByteArray();
|
||||
} catch (IOException exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "下载模板失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void writeGuideSheet(XSSFSheet sheet) {
|
||||
Row row0 = sheet.createRow(0);
|
||||
row0.createCell(0).setCellValue("1. 汇总文件名固定为台账信息汇总.xlsx");
|
||||
Row row1 = sheet.createRow(1);
|
||||
row1.createCell(0).setCellValue("2. 监测点信息sheet必填,检测设备/委托单位sheet按需填写");
|
||||
Row row2 = sheet.createRow(2);
|
||||
row2.createCell(0).setCellValue("3. 每个监测点必须同时填写Excel附件名称和Word附件名称");
|
||||
Row row3 = sheet.createRow(3);
|
||||
row3.createCell(0).setCellValue("4. 检测设备编号、委托单位名称优先匹配数据库,未命中时再匹配本次sheet");
|
||||
sheet.setColumnWidth(0, 120 * 256);
|
||||
}
|
||||
|
||||
private void writeHeaders(XSSFWorkbook workbook, XSSFSheet sheet, List<String> headers) {
|
||||
CellStyle style = workbook.createCellStyle();
|
||||
Font font = workbook.createFont();
|
||||
font.setBold(true);
|
||||
style.setFont(font);
|
||||
Row headerRow = sheet.createRow(0);
|
||||
for (int i = 0; i < headers.size(); i++) {
|
||||
Cell cell = headerRow.createCell(i);
|
||||
cell.setCellValue(headers.get(i));
|
||||
cell.setCellStyle(style);
|
||||
sheet.setColumnWidth(i, 24 * 256);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.njcn.gather.aireport.testreport.component;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
|
||||
import com.njcn.common.pojo.exception.BusinessException;
|
||||
import lombok.Data;
|
||||
import org.apache.poi.ss.usermodel.DataFormatter;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class TestReportPointExcelParser {
|
||||
|
||||
private static final String NODE_INFO_SHEET_NAME = "节点信息";
|
||||
|
||||
public ParsedPoint parse(String fileName, InputStream inputStream) {
|
||||
try (Workbook workbook = WorkbookFactory.create(inputStream)) {
|
||||
Sheet sheet = workbook.getSheet(NODE_INFO_SHEET_NAME);
|
||||
if (sheet == null) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "原始Excel缺少节点信息sheet");
|
||||
}
|
||||
return parseNodeInfoSheet(fileName, sheet);
|
||||
} catch (BusinessException exception) {
|
||||
throw exception;
|
||||
} catch (IOException exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "读取原始Excel失败");
|
||||
} catch (Exception exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "解析原始Excel失败");
|
||||
}
|
||||
}
|
||||
|
||||
private ParsedPoint parseNodeInfoSheet(String fileName, Sheet sheet) {
|
||||
Map<String, String> keyValueMap = new LinkedHashMap<String, String>();
|
||||
DataFormatter formatter = new DataFormatter();
|
||||
for (Row row : sheet) {
|
||||
short lastCellNum = row.getLastCellNum();
|
||||
if (lastCellNum < 2) {
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < lastCellNum - 1; i++) {
|
||||
String key = trim(formatter.formatCellValue(row.getCell(i)));
|
||||
String value = trim(formatter.formatCellValue(row.getCell(i + 1)));
|
||||
if (StrUtil.isNotBlank(key) && StrUtil.isNotBlank(value) && !keyValueMap.containsKey(key)) {
|
||||
keyValueMap.put(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParsedPoint point = new ParsedPoint();
|
||||
point.setExcelAttachmentName(fileName);
|
||||
point.setSubstationName(requireAny(keyValueMap, "变电站", "厂站名称", "变电站名称"));
|
||||
point.setPointName(requireAny(keyValueMap, "监测点名称", "节点名称", "监测点"));
|
||||
point.setMonitorTime(optionalAny(keyValueMap, "监测时间", "录波时间", "开始时间"));
|
||||
point.setVoltageLevel(optionalAny(keyValueMap, "电压等级", "电压"));
|
||||
point.setPtRatio(optionalAny(keyValueMap, "PT变比", "PT"));
|
||||
point.setCtRatio(optionalAny(keyValueMap, "CT变比", "CT"));
|
||||
point.setBaseShortCircuitCapacity(optionalAny(keyValueMap, "基准短路容量"));
|
||||
point.setMinShortCircuitCapacity(optionalAny(keyValueMap, "最小短路容量"));
|
||||
point.setProtocolCapacity(optionalAny(keyValueMap, "用户协议容量"));
|
||||
point.setPowerSupplyCapacity(optionalAny(keyValueMap, "供电设备容量"));
|
||||
return point;
|
||||
}
|
||||
|
||||
private String requireAny(Map<String, String> keyValueMap, String... candidateKeys) {
|
||||
String value = optionalAny(keyValueMap, candidateKeys);
|
||||
if (StrUtil.isBlank(value)) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "节点信息sheet缺少必要字段");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private String optionalAny(Map<String, String> keyValueMap, String... candidateKeys) {
|
||||
for (String candidateKey : candidateKeys) {
|
||||
String value = keyValueMap.get(candidateKey);
|
||||
if (StrUtil.isNotBlank(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String trim(String value) {
|
||||
return StrUtil.trimToNull(value);
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ParsedPoint {
|
||||
private String excelAttachmentName;
|
||||
private String substationName;
|
||||
private String pointName;
|
||||
private String monitorTime;
|
||||
private String voltageLevel;
|
||||
private String ptRatio;
|
||||
private String ctRatio;
|
||||
private String baseShortCircuitCapacity;
|
||||
private String minShortCircuitCapacity;
|
||||
private String protocolCapacity;
|
||||
private String powerSupplyCapacity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
package com.njcn.gather.aireport.testreport.component;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
|
||||
import com.njcn.common.pojo.exception.BusinessException;
|
||||
import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Component
|
||||
public class TestReportStorageService {
|
||||
|
||||
@Value("${files.path}")
|
||||
private String filesRootPath;
|
||||
|
||||
public String saveSourceExcel(String reportId, MultipartFile file) {
|
||||
return saveMultipartFile(reportId, TestReportConst.STORAGE_SOURCE_EXCEL_DIR, file, "淇濆瓨鍘熷Excel澶辫触");
|
||||
}
|
||||
|
||||
public String saveLedgerSummaryExcel(String reportId, MultipartFile file) {
|
||||
return saveMultipartFile(reportId, TestReportConst.STORAGE_LEDGER_SUMMARY_DIR, file, "淇濆瓨鍙拌处姹囨€籈xcel澶辫触");
|
||||
}
|
||||
|
||||
public String savePointExcelAttachment(String reportId, MultipartFile file) {
|
||||
return saveMultipartFile(reportId, TestReportConst.STORAGE_POINT_EXCEL_DIR, file, "淇濆瓨鐩戞祴鐐笶xcel闄勪欢澶辫触");
|
||||
}
|
||||
|
||||
public String savePointWordAttachment(String reportId, MultipartFile file) {
|
||||
return saveMultipartFile(reportId, TestReportConst.STORAGE_POINT_WORD_DIR, file, "淇濆瓨鐩戞祴鐐筗ord闄勪欢澶辫触");
|
||||
}
|
||||
|
||||
public UploadedBatch saveTempBatchFiles(String reportId, String batchId, Map<String, MultipartFile> fileMap) {
|
||||
if (fileMap == null || fileMap.isEmpty()) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "文件不能为空");
|
||||
}
|
||||
Map<String, String> storedFileMap = new LinkedHashMap<String, String>();
|
||||
for (Map.Entry<String, MultipartFile> entry : fileMap.entrySet()) {
|
||||
String originalFileName = entry.getKey();
|
||||
MultipartFile file = entry.getValue();
|
||||
if (file == null || file.isEmpty() || StrUtil.isBlank(originalFileName)) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "文件不能为空");
|
||||
}
|
||||
String safeFileName = Paths.get(originalFileName).getFileName().toString();
|
||||
Path targetPath = resolveTempUploadDir(reportId, batchId).resolve(safeFileName);
|
||||
try {
|
||||
Files.createDirectories(targetPath.getParent());
|
||||
file.transferTo(targetPath.toFile());
|
||||
} catch (IOException exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "文件上传阶段:保存临时文件失败");
|
||||
}
|
||||
storedFileMap.put(originalFileName, buildTempUploadRelativePath(reportId, batchId, safeFileName));
|
||||
}
|
||||
UploadedBatch uploadedBatch = new UploadedBatch();
|
||||
uploadedBatch.setBatchId(batchId);
|
||||
uploadedBatch.setTempStoragePath(buildTempBatchRelativePath(reportId, batchId));
|
||||
uploadedBatch.setStoredFileMap(storedFileMap);
|
||||
return uploadedBatch;
|
||||
}
|
||||
|
||||
public UploadedBatch saveValidateSessionFiles(String sessionId, Map<String, MultipartFile> fileMap) {
|
||||
if (fileMap == null || fileMap.isEmpty()) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "鏂囦欢涓嶈兘涓虹┖");
|
||||
}
|
||||
Map<String, String> storedFileMap = new LinkedHashMap<String, String>();
|
||||
for (Map.Entry<String, MultipartFile> entry : fileMap.entrySet()) {
|
||||
String originalFileName = entry.getKey();
|
||||
MultipartFile file = entry.getValue();
|
||||
if (file == null || file.isEmpty() || StrUtil.isBlank(originalFileName)) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "鏂囦欢涓嶈兘涓虹┖");
|
||||
}
|
||||
String safeFileName = Paths.get(originalFileName).getFileName().toString();
|
||||
Path targetPath = resolveValidateSessionUploadDir(sessionId).resolve(safeFileName);
|
||||
try {
|
||||
Files.createDirectories(targetPath.getParent());
|
||||
file.transferTo(targetPath.toFile());
|
||||
} catch (IOException exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "鏂囦欢涓婁紶闃舵锛氫繚瀛樻牎楠屼細璇濇枃浠跺け璐?");
|
||||
}
|
||||
storedFileMap.put(originalFileName, buildValidateSessionUploadRelativePath(sessionId, safeFileName));
|
||||
}
|
||||
UploadedBatch uploadedBatch = new UploadedBatch();
|
||||
uploadedBatch.setBatchId(sessionId);
|
||||
uploadedBatch.setTempStoragePath(buildValidateSessionRelativePath(sessionId));
|
||||
uploadedBatch.setStoredFileMap(storedFileMap);
|
||||
return uploadedBatch;
|
||||
}
|
||||
|
||||
public Path resolveTempBatchFile(String reportId, String batchId, String fileName) {
|
||||
return resolveTempUploadDir(reportId, batchId).resolve(Paths.get(fileName).getFileName().toString()).normalize();
|
||||
}
|
||||
|
||||
public void writeTempBatchMeta(String reportId, String batchId, String content) {
|
||||
Path metaPath = resolveTempBatchMetaPath(reportId, batchId);
|
||||
try {
|
||||
Files.createDirectories(metaPath.getParent());
|
||||
Files.write(metaPath, content.getBytes(StandardCharsets.UTF_8));
|
||||
} catch (IOException exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:保存批次元数据失败");
|
||||
}
|
||||
}
|
||||
|
||||
public void writeValidateSessionResult(String sessionId, String content) {
|
||||
Path resultPath = resolveValidateSessionResultPath(sessionId);
|
||||
try {
|
||||
Files.createDirectories(resultPath.getParent());
|
||||
Files.write(resultPath, content.getBytes(StandardCharsets.UTF_8));
|
||||
} catch (IOException exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "鏍¢獙鏂囦欢闃舵锛氫繚瀛樻牎楠岀粨鏋滃け璐?");
|
||||
}
|
||||
}
|
||||
|
||||
public Path resolveValidateSessionFile(String sessionId, String fileName) {
|
||||
return resolveValidateSessionUploadDir(sessionId).resolve(Paths.get(fileName).getFileName().toString()).normalize();
|
||||
}
|
||||
|
||||
public List<String> listValidateSessionFileNames(String sessionId) {
|
||||
Path uploadDir = resolveValidateSessionUploadDir(sessionId);
|
||||
if (!Files.exists(uploadDir) || !Files.isDirectory(uploadDir)) {
|
||||
return new ArrayList<String>();
|
||||
}
|
||||
try {
|
||||
List<String> result = new ArrayList<String>();
|
||||
Files.list(uploadDir)
|
||||
.filter(Files::isRegularFile)
|
||||
.forEach(path -> result.add(path.getFileName().toString()));
|
||||
return result;
|
||||
} catch (IOException exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "鏍¢獙鏂囦欢闃舵锛氳鍙栦細璇濇枃浠跺垪琛ㄥけ璐?");
|
||||
}
|
||||
}
|
||||
|
||||
public String readTempBatchMeta(String reportId, String batchId) {
|
||||
Path metaPath = resolveTempBatchMetaPath(reportId, batchId);
|
||||
if (!Files.exists(metaPath) || !Files.isRegularFile(metaPath)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new String(Files.readAllBytes(metaPath), StandardCharsets.UTF_8);
|
||||
} catch (IOException exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "璇诲彇瀵煎叆鎵规淇℃伅澶辫触");
|
||||
}
|
||||
}
|
||||
|
||||
public String saveLedgerSummaryFromTemp(String reportId, String batchId, String fileName) {
|
||||
return savePathFile(reportId, TestReportConst.STORAGE_LEDGER_SUMMARY_DIR,
|
||||
resolveTempBatchFile(reportId, batchId, fileName), fileName, "淇濆瓨鍙拌处姹囨€籈xcel澶辫触");
|
||||
}
|
||||
|
||||
public String savePointExcelFromTemp(String reportId, String batchId, String fileName) {
|
||||
return savePathFile(reportId, TestReportConst.STORAGE_POINT_EXCEL_DIR,
|
||||
resolveTempBatchFile(reportId, batchId, fileName), fileName, "淇濆瓨鐩戞祴鐐笶xcel闄勪欢澶辫触");
|
||||
}
|
||||
|
||||
public String savePointWordFromTemp(String reportId, String batchId, String fileName) {
|
||||
return savePathFile(reportId, TestReportConst.STORAGE_POINT_WORD_DIR,
|
||||
resolveTempBatchFile(reportId, batchId, fileName), fileName, "淇濆瓨鐩戞祴鐐筗ord闄勪欢澶辫触");
|
||||
}
|
||||
|
||||
public String saveGeneratedPlaceholder(String reportId, String fileName, byte[] content) {
|
||||
Path targetPath = resolveReportDir(reportId, TestReportConst.STORAGE_GENERATED_REPORT_DIR).resolve(fileName);
|
||||
try {
|
||||
Files.createDirectories(targetPath.getParent());
|
||||
Files.write(targetPath, content);
|
||||
} catch (IOException exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "淇濆瓨鐢熸垚鎶ュ憡澶辫触");
|
||||
}
|
||||
return buildRelativePath(reportId, TestReportConst.STORAGE_GENERATED_REPORT_DIR, fileName);
|
||||
}
|
||||
|
||||
public Path resolveBusinessPath(String relativePath) {
|
||||
return Paths.get(filesRootPath, TestReportConst.STORAGE_DIR).resolve(relativePath).normalize();
|
||||
}
|
||||
|
||||
public void deleteQuietly(String relativePath) {
|
||||
if (StrUtil.isBlank(relativePath)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Files.deleteIfExists(resolveBusinessPath(relativePath));
|
||||
} catch (IOException ignored) {
|
||||
// 鏂囦欢娓呯悊澶辫触涓嶅奖鍝嶄富娴佺▼
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteTempBatchQuietly(String reportId, String batchId) {
|
||||
if (StrUtil.isBlank(reportId) || StrUtil.isBlank(batchId)) {
|
||||
return;
|
||||
}
|
||||
deletePathQuietly(resolveTempBatchDir(reportId, batchId));
|
||||
}
|
||||
|
||||
private String saveMultipartFile(String reportId, String childDir, MultipartFile file, String errorMessage) {
|
||||
String originalFileName = file == null ? null : file.getOriginalFilename();
|
||||
if (file == null || file.isEmpty() || StrUtil.isBlank(originalFileName)) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "文件不能为空");
|
||||
}
|
||||
return savePathFile(reportId, childDir, toTempTransferredPath(file), originalFileName, errorMessage);
|
||||
}
|
||||
|
||||
private Path toTempTransferredPath(MultipartFile file) {
|
||||
try {
|
||||
Path tempFile = Files.createTempFile("test-report-", ".upload");
|
||||
file.transferTo(tempFile.toFile());
|
||||
return tempFile;
|
||||
} catch (IOException exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "文件上传失败");
|
||||
}
|
||||
}
|
||||
|
||||
private String savePathFile(String reportId, String childDir, Path sourcePath, String originalFileName, String errorMessage) {
|
||||
String normalizedFileName = originalFileName == null ? null : Paths.get(originalFileName).getFileName().toString();
|
||||
if (sourcePath == null || StrUtil.isBlank(normalizedFileName) || !Files.exists(sourcePath)) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "文件不能为空");
|
||||
}
|
||||
String fileName = UUID.randomUUID().toString().replace("-", "") + "-" + normalizedFileName;
|
||||
Path targetPath = resolveReportDir(reportId, childDir).resolve(fileName);
|
||||
try {
|
||||
Files.createDirectories(targetPath.getParent());
|
||||
Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, errorMessage);
|
||||
} finally {
|
||||
deletePathQuietlyIfTemp(sourcePath);
|
||||
}
|
||||
return buildRelativePath(reportId, childDir, fileName);
|
||||
}
|
||||
|
||||
private void deletePathQuietlyIfTemp(Path path) {
|
||||
if (path == null) {
|
||||
return;
|
||||
}
|
||||
String fileName = path.getFileName() == null ? null : path.getFileName().toString();
|
||||
if (fileName != null && fileName.startsWith("test-report-")) {
|
||||
deletePathQuietly(path);
|
||||
}
|
||||
}
|
||||
|
||||
private void deletePathQuietly(Path path) {
|
||||
if (path == null || !Files.exists(path)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (Files.isDirectory(path)) {
|
||||
Files.walk(path)
|
||||
.sorted((left, right) -> right.getNameCount() - left.getNameCount())
|
||||
.forEach(this::deleteSingleQuietly);
|
||||
return;
|
||||
}
|
||||
Files.deleteIfExists(path);
|
||||
} catch (IOException ignored) {
|
||||
// 涓存椂鐩綍娓呯悊澶辫触涓嶅奖鍝嶄富娴佺▼
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteSingleQuietly(Path path) {
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
} catch (IOException ignored) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
private Path resolveReportDir(String reportId, String childDir) {
|
||||
return Paths.get(filesRootPath, TestReportConst.STORAGE_DIR, reportId, childDir).normalize();
|
||||
}
|
||||
|
||||
private Path resolveTempBatchDir(String reportId, String batchId) {
|
||||
return Paths.get(filesRootPath, TestReportConst.STORAGE_DIR, reportId,
|
||||
TestReportConst.STORAGE_IMPORT_TEMP_DIR, batchId).normalize();
|
||||
}
|
||||
|
||||
private Path resolveTempUploadDir(String reportId, String batchId) {
|
||||
return resolveTempBatchDir(reportId, batchId).resolve(TestReportConst.STORAGE_IMPORT_TEMP_UPLOAD_DIR);
|
||||
}
|
||||
|
||||
private Path resolveTempBatchMetaPath(String reportId, String batchId) {
|
||||
return resolveTempBatchDir(reportId, batchId).resolve(TestReportConst.STORAGE_IMPORT_META_FILE_NAME);
|
||||
}
|
||||
|
||||
private Path resolveValidateSessionDir(String sessionId) {
|
||||
return Paths.get(filesRootPath, TestReportConst.STORAGE_DIR,
|
||||
TestReportConst.STORAGE_VALIDATE_SESSION_DIR, sessionId).normalize();
|
||||
}
|
||||
|
||||
private Path resolveValidateSessionUploadDir(String sessionId) {
|
||||
return resolveValidateSessionDir(sessionId).resolve(TestReportConst.STORAGE_IMPORT_TEMP_UPLOAD_DIR);
|
||||
}
|
||||
|
||||
private Path resolveValidateSessionResultPath(String sessionId) {
|
||||
return resolveValidateSessionDir(sessionId).resolve(TestReportConst.STORAGE_VALIDATE_RESULT_FILE_NAME);
|
||||
}
|
||||
|
||||
private String buildRelativePath(String reportId, String childDir, String fileName) {
|
||||
return reportId + "/" + childDir + "/" + fileName;
|
||||
}
|
||||
|
||||
private String buildTempBatchRelativePath(String reportId, String batchId) {
|
||||
return reportId + "/" + TestReportConst.STORAGE_IMPORT_TEMP_DIR + "/" + batchId;
|
||||
}
|
||||
|
||||
private String buildTempUploadRelativePath(String reportId, String batchId, String fileName) {
|
||||
return buildTempBatchRelativePath(reportId, batchId) + "/"
|
||||
+ TestReportConst.STORAGE_IMPORT_TEMP_UPLOAD_DIR + "/" + fileName;
|
||||
}
|
||||
|
||||
private String buildValidateSessionRelativePath(String sessionId) {
|
||||
return TestReportConst.STORAGE_VALIDATE_SESSION_DIR + "/" + sessionId;
|
||||
}
|
||||
|
||||
private String buildValidateSessionUploadRelativePath(String sessionId, String fileName) {
|
||||
return buildValidateSessionRelativePath(sessionId) + "/"
|
||||
+ TestReportConst.STORAGE_IMPORT_TEMP_UPLOAD_DIR + "/" + fileName;
|
||||
}
|
||||
|
||||
public static class UploadedBatch {
|
||||
private String batchId;
|
||||
private String tempStoragePath;
|
||||
private Map<String, String> storedFileMap = new LinkedHashMap<String, String>();
|
||||
|
||||
public String getBatchId() {
|
||||
return batchId;
|
||||
}
|
||||
|
||||
public void setBatchId(String batchId) {
|
||||
this.batchId = batchId;
|
||||
}
|
||||
|
||||
public String getTempStoragePath() {
|
||||
return tempStoragePath;
|
||||
}
|
||||
|
||||
public void setTempStoragePath(String tempStoragePath) {
|
||||
this.tempStoragePath = tempStoragePath;
|
||||
}
|
||||
|
||||
public Map<String, String> getStoredFileMap() {
|
||||
return storedFileMap;
|
||||
}
|
||||
|
||||
public void setStoredFileMap(Map<String, String> storedFileMap) {
|
||||
this.storedFileMap = storedFileMap;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
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.TestReportGroupSaveParam;
|
||||
import com.njcn.gather.aireport.testreport.pojo.param.TestReportParam;
|
||||
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportGroupReportVO;
|
||||
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportLedgerImportResultVO;
|
||||
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportPointVO;
|
||||
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO;
|
||||
import com.njcn.gather.aireport.testreport.service.TestReportService;
|
||||
import com.njcn.web.controller.BaseController;
|
||||
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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
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("/ledger/template")
|
||||
public ResponseEntity<byte[]> downloadLedgerTemplate() {
|
||||
return testReportService.downloadLedgerTemplate();
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
|
||||
@ApiOperation("校验台账汇总与监测点附件")
|
||||
@PostMapping("/{id}/ledger/validate")
|
||||
public HttpResult<TestReportLedgerImportResultVO> validateLedger(@PathVariable("id") String id,
|
||||
@RequestParam("files") MultipartFile[] files) {
|
||||
String methodDescribe = getMethodDescribe("validateLedger");
|
||||
TestReportLedgerImportResultVO result = testReportService.validateLedger(id, files);
|
||||
return HttpResultUtil.assembleCommonResponseResult(Boolean.TRUE.equals(result.getSuccess())
|
||||
? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
|
||||
result, methodDescribe);
|
||||
}
|
||||
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
|
||||
@ApiOperation("导入已校验通过的台账批次")
|
||||
@PostMapping("/{id}/ledger/import")
|
||||
public HttpResult<TestReportLedgerImportResultVO> importLedger(@PathVariable("id") String id,
|
||||
@RequestBody @Validated TestReportParam.LedgerImportParam param) {
|
||||
String methodDescribe = getMethodDescribe("importLedger");
|
||||
TestReportLedgerImportResultVO result = testReportService.importLedger(id, param);
|
||||
return HttpResultUtil.assembleCommonResponseResult(Boolean.TRUE.equals(result.getSuccess())
|
||||
? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
|
||||
result, methodDescribe);
|
||||
}
|
||||
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
|
||||
@ApiOperation("查询监测点列表")
|
||||
@GetMapping("/{id}/point/list")
|
||||
public HttpResult<List<TestReportPointVO>> listPoints(@PathVariable("id") String id) {
|
||||
String methodDescribe = getMethodDescribe("listPoints");
|
||||
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
|
||||
testReportService.listPoints(id), methodDescribe);
|
||||
}
|
||||
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
|
||||
@ApiOperation("保存监测点分组")
|
||||
@PostMapping("/{id}/group/save")
|
||||
public HttpResult<Boolean> saveGroups(@PathVariable("id") String id,
|
||||
@RequestBody @Validated TestReportGroupSaveParam param) {
|
||||
String methodDescribe = getMethodDescribe("saveGroups");
|
||||
boolean result = testReportService.saveGroups(id, param);
|
||||
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
|
||||
result, methodDescribe);
|
||||
}
|
||||
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
|
||||
@ApiOperation("查询分组报告结果")
|
||||
@GetMapping("/{id}/group-report/list")
|
||||
public HttpResult<List<TestReportGroupReportVO>> listGroupReports(@PathVariable("id") String id) {
|
||||
String methodDescribe = getMethodDescribe("listGroupReports");
|
||||
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
|
||||
testReportService.listGroupReports(id), methodDescribe);
|
||||
}
|
||||
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
|
||||
@ApiOperation("按分组生成报告")
|
||||
@PostMapping("/{id}/generate")
|
||||
public HttpResult<Boolean> generateGroupedReports(@PathVariable("id") String id) {
|
||||
String methodDescribe = getMethodDescribe("generateGroupedReports");
|
||||
boolean result = testReportService.generateGroupedReports(id);
|
||||
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
|
||||
result, methodDescribe);
|
||||
}
|
||||
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
|
||||
@ApiOperation("下载分组报告")
|
||||
@ApiImplicitParam(name = "groupReportId", value = "分组报告结果ID", required = true)
|
||||
@GetMapping("/group-report/{groupReportId}/download")
|
||||
public ResponseEntity<byte[]> downloadGroupReport(@PathVariable("groupReportId") String groupReportId) {
|
||||
return testReportService.downloadGroupReport(groupReportId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.njcn.gather.aireport.testreport.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.njcn.gather.aireport.testreport.pojo.po.TestReportGroupReportPO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface TestReportGroupReportMapper extends BaseMapper<TestReportGroupReportPO> {
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user