feat(ai-report): 新增委托单位管理和报告审批功能
- 新增委托单位管理模块,支持增删改查、导入导出功能 - 实现委托单位 Excel 模板下载和数据导入功能 - 添加报告审批抽象处理器,支持审批流程日志记录 - 统一文件存储路径配置,将各模块独立路径改为统一根目录 - 新增 PQDIF 文件名存储字段,完善文件管理功能 - 修复代码中的乱码问题和错误提示信息 - 优化系统常量定义和字典配置管理
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.njcn.gather.aireport.reportmodel.component;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Local storage for uploaded report model template files.
|
||||
*/
|
||||
@Component
|
||||
public class ReportModelFileStorageService {
|
||||
|
||||
private static final String DEFAULT_STORAGE_DIR = "data/report-model";
|
||||
private static final String REPORT_MODEL_STORAGE_DIR = "report-model";
|
||||
private static final DateTimeFormatter DATE_DIR_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
|
||||
|
||||
@Value("${files.path:}")
|
||||
private String filesPath;
|
||||
|
||||
public StoredFile save(MultipartFile templateFile) {
|
||||
if (templateFile == null || templateFile.isEmpty()) {
|
||||
throw new IllegalArgumentException("模板文件不能为空");
|
||||
}
|
||||
try {
|
||||
Path storageRoot = resolveStorageRoot();
|
||||
String dateDir = LocalDate.now().format(DATE_DIR_FORMATTER);
|
||||
Path storageDir = storageRoot.resolve(dateDir).normalize();
|
||||
if (!storageDir.startsWith(storageRoot)) {
|
||||
throw new IllegalArgumentException("模板文件存储路径不合法");
|
||||
}
|
||||
Files.createDirectories(storageDir);
|
||||
String sanitizedFileName = sanitizeFileName(templateFile.getOriginalFilename());
|
||||
String storedFileName = UUID.randomUUID().toString().replace("-", "") + "-" + sanitizedFileName;
|
||||
Path target = storageDir.resolve(storedFileName).normalize();
|
||||
ensurePathInRoot(storageRoot, target);
|
||||
try (InputStream inputStream = templateFile.getInputStream()) {
|
||||
Files.copy(inputStream, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
String storedPath = storageRoot.relativize(target).toString().replace('\\', '/');
|
||||
return new StoredFile(storedPath, sanitizedFileName);
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalArgumentException("保存模板文件失败: " + exception.getMessage(), exception);
|
||||
}
|
||||
}
|
||||
|
||||
public Path resolveDownloadPath(String storedPath) {
|
||||
if (trimToNull(storedPath) == null) {
|
||||
throw new IllegalArgumentException("模板文件路径不能为空");
|
||||
}
|
||||
Path storageRoot = resolveStorageRoot();
|
||||
Path target = storageRoot.resolve(storedPath).normalize();
|
||||
ensurePathInRoot(storageRoot, target);
|
||||
return target;
|
||||
}
|
||||
|
||||
public void deleteQuietly(String storedPath) {
|
||||
try {
|
||||
if (trimToNull(storedPath) != null) {
|
||||
Files.deleteIfExists(resolveDownloadPath(storedPath));
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// Cleanup failure must not hide the original database error.
|
||||
}
|
||||
}
|
||||
|
||||
public String resolveDownloadFileName(String storedPath) {
|
||||
Path fileName = Paths.get(storedPath).getFileName();
|
||||
if (fileName == null) {
|
||||
return "report-model";
|
||||
}
|
||||
String name = fileName.toString();
|
||||
int separatorIndex = name.indexOf('-');
|
||||
return separatorIndex >= 0 && separatorIndex + 1 < name.length() ? name.substring(separatorIndex + 1) : name;
|
||||
}
|
||||
|
||||
private Path resolveStorageRoot() {
|
||||
String configuredPath = trimToNull(filesPath);
|
||||
Path path = configuredPath == null ? Paths.get(DEFAULT_STORAGE_DIR) : Paths.get(configuredPath, REPORT_MODEL_STORAGE_DIR);
|
||||
return path.toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
private void ensurePathInRoot(Path storageRoot, Path target) {
|
||||
if (!target.startsWith(storageRoot)) {
|
||||
throw new IllegalArgumentException("模板文件路径不合法");
|
||||
}
|
||||
}
|
||||
|
||||
private String sanitizeFileName(String originalFilename) {
|
||||
String fileName = trimToNull(originalFilename);
|
||||
if (fileName == null) {
|
||||
return "unnamed";
|
||||
}
|
||||
String normalizedName = Paths.get(fileName).getFileName().toString();
|
||||
String sanitized = normalizedName.replaceAll("[\\\\/:*?\"<>|]", "_");
|
||||
return trimToNull(sanitized) == null ? "unnamed" : sanitized;
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String text = value.trim();
|
||||
return text.isEmpty() ? null : text;
|
||||
}
|
||||
|
||||
public static class StoredFile {
|
||||
private final String path;
|
||||
private final String fileName;
|
||||
|
||||
public StoredFile(String path, String fileName) {
|
||||
this.path = path;
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.njcn.gather.aireport.reportmodel.component;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.njcn.gather.aireport.reportmodel.mapper.ReportModelMapper;
|
||||
import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelUserNameVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class ReportModelUserNameResolver {
|
||||
|
||||
private final ReportModelMapper reportModelMapper;
|
||||
|
||||
public Map<String, String> resolveUserNames(Collection<String> userIds) {
|
||||
if (userIds == null || userIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Set<String> validIds = userIds.stream()
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
if (validIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<ReportModelUserNameVO> users = reportModelMapper.selectUserNamesByIds(validIds);
|
||||
if (users == null || users.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
return users.stream()
|
||||
.filter(user -> user != null && StrUtil.isNotBlank(user.getId()) && StrUtil.isNotBlank(user.getName()))
|
||||
.collect(Collectors.toMap(ReportModelUserNameVO::getId, ReportModelUserNameVO::getName, (first, second) -> first));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package com.njcn.gather.aireport.reportmodel.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.njcn.common.pojo.annotation.OperateInfo;
|
||||
import com.njcn.common.pojo.constant.OperateType;
|
||||
import com.njcn.common.pojo.enums.common.LogEnum;
|
||||
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
|
||||
import com.njcn.common.pojo.exception.BusinessException;
|
||||
import com.njcn.common.pojo.response.HttpResult;
|
||||
import com.njcn.gather.aireport.reportmodel.component.ReportModelFileStorageService;
|
||||
import com.njcn.gather.aireport.reportmodel.pojo.param.ReportModelParam;
|
||||
import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO;
|
||||
import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelVO;
|
||||
import com.njcn.gather.aireport.reportmodel.service.ReportModelService;
|
||||
import com.njcn.gather.comservice.filepreview.FilePreviewService;
|
||||
import com.njcn.web.controller.BaseController;
|
||||
import com.njcn.web.utils.HttpResultUtil;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiImplicitParam;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Report model management endpoints.
|
||||
*/
|
||||
@Api(tags = "报告模板管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/report-model")
|
||||
@RequiredArgsConstructor
|
||||
public class ReportModelController extends BaseController {
|
||||
|
||||
private final ReportModelService reportModelService;
|
||||
private final ReportModelFileStorageService reportModelFileStorageService;
|
||||
private final FilePreviewService filePreviewService;
|
||||
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
|
||||
@ApiOperation("查询报告模板列表")
|
||||
@PostMapping("/list")
|
||||
public HttpResult<Page<ReportModelVO>> list(@RequestBody(required = false) ReportModelParam.QueryParam param) {
|
||||
String methodDescribe = getMethodDescribe("list");
|
||||
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
|
||||
reportModelService.listReportModels(param), methodDescribe);
|
||||
}
|
||||
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD)
|
||||
@ApiOperation("新增报告模板")
|
||||
@PostMapping(value = "/add", consumes = {"multipart/form-data"})
|
||||
public HttpResult<Boolean> add(@RequestPart("request") @Validated ReportModelParam.AddParam param,
|
||||
@RequestPart("templateFile") MultipartFile templateFile) {
|
||||
String methodDescribe = getMethodDescribe("add");
|
||||
boolean result = reportModelService.addReportModel(param, templateFile);
|
||||
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
|
||||
result, methodDescribe);
|
||||
}
|
||||
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
|
||||
@ApiOperation("编辑报告模板")
|
||||
@PostMapping(value = "/update", consumes = {"multipart/form-data"})
|
||||
public HttpResult<Boolean> update(@RequestPart("request") @Validated ReportModelParam.UpdateParam param,
|
||||
@RequestPart(value = "templateFile", required = false) MultipartFile templateFile) {
|
||||
String methodDescribe = getMethodDescribe("update");
|
||||
boolean result = reportModelService.updateReportModel(param, templateFile);
|
||||
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
|
||||
result, methodDescribe);
|
||||
}
|
||||
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DELETE)
|
||||
@ApiOperation("删除报告模板")
|
||||
@PostMapping("/delete")
|
||||
public HttpResult<Boolean> delete(@RequestBody List<String> ids) {
|
||||
String methodDescribe = getMethodDescribe("delete");
|
||||
boolean result = reportModelService.deleteReportModels(ids);
|
||||
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
|
||||
result, methodDescribe);
|
||||
}
|
||||
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
|
||||
@ApiOperation("查询报告模板详情")
|
||||
@ApiImplicitParam(name = "id", value = "模板ID", required = true)
|
||||
@GetMapping("/{id}")
|
||||
public HttpResult<ReportModelVO> detail(@PathVariable("id") String id) {
|
||||
String methodDescribe = getMethodDescribe("detail");
|
||||
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
|
||||
reportModelService.getReportModel(id), methodDescribe);
|
||||
}
|
||||
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
|
||||
@ApiOperation("下载报告模板文件")
|
||||
@ApiImplicitParam(name = "id", value = "模板ID", required = true)
|
||||
@GetMapping("/{id}/download")
|
||||
public ResponseEntity<InputStreamResource> download(@PathVariable("id") String id) {
|
||||
ReportModelPO model = reportModelService.requireNormal(id);
|
||||
Path filePath = reportModelFileStorageService.resolveDownloadPath(model.getPath());
|
||||
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "模板文件不存在");
|
||||
}
|
||||
try {
|
||||
String fileName = model.getFileName();
|
||||
if (fileName == null || fileName.trim().isEmpty()) {
|
||||
fileName = reportModelFileStorageService.resolveDownloadFileName(model.getPath());
|
||||
}
|
||||
String encodedFileName = encodeFileName(fileName);
|
||||
InputStream inputStream = Files.newInputStream(filePath);
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFileName)
|
||||
.body(new InputStreamResource(inputStream));
|
||||
} catch (Exception exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "读取模板文件失败");
|
||||
}
|
||||
}
|
||||
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
|
||||
@ApiOperation("预览报告模板文件")
|
||||
@ApiImplicitParam(name = "id", value = "模板ID", required = true)
|
||||
@GetMapping("/{id}/preview")
|
||||
public ResponseEntity<byte[]> preview(@PathVariable("id") String id) {
|
||||
ReportModelPO model = reportModelService.requireNormal(id);
|
||||
Path filePath = reportModelFileStorageService.resolveDownloadPath(model.getPath());
|
||||
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "模板文件不存在");
|
||||
}
|
||||
String fileName = model.getFileName();
|
||||
if (fileName == null || fileName.trim().isEmpty()) {
|
||||
fileName = reportModelFileStorageService.resolveDownloadFileName(model.getPath());
|
||||
}
|
||||
FilePreviewService.PreviewContent previewContent = filePreviewService.preview(filePath, fileName);
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION,
|
||||
"inline; filename*=UTF-8''" + encodeFileName(fileName))
|
||||
.header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate")
|
||||
.contentType(MediaType.parseMediaType(previewContent.getContentType()))
|
||||
.body(previewContent.getContent());
|
||||
}
|
||||
|
||||
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
|
||||
@ApiOperation("提交报告模板审核")
|
||||
@PostMapping("/submit-audit")
|
||||
public HttpResult<Boolean> submitAudit(@RequestBody @Validated ReportModelParam.SubmitAuditParam param) {
|
||||
String methodDescribe = getMethodDescribe("submitAudit");
|
||||
boolean result = reportModelService.submitAudit(param);
|
||||
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
|
||||
result, methodDescribe);
|
||||
}
|
||||
|
||||
private String encodeFileName(String fileName) {
|
||||
try {
|
||||
return URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replace("+", "%20");
|
||||
} catch (Exception exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "编码模板文件名失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.njcn.gather.aireport.reportmodel.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO;
|
||||
import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelUserNameVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ReportModelMapper extends BaseMapper<ReportModelPO> {
|
||||
|
||||
@Select({
|
||||
"<script>",
|
||||
"select id, name from sys_user where id in",
|
||||
"<foreach collection='ids' item='id' open='(' separator=',' close=')'>",
|
||||
"#{id}",
|
||||
"</foreach>",
|
||||
"</script>"
|
||||
})
|
||||
List<ReportModelUserNameVO> selectUserNamesByIds(@Param("ids") Collection<String> ids);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.njcn.gather.aireport.reportmodel.pojo.constant;
|
||||
|
||||
/**
|
||||
* Report model state constants.
|
||||
*/
|
||||
public final class ReportModelConst {
|
||||
|
||||
private ReportModelConst() {
|
||||
}
|
||||
|
||||
public static final String STATE_EDITING = "01";
|
||||
public static final String STATE_AUDITING = "02";
|
||||
public static final String STATE_APPROVED = "03";
|
||||
public static final String STATE_REJECTED = "04";
|
||||
public static final String STATE_DELETED = "05";
|
||||
|
||||
public static final Integer STATUS_DELETED = 0;
|
||||
public static final Integer STATUS_NORMAL = 1;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.njcn.gather.aireport.reportmodel.pojo.param;
|
||||
|
||||
import com.njcn.web.pojo.param.BaseParam;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* Report model request parameters.
|
||||
*/
|
||||
public class ReportModelParam {
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public static class QueryParam extends BaseParam {
|
||||
@ApiModelProperty("Template name keyword")
|
||||
private String name;
|
||||
|
||||
@ApiModelProperty("Template state")
|
||||
private String state;
|
||||
|
||||
@ApiModelProperty("Create time begin, yyyy-MM-dd HH:mm:ss")
|
||||
private String searchBeginTime;
|
||||
|
||||
@ApiModelProperty("Create time end, yyyy-MM-dd HH:mm:ss")
|
||||
private String searchEndTime;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class AddParam {
|
||||
@ApiModelProperty("Template name")
|
||||
@NotBlank(message = "模板名称不能为空")
|
||||
@Size(max = 32, message = "模板名称不能超过32个字符")
|
||||
private String name;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class UpdateParam {
|
||||
@ApiModelProperty("Template ID")
|
||||
@NotBlank(message = "模板ID不能为空")
|
||||
private String id;
|
||||
|
||||
@ApiModelProperty("Template name")
|
||||
@NotBlank(message = "模板名称不能为空")
|
||||
@Size(max = 32, message = "模板名称不能超过32个字符")
|
||||
private String name;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class SubmitAuditParam {
|
||||
@ApiModelProperty("Template ID")
|
||||
@NotBlank(message = "模板ID不能为空")
|
||||
private String id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.njcn.gather.aireport.reportmodel.pojo.po;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Report model template record.
|
||||
*/
|
||||
@Data
|
||||
@TableName("ai_reportmodel")
|
||||
public class ReportModelPO implements Serializable {
|
||||
private static final long serialVersionUID = -1413572272542367267L;
|
||||
|
||||
@TableId("id")
|
||||
private String id;
|
||||
@TableField("name")
|
||||
private String name;
|
||||
@TableField("state")
|
||||
private String state;
|
||||
@TableField("check_id")
|
||||
private String checkId;
|
||||
@TableField("check_time")
|
||||
private LocalDateTime checkTime;
|
||||
@TableField("check_result")
|
||||
private String checkResult;
|
||||
@TableField("path")
|
||||
private String path;
|
||||
@TableField("file_name")
|
||||
private String fileName;
|
||||
@TableField("status")
|
||||
private Integer status;
|
||||
@TableField("create_by")
|
||||
private String createBy;
|
||||
@TableField("create_time")
|
||||
private LocalDateTime createTime;
|
||||
@TableField("update_by")
|
||||
private String updateBy;
|
||||
@TableField("update_time")
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.njcn.gather.aireport.reportmodel.pojo.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ReportModelUserNameVO {
|
||||
private String id;
|
||||
private String name;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.njcn.gather.aireport.reportmodel.pojo.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Report model view object.
|
||||
*/
|
||||
@Data
|
||||
public class ReportModelVO {
|
||||
private static final String DEFAULT_EMPTY_DISPLAY = "-";
|
||||
|
||||
@ApiModelProperty("Template ID")
|
||||
private String id;
|
||||
@ApiModelProperty("Template name")
|
||||
private String name;
|
||||
@ApiModelProperty("Template state")
|
||||
private String state;
|
||||
@ApiModelProperty("Auditor ID")
|
||||
private String checkId;
|
||||
@ApiModelProperty("Auditor display name")
|
||||
private String checkerName;
|
||||
@ApiModelProperty("Audit time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
|
||||
@JsonSerialize(using = LocalDateTimeSerializer.class)
|
||||
private LocalDateTime checkTime;
|
||||
@ApiModelProperty("Audit comment")
|
||||
private String checkResult;
|
||||
@ApiModelProperty("Template file path")
|
||||
private String path;
|
||||
@ApiModelProperty("Template document file name")
|
||||
private String fileName;
|
||||
@ApiModelProperty("Logical status")
|
||||
private Integer status;
|
||||
@ApiModelProperty("Creator display name")
|
||||
private String createBy;
|
||||
@ApiModelProperty("Create time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
|
||||
@JsonSerialize(using = LocalDateTimeSerializer.class)
|
||||
private LocalDateTime createTime;
|
||||
@ApiModelProperty("Editor display name")
|
||||
private String updateBy;
|
||||
@ApiModelProperty("Editor display name")
|
||||
private String editorName;
|
||||
@ApiModelProperty("Update time")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
|
||||
@JsonSerialize(using = LocalDateTimeSerializer.class)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
public String getId() {
|
||||
return defaultDisplay(id);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return defaultDisplay(name);
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return defaultDisplay(state);
|
||||
}
|
||||
|
||||
public String getCheckId() {
|
||||
return defaultDisplay(checkId);
|
||||
}
|
||||
|
||||
public String getCheckerName() {
|
||||
return defaultDisplay(checkerName);
|
||||
}
|
||||
|
||||
public String getCheckResult() {
|
||||
return defaultDisplay(checkResult);
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return defaultDisplay(path);
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return defaultDisplay(fileName);
|
||||
}
|
||||
|
||||
public String getCreateBy() {
|
||||
return defaultDisplay(createBy);
|
||||
}
|
||||
|
||||
public String getUpdateBy() {
|
||||
return defaultDisplay(updateBy);
|
||||
}
|
||||
|
||||
public String getEditorName() {
|
||||
return defaultDisplay(editorName);
|
||||
}
|
||||
|
||||
private String defaultDisplay(String value) {
|
||||
return StringUtils.hasText(value) ? value : DEFAULT_EMPTY_DISPLAY;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.njcn.gather.aireport.reportmodel.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.njcn.gather.aireport.reportmodel.pojo.param.ReportModelParam;
|
||||
import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO;
|
||||
import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelVO;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ReportModelService extends IService<ReportModelPO> {
|
||||
|
||||
Page<ReportModelVO> listReportModels(ReportModelParam.QueryParam param);
|
||||
|
||||
boolean addReportModel(ReportModelParam.AddParam param, MultipartFile templateFile);
|
||||
|
||||
boolean updateReportModel(ReportModelParam.UpdateParam param, MultipartFile templateFile);
|
||||
|
||||
boolean deleteReportModels(List<String> ids);
|
||||
|
||||
ReportModelVO getReportModel(String id);
|
||||
|
||||
ReportModelPO requireNormal(String id);
|
||||
|
||||
boolean submitAudit(ReportModelParam.SubmitAuditParam param);
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
package com.njcn.gather.aireport.reportmodel.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
|
||||
import com.njcn.common.pojo.exception.BusinessException;
|
||||
import com.njcn.gather.aireport.reportmodel.component.ReportModelFileStorageService;
|
||||
import com.njcn.gather.aireport.reportmodel.component.ReportModelUserNameResolver;
|
||||
import com.njcn.gather.aireport.reportmodel.mapper.ReportModelMapper;
|
||||
import com.njcn.gather.aireport.reportmodel.pojo.constant.ReportModelConst;
|
||||
import com.njcn.gather.aireport.reportmodel.pojo.param.ReportModelParam;
|
||||
import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO;
|
||||
import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelVO;
|
||||
import com.njcn.gather.aireport.reportmodel.service.ReportModelService;
|
||||
import com.njcn.web.factory.PageFactory;
|
||||
import com.njcn.web.utils.RequestUtil;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Report model management service.
|
||||
*/
|
||||
@Service
|
||||
public class ReportModelServiceImpl extends ServiceImpl<ReportModelMapper, ReportModelPO> implements ReportModelService {
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private final ReportModelFileStorageService reportModelFileStorageService;
|
||||
private final ReportModelUserNameResolver reportModelUserNameResolver;
|
||||
|
||||
public ReportModelServiceImpl(ReportModelFileStorageService reportModelFileStorageService,
|
||||
ReportModelUserNameResolver reportModelUserNameResolver) {
|
||||
this.reportModelFileStorageService = reportModelFileStorageService;
|
||||
this.reportModelUserNameResolver = reportModelUserNameResolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<ReportModelVO> listReportModels(ReportModelParam.QueryParam param) {
|
||||
ReportModelParam.QueryParam query = param == null ? new ReportModelParam.QueryParam() : param;
|
||||
LambdaQueryWrapper<ReportModelPO> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(ReportModelPO::getStatus, ReportModelConst.STATUS_NORMAL)
|
||||
.like(StrUtil.isNotBlank(query.getName()), ReportModelPO::getName, query.getName())
|
||||
.eq(StrUtil.isNotBlank(query.getState()), ReportModelPO::getState, query.getState());
|
||||
if (StrUtil.isNotBlank(query.getSearchBeginTime())) {
|
||||
wrapper.ge(ReportModelPO::getUpdateTime, parseDateTime(query.getSearchBeginTime(), "开始时间格式不正确"));
|
||||
}
|
||||
if (StrUtil.isNotBlank(query.getSearchEndTime())) {
|
||||
wrapper.le(ReportModelPO::getUpdateTime, parseDateTime(query.getSearchEndTime(), "结束时间格式不正确"));
|
||||
}
|
||||
wrapper.orderByDesc(ReportModelPO::getCreateTime);
|
||||
Page<ReportModelPO> page = this.page(new Page<>(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), wrapper);
|
||||
Page<ReportModelVO> result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
|
||||
Map<String, String> userNameMap = resolveUserNameMap(page.getRecords());
|
||||
result.setRecords(page.getRecords().stream().map(model -> toVO(model, userNameMap)).collect(Collectors.toList()));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean addReportModel(ReportModelParam.AddParam param, MultipartFile templateFile) {
|
||||
String name = resolveName(param == null ? null : param.getName());
|
||||
checkNameUnique(name, null);
|
||||
ReportModelFileStorageService.StoredFile storedFile = reportModelFileStorageService.save(templateFile);
|
||||
try {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
String userId = resolveCurrentUserId();
|
||||
ReportModelPO model = new ReportModelPO();
|
||||
model.setId(UUID.randomUUID().toString());
|
||||
model.setName(name);
|
||||
model.setState(ReportModelConst.STATE_EDITING);
|
||||
model.setPath(storedFile.getPath());
|
||||
model.setFileName(storedFile.getFileName());
|
||||
model.setStatus(ReportModelConst.STATUS_NORMAL);
|
||||
model.setCreateBy(userId);
|
||||
model.setCreateTime(now);
|
||||
model.setUpdateBy(userId);
|
||||
model.setUpdateTime(now);
|
||||
return this.save(model);
|
||||
} catch (RuntimeException exception) {
|
||||
reportModelFileStorageService.deleteQuietly(storedFile.getPath());
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean updateReportModel(ReportModelParam.UpdateParam param, MultipartFile templateFile) {
|
||||
if (param == null || StrUtil.isBlank(param.getId())) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "模板ID不能为空");
|
||||
}
|
||||
ReportModelPO model = requireNormal(param.getId());
|
||||
if (!allowEdit(model.getState())) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "仅编制或退回状态的模板允许编辑");
|
||||
}
|
||||
String name = resolveName(param.getName());
|
||||
checkNameUnique(name, model.getId());
|
||||
String newStoredPath = null;
|
||||
if (templateFile != null && !templateFile.isEmpty()) {
|
||||
ReportModelFileStorageService.StoredFile storedFile = reportModelFileStorageService.save(templateFile);
|
||||
newStoredPath = storedFile.getPath();
|
||||
model.setPath(storedFile.getPath());
|
||||
model.setFileName(storedFile.getFileName());
|
||||
}
|
||||
try {
|
||||
model.setName(name);
|
||||
model.setUpdateBy(resolveCurrentUserId());
|
||||
model.setUpdateTime(LocalDateTime.now());
|
||||
return this.updateById(model);
|
||||
} catch (RuntimeException exception) {
|
||||
reportModelFileStorageService.deleteQuietly(newStoredPath);
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean deleteReportModels(List<String> ids) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "模板ID不能为空");
|
||||
}
|
||||
List<String> validIds = ids.stream().filter(StrUtil::isNotBlank).collect(Collectors.toList());
|
||||
if (validIds.isEmpty()) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "模板ID不能为空");
|
||||
}
|
||||
return this.lambdaUpdate()
|
||||
.set(ReportModelPO::getStatus, ReportModelConst.STATUS_DELETED)
|
||||
.set(ReportModelPO::getState, ReportModelConst.STATE_DELETED)
|
||||
.set(ReportModelPO::getUpdateBy, resolveCurrentUserId())
|
||||
.set(ReportModelPO::getUpdateTime, LocalDateTime.now())
|
||||
.in(ReportModelPO::getId, validIds)
|
||||
.eq(ReportModelPO::getStatus, ReportModelConst.STATUS_NORMAL)
|
||||
.update();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReportModelVO getReportModel(String id) {
|
||||
ReportModelPO model = requireNormal(id);
|
||||
return toVO(model, resolveUserNameMap(Arrays.asList(model)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReportModelPO requireNormal(String id) {
|
||||
if (StrUtil.isBlank(id)) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "模板ID不能为空");
|
||||
}
|
||||
ReportModelPO model = this.lambdaQuery()
|
||||
.eq(ReportModelPO::getId, id)
|
||||
.eq(ReportModelPO::getStatus, ReportModelConst.STATUS_NORMAL)
|
||||
.one();
|
||||
if (model == null) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "模板不存在或已删除");
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean submitAudit(ReportModelParam.SubmitAuditParam param) {
|
||||
if (param == null || StrUtil.isBlank(param.getId())) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "模板ID不能为空");
|
||||
}
|
||||
ReportModelPO model = requireNormal(param.getId());
|
||||
if (!ReportModelConst.STATE_EDITING.equals(model.getState())) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "仅编制状态的模板允许提交审核");
|
||||
}
|
||||
model.setState(ReportModelConst.STATE_AUDITING);
|
||||
model.setCheckId(null);
|
||||
model.setCheckTime(null);
|
||||
model.setCheckResult(null);
|
||||
model.setUpdateBy(resolveCurrentUserId());
|
||||
model.setUpdateTime(LocalDateTime.now());
|
||||
return this.updateById(model);
|
||||
}
|
||||
|
||||
private boolean allowEdit(String state) {
|
||||
return ReportModelConst.STATE_EDITING.equals(state) || ReportModelConst.STATE_REJECTED.equals(state);
|
||||
}
|
||||
|
||||
private void checkNameUnique(String name, String excludeId) {
|
||||
LambdaQueryWrapper<ReportModelPO> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(ReportModelPO::getName, name)
|
||||
.eq(ReportModelPO::getStatus, ReportModelConst.STATUS_NORMAL)
|
||||
.ne(StrUtil.isNotBlank(excludeId), ReportModelPO::getId, excludeId);
|
||||
if (this.count(wrapper) > 0) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "模板名称已存在");
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveName(String name) {
|
||||
String result = trimToNull(name);
|
||||
if (result == null) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "模板名称不能为空");
|
||||
}
|
||||
if (result.length() > 32) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, "模板名称不能超过32个字符");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private LocalDateTime parseDateTime(String value, String errorMessage) {
|
||||
if (StrUtil.isBlank(value)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDateTime.parse(value.trim(), DATE_TIME_FORMATTER);
|
||||
} catch (DateTimeParseException exception) {
|
||||
throw new BusinessException(CommonResponseEnum.FAIL, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private ReportModelVO toVO(ReportModelPO model) {
|
||||
return toVO(model, resolveUserNameMap(Arrays.asList(model)));
|
||||
}
|
||||
|
||||
private ReportModelVO toVO(ReportModelPO model, Map<String, String> userNameMap) {
|
||||
ReportModelVO vo = new ReportModelVO();
|
||||
BeanUtil.copyProperties(model, vo);
|
||||
vo.setCreateBy(resolveUserName(model.getCreateBy(), userNameMap));
|
||||
vo.setUpdateBy(resolveUserName(model.getUpdateBy(), userNameMap));
|
||||
vo.setEditorName(resolveEditorName(model, userNameMap));
|
||||
vo.setCheckerName(resolveUserName(model.getCheckId(), userNameMap));
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表“编辑人”优先取更新人,新增后若尚未产生独立更新记录,则回退到创建人。
|
||||
*/
|
||||
private Map<String, String> resolveUserNameMap(List<ReportModelPO> models) {
|
||||
List<String> userIds = models.stream()
|
||||
.flatMap(model -> Arrays.asList(model.getCreateBy(), model.getUpdateBy(), model.getCheckId()).stream())
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.collect(Collectors.toList());
|
||||
return reportModelUserNameResolver.resolveUserNames(userIds);
|
||||
}
|
||||
|
||||
private String resolveEditorName(ReportModelPO model, Map<String, String> userNameMap) {
|
||||
if (model == null) {
|
||||
return null;
|
||||
}
|
||||
if (StrUtil.isNotBlank(model.getUpdateBy())) {
|
||||
return resolveUserName(model.getUpdateBy(), userNameMap);
|
||||
}
|
||||
return StrUtil.isNotBlank(model.getCreateBy()) ? resolveUserName(model.getCreateBy(), userNameMap) : null;
|
||||
}
|
||||
|
||||
private String resolveUserName(String userId, Map<String, String> userNameMap) {
|
||||
if (StrUtil.isBlank(userId)) {
|
||||
return null;
|
||||
}
|
||||
String userName = userNameMap == null ? null : userNameMap.get(userId);
|
||||
return StrUtil.isNotBlank(userName) ? userName : userId;
|
||||
}
|
||||
|
||||
private String resolveCurrentUserId() {
|
||||
String userId = RequestUtil.getUserId();
|
||||
return StrUtil.isBlank(userId) ? null : userId;
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String text = value.trim();
|
||||
return text.isEmpty() ? null : text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.njcn.gather.aireport.reportmodel.component;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class ReportModelFileStorageServiceTest {
|
||||
|
||||
@Test
|
||||
public void saveSanitizesFileNameAndReturnsRelativePath() throws Exception {
|
||||
Path filesRoot = Files.createTempDirectory("files-root");
|
||||
ReportModelFileStorageService service = new ReportModelFileStorageService();
|
||||
ReflectionTestUtils.setField(service, "filesPath", filesRoot.toString());
|
||||
|
||||
ReportModelFileStorageService.StoredFile storedFile = service.save(new MockMultipartFile("templateFile", "../a:b.docx",
|
||||
"application/octet-stream", "content".getBytes("UTF-8")));
|
||||
String storedPath = storedFile.getPath();
|
||||
|
||||
Assert.assertFalse(storedPath.contains(".."));
|
||||
Assert.assertTrue(storedPath.endsWith("-a_b.docx"));
|
||||
Assert.assertEquals("a_b.docx", storedFile.getFileName());
|
||||
Assert.assertTrue(service.resolveDownloadPath(storedPath)
|
||||
.startsWith(filesRoot.resolve("report-model").toAbsolutePath().normalize()));
|
||||
Assert.assertTrue(Files.exists(service.resolveDownloadPath(storedPath)));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void resolveDownloadPathRejectsPathOutsideStorageRoot() throws Exception {
|
||||
Path filesRoot = Files.createTempDirectory("files-root");
|
||||
ReportModelFileStorageService service = new ReportModelFileStorageService();
|
||||
ReflectionTestUtils.setField(service, "filesPath", filesRoot.toString());
|
||||
|
||||
service.resolveDownloadPath("../outside.docx");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.njcn.gather.aireport.reportmodel.component;
|
||||
|
||||
import com.njcn.gather.aireport.reportmodel.mapper.ReportModelMapper;
|
||||
import com.njcn.gather.aireport.reportmodel.pojo.vo.ReportModelUserNameVO;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.anyCollection;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ReportModelUserNameResolverTest {
|
||||
|
||||
@Test
|
||||
public void resolveUserNamesReturnsSysUserNameById() {
|
||||
ReportModelMapper mapper = mock(ReportModelMapper.class);
|
||||
ReportModelUserNameVO user = new ReportModelUserNameVO();
|
||||
user.setId("user-001");
|
||||
user.setName("张三");
|
||||
when(mapper.selectUserNamesByIds(anyCollection())).thenReturn(Arrays.asList(user));
|
||||
|
||||
ReportModelUserNameResolver resolver = new ReportModelUserNameResolver(mapper);
|
||||
Map<String, String> result = resolver.resolveUserNames(Arrays.asList("user-001"));
|
||||
|
||||
Assert.assertEquals("张三", result.get("user-001"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.njcn.gather.aireport.reportmodel.pojo.vo;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class ReportModelVODefaultDisplayTest {
|
||||
|
||||
@Test
|
||||
public void stringFieldsUseDashWhenValueMissing() {
|
||||
ReportModelVO vo = new ReportModelVO();
|
||||
vo.setCheckerName(" ");
|
||||
vo.setCheckResult(null);
|
||||
vo.setEditorName("");
|
||||
vo.setUpdateBy(" ");
|
||||
vo.setFileName(null);
|
||||
|
||||
Assert.assertEquals("-", vo.getCheckerName());
|
||||
Assert.assertEquals("-", vo.getCheckResult());
|
||||
Assert.assertEquals("-", vo.getEditorName());
|
||||
Assert.assertEquals("-", vo.getUpdateBy());
|
||||
Assert.assertEquals("-", vo.getFileName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stringFieldsKeepOriginalTextWhenValuePresent() {
|
||||
ReportModelVO vo = new ReportModelVO();
|
||||
vo.setCheckerName("审核人A");
|
||||
vo.setCheckResult("审核通过");
|
||||
|
||||
Assert.assertEquals("审核人A", vo.getCheckerName());
|
||||
Assert.assertEquals("审核通过", vo.getCheckResult());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.njcn.gather.aireport.reportmodel.pojo.vo;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class ReportModelVOTimeFormatTest {
|
||||
|
||||
@Test
|
||||
public void timeJsonUsesStandardTwentyFourHourFormat() throws Exception {
|
||||
ReportModelVO vo = new ReportModelVO();
|
||||
vo.setCreateTime(LocalDateTime.of(2026, 6, 25, 13, 19, 25));
|
||||
vo.setUpdateTime(LocalDateTime.of(2026, 6, 25, 13, 20, 41));
|
||||
|
||||
String json = new ObjectMapper().writeValueAsString(vo);
|
||||
|
||||
Assert.assertTrue(json.contains("\"createTime\":\"2026-06-25 13:19:25\""));
|
||||
Assert.assertTrue(json.contains("\"updateTime\":\"2026-06-25 13:20:41\""));
|
||||
Assert.assertFalse(json.contains("2026-06-25T13"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.njcn.gather.aireport.reportmodel.service.impl;
|
||||
|
||||
import com.njcn.gather.aireport.reportmodel.component.ReportModelFileStorageService;
|
||||
import com.njcn.gather.aireport.reportmodel.component.ReportModelUserNameResolver;
|
||||
import com.njcn.gather.aireport.reportmodel.pojo.constant.ReportModelConst;
|
||||
import com.njcn.gather.aireport.reportmodel.pojo.param.ReportModelParam;
|
||||
import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
public class ReportModelServiceImplTest {
|
||||
|
||||
@Test
|
||||
public void submitAuditShouldResetAuditFieldsWhenMoveToAuditing() {
|
||||
ReportModelPO model = new ReportModelPO();
|
||||
model.setId("model-001");
|
||||
model.setState(ReportModelConst.STATE_EDITING);
|
||||
model.setStatus(ReportModelConst.STATUS_NORMAL);
|
||||
model.setCheckId("checker-001");
|
||||
model.setCheckTime(LocalDateTime.of(2026, 6, 26, 9, 30, 0));
|
||||
model.setCheckResult("已有审核意见");
|
||||
|
||||
TestableReportModelServiceImpl service = new TestableReportModelServiceImpl(model);
|
||||
ReportModelParam.SubmitAuditParam param = new ReportModelParam.SubmitAuditParam();
|
||||
param.setId("model-001");
|
||||
|
||||
boolean result = service.submitAudit(param);
|
||||
|
||||
Assert.assertTrue(result);
|
||||
Assert.assertEquals(ReportModelConst.STATE_AUDITING, model.getState());
|
||||
Assert.assertNull(model.getCheckId());
|
||||
Assert.assertNull(model.getCheckTime());
|
||||
Assert.assertNull(model.getCheckResult());
|
||||
Assert.assertSame(model, service.getUpdatedModel());
|
||||
Assert.assertNotNull(model.getUpdateTime());
|
||||
}
|
||||
|
||||
private static class TestableReportModelServiceImpl extends ReportModelServiceImpl {
|
||||
|
||||
private final ReportModelPO model;
|
||||
private ReportModelPO updatedModel;
|
||||
|
||||
private TestableReportModelServiceImpl(ReportModelPO model) {
|
||||
super(mock(ReportModelFileStorageService.class), mock(ReportModelUserNameResolver.class));
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReportModelPO requireNormal(String id) {
|
||||
return model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateById(ReportModelPO entity) {
|
||||
this.updatedModel = entity;
|
||||
return true;
|
||||
}
|
||||
|
||||
private ReportModelPO getUpdatedModel() {
|
||||
return updatedModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user