feat(ai-report): 新增委托单位管理和报告审批功能

- 新增委托单位管理模块,支持增删改查、导入导出功能
- 实现委托单位 Excel 模板下载和数据导入功能
- 添加报告审批抽象处理器,支持审批流程日志记录
- 统一文件存储路径配置,将各模块独立路径改为统一根目录
- 新增 PQDIF 文件名存储字段,完善文件管理功能
- 修复代码中的乱码问题和错误提示信息
- 优化系统常量定义和字典配置管理
This commit is contained in:
2026-07-01 13:15:57 +08:00
parent 0ea686d3cb
commit c1ad7feec2
99 changed files with 6511 additions and 24 deletions

View File

@@ -18,7 +18,9 @@
- 只清理自己造成的问题:可以删除因本次修改而产生的未使用 `import`、变量或方法;不要删除仓库中原本就存在的死代码,除非用户明确要求。
- 先定义验证方式:执行方案里要写清楚“改哪里、怎么判断改对了”;默认通过代码路径、配置一致性和受影响范围检查进行验证。
- 除非用户明确要求,否则不执行任何 `mvn` 编译、打包、测试或其他构建命令。
- 系统中所有无数据项默认显示 `-`;新增或修改列表、详情、导出、汇总等展示逻辑时,若字段无值,统一按该约定处理,避免空白、`null` 或其他占位写法并存。
- 所有导出或生成文件名统一追加日期,格式为“原文件名 + `_` + `yyyyMMdd` + 原扩展名”,例如 `暂态事件列表_20260516.xlsx`;新增下载导出、临时生成或落盘保存文件时,优先复用目标模块已有文件名工具,不要在业务代码中散落手写日期拼接。
- 后端本地文件存储统一使用 `files.path` 作为根目录配置;各业务模块只能在该根目录下追加自己的固定子目录,例如 PQDIF 文件使用 `pqdif-files`,报告模板使用 `report-model`,检测设备校准报告使用 `test-device-report`,检测报告相关文件使用 `test-report`。不要再新增 `parse-pqdif.storage.path``report-model.storage.path``test-device.report.storage.path` 这类模块级根路径配置。
## 项目结构与模块划分
`CN_Tool` 是一个 Maven 多模块后端项目,根目录的 [`pom.xml`](C:/code/gitea/cn_tool/CN_Tool/pom.xml) 聚合了 `entrance``system``user``detection``tools`

View File

@@ -11,6 +11,7 @@
</parent>
<artifactId>client-unit</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
@@ -30,5 +31,27 @@
<artifactId>spingboot2.3.12</artifactId>
<version>2.3.12</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>system</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,41 @@
package com.njcn.gather.aireport.clientunit.component;
import cn.hutool.core.util.StrUtil;
import com.njcn.gather.aireport.clientunit.mapper.ClientUnitMapper;
import com.njcn.gather.aireport.clientunit.pojo.vo.ClientUnitUserNameVO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Component
@RequiredArgsConstructor
public class ClientUnitUserNameResolver {
private final ClientUnitMapper clientUnitMapper;
public Map<String, String> resolveUserNames(Collection<String> userIds) {
if (userIds == null || userIds.isEmpty()) {
return Collections.emptyMap();
}
Set<String> validIds = userIds.stream()
.filter(StrUtil::isNotBlank)
.collect(Collectors.toCollection(LinkedHashSet::new));
if (validIds.isEmpty()) {
return Collections.emptyMap();
}
List<ClientUnitUserNameVO> users = clientUnitMapper.selectUserNamesByIds(validIds);
if (users == null || users.isEmpty()) {
return Collections.emptyMap();
}
return users.stream()
.filter(user -> user != null && StrUtil.isNotBlank(user.getId()) && StrUtil.isNotBlank(user.getName()))
.collect(Collectors.toMap(ClientUnitUserNameVO::getId, ClientUnitUserNameVO::getName, (first, second) -> first));
}
}

View File

@@ -0,0 +1,112 @@
package com.njcn.gather.aireport.clientunit.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.constant.OperateType;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.gather.aireport.clientunit.pojo.param.ClientUnitParam;
import com.njcn.gather.aireport.clientunit.pojo.vo.ClientUnitImportResultVO;
import com.njcn.gather.aireport.clientunit.pojo.vo.ClientUnitVO;
import com.njcn.gather.aireport.clientunit.service.IClientUnitService;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.HttpResultUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* 委托单位管理接口。
*/
@Api(tags = "委托单位管理")
@RestController
@RequestMapping("/clientUnit")
@RequiredArgsConstructor
public class ClientUnitController extends BaseController {
private final IClientUnitService clientUnitService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询委托单位列表")
@PostMapping("/list")
public HttpResult<Page<ClientUnitVO>> list(@RequestBody(required = false) ClientUnitParam.QueryParam param) {
String methodDescribe = getMethodDescribe("list");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
clientUnitService.listClientUnits(param), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD)
@ApiOperation("新增委托单位")
@PostMapping("/add")
public HttpResult<Boolean> add(@RequestBody @Validated ClientUnitParam.AddParam param) {
String methodDescribe = getMethodDescribe("add");
boolean result = clientUnitService.addClientUnit(param);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
@ApiOperation("编辑委托单位")
@PostMapping("/update")
public HttpResult<Boolean> update(@RequestBody @Validated ClientUnitParam.UpdateParam param) {
String methodDescribe = getMethodDescribe("update");
boolean result = clientUnitService.updateClientUnit(param);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询委托单位详情")
@ApiImplicitParam(name = "id", value = "委托单位ID", required = true)
@GetMapping("/getById")
public HttpResult<ClientUnitVO> getById(@RequestParam("id") String id) {
String methodDescribe = getMethodDescribe("getById");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
clientUnitService.getClientUnit(id), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DELETE)
@ApiOperation("删除委托单位")
@PostMapping("/delete")
public HttpResult<Boolean> delete(@RequestBody List<String> ids) {
String methodDescribe = getMethodDescribe("delete");
boolean result = clientUnitService.deleteClientUnits(ids);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("导出委托单位")
@PostMapping("/export")
public void export(@RequestBody(required = false) ClientUnitParam.QueryParam param) {
clientUnitService.exportClientUnits(param);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("下载委托单位导入模板")
@GetMapping("/importTemplate")
public void importTemplate() {
clientUnitService.downloadImportTemplate();
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD)
@ApiOperation("导入委托单位")
@PostMapping(value = "/import", consumes = {"multipart/form-data"})
public HttpResult<ClientUnitImportResultVO> importExcel(@RequestParam("file") MultipartFile file) {
String methodDescribe = getMethodDescribe("importExcel");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
clientUnitService.importClientUnits(file), methodDescribe);
}
}

View File

@@ -0,0 +1,25 @@
package com.njcn.gather.aireport.clientunit.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.njcn.gather.aireport.clientunit.pojo.po.ClientUnitPO;
import com.njcn.gather.aireport.clientunit.pojo.vo.ClientUnitUserNameVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.Collection;
import java.util.List;
@Mapper
public interface ClientUnitMapper extends BaseMapper<ClientUnitPO> {
@Select({
"<script>",
"select id, name from sys_user where id in",
"<foreach collection='ids' item='id' open='(' separator=',' close=')'>",
"#{id}",
"</foreach>",
"</script>"
})
List<ClientUnitUserNameVO> selectUserNamesByIds(@Param("ids") Collection<String> ids);
}

View File

@@ -0,0 +1,13 @@
package com.njcn.gather.aireport.clientunit.pojo.constant;
/**
* 委托单位业务常量。
*/
public final class ClientUnitConst {
public static final int STATUS_DELETED = 0;
public static final int STATUS_NORMAL = 1;
private ClientUnitConst() {
}
}

View File

@@ -0,0 +1,55 @@
package com.njcn.gather.aireport.clientunit.pojo.param;
import com.njcn.web.pojo.param.BaseParam;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
/**
* 委托单位请求参数。
*/
public class ClientUnitParam {
@Data
@EqualsAndHashCode(callSuper = true)
public static class QueryParam extends BaseParam {
@ApiModelProperty("单位名称关键字")
private String name;
@ApiModelProperty("创建开始时间yyyy-MM-dd HH:mm:ss")
private String searchBeginTime;
@ApiModelProperty("创建结束时间yyyy-MM-dd HH:mm:ss")
private String searchEndTime;
}
@Data
public static class AddParam {
@ApiModelProperty("单位名称")
@NotBlank(message = "单位名称不能为空")
@Size(max = 32, message = "单位名称不能超过32个字符")
private String name;
@ApiModelProperty("联系人")
@Size(max = 200, message = "联系人不能超过200个字符")
private String contactName;
@ApiModelProperty("联系方式")
@Size(max = 32, message = "联系方式不能超过32个字符")
private String phonenumber;
@ApiModelProperty("地址")
@Size(max = 128, message = "地址不能超过128个字符")
private String address;
}
@Data
public static class UpdateParam extends AddParam {
@ApiModelProperty("委托单位ID")
@NotBlank(message = "委托单位ID不能为空")
private String id;
}
}

View File

@@ -0,0 +1,39 @@
package com.njcn.gather.aireport.clientunit.pojo.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 委托单位记录。
*/
@Data
@TableName("ai_clientunit")
public class ClientUnitPO implements Serializable {
private static final long serialVersionUID = 2517699918054153742L;
@TableId("id")
private String id;
@TableField("name")
private String name;
@TableField("contact_name")
private String contactName;
@TableField("phonenumber")
private String phonenumber;
@TableField("address")
private String address;
@TableField("status")
private Integer status;
@TableField("create_by")
private String createBy;
@TableField("create_time")
private LocalDateTime createTime;
@TableField("update_by")
private String updateBy;
@TableField("update_time")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,26 @@
package com.njcn.gather.aireport.clientunit.pojo.vo;
import cn.afterturn.easypoi.excel.annotation.Excel;
import lombok.Data;
import java.io.Serializable;
/**
* 委托单位 Excel 行对象。
*/
@Data
public class ClientUnitExcelVO implements Serializable {
private static final long serialVersionUID = 8533457988358235671L;
@Excel(name = "单位名称", width = 25)
private String name;
@Excel(name = "联系人", width = 20)
private String contactName;
@Excel(name = "联系方式", width = 20)
private String phonenumber;
@Excel(name = "地址", width = 35)
private String address;
}

View File

@@ -0,0 +1,19 @@
package com.njcn.gather.aireport.clientunit.pojo.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* 委托单位导入结果。
*/
@Data
public class ClientUnitImportResultVO implements Serializable {
private static final long serialVersionUID = 509902603493571405L;
private int successCount;
private int failCount;
private List<String> failReasons = new ArrayList<String>();
}

View File

@@ -0,0 +1,9 @@
package com.njcn.gather.aireport.clientunit.pojo.vo;
import lombok.Data;
@Data
public class ClientUnitUserNameVO {
private String id;
private String name;
}

View File

@@ -0,0 +1,69 @@
package com.njcn.gather.aireport.clientunit.pojo.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import lombok.Data;
import org.springframework.util.StringUtils;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 委托单位展示对象。
*/
@Data
public class ClientUnitVO implements Serializable {
private static final long serialVersionUID = 8924649462626990262L;
private static final String DEFAULT_EMPTY_DISPLAY = "-";
private String id;
private String name;
private String contactName;
private String phonenumber;
private String address;
private String createBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime createTime;
private String updateBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime updateTime;
public String getId() {
return defaultDisplay(id);
}
public String getName() {
return defaultDisplay(name);
}
public String getContactName() {
return defaultDisplay(contactName);
}
public String getPhonenumber() {
return defaultDisplay(phonenumber);
}
public String getAddress() {
return defaultDisplay(address);
}
public String getCreateBy() {
return defaultDisplay(createBy);
}
public String getUpdateBy() {
return defaultDisplay(updateBy);
}
private String defaultDisplay(String value) {
return StringUtils.hasText(value) ? value : DEFAULT_EMPTY_DISPLAY;
}
}

View File

@@ -0,0 +1,30 @@
package com.njcn.gather.aireport.clientunit.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.gather.aireport.clientunit.pojo.param.ClientUnitParam;
import com.njcn.gather.aireport.clientunit.pojo.po.ClientUnitPO;
import com.njcn.gather.aireport.clientunit.pojo.vo.ClientUnitImportResultVO;
import com.njcn.gather.aireport.clientunit.pojo.vo.ClientUnitVO;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
public interface IClientUnitService extends IService<ClientUnitPO> {
Page<ClientUnitVO> listClientUnits(ClientUnitParam.QueryParam param);
boolean addClientUnit(ClientUnitParam.AddParam param);
boolean updateClientUnit(ClientUnitParam.UpdateParam param);
ClientUnitVO getClientUnit(String id);
boolean deleteClientUnits(List<String> ids);
void exportClientUnits(ClientUnitParam.QueryParam param);
void downloadImportTemplate();
ClientUnitImportResultVO importClientUnits(MultipartFile file);
}

View File

@@ -0,0 +1,437 @@
package com.njcn.gather.aireport.clientunit.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.clientunit.component.ClientUnitUserNameResolver;
import com.njcn.gather.aireport.clientunit.mapper.ClientUnitMapper;
import com.njcn.gather.aireport.clientunit.pojo.constant.ClientUnitConst;
import com.njcn.gather.aireport.clientunit.pojo.param.ClientUnitParam;
import com.njcn.gather.aireport.clientunit.pojo.po.ClientUnitPO;
import com.njcn.gather.aireport.clientunit.pojo.vo.ClientUnitExcelVO;
import com.njcn.gather.aireport.clientunit.pojo.vo.ClientUnitImportResultVO;
import com.njcn.gather.aireport.clientunit.pojo.vo.ClientUnitVO;
import com.njcn.gather.aireport.clientunit.service.IClientUnitService;
import com.njcn.gather.system.util.ExportFileNameUtil;
import com.njcn.web.factory.PageFactory;
import com.njcn.web.utils.ExcelUtil;
import com.njcn.web.utils.HttpServletUtil;
import com.njcn.web.utils.RequestUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* 委托单位业务实现。
*/
@Slf4j
@Service
public class ClientUnitServiceImpl extends ServiceImpl<ClientUnitMapper, ClientUnitPO> implements IClientUnitService {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final String[] TEMPLATE_HEADERS = new String[]{"单位名称", "联系人", "联系方式", "地址"};
private final ClientUnitUserNameResolver clientUnitUserNameResolver;
public ClientUnitServiceImpl(ClientUnitUserNameResolver clientUnitUserNameResolver) {
this.clientUnitUserNameResolver = clientUnitUserNameResolver;
}
@Override
public Page<ClientUnitVO> listClientUnits(ClientUnitParam.QueryParam param) {
ClientUnitParam.QueryParam query = param == null ? new ClientUnitParam.QueryParam() : param;
Page<ClientUnitPO> page = this.page(
new Page<ClientUnitPO>(PageFactory.getPageNum(query), PageFactory.getPageSize(query)),
buildQueryWrapper(query));
Page<ClientUnitVO> result = new Page<ClientUnitVO>(page.getCurrent(), page.getSize(), page.getTotal());
Map<String, String> userNameMap = resolveUserNameMap(page.getRecords());
result.setRecords(page.getRecords().stream()
.map(clientUnit -> toVO(clientUnit, userNameMap))
.collect(Collectors.toList()));
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean addClientUnit(ClientUnitParam.AddParam param) {
NormalizedClientUnit data = normalizeParam(param);
LocalDateTime now = LocalDateTime.now();
String userId = resolveCurrentUserId();
ClientUnitPO clientUnit = new ClientUnitPO();
clientUnit.setId(UUID.randomUUID().toString());
clientUnit.setName(data.getName());
clientUnit.setContactName(data.getContactName());
clientUnit.setPhonenumber(data.getPhonenumber());
clientUnit.setAddress(data.getAddress());
clientUnit.setStatus(ClientUnitConst.STATUS_NORMAL);
clientUnit.setCreateBy(userId);
clientUnit.setCreateTime(now);
clientUnit.setUpdateBy(userId);
clientUnit.setUpdateTime(now);
return this.save(clientUnit);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateClientUnit(ClientUnitParam.UpdateParam param) {
if (param == null || StrUtil.isBlank(param.getId())) {
throw new BusinessException(CommonResponseEnum.FAIL, "委托单位ID不能为空");
}
ClientUnitPO clientUnit = requireNormal(param.getId());
NormalizedClientUnit data = normalizeParam(param);
checkDuplicate(data.getName(), clientUnit.getId());
clientUnit.setName(data.getName());
clientUnit.setContactName(data.getContactName());
clientUnit.setPhonenumber(data.getPhonenumber());
clientUnit.setAddress(data.getAddress());
clientUnit.setUpdateBy(resolveCurrentUserId());
clientUnit.setUpdateTime(LocalDateTime.now());
return this.updateById(clientUnit);
}
@Override
public ClientUnitVO getClientUnit(String id) {
ClientUnitPO clientUnit = requireNormal(id);
return toVO(clientUnit, resolveUserNameMap(Arrays.asList(clientUnit)));
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteClientUnits(List<String> ids) {
List<String> validIds = normalizeIds(ids);
if (validIds.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "委托单位ID不能为空");
}
return this.lambdaUpdate()
.set(ClientUnitPO::getStatus, ClientUnitConst.STATUS_DELETED)
.set(ClientUnitPO::getUpdateBy, resolveCurrentUserId())
.set(ClientUnitPO::getUpdateTime, LocalDateTime.now())
.in(ClientUnitPO::getId, validIds)
.eq(ClientUnitPO::getStatus, ClientUnitConst.STATUS_NORMAL)
.update();
}
@Override
public void exportClientUnits(ClientUnitParam.QueryParam param) {
List<ClientUnitExcelVO> exportRows = this.list(buildQueryWrapper(param == null ? new ClientUnitParam.QueryParam() : param))
.stream()
.map(this::toExcelVO)
.collect(Collectors.toList());
ExcelUtil.exportExcel(ExportFileNameUtil.appendToday("委托单位列表.xlsx"), "委托单位列表", ClientUnitExcelVO.class, exportRows);
}
@Override
public void downloadImportTemplate() {
HttpServletResponse response = HttpServletUtil.getResponse();
try (XSSFWorkbook workbook = new XSSFWorkbook()) {
XSSFSheet sheet = workbook.createSheet("委托单位导入模板");
writeTemplateHeader(workbook, sheet);
String fileName = URLEncoder.encode(ExportFileNameUtil.appendToday("委托单位导入模板.xlsx"), "UTF-8");
response.reset();
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
try (ServletOutputStream outputStream = response.getOutputStream()) {
workbook.write(outputStream);
outputStream.flush();
}
} catch (IOException exception) {
log.error("下载委托单位导入模板失败", exception);
throw new BusinessException(CommonResponseEnum.FAIL, "下载委托单位导入模板失败");
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public ClientUnitImportResultVO importClientUnits(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "导入文件不能为空");
}
List<ClientUnitExcelVO> rows = readImportRows(file);
ClientUnitImportResultVO result = new ClientUnitImportResultVO();
if (rows.isEmpty()) {
result.setFailCount(0);
result.setSuccessCount(0);
return result;
}
LocalDateTime now = LocalDateTime.now();
String userId = resolveCurrentUserId();
Set<String> fileKeys = new HashSet<String>();
List<ClientUnitPO> saveRows = new ArrayList<ClientUnitPO>();
for (int i = 0; i < rows.size(); i++) {
int rowNumber = i + 2;
try {
NormalizedClientUnit data = normalizeExcelRow(rows.get(i));
String duplicateKey = data.getName();
if (!fileKeys.add(duplicateKey)) {
throw new BusinessException(CommonResponseEnum.FAIL, "文件内存在重复委托单位名称");
}
checkDuplicate(data.getName(), null);
ClientUnitPO clientUnit = new ClientUnitPO();
clientUnit.setId(UUID.randomUUID().toString());
clientUnit.setName(data.getName());
clientUnit.setContactName(data.getContactName());
clientUnit.setPhonenumber(data.getPhonenumber());
clientUnit.setAddress(data.getAddress());
clientUnit.setStatus(ClientUnitConst.STATUS_NORMAL);
clientUnit.setCreateBy(userId);
clientUnit.setCreateTime(now);
clientUnit.setUpdateBy(userId);
clientUnit.setUpdateTime(now);
saveRows.add(clientUnit);
result.setSuccessCount(result.getSuccessCount() + 1);
} catch (BusinessException exception) {
result.setFailCount(result.getFailCount() + 1);
result.getFailReasons().add("" + rowNumber + "行:" + exception.getMessage());
}
}
if (!saveRows.isEmpty()) {
this.saveBatch(saveRows);
}
return result;
}
private LambdaQueryWrapper<ClientUnitPO> buildQueryWrapper(ClientUnitParam.QueryParam query) {
LambdaQueryWrapper<ClientUnitPO> wrapper = new LambdaQueryWrapper<ClientUnitPO>();
wrapper.eq(ClientUnitPO::getStatus, ClientUnitConst.STATUS_NORMAL)
.like(StrUtil.isNotBlank(query.getName()), ClientUnitPO::getName, query.getName());
if (StrUtil.isNotBlank(query.getSearchBeginTime())) {
wrapper.ge(ClientUnitPO::getCreateTime, parseDateTime(query.getSearchBeginTime(), "开始时间格式不正确"));
}
if (StrUtil.isNotBlank(query.getSearchEndTime())) {
wrapper.le(ClientUnitPO::getCreateTime, parseDateTime(query.getSearchEndTime(), "结束时间格式不正确"));
}
wrapper.orderByDesc(ClientUnitPO::getCreateTime);
return wrapper;
}
private ClientUnitPO requireNormal(String id) {
String normalizedId = trimToNull(id);
if (normalizedId == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "委托单位ID不能为空");
}
ClientUnitPO clientUnit = this.lambdaQuery()
.eq(ClientUnitPO::getId, normalizedId)
.eq(ClientUnitPO::getStatus, ClientUnitConst.STATUS_NORMAL)
.one();
if (clientUnit == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "委托单位不存在或已删除");
}
return clientUnit;
}
private void checkDuplicate(String name, String excludeId) {
LambdaQueryWrapper<ClientUnitPO> wrapper = new LambdaQueryWrapper<ClientUnitPO>();
wrapper.eq(ClientUnitPO::getName, name)
.eq(ClientUnitPO::getStatus, ClientUnitConst.STATUS_NORMAL)
.ne(StrUtil.isNotBlank(excludeId), ClientUnitPO::getId, excludeId);
if (this.count(wrapper) > 0) {
throw new BusinessException(CommonResponseEnum.FAIL, "委托单位名称已存在");
}
}
private NormalizedClientUnit normalizeParam(ClientUnitParam.AddParam param) {
if (param == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "委托单位参数不能为空");
}
return normalizeFields(param.getName(), param.getContactName(), param.getPhonenumber(), param.getAddress());
}
private NormalizedClientUnit normalizeExcelRow(ClientUnitExcelVO row) {
if (row == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "空行");
}
return normalizeFields(row.getName(), row.getContactName(), row.getPhonenumber(), row.getAddress());
}
private NormalizedClientUnit normalizeFields(String name, String contactName, String phonenumber, String address) {
NormalizedClientUnit data = new NormalizedClientUnit();
data.setName(requireText(name, "单位名称不能为空", 32, "单位名称不能超过32个字符"));
data.setContactName(optionalText(contactName, 200, "联系人不能超过200个字符"));
data.setPhonenumber(optionalText(phonenumber, 32, "联系方式不能超过32个字符"));
data.setAddress(optionalText(address, 128, "地址不能超过128个字符"));
return data;
}
private String requireText(String value, String blankMessage, int maxLength, String lengthMessage) {
String text = trimToNull(value);
if (text == null) {
throw new BusinessException(CommonResponseEnum.FAIL, blankMessage);
}
if (text.length() > maxLength) {
throw new BusinessException(CommonResponseEnum.FAIL, lengthMessage);
}
return text;
}
private String optionalText(String value, int maxLength, String lengthMessage) {
String text = trimToNull(value);
if (text != null && text.length() > maxLength) {
throw new BusinessException(CommonResponseEnum.FAIL, lengthMessage);
}
return text;
}
private List<ClientUnitExcelVO> readImportRows(MultipartFile file) {
try (InputStream inputStream = file.getInputStream();
XSSFWorkbook workbook = new XSSFWorkbook(inputStream)) {
XSSFSheet sheet = workbook.getNumberOfSheets() == 0 ? null : workbook.getSheetAt(0);
if (sheet == null || sheet.getLastRowNum() < 1) {
return Collections.emptyList();
}
List<ClientUnitExcelVO> rows = new ArrayList<ClientUnitExcelVO>();
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
if (isEmptyRow(row)) {
continue;
}
ClientUnitExcelVO excelVO = new ClientUnitExcelVO();
excelVO.setName(readCell(row, 0));
excelVO.setContactName(readCell(row, 1));
excelVO.setPhonenumber(readCell(row, 2));
excelVO.setAddress(readCell(row, 3));
rows.add(excelVO);
}
return rows;
} catch (IOException exception) {
log.error("读取委托单位导入文件失败", exception);
throw new BusinessException(CommonResponseEnum.FAIL, "读取导入文件失败");
}
}
private void writeTemplateHeader(XSSFWorkbook workbook, XSSFSheet sheet) {
CellStyle style = workbook.createCellStyle();
Font font = workbook.createFont();
font.setBold(true);
style.setFont(font);
Row row = sheet.createRow(0);
for (int i = 0; i < TEMPLATE_HEADERS.length; i++) {
Cell cell = row.createCell(i);
cell.setCellValue(TEMPLATE_HEADERS[i]);
cell.setCellStyle(style);
sheet.setColumnWidth(i, 25 * 256);
}
}
private boolean isEmptyRow(Row row) {
if (row == null) {
return true;
}
for (int i = 0; i < TEMPLATE_HEADERS.length; i++) {
if (trimToNull(readCell(row, i)) != null) {
return false;
}
}
return true;
}
private String readCell(Row row, int index) {
Cell cell = row.getCell(index);
if (cell == null) {
return null;
}
return trimToNull(new DataFormatter().formatCellValue(cell));
}
private List<String> normalizeIds(List<String> ids) {
if (ids == null || ids.isEmpty()) {
return Collections.emptyList();
}
List<String> result = new ArrayList<String>();
for (String id : ids) {
String normalizedId = trimToNull(id);
if (normalizedId != null && !result.contains(normalizedId)) {
result.add(normalizedId);
}
}
return result;
}
private LocalDateTime parseDateTime(String value, String errorMessage) {
try {
return LocalDateTime.parse(value.trim(), DATE_TIME_FORMATTER);
} catch (DateTimeParseException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, errorMessage);
}
}
private ClientUnitVO toVO(ClientUnitPO clientUnit, Map<String, String> userNameMap) {
ClientUnitVO vo = new ClientUnitVO();
BeanUtil.copyProperties(clientUnit, vo);
vo.setCreateBy(resolveUserName(clientUnit.getCreateBy(), userNameMap));
vo.setUpdateBy(resolveUserName(clientUnit.getUpdateBy(), userNameMap));
return vo;
}
private ClientUnitExcelVO toExcelVO(ClientUnitPO clientUnit) {
ClientUnitExcelVO vo = new ClientUnitExcelVO();
BeanUtil.copyProperties(clientUnit, vo);
return vo;
}
private Map<String, String> resolveUserNameMap(List<ClientUnitPO> clientUnits) {
List<String> userIds = clientUnits.stream()
.flatMap(clientUnit -> Arrays.asList(clientUnit.getCreateBy(), clientUnit.getUpdateBy()).stream())
.filter(StrUtil::isNotBlank)
.collect(Collectors.toList());
return clientUnitUserNameResolver.resolveUserNames(userIds);
}
private String resolveUserName(String userId, Map<String, String> userNameMap) {
if (StrUtil.isBlank(userId)) {
return null;
}
String userName = userNameMap == null ? null : userNameMap.get(userId);
return StrUtil.isNotBlank(userName) ? userName : userId;
}
private String resolveCurrentUserId() {
String userId = RequestUtil.getUserId();
return StrUtil.isBlank(userId) ? null : userId;
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String text = value.trim();
return text.isEmpty() ? null : text;
}
@lombok.Data
private static class NormalizedClientUnit {
private String name;
private String contactName;
private String phonenumber;
private String address;
}
}

View File

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

View File

@@ -20,6 +20,7 @@
<module>client-unit</module>
<module>test-device</module>
<module>test-report</module>
<module>report-approval</module>
</modules>
</project>

View File

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

View File

@@ -0,0 +1,41 @@
package com.njcn.gather.aireport.reportapproval.component;
import cn.hutool.core.util.StrUtil;
import com.njcn.gather.aireport.reportapproval.mapper.ReportApprovalMapper;
import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelUserNameVO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Component
@RequiredArgsConstructor
public class ReportApprovalUserNameResolver {
private final ReportApprovalMapper reportApprovalMapper;
public Map<String, String> resolveUserNames(Collection<String> userIds) {
if (userIds == null || userIds.isEmpty()) {
return Collections.emptyMap();
}
Set<String> validIds = userIds.stream()
.filter(StrUtil::isNotBlank)
.collect(Collectors.toCollection(LinkedHashSet::new));
if (validIds.isEmpty()) {
return Collections.emptyMap();
}
List<ReportModelUserNameVO> users = reportApprovalMapper.selectUserNamesByIds(validIds);
if (users == null || users.isEmpty()) {
return Collections.emptyMap();
}
return users.stream()
.filter(user -> user != null && StrUtil.isNotBlank(user.getId()) && StrUtil.isNotBlank(user.getName()))
.collect(Collectors.toMap(ReportModelUserNameVO::getId, ReportModelUserNameVO::getName, (first, second) -> first));
}
}

View File

@@ -0,0 +1,71 @@
package com.njcn.gather.aireport.reportapproval.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.constant.OperateType;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.gather.aireport.reportapproval.pojo.param.ReportApprovalParam;
import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalLogVO;
import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalPendingVO;
import com.njcn.gather.aireport.reportapproval.service.ReportApprovalService;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.HttpResultUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Api(tags = "报告审批管理")
@RestController
@RequestMapping("/api/report-approval")
@RequiredArgsConstructor
public class ReportApprovalController extends BaseController {
private final ReportApprovalService reportApprovalService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询待审报告分页")
@GetMapping("/pending-page")
public HttpResult<Page<ReportApprovalPendingVO>> pendingPage(@ModelAttribute ReportApprovalParam.PendingQueryParam param) {
String methodDescribe = getMethodDescribe("pendingPage");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
reportApprovalService.pendingPage(param), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
@ApiOperation("审批通过")
@PostMapping("/pass")
public HttpResult<Boolean> pass(@RequestBody @Validated ReportApprovalParam.AuditParam param) {
String methodDescribe = getMethodDescribe("pass");
boolean result = reportApprovalService.pass(param);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
@ApiOperation("审批退回")
@PostMapping("/reject")
public HttpResult<Boolean> reject(@RequestBody @Validated ReportApprovalParam.AuditParam param) {
String methodDescribe = getMethodDescribe("reject");
boolean result = reportApprovalService.reject(param);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询审批日志分页")
@GetMapping("/log-page")
public HttpResult<Page<ReportApprovalLogVO>> logPage(@ModelAttribute ReportApprovalParam.LogQueryParam param) {
String methodDescribe = getMethodDescribe("logPage");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
reportApprovalService.logPage(param), methodDescribe);
}
}

View File

@@ -0,0 +1,85 @@
package com.njcn.gather.aireport.reportapproval.handler;
import cn.hutool.core.util.StrUtil;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.reportapproval.component.ReportApprovalUserNameResolver;
import com.njcn.gather.aireport.reportapproval.mapper.ReportApprovalMapper;
import com.njcn.gather.aireport.reportapproval.pojo.constant.ReportApprovalConst;
import com.njcn.gather.aireport.reportapproval.pojo.param.ReportApprovalParam;
import com.njcn.gather.aireport.reportapproval.pojo.po.ReportApprovalLogPO;
import com.njcn.web.utils.RequestUtil;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Map;
import java.util.UUID;
public abstract class AbstractReportApprovalHandler implements ReportApprovalHandler {
private final ReportApprovalMapper reportApprovalMapper;
private final ReportApprovalUserNameResolver reportApprovalUserNameResolver;
protected AbstractReportApprovalHandler(ReportApprovalMapper reportApprovalMapper,
ReportApprovalUserNameResolver reportApprovalUserNameResolver) {
this.reportApprovalMapper = reportApprovalMapper;
this.reportApprovalUserNameResolver = reportApprovalUserNameResolver;
}
@Override
public boolean audit(ReportApprovalParam.AuditParam param, boolean passed) {
if (param == null || StrUtil.isBlank(param.getReportId())) {
throw new BusinessException(CommonResponseEnum.FAIL, "审批参数不能为空");
}
String reason = trimToNull(param.getApprovalReason());
if (reason == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "审批原因不能为空");
}
return doAudit(param.getReportId(), reason, passed);
}
protected abstract boolean doAudit(String reportId, String reason, boolean passed);
protected void saveLog(String reportId, String reportName, String beforeState, String afterState,
String reason, String approverId, LocalDateTime approvalTime) {
ReportApprovalLogPO log = new ReportApprovalLogPO();
log.setId(UUID.randomUUID().toString());
log.setReportType(getReportType());
log.setReportId(reportId);
log.setReportName(reportName);
log.setApprovalResult(afterState);
log.setApprovalReason(reason);
log.setBeforeState(beforeState);
log.setAfterState(afterState);
log.setApproverId(approverId);
log.setApproverName(resolveUserName(approverId));
log.setApprovalTime(approvalTime);
log.setStatus(ReportApprovalConst.STATUS_NORMAL);
log.setCreateBy(approverId);
log.setCreateTime(approvalTime);
log.setUpdateBy(approverId);
log.setUpdateTime(approvalTime);
reportApprovalMapper.insert(log);
}
protected String resolveCurrentUserId() {
String userId = RequestUtil.getUserId();
return StrUtil.isBlank(userId) ? null : userId;
}
private String resolveUserName(String userId) {
if (StrUtil.isBlank(userId)) {
return null;
}
Map<String, String> userNameMap = reportApprovalUserNameResolver.resolveUserNames(Arrays.asList(userId));
return userNameMap.containsKey(userId) ? userNameMap.get(userId) : userId;
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String text = value.trim();
return text.isEmpty() ? null : text;
}
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.aireport.reportapproval.handler;
import com.njcn.gather.aireport.reportapproval.pojo.param.ReportApprovalParam;
public interface ReportApprovalHandler {
String getReportType();
boolean audit(ReportApprovalParam.AuditParam param, boolean passed);
}

View File

@@ -0,0 +1,35 @@
package com.njcn.gather.aireport.reportapproval.handler;
import cn.hutool.core.util.StrUtil;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Component
public class ReportApprovalHandlerRegistry {
private final Map<String, ReportApprovalHandler> handlerMap = new LinkedHashMap<String, ReportApprovalHandler>();
public ReportApprovalHandlerRegistry(List<ReportApprovalHandler> handlers) {
if (handlers == null || handlers.isEmpty()) {
return;
}
for (ReportApprovalHandler handler : handlers) {
if (handler != null && StrUtil.isNotBlank(handler.getReportType())) {
handlerMap.put(handler.getReportType(), handler);
}
}
}
public ReportApprovalHandler requireHandler(String reportType) {
ReportApprovalHandler handler = handlerMap.get(reportType);
if (handler == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "不支持的报告类型");
}
return handler;
}
}

View File

@@ -0,0 +1,58 @@
package com.njcn.gather.aireport.reportapproval.handler;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.reportapproval.component.ReportApprovalUserNameResolver;
import com.njcn.gather.aireport.reportapproval.mapper.ReportApprovalMapper;
import com.njcn.gather.aireport.reportapproval.pojo.constant.ReportApprovalConst;
import com.njcn.gather.aireport.reportmodel.pojo.constant.ReportModelConst;
import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO;
import com.njcn.gather.aireport.reportmodel.service.ReportModelService;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class ReportModelApprovalHandler extends AbstractReportApprovalHandler {
private final ReportModelService reportModelService;
public ReportModelApprovalHandler(ReportApprovalMapper reportApprovalMapper,
ReportApprovalUserNameResolver reportApprovalUserNameResolver,
ReportModelService reportModelService) {
super(reportApprovalMapper, reportApprovalUserNameResolver);
this.reportModelService = reportModelService;
}
@Override
public String getReportType() {
return ReportApprovalConst.REPORT_TYPE_REPORT_MODEL;
}
@Override
protected boolean doAudit(String reportId, String reason, boolean passed) {
ReportModelPO reportModel = reportModelService.requireNormal(reportId);
if (!ReportModelConst.STATE_AUDITING.equals(reportModel.getState())) {
throw new BusinessException(CommonResponseEnum.FAIL, "仅待审状态的报告模板允许审批");
}
String userId = resolveCurrentUserId();
LocalDateTime now = LocalDateTime.now();
String targetState = passed ? ReportModelConst.STATE_APPROVED : ReportModelConst.STATE_REJECTED;
boolean updated = reportModelService.lambdaUpdate()
.set(ReportModelPO::getState, targetState)
.set(ReportModelPO::getCheckId, userId)
.set(ReportModelPO::getCheckTime, now)
.set(ReportModelPO::getCheckResult, reason)
.set(ReportModelPO::getUpdateBy, userId)
.set(ReportModelPO::getUpdateTime, now)
.eq(ReportModelPO::getId, reportId)
.eq(ReportModelPO::getStatus, ReportModelConst.STATUS_NORMAL)
.eq(ReportModelPO::getState, ReportModelConst.STATE_AUDITING)
.update();
if (!updated) {
throw new BusinessException(CommonResponseEnum.FAIL, "报告模板状态已变更,请刷新后重试");
}
saveLog(reportModel.getId(), reportModel.getName(), ReportModelConst.STATE_AUDITING, targetState, reason, userId, now);
return true;
}
}

View File

@@ -0,0 +1,59 @@
package com.njcn.gather.aireport.reportapproval.handler;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.reportapproval.component.ReportApprovalUserNameResolver;
import com.njcn.gather.aireport.reportapproval.mapper.ReportApprovalMapper;
import com.njcn.gather.aireport.reportapproval.pojo.constant.ReportApprovalConst;
import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst;
import com.njcn.gather.aireport.testreport.pojo.po.TestReportPO;
import com.njcn.gather.aireport.testreport.service.TestReportService;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Component
public class TestReportApprovalHandler extends AbstractReportApprovalHandler {
private final TestReportService testReportService;
public TestReportApprovalHandler(ReportApprovalMapper reportApprovalMapper,
ReportApprovalUserNameResolver reportApprovalUserNameResolver,
TestReportService testReportService) {
super(reportApprovalMapper, reportApprovalUserNameResolver);
this.testReportService = testReportService;
}
@Override
public String getReportType() {
return ReportApprovalConst.REPORT_TYPE_TEST_REPORT;
}
@Override
protected boolean doAudit(String reportId, String reason, boolean passed) {
TestReportPO testReport = testReportService.requireNormal(reportId);
if (!TestReportConst.STATE_AUDITING.equals(testReport.getState())) {
throw new BusinessException(CommonResponseEnum.FAIL, "仅待审状态的普测报告允许审批");
}
String userId = resolveCurrentUserId();
LocalDateTime now = LocalDateTime.now();
String targetState = passed ? TestReportConst.STATE_APPROVED : TestReportConst.STATE_REJECTED;
boolean updated = testReportService.lambdaUpdate()
.set(TestReportPO::getState, targetState)
.set(TestReportPO::getCheckId, userId)
.set(TestReportPO::getCheckTime, LocalDate.now())
.set(TestReportPO::getCheckResult, reason)
.set(TestReportPO::getUpdateBy, userId)
.set(TestReportPO::getUpdateTime, now)
.eq(TestReportPO::getId, reportId)
.eq(TestReportPO::getStatus, TestReportConst.STATUS_NORMAL)
.eq(TestReportPO::getState, TestReportConst.STATE_AUDITING)
.update();
if (!updated) {
throw new BusinessException(CommonResponseEnum.FAIL, "普测报告状态已变更,请刷新后重试");
}
saveLog(testReport.getId(), testReport.getNo(), TestReportConst.STATE_AUDITING, targetState, reason, userId, now);
return true;
}
}

View File

@@ -0,0 +1,95 @@
package com.njcn.gather.aireport.reportapproval.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.gather.aireport.reportapproval.pojo.param.ReportApprovalParam;
import com.njcn.gather.aireport.reportapproval.pojo.po.ReportApprovalLogPO;
import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalLogVO;
import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalPendingVO;
import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelUserNameVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.Collection;
import java.util.List;
@Mapper
public interface ReportApprovalMapper extends BaseMapper<ReportApprovalLogPO> {
@Select({
"<script>",
"select id, name from sys_user where id in",
"<foreach collection='ids' item='id' open='(' separator=',' close=')'>",
"#{id}",
"</foreach>",
"</script>"
})
List<ReportModelUserNameVO> selectUserNamesByIds(@Param("ids") Collection<String> ids);
@Select({
"<script>",
"select count(1) from (",
"<if test='param.reportType == null or param.reportType == \"\" or param.reportType == \"REPORT_MODEL\"'>",
"select model.id",
"from ai_reportmodel model",
"where model.status = 1 and model.state = '02'",
"</if>",
"<if test='param.reportType == null or param.reportType == \"\"'>",
"union all",
"</if>",
"<if test='param.reportType == null or param.reportType == \"\" or param.reportType == \"TEST_REPORT\"'>",
"select report.id",
"from ai_testreport report",
"where report.status = 1 and report.state = '02'",
"</if>",
") pending",
"</script>"
})
long countPendingReports(@Param("param") ReportApprovalParam.PendingQueryParam param);
@Select({
"<script>",
"select * from (",
"<if test='param.reportType == null or param.reportType == \"\" or param.reportType == \"REPORT_MODEL\"'>",
"select 'REPORT_MODEL' as reportType, model.id as reportId, model.name as reportName, model.state,",
"model.update_by as submitterId, user.name as submitterName, model.update_time as submitTime",
"from ai_reportmodel model",
"left join sys_user user on user.id = model.update_by",
"where model.status = 1 and model.state = '02'",
"</if>",
"<if test='param.reportType == null or param.reportType == \"\"'>",
"union all",
"</if>",
"<if test='param.reportType == null or param.reportType == \"\" or param.reportType == \"TEST_REPORT\"'>",
"select 'TEST_REPORT' as reportType, report.id as reportId, report.no as reportName, report.state,",
"report.update_by as submitterId, user.name as submitterName, report.update_time as submitTime",
"from ai_testreport report",
"left join sys_user user on user.id = report.update_by",
"where report.status = 1 and report.state = '02'",
"</if>",
") pending",
"order by pending.submitTime desc",
"limit #{offset}, #{size}",
"</script>"
})
List<ReportApprovalPendingVO> selectPendingPage(@Param("offset") long offset,
@Param("size") long size,
@Param("param") ReportApprovalParam.PendingQueryParam param);
@Select({
"<script>",
"select id, report_type as reportType, report_id as reportId, report_name as reportName,",
"approval_result as approvalResult, approval_reason as approvalReason, before_state as beforeState,",
"after_state as afterState, approver_name as approverName, approval_time as approvalTime",
"from ai_report_approval_log",
"where status = 1",
"<if test='param.reportType != null and param.reportType != \"\"'>",
"and report_type = #{param.reportType}",
"</if>",
"order by approval_time desc, create_time desc",
"</script>"
})
Page<ReportApprovalLogVO> selectLogPage(Page<ReportApprovalLogVO> page,
@Param("param") ReportApprovalParam.LogQueryParam param);
}

View File

@@ -0,0 +1,12 @@
package com.njcn.gather.aireport.reportapproval.pojo.constant;
public final class ReportApprovalConst {
public static final String REPORT_TYPE_REPORT_MODEL = "REPORT_MODEL";
public static final String REPORT_TYPE_TEST_REPORT = "TEST_REPORT";
public static final Integer STATUS_NORMAL = 1;
private ReportApprovalConst() {
}
}

View File

@@ -0,0 +1,42 @@
package com.njcn.gather.aireport.reportapproval.pojo.param;
import com.njcn.web.pojo.param.BaseParam;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
public class ReportApprovalParam {
@Data
public static class AuditParam {
@ApiModelProperty("报告类型")
@NotBlank(message = "报告类型不能为空")
private String reportType;
@ApiModelProperty("报告ID")
@NotBlank(message = "报告ID不能为空")
private String reportId;
@ApiModelProperty("审批原因")
@NotBlank(message = "审批原因不能为空")
@Size(max = 128, message = "审批原因不能超过128个字符")
private String approvalReason;
}
@Data
@EqualsAndHashCode(callSuper = true)
public static class PendingQueryParam extends BaseParam {
@ApiModelProperty("报告类型")
private String reportType;
}
@Data
@EqualsAndHashCode(callSuper = true)
public static class LogQueryParam extends BaseParam {
@ApiModelProperty("报告类型")
private String reportType;
}
}

View File

@@ -0,0 +1,48 @@
package com.njcn.gather.aireport.reportapproval.pojo.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@TableName("ai_report_approval_log")
public class ReportApprovalLogPO implements Serializable {
private static final long serialVersionUID = 1900859716151337132L;
@TableId("id")
private String id;
@TableField("report_type")
private String reportType;
@TableField("report_id")
private String reportId;
@TableField("report_name")
private String reportName;
@TableField("approval_result")
private String approvalResult;
@TableField("approval_reason")
private String approvalReason;
@TableField("before_state")
private String beforeState;
@TableField("after_state")
private String afterState;
@TableField("approver_id")
private String approverId;
@TableField("approver_name")
private String approverName;
@TableField("approval_time")
private LocalDateTime approvalTime;
@TableField("status")
private Integer status;
@TableField("create_by")
private String createBy;
@TableField("create_time")
private LocalDateTime createTime;
@TableField("update_by")
private String updateBy;
@TableField("update_time")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,38 @@
package com.njcn.gather.aireport.reportapproval.pojo.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class ReportApprovalLogVO {
@ApiModelProperty("日志ID")
private String id;
@ApiModelProperty("报告类型")
private String reportType;
@ApiModelProperty("报告ID")
private String reportId;
@ApiModelProperty("报告名称")
private String reportName;
@ApiModelProperty("审批结果")
private String approvalResult;
@ApiModelProperty("审批原因")
private String approvalReason;
@ApiModelProperty("审批前状态")
private String beforeState;
@ApiModelProperty("审批后状态")
private String afterState;
@ApiModelProperty("审批人")
private String approverName;
@ApiModelProperty("审批时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime approvalTime;
}

View File

@@ -0,0 +1,32 @@
package com.njcn.gather.aireport.reportapproval.pojo.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class ReportApprovalPendingVO {
@ApiModelProperty("报告类型")
private String reportType;
@ApiModelProperty("报告ID")
private String reportId;
@ApiModelProperty("报告名称")
private String reportName;
@ApiModelProperty("报告状态")
private String state;
@ApiModelProperty("提交人ID")
private String submitterId;
@ApiModelProperty("提交人")
private String submitterName;
@ApiModelProperty("提交时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime submitTime;
}

View File

@@ -0,0 +1,17 @@
package com.njcn.gather.aireport.reportapproval.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.gather.aireport.reportapproval.pojo.param.ReportApprovalParam;
import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalLogVO;
import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalPendingVO;
public interface ReportApprovalService {
boolean pass(ReportApprovalParam.AuditParam param);
boolean reject(ReportApprovalParam.AuditParam param);
Page<ReportApprovalPendingVO> pendingPage(ReportApprovalParam.PendingQueryParam param);
Page<ReportApprovalLogVO> logPage(ReportApprovalParam.LogQueryParam param);
}

View File

@@ -0,0 +1,77 @@
package com.njcn.gather.aireport.reportapproval.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.reportapproval.handler.ReportApprovalHandler;
import com.njcn.gather.aireport.reportapproval.handler.ReportApprovalHandlerRegistry;
import com.njcn.gather.aireport.reportapproval.mapper.ReportApprovalMapper;
import com.njcn.gather.aireport.reportapproval.pojo.param.ReportApprovalParam;
import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalLogVO;
import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalPendingVO;
import com.njcn.gather.aireport.reportapproval.service.ReportApprovalService;
import com.njcn.web.factory.PageFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
import java.util.List;
@Service
public class ReportApprovalServiceImpl implements ReportApprovalService {
private final ReportApprovalMapper reportApprovalMapper;
private final ReportApprovalHandlerRegistry reportApprovalHandlerRegistry;
public ReportApprovalServiceImpl(ReportApprovalMapper reportApprovalMapper,
ReportApprovalHandlerRegistry reportApprovalHandlerRegistry) {
this.reportApprovalMapper = reportApprovalMapper;
this.reportApprovalHandlerRegistry = reportApprovalHandlerRegistry;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean pass(ReportApprovalParam.AuditParam param) {
return audit(param, true);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean reject(ReportApprovalParam.AuditParam param) {
return audit(param, false);
}
@Override
public Page<ReportApprovalPendingVO> pendingPage(ReportApprovalParam.PendingQueryParam param) {
ReportApprovalParam.PendingQueryParam query = param == null ? new ReportApprovalParam.PendingQueryParam() : param;
if (StrUtil.isNotBlank(query.getReportType())) {
reportApprovalHandlerRegistry.requireHandler(query.getReportType());
}
long current = PageFactory.getPageNum(query);
long size = PageFactory.getPageSize(query);
long offset = Math.max(0L, (current - 1L) * size);
long total = reportApprovalMapper.countPendingReports(query);
List<ReportApprovalPendingVO> records = total <= 0
? Collections.<ReportApprovalPendingVO>emptyList()
: reportApprovalMapper.selectPendingPage(offset, size, query);
Page<ReportApprovalPendingVO> page = new Page<ReportApprovalPendingVO>(current, size, total);
page.setRecords(records);
return page;
}
@Override
public Page<ReportApprovalLogVO> logPage(ReportApprovalParam.LogQueryParam param) {
ReportApprovalParam.LogQueryParam query = param == null ? new ReportApprovalParam.LogQueryParam() : param;
return reportApprovalMapper.selectLogPage(
new Page<ReportApprovalLogVO>(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), query);
}
private boolean audit(ReportApprovalParam.AuditParam param, boolean passed) {
if (param == null || StrUtil.isBlank(param.getReportType()) || StrUtil.isBlank(param.getReportId())) {
throw new BusinessException(CommonResponseEnum.FAIL, "审批参数不能为空");
}
ReportApprovalHandler handler = reportApprovalHandlerRegistry.requireHandler(param.getReportType());
return handler.audit(param, passed);
}
}

View File

@@ -0,0 +1,27 @@
package com.njcn.gather.aireport.reportapproval.pojo.vo;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Assert;
import org.junit.Test;
import java.time.LocalDateTime;
public class ReportApprovalVOTimeFormatTest {
@Test
public void pendingAndLogTimeJsonUseStandardTwentyFourHourFormat() throws Exception {
ReportApprovalPendingVO pendingVO = new ReportApprovalPendingVO();
pendingVO.setSubmitTime(LocalDateTime.of(2026, 6, 29, 10, 0, 0));
ReportApprovalLogVO logVO = new ReportApprovalLogVO();
logVO.setApprovalTime(LocalDateTime.of(2026, 6, 29, 18, 30, 45));
ObjectMapper objectMapper = new ObjectMapper();
String pendingJson = objectMapper.writeValueAsString(pendingVO);
String logJson = objectMapper.writeValueAsString(logVO);
Assert.assertTrue(pendingJson.contains("\"submitTime\":\"2026-06-29 10:00:00\""));
Assert.assertFalse(pendingJson.contains("2026-06-29T10"));
Assert.assertTrue(logJson.contains("\"approvalTime\":\"2026-06-29 18:30:45\""));
Assert.assertFalse(logJson.contains("2026-06-29T18"));
}
}

View File

@@ -0,0 +1,75 @@
package com.njcn.gather.aireport.reportapproval.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.gather.aireport.reportapproval.handler.ReportApprovalHandler;
import com.njcn.gather.aireport.reportapproval.handler.ReportApprovalHandlerRegistry;
import com.njcn.gather.aireport.reportapproval.mapper.ReportApprovalMapper;
import com.njcn.gather.aireport.reportapproval.pojo.constant.ReportApprovalConst;
import com.njcn.gather.aireport.reportapproval.pojo.param.ReportApprovalParam;
import com.njcn.gather.aireport.reportapproval.pojo.vo.ReportApprovalPendingVO;
import org.junit.Assert;
import org.junit.Test;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class ReportApprovalServiceImplTest {
@Test
public void pendingPageShouldUseDatabasePagingResult() {
ReportApprovalMapper mapper = mock(ReportApprovalMapper.class);
ReportApprovalHandlerRegistry registry = new ReportApprovalHandlerRegistry(Collections.<ReportApprovalHandler>emptyList());
ReportApprovalServiceImpl service = new ReportApprovalServiceImpl(mapper, registry);
ReportApprovalParam.PendingQueryParam param = new ReportApprovalParam.PendingQueryParam();
param.setPageNum(2);
param.setPageSize(5);
ReportApprovalPendingVO record = new ReportApprovalPendingVO();
record.setReportType(ReportApprovalConst.REPORT_TYPE_REPORT_MODEL);
record.setReportId("model-001");
record.setReportName("模板A");
record.setSubmitterId("user-001");
record.setSubmitterName("张三");
record.setSubmitTime(LocalDateTime.of(2026, 6, 29, 10, 0, 0));
List<ReportApprovalPendingVO> records = Arrays.asList(record);
when(mapper.countPendingReports(param)).thenReturn(11L);
when(mapper.selectPendingPage(5L, 5L, param)).thenReturn(records);
Page<ReportApprovalPendingVO> page = service.pendingPage(param);
Assert.assertEquals(2L, page.getCurrent());
Assert.assertEquals(5L, page.getSize());
Assert.assertEquals(11L, page.getTotal());
Assert.assertEquals(1, page.getRecords().size());
Assert.assertEquals("user-001", page.getRecords().get(0).getSubmitterId());
Assert.assertEquals("张三", page.getRecords().get(0).getSubmitterName());
verify(mapper).countPendingReports(param);
verify(mapper).selectPendingPage(5L, 5L, param);
}
@Test
public void passShouldDelegateToMatchedHandler() {
ReportApprovalMapper mapper = mock(ReportApprovalMapper.class);
ReportApprovalHandler handler = mock(ReportApprovalHandler.class);
when(handler.getReportType()).thenReturn(ReportApprovalConst.REPORT_TYPE_TEST_REPORT);
when(handler.audit(org.mockito.ArgumentMatchers.any(ReportApprovalParam.AuditParam.class), eq(true))).thenReturn(true);
ReportApprovalHandlerRegistry registry = new ReportApprovalHandlerRegistry(Arrays.asList(handler));
ReportApprovalServiceImpl service = new ReportApprovalServiceImpl(mapper, registry);
ReportApprovalParam.AuditParam param = new ReportApprovalParam.AuditParam();
param.setReportType(ReportApprovalConst.REPORT_TYPE_TEST_REPORT);
param.setReportId("report-001");
param.setApprovalReason("通过");
boolean result = service.pass(param);
Assert.assertTrue(result);
verify(handler).audit(param, true);
}
}

View File

@@ -11,8 +11,15 @@
</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>
@@ -30,5 +37,21 @@
<artifactId>spingboot2.3.12</artifactId>
<version>2.3.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,132 @@
package com.njcn.gather.aireport.reportmodel.component;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
/**
* Local storage for uploaded report model template files.
*/
@Component
public class ReportModelFileStorageService {
private static final String DEFAULT_STORAGE_DIR = "data/report-model";
private static final String REPORT_MODEL_STORAGE_DIR = "report-model";
private static final DateTimeFormatter DATE_DIR_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
@Value("${files.path:}")
private String filesPath;
public StoredFile save(MultipartFile templateFile) {
if (templateFile == null || templateFile.isEmpty()) {
throw new IllegalArgumentException("模板文件不能为空");
}
try {
Path storageRoot = resolveStorageRoot();
String dateDir = LocalDate.now().format(DATE_DIR_FORMATTER);
Path storageDir = storageRoot.resolve(dateDir).normalize();
if (!storageDir.startsWith(storageRoot)) {
throw new IllegalArgumentException("模板文件存储路径不合法");
}
Files.createDirectories(storageDir);
String sanitizedFileName = sanitizeFileName(templateFile.getOriginalFilename());
String storedFileName = UUID.randomUUID().toString().replace("-", "") + "-" + sanitizedFileName;
Path target = storageDir.resolve(storedFileName).normalize();
ensurePathInRoot(storageRoot, target);
try (InputStream inputStream = templateFile.getInputStream()) {
Files.copy(inputStream, target, StandardCopyOption.REPLACE_EXISTING);
}
String storedPath = storageRoot.relativize(target).toString().replace('\\', '/');
return new StoredFile(storedPath, sanitizedFileName);
} catch (Exception exception) {
throw new IllegalArgumentException("保存模板文件失败: " + exception.getMessage(), exception);
}
}
public Path resolveDownloadPath(String storedPath) {
if (trimToNull(storedPath) == null) {
throw new IllegalArgumentException("模板文件路径不能为空");
}
Path storageRoot = resolveStorageRoot();
Path target = storageRoot.resolve(storedPath).normalize();
ensurePathInRoot(storageRoot, target);
return target;
}
public void deleteQuietly(String storedPath) {
try {
if (trimToNull(storedPath) != null) {
Files.deleteIfExists(resolveDownloadPath(storedPath));
}
} catch (Exception ignored) {
// Cleanup failure must not hide the original database error.
}
}
public String resolveDownloadFileName(String storedPath) {
Path fileName = Paths.get(storedPath).getFileName();
if (fileName == null) {
return "report-model";
}
String name = fileName.toString();
int separatorIndex = name.indexOf('-');
return separatorIndex >= 0 && separatorIndex + 1 < name.length() ? name.substring(separatorIndex + 1) : name;
}
private Path resolveStorageRoot() {
String configuredPath = trimToNull(filesPath);
Path path = configuredPath == null ? Paths.get(DEFAULT_STORAGE_DIR) : Paths.get(configuredPath, REPORT_MODEL_STORAGE_DIR);
return path.toAbsolutePath().normalize();
}
private void ensurePathInRoot(Path storageRoot, Path target) {
if (!target.startsWith(storageRoot)) {
throw new IllegalArgumentException("模板文件路径不合法");
}
}
private String sanitizeFileName(String originalFilename) {
String fileName = trimToNull(originalFilename);
if (fileName == null) {
return "unnamed";
}
String normalizedName = Paths.get(fileName).getFileName().toString();
String sanitized = normalizedName.replaceAll("[\\\\/:*?\"<>|]", "_");
return trimToNull(sanitized) == null ? "unnamed" : sanitized;
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String text = value.trim();
return text.isEmpty() ? null : text;
}
public static class StoredFile {
private final String path;
private final String fileName;
public StoredFile(String path, String fileName) {
this.path = path;
this.fileName = fileName;
}
public String getPath() {
return path;
}
public String getFileName() {
return fileName;
}
}
}

View File

@@ -0,0 +1,41 @@
package com.njcn.gather.aireport.reportmodel.component;
import cn.hutool.core.util.StrUtil;
import com.njcn.gather.aireport.reportmodel.mapper.ReportModelMapper;
import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelUserNameVO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Component
@RequiredArgsConstructor
public class ReportModelUserNameResolver {
private final ReportModelMapper reportModelMapper;
public Map<String, String> resolveUserNames(Collection<String> userIds) {
if (userIds == null || userIds.isEmpty()) {
return Collections.emptyMap();
}
Set<String> validIds = userIds.stream()
.filter(StrUtil::isNotBlank)
.collect(Collectors.toCollection(LinkedHashSet::new));
if (validIds.isEmpty()) {
return Collections.emptyMap();
}
List<ReportModelUserNameVO> users = reportModelMapper.selectUserNamesByIds(validIds);
if (users == null || users.isEmpty()) {
return Collections.emptyMap();
}
return users.stream()
.filter(user -> user != null && StrUtil.isNotBlank(user.getId()) && StrUtil.isNotBlank(user.getName()))
.collect(Collectors.toMap(ReportModelUserNameVO::getId, ReportModelUserNameVO::getName, (first, second) -> first));
}
}

View File

@@ -0,0 +1,173 @@
package com.njcn.gather.aireport.reportmodel.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.constant.OperateType;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.gather.aireport.reportmodel.component.ReportModelFileStorageService;
import com.njcn.gather.aireport.reportmodel.pojo.param.ReportModelParam;
import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO;
import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelVO;
import com.njcn.gather.aireport.reportmodel.service.ReportModelService;
import com.njcn.gather.comservice.filepreview.FilePreviewService;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.HttpResultUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
/**
* Report model management endpoints.
*/
@Api(tags = "报告模板管理")
@RestController
@RequestMapping("/api/report-model")
@RequiredArgsConstructor
public class ReportModelController extends BaseController {
private final ReportModelService reportModelService;
private final ReportModelFileStorageService reportModelFileStorageService;
private final FilePreviewService filePreviewService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询报告模板列表")
@PostMapping("/list")
public HttpResult<Page<ReportModelVO>> list(@RequestBody(required = false) ReportModelParam.QueryParam param) {
String methodDescribe = getMethodDescribe("list");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
reportModelService.listReportModels(param), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD)
@ApiOperation("新增报告模板")
@PostMapping(value = "/add", consumes = {"multipart/form-data"})
public HttpResult<Boolean> add(@RequestPart("request") @Validated ReportModelParam.AddParam param,
@RequestPart("templateFile") MultipartFile templateFile) {
String methodDescribe = getMethodDescribe("add");
boolean result = reportModelService.addReportModel(param, templateFile);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
@ApiOperation("编辑报告模板")
@PostMapping(value = "/update", consumes = {"multipart/form-data"})
public HttpResult<Boolean> update(@RequestPart("request") @Validated ReportModelParam.UpdateParam param,
@RequestPart(value = "templateFile", required = false) MultipartFile templateFile) {
String methodDescribe = getMethodDescribe("update");
boolean result = reportModelService.updateReportModel(param, templateFile);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DELETE)
@ApiOperation("删除报告模板")
@PostMapping("/delete")
public HttpResult<Boolean> delete(@RequestBody List<String> ids) {
String methodDescribe = getMethodDescribe("delete");
boolean result = reportModelService.deleteReportModels(ids);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询报告模板详情")
@ApiImplicitParam(name = "id", value = "模板ID", required = true)
@GetMapping("/{id}")
public HttpResult<ReportModelVO> detail(@PathVariable("id") String id) {
String methodDescribe = getMethodDescribe("detail");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
reportModelService.getReportModel(id), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("下载报告模板文件")
@ApiImplicitParam(name = "id", value = "模板ID", required = true)
@GetMapping("/{id}/download")
public ResponseEntity<InputStreamResource> download(@PathVariable("id") String id) {
ReportModelPO model = reportModelService.requireNormal(id);
Path filePath = reportModelFileStorageService.resolveDownloadPath(model.getPath());
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
throw new BusinessException(CommonResponseEnum.FAIL, "模板文件不存在");
}
try {
String fileName = model.getFileName();
if (fileName == null || fileName.trim().isEmpty()) {
fileName = reportModelFileStorageService.resolveDownloadFileName(model.getPath());
}
String encodedFileName = encodeFileName(fileName);
InputStream inputStream = Files.newInputStream(filePath);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFileName)
.body(new InputStreamResource(inputStream));
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "读取模板文件失败");
}
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("预览报告模板文件")
@ApiImplicitParam(name = "id", value = "模板ID", required = true)
@GetMapping("/{id}/preview")
public ResponseEntity<byte[]> preview(@PathVariable("id") String id) {
ReportModelPO model = reportModelService.requireNormal(id);
Path filePath = reportModelFileStorageService.resolveDownloadPath(model.getPath());
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
throw new BusinessException(CommonResponseEnum.FAIL, "模板文件不存在");
}
String fileName = model.getFileName();
if (fileName == null || fileName.trim().isEmpty()) {
fileName = reportModelFileStorageService.resolveDownloadFileName(model.getPath());
}
FilePreviewService.PreviewContent previewContent = filePreviewService.preview(filePath, fileName);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"inline; filename*=UTF-8''" + encodeFileName(fileName))
.header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate")
.contentType(MediaType.parseMediaType(previewContent.getContentType()))
.body(previewContent.getContent());
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
@ApiOperation("提交报告模板审核")
@PostMapping("/submit-audit")
public HttpResult<Boolean> submitAudit(@RequestBody @Validated ReportModelParam.SubmitAuditParam param) {
String methodDescribe = getMethodDescribe("submitAudit");
boolean result = reportModelService.submitAudit(param);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
private String encodeFileName(String fileName) {
try {
return URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replace("+", "%20");
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "编码模板文件名失败");
}
}
}

View File

@@ -0,0 +1,25 @@
package com.njcn.gather.aireport.reportmodel.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO;
import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelUserNameVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.Collection;
import java.util.List;
@Mapper
public interface ReportModelMapper extends BaseMapper<ReportModelPO> {
@Select({
"<script>",
"select id, name from sys_user where id in",
"<foreach collection='ids' item='id' open='(' separator=',' close=')'>",
"#{id}",
"</foreach>",
"</script>"
})
List<ReportModelUserNameVO> selectUserNamesByIds(@Param("ids") Collection<String> ids);
}

View File

@@ -0,0 +1,19 @@
package com.njcn.gather.aireport.reportmodel.pojo.constant;
/**
* Report model state constants.
*/
public final class ReportModelConst {
private ReportModelConst() {
}
public static final String STATE_EDITING = "01";
public static final String STATE_AUDITING = "02";
public static final String STATE_APPROVED = "03";
public static final String STATE_REJECTED = "04";
public static final String STATE_DELETED = "05";
public static final Integer STATUS_DELETED = 0;
public static final Integer STATUS_NORMAL = 1;
}

View File

@@ -0,0 +1,58 @@
package com.njcn.gather.aireport.reportmodel.pojo.param;
import com.njcn.web.pojo.param.BaseParam;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
/**
* Report model request parameters.
*/
public class ReportModelParam {
@Data
@EqualsAndHashCode(callSuper = true)
public static class QueryParam extends BaseParam {
@ApiModelProperty("Template name keyword")
private String name;
@ApiModelProperty("Template state")
private String state;
@ApiModelProperty("Create time begin, yyyy-MM-dd HH:mm:ss")
private String searchBeginTime;
@ApiModelProperty("Create time end, yyyy-MM-dd HH:mm:ss")
private String searchEndTime;
}
@Data
public static class AddParam {
@ApiModelProperty("Template name")
@NotBlank(message = "模板名称不能为空")
@Size(max = 32, message = "模板名称不能超过32个字符")
private String name;
}
@Data
public static class UpdateParam {
@ApiModelProperty("Template ID")
@NotBlank(message = "模板ID不能为空")
private String id;
@ApiModelProperty("Template name")
@NotBlank(message = "模板名称不能为空")
@Size(max = 32, message = "模板名称不能超过32个字符")
private String name;
}
@Data
public static class SubmitAuditParam {
@ApiModelProperty("Template ID")
@NotBlank(message = "模板ID不能为空")
private String id;
}
}

View File

@@ -0,0 +1,45 @@
package com.njcn.gather.aireport.reportmodel.pojo.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* Report model template record.
*/
@Data
@TableName("ai_reportmodel")
public class ReportModelPO implements Serializable {
private static final long serialVersionUID = -1413572272542367267L;
@TableId("id")
private String id;
@TableField("name")
private String name;
@TableField("state")
private String state;
@TableField("check_id")
private String checkId;
@TableField("check_time")
private LocalDateTime checkTime;
@TableField("check_result")
private String checkResult;
@TableField("path")
private String path;
@TableField("file_name")
private String fileName;
@TableField("status")
private Integer status;
@TableField("create_by")
private String createBy;
@TableField("create_time")
private LocalDateTime createTime;
@TableField("update_by")
private String updateBy;
@TableField("update_time")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,9 @@
package com.njcn.gather.aireport.reportmodel.pojo.vo;
import lombok.Data;
@Data
public class ReportModelUserNameVO {
private String id;
private String name;
}

View File

@@ -0,0 +1,108 @@
package com.njcn.gather.aireport.reportmodel.pojo.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
/**
* Report model view object.
*/
@Data
public class ReportModelVO {
private static final String DEFAULT_EMPTY_DISPLAY = "-";
@ApiModelProperty("Template ID")
private String id;
@ApiModelProperty("Template name")
private String name;
@ApiModelProperty("Template state")
private String state;
@ApiModelProperty("Auditor ID")
private String checkId;
@ApiModelProperty("Auditor display name")
private String checkerName;
@ApiModelProperty("Audit time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime checkTime;
@ApiModelProperty("Audit comment")
private String checkResult;
@ApiModelProperty("Template file path")
private String path;
@ApiModelProperty("Template document file name")
private String fileName;
@ApiModelProperty("Logical status")
private Integer status;
@ApiModelProperty("Creator display name")
private String createBy;
@ApiModelProperty("Create time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime createTime;
@ApiModelProperty("Editor display name")
private String updateBy;
@ApiModelProperty("Editor display name")
private String editorName;
@ApiModelProperty("Update time")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime updateTime;
public String getId() {
return defaultDisplay(id);
}
public String getName() {
return defaultDisplay(name);
}
public String getState() {
return defaultDisplay(state);
}
public String getCheckId() {
return defaultDisplay(checkId);
}
public String getCheckerName() {
return defaultDisplay(checkerName);
}
public String getCheckResult() {
return defaultDisplay(checkResult);
}
public String getPath() {
return defaultDisplay(path);
}
public String getFileName() {
return defaultDisplay(fileName);
}
public String getCreateBy() {
return defaultDisplay(createBy);
}
public String getUpdateBy() {
return defaultDisplay(updateBy);
}
public String getEditorName() {
return defaultDisplay(editorName);
}
private String defaultDisplay(String value) {
return StringUtils.hasText(value) ? value : DEFAULT_EMPTY_DISPLAY;
}
}

View File

@@ -0,0 +1,27 @@
package com.njcn.gather.aireport.reportmodel.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.gather.aireport.reportmodel.pojo.param.ReportModelParam;
import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO;
import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelVO;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
public interface ReportModelService extends IService<ReportModelPO> {
Page<ReportModelVO> listReportModels(ReportModelParam.QueryParam param);
boolean addReportModel(ReportModelParam.AddParam param, MultipartFile templateFile);
boolean updateReportModel(ReportModelParam.UpdateParam param, MultipartFile templateFile);
boolean deleteReportModels(List<String> ids);
ReportModelVO getReportModel(String id);
ReportModelPO requireNormal(String id);
boolean submitAudit(ReportModelParam.SubmitAuditParam param);
}

View File

@@ -0,0 +1,279 @@
package com.njcn.gather.aireport.reportmodel.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.reportmodel.component.ReportModelFileStorageService;
import com.njcn.gather.aireport.reportmodel.component.ReportModelUserNameResolver;
import com.njcn.gather.aireport.reportmodel.mapper.ReportModelMapper;
import com.njcn.gather.aireport.reportmodel.pojo.constant.ReportModelConst;
import com.njcn.gather.aireport.reportmodel.pojo.param.ReportModelParam;
import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO;
import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelVO;
import com.njcn.gather.aireport.reportmodel.service.ReportModelService;
import com.njcn.web.factory.PageFactory;
import com.njcn.web.utils.RequestUtil;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Report model management service.
*/
@Service
public class ReportModelServiceImpl extends ServiceImpl<ReportModelMapper, ReportModelPO> implements ReportModelService {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private final ReportModelFileStorageService reportModelFileStorageService;
private final ReportModelUserNameResolver reportModelUserNameResolver;
public ReportModelServiceImpl(ReportModelFileStorageService reportModelFileStorageService,
ReportModelUserNameResolver reportModelUserNameResolver) {
this.reportModelFileStorageService = reportModelFileStorageService;
this.reportModelUserNameResolver = reportModelUserNameResolver;
}
@Override
public Page<ReportModelVO> listReportModels(ReportModelParam.QueryParam param) {
ReportModelParam.QueryParam query = param == null ? new ReportModelParam.QueryParam() : param;
LambdaQueryWrapper<ReportModelPO> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ReportModelPO::getStatus, ReportModelConst.STATUS_NORMAL)
.like(StrUtil.isNotBlank(query.getName()), ReportModelPO::getName, query.getName())
.eq(StrUtil.isNotBlank(query.getState()), ReportModelPO::getState, query.getState());
if (StrUtil.isNotBlank(query.getSearchBeginTime())) {
wrapper.ge(ReportModelPO::getUpdateTime, parseDateTime(query.getSearchBeginTime(), "开始时间格式不正确"));
}
if (StrUtil.isNotBlank(query.getSearchEndTime())) {
wrapper.le(ReportModelPO::getUpdateTime, parseDateTime(query.getSearchEndTime(), "结束时间格式不正确"));
}
wrapper.orderByDesc(ReportModelPO::getCreateTime);
Page<ReportModelPO> page = this.page(new Page<>(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), wrapper);
Page<ReportModelVO> result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
Map<String, String> userNameMap = resolveUserNameMap(page.getRecords());
result.setRecords(page.getRecords().stream().map(model -> toVO(model, userNameMap)).collect(Collectors.toList()));
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean addReportModel(ReportModelParam.AddParam param, MultipartFile templateFile) {
String name = resolveName(param == null ? null : param.getName());
checkNameUnique(name, null);
ReportModelFileStorageService.StoredFile storedFile = reportModelFileStorageService.save(templateFile);
try {
LocalDateTime now = LocalDateTime.now();
String userId = resolveCurrentUserId();
ReportModelPO model = new ReportModelPO();
model.setId(UUID.randomUUID().toString());
model.setName(name);
model.setState(ReportModelConst.STATE_EDITING);
model.setPath(storedFile.getPath());
model.setFileName(storedFile.getFileName());
model.setStatus(ReportModelConst.STATUS_NORMAL);
model.setCreateBy(userId);
model.setCreateTime(now);
model.setUpdateBy(userId);
model.setUpdateTime(now);
return this.save(model);
} catch (RuntimeException exception) {
reportModelFileStorageService.deleteQuietly(storedFile.getPath());
throw exception;
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateReportModel(ReportModelParam.UpdateParam param, MultipartFile templateFile) {
if (param == null || StrUtil.isBlank(param.getId())) {
throw new BusinessException(CommonResponseEnum.FAIL, "模板ID不能为空");
}
ReportModelPO model = requireNormal(param.getId());
if (!allowEdit(model.getState())) {
throw new BusinessException(CommonResponseEnum.FAIL, "仅编制或退回状态的模板允许编辑");
}
String name = resolveName(param.getName());
checkNameUnique(name, model.getId());
String newStoredPath = null;
if (templateFile != null && !templateFile.isEmpty()) {
ReportModelFileStorageService.StoredFile storedFile = reportModelFileStorageService.save(templateFile);
newStoredPath = storedFile.getPath();
model.setPath(storedFile.getPath());
model.setFileName(storedFile.getFileName());
}
try {
model.setName(name);
model.setUpdateBy(resolveCurrentUserId());
model.setUpdateTime(LocalDateTime.now());
return this.updateById(model);
} catch (RuntimeException exception) {
reportModelFileStorageService.deleteQuietly(newStoredPath);
throw exception;
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteReportModels(List<String> ids) {
if (ids == null || ids.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "模板ID不能为空");
}
List<String> validIds = ids.stream().filter(StrUtil::isNotBlank).collect(Collectors.toList());
if (validIds.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "模板ID不能为空");
}
return this.lambdaUpdate()
.set(ReportModelPO::getStatus, ReportModelConst.STATUS_DELETED)
.set(ReportModelPO::getState, ReportModelConst.STATE_DELETED)
.set(ReportModelPO::getUpdateBy, resolveCurrentUserId())
.set(ReportModelPO::getUpdateTime, LocalDateTime.now())
.in(ReportModelPO::getId, validIds)
.eq(ReportModelPO::getStatus, ReportModelConst.STATUS_NORMAL)
.update();
}
@Override
public ReportModelVO getReportModel(String id) {
ReportModelPO model = requireNormal(id);
return toVO(model, resolveUserNameMap(Arrays.asList(model)));
}
@Override
public ReportModelPO requireNormal(String id) {
if (StrUtil.isBlank(id)) {
throw new BusinessException(CommonResponseEnum.FAIL, "模板ID不能为空");
}
ReportModelPO model = this.lambdaQuery()
.eq(ReportModelPO::getId, id)
.eq(ReportModelPO::getStatus, ReportModelConst.STATUS_NORMAL)
.one();
if (model == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "模板不存在或已删除");
}
return model;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean submitAudit(ReportModelParam.SubmitAuditParam param) {
if (param == null || StrUtil.isBlank(param.getId())) {
throw new BusinessException(CommonResponseEnum.FAIL, "模板ID不能为空");
}
ReportModelPO model = requireNormal(param.getId());
if (!ReportModelConst.STATE_EDITING.equals(model.getState())) {
throw new BusinessException(CommonResponseEnum.FAIL, "仅编制状态的模板允许提交审核");
}
model.setState(ReportModelConst.STATE_AUDITING);
model.setCheckId(null);
model.setCheckTime(null);
model.setCheckResult(null);
model.setUpdateBy(resolveCurrentUserId());
model.setUpdateTime(LocalDateTime.now());
return this.updateById(model);
}
private boolean allowEdit(String state) {
return ReportModelConst.STATE_EDITING.equals(state) || ReportModelConst.STATE_REJECTED.equals(state);
}
private void checkNameUnique(String name, String excludeId) {
LambdaQueryWrapper<ReportModelPO> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(ReportModelPO::getName, name)
.eq(ReportModelPO::getStatus, ReportModelConst.STATUS_NORMAL)
.ne(StrUtil.isNotBlank(excludeId), ReportModelPO::getId, excludeId);
if (this.count(wrapper) > 0) {
throw new BusinessException(CommonResponseEnum.FAIL, "模板名称已存在");
}
}
private String resolveName(String name) {
String result = trimToNull(name);
if (result == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "模板名称不能为空");
}
if (result.length() > 32) {
throw new BusinessException(CommonResponseEnum.FAIL, "模板名称不能超过32个字符");
}
return result;
}
private LocalDateTime parseDateTime(String value, String errorMessage) {
if (StrUtil.isBlank(value)) {
return null;
}
try {
return LocalDateTime.parse(value.trim(), DATE_TIME_FORMATTER);
} catch (DateTimeParseException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, errorMessage);
}
}
private ReportModelVO toVO(ReportModelPO model) {
return toVO(model, resolveUserNameMap(Arrays.asList(model)));
}
private ReportModelVO toVO(ReportModelPO model, Map<String, String> userNameMap) {
ReportModelVO vo = new ReportModelVO();
BeanUtil.copyProperties(model, vo);
vo.setCreateBy(resolveUserName(model.getCreateBy(), userNameMap));
vo.setUpdateBy(resolveUserName(model.getUpdateBy(), userNameMap));
vo.setEditorName(resolveEditorName(model, userNameMap));
vo.setCheckerName(resolveUserName(model.getCheckId(), userNameMap));
return vo;
}
/**
* 列表“编辑人”优先取更新人,新增后若尚未产生独立更新记录,则回退到创建人。
*/
private Map<String, String> resolveUserNameMap(List<ReportModelPO> models) {
List<String> userIds = models.stream()
.flatMap(model -> Arrays.asList(model.getCreateBy(), model.getUpdateBy(), model.getCheckId()).stream())
.filter(StrUtil::isNotBlank)
.collect(Collectors.toList());
return reportModelUserNameResolver.resolveUserNames(userIds);
}
private String resolveEditorName(ReportModelPO model, Map<String, String> userNameMap) {
if (model == null) {
return null;
}
if (StrUtil.isNotBlank(model.getUpdateBy())) {
return resolveUserName(model.getUpdateBy(), userNameMap);
}
return StrUtil.isNotBlank(model.getCreateBy()) ? resolveUserName(model.getCreateBy(), userNameMap) : null;
}
private String resolveUserName(String userId, Map<String, String> userNameMap) {
if (StrUtil.isBlank(userId)) {
return null;
}
String userName = userNameMap == null ? null : userNameMap.get(userId);
return StrUtil.isNotBlank(userName) ? userName : userId;
}
private String resolveCurrentUserId() {
String userId = RequestUtil.getUserId();
return StrUtil.isBlank(userId) ? null : userId;
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String text = value.trim();
return text.isEmpty() ? null : text;
}
}

View File

@@ -0,0 +1,39 @@
package com.njcn.gather.aireport.reportmodel.component;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.util.ReflectionTestUtils;
import java.nio.file.Files;
import java.nio.file.Path;
public class ReportModelFileStorageServiceTest {
@Test
public void saveSanitizesFileNameAndReturnsRelativePath() throws Exception {
Path filesRoot = Files.createTempDirectory("files-root");
ReportModelFileStorageService service = new ReportModelFileStorageService();
ReflectionTestUtils.setField(service, "filesPath", filesRoot.toString());
ReportModelFileStorageService.StoredFile storedFile = service.save(new MockMultipartFile("templateFile", "../a:b.docx",
"application/octet-stream", "content".getBytes("UTF-8")));
String storedPath = storedFile.getPath();
Assert.assertFalse(storedPath.contains(".."));
Assert.assertTrue(storedPath.endsWith("-a_b.docx"));
Assert.assertEquals("a_b.docx", storedFile.getFileName());
Assert.assertTrue(service.resolveDownloadPath(storedPath)
.startsWith(filesRoot.resolve("report-model").toAbsolutePath().normalize()));
Assert.assertTrue(Files.exists(service.resolveDownloadPath(storedPath)));
}
@Test(expected = IllegalArgumentException.class)
public void resolveDownloadPathRejectsPathOutsideStorageRoot() throws Exception {
Path filesRoot = Files.createTempDirectory("files-root");
ReportModelFileStorageService service = new ReportModelFileStorageService();
ReflectionTestUtils.setField(service, "filesPath", filesRoot.toString());
service.resolveDownloadPath("../outside.docx");
}
}

View File

@@ -0,0 +1,30 @@
package com.njcn.gather.aireport.reportmodel.component;
import com.njcn.gather.aireport.reportmodel.mapper.ReportModelMapper;
import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelUserNameVO;
import org.junit.Assert;
import org.junit.Test;
import java.util.Arrays;
import java.util.Map;
import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ReportModelUserNameResolverTest {
@Test
public void resolveUserNamesReturnsSysUserNameById() {
ReportModelMapper mapper = mock(ReportModelMapper.class);
ReportModelUserNameVO user = new ReportModelUserNameVO();
user.setId("user-001");
user.setName("张三");
when(mapper.selectUserNamesByIds(anyCollection())).thenReturn(Arrays.asList(user));
ReportModelUserNameResolver resolver = new ReportModelUserNameResolver(mapper);
Map<String, String> result = resolver.resolveUserNames(Arrays.asList("user-001"));
Assert.assertEquals("张三", result.get("user-001"));
}
}

View File

@@ -0,0 +1,33 @@
package com.njcn.gather.aireport.reportmodel.pojo.vo;
import org.junit.Assert;
import org.junit.Test;
public class ReportModelVODefaultDisplayTest {
@Test
public void stringFieldsUseDashWhenValueMissing() {
ReportModelVO vo = new ReportModelVO();
vo.setCheckerName(" ");
vo.setCheckResult(null);
vo.setEditorName("");
vo.setUpdateBy(" ");
vo.setFileName(null);
Assert.assertEquals("-", vo.getCheckerName());
Assert.assertEquals("-", vo.getCheckResult());
Assert.assertEquals("-", vo.getEditorName());
Assert.assertEquals("-", vo.getUpdateBy());
Assert.assertEquals("-", vo.getFileName());
}
@Test
public void stringFieldsKeepOriginalTextWhenValuePresent() {
ReportModelVO vo = new ReportModelVO();
vo.setCheckerName("审核人A");
vo.setCheckResult("审核通过");
Assert.assertEquals("审核人A", vo.getCheckerName());
Assert.assertEquals("审核通过", vo.getCheckResult());
}
}

View File

@@ -0,0 +1,23 @@
package com.njcn.gather.aireport.reportmodel.pojo.vo;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Assert;
import org.junit.Test;
import java.time.LocalDateTime;
public class ReportModelVOTimeFormatTest {
@Test
public void timeJsonUsesStandardTwentyFourHourFormat() throws Exception {
ReportModelVO vo = new ReportModelVO();
vo.setCreateTime(LocalDateTime.of(2026, 6, 25, 13, 19, 25));
vo.setUpdateTime(LocalDateTime.of(2026, 6, 25, 13, 20, 41));
String json = new ObjectMapper().writeValueAsString(vo);
Assert.assertTrue(json.contains("\"createTime\":\"2026-06-25 13:19:25\""));
Assert.assertTrue(json.contains("\"updateTime\":\"2026-06-25 13:20:41\""));
Assert.assertFalse(json.contains("2026-06-25T13"));
}
}

View File

@@ -0,0 +1,67 @@
package com.njcn.gather.aireport.reportmodel.service.impl;
import com.njcn.gather.aireport.reportmodel.component.ReportModelFileStorageService;
import com.njcn.gather.aireport.reportmodel.component.ReportModelUserNameResolver;
import com.njcn.gather.aireport.reportmodel.pojo.constant.ReportModelConst;
import com.njcn.gather.aireport.reportmodel.pojo.param.ReportModelParam;
import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO;
import org.junit.Assert;
import org.junit.Test;
import java.time.LocalDateTime;
import static org.mockito.Mockito.mock;
public class ReportModelServiceImplTest {
@Test
public void submitAuditShouldResetAuditFieldsWhenMoveToAuditing() {
ReportModelPO model = new ReportModelPO();
model.setId("model-001");
model.setState(ReportModelConst.STATE_EDITING);
model.setStatus(ReportModelConst.STATUS_NORMAL);
model.setCheckId("checker-001");
model.setCheckTime(LocalDateTime.of(2026, 6, 26, 9, 30, 0));
model.setCheckResult("已有审核意见");
TestableReportModelServiceImpl service = new TestableReportModelServiceImpl(model);
ReportModelParam.SubmitAuditParam param = new ReportModelParam.SubmitAuditParam();
param.setId("model-001");
boolean result = service.submitAudit(param);
Assert.assertTrue(result);
Assert.assertEquals(ReportModelConst.STATE_AUDITING, model.getState());
Assert.assertNull(model.getCheckId());
Assert.assertNull(model.getCheckTime());
Assert.assertNull(model.getCheckResult());
Assert.assertSame(model, service.getUpdatedModel());
Assert.assertNotNull(model.getUpdateTime());
}
private static class TestableReportModelServiceImpl extends ReportModelServiceImpl {
private final ReportModelPO model;
private ReportModelPO updatedModel;
private TestableReportModelServiceImpl(ReportModelPO model) {
super(mock(ReportModelFileStorageService.class), mock(ReportModelUserNameResolver.class));
this.model = model;
}
@Override
public ReportModelPO requireNormal(String id) {
return model;
}
@Override
public boolean updateById(ReportModelPO entity) {
this.updatedModel = entity;
return true;
}
private ReportModelPO getUpdatedModel() {
return updatedModel;
}
}
}

View File

@@ -11,6 +11,7 @@
</parent>
<artifactId>test-device</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
@@ -30,5 +31,33 @@
<artifactId>spingboot2.3.12</artifactId>
<version>2.3.12</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>system</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,149 @@
package com.njcn.gather.aireport.testdevice.component;
import cn.hutool.core.util.StrUtil;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
@Component
public class TestDeviceReportStorageService {
private static final String DEFAULT_STORAGE_DIR = "data/test-device-report";
private static final String TEST_DEVICE_REPORT_STORAGE_DIR = "test-device-report";
private static final DateTimeFormatter DATE_DIR_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
private static final long MAX_REPORT_SIZE = 10L * 1024L * 1024L;
private static final int MAX_ORIGINAL_NAME_LENGTH = 80;
@Value("${files.path:}")
private String filesPath;
public String save(MultipartFile reportFile) {
if (reportFile == null || reportFile.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "校准报告不能为空");
}
validatePdf(reportFile.getOriginalFilename(), reportFile.getSize());
try (InputStream inputStream = reportFile.getInputStream()) {
return savePdf(reportFile.getOriginalFilename(), inputStream, reportFile.getSize());
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "保存校准报告失败");
}
}
public String savePdf(String originalFilename, InputStream inputStream, long size) {
validatePdf(originalFilename, size);
try {
Path storageRoot = resolveStorageRoot();
String dateDir = LocalDate.now().format(DATE_DIR_FORMATTER);
Path storageDir = storageRoot.resolve(dateDir).normalize();
ensurePathInRoot(storageRoot, storageDir);
Files.createDirectories(storageDir);
String fileName = UUID.randomUUID().toString().replace("-", "") + "-" + sanitizeFileName(originalFilename);
Path target = storageDir.resolve(fileName).normalize();
ensurePathInRoot(storageRoot, target);
copyWithSizeLimit(inputStream, target);
return storageRoot.relativize(target).toString().replace('\\', '/');
} catch (BusinessException exception) {
throw exception;
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "保存校准报告失败");
}
}
public Path resolveDownloadPath(String storedPath) {
if (StrUtil.isBlank(storedPath)) {
throw new BusinessException(CommonResponseEnum.FAIL, "校准报告路径不能为空");
}
Path storageRoot = resolveStorageRoot();
Path target = storageRoot.resolve(storedPath).normalize();
ensurePathInRoot(storageRoot, target);
return target;
}
public String resolveDownloadFileName(String storedPath) {
Path fileName = Paths.get(storedPath).getFileName();
if (fileName == null) {
return "校准报告.pdf";
}
String name = fileName.toString();
int separatorIndex = name.indexOf('-');
return separatorIndex >= 0 && separatorIndex + 1 < name.length() ? name.substring(separatorIndex + 1) : name;
}
public void deleteQuietly(String storedPath) {
try {
if (StrUtil.isNotBlank(storedPath)) {
Files.deleteIfExists(resolveDownloadPath(storedPath));
}
} catch (Exception ignored) {
// 文件清理失败不能覆盖原始业务结果。
}
}
private void validatePdf(String originalFilename, long size) {
if (StrUtil.isBlank(originalFilename) || !originalFilename.toLowerCase().endsWith(".pdf")) {
throw new BusinessException(CommonResponseEnum.FAIL, "校准报告仅支持PDF文件");
}
if (size > MAX_REPORT_SIZE) {
throw new BusinessException(CommonResponseEnum.FAIL, "校准报告不能超过10MB");
}
}
private void copyWithSizeLimit(InputStream inputStream, Path target) throws IOException {
Path tempFile = Files.createTempFile(target.getParent(), "upload-", ".tmp");
long bytes = 0L;
byte[] buffer = new byte[8192];
try {
int length;
try (InputStream source = inputStream;
OutputStream targetStream = Files.newOutputStream(tempFile)) {
while ((length = source.read(buffer)) != -1) {
bytes += length;
if (bytes > MAX_REPORT_SIZE) {
throw new BusinessException(CommonResponseEnum.FAIL, "校准报告不能超过10MB");
}
targetStream.write(buffer, 0, length);
}
}
Files.move(tempFile, target, StandardCopyOption.REPLACE_EXISTING);
} finally {
Files.deleteIfExists(tempFile);
}
}
private Path resolveStorageRoot() {
String configuredPath = StrUtil.isBlank(filesPath) ? null : filesPath.trim();
Path path = configuredPath == null ? Paths.get(DEFAULT_STORAGE_DIR) : Paths.get(configuredPath, TEST_DEVICE_REPORT_STORAGE_DIR);
return path.toAbsolutePath().normalize();
}
private void ensurePathInRoot(Path storageRoot, Path target) {
if (!target.startsWith(storageRoot)) {
throw new BusinessException(CommonResponseEnum.FAIL, "校准报告路径不合法");
}
}
private String sanitizeFileName(String originalFilename) {
String fileName = Paths.get(originalFilename).getFileName().toString();
String sanitized = fileName.replaceAll("[\\\\/:*?\"<>|]", "_");
if (sanitized.length() > MAX_ORIGINAL_NAME_LENGTH) {
String suffix = sanitized.toLowerCase().endsWith(".pdf") ? ".pdf" : "";
int keepLength = MAX_ORIGINAL_NAME_LENGTH - suffix.length();
sanitized = sanitized.substring(0, keepLength) + suffix;
}
return StrUtil.isBlank(sanitized) ? "report.pdf" : sanitized;
}
}

View File

@@ -0,0 +1,165 @@
package com.njcn.gather.aireport.testdevice.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.constant.OperateType;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.gather.aireport.testdevice.component.TestDeviceReportStorageService;
import com.njcn.gather.aireport.testdevice.pojo.param.TestDeviceParam;
import com.njcn.gather.aireport.testdevice.pojo.po.TestDevicePO;
import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceImportResultVO;
import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceVO;
import com.njcn.gather.aireport.testdevice.service.TestDeviceService;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.HttpResultUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
@Api(tags = "检测设备管理")
@RestController
@RequestMapping("/api/test-device")
@RequiredArgsConstructor
public class TestDeviceController extends BaseController {
private final TestDeviceService testDeviceService;
private final TestDeviceReportStorageService reportStorageService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询检测设备列表")
@PostMapping("/list")
public HttpResult<Page<TestDeviceVO>> list(@RequestBody(required = false) TestDeviceParam.QueryParam param) {
String methodDescribe = getMethodDescribe("list");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
testDeviceService.listTestDevices(param), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD)
@ApiOperation("新增检测设备")
@PostMapping(value = "/add", consumes = {"multipart/form-data"})
public HttpResult<Boolean> add(@RequestPart("request") @Validated TestDeviceParam.AddParam param,
@RequestPart("reportFile") MultipartFile reportFile) {
String methodDescribe = getMethodDescribe("add");
boolean result = testDeviceService.addTestDevice(param, reportFile);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
@ApiOperation("编辑检测设备")
@PostMapping(value = "/update", consumes = {"multipart/form-data"})
public HttpResult<Boolean> update(@RequestPart("request") @Validated TestDeviceParam.UpdateParam param,
@RequestPart(value = "reportFile", required = false) MultipartFile reportFile) {
String methodDescribe = getMethodDescribe("update");
boolean result = testDeviceService.updateTestDevice(param, reportFile);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DELETE)
@ApiOperation("删除检测设备")
@PostMapping("/delete")
public HttpResult<Boolean> delete(@RequestBody List<String> ids) {
String methodDescribe = getMethodDescribe("delete");
boolean result = testDeviceService.deleteTestDevices(ids);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询检测设备详情")
@ApiImplicitParam(name = "id", value = "检测设备ID", required = true)
@GetMapping("/{id}")
public HttpResult<TestDeviceVO> detail(@PathVariable("id") String id) {
String methodDescribe = getMethodDescribe("detail");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
testDeviceService.getTestDevice(id), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("下载检测设备导入模板")
@GetMapping("/template")
public void template() {
testDeviceService.downloadTemplate();
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD)
@ApiOperation("导入检测设备")
@PostMapping(value = "/import", consumes = {"multipart/form-data"})
public HttpResult<TestDeviceImportResultVO> importExcel(@RequestPart("file") MultipartFile file,
@RequestPart("reportZip") MultipartFile reportZip) {
String methodDescribe = getMethodDescribe("importExcel");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
testDeviceService.importTestDevices(file, reportZip), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("导出检测设备")
@GetMapping("/export")
public void export(@ModelAttribute TestDeviceParam.QueryParam param) {
testDeviceService.exportTestDevices(param);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("下载检测设备校准报告")
@ApiImplicitParam(name = "id", value = "检测设备ID", required = true)
@GetMapping("/{id}/report")
public ResponseEntity<InputStreamResource> report(@PathVariable("id") String id) {
return buildReportResponse(id, false);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("预览检测设备校准报告")
@ApiImplicitParam(name = "id", value = "检测设备ID", required = true)
@GetMapping("/{id}/report/preview")
public ResponseEntity<InputStreamResource> previewReport(@PathVariable("id") String id) {
return buildReportResponse(id, true);
}
private ResponseEntity<InputStreamResource> buildReportResponse(String id, boolean preview) {
TestDevicePO device = testDeviceService.requireNormal(id);
if (device == null || device.getValidityReport() == null || device.getValidityReport().trim().isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "校准报告文件不存在");
}
Path filePath = reportStorageService.resolveDownloadPath(device.getValidityReport());
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
throw new BusinessException(CommonResponseEnum.FAIL, "校准报告文件不存在");
}
try {
String fileName = reportStorageService.resolveDownloadFileName(device.getValidityReport());
String encodedFileName = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20");
InputStream inputStream = Files.newInputStream(filePath);
return ResponseEntity.ok()
.contentType(preview ? MediaType.APPLICATION_PDF : MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION,
(preview ? "inline" : "attachment") + "; filename*=UTF-8''" + encodedFileName)
.body(new InputStreamResource(inputStream));
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "读取校准报告失败");
}
}
}

View File

@@ -0,0 +1,61 @@
package com.njcn.gather.aireport.testdevice.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.gather.aireport.testdevice.pojo.param.TestDeviceParam;
import com.njcn.gather.aireport.testdevice.pojo.po.TestDevicePO;
import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceTypeNameVO;
import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface TestDeviceMapper extends BaseMapper<TestDevicePO> {
@Select({
"<script>",
"select d.id, d.type, dt.name as typeName, d.no, d.validity_period as validityPeriod,",
"d.validity_report as validityReport, d.state, d.status, d.create_by as createBy,",
"creator.name as createByName, d.create_time as createTime, d.update_by as updateBy,",
"updater.name as updateByName, d.update_time as updateTime",
"from ai_testdevice d",
"left join cs_dev_type dt on d.type = dt.id",
"left join sys_user creator on d.create_by = creator.id",
"left join sys_user updater on d.update_by = updater.id",
"where d.status = 1",
"<if test='param.keyword != null and param.keyword != \"\"'>",
"and (d.no like concat('%', #{param.keyword}, '%') or dt.name like concat('%', #{param.keyword}, '%'))",
"</if>",
"<if test='param.searchBeginTime != null and param.searchBeginTime != \"\"'>",
"and d.create_time &gt;= #{param.searchBeginTime}",
"</if>",
"<if test='param.searchEndTime != null and param.searchEndTime != \"\"'>",
"and d.create_time &lt;= #{param.searchEndTime}",
"</if>",
"order by d.create_time desc",
"</script>"
})
Page<TestDeviceVO> selectDevicePage(Page<TestDeviceVO> page, @Param("param") TestDeviceParam.QueryParam param);
@Select({
"select d.id, d.type, dt.name as typeName, d.no, d.validity_period as validityPeriod,",
"d.validity_report as validityReport, d.state, d.status, d.create_by as createBy,",
"creator.name as createByName, d.create_time as createTime, d.update_by as updateBy,",
"updater.name as updateByName, d.update_time as updateTime",
"from ai_testdevice d",
"left join cs_dev_type dt on d.type = dt.id",
"left join sys_user creator on d.create_by = creator.id",
"left join sys_user updater on d.update_by = updater.id",
"where d.id = #{id} and d.status = 1"
})
TestDeviceVO selectDeviceDetail(@Param("id") String id);
@Select("select count(1) from cs_dev_type where id = #{typeId} and State = 1")
int countNormalDeviceType(@Param("typeId") String typeId);
@Select("select id, name from cs_dev_type where State = 1")
List<TestDeviceTypeNameVO> selectNormalDeviceTypes();
}

View File

@@ -0,0 +1,12 @@
package com.njcn.gather.aireport.testdevice.pojo.constant;
public final class TestDeviceConst {
public static final String STATE_RUNNING = "01";
public static final String STATE_RETIRED = "02";
public static final Integer STATUS_NORMAL = 1;
public static final Integer STATUS_DELETED = 0;
private TestDeviceConst() {
}
}

View File

@@ -0,0 +1,49 @@
package com.njcn.gather.aireport.testdevice.pojo.param;
import com.njcn.web.pojo.param.BaseParam;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
public class TestDeviceParam {
@Data
@EqualsAndHashCode(callSuper = true)
public static class QueryParam extends BaseParam {
@ApiModelProperty("关键字,匹配设备型号名称或设备编号")
private String keyword;
@ApiModelProperty("创建开始时间yyyy-MM-dd HH:mm:ss")
private String searchBeginTime;
@ApiModelProperty("创建结束时间yyyy-MM-dd HH:mm:ss")
private String searchEndTime;
}
@Data
public static class AddParam {
@ApiModelProperty("设备型号ID")
@NotBlank(message = "设备型号不能为空")
private String type;
@ApiModelProperty("设备编号")
@NotBlank(message = "设备编号不能为空")
private String no;
@ApiModelProperty("校准有效期yyyy-MM-dd")
@NotBlank(message = "校准有效期不能为空")
private String validityPeriod;
@ApiModelProperty("设备状态01-在运02-退运")
private String state;
}
@Data
public static class UpdateParam extends AddParam {
@ApiModelProperty("检测设备ID")
@NotBlank(message = "检测设备ID不能为空")
private String id;
}
}

View File

@@ -0,0 +1,39 @@
package com.njcn.gather.aireport.testdevice.pojo.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@TableName("ai_testdevice")
public class TestDevicePO implements Serializable {
private static final long serialVersionUID = 4153967564253668210L;
@TableId("id")
private String id;
@TableField("type")
private String type;
@TableField("no")
private String no;
@TableField("validity_period")
private LocalDate validityPeriod;
@TableField("validity_report")
private String validityReport;
@TableField("state")
private String state;
@TableField("status")
private Integer status;
@TableField("create_by")
private String createBy;
@TableField("create_time")
private LocalDateTime createTime;
@TableField("update_by")
private String updateBy;
@TableField("update_time")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,32 @@
package com.njcn.gather.aireport.testdevice.pojo.vo;
import cn.afterturn.easypoi.excel.annotation.Excel;
import lombok.Data;
import java.io.Serializable;
@Data
public class TestDeviceExcelVO implements Serializable {
private static final long serialVersionUID = 8111740807955262764L;
@Excel(name = "设备型号", width = 25)
private String typeName;
@Excel(name = "设备编号", width = 25)
private String no;
@Excel(name = "校准有效期", width = 20)
private String validityPeriod;
@Excel(name = "设备状态", width = 15)
private String stateName;
@Excel(name = "校准报告文件名", width = 35)
private String validityReportName;
@Excel(name = "创建时间", width = 25)
private String createTime;
@Excel(name = "创建人", width = 20)
private String createByName;
}

View File

@@ -0,0 +1,13 @@
package com.njcn.gather.aireport.testdevice.pojo.vo;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class TestDeviceImportResultVO {
private int successCount;
private int failCount;
private List<String> failReasons = new ArrayList<String>();
}

View File

@@ -0,0 +1,9 @@
package com.njcn.gather.aireport.testdevice.pojo.vo;
import lombok.Data;
@Data
public class TestDeviceTypeNameVO {
private String id;
private String name;
}

View File

@@ -0,0 +1,59 @@
package com.njcn.gather.aireport.testdevice.pojo.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
public class TestDeviceVO {
@ApiModelProperty("检测设备ID")
private String id;
@ApiModelProperty("设备型号ID")
private String type;
@ApiModelProperty("设备型号名称")
private String typeName;
@ApiModelProperty("设备编号")
private String no;
@ApiModelProperty("校准有效期")
@JsonFormat(pattern = "yyyy-MM-dd")
@JsonDeserialize(using = LocalDateDeserializer.class)
@JsonSerialize(using = LocalDateSerializer.class)
private LocalDate validityPeriod;
@ApiModelProperty("校准报告存储路径")
private String validityReport;
@ApiModelProperty("校准报告文件名")
private String validityReportName;
@ApiModelProperty("校准报告下载地址")
private String validityReportUrl;
@ApiModelProperty("设备状态01-在运02-退役")
private String state;
@ApiModelProperty("逻辑状态")
private Integer status;
@ApiModelProperty("创建人ID")
private String createBy;
@ApiModelProperty("创建人显示名")
private String createByName;
@ApiModelProperty("创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime createTime;
@ApiModelProperty("更新人ID")
private String updateBy;
@ApiModelProperty("更新人显示名")
private String updateByName;
@ApiModelProperty("更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,32 @@
package com.njcn.gather.aireport.testdevice.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.gather.aireport.testdevice.pojo.param.TestDeviceParam;
import com.njcn.gather.aireport.testdevice.pojo.po.TestDevicePO;
import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceImportResultVO;
import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceVO;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
public interface TestDeviceService extends IService<TestDevicePO> {
Page<TestDeviceVO> listTestDevices(TestDeviceParam.QueryParam param);
boolean addTestDevice(TestDeviceParam.AddParam param, MultipartFile reportFile);
boolean updateTestDevice(TestDeviceParam.UpdateParam param, MultipartFile reportFile);
boolean deleteTestDevices(List<String> ids);
TestDeviceVO getTestDevice(String id);
TestDevicePO requireNormal(String id);
void downloadTemplate();
TestDeviceImportResultVO importTestDevices(MultipartFile file, MultipartFile reportZip);
void exportTestDevices(TestDeviceParam.QueryParam param);
}

View File

@@ -0,0 +1,666 @@
package com.njcn.gather.aireport.testdevice.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.testdevice.component.TestDeviceReportStorageService;
import com.njcn.gather.aireport.testdevice.mapper.TestDeviceMapper;
import com.njcn.gather.aireport.testdevice.pojo.constant.TestDeviceConst;
import com.njcn.gather.aireport.testdevice.pojo.param.TestDeviceParam;
import com.njcn.gather.aireport.testdevice.pojo.po.TestDevicePO;
import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceExcelVO;
import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceImportResultVO;
import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceTypeNameVO;
import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceVO;
import com.njcn.gather.aireport.testdevice.service.TestDeviceService;
import com.njcn.gather.system.util.ExportFileNameUtil;
import com.njcn.web.factory.PageFactory;
import com.njcn.web.utils.ExcelUtil;
import com.njcn.web.utils.HttpServletUtil;
import com.njcn.web.utils.RequestUtil;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@Slf4j
@Service
@RequiredArgsConstructor
public class TestDeviceServiceImpl extends ServiceImpl<TestDeviceMapper, TestDevicePO> implements TestDeviceService {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final String[] TEMPLATE_HEADERS = new String[]{"设备型号", "设备编号", "校准有效期", "设备状态", "校准报告文件名"};
private static final long MAX_IMPORT_REPORT_SIZE = 10L * 1024L * 1024L;
private final TestDeviceReportStorageService reportStorageService;
@Override
public Page<TestDeviceVO> listTestDevices(TestDeviceParam.QueryParam param) {
TestDeviceParam.QueryParam query = normalizeQueryParam(param);
Page<TestDeviceVO> page = this.baseMapper.selectDevicePage(
new Page<TestDeviceVO>(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), query);
page.getRecords().forEach(this::fillReportDisplayFields);
return page;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean addTestDevice(TestDeviceParam.AddParam param, MultipartFile reportFile) {
NormalizedDevice data = normalizeParam(param);
checkDeviceType(data.getType());
checkNoUnique(data.getNo(), null);
String storedPath = reportStorageService.save(reportFile);
try {
LocalDateTime now = LocalDateTime.now();
String userId = resolveCurrentUserId();
TestDevicePO device = new TestDevicePO();
device.setId(UUID.randomUUID().toString());
device.setType(data.getType());
device.setNo(data.getNo());
device.setValidityPeriod(data.getValidityPeriod());
device.setValidityReport(storedPath);
device.setState(data.getState());
device.setStatus(TestDeviceConst.STATUS_NORMAL);
device.setCreateBy(userId);
device.setCreateTime(now);
device.setUpdateBy(userId);
device.setUpdateTime(now);
boolean saved = this.save(device);
if (!saved) {
reportStorageService.deleteQuietly(storedPath);
}
return saved;
} catch (RuntimeException exception) {
reportStorageService.deleteQuietly(storedPath);
throw exception;
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateTestDevice(TestDeviceParam.UpdateParam param, MultipartFile reportFile) {
if (param == null || StrUtil.isBlank(param.getId())) {
throw new BusinessException(CommonResponseEnum.FAIL, "检测设备ID不能为空");
}
TestDevicePO device = requireNormal(param.getId());
NormalizedDevice data = normalizeParam(param);
checkDeviceType(data.getType());
checkNoUnique(data.getNo(), device.getId());
String oldReport = device.getValidityReport();
String newReport = null;
if (reportFile != null && !reportFile.isEmpty()) {
newReport = reportStorageService.save(reportFile);
device.setValidityReport(newReport);
} else if (StrUtil.isBlank(oldReport)) {
throw new BusinessException(CommonResponseEnum.FAIL, "校准报告不能为空");
}
try {
device.setType(data.getType());
device.setNo(data.getNo());
device.setValidityPeriod(data.getValidityPeriod());
device.setState(data.getState());
device.setUpdateBy(resolveCurrentUserId());
device.setUpdateTime(LocalDateTime.now());
boolean updated = this.updateById(device);
if (!updated) {
reportStorageService.deleteQuietly(newReport);
return false;
}
if (StrUtil.isNotBlank(newReport)) {
reportStorageService.deleteQuietly(oldReport);
}
return true;
} catch (RuntimeException exception) {
reportStorageService.deleteQuietly(newReport);
throw exception;
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteTestDevices(List<String> ids) {
List<String> validIds = normalizeIds(ids);
if (validIds.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "检测设备ID不能为空");
}
return this.lambdaUpdate()
.set(TestDevicePO::getStatus, TestDeviceConst.STATUS_DELETED)
.set(TestDevicePO::getUpdateBy, resolveCurrentUserId())
.set(TestDevicePO::getUpdateTime, LocalDateTime.now())
.in(TestDevicePO::getId, validIds)
.eq(TestDevicePO::getStatus, TestDeviceConst.STATUS_NORMAL)
.update();
}
@Override
public TestDeviceVO getTestDevice(String id) {
TestDeviceVO vo = this.baseMapper.selectDeviceDetail(requireText(id, "检测设备ID不能为空"));
if (vo == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "检测设备不存在或已删除");
}
fillReportDisplayFields(vo);
return vo;
}
@Override
public TestDevicePO requireNormal(String id) {
String normalizedId = requireText(id, "检测设备ID不能为空");
TestDevicePO device = this.lambdaQuery()
.eq(TestDevicePO::getId, normalizedId)
.eq(TestDevicePO::getStatus, TestDeviceConst.STATUS_NORMAL)
.one();
if (device == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "检测设备不存在或已删除");
}
return device;
}
@Override
public void downloadTemplate() {
HttpServletResponse response = HttpServletUtil.getResponse();
try (XSSFWorkbook workbook = new XSSFWorkbook()) {
XSSFSheet sheet = workbook.createSheet("检测设备导入模板");
writeTemplateHeader(workbook, sheet);
String fileName = URLEncoder.encode(ExportFileNameUtil.appendToday("检测设备导入模板.xlsx"), "UTF-8");
response.reset();
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
try (ServletOutputStream outputStream = response.getOutputStream()) {
workbook.write(outputStream);
outputStream.flush();
}
} catch (IOException exception) {
log.error("下载检测设备导入模板失败", exception);
throw new BusinessException(CommonResponseEnum.FAIL, "下载检测设备导入模板失败");
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public TestDeviceImportResultVO importTestDevices(MultipartFile file, MultipartFile reportZip) {
if (file == null || file.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "导入文件不能为空");
}
if (reportZip == null || reportZip.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "校准报告压缩包不能为空");
}
List<TestDeviceExcelVO> rows = readImportRows(file);
TestDeviceImportResultVO result = new TestDeviceImportResultVO();
if (rows.isEmpty()) {
return result;
}
ImportArchive archive = readReportZip(reportZip);
try {
Map<String, String> typeNameIdMap = loadTypeNameIdMap();
List<NormalizedImportDevice> devices = validateImportRows(rows, archive, typeNameIdMap, result);
if (result.getFailCount() > 0) {
return result;
}
List<String> savedReports = new ArrayList<String>();
try {
LocalDateTime now = LocalDateTime.now();
String userId = resolveCurrentUserId();
List<TestDevicePO> saveRows = new ArrayList<TestDevicePO>();
for (NormalizedImportDevice data : devices) {
String storedPath;
try (InputStream inputStream = Files.newInputStream(data.getReportPath())) {
storedPath = reportStorageService.savePdf(data.getReportName(), inputStream, Files.size(data.getReportPath()));
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "读取校准报告压缩包失败");
}
savedReports.add(storedPath);
TestDevicePO device = new TestDevicePO();
device.setId(UUID.randomUUID().toString());
device.setType(data.getType());
device.setNo(data.getNo());
device.setValidityPeriod(data.getValidityPeriod());
device.setValidityReport(storedPath);
device.setState(data.getState());
device.setStatus(TestDeviceConst.STATUS_NORMAL);
device.setCreateBy(userId);
device.setCreateTime(now);
device.setUpdateBy(userId);
device.setUpdateTime(now);
saveRows.add(device);
}
boolean saved = saveRows.isEmpty() || this.saveBatch(saveRows);
if (!saved) {
savedReports.forEach(reportStorageService::deleteQuietly);
return result;
}
result.setSuccessCount(saveRows.size());
return result;
} catch (RuntimeException exception) {
savedReports.forEach(reportStorageService::deleteQuietly);
throw exception;
}
} finally {
archive.cleanup();
}
}
@Override
public void exportTestDevices(TestDeviceParam.QueryParam param) {
TestDeviceParam.QueryParam query = normalizeQueryParam(param);
Page<TestDeviceVO> page = this.baseMapper.selectDevicePage(new Page<TestDeviceVO>(1, Integer.MAX_VALUE), query);
List<TestDeviceExcelVO> exportRows = page.getRecords().stream()
.peek(this::fillReportDisplayFields)
.map(this::toExcelVO)
.collect(Collectors.toList());
ExcelUtil.exportExcel(ExportFileNameUtil.appendToday("检测设备列表.xlsx"), "检测设备列表",
TestDeviceExcelVO.class, exportRows);
}
private TestDeviceParam.QueryParam normalizeQueryParam(TestDeviceParam.QueryParam param) {
TestDeviceParam.QueryParam query = param == null ? new TestDeviceParam.QueryParam() : param;
query.setKeyword(trimToNull(query.getKeyword()));
if (StrUtil.isNotBlank(query.getSearchBeginTime())) {
query.setSearchBeginTime(formatDateTime(parseDateTime(query.getSearchBeginTime(), "开始时间格式不正确")));
}
if (StrUtil.isNotBlank(query.getSearchEndTime())) {
query.setSearchEndTime(formatDateTime(parseDateTime(query.getSearchEndTime(), "结束时间格式不正确")));
}
return query;
}
private NormalizedDevice normalizeParam(TestDeviceParam.AddParam param) {
if (param == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "检测设备参数不能为空");
}
NormalizedDevice data = new NormalizedDevice();
data.setType(requireText(param.getType(), "设备型号不能为空"));
data.setNo(requireText(param.getNo(), "设备编号不能为空", 64, "设备编号不能超过64个字符"));
data.setValidityPeriod(parseDate(requireText(param.getValidityPeriod(), "校准有效期不能为空")));
data.setState(normalizeState(param.getState()));
return data;
}
private List<NormalizedImportDevice> validateImportRows(List<TestDeviceExcelVO> rows, ImportArchive archive,
Map<String, String> typeNameIdMap, TestDeviceImportResultVO result) {
Set<String> fileNos = new HashSet<String>();
List<NormalizedImportDevice> devices = new ArrayList<NormalizedImportDevice>();
for (int i = 0; i < rows.size(); i++) {
int rowNumber = i + 2;
try {
TestDeviceExcelVO row = rows.get(i);
NormalizedImportDevice device = new NormalizedImportDevice();
String typeName = requireText(row.getTypeName(), "设备型号不能为空");
String typeId = typeNameIdMap.get(typeName);
if (StrUtil.isBlank(typeId)) {
throw new BusinessException(CommonResponseEnum.FAIL, "设备型号不存在或已删除");
}
device.setType(typeId);
device.setNo(requireText(row.getNo(), "设备编号不能为空", 64, "设备编号不能超过64个字符"));
if (!fileNos.add(device.getNo())) {
throw new BusinessException(CommonResponseEnum.FAIL, "文件内存在重复设备编号");
}
checkNoUnique(device.getNo(), null);
device.setValidityPeriod(parseDate(requireText(row.getValidityPeriod(), "校准有效期不能为空")));
device.setState(normalizeState(row.getStateName()));
device.setReportName(requireText(row.getValidityReportName(), "校准报告文件名不能为空"));
Path reportPath = archive.getReportPath(device.getReportName());
if (reportPath == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "校准报告压缩包中未找到对应PDF");
}
device.setReportPath(reportPath);
devices.add(device);
} catch (BusinessException exception) {
result.setFailCount(result.getFailCount() + 1);
result.getFailReasons().add("" + rowNumber + "行:" + exception.getMessage());
}
}
return devices;
}
private List<TestDeviceExcelVO> readImportRows(MultipartFile file) {
try (InputStream inputStream = file.getInputStream();
XSSFWorkbook workbook = new XSSFWorkbook(inputStream)) {
XSSFSheet sheet = workbook.getNumberOfSheets() == 0 ? null : workbook.getSheetAt(0);
if (sheet == null || sheet.getLastRowNum() < 1) {
return Collections.emptyList();
}
List<TestDeviceExcelVO> rows = new ArrayList<TestDeviceExcelVO>();
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
if (isEmptyRow(row)) {
continue;
}
TestDeviceExcelVO excelVO = new TestDeviceExcelVO();
excelVO.setTypeName(readCell(row, 0));
excelVO.setNo(readCell(row, 1));
excelVO.setValidityPeriod(readCell(row, 2));
excelVO.setStateName(readCell(row, 3));
excelVO.setValidityReportName(readCell(row, 4));
rows.add(excelVO);
}
return rows;
} catch (IOException exception) {
log.error("读取检测设备导入文件失败", exception);
throw new BusinessException(CommonResponseEnum.FAIL, "读取导入文件失败");
}
}
ImportArchive readReportZip(MultipartFile reportZip) {
Path tempDir;
try {
tempDir = Files.createTempDirectory("test-device-import-");
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "读取校准报告压缩包失败");
}
Map<String, Path> files = new HashMap<String, Path>();
try (ZipInputStream zipInputStream = new ZipInputStream(reportZip.getInputStream())) {
ZipEntry entry;
while ((entry = zipInputStream.getNextEntry()) != null) {
if (entry.isDirectory()) {
continue;
}
String fileName = Paths.get(entry.getName()).getFileName().toString();
if (!fileName.toLowerCase().endsWith(".pdf")) {
throw new BusinessException(CommonResponseEnum.FAIL, "校准报告压缩包仅允许包含PDF文件");
}
if (files.containsKey(fileName)) {
throw new BusinessException(CommonResponseEnum.FAIL, "校准报告压缩包中存在重复文件名");
}
files.put(fileName, readZipEntryToFile(tempDir, fileName, zipInputStream));
}
return new ImportArchive(tempDir, files);
} catch (BusinessException exception) {
deleteDirectoryQuietly(tempDir);
throw exception;
} catch (IOException exception) {
deleteDirectoryQuietly(tempDir);
log.error("读取校准报告压缩包失败", exception);
throw new BusinessException(CommonResponseEnum.FAIL, "读取校准报告压缩包失败");
}
}
private Path readZipEntryToFile(Path tempDir, String fileName, ZipInputStream zipInputStream) throws IOException {
Path tempFile = tempDir.resolve(UUID.randomUUID().toString().replace("-", "") + "-" + fileName).normalize();
long bytes = 0L;
byte[] buffer = new byte[8192];
try (OutputStream outputStream = Files.newOutputStream(tempFile)) {
int length;
while ((length = zipInputStream.read(buffer)) != -1) {
bytes += length;
if (bytes > MAX_IMPORT_REPORT_SIZE) {
throw new BusinessException(CommonResponseEnum.FAIL, "校准报告不能超过10MB");
}
outputStream.write(buffer, 0, length);
}
} catch (BusinessException exception) {
Files.deleteIfExists(tempFile);
throw exception;
} catch (IOException exception) {
Files.deleteIfExists(tempFile);
throw exception;
}
return tempFile;
}
private void deleteDirectoryQuietly(Path directory) {
if (directory == null || !Files.exists(directory)) {
return;
}
try (Stream<Path> pathStream = Files.walk(directory)) {
pathStream.sorted(Comparator.reverseOrder()).forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (IOException ignored) {
// 导入临时目录清理失败不影响主流程返回。
}
});
} catch (IOException ignored) {
// 导入临时目录清理失败不影响主流程返回。
}
}
private void writeTemplateHeader(XSSFWorkbook workbook, XSSFSheet sheet) {
CellStyle style = workbook.createCellStyle();
Font font = workbook.createFont();
font.setBold(true);
style.setFont(font);
Row row = sheet.createRow(0);
for (int i = 0; i < TEMPLATE_HEADERS.length; i++) {
Cell cell = row.createCell(i);
cell.setCellValue(TEMPLATE_HEADERS[i]);
cell.setCellStyle(style);
sheet.setColumnWidth(i, 25 * 256);
}
}
private boolean isEmptyRow(Row row) {
if (row == null) {
return true;
}
for (int i = 0; i < TEMPLATE_HEADERS.length; i++) {
if (StrUtil.isNotBlank(readCell(row, i))) {
return false;
}
}
return true;
}
private String readCell(Row row, int index) {
Cell cell = row.getCell(index);
return cell == null ? null : trimToNull(new DataFormatter().formatCellValue(cell));
}
private void fillReportDisplayFields(TestDeviceVO vo) {
if (vo == null || StrUtil.isBlank(vo.getValidityReport())) {
return;
}
vo.setValidityReportName(reportStorageService.resolveDownloadFileName(vo.getValidityReport()));
vo.setValidityReportUrl("/api/test-device/" + vo.getId() + "/report");
}
private TestDeviceExcelVO toExcelVO(TestDeviceVO vo) {
TestDeviceExcelVO excelVO = new TestDeviceExcelVO();
excelVO.setTypeName(vo.getTypeName());
excelVO.setNo(vo.getNo());
excelVO.setValidityPeriod(vo.getValidityPeriod() == null ? null : DATE_FORMATTER.format(vo.getValidityPeriod()));
excelVO.setStateName(resolveStateName(vo.getState()));
excelVO.setValidityReportName(vo.getValidityReportName());
excelVO.setCreateByName(StrUtil.isNotBlank(vo.getCreateByName()) ? vo.getCreateByName() : vo.getCreateBy());
excelVO.setCreateTime(vo.getCreateTime() == null ? null : DATE_TIME_FORMATTER.format(vo.getCreateTime()));
return excelVO;
}
private Map<String, String> loadTypeNameIdMap() {
List<TestDeviceTypeNameVO> types = this.baseMapper.selectNormalDeviceTypes();
if (types == null || types.isEmpty()) {
return Collections.emptyMap();
}
return types.stream()
.filter(type -> type != null && StrUtil.isNotBlank(type.getName()) && StrUtil.isNotBlank(type.getId()))
.collect(Collectors.toMap(TestDeviceTypeNameVO::getName, TestDeviceTypeNameVO::getId, (first, second) -> first));
}
private void checkDeviceType(String type) {
if (this.baseMapper.countNormalDeviceType(type) <= 0) {
throw new BusinessException(CommonResponseEnum.FAIL, "设备型号不存在或已删除");
}
}
private void checkNoUnique(String no, String excludeId) {
LambdaQueryWrapper<TestDevicePO> wrapper = new LambdaQueryWrapper<TestDevicePO>();
wrapper.eq(TestDevicePO::getNo, no)
.ne(StrUtil.isNotBlank(excludeId), TestDevicePO::getId, excludeId);
if (this.count(wrapper) > 0) {
throw new BusinessException(CommonResponseEnum.FAIL, "设备编号已存在");
}
}
private List<String> normalizeIds(List<String> ids) {
if (ids == null || ids.isEmpty()) {
return Collections.emptyList();
}
List<String> result = new ArrayList<String>();
for (String id : ids) {
String normalizedId = trimToNull(id);
if (normalizedId != null && !result.contains(normalizedId)) {
result.add(normalizedId);
}
}
return result;
}
private LocalDate parseDate(String value) {
try {
return LocalDate.parse(value.trim(), DATE_FORMATTER);
} catch (DateTimeParseException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "校准有效期格式不正确");
}
}
private LocalDateTime parseDateTime(String value, String errorMessage) {
try {
return LocalDateTime.parse(value.trim(), DATE_TIME_FORMATTER);
} catch (DateTimeParseException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, errorMessage);
}
}
private String formatDateTime(LocalDateTime value) {
return DATE_TIME_FORMATTER.format(value);
}
private String normalizeState(String state) {
String text = trimToNull(state);
if (text == null || "在运".equals(text) || TestDeviceConst.STATE_RUNNING.equals(text)) {
return TestDeviceConst.STATE_RUNNING;
}
if ("退运".equals(text) || TestDeviceConst.STATE_RETIRED.equals(text)) {
return TestDeviceConst.STATE_RETIRED;
}
throw new BusinessException(CommonResponseEnum.FAIL, "设备状态不正确");
}
private String resolveStateName(String state) {
if (TestDeviceConst.STATE_RUNNING.equals(state)) {
return "在运";
}
if (TestDeviceConst.STATE_RETIRED.equals(state)) {
return "退运";
}
return state;
}
private String requireText(String value, String blankMessage) {
String text = trimToNull(value);
if (text == null) {
throw new BusinessException(CommonResponseEnum.FAIL, blankMessage);
}
return text;
}
private String requireText(String value, String blankMessage, int maxLength, String lengthMessage) {
String text = requireText(value, blankMessage);
if (text.length() > maxLength) {
throw new BusinessException(CommonResponseEnum.FAIL, lengthMessage);
}
return text;
}
private String resolveCurrentUserId() {
String userId = RequestUtil.getUserId();
return StrUtil.isBlank(userId) ? null : userId;
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String text = value.trim();
return text.isEmpty() ? null : text;
}
@Data
private static class NormalizedDevice {
private String type;
private String no;
private LocalDate validityPeriod;
private String state;
}
@Data
private static class NormalizedImportDevice extends NormalizedDevice {
private String reportName;
private Path reportPath;
}
static class ImportArchive {
private final Path tempDir;
private final Map<String, Path> reportFiles;
ImportArchive(Path tempDir, Map<String, Path> reportFiles) {
this.tempDir = tempDir;
this.reportFiles = reportFiles;
}
Path getReportPath(String fileName) {
return reportFiles.get(fileName);
}
void cleanup() {
if (tempDir == null || !Files.exists(tempDir)) {
return;
}
try (Stream<Path> pathStream = Files.walk(tempDir)) {
pathStream.sorted(Comparator.reverseOrder()).forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (IOException ignored) {
// 导入临时文件清理失败不影响主流程返回。
}
});
} catch (IOException ignored) {
// 导入临时目录清理失败不影响主流程返回。
}
}
}
}

View File

@@ -0,0 +1,28 @@
package com.njcn.gather.aireport.testdevice.component;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.util.ReflectionTestUtils;
import java.nio.file.Files;
import java.nio.file.Path;
public class TestDeviceReportStorageServiceTest {
@Test
public void saveUsesTestDeviceReportDirectoryUnderFilesRoot() throws Exception {
Path filesRoot = Files.createTempDirectory("files-root");
TestDeviceReportStorageService service = new TestDeviceReportStorageService();
ReflectionTestUtils.setField(service, "filesPath", filesRoot.toString());
String storedPath = service.save(new MockMultipartFile("reportFile", "../report.pdf",
"application/pdf", "%PDF-1.4".getBytes("UTF-8")));
Path expectedRoot = filesRoot.resolve("test-device-report").toAbsolutePath().normalize();
Path actualPath = service.resolveDownloadPath(storedPath);
Assert.assertTrue(actualPath.startsWith(expectedRoot));
Assert.assertTrue(storedPath.endsWith("-report.pdf"));
Assert.assertTrue(Files.exists(actualPath));
}
}

View File

@@ -0,0 +1,45 @@
package com.njcn.gather.aireport.testdevice.controller;
import com.njcn.gather.aireport.testdevice.component.TestDeviceReportStorageService;
import com.njcn.gather.aireport.testdevice.pojo.po.TestDevicePO;
import com.njcn.gather.aireport.testdevice.service.TestDeviceService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.util.ReflectionTestUtils;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
class TestDeviceControllerTest {
private final TestDeviceService testDeviceService = Mockito.mock(TestDeviceService.class);
private final TestDeviceReportStorageService reportStorageService = Mockito.mock(TestDeviceReportStorageService.class);
@Test
void previewReportShouldReturnInlinePdfResponse() throws Exception {
TestDeviceController controller = new TestDeviceController(testDeviceService, reportStorageService);
TestDevicePO device = new TestDevicePO();
device.setId("device-1");
device.setValidityReport("20260626/report.pdf");
Path tempFile = Files.createTempFile("test-device-preview-", ".pdf");
Files.write(tempFile, "%PDF-1.4".getBytes(StandardCharsets.UTF_8));
Mockito.when(testDeviceService.requireNormal("device-1")).thenReturn(device);
Mockito.when(reportStorageService.resolveDownloadPath("20260626/report.pdf")).thenReturn(tempFile);
Mockito.when(reportStorageService.resolveDownloadFileName("20260626/report.pdf")).thenReturn("report.pdf");
@SuppressWarnings("unchecked")
ResponseEntity<InputStreamResource> response = (ResponseEntity<InputStreamResource>)
ReflectionTestUtils.invokeMethod(controller, "buildReportResponse", "device-1", true);
Assertions.assertNotNull(response);
Assertions.assertEquals(MediaType.APPLICATION_PDF, response.getHeaders().getContentType());
Assertions.assertEquals("inline; filename*=UTF-8''report.pdf",
response.getHeaders().getFirst(HttpHeaders.CONTENT_DISPOSITION));
}
}

View File

@@ -0,0 +1,31 @@
package com.njcn.gather.aireport.testdevice.mapper;
import org.apache.ibatis.annotations.Select;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
import java.util.Arrays;
class TestDeviceMapperTest {
@Test
void selectQueriesShouldContainUpdateUserNameMapping() throws Exception {
assertSelectContainsUpdateUserName("selectDevicePage");
assertSelectContainsUpdateUserName("selectDeviceDetail");
}
private void assertSelectContainsUpdateUserName(String methodName) throws Exception {
Method method = Arrays.stream(TestDeviceMapper.class.getMethods())
.filter(item -> item.getName().equals(methodName))
.findFirst()
.orElseThrow(NoSuchMethodException::new);
Select select = method.getAnnotation(Select.class);
Assertions.assertNotNull(select);
String sql = String.join(" ", select.value());
Assertions.assertTrue(sql.contains("updateByName"),
methodName + " should select updateByName");
Assertions.assertTrue(sql.contains("d.update_by = updater.id"),
methodName + " should join update user");
}
}

View File

@@ -0,0 +1,161 @@
package com.njcn.gather.aireport.testdevice.service.impl;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.testdevice.component.TestDeviceReportStorageService;
import com.njcn.gather.aireport.testdevice.mapper.TestDeviceMapper;
import com.njcn.gather.aireport.testdevice.pojo.param.TestDeviceParam;
import com.njcn.gather.aireport.testdevice.pojo.po.TestDevicePO;
import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceTypeNameVO;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.util.ReflectionTestUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.LocalDate;
import java.util.Collections;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
class TestDeviceServiceImplTest {
private final TestDeviceMapper testDeviceMapper = Mockito.mock(TestDeviceMapper.class);
private final TestDeviceReportStorageService reportStorageService = Mockito.mock(TestDeviceReportStorageService.class);
@Test
void addTestDeviceShouldRejectExistingDeletedNumber() {
TestableService service = buildService();
service.countResult = 1;
Mockito.when(testDeviceMapper.countNormalDeviceType("type-1")).thenReturn(1);
TestDeviceParam.AddParam param = new TestDeviceParam.AddParam();
param.setType("type-1");
param.setNo("TD-001");
param.setValidityPeriod("2026-06-25");
BusinessException exception = Assertions.assertThrows(BusinessException.class,
() -> service.addTestDevice(param, new MockMultipartFile("reportFile", "report.pdf",
"application/pdf", "%PDF-1.4".getBytes())));
Assertions.assertEquals("设备编号已存在", exception.getMessage());
Mockito.verifyNoInteractions(reportStorageService);
}
@Test
void updateTestDeviceShouldDeleteNewReportWhenUpdateReturnsFalse() {
TestableService service = buildService();
service.countResult = 0;
service.updateResult = false;
service.requiredDevice = buildDevice("device-1", "old/report.pdf");
Mockito.when(testDeviceMapper.countNormalDeviceType("type-1")).thenReturn(1);
Mockito.when(reportStorageService.save(Mockito.any())).thenReturn("new/report.pdf");
TestDeviceParam.UpdateParam param = new TestDeviceParam.UpdateParam();
param.setId("device-1");
param.setType("type-1");
param.setNo("TD-001");
param.setValidityPeriod("2026-06-25");
boolean result = service.updateTestDevice(param,
new MockMultipartFile("reportFile", "report.pdf", "application/pdf", "%PDF-1.4".getBytes()));
Assertions.assertFalse(result);
Mockito.verify(reportStorageService).deleteQuietly("new/report.pdf");
Mockito.verify(reportStorageService, Mockito.never()).deleteQuietly("old/report.pdf");
}
@Test
void importTestDevicesShouldRejectOversizedPdfInZip() throws Exception {
TestableService service = buildService();
Mockito.when(testDeviceMapper.selectNormalDeviceTypes()).thenReturn(Collections.singletonList(buildType("type-1", "示波器")));
BusinessException exception = Assertions.assertThrows(BusinessException.class,
() -> service.importTestDevices(buildExcelFile(), buildZipFile(10 * 1024 * 1024 + 1)));
Assertions.assertEquals("校准报告不能超过10MB", exception.getMessage());
}
private TestableService buildService() {
TestableService service = new TestableService(reportStorageService);
ReflectionTestUtils.setField(service, "baseMapper", testDeviceMapper);
return service;
}
private TestDevicePO buildDevice(String id, String reportPath) {
TestDevicePO device = new TestDevicePO();
device.setId(id);
device.setType("type-1");
device.setNo("TD-001");
device.setValidityPeriod(LocalDate.of(2026, 6, 25));
device.setValidityReport(reportPath);
return device;
}
private TestDeviceTypeNameVO buildType(String id, String name) {
TestDeviceTypeNameVO type = new TestDeviceTypeNameVO();
type.setId(id);
type.setName(name);
return type;
}
private MockMultipartFile buildExcelFile() throws IOException {
try (XSSFWorkbook workbook = new XSSFWorkbook();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
XSSFSheet sheet = workbook.createSheet("导入");
sheet.createRow(0).createCell(0).setCellValue("设备型号");
sheet.getRow(0).createCell(1).setCellValue("设备编号");
sheet.getRow(0).createCell(2).setCellValue("校准有效期");
sheet.getRow(0).createCell(3).setCellValue("设备状态");
sheet.getRow(0).createCell(4).setCellValue("校准报告文件名");
sheet.createRow(1).createCell(0).setCellValue("示波器");
sheet.getRow(1).createCell(1).setCellValue("TD-001");
sheet.getRow(1).createCell(2).setCellValue("2026-06-25");
sheet.getRow(1).createCell(3).setCellValue("在运");
sheet.getRow(1).createCell(4).setCellValue("report.pdf");
workbook.write(outputStream);
return new MockMultipartFile("file", "test-device.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", outputStream.toByteArray());
}
}
private MockMultipartFile buildZipFile(int pdfSize) throws IOException {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
zipOutputStream.putNextEntry(new ZipEntry("report.pdf"));
zipOutputStream.write(new byte[pdfSize]);
zipOutputStream.closeEntry();
zipOutputStream.finish();
return new MockMultipartFile("reportZip", "report.zip", "application/zip", outputStream.toByteArray());
}
}
private static class TestableService extends TestDeviceServiceImpl {
private int countResult;
private boolean updateResult = true;
private TestDevicePO requiredDevice;
private TestableService(TestDeviceReportStorageService reportStorageService) {
super(reportStorageService);
}
@Override
public int count(Wrapper<TestDevicePO> queryWrapper) {
return countResult;
}
@Override
public boolean updateById(TestDevicePO entity) {
return updateResult;
}
@Override
public TestDevicePO requireNormal(String id) {
return requiredDevice;
}
}
}

View File

@@ -11,8 +11,21 @@
</parent>
<artifactId>test-report</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>comservice</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>report-model</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>njcn-common</artifactId>
@@ -30,5 +43,27 @@
<artifactId>spingboot2.3.12</artifactId>
<version>2.3.12</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>system</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,111 @@
package com.njcn.gather.aireport.testreport.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.constant.OperateType;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.gather.aireport.testreport.pojo.param.TestReportParam;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO;
import com.njcn.gather.aireport.testreport.service.TestReportService;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.HttpResultUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Api(tags = "普测报告管理")
@RestController
@RequestMapping("/api/test-report")
@RequiredArgsConstructor
public class TestReportController extends BaseController {
private final TestReportService testReportService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询普测报告列表")
@PostMapping("/list")
public HttpResult<Page<TestReportVO>> list(@RequestBody(required = false) TestReportParam.QueryParam param) {
String methodDescribe = getMethodDescribe("list");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
testReportService.listTestReports(param), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD)
@ApiOperation("新增普测报告")
@PostMapping("/add")
public HttpResult<Boolean> add(@RequestBody @Validated TestReportParam.AddParam param) {
String methodDescribe = getMethodDescribe("add");
boolean result = testReportService.addTestReport(param);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
@ApiOperation("编辑普测报告")
@PostMapping("/update")
public HttpResult<Boolean> update(@RequestBody @Validated TestReportParam.UpdateParam param) {
String methodDescribe = getMethodDescribe("update");
boolean result = testReportService.updateTestReport(param);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DELETE)
@ApiOperation("删除普测报告")
@PostMapping("/delete")
public HttpResult<Boolean> delete(@RequestBody List<String> ids) {
String methodDescribe = getMethodDescribe("delete");
boolean result = testReportService.deleteTestReports(ids);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询普测报告详情")
@ApiImplicitParam(name = "id", value = "普测报告ID", required = true)
@GetMapping("/{id}")
public HttpResult<TestReportVO> detail(@PathVariable("id") String id) {
String methodDescribe = getMethodDescribe("detail");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
testReportService.getTestReport(id), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("预览普测报告关联模板文件")
@ApiImplicitParam(name = "id", value = "普测报告ID", required = true)
@GetMapping("/{id}/preview")
public ResponseEntity<byte[]> preview(@PathVariable("id") String id) {
return testReportService.previewTemplate(id);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("导出普测报告")
@GetMapping("/export")
public void export(@ModelAttribute TestReportParam.QueryParam param) {
testReportService.exportTestReports(param);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
@ApiOperation("提交普测报告审核")
@PostMapping("/submit-audit")
public HttpResult<Boolean> submitAudit(@RequestBody @Validated TestReportParam.SubmitAuditParam param) {
String methodDescribe = getMethodDescribe("submitAudit");
boolean result = testReportService.submitAudit(param);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
}

View File

@@ -0,0 +1,72 @@
package com.njcn.gather.aireport.testreport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.gather.aireport.testreport.pojo.param.TestReportParam;
import com.njcn.gather.aireport.testreport.pojo.po.TestReportPO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface TestReportMapper extends BaseMapper<TestReportPO> {
@Select({
"<script>",
"select r.id, r.no, r.client_unit_id as clientUnitId, cu.name as clientUnitName,",
"r.report_model_id as reportModelId, rm.name as reportModelName, r.create_unit as createUnit,",
"r.contract_number as contractNumber, r.standard, r.data, r.phonenumber, r.check_id as checkId,",
"checker.name as checkerName, r.check_time as checkTime, r.check_result as checkResult, r.state,",
"r.status, r.create_by as createBy, creator.name as createByName, r.create_time as createTime,",
"r.update_by as updateBy, updater.name as updateByName, r.update_time as updateTime",
"from ai_testreport r",
"left join ai_clientunit cu on r.client_unit_id = cu.id",
"left join ai_reportmodel rm on r.report_model_id = rm.id",
"left join sys_user creator on r.create_by = creator.id",
"left join sys_user updater on r.update_by = updater.id",
"left join sys_user checker on r.check_id = checker.id",
"where r.status = 1",
"<if test='param.keyword != null and param.keyword != \"\"'>",
"and (r.no like concat('%', #{param.keyword}, '%')",
"or r.contract_number like concat('%', #{param.keyword}, '%')",
"or cu.name like concat('%', #{param.keyword}, '%')",
"or rm.name like concat('%', #{param.keyword}, '%'))",
"</if>",
"<if test='param.searchBeginTime != null and param.searchBeginTime != \"\"'>",
"and r.create_time &gt;= #{param.searchBeginTime}",
"</if>",
"<if test='param.searchEndTime != null and param.searchEndTime != \"\"'>",
"and r.create_time &lt;= #{param.searchEndTime}",
"</if>",
"<if test='param.state != null and param.state != \"\"'>",
"and r.state = #{param.state}",
"</if>",
"order by r.create_time desc",
"</script>"
})
Page<TestReportVO> selectReportPage(Page<TestReportVO> page, @Param("param") TestReportParam.QueryParam param);
@Select({
"select r.id, r.no, r.client_unit_id as clientUnitId, cu.name as clientUnitName,",
"r.report_model_id as reportModelId, rm.name as reportModelName, r.create_unit as createUnit,",
"r.contract_number as contractNumber, r.standard, r.data, r.phonenumber, r.check_id as checkId,",
"checker.name as checkerName, r.check_time as checkTime, r.check_result as checkResult, r.state,",
"r.status, r.create_by as createBy, creator.name as createByName, r.create_time as createTime,",
"r.update_by as updateBy, updater.name as updateByName, r.update_time as updateTime",
"from ai_testreport r",
"left join ai_clientunit cu on r.client_unit_id = cu.id",
"left join ai_reportmodel rm on r.report_model_id = rm.id",
"left join sys_user creator on r.create_by = creator.id",
"left join sys_user updater on r.update_by = updater.id",
"left join sys_user checker on r.check_id = checker.id",
"where r.id = #{id} and r.status = 1"
})
TestReportVO selectReportDetail(@Param("id") String id);
@Select("select count(1) from ai_clientunit where id = #{id} and status = 1")
int countNormalClientUnit(@Param("id") String id);
@Select("select count(1) from ai_reportmodel where id = #{id} and status = 1")
int countNormalReportModel(@Param("id") String id);
}

View File

@@ -0,0 +1,16 @@
package com.njcn.gather.aireport.testreport.pojo.constant;
public final class TestReportConst {
public static final String STATE_EDITING = "01";
public static final String STATE_AUDITING = "02";
public static final String STATE_APPROVED = "03";
public static final String STATE_REJECTED = "04";
public static final String STATE_DELETED = "05";
public static final Integer STATUS_DELETED = 0;
public static final Integer STATUS_NORMAL = 1;
private TestReportConst() {
}
}

View File

@@ -0,0 +1,83 @@
package com.njcn.gather.aireport.testreport.pojo.param;
import com.njcn.web.pojo.param.BaseParam;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
public class TestReportParam {
@Data
@EqualsAndHashCode(callSuper = true)
public static class QueryParam extends BaseParam {
@ApiModelProperty("\u5173\u952e\u5b57\uff0c\u5339\u914d\u62a5\u544a\u7f16\u53f7\u3001\u5408\u540c\u7f16\u53f7\u3001\u59d4\u6258\u5355\u4f4d\u6216\u62a5\u544a\u6a21\u677f")
private String keyword;
@ApiModelProperty("\u521b\u5efa\u5f00\u59cb\u65f6\u95f4\uff0cyyyy-MM-dd HH:mm:ss")
private String searchBeginTime;
@ApiModelProperty("\u521b\u5efa\u7ed3\u675f\u65f6\u95f4\uff0cyyyy-MM-dd HH:mm:ss")
private String searchEndTime;
@ApiModelProperty("\u62a5\u544a\u72b6\u6001")
private String state;
}
@Data
public static class AddParam {
@ApiModelProperty("\u666e\u6d4b\u62a5\u544a\u7f16\u53f7")
@NotBlank(message = "\u666e\u6d4b\u62a5\u544a\u7f16\u53f7\u4e0d\u80fd\u4e3a\u7a7a")
@Size(max = 32, message = "\u666e\u6d4b\u62a5\u544a\u7f16\u53f7\u4e0d\u80fd\u8d85\u8fc732\u4e2a\u5b57\u7b26")
private String no;
@ApiModelProperty("\u59d4\u6258\u5355\u4f4dID")
@NotBlank(message = "\u59d4\u6258\u5355\u4f4d\u4e0d\u80fd\u4e3a\u7a7a")
private String clientUnitId;
@ApiModelProperty("\u62a5\u544a\u6a21\u677fID")
@NotBlank(message = "\u62a5\u544a\u6a21\u677f\u4e0d\u80fd\u4e3a\u7a7a")
private String reportModelId;
@ApiModelProperty("\u68c0\u6d4b\u516c\u53f8ID")
@NotBlank(message = "\u68c0\u6d4b\u516c\u53f8\u4e0d\u80fd\u4e3a\u7a7a")
private String createUnit;
@ApiModelProperty("\u5408\u540c\u7f16\u53f7")
@NotBlank(message = "\u5408\u540c\u7f16\u53f7\u4e0d\u80fd\u4e3a\u7a7a")
@Size(max = 32, message = "\u5408\u540c\u7f16\u53f7\u4e0d\u80fd\u8d85\u8fc732\u4e2a\u5b57\u7b26")
private String contractNumber;
@ApiModelProperty("\u68c0\u6d4b\u4f9d\u636e\uff0cJSON\u5b57\u7b26\u4e32\u6570\u7ec4")
@NotBlank(message = "\u68c0\u6d4b\u4f9d\u636e\u4e0d\u80fd\u4e3a\u7a7a")
private String standard;
@ApiModelProperty("\u53d7\u8d44\u6570\u636e\uff0cJSON\u5bf9\u8c61\u6216JSON\u6570\u7ec4")
@NotBlank(message = "\u53d7\u8d44\u6570\u636e\u4e0d\u80fd\u4e3a\u7a7a")
private String data;
@ApiModelProperty("\u8054\u7cfb\u7535\u8bdd")
@Size(max = 32, message = "\u8054\u7cfb\u7535\u8bdd\u4e0d\u80fd\u8d85\u8fc732\u4e2a\u5b57\u7b26")
private String phonenumber;
}
@Data
public static class UpdateParam extends AddParam {
@ApiModelProperty("\u666e\u6d4b\u62a5\u544aID")
@NotBlank(message = "\u666e\u6d4b\u62a5\u544aID\u4e0d\u80fd\u4e3a\u7a7a")
private String id;
}
@Data
public static class SubmitAuditParam {
@ApiModelProperty("\u666e\u6d4b\u62a5\u544aID")
@NotBlank(message = "\u666e\u6d4b\u62a5\u544aID\u4e0d\u80fd\u4e3a\u7a7a")
private String id;
@ApiModelProperty("\u5ba1\u6838\u610f\u89c1")
@Size(max = 128, message = "\u5ba1\u6838\u610f\u89c1\u4e0d\u80fd\u8d85\u8fc7128\u4e2a\u5b57\u7b26")
private String checkResult;
}
}

View File

@@ -0,0 +1,53 @@
package com.njcn.gather.aireport.testreport.pojo.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@TableName("ai_testreport")
public class TestReportPO implements Serializable {
private static final long serialVersionUID = -3038286126414044160L;
@TableId("id")
private String id;
@TableField("no")
private String no;
@TableField("client_unit_id")
private String clientUnitId;
@TableField("report_model_id")
private String reportModelId;
@TableField("create_unit")
private String createUnit;
@TableField("contract_number")
private String contractNumber;
@TableField("standard")
private String standard;
@TableField("data")
private String data;
@TableField("phonenumber")
private String phonenumber;
@TableField("state")
private String state;
@TableField("check_id")
private String checkId;
@TableField("check_time")
private LocalDate checkTime;
@TableField("check_result")
private String checkResult;
@TableField("status")
private Integer status;
@TableField("create_by")
private String createBy;
@TableField("create_time")
private LocalDateTime createTime;
@TableField("update_by")
private String updateBy;
@TableField("update_time")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,53 @@
package com.njcn.gather.aireport.testreport.pojo.vo;
import cn.afterturn.easypoi.excel.annotation.Excel;
import lombok.Data;
import java.io.Serializable;
@Data
public class TestReportExcelVO implements Serializable {
private static final long serialVersionUID = -3010688465572795334L;
@Excel(name = "\u666e\u6d4b\u62a5\u544a\u7f16\u53f7", width = 25)
private String no;
@Excel(name = "\u59d4\u6258\u5355\u4f4d", width = 25)
private String clientUnitName;
@Excel(name = "\u62a5\u544a\u6a21\u677f", width = 25)
private String reportModelName;
@Excel(name = "\u68c0\u6d4b\u516c\u53f8", width = 20)
private String createUnit;
@Excel(name = "\u5408\u540c\u7f16\u53f7", width = 20)
private String contractNumber;
@Excel(name = "\u68c0\u6d4b\u4f9d\u636e", width = 35)
private String standard;
@Excel(name = "\u53d7\u8d44\u6570\u636e", width = 35)
private String data;
@Excel(name = "\u8054\u7cfb\u7535\u8bdd", width = 20)
private String phonenumber;
@Excel(name = "\u5ba1\u6838\u4eba", width = 20)
private String checkerName;
@Excel(name = "\u5ba1\u6838\u65f6\u95f4", width = 20)
private String checkTime;
@Excel(name = "\u5ba1\u6838\u610f\u89c1", width = 30)
private String checkResult;
@Excel(name = "\u62a5\u544a\u72b6\u6001", width = 15)
private String state;
@Excel(name = "\u521b\u5efa\u65f6\u95f4", width = 25)
private String createTime;
@Excel(name = "\u521b\u5efa\u4eba", width = 20)
private String createByName;
}

View File

@@ -0,0 +1,156 @@
package com.njcn.gather.aireport.testreport.pojo.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
public class TestReportVO {
private static final String DEFAULT_EMPTY_DISPLAY = "-";
@ApiModelProperty("\u666e\u6d4b\u62a5\u544aID")
private String id;
@ApiModelProperty("\u666e\u6d4b\u62a5\u544a\u7f16\u53f7")
private String no;
@ApiModelProperty("\u59d4\u6258\u5355\u4f4dID")
private String clientUnitId;
@ApiModelProperty("\u59d4\u6258\u5355\u4f4d\u540d\u79f0")
private String clientUnitName;
@ApiModelProperty("\u62a5\u544a\u6a21\u677fID")
private String reportModelId;
@ApiModelProperty("\u62a5\u544a\u6a21\u677f\u540d\u79f0")
private String reportModelName;
@ApiModelProperty("\u68c0\u6d4b\u516c\u53f8ID")
private String createUnit;
@ApiModelProperty("\u5408\u540c\u7f16\u53f7")
private String contractNumber;
@ApiModelProperty("\u68c0\u6d4b\u4f9d\u636e")
private String standard;
@ApiModelProperty("\u53d7\u8d44\u6570\u636e")
private String data;
@ApiModelProperty("\u8054\u7cfb\u7535\u8bdd")
private String phonenumber;
@ApiModelProperty("\u5ba1\u6838\u4ebaID")
private String checkId;
@ApiModelProperty("\u5ba1\u6838\u4eba\u540d\u79f0")
private String checkerName;
@ApiModelProperty("\u5ba1\u6838\u65f6\u95f4")
@JsonFormat(pattern = "yyyy-MM-dd")
@JsonDeserialize(using = LocalDateDeserializer.class)
@JsonSerialize(using = LocalDateSerializer.class)
private LocalDate checkTime;
@ApiModelProperty("\u5ba1\u6838\u610f\u89c1")
private String checkResult;
@ApiModelProperty("\u62a5\u544a\u72b6\u6001")
private String state;
@ApiModelProperty("\u903b\u8f91\u72b6\u6001")
private Integer status;
@ApiModelProperty("\u521b\u5efa\u4eba\u663e\u793a\u540d")
private String createBy;
@ApiModelProperty("\u521b\u5efa\u4eba\u540d\u79f0")
private String createByName;
@ApiModelProperty("\u521b\u5efa\u65f6\u95f4")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime createTime;
@ApiModelProperty("\u66f4\u65b0\u4eba\u663e\u793a\u540d")
private String updateBy;
@ApiModelProperty("\u66f4\u65b0\u4eba\u540d\u79f0")
private String updateByName;
@ApiModelProperty("\u66f4\u65b0\u65f6\u95f4")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime updateTime;
public String getId() {
return defaultDisplay(id);
}
public String getNo() {
return defaultDisplay(no);
}
public String getClientUnitId() {
return defaultDisplay(clientUnitId);
}
public String getClientUnitName() {
return defaultDisplay(clientUnitName);
}
public String getReportModelId() {
return defaultDisplay(reportModelId);
}
public String getReportModelName() {
return defaultDisplay(reportModelName);
}
public String getCreateUnit() {
return defaultDisplay(createUnit);
}
public String getContractNumber() {
return defaultDisplay(contractNumber);
}
public String getStandard() {
return defaultDisplay(standard);
}
public String getData() {
return defaultDisplay(data);
}
public String getPhonenumber() {
return defaultDisplay(phonenumber);
}
public String getCheckId() {
return defaultDisplay(checkId);
}
public String getCheckerName() {
return defaultDisplay(checkerName);
}
public String getCheckResult() {
return defaultDisplay(checkResult);
}
public String getState() {
return defaultDisplay(state);
}
public String getCreateBy() {
return defaultDisplay(createBy);
}
public String getCreateByName() {
return defaultDisplay(createByName);
}
public String getUpdateBy() {
return defaultDisplay(updateBy);
}
public String getUpdateByName() {
return defaultDisplay(updateByName);
}
private String defaultDisplay(String value) {
return StringUtils.hasText(value) ? value : DEFAULT_EMPTY_DISPLAY;
}
}

View File

@@ -0,0 +1,31 @@
package com.njcn.gather.aireport.testreport.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.gather.aireport.testreport.pojo.param.TestReportParam;
import com.njcn.gather.aireport.testreport.pojo.po.TestReportPO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO;
import org.springframework.http.ResponseEntity;
import java.util.List;
public interface TestReportService extends IService<TestReportPO> {
Page<TestReportVO> listTestReports(TestReportParam.QueryParam param);
boolean addTestReport(TestReportParam.AddParam param);
boolean updateTestReport(TestReportParam.UpdateParam param);
boolean deleteTestReports(List<String> ids);
TestReportVO getTestReport(String id);
TestReportPO requireNormal(String id);
boolean submitAudit(TestReportParam.SubmitAuditParam param);
void exportTestReports(TestReportParam.QueryParam param);
ResponseEntity<byte[]> previewTemplate(String id);
}

View File

@@ -0,0 +1,621 @@
package com.njcn.gather.aireport.testreport.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.reportmodel.component.ReportModelFileStorageService;
import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO;
import com.njcn.gather.aireport.reportmodel.service.ReportModelService;
import com.njcn.gather.aireport.testreport.mapper.TestReportMapper;
import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst;
import com.njcn.gather.aireport.testreport.pojo.param.TestReportParam;
import com.njcn.gather.aireport.testreport.pojo.po.TestReportPO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportExcelVO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO;
import com.njcn.gather.aireport.testreport.service.TestReportService;
import com.njcn.gather.comservice.filepreview.FilePreviewService;
import com.njcn.gather.system.dictionary.pojo.po.DictData;
import com.njcn.gather.system.dictionary.pojo.po.DictType;
import com.njcn.gather.system.dictionary.service.IDictDataService;
import com.njcn.gather.system.dictionary.service.IDictTypeService;
import com.njcn.gather.system.pojo.constant.DictConst;
import com.njcn.gather.system.util.ExportFileNameUtil;
import com.njcn.web.factory.PageFactory;
import com.njcn.web.utils.ExcelUtil;
import com.njcn.web.utils.RequestUtil;
import lombok.Data;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestReportPO> implements TestReportService {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final Integer DICT_STATE_NORMAL = 1;
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final String MSG_ID_REQUIRED = "\u666e\u6d4b\u62a5\u544aID\u4e0d\u80fd\u4e3a\u7a7a";
private static final String MSG_REPORT_NOT_FOUND = "\u666e\u6d4b\u62a5\u544a\u4e0d\u5b58\u5728\u6216\u5df2\u5220\u9664";
private static final String MSG_ONLY_EDITABLE_CAN_UPDATE =
"\u4ec5\u7f16\u5236\u6216\u9000\u56de\u72b6\u6001\u7684\u666e\u6d4b\u62a5\u544a\u5141\u8bb8\u7f16\u8f91";
private static final String MSG_ONLY_EDITABLE_CAN_AUDIT =
"\u4ec5\u7f16\u5236\u6216\u9000\u56de\u72b6\u6001\u7684\u666e\u6d4b\u62a5\u544a\u5141\u8bb8\u63d0\u4ea4\u5ba1\u6838";
private static final String MSG_START_TIME_INVALID = "\u5f00\u59cb\u65f6\u95f4\u683c\u5f0f\u4e0d\u6b63\u786e";
private static final String MSG_END_TIME_INVALID = "\u7ed3\u675f\u65f6\u95f4\u683c\u5f0f\u4e0d\u6b63\u786e";
private static final String MSG_PARAM_REQUIRED = "\u666e\u6d4b\u62a5\u544a\u53c2\u6570\u4e0d\u80fd\u4e3a\u7a7a";
private static final String MSG_NO_REQUIRED = "\u666e\u6d4b\u62a5\u544a\u7f16\u53f7\u4e0d\u80fd\u4e3a\u7a7a";
private static final String MSG_NO_TOO_LONG = "\u666e\u6d4b\u62a5\u544a\u7f16\u53f7\u4e0d\u80fd\u8d85\u8fc732\u4e2a\u5b57\u7b26";
private static final String MSG_CLIENT_UNIT_REQUIRED = "\u59d4\u6258\u5355\u4f4d\u4e0d\u80fd\u4e3a\u7a7a";
private static final String MSG_REPORT_MODEL_REQUIRED = "\u62a5\u544a\u6a21\u677f\u4e0d\u80fd\u4e3a\u7a7a";
private static final String MSG_CREATE_UNIT_REQUIRED = "\u68c0\u6d4b\u516c\u53f8\u4e0d\u80fd\u4e3a\u7a7a";
private static final String MSG_CONTRACT_REQUIRED = "\u5408\u540c\u7f16\u53f7\u4e0d\u80fd\u4e3a\u7a7a";
private static final String MSG_CONTRACT_TOO_LONG = "\u5408\u540c\u7f16\u53f7\u4e0d\u80fd\u8d85\u8fc732\u4e2a\u5b57\u7b26";
private static final String MSG_STANDARD_REQUIRED = "\u68c0\u6d4b\u4f9d\u636e\u4e0d\u80fd\u4e3a\u7a7a";
private static final String MSG_DATA_REQUIRED = "\u53d7\u8d44\u6570\u636e\u4e0d\u80fd\u4e3a\u7a7a";
private static final String MSG_DATA_INVALID = "\u53d7\u8d44\u6570\u636e\u683c\u5f0f\u4e0d\u6b63\u786e";
private static final String MSG_PHONE_TOO_LONG = "\u8054\u7cfb\u7535\u8bdd\u4e0d\u80fd\u8d85\u8fc732\u4e2a\u5b57\u7b26";
private static final String MSG_NO_EXISTS = "\u666e\u6d4b\u62a5\u544a\u7f16\u53f7\u5df2\u5b58\u5728";
private static final String MSG_COMPANY_NOT_FOUND = "\u68c0\u6d4b\u516c\u53f8\u4e0d\u5b58\u5728\u6216\u5df2\u5220\u9664";
private static final String MSG_STANDARD_INVALID = "\u6d4b\u8bd5\u4f9d\u636e\u683c\u5f0f\u4e0d\u6b63\u786e";
private static final String MSG_STANDARD_NOT_FOUND = "\u6d4b\u8bd5\u4f9d\u636e\u4e0d\u5b58\u5728\u6216\u5df2\u5220\u9664";
private static final String MSG_CLIENT_UNIT_NOT_FOUND = "\u59d4\u6258\u5355\u4f4d\u4e0d\u5b58\u5728\u6216\u5df2\u5220\u9664";
private static final String MSG_REPORT_MODEL_NOT_FOUND = "\u62a5\u544a\u6a21\u677f\u4e0d\u5b58\u5728\u6216\u5df2\u5220\u9664";
private static final String MSG_PREVIEW_NOT_READY = "\u62a5\u544a\u6a21\u677f\u9884\u89c8\u670d\u52a1\u672a\u521d\u59cb\u5316";
private static final String MSG_TEMPLATE_FILE_NOT_FOUND = "\u62a5\u544a\u6a21\u677f\u6587\u4ef6\u4e0d\u5b58\u5728";
private static final String MSG_ENCODE_FILE_NAME_FAILED = "\u7f16\u7801\u62a5\u544a\u6a21\u677f\u6587\u4ef6\u540d\u5931\u8d25";
private final ReportModelService reportModelService;
private final ReportModelFileStorageService reportModelFileStorageService;
private final FilePreviewService filePreviewService;
private final IDictTypeService dictTypeService;
private final IDictDataService dictDataService;
public TestReportServiceImpl(ReportModelService reportModelService,
ReportModelFileStorageService reportModelFileStorageService,
FilePreviewService filePreviewService,
IDictTypeService dictTypeService,
IDictDataService dictDataService) {
this.reportModelService = reportModelService;
this.reportModelFileStorageService = reportModelFileStorageService;
this.filePreviewService = filePreviewService;
this.dictTypeService = dictTypeService;
this.dictDataService = dictDataService;
}
@Override
public Page<TestReportVO> listTestReports(TestReportParam.QueryParam param) {
TestReportParam.QueryParam query = normalizeQueryParam(param);
Page<TestReportVO> page = this.baseMapper.selectReportPage(
new Page<TestReportVO>(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), query);
fillDisplayUserNames(page.getRecords(), buildUserNameMap(page.getRecords()));
fillDisplayFields(page.getRecords());
return page;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean addTestReport(TestReportParam.AddParam param) {
NormalizedReport data = normalizeParam(param);
checkNoUnique(data.getNo(), null);
checkClientUnit(data.getClientUnitId());
checkReportModel(data.getReportModelId());
checkCreateUnit(data.getCreateUnit());
checkStandard(data.getStandard());
LocalDateTime now = LocalDateTime.now();
String userId = resolveCurrentUserId();
TestReportPO report = new TestReportPO();
report.setId(UUID.randomUUID().toString());
applyBusinessFields(report, data);
report.setState(TestReportConst.STATE_EDITING);
report.setStatus(TestReportConst.STATUS_NORMAL);
report.setCreateBy(userId);
report.setCreateTime(now);
report.setUpdateBy(userId);
report.setUpdateTime(now);
return this.save(report);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateTestReport(TestReportParam.UpdateParam param) {
if (param == null || StrUtil.isBlank(param.getId())) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_ID_REQUIRED);
}
TestReportPO report = requireNormal(param.getId());
if (!allowEdit(report.getState())) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_ONLY_EDITABLE_CAN_UPDATE);
}
NormalizedReport data = normalizeParam(param);
checkNoUnique(data.getNo(), report.getId());
checkClientUnit(data.getClientUnitId());
checkReportModel(data.getReportModelId());
checkCreateUnit(data.getCreateUnit());
checkStandard(data.getStandard());
applyBusinessFields(report, data);
report.setUpdateBy(resolveCurrentUserId());
report.setUpdateTime(LocalDateTime.now());
return this.updateById(report);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteTestReports(List<String> ids) {
List<String> validIds = normalizeIds(ids);
if (validIds.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_ID_REQUIRED);
}
return this.lambdaUpdate()
.set(TestReportPO::getState, TestReportConst.STATE_DELETED)
.set(TestReportPO::getStatus, TestReportConst.STATUS_DELETED)
.set(TestReportPO::getUpdateBy, resolveCurrentUserId())
.set(TestReportPO::getUpdateTime, LocalDateTime.now())
.in(TestReportPO::getId, validIds)
.eq(TestReportPO::getStatus, TestReportConst.STATUS_NORMAL)
.update();
}
@Override
public TestReportVO getTestReport(String id) {
TestReportVO report = this.baseMapper.selectReportDetail(requireText(id, MSG_ID_REQUIRED));
if (report == null) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_REPORT_NOT_FOUND);
}
fillDisplayUserNames(report, buildUserNameMap(Collections.singletonList(report)));
fillDisplayFields(Collections.singletonList(report));
return report;
}
@Override
public TestReportPO requireNormal(String id) {
String normalizedId = requireText(id, MSG_ID_REQUIRED);
TestReportPO report = this.lambdaQuery()
.eq(TestReportPO::getId, normalizedId)
.eq(TestReportPO::getStatus, TestReportConst.STATUS_NORMAL)
.one();
if (report == null) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_REPORT_NOT_FOUND);
}
return report;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean submitAudit(TestReportParam.SubmitAuditParam param) {
if (param == null || StrUtil.isBlank(param.getId())) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_ID_REQUIRED);
}
TestReportPO report = requireNormal(param.getId());
if (!allowEdit(report.getState())) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_ONLY_EDITABLE_CAN_AUDIT);
}
report.setCheckId(null);
report.setCheckTime(null);
report.setCheckResult(null);
report.setState(TestReportConst.STATE_AUDITING);
report.setUpdateBy(resolveCurrentUserId());
report.setUpdateTime(LocalDateTime.now());
return this.updateById(report);
}
@Override
public void exportTestReports(TestReportParam.QueryParam param) {
TestReportParam.QueryParam query = normalizeQueryParam(param);
Page<TestReportVO> page = this.baseMapper.selectReportPage(new Page<TestReportVO>(1, Integer.MAX_VALUE), query);
fillDisplayUserNames(page.getRecords(), buildUserNameMap(page.getRecords()));
fillDisplayFields(page.getRecords());
List<TestReportExcelVO> exportRows = page.getRecords().stream()
.map(this::toExcelVO)
.collect(Collectors.toList());
ExcelUtil.exportExcel(ExportFileNameUtil.appendToday("\u666e\u6d4b\u62a5\u544a\u5217\u8868.xlsx"),
"\u666e\u6d4b\u62a5\u544a\u5217\u8868", TestReportExcelVO.class, exportRows);
}
@Override
public ResponseEntity<byte[]> previewTemplate(String id) {
TestReportPO report = requireNormal(id);
ReportModelPO model = requireReportModel(report.getReportModelId());
Path filePath = reportModelFileStorageService.resolveDownloadPath(model.getPath());
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_TEMPLATE_FILE_NOT_FOUND);
}
String fileName = resolvePreviewFileName(model);
FilePreviewService.PreviewContent previewContent = filePreviewService.preview(filePath, fileName);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename*=UTF-8''" + encodeFileName(fileName))
.header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate")
.contentType(MediaType.parseMediaType(previewContent.getContentType()))
.body(previewContent.getContent());
}
private TestReportParam.QueryParam normalizeQueryParam(TestReportParam.QueryParam param) {
TestReportParam.QueryParam query = param == null ? new TestReportParam.QueryParam() : param;
query.setKeyword(trimToNull(query.getKeyword()));
if (StrUtil.isNotBlank(query.getSearchBeginTime())) {
query.setSearchBeginTime(formatDateTime(parseDateTime(query.getSearchBeginTime(), MSG_START_TIME_INVALID)));
}
if (StrUtil.isNotBlank(query.getSearchEndTime())) {
query.setSearchEndTime(formatDateTime(parseDateTime(query.getSearchEndTime(), MSG_END_TIME_INVALID)));
}
return query;
}
private NormalizedReport normalizeParam(TestReportParam.AddParam param) {
if (param == null) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_PARAM_REQUIRED);
}
NormalizedReport data = new NormalizedReport();
data.setNo(requireText(param.getNo(), MSG_NO_REQUIRED, 32, MSG_NO_TOO_LONG));
data.setClientUnitId(requireText(param.getClientUnitId(), MSG_CLIENT_UNIT_REQUIRED));
data.setReportModelId(requireText(param.getReportModelId(), MSG_REPORT_MODEL_REQUIRED));
data.setCreateUnit(requireText(param.getCreateUnit(), MSG_CREATE_UNIT_REQUIRED));
data.setContractNumber(requireText(param.getContractNumber(), MSG_CONTRACT_REQUIRED, 32, MSG_CONTRACT_TOO_LONG));
data.setStandard(requireText(param.getStandard(), MSG_STANDARD_REQUIRED));
data.setData(requireJsonObjectOrArrayText(param.getData(), MSG_DATA_REQUIRED, MSG_DATA_INVALID));
data.setPhonenumber(trimToNull(param.getPhonenumber()));
if (data.getPhonenumber() != null && data.getPhonenumber().length() > 32) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_PHONE_TOO_LONG);
}
return data;
}
private void applyBusinessFields(TestReportPO report, NormalizedReport data) {
report.setNo(data.getNo());
report.setClientUnitId(data.getClientUnitId());
report.setReportModelId(data.getReportModelId());
report.setCreateUnit(data.getCreateUnit());
report.setContractNumber(data.getContractNumber());
report.setStandard(data.getStandard());
report.setData(data.getData());
report.setPhonenumber(data.getPhonenumber());
}
private boolean allowEdit(String state) {
return TestReportConst.STATE_EDITING.equals(state) || TestReportConst.STATE_REJECTED.equals(state);
}
private void checkNoUnique(String no, String excludeId) {
LambdaQueryWrapper<TestReportPO> wrapper = new LambdaQueryWrapper<TestReportPO>();
wrapper.eq(TestReportPO::getNo, no)
.eq(TestReportPO::getStatus, TestReportConst.STATUS_NORMAL)
.ne(StrUtil.isNotBlank(excludeId), TestReportPO::getId, excludeId);
if (this.count(wrapper) > 0) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_NO_EXISTS);
}
}
private TestReportExcelVO toExcelVO(TestReportVO vo) {
TestReportExcelVO excelVO = new TestReportExcelVO();
excelVO.setNo(vo.getNo());
excelVO.setClientUnitName(StrUtil.isNotBlank(vo.getClientUnitName()) ? vo.getClientUnitName() : vo.getClientUnitId());
excelVO.setReportModelName(StrUtil.isNotBlank(vo.getReportModelName()) ? vo.getReportModelName() : vo.getReportModelId());
excelVO.setCreateUnit(vo.getCreateUnit());
excelVO.setContractNumber(vo.getContractNumber());
excelVO.setStandard(vo.getStandard());
excelVO.setData(vo.getData());
excelVO.setPhonenumber(vo.getPhonenumber());
excelVO.setCheckerName(StrUtil.isNotBlank(vo.getCheckerName()) ? vo.getCheckerName() : vo.getCheckId());
excelVO.setCheckTime(vo.getCheckTime() == null ? null : vo.getCheckTime().toString());
excelVO.setCheckResult(vo.getCheckResult());
excelVO.setState(vo.getState());
excelVO.setCreateTime(vo.getCreateTime() == null ? null : DATE_TIME_FORMATTER.format(vo.getCreateTime()));
excelVO.setCreateByName(StrUtil.isNotBlank(vo.getCreateByName()) ? vo.getCreateByName() : vo.getCreateBy());
return excelVO;
}
void fillDisplayUserNames(List<TestReportVO> reports, Map<String, String> userNameMap) {
if (reports == null || reports.isEmpty()) {
return;
}
for (TestReportVO report : reports) {
fillDisplayUserNames(report, userNameMap);
}
}
void fillDisplayUserNames(TestReportVO report, Map<String, String> userNameMap) {
if (report == null) {
return;
}
report.setCreateBy(resolveDisplayUserName(report.getCreateBy(), userNameMap));
report.setUpdateBy(resolveDisplayUserName(report.getUpdateBy(), userNameMap));
}
void fillDisplayFields(List<TestReportVO> reports) {
if (reports == null || reports.isEmpty()) {
return;
}
Map<String, String> companyNameMap = buildDictNameMap(DictConst.TEST_REPORT_COMPANY_DICT_CODE);
Map<String, String> standardNameMap = buildDictNameMap(DictConst.TEST_REPORT_STANDARD_DICT_CODE);
for (TestReportVO report : reports) {
if (report == null) {
continue;
}
report.setCreateUnit(resolveDictDisplayValue(report.getCreateUnit(), companyNameMap));
report.setStandard(resolveStandardDisplayValue(report.getStandard(), standardNameMap));
}
}
private Map<String, String> buildUserNameMap(Collection<TestReportVO> reports) {
if (reports == null || reports.isEmpty()) {
return Collections.emptyMap();
}
Map<String, String> userNameMap = new LinkedHashMap<String, String>();
for (TestReportVO report : reports) {
if (report == null) {
continue;
}
putUserName(userNameMap, report.getCreateBy(), report.getCreateByName());
putUserName(userNameMap, report.getUpdateBy(), report.getUpdateByName());
putUserName(userNameMap, report.getCheckId(), report.getCheckerName());
}
return userNameMap;
}
private Map<String, String> buildDictNameMap(String typeCode) {
if (dictTypeService == null || dictDataService == null || StrUtil.isBlank(typeCode)) {
return Collections.emptyMap();
}
DictType dictType = dictTypeService.getByCode(typeCode);
if (dictType == null || StrUtil.isBlank(dictType.getId()) || !DICT_STATE_NORMAL.equals(dictType.getState())) {
return Collections.emptyMap();
}
List<DictData> dictDataList = dictDataService.listDictDataByTypeId(dictType.getId());
if (dictDataList == null || dictDataList.isEmpty()) {
return Collections.emptyMap();
}
Map<String, String> result = new LinkedHashMap<String, String>();
for (DictData dictData : dictDataList) {
if (dictData == null || StrUtil.isBlank(dictData.getId()) || StrUtil.isBlank(dictData.getName())
|| !DICT_STATE_NORMAL.equals(dictData.getState())) {
continue;
}
result.put(dictData.getId(), dictData.getName());
}
return result;
}
private void putUserName(Map<String, String> userNameMap, String userId, String userName) {
String validUserId = trimToNull(userId);
String validUserName = trimToNull(userName);
if (validUserId == null || validUserName == null || "-".equals(validUserName)) {
return;
}
userNameMap.put(validUserId, validUserName);
}
private String resolveDisplayUserName(String userId, Map<String, String> userNameMap) {
String validUserId = trimToNull(userId);
if (validUserId == null) {
return null;
}
if (userNameMap == null || userNameMap.isEmpty()) {
return validUserId;
}
String userName = trimToNull(userNameMap.get(validUserId));
return userName == null ? validUserId : userName;
}
private String resolveDictDisplayValue(String rawValue, Map<String, String> dictNameMap) {
String validValue = trimToNull(rawValue);
if (validValue == null) {
return null;
}
if (dictNameMap == null || dictNameMap.isEmpty()) {
return validValue;
}
String displayValue = trimToNull(dictNameMap.get(validValue));
return displayValue == null ? validValue : displayValue;
}
private String resolveStandardDisplayValue(String rawValue, Map<String, String> dictNameMap) {
String validValue = trimToNull(rawValue);
if (validValue == null) {
return null;
}
List<String> standardIds = parseJsonArray(validValue);
if (standardIds.isEmpty()) {
return validValue;
}
List<String> displayValues = new ArrayList<String>();
for (String standardId : standardIds) {
String displayValue = resolveDictDisplayValue(standardId, dictNameMap);
if (displayValue != null) {
displayValues.add(displayValue);
}
}
if (displayValues.isEmpty()) {
return validValue;
}
return String.join(", ", displayValues);
}
private List<String> parseJsonArray(String jsonArrayText) {
String text = trimToNull(jsonArrayText);
if (text == null) {
return Collections.emptyList();
}
try {
List<String> parsedItems = OBJECT_MAPPER.readValue(text, new TypeReference<List<String>>() {
});
if (parsedItems == null || parsedItems.isEmpty()) {
return Collections.emptyList();
}
List<String> result = new ArrayList<String>();
for (String item : parsedItems) {
String normalizedItem = trimToNull(item);
if (normalizedItem != null) {
result.add(normalizedItem);
}
}
return result;
} catch (Exception exception) {
return Collections.emptyList();
}
}
private String requireJsonObjectOrArrayText(String value, String blankMessage, String invalidMessage) {
String text = requireText(value, blankMessage);
try {
JsonNode jsonNode = OBJECT_MAPPER.readTree(text);
if (jsonNode == null || !(jsonNode.isObject() || jsonNode.isArray())) {
throw new BusinessException(CommonResponseEnum.FAIL, invalidMessage);
}
return text;
} catch (BusinessException exception) {
throw exception;
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, invalidMessage);
}
}
private void checkCreateUnit(String createUnit) {
Map<String, String> companyNameMap = buildDictNameMap(DictConst.TEST_REPORT_COMPANY_DICT_CODE);
if (!companyNameMap.containsKey(createUnit)) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_COMPANY_NOT_FOUND);
}
}
private void checkStandard(String standard) {
List<String> standardIds = parseJsonArray(standard);
if (standardIds.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_STANDARD_INVALID);
}
Map<String, String> standardNameMap = buildDictNameMap(DictConst.TEST_REPORT_STANDARD_DICT_CODE);
for (String standardId : standardIds) {
if (!standardNameMap.containsKey(standardId)) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_STANDARD_NOT_FOUND);
}
}
}
private void checkClientUnit(String id) {
if (this.baseMapper.countNormalClientUnit(id) <= 0) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_CLIENT_UNIT_NOT_FOUND);
}
}
private void checkReportModel(String id) {
if (this.baseMapper.countNormalReportModel(id) <= 0) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_REPORT_MODEL_NOT_FOUND);
}
}
private ReportModelPO requireReportModel(String id) {
if (reportModelService == null || reportModelFileStorageService == null || filePreviewService == null) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_PREVIEW_NOT_READY);
}
return reportModelService.requireNormal(id);
}
private String resolvePreviewFileName(ReportModelPO model) {
String fileName = trimToNull(model.getFileName());
if (fileName != null) {
return fileName;
}
return reportModelFileStorageService.resolveDownloadFileName(model.getPath());
}
private List<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 String formatDateTime(LocalDateTime value) {
return DATE_TIME_FORMATTER.format(value);
}
private String requireText(String value, String blankMessage) {
String text = trimToNull(value);
if (text == null) {
throw new BusinessException(CommonResponseEnum.FAIL, blankMessage);
}
return text;
}
private String requireText(String value, String blankMessage, int maxLength, String lengthMessage) {
String text = requireText(value, blankMessage);
if (text.length() > maxLength) {
throw new BusinessException(CommonResponseEnum.FAIL, lengthMessage);
}
return text;
}
private String resolveCurrentUserId() {
String userId = RequestUtil.getUserId();
return StrUtil.isBlank(userId) ? null : userId;
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String text = value.trim();
if (text.isEmpty() || "-".equals(text)) {
return null;
}
return text;
}
private String encodeFileName(String fileName) {
try {
return URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replace("+", "%20");
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_ENCODE_FILE_NAME_FAILED);
}
}
@Data
private static class NormalizedReport {
private String no;
private String clientUnitId;
private String reportModelId;
private String createUnit;
private String contractNumber;
private String standard;
private String data;
private String phonenumber;
}
}

View File

@@ -0,0 +1,42 @@
package com.njcn.gather.aireport.testreport.pojo.vo;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.time.LocalDate;
import java.time.LocalDateTime;
class TestReportVOTest {
@Test
void getStateShouldUseDefaultDisplayWhenStateIsBlank() {
TestReportVO report = new TestReportVO();
Assertions.assertEquals("-", report.getState());
}
@Test
void getStateShouldReturnOriginalValueWhenStateExists() {
TestReportVO report = new TestReportVO();
report.setState("02");
Assertions.assertEquals("02", report.getState());
}
@Test
void timeJsonShouldUseConfiguredDateFormats() throws Exception {
TestReportVO report = new TestReportVO();
report.setCheckTime(LocalDate.of(2026, 6, 29));
report.setCreateTime(LocalDateTime.of(2026, 6, 29, 9, 8, 7));
report.setUpdateTime(LocalDateTime.of(2026, 6, 29, 18, 16, 14));
String json = new ObjectMapper().writeValueAsString(report);
Assertions.assertTrue(json.contains("\"checkTime\":\"2026-06-29\""));
Assertions.assertTrue(json.contains("\"createTime\":\"2026-06-29 09:08:07\""));
Assertions.assertTrue(json.contains("\"updateTime\":\"2026-06-29 18:16:14\""));
Assertions.assertFalse(json.contains("2026-06-29T09"));
Assertions.assertFalse(json.contains("2026-06-29T18"));
}
}

View File

@@ -0,0 +1,318 @@
package com.njcn.gather.aireport.testreport.service.impl;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.reportmodel.component.ReportModelFileStorageService;
import com.njcn.gather.aireport.reportmodel.service.ReportModelService;
import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst;
import com.njcn.gather.aireport.testreport.pojo.param.TestReportParam;
import com.njcn.gather.aireport.testreport.pojo.po.TestReportPO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO;
import com.njcn.gather.comservice.filepreview.FilePreviewService;
import com.njcn.gather.system.dictionary.pojo.po.DictData;
import com.njcn.gather.system.dictionary.pojo.po.DictType;
import com.njcn.gather.system.dictionary.service.IDictDataService;
import com.njcn.gather.system.dictionary.service.IDictTypeService;
import com.njcn.gather.system.pojo.constant.DictConst;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class TestReportServiceImplTest {
private static final Integer DICT_STATE_NORMAL = 1;
private static final Integer DICT_STATE_DELETED = 0;
@Test
void addTestReportShouldRejectMissingNo() {
TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class));
TestReportParam.AddParam param = new TestReportParam.AddParam();
param.setClientUnitId("client-1");
param.setReportModelId("model-1");
param.setCreateUnit("unit-1");
param.setContractNumber("HT-001");
param.setStandard("[\"std-1\"]");
param.setData("{}");
BusinessException exception = Assertions.assertThrows(BusinessException.class,
() -> service.addTestReport(param));
Assertions.assertNotNull(exception.getMessage());
}
@Test
void submitAuditShouldRejectMissingId() {
TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class));
TestReportParam.SubmitAuditParam param = new TestReportParam.SubmitAuditParam();
param.setCheckResult("approved");
BusinessException exception = Assertions.assertThrows(BusinessException.class,
() -> service.submitAudit(param));
Assertions.assertNotNull(exception.getMessage());
}
@Test
void addTestReportShouldRejectInvalidStandardJsonArray() {
TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class));
TestReportParam.AddParam param = new TestReportParam.AddParam();
param.setNo("PT-001");
param.setClientUnitId("client-1");
param.setReportModelId("model-1");
param.setCreateUnit("company-1");
param.setContractNumber("HT-001");
param.setStandard("not-json-array");
param.setData("{}");
BusinessException exception = Assertions.assertThrows(BusinessException.class,
() -> service.addTestReport(param));
Assertions.assertTrue(exception.getMessage().contains("\u6d4b\u8bd5\u4f9d\u636e"));
}
@Test
void addTestReportShouldRejectInvalidDataJson() {
TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class));
TestReportParam.AddParam param = buildValidNormalizeParam();
param.setData("not-json");
BusinessException exception = Assertions.assertThrows(BusinessException.class,
() -> invokeNormalizeParam(service, param));
Assertions.assertTrue(exception.getMessage().contains("\u6536\u8d44\u6570\u636e"));
}
@Test
void addTestReportShouldRejectScalarDataJson() {
TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class));
TestReportParam.AddParam param = buildValidNormalizeParam();
param.setData("\"plain-text\"");
BusinessException exception = Assertions.assertThrows(BusinessException.class,
() -> invokeNormalizeParam(service, param));
Assertions.assertTrue(exception.getMessage().contains("\u6536\u8d44\u6570\u636e"));
}
@Test
void normalizeParamShouldAcceptJsonObjectAndJsonArrayData() {
TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class));
TestReportParam.AddParam objectParam = buildValidNormalizeParam();
objectParam.setData("{\"fileName\":\"report.json\"}");
TestReportParam.AddParam arrayParam = buildValidNormalizeParam();
arrayParam.setData("[{\"fileName\":\"report-a.json\"}]");
Assertions.assertDoesNotThrow(() -> invokeNormalizeParam(service, objectParam));
Assertions.assertDoesNotThrow(() -> invokeNormalizeParam(service, arrayParam));
}
@Test
void addTestReportShouldRejectDeletedStandardDictData() {
IDictTypeService dictTypeService = mock(IDictTypeService.class);
IDictDataService dictDataService = mock(IDictDataService.class);
TestReportServiceImpl service = createService(dictTypeService, dictDataService);
mockNormalDictType(dictTypeService, DictConst.TEST_REPORT_COMPANY_DICT_CODE, "type-company");
mockNormalDictType(dictTypeService, DictConst.TEST_REPORT_STANDARD_DICT_CODE, "type-standard");
when(dictDataService.listDictDataByTypeId("type-company")).thenReturn(Collections.singletonList(
buildDictData("company-1", "company-A", DICT_STATE_NORMAL)));
when(dictDataService.listDictDataByTypeId("type-standard")).thenReturn(Collections.singletonList(
buildDictData("std-1", "standard-A", DICT_STATE_DELETED)));
TestReportParam.AddParam param = new TestReportParam.AddParam();
param.setNo("PT-001");
param.setClientUnitId("client-1");
param.setReportModelId("model-1");
param.setCreateUnit("company-1");
param.setContractNumber("HT-001");
param.setStandard("[\"std-1\"]");
param.setData("{}");
BusinessException exception = Assertions.assertThrows(BusinessException.class,
() -> service.addTestReport(param));
Assertions.assertTrue(exception.getMessage().contains("\u6d4b\u8bd5\u4f9d\u636e"));
}
@Test
void fillDisplayUserNamesShouldPreferUserNameFallbackToUserIdAndDash() {
TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class));
TestReportVO first = new TestReportVO();
first.setCreateBy("creator-001");
first.setUpdateBy("updater-001");
first.setCreateByName("creator-A");
first.setUpdateByName("updater-A");
TestReportVO second = new TestReportVO();
second.setCreateBy("creator-002");
second.setUpdateBy(null);
Map<String, String> userNameMap = new HashMap<String, String>();
userNameMap.put("creator-001", "creator-A");
userNameMap.put("updater-001", "updater-A");
service.fillDisplayUserNames(first, userNameMap);
service.fillDisplayUserNames(second, userNameMap);
Assertions.assertEquals("creator-A", first.getCreateBy());
Assertions.assertEquals("updater-A", first.getUpdateBy());
Assertions.assertEquals("creator-002", second.getCreateBy());
Assertions.assertEquals("-", second.getUpdateBy());
}
@Test
void fillDisplayFieldsShouldResolveDictNamesAndFallbackToRawValue() {
IDictTypeService dictTypeService = mock(IDictTypeService.class);
IDictDataService dictDataService = mock(IDictDataService.class);
TestReportServiceImpl service = createService(dictTypeService, dictDataService);
mockNormalDictType(dictTypeService, DictConst.TEST_REPORT_COMPANY_DICT_CODE, "type-company");
mockNormalDictType(dictTypeService, DictConst.TEST_REPORT_STANDARD_DICT_CODE, "type-standard");
when(dictDataService.listDictDataByTypeId("type-company")).thenReturn(Collections.singletonList(
buildDictData("company-1", "Nanjing Zhiyou", DICT_STATE_NORMAL)));
when(dictDataService.listDictDataByTypeId("type-standard")).thenReturn(Arrays.asList(
buildDictData("std-1", "GB 50150-2016", DICT_STATE_NORMAL),
buildDictData("std-2", "DL/T 596-2021", DICT_STATE_NORMAL)));
TestReportVO first = new TestReportVO();
first.setCreateUnit("company-1");
first.setStandard("[\"std-1\",\"std-2\"]");
TestReportVO second = new TestReportVO();
second.setCreateUnit("company-unknown");
second.setStandard("[\"std-unknown\"]");
service.fillDisplayFields(Arrays.asList(first, second));
Assertions.assertEquals("Nanjing Zhiyou", first.getCreateUnit());
Assertions.assertEquals("GB 50150-2016, DL/T 596-2021", first.getStandard());
Assertions.assertEquals("company-unknown", second.getCreateUnit());
Assertions.assertEquals("std-unknown", second.getStandard());
}
@Test
void fillDisplayFieldsShouldIgnoreDeletedDictData() {
IDictTypeService dictTypeService = mock(IDictTypeService.class);
IDictDataService dictDataService = mock(IDictDataService.class);
TestReportServiceImpl service = createService(dictTypeService, dictDataService);
mockNormalDictType(dictTypeService, DictConst.TEST_REPORT_COMPANY_DICT_CODE, "type-company");
mockNormalDictType(dictTypeService, DictConst.TEST_REPORT_STANDARD_DICT_CODE, "type-standard");
when(dictDataService.listDictDataByTypeId("type-company")).thenReturn(Collections.singletonList(
buildDictData("company-1", "company-A", DICT_STATE_DELETED)));
when(dictDataService.listDictDataByTypeId("type-standard")).thenReturn(Collections.singletonList(
buildDictData("std-1", "standard-A", DICT_STATE_DELETED)));
TestReportVO report = new TestReportVO();
report.setCreateUnit("company-1");
report.setStandard("[\"std-1\"]");
service.fillDisplayFields(Collections.singletonList(report));
Assertions.assertEquals("company-1", report.getCreateUnit());
Assertions.assertEquals("std-1", report.getStandard());
}
@Test
void submitAuditShouldMoveEditingStateToAuditingAndClearAuditFields() {
TestReportPO report = new TestReportPO();
report.setId("report-001");
report.setState(TestReportConst.STATE_EDITING);
report.setStatus(TestReportConst.STATUS_NORMAL);
report.setCheckId("checker-001");
report.setCheckTime(java.time.LocalDate.of(2026, 6, 26));
report.setCheckResult("done");
TestableTestReportServiceImpl service = new TestableTestReportServiceImpl(mock(IDictTypeService.class),
mock(IDictDataService.class), report);
TestReportParam.SubmitAuditParam param = new TestReportParam.SubmitAuditParam();
param.setId("report-001");
boolean result = service.submitAudit(param);
Assertions.assertTrue(result);
Assertions.assertEquals(TestReportConst.STATE_AUDITING, report.getState());
Assertions.assertNull(report.getCheckId());
Assertions.assertNull(report.getCheckTime());
Assertions.assertNull(report.getCheckResult());
Assertions.assertSame(report, service.updatedReport);
}
private TestReportServiceImpl createService(IDictTypeService dictTypeService, IDictDataService dictDataService) {
return new TestReportServiceImpl(
mock(ReportModelService.class),
mock(ReportModelFileStorageService.class),
mock(FilePreviewService.class),
dictTypeService,
dictDataService);
}
private TestReportParam.AddParam buildValidNormalizeParam() {
TestReportParam.AddParam param = new TestReportParam.AddParam();
param.setNo("PT-001");
param.setClientUnitId("client-1");
param.setReportModelId("model-1");
param.setCreateUnit("company-1");
param.setContractNumber("HT-001");
param.setStandard("[\"std-1\"]");
param.setData("{}");
return param;
}
private Object invokeNormalizeParam(TestReportServiceImpl service, TestReportParam.AddParam param) {
try {
Method method = TestReportServiceImpl.class.getDeclaredMethod("normalizeParam", TestReportParam.AddParam.class);
method.setAccessible(true);
return method.invoke(service, param);
} catch (java.lang.reflect.InvocationTargetException exception) {
Throwable targetException = exception.getTargetException();
if (targetException instanceof RuntimeException) {
throw (RuntimeException) targetException;
}
throw new RuntimeException(targetException);
} catch (Exception exception) {
throw new RuntimeException(exception);
}
}
private static class TestableTestReportServiceImpl extends TestReportServiceImpl {
private final TestReportPO report;
private TestReportPO updatedReport;
private TestableTestReportServiceImpl(IDictTypeService dictTypeService, IDictDataService dictDataService,
TestReportPO report) {
super(mock(ReportModelService.class), mock(ReportModelFileStorageService.class),
mock(FilePreviewService.class), dictTypeService, dictDataService);
this.report = report;
}
@Override
public TestReportPO requireNormal(String id) {
return report;
}
@Override
public boolean updateById(TestReportPO entity) {
this.updatedReport = entity;
return true;
}
}
private void mockNormalDictType(IDictTypeService dictTypeService, String code, String typeId) {
when(dictTypeService.getByCode(code)).thenReturn(buildDictType(typeId, code));
}
private DictType buildDictType(String id, String code) {
DictType dictType = new DictType();
dictType.setId(id);
dictType.setCode(code);
dictType.setState(DICT_STATE_NORMAL);
return dictType;
}
private DictData buildDictData(String id, String name, Integer state) {
DictData dictData = new DictData();
dictData.setId(id);
dictData.setName(name);
dictData.setState(state);
return dictData;
}
}

58
comservice/pom.xml Normal file
View File

@@ -0,0 +1,58 @@
<?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://www.w3.org/2001/XMLSchema-instance 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>comservice</artifactId>
<packaging>jar</packaging>
<dependencies>
<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>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.36</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,116 @@
package com.njcn.gather.comservice.filepreview;
import cn.hutool.core.util.StrUtil;
import org.apache.poi.hwpf.HWPFDocument;
import org.apache.poi.hwpf.extractor.WordExtractor;
import org.apache.poi.xwpf.extractor.XWPFWordExtractor;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.util.HtmlUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* 公共文件预览服务,统一处理报告模板相关预览内容转换。
*/
@Component
public class FilePreviewService {
private static final String PDF_EXTENSION = ".pdf";
private static final String DOC_EXTENSION = ".doc";
private static final String DOCX_EXTENSION = ".docx";
private static final String HTML_CONTENT_TYPE = MediaType.TEXT_HTML_VALUE + ";charset=UTF-8";
public PreviewContent preview(Path filePath, String fileName) {
String extension = resolveExtension(fileName);
try {
if (PDF_EXTENSION.equals(extension)) {
return new PreviewContent(MediaType.APPLICATION_PDF_VALUE, Files.readAllBytes(filePath));
}
if (DOCX_EXTENSION.equals(extension)) {
return buildHtmlPreview(extractDocxText(filePath));
}
if (DOC_EXTENSION.equals(extension)) {
return buildHtmlPreview(extractDocText(filePath));
}
throw new IllegalArgumentException("仅支持预览 .docx、.doc、.pdf 文件");
} catch (IOException exception) {
throw new IllegalArgumentException("读取文件预览内容失败", exception);
}
}
private PreviewContent buildHtmlPreview(String text) {
String[] lines = StrUtil.nullToEmpty(text).replace("\r\n", "\n").replace('\r', '\n').split("\n");
StringBuilder htmlBuilder = new StringBuilder(256);
htmlBuilder.append("<!DOCTYPE html><html><head><meta charset=\"UTF-8\">")
.append("<title>文件预览</title>")
.append("<style>")
.append("body{margin:0;padding:24px;background:#f5f7fa;color:#1f2329;font:14px/1.7 'Microsoft YaHei',sans-serif;}")
.append(".preview{max-width:960px;margin:0 auto;padding:24px;background:#fff;border-radius:8px;box-shadow:0 4px 16px rgba(31,35,41,.08);}")
.append("p{margin:0 0 12px;white-space:pre-wrap;word-break:break-word;}")
.append("</style></head><body><div class=\"preview\">");
if (lines.length == 1 && StrUtil.isBlank(lines[0])) {
htmlBuilder.append("<p>文件内容为空</p>");
} else {
for (String line : lines) {
if (StrUtil.isBlank(line)) {
htmlBuilder.append("<p>&nbsp;</p>");
} else {
htmlBuilder.append("<p>").append(HtmlUtils.htmlEscape(line)).append("</p>");
}
}
}
htmlBuilder.append("</div></body></html>");
return new PreviewContent(HTML_CONTENT_TYPE, htmlBuilder.toString().getBytes(StandardCharsets.UTF_8));
}
private String extractDocxText(Path filePath) throws IOException {
try (XWPFDocument document = new XWPFDocument(Files.newInputStream(filePath));
XWPFWordExtractor extractor = new XWPFWordExtractor(document)) {
return extractor.getText();
}
}
private String extractDocText(Path filePath) throws IOException {
try (HWPFDocument document = new HWPFDocument(Files.newInputStream(filePath));
WordExtractor extractor = new WordExtractor(document)) {
return extractor.getText();
}
}
private String resolveExtension(String fileName) {
String normalized = StrUtil.nullToEmpty(fileName).trim().toLowerCase();
if (normalized.endsWith(DOCX_EXTENSION)) {
return DOCX_EXTENSION;
}
if (normalized.endsWith(DOC_EXTENSION)) {
return DOC_EXTENSION;
}
if (normalized.endsWith(PDF_EXTENSION)) {
return PDF_EXTENSION;
}
return "";
}
public static class PreviewContent {
private final String contentType;
private final byte[] content;
public PreviewContent(String contentType, byte[] content) {
this.contentType = contentType;
this.content = content;
}
public String getContentType() {
return contentType;
}
public byte[] getContent() {
return content;
}
}
}

View File

@@ -0,0 +1,77 @@
package com.njcn.gather.comservice.filepreview;
import org.apache.poi.hwpf.HWPFDocument;
import org.apache.poi.hwpf.usermodel.Range;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.http.MediaType;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class FilePreviewServiceTest {
@Test
public void previewPdfReturnsInlinePdfContent() throws Exception {
Path file = Files.createTempFile("common-preview", ".pdf");
byte[] content = "%PDF-1.4".getBytes(StandardCharsets.UTF_8);
Files.write(file, content);
FilePreviewService service = new FilePreviewService();
FilePreviewService.PreviewContent previewContent = service.preview(file, "sample.pdf");
Assert.assertEquals(MediaType.APPLICATION_PDF_VALUE, previewContent.getContentType());
Assert.assertArrayEquals(content, previewContent.getContent());
}
@Test
public void previewDocxReturnsHtmlContent() throws Exception {
Path file = Files.createTempFile("common-preview", ".docx");
try (XWPFDocument document = new XWPFDocument();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
document.createParagraph().createRun().setText("第一行");
document.createParagraph().createRun().setText("第二行<tag>");
document.write(outputStream);
Files.write(file, outputStream.toByteArray());
}
FilePreviewService service = new FilePreviewService();
FilePreviewService.PreviewContent previewContent = service.preview(file, "sample.docx");
String html = new String(previewContent.getContent(), StandardCharsets.UTF_8);
Assert.assertEquals(MediaType.TEXT_HTML_VALUE + ";charset=UTF-8", previewContent.getContentType());
Assert.assertTrue(html.contains("第一行"));
Assert.assertTrue(html.contains("第二行&lt;tag&gt;"));
}
@Test
public void previewDocReturnsHtmlContent() throws Exception {
Path file = Files.createTempFile("common-preview", ".doc");
try (HWPFDocument document = new HWPFDocument();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
Range range = document.getRange();
range.insertAfter("DOC 正文");
document.write(outputStream);
Files.write(file, outputStream.toByteArray());
}
FilePreviewService service = new FilePreviewService();
FilePreviewService.PreviewContent previewContent = service.preview(file, "sample.doc");
String html = new String(previewContent.getContent(), StandardCharsets.UTF_8);
Assert.assertEquals(MediaType.TEXT_HTML_VALUE + ";charset=UTF-8", previewContent.getContentType());
Assert.assertTrue(html.contains("DOC 正文"));
}
@Test(expected = IllegalArgumentException.class)
public void previewRejectsUnsupportedExtension() throws Exception {
Path file = Files.createTempFile("common-preview", ".txt");
Files.write(file, "text".getBytes(StandardCharsets.UTF_8));
FilePreviewService service = new FilePreviewService();
service.preview(file, "sample.txt");
}
}

View File

@@ -11,6 +11,11 @@
<artifactId>entrance</artifactId>
<dependencies>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>comservice</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>system</artifactId>
@@ -78,6 +83,31 @@
<artifactId>add-ledger</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-report</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>report-approval</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.gather</groupId>
<artifactId>event-list</artifactId>

View File

@@ -62,9 +62,8 @@ log:
homeDir: D:\logs
commonLevel: info
parse-pqdif:
storage:
path: D:\file\pqdif-files
files:
path: D:\file
activate:
private-key: "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCcUyYhVqczGxblL+o/xZzF/8nf+LjrfUE/dS1aRHM7uMDD0cgCArhjtfneFePrMxt+Z7W8yNBzSarub8qsfhaVNikV7Es7oaeTygfjQXTi2n4AFkir3fM07J08RpWhl5M8f8uWTCuvFUYAw00gq55typqmnbkmJa2VIUy/iQf+cMCP7abz4/jNhUzUR3qA7TV4oMRgTdIEDUp63YF8dOC+JH8XxYrCVeHXV6fLCwmesdMzl0lB2VTEKMfLbXhOmF5g7P9y/16VCcN8UBuZlbyYfn+GAxJOSbeHi5HshOKfoSuD7Jz+3WQZpNavOWjIFExKIU38/CvnJCOP7XBCqpSTAgMBAAECggEAYeWokWRE3TpvwiOZnUpR/aVMdVi75a3ROL5XIpqPV61B+t/bU3cEpl0GF9C5pUeiRi0IoStZb3mI9D1KPW/REKyUWkhabQO1gFYbTnRlkNOn6MILzKX4cwJjDaZeeo4EBPU7N+qHyOOXrU6hdH5FfxhMdV983ajm5eeuupxER1C2kAcIklTeVpTX6EKOgZb5LBp5ssOVm2P42pOauvcRozRcvZmqnErXmukv0H4l3EVNt4rHpTn9riHUC63e8JfiYzVaF6zuNUxv6nHEft0/SRMw11XSTnNfDzcKqgjz6ksFBS/6eQQYKESk+ONC53HUuYHFAknkwsPupDCT2W8FIQKBgQDLHT/xCU3nxGr4vFKBDNaO2D5oK20ECbBO4oDvLWWmQG7f+6TsMy8PgVdMnoL4RfqGlwFAKEpS6KVFHnBVqnNEhcdy9uCI7x7Xx8UnyUtxj1EDTm76uta9Ki9OrlqB6tImDM9+Ya3vGktW37ht4WOx2OsJRhG1dbf6RLwFlH7DWwKBgQDFBxvi5I1BR6hg6Tj7xd2SqOT2Y+BED3xuSYENhWbmMhLJDResaB7mjztbxlYaY2mOE0holWm2uDmVFFhMh4jYXik4hYH8nmDzq9mDpZCZ9pyjYqnAP8THoAa8EbgrUWB8A6BPH4iL3KbMnBfBKY0pIr2xrvnjQjNBAgta7KDRKQKBgCe6oe4wxrdF2TKsC2tIqpMoQxS3Icy/ZGgZr+SYuaBKTCWtoDW/UT40K3JGMxIDBhzbXphBCUCsVt9tM8Xd4EwP6tJW7dZ7B0pnve2pVwNwaAVAiz6p2yUHIle+jN+Koe5lZRSwYIg7WW81tWpwwsJfzqFyvjYDP6hJV4mz4ROvAoGAaRcdnKvjXApomShMqJ4lTPChD3q+SA8qg3jZSOj6tZXHx00gb2kp8jg7pPvpOTIFPy6x1Ha9aCRjMk0ju84fA6lVuzwa1S907wOehUVuF3Eeo1cgy9Y3k3KbpPyeixxgpkUY4JslLdSHc2NemD0dee951qhJyRmqVOZOQDUuoeECgYEAqBw2cAFk3vM97WY06TSldGA8ajVHx3BYRjj+zl62NTQthy8fw3tqxb3c5e8toOmZWKjZvDhg2TRLhsDDQWEYg3LZG87REqVIjgEPcpjNLidjygGX8n3JF2o0O5I/EMvl0s/+LVQONfduOBvhwDqr8QNisbLsyneiAq7umewMolo="

View File

@@ -11,6 +11,7 @@
<modules>
<module>entrance</module>
<module>comservice</module>
<module>system</module>
<module>systemmonitor</module>
<module>system-ops</module>

View File

@@ -1051,7 +1051,7 @@ public class SteadyChecksquareServiceImpl implements SteadyChecksquareService {
Map<String, AddLedgerLinePathVO> linePathMap = addLedgerService.listLinePathByLineIds(lineIds);
for (String lineId : lineIds) {
if (linePathMap == null || linePathMap.get(lineId) == null) {
throw fail("鐩戞祴鐐逛笉瀛樺湪鎴栦笉鍙敤");
throw fail("监测点不存在或不可用");
}
}
return linePathMap;

View File

@@ -61,7 +61,8 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 鏁版嵁鏍¢獙鏈嶅姟娴嬭瘯<EFBFBD>? */
* 数据校验服务测试。
*/
class SteadyChecksquareServiceImplTest {
@BeforeAll
@@ -326,7 +327,7 @@ class SteadyChecksquareServiceImplTest {
.thenReturn(emptyHarmonicParityRuleResult());
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("杩涚嚎涓<EFBFBD>?);
linePath.setLineName("进线一");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
@@ -414,7 +415,7 @@ class SteadyChecksquareServiceImplTest {
.thenReturn(emptyHarmonicParityRuleResult());
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("杩涚嚎涓<EFBFBD>?);
linePath.setLineName("进线一");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
@@ -453,7 +454,7 @@ class SteadyChecksquareServiceImplTest {
.thenReturn(emptyHarmonicParityRuleResult());
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("杩涚嚎涓<EFBFBD>?);
linePath.setLineName("进线一");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
@@ -490,7 +491,7 @@ class SteadyChecksquareServiceImplTest {
Assertions.assertEquals(Integer.valueOf(1), items.get(0).getAbnormalPointCount());
Assertions.assertEquals(1, items.get(0).getAbnormalDetails().size());
Assertions.assertEquals(Integer.valueOf(2), items.get(0).getAbnormalDetails().get(0).getHarmonicOrder());
Assertions.assertEquals(Integer.valueOf(8), items.get(0).getExpectedPointCount());
Assertions.assertEquals(Integer.valueOf(4), items.get(0).getExpectedPointCount());
Assertions.assertEquals(Integer.valueOf(4), items.get(0).getActualPointCount());
}
@@ -507,7 +508,7 @@ class SteadyChecksquareServiceImplTest {
mock(SteadyChecksquareDetailService.class), new ObjectMapper());
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("杩涚嚎涓<EFBFBD>?);
linePath.setLineName("进线一");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
@@ -556,7 +557,7 @@ class SteadyChecksquareServiceImplTest {
.thenReturn(emptyHarmonicParityRuleResult());
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("杩涚嚎涓<EFBFBD>?);
linePath.setLineName("进线一");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
@@ -603,7 +604,7 @@ class SteadyChecksquareServiceImplTest {
.thenReturn(emptyRuleResult());
AddLedgerLinePathVO linePath = new AddLedgerLinePathVO();
linePath.setLineId("line-001");
linePath.setLineName("杩涚嚎涓<EFBFBD>?);
linePath.setLineName("进线一");
linePath.setLineInterval(1);
when(addLedgerService.listLinePathByLineIds(eq(Collections.singletonList("line-001"))))
.thenReturn(Collections.singletonMap("line-001", linePath));
@@ -665,7 +666,7 @@ class SteadyChecksquareServiceImplTest {
task.setId("task-001");
task.setState(1);
task.setLineId("line-001");
task.setLineName("杩涚嚎涓<EFBFBD>?);
task.setLineName("进线一");
task.setTimeStart(LocalDateTime.of(2026, 5, 1, 0, 0));
task.setTimeEnd(LocalDateTime.of(2026, 5, 1, 0, 1));
task.setIntervalMinutes(1);
@@ -851,7 +852,7 @@ class SteadyChecksquareServiceImplTest {
param.setIndicatorCodes(Collections.singletonList("V_RMS"));
SteadyChecksquareQueryVO result = new SteadyChecksquareQueryVO();
result.setLineIds(Collections.singletonList("line-001"));
result.setLineName("杩涚嚎涓<EFBFBD>?);
result.setLineName("进线一");
result.setTimeStart("2026-05-01 00:00:00");
result.setTimeEnd("2026-05-01 00:01:00");
result.setIntervalMinutes(1);
@@ -860,7 +861,7 @@ class SteadyChecksquareServiceImplTest {
item.setLineId("line-001");
item.setLineName("line-001-name");
item.setIndicatorCode("V_RMS");
item.setIndicatorName("鐩哥數鍘嬫湁鏁堝<EFBFBD>?");
item.setIndicatorName("相电压有效值");
item.setIntervalMinutes(1);
item.setHasData(true);
item.setExpectedPointCount(2);
@@ -943,7 +944,7 @@ class SteadyChecksquareServiceImplTest {
param.setIndicatorCodes(Collections.singletonList("V_RMS"));
SteadyChecksquareQueryVO result = new SteadyChecksquareQueryVO();
result.setLineIds(Collections.singletonList("line-001"));
result.setLineName("杩涚嚎涓<EFBFBD>?);
result.setLineName("进线一");
result.setTimeStart("2026-05-01 00:00:00");
result.setTimeEnd("2026-05-01 00:01:00");
result.setIntervalMinutes(1);

View File

@@ -6,7 +6,7 @@ package com.njcn.gather.system.pojo.constant;
*/
public interface DictConst {
/**
* 状态 0-正常1-停用2-删除 默认正常
* 状态0-正常1-停用2-删除默认正常
*/
int ENABLE = 0;
@@ -15,7 +15,7 @@ public interface DictConst {
int DELETE = 2;
/**
* 顶父类pid
* 顶父类 pid
*/
String FATHER_ID = "0";
@@ -40,7 +40,17 @@ public interface DictConst {
String REG_RES_CONTRAST_ID = "7cd65363a6bf675ae408f28a281b77d4";
/**
* 事件类型字典类型
* 事件类型字典类型
*/
String EVENT_TYPE_DICT = "事件类型";
/**
* 报告任务-检测公司字典编码
*/
String TEST_REPORT_COMPANY_DICT_CODE = "test_report_company";
/**
* 报告任务-测试依据字典编码
*/
String TEST_REPORT_STANDARD_DICT_CODE = "test_report_standard";
}

View File

@@ -18,9 +18,10 @@ import java.util.UUID;
public class PqdifFileStorageService {
private static final String DEFAULT_STORAGE_DIR = "data/parse-pqdif";
private static final String PQDIF_STORAGE_DIR = "pqdif-files";
@Value("${parse-pqdif.storage.path:}")
private String storagePath;
@Value("${files.path:}")
private String filesPath;
public String save(MultipartFile pqdifFile) {
if (pqdifFile == null || pqdifFile.isEmpty()) {
@@ -47,8 +48,8 @@ public class PqdifFileStorageService {
}
private Path resolveStorageDir() {
String configuredPath = trimToNull(storagePath);
Path path = configuredPath == null ? Paths.get(DEFAULT_STORAGE_DIR) : Paths.get(configuredPath);
String configuredPath = trimToNull(filesPath);
Path path = configuredPath == null ? Paths.get(DEFAULT_STORAGE_DIR) : Paths.get(configuredPath, PQDIF_STORAGE_DIR);
return path.toAbsolutePath().normalize();
}

View File

@@ -145,6 +145,7 @@ public class CsPqdifPathController extends BaseController {
private void fillPqdifFilePath(CsPqdifPathParam param, MultipartFile pqdifFile) {
param.setFilePath(pqdifFileStorageService.save(pqdifFile));
param.setFileName(resolveFileName(pqdifFile));
}
private String resolveFileName(MultipartFile pqdifFile) {

View File

@@ -8,6 +8,7 @@
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="filePath" property="filePath"/>
<result column="fileName" property="fileName"/>
<result column="recordCount" property="recordCount"/>
<result column="observationCount" property="observationCount"/>
<result column="sampleValueCount" property="sampleValueCount"/>
@@ -27,6 +28,7 @@
ID AS id,
Name AS name,
File_Path AS filePath,
File_Name AS fileName,
Record_Count AS recordCount,
Observation_Count AS observationCount,
Sample_Value_Count AS sampleValueCount,
@@ -60,7 +62,8 @@
SELECT
ID AS id,
Name AS name,
File_Path AS filePath
File_Path AS filePath,
File_Name AS fileName
FROM cs_pqdif_path
WHERE ID = #{id}
AND State = 1

View File

@@ -23,6 +23,10 @@ public class CsPqdifPathParam {
@JsonIgnore
private String filePath;
@ApiModelProperty("PQDIF鍘熷鏂囦欢鍚?")
@JsonIgnore
private String fileName;
/**
* PQDIF 存储记录编辑参数。
*/

View File

@@ -28,6 +28,9 @@ public class CsPqdifPathPO implements Serializable {
@TableField("File_Path")
private String filePath;
@TableField("File_Name")
private String fileName;
@TableField("Record_Count")
private Long recordCount;

View File

@@ -19,4 +19,7 @@ public class CsPqdifPathDetailVO {
@ApiModelProperty("PQDIF原始文件存储路径")
private String filePath;
@ApiModelProperty("PQDIF鍘熷鏂囦欢鍚?")
private String fileName;
}

View File

@@ -23,6 +23,9 @@ public class CsPqdifPathVO {
@ApiModelProperty("PQDIF原始文件存储路径")
private String filePath;
@ApiModelProperty("PQDIF鍘熷鏂囦欢鍚?")
private String fileName;
@ApiModelProperty("Record总数")
private Long recordCount;

View File

@@ -122,6 +122,7 @@ public class CsPqdifPathServiceImpl implements CsPqdifPathService {
CsPqdifPathPO pqdifPath = new CsPqdifPathPO();
pqdifPath.setName(requireText(param.getName(), "PQDIF名称不能为空"));
pqdifPath.setFilePath(trimToNull(param.getFilePath()));
pqdifPath.setFileName(trimToNull(param.getFileName()));
return pqdifPath;
}

View File

@@ -0,0 +1,28 @@
package com.njcn.gather.tool.parsepqdif.component;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.util.ReflectionTestUtils;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class PqdifFileStorageServiceTest {
@Test
public void saveUsesPqdifFilesDirectoryUnderFilesRoot() throws Exception {
Path filesRoot = Files.createTempDirectory("files-root");
PqdifFileStorageService service = new PqdifFileStorageService();
ReflectionTestUtils.setField(service, "filesPath", filesRoot.toString());
String storedPath = service.save(new MockMultipartFile("pqdifFile", "sample.pqd",
"application/octet-stream", "content".getBytes("UTF-8")));
Path expectedRoot = filesRoot.resolve("pqdif-files").toAbsolutePath().normalize();
Path actualPath = Paths.get(storedPath).toAbsolutePath().normalize();
Assert.assertTrue(actualPath.startsWith(expectedRoot));
Assert.assertTrue(Files.exists(actualPath));
}
}

View File

@@ -0,0 +1,61 @@
package com.njcn.gather.tool.parsepqdif.service.impl;
import com.njcn.gather.tool.parsepqdif.mapper.CsPqdifPathMapper;
import com.njcn.gather.tool.parsepqdif.pojo.param.CsPqdifPathParam;
import com.njcn.gather.tool.parsepqdif.pojo.po.CsPqdifPathPO;
import com.njcn.gather.tool.parsepqdif.pojo.vo.CsPqdifPathDetailVO;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class CsPqdifPathServiceImplTest {
@Test
public void addPqdifPathShouldPersistFileName() {
CsPqdifPathMapper mapper = mock(CsPqdifPathMapper.class);
when(mapper.insert(any(CsPqdifPathPO.class))).thenReturn(1);
CsPqdifPathServiceImpl service = new CsPqdifPathServiceImpl(mapper);
CsPqdifPathParam param = new CsPqdifPathParam();
param.setName("sample");
param.setFilePath("D:/files/pqdif-files/uuid_sample.pqd");
param.setFileName("sample.pqd");
boolean result = service.addPqdifPath(param);
Assert.assertTrue(result);
ArgumentCaptor<CsPqdifPathPO> captor = ArgumentCaptor.forClass(CsPqdifPathPO.class);
verify(mapper).insert(captor.capture());
Assert.assertEquals("sample.pqd", captor.getValue().getFileName());
}
@Test
public void updatePqdifPathShouldPersistFileName() {
CsPqdifPathMapper mapper = mock(CsPqdifPathMapper.class);
CsPqdifPathDetailVO detail = new CsPqdifPathDetailVO();
detail.setId("pqdif-id");
when(mapper.selectPqdifPathDetailById("pqdif-id")).thenReturn(detail);
when(mapper.updateById(any(CsPqdifPathPO.class))).thenReturn(1);
CsPqdifPathServiceImpl service = new CsPqdifPathServiceImpl(mapper);
CsPqdifPathParam.UpdateParam param = new CsPqdifPathParam.UpdateParam();
param.setId("pqdif-id");
param.setName("sample");
param.setFilePath("D:/files/pqdif-files/uuid_sample_new.pqd");
param.setFileName("sample-new.pqd");
boolean result = service.updatePqdifPath(param);
Assert.assertTrue(result);
ArgumentCaptor<CsPqdifPathPO> captor = ArgumentCaptor.forClass(CsPqdifPathPO.class);
verify(mapper).updateById(captor.capture());
Assert.assertEquals("sample-new.pqd", captor.getValue().getFileName());
verify(mapper).selectPqdifPathDetailById(eq("pqdif-id"));
}
}