11 Commits

Author SHA1 Message Date
dk
55a74815b2 feat(ai-report): 新增MinIO文件存储服务
- 添加AiReportFileConst和AiReportFileExtensionConst常量类
- 实现AiReportMinioStorageService存储服务
- 添加AiReportMinioProperties配置类
- 集成MinIO依赖到ai-report-common模块
- 配置应用yml中的ai-report桶设置
- 更新报告模型控制器使用MinIO存储服务
- 修改报告模型文件存储组件集成MinIO
- 添加相关单元测试验证功能
2026-07-14 16:38:11 +08:00
aac21e5f73 Merge remote-tracking branch 'origin/main' 2026-07-14 08:37:23 +08:00
6659df649a 添加了单独的长闪报文 2026-07-14 08:37:12 +08:00
dk
9b71dfdce3 Merge remote-tracking branch 'origin/main' 2026-07-13 22:00:49 +08:00
dk
46ae986db2 feat(storage): 集成 MinIO 存储系统并支持用户签名功能
- 添加 MinIO 配置和依赖项,包括 minio、okhttp、okio 和 kotlin 库
- 创建通用文件服务接口和实现,支持文件上传到 MinIO 并记录元数据
- 添加用户签名服务接口和数据库表结构,支持签名文件管理
- 修改用户服务以支持签名文件上传,在新增和更新用户时可上传签名文件
- 实现文件预览服务测试用例的注释调整
- 创建 SysFile 和 SysUserSignature 数据库实体及对应常量类
- 实现 MinIO 存储组件,提供文件上传、下载和删除功能
- 在用户实体中添加签名相关字段用于展示签名信息
2026-07-13 22:00:39 +08:00
周宇 蔡
6fc0d58fe4 fix(config): 修正配置文件中的命名规范和路径引用
- 将XML模板中的零序电流不平衡字段名从G_VZSEQ_UNBAN/VZSEQ_UNBAN更正为G_IZSEQ_UNBAN/IZSEQ_UNBAN
- 将默认规则文件路径从template/默认规则.txt统一更改为template/DefaultRule.txt
- 更新配置类和映射服务中对默认规则文件的引用路径
2026-07-13 10:48:56 +08:00
周宇 蔡
42911e35bb Merge branch 'dev-czy' 2026-07-10 11:20:58 +08:00
周宇 蔡
cbafe85c4a fix(mms-mapping): 修复谐波数据映射配置和偏移量计算问题
- 修改JiangSu_Config1.xml中谐波电流相关指标的命名规则
- 修改默认规则文件中相电压间谐波含有率序列的描述名称
- 添加相电压间谐波含有率序列实时数据值配置
- 更新JsonToXmlConversionService中遥测数据处理逻辑,将urcb相关标识修改为brcb
- 新增calculateOffset方法用于计算指标偏移量
- 实现adjustPathWithOffset方法根据offset调整路径中的数值格式
- 添加offset字段到MetricInfo类中用于存储偏移信息
- 在规则匹配过程中传递offset值并在生成Value节点时应用偏移调整
2026-07-10 11:20:31 +08:00
d8ae012bd2 feat(test-report): 重构台账导入功能并增加验证机制
- 新增导入临时目录、验证会话等常量配置
- 实现台账文件分步验证流程,支持先验证后导入模式
- 在控制器层新增validateLedger接口用于文件校验
- 修改importLedger接口参数结构,改为导入已验证批次
- 重构TestReportLedgerExcelParser解析器,支持独立验证和解析
- 新增验证结果对象TestReportLedgerValidateResultVO
- 实现台账文件批量存储和临时文件管理机制
- 完善文件上传、校验、导入各阶段的状态跟踪
- 优化错误提示信息,提供更详细的验证反馈
- 更新导入成功结果对象,增加批次ID和存储路径信息
2026-07-10 10:34:07 +08:00
周宇 蔡
fa1a285e04 fix(config): 修复江苏配置模板中的相角参数和波形时间标志处理
- 实现了从JSON映射文档动态替换ComtradeFile时间标志功能
- 调整了URCB相关配置的参数序列以匹配正确的数据结构
2026-07-08 13:43:10 +08:00
f5f828cd05 feat(test-report): 实现测试报告台账导入与分组生成功能
- 在.gitignore中添加各模块SQL资源目录忽略规则
- 从client-unit模块移除空的MyBatis映射文件
- 在test-report模块pom.xml中添加client-unit、test-device依赖及poi-ooxml库
- 扩展TestReportConst类添加报告生成状态、存储路径等常量定义
- 增强TestReportController实现台账模板下载、数据导入、分组管理等REST接口
- 修改TestReportMapper查询语句添加报告生成状态和数量统计字段
- 扩展TestReportParam.AddParam添加可选ID参数支持前端指定ID
- 在TestReportPO实体类中增加reportGenerateState字段存储生成状态
- 完善TestReportService接口定义台账导入、分组管理等业务方法
- 实现TestReportServiceImpl服务类中的台账导入、分组生成核心逻辑
- 添加报告ID解析、导入验证、文件处理等辅助方法实现
2026-07-03 16:05:17 +08:00
84 changed files with 13158 additions and 761 deletions

5
.gitignore vendored
View File

@@ -52,3 +52,8 @@ rebel.xml
/.fastRequest/collections/Root/Default Group/directory.json /.fastRequest/collections/Root/Default Group/directory.json
/.fastRequest/collections/Root/directory.json /.fastRequest/collections/Root/directory.json
/.fastRequest/config/fastRequestCurrentProjectConfig.json /.fastRequest/config/fastRequestCurrentProjectConfig.json
/ai-report/report-approval/src/main/resources/sql/report-approval/
/ai-report/test-report/src/main/resources/sql/test-report/
/ai-report/client-unit/src/main/resources/sql/client-unit/
/ai-report/report-model/src/main/resources/sql/report-model/
/ai-report/test-device/src/main/resources/sql/test-device/

View File

@@ -0,0 +1,35 @@
<?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>ai-report-common</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>njcn-common</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.njcn</groupId>
<artifactId>spingboot2.3.12</artifactId>
<version>2.3.12</version>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.4.3</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,36 @@
package com.njcn.gather.aireport.common.config;
import cn.hutool.core.util.StrUtil;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* ai-report MinIO 配置。
*/
@Data
@Component
@ConfigurationProperties(prefix = "storage.minio")
public class AiReportMinioProperties {
private String endpoint;
private String accessKey;
private String secretKey;
private Map<String, String> buckets = new LinkedHashMap<String, String>();
public String requireBucket(String bucketKey) {
String bucketName = buckets.get(bucketKey);
if (StrUtil.isBlank(bucketName)) {
throw new BusinessException(CommonResponseEnum.FAIL, "MinIO桶配置缺失" + bucketKey);
}
return bucketName;
}
}

View File

@@ -0,0 +1,16 @@
package com.njcn.gather.aireport.common.constant;
/**
* ai-report 文件常量。
*/
public final class AiReportFileConst {
private AiReportFileConst() {
}
public static final String BUCKET_KEY_AI_REPORT = "ai-report";
public static final String BIZ_DIR_REPORT_MODEL = "report-model";
public static final String BIZ_DIR_TEST_DEVICE = "test-device";
public static final String BIZ_DIR_TEST_REPORT = "test-report";
}

View File

@@ -0,0 +1,27 @@
package com.njcn.gather.aireport.common.constant;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* ai-report 文件扩展名白名单。
*/
public final class AiReportFileExtensionConst {
private AiReportFileExtensionConst() {
}
public static final Set<String> ALLOWED_EXTENSIONS = unmodifiable("docx", "xlsx", "pdf");
public static final Set<String> WORD_EXTENSIONS = unmodifiable("docx");
public static final Set<String> EXCEL_EXTENSIONS = unmodifiable("xlsx");
public static final Set<String> PDF_EXTENSIONS = unmodifiable("pdf");
private static Set<String> unmodifiable(String... extensions) {
return Collections.unmodifiableSet(new LinkedHashSet<String>(Arrays.asList(extensions)));
}
}

View File

@@ -0,0 +1,384 @@
package com.njcn.gather.aireport.common.storage;
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.common.config.AiReportMinioProperties;
import com.njcn.gather.aireport.common.constant.AiReportFileConst;
import io.minio.BucketExistsArgs;
import io.minio.GetObjectArgs;
import io.minio.GetObjectResponse;
import io.minio.ListObjectsArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import io.minio.Result;
import io.minio.StatObjectArgs;
import io.minio.errors.ErrorResponseException;
import io.minio.messages.Item;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
/**
* ai-report MinIO 存储服务。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class AiReportMinioStorageService {
private final AiReportMinioProperties properties;
public StoredFile saveMultipartFile(String bizDir, String ownerId, MultipartFile file, boolean keepOriginalName) {
MultipartFile normalizedFile = requireFile(file);
String originalFileName = normalizeFileName(normalizedFile.getOriginalFilename());
String targetFileName = keepOriginalName ? sanitizeFileName(originalFileName) : buildStoredFileName(originalFileName);
String objectName = buildObjectName(bizDir, ownerId, targetFileName);
String bucketName = requireBucket();
String contentType = resolveContentType(normalizedFile.getContentType(), originalFileName);
try (InputStream inputStream = normalizedFile.getInputStream()) {
putObject(bucketName, objectName, inputStream, normalizedFile.getSize(), contentType);
return new StoredFile(bucketName, objectName, targetFileName, originalFileName, contentType, normalizedFile.getSize());
} catch (BusinessException exception) {
throw exception;
} catch (Exception exception) {
log.error("save multipart file to minio failed, objectName={}", objectName, exception);
throw new BusinessException(CommonResponseEnum.FAIL, "保存文件失败");
}
}
public StoredFile saveInputStream(String bizDir, String ownerId, String originalFileName, InputStream inputStream,
long size, String contentType, boolean keepOriginalName) {
String normalizedFileName = normalizeFileName(originalFileName);
String targetFileName = keepOriginalName ? sanitizeFileName(normalizedFileName) : buildStoredFileName(normalizedFileName);
String objectName = buildObjectName(bizDir, ownerId, targetFileName);
String bucketName = requireBucket();
try {
putObject(bucketName, objectName, inputStream, size, resolveContentType(contentType, normalizedFileName));
return new StoredFile(bucketName, objectName, targetFileName, normalizedFileName,
resolveContentType(contentType, normalizedFileName), size);
} catch (BusinessException exception) {
throw exception;
} catch (Exception exception) {
log.error("save input stream to minio failed, objectName={}", objectName, exception);
throw new BusinessException(CommonResponseEnum.FAIL, "保存文件失败");
}
}
public StoredFile saveBytes(String bizDir, String ownerId, String originalFileName, byte[] content,
String contentType, boolean keepOriginalName) {
byte[] fileContent = content == null ? new byte[0] : content;
return saveInputStream(bizDir, ownerId, originalFileName, new ByteArrayInputStream(fileContent),
fileContent.length, contentType, keepOriginalName);
}
public StoredObject getObject(String objectName) {
if (StrUtil.isBlank(objectName)) {
throw new BusinessException(CommonResponseEnum.FAIL, "文件不存在");
}
String bucketName = requireBucket();
try {
GetObjectResponse response = buildClient().getObject(GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
return new StoredObject(bucketName, objectName, response);
} catch (BusinessException exception) {
throw exception;
} catch (Exception exception) {
log.error("read file from minio failed, objectName={}", objectName, exception);
throw new BusinessException(CommonResponseEnum.FAIL, "读取文件失败");
}
}
public byte[] readBytes(String objectName) {
try (StoredObject storedObject = getObject(objectName);
InputStream inputStream = storedObject.getInputStream();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
byte[] buffer = new byte[8192];
int length;
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
return outputStream.toByteArray();
} catch (BusinessException exception) {
throw exception;
} catch (Exception exception) {
log.error("read file bytes from minio failed, objectName={}", objectName, exception);
throw new BusinessException(CommonResponseEnum.FAIL, "读取文件失败");
}
}
public Path downloadToTempFile(String objectName, String fileName) {
String normalizedFileName = normalizeFileName(fileName);
String extension = resolveExtension(normalizedFileName);
String suffix = StrUtil.isBlank(extension) ? ".tmp" : "." + extension;
try (StoredObject storedObject = getObject(objectName);
InputStream inputStream = storedObject.getInputStream()) {
Path tempFile = Files.createTempFile("ai-report-", suffix);
Files.copy(inputStream, tempFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
return tempFile;
} catch (BusinessException exception) {
throw exception;
} catch (Exception exception) {
log.error("download minio object to temp file failed, objectName={}", objectName, exception);
throw new BusinessException(CommonResponseEnum.FAIL, "读取文件失败");
}
}
public List<String> listFileNames(String prefix) {
List<String> objectNames = listObjectNames(prefix);
List<String> result = new ArrayList<String>();
for (String objectName : objectNames) {
int slashIndex = objectName.lastIndexOf('/');
result.add(slashIndex >= 0 ? objectName.substring(slashIndex + 1) : objectName);
}
return result;
}
public List<String> listObjectNames(String prefix) {
List<String> result = new ArrayList<String>();
if (StrUtil.isBlank(prefix)) {
return result;
}
try {
Iterable<Result<Item>> iterable = buildClient().listObjects(ListObjectsArgs.builder()
.bucket(requireBucket())
.prefix(trimPath(prefix) + "/")
.recursive(true)
.build());
for (Result<Item> itemResult : iterable) {
Item item = itemResult.get();
if (item == null || item.isDir()) {
continue;
}
String objectName = item.objectName();
if (StrUtil.isBlank(objectName)) {
continue;
}
result.add(objectName);
}
return result;
} catch (Exception exception) {
log.error("list minio objects failed, prefix={}", prefix, exception);
throw new BusinessException(CommonResponseEnum.FAIL, "读取文件失败");
}
}
public void deleteQuietly(String objectName) {
if (StrUtil.isBlank(objectName)) {
return;
}
try {
buildClient().removeObject(RemoveObjectArgs.builder()
.bucket(requireBucket())
.object(objectName)
.build());
} catch (Exception exception) {
log.warn("delete file from minio failed, objectName={}", objectName, exception);
}
}
public boolean exists(String objectName) {
if (StrUtil.isBlank(objectName)) {
return false;
}
try {
buildClient().statObject(StatObjectArgs.builder()
.bucket(requireBucket())
.object(objectName)
.build());
return true;
} catch (ErrorResponseException exception) {
String errorCode = exception.errorResponse() == null ? null : exception.errorResponse().code();
if ("NoSuchKey".equals(errorCode) || "NoSuchObject".equals(errorCode) || "NoSuchBucket".equals(errorCode)) {
return false;
}
log.error("stat minio object failed, objectName={}", objectName, exception);
throw new BusinessException(CommonResponseEnum.FAIL, "读取文件失败");
} catch (Exception exception) {
log.error("stat minio object failed, objectName={}", objectName, exception);
throw new BusinessException(CommonResponseEnum.FAIL, "读取文件失败");
}
}
public void deleteTempFileQuietly(Path tempFile) {
if (tempFile == null) {
return;
}
try {
Files.deleteIfExists(tempFile);
} catch (IOException ignored) {
// 临时文件清理失败不影响主流程。
}
}
private void putObject(String bucketName, String objectName, InputStream inputStream, long size, String contentType) throws Exception {
ensureBucket(buildClient(), bucketName);
buildClient().putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(inputStream, size, -1)
.contentType(contentType)
.build());
}
private MultipartFile requireFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "上传文件不能为空");
}
return file;
}
private String requireBucket() {
validateConfig();
return properties.requireBucket(AiReportFileConst.BUCKET_KEY_AI_REPORT);
}
private void validateConfig() {
if (StrUtil.hasBlank(properties.getEndpoint(), properties.getAccessKey(), properties.getSecretKey())) {
throw new BusinessException(CommonResponseEnum.FAIL, "MinIO配置不完整");
}
}
private MinioClient buildClient() {
return MinioClient.builder()
.endpoint(properties.getEndpoint())
.credentials(properties.getAccessKey(), properties.getSecretKey())
.build();
}
private void ensureBucket(MinioClient client, String bucketName) throws Exception {
boolean exists = client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!exists) {
client.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
}
private String buildObjectName(String bizDir, String ownerId, String fileName) {
StringBuilder builder = new StringBuilder();
builder.append(trimPath(bizDir));
if (StrUtil.isNotBlank(ownerId)) {
builder.append("/").append(trimPath(ownerId));
}
builder.append("/").append(fileName);
return builder.toString();
}
private String buildStoredFileName(String originalFileName) {
return UUID.randomUUID().toString().replace("-", "") + "-" + sanitizeFileName(originalFileName);
}
private String sanitizeFileName(String originalFileName) {
String fileName = normalizeFileName(originalFileName);
int slashIndex = Math.max(fileName.lastIndexOf('/'), fileName.lastIndexOf('\\'));
String normalizedFileName = slashIndex >= 0 ? fileName.substring(slashIndex + 1) : fileName;
return normalizedFileName.replaceAll("[\\\\/:*?\"<>|]", "_");
}
private String normalizeFileName(String originalFileName) {
String fileName = StrUtil.trimToEmpty(originalFileName);
if (StrUtil.isBlank(fileName)) {
throw new BusinessException(CommonResponseEnum.FAIL, "文件名不能为空");
}
return fileName;
}
private String trimPath(String value) {
String normalized = StrUtil.trimToEmpty(value).replace("\\", "/");
while (normalized.startsWith("/")) {
normalized = normalized.substring(1);
}
while (normalized.endsWith("/")) {
normalized = normalized.substring(0, normalized.length() - 1);
}
if (StrUtil.isBlank(normalized)) {
throw new BusinessException(CommonResponseEnum.FAIL, "对象目录配置不合法");
}
return normalized;
}
private String resolveExtension(String fileName) {
int dotIndex = fileName.lastIndexOf('.');
if (dotIndex < 0 || dotIndex == fileName.length() - 1) {
return "";
}
return fileName.substring(dotIndex + 1).toLowerCase(Locale.ENGLISH);
}
private String resolveContentType(String contentType, String fileName) {
if (StrUtil.isNotBlank(contentType)) {
return contentType;
}
return MediaTypeFactory.getMediaType(fileName).map(MediaType::toString)
.orElse(MediaType.APPLICATION_OCTET_STREAM_VALUE);
}
@Getter
public static class StoredFile {
private final String bucketName;
private final String objectName;
private final String storedFileName;
private final String originalFileName;
private final String contentType;
private final long fileSize;
public StoredFile(String bucketName, String objectName, String storedFileName, String originalFileName,
String contentType, long fileSize) {
this.bucketName = bucketName;
this.objectName = objectName;
this.storedFileName = storedFileName;
this.originalFileName = originalFileName;
this.contentType = contentType;
this.fileSize = fileSize;
}
}
public static class StoredObject implements AutoCloseable {
private final String bucketName;
private final String objectName;
private final GetObjectResponse inputStream;
public StoredObject(String bucketName, String objectName, GetObjectResponse inputStream) {
this.bucketName = bucketName;
this.objectName = objectName;
this.inputStream = inputStream;
}
public String getBucketName() {
return bucketName;
}
public String getObjectName() {
return objectName;
}
public GetObjectResponse getInputStream() {
return inputStream;
}
@Override
public void close() throws Exception {
if (inputStream != null) {
inputStream.close();
}
}
}
}

View File

@@ -1,6 +0,0 @@
<?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

@@ -16,6 +16,7 @@
<description>AI report capability aggregator.</description> <description>AI report capability aggregator.</description>
<modules> <modules>
<module>ai-report-common</module>
<module>report-model</module> <module>report-model</module>
<module>client-unit</module> <module>client-unit</module>
<module>test-device</module> <module>test-device</module>

View File

@@ -14,6 +14,12 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<dependencies> <dependencies>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>ai-report-common</artifactId>
<version>1.0.0</version>
</dependency>
<dependency> <dependency>
<groupId>com.njcn.gather</groupId> <groupId>com.njcn.gather</groupId>
<artifactId>comservice</artifactId> <artifactId>comservice</artifactId>

View File

@@ -1,115 +1,84 @@
package com.njcn.gather.aireport.reportmodel.component; package com.njcn.gather.aireport.reportmodel.component;
import org.springframework.beans.factory.annotation.Value; 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.common.constant.AiReportFileConst;
import com.njcn.gather.aireport.common.constant.AiReportFileExtensionConst;
import com.njcn.gather.aireport.common.storage.AiReportMinioStorageService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.util.Locale;
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. * 报告模板文件 MinIO 存储服务。
*/ */
@Component @Component
@RequiredArgsConstructor
public class ReportModelFileStorageService { public class ReportModelFileStorageService {
private static final String DEFAULT_STORAGE_DIR = "data/report-model"; private final AiReportMinioStorageService minioStorageService;
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) { public StoredFile save(MultipartFile templateFile) {
if (templateFile == null || templateFile.isEmpty()) { validateTemplateFile(templateFile);
throw new IllegalArgumentException("模板文件不能为空"); AiReportMinioStorageService.StoredFile storedFile = minioStorageService.saveMultipartFile(
} AiReportFileConst.BIZ_DIR_REPORT_MODEL, null, templateFile, false);
try { return new StoredFile(storedFile.getObjectName(), storedFile.getOriginalFileName());
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) { public AiReportMinioStorageService.StoredObject getObject(String storedPath) {
if (trimToNull(storedPath) == null) { return minioStorageService.getObject(storedPath);
throw new IllegalArgumentException("模板文件路径不能为空"); }
}
Path storageRoot = resolveStorageRoot(); public byte[] readBytes(String storedPath) {
Path target = storageRoot.resolve(storedPath).normalize(); return minioStorageService.readBytes(storedPath);
ensurePathInRoot(storageRoot, target); }
return target;
public Path downloadToTempFile(String storedPath, String fileName) {
return minioStorageService.downloadToTempFile(storedPath, fileName);
} }
public void deleteQuietly(String storedPath) { public void deleteQuietly(String storedPath) {
try { minioStorageService.deleteQuietly(storedPath);
if (trimToNull(storedPath) != null) { }
Files.deleteIfExists(resolveDownloadPath(storedPath));
} public void deleteTempFileQuietly(Path tempFile) {
} catch (Exception ignored) { minioStorageService.deleteTempFileQuietly(tempFile);
// Cleanup failure must not hide the original database error.
}
} }
public String resolveDownloadFileName(String storedPath) { public String resolveDownloadFileName(String storedPath) {
Path fileName = Paths.get(storedPath).getFileName(); if (StrUtil.isBlank(storedPath)) {
if (fileName == null) { return "report-model.docx";
return "report-model";
} }
String name = fileName.toString(); int slashIndex = storedPath.lastIndexOf('/');
int separatorIndex = name.indexOf('-'); String fileName = slashIndex >= 0 ? storedPath.substring(slashIndex + 1) : storedPath;
return separatorIndex >= 0 && separatorIndex + 1 < name.length() ? name.substring(separatorIndex + 1) : name; int separatorIndex = fileName.indexOf('-');
return separatorIndex >= 0 && separatorIndex + 1 < fileName.length() ? fileName.substring(separatorIndex + 1) : fileName;
} }
private Path resolveStorageRoot() { private void validateTemplateFile(MultipartFile templateFile) {
String configuredPath = trimToNull(filesPath); if (templateFile == null || templateFile.isEmpty()) {
Path path = configuredPath == null ? Paths.get(DEFAULT_STORAGE_DIR) : Paths.get(configuredPath, REPORT_MODEL_STORAGE_DIR); throw new IllegalArgumentException("模板文件不能为空");
return path.toAbsolutePath().normalize(); }
} String fileName = templateFile.getOriginalFilename();
if (StrUtil.isBlank(fileName)) {
private void ensurePathInRoot(Path storageRoot, Path target) { throw new IllegalArgumentException("模板文件名不能为空");
if (!target.startsWith(storageRoot)) { }
throw new IllegalArgumentException("模板文件路径不合法"); String extension = resolveExtension(fileName);
if (!AiReportFileExtensionConst.WORD_EXTENSIONS.contains(extension)) {
throw new IllegalArgumentException("模板文件仅支持.docx格式");
} }
} }
private String sanitizeFileName(String originalFilename) { private String resolveExtension(String fileName) {
String fileName = trimToNull(originalFilename); int dotIndex = fileName.lastIndexOf('.');
if (fileName == null) { if (dotIndex < 0 || dotIndex == fileName.length() - 1) {
return "unnamed"; return "";
} }
String normalizedName = Paths.get(fileName).getFileName().toString(); return fileName.substring(dotIndex + 1).toLowerCase(Locale.ENGLISH);
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 { public static class StoredFile {

View File

@@ -7,6 +7,7 @@ import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum; import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException; import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.common.pojo.response.HttpResult; import com.njcn.common.pojo.response.HttpResult;
import com.njcn.gather.aireport.common.storage.AiReportMinioStorageService;
import com.njcn.gather.aireport.reportmodel.component.ReportModelFileStorageService; import com.njcn.gather.aireport.reportmodel.component.ReportModelFileStorageService;
import com.njcn.gather.aireport.reportmodel.pojo.param.ReportModelParam; 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.po.ReportModelPO;
@@ -33,10 +34,8 @@ import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
@@ -110,21 +109,14 @@ public class ReportModelController extends BaseController {
@GetMapping("/{id}/download") @GetMapping("/{id}/download")
public ResponseEntity<InputStreamResource> download(@PathVariable("id") String id) { public ResponseEntity<InputStreamResource> download(@PathVariable("id") String id) {
ReportModelPO model = reportModelService.requireNormal(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 { try {
String fileName = model.getFileName(); String fileName = resolveFileName(model);
if (fileName == null || fileName.trim().isEmpty()) {
fileName = reportModelFileStorageService.resolveDownloadFileName(model.getPath());
}
String encodedFileName = encodeFileName(fileName); String encodedFileName = encodeFileName(fileName);
InputStream inputStream = Files.newInputStream(filePath); AiReportMinioStorageService.StoredObject storedObject = reportModelFileStorageService.getObject(model.getPath());
return ResponseEntity.ok() return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM) .contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFileName) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFileName)
.body(new InputStreamResource(inputStream)); .body(new InputStreamResource(storedObject.getInputStream()));
} catch (Exception exception) { } catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "读取模板文件失败"); throw new BusinessException(CommonResponseEnum.FAIL, "读取模板文件失败");
} }
@@ -136,21 +128,19 @@ public class ReportModelController extends BaseController {
@GetMapping("/{id}/preview") @GetMapping("/{id}/preview")
public ResponseEntity<byte[]> preview(@PathVariable("id") String id) { public ResponseEntity<byte[]> preview(@PathVariable("id") String id) {
ReportModelPO model = reportModelService.requireNormal(id); ReportModelPO model = reportModelService.requireNormal(id);
Path filePath = reportModelFileStorageService.resolveDownloadPath(model.getPath()); String fileName = resolveFileName(model);
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) { Path tempFile = reportModelFileStorageService.downloadToTempFile(model.getPath(), fileName);
throw new BusinessException(CommonResponseEnum.FAIL, "模板文件不存在"); try {
FilePreviewService.PreviewContent previewContent = filePreviewService.preview(tempFile, 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());
} finally {
reportModelFileStorageService.deleteTempFileQuietly(tempFile);
} }
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) @OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
@@ -170,4 +160,12 @@ public class ReportModelController extends BaseController {
throw new BusinessException(CommonResponseEnum.FAIL, "编码模板文件名失败"); throw new BusinessException(CommonResponseEnum.FAIL, "编码模板文件名失败");
} }
} }
private String resolveFileName(ReportModelPO model) {
String fileName = model.getFileName();
if (fileName == null || fileName.trim().isEmpty()) {
return reportModelFileStorageService.resolveDownloadFileName(model.getPath());
}
return fileName;
}
} }

View File

@@ -107,6 +107,7 @@ public class ReportModelServiceImpl extends ServiceImpl<ReportModelMapper, Repor
} }
String name = resolveName(param.getName()); String name = resolveName(param.getName());
checkNameUnique(name, model.getId()); checkNameUnique(name, model.getId());
String oldStoredPath = model.getPath();
String newStoredPath = null; String newStoredPath = null;
if (templateFile != null && !templateFile.isEmpty()) { if (templateFile != null && !templateFile.isEmpty()) {
ReportModelFileStorageService.StoredFile storedFile = reportModelFileStorageService.save(templateFile); ReportModelFileStorageService.StoredFile storedFile = reportModelFileStorageService.save(templateFile);
@@ -118,7 +119,12 @@ public class ReportModelServiceImpl extends ServiceImpl<ReportModelMapper, Repor
model.setName(name); model.setName(name);
model.setUpdateBy(resolveCurrentUserId()); model.setUpdateBy(resolveCurrentUserId());
model.setUpdateTime(LocalDateTime.now()); model.setUpdateTime(LocalDateTime.now());
return this.updateById(model); boolean updated = this.updateById(model);
if (updated && StrUtil.isNotBlank(newStoredPath) && StrUtil.isNotBlank(oldStoredPath)
&& !StrUtil.equals(oldStoredPath, newStoredPath)) {
reportModelFileStorageService.deleteQuietly(oldStoredPath);
}
return updated;
} catch (RuntimeException exception) { } catch (RuntimeException exception) {
reportModelFileStorageService.deleteQuietly(newStoredPath); reportModelFileStorageService.deleteQuietly(newStoredPath);
throw exception; throw exception;
@@ -135,7 +141,11 @@ public class ReportModelServiceImpl extends ServiceImpl<ReportModelMapper, Repor
if (validIds.isEmpty()) { if (validIds.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "模板ID不能为空"); throw new BusinessException(CommonResponseEnum.FAIL, "模板ID不能为空");
} }
return this.lambdaUpdate() List<ReportModelPO> models = this.lambdaQuery()
.in(ReportModelPO::getId, validIds)
.eq(ReportModelPO::getStatus, ReportModelConst.STATUS_NORMAL)
.list();
boolean deleted = this.lambdaUpdate()
.set(ReportModelPO::getStatus, ReportModelConst.STATUS_DELETED) .set(ReportModelPO::getStatus, ReportModelConst.STATUS_DELETED)
.set(ReportModelPO::getState, ReportModelConst.STATE_DELETED) .set(ReportModelPO::getState, ReportModelConst.STATE_DELETED)
.set(ReportModelPO::getUpdateBy, resolveCurrentUserId()) .set(ReportModelPO::getUpdateBy, resolveCurrentUserId())
@@ -143,6 +153,12 @@ public class ReportModelServiceImpl extends ServiceImpl<ReportModelMapper, Repor
.in(ReportModelPO::getId, validIds) .in(ReportModelPO::getId, validIds)
.eq(ReportModelPO::getStatus, ReportModelConst.STATUS_NORMAL) .eq(ReportModelPO::getStatus, ReportModelConst.STATUS_NORMAL)
.update(); .update();
if (deleted) {
for (ReportModelPO model : models) {
reportModelFileStorageService.deleteQuietly(model.getPath());
}
}
return deleted;
} }
@Override @Override

View File

@@ -1,39 +1,33 @@
package com.njcn.gather.aireport.reportmodel.component; package com.njcn.gather.aireport.reportmodel.component;
import com.njcn.gather.aireport.common.storage.AiReportMinioStorageService;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.mock.web.MockMultipartFile; 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 { public class ReportModelFileStorageServiceTest {
@Test @Test
public void saveSanitizesFileNameAndReturnsRelativePath() throws Exception { public void saveShouldDelegateToMinioStorage() {
Path filesRoot = Files.createTempDirectory("files-root"); AiReportMinioStorageService minioStorageService = Mockito.mock(AiReportMinioStorageService.class);
ReportModelFileStorageService service = new ReportModelFileStorageService(); ReportModelFileStorageService service = new ReportModelFileStorageService(minioStorageService);
ReflectionTestUtils.setField(service, "filesPath", filesRoot.toString()); Mockito.when(minioStorageService.saveMultipartFile(Mockito.anyString(), Mockito.isNull(),
Mockito.any(MockMultipartFile.class), Mockito.eq(false)))
.thenReturn(new AiReportMinioStorageService.StoredFile("cn-tool-ai-report",
"report-model/template-id-a_b.docx", "template-id-a_b.docx", "a_b.docx",
"application/octet-stream", 7L));
ReportModelFileStorageService.StoredFile storedFile = service.save(new MockMultipartFile("templateFile", "../a:b.docx", ReportModelFileStorageService.StoredFile storedFile = service.save(new MockMultipartFile(
"application/octet-stream", "content".getBytes("UTF-8"))); "templateFile", "../a:b.docx", "application/octet-stream", "content".getBytes()));
String storedPath = storedFile.getPath();
Assert.assertFalse(storedPath.contains("..")); Assert.assertEquals("report-model/template-id-a_b.docx", storedFile.getPath());
Assert.assertTrue(storedPath.endsWith("-a_b.docx"));
Assert.assertEquals("a_b.docx", storedFile.getFileName()); 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) @Test(expected = IllegalArgumentException.class)
public void resolveDownloadPathRejectsPathOutsideStorageRoot() throws Exception { public void saveShouldRejectNonDocxFile() {
Path filesRoot = Files.createTempDirectory("files-root"); ReportModelFileStorageService service = new ReportModelFileStorageService(Mockito.mock(AiReportMinioStorageService.class));
ReportModelFileStorageService service = new ReportModelFileStorageService(); service.save(new MockMultipartFile("templateFile", "template.pdf", "application/pdf", new byte[]{1}));
ReflectionTestUtils.setField(service, "filesPath", filesRoot.toString());
service.resolveDownloadPath("../outside.docx");
} }
} }

View File

@@ -14,6 +14,12 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<dependencies> <dependencies>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>ai-report-common</artifactId>
<version>1.0.0</version>
</dependency>
<dependency> <dependency>
<groupId>com.njcn</groupId> <groupId>com.njcn</groupId>
<artifactId>njcn-common</artifactId> <artifactId>njcn-common</artifactId>

View File

@@ -3,98 +3,65 @@ package com.njcn.gather.aireport.testdevice.component;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.njcn.common.pojo.enums.response.CommonResponseEnum; import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException; import com.njcn.common.pojo.exception.BusinessException;
import org.springframework.beans.factory.annotation.Value; import com.njcn.gather.aireport.common.constant.AiReportFileConst;
import com.njcn.gather.aireport.common.constant.AiReportFileExtensionConst;
import com.njcn.gather.aireport.common.storage.AiReportMinioStorageService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.util.Locale;
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 @Component
@RequiredArgsConstructor
public class TestDeviceReportStorageService { 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 long MAX_REPORT_SIZE = 10L * 1024L * 1024L;
private static final int MAX_ORIGINAL_NAME_LENGTH = 80; private static final int MAX_ORIGINAL_NAME_LENGTH = 80;
@Value("${files.path:}") private final AiReportMinioStorageService minioStorageService;
private String filesPath;
public String save(MultipartFile reportFile) { public String save(MultipartFile reportFile) {
if (reportFile == null || reportFile.isEmpty()) { if (reportFile == null || reportFile.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "校准报告不能为空"); throw new BusinessException(CommonResponseEnum.FAIL, "校准报告不能为空");
} }
validatePdf(reportFile.getOriginalFilename(), reportFile.getSize()); validatePdf(reportFile.getOriginalFilename(), reportFile.getSize());
try (InputStream inputStream = reportFile.getInputStream()) { return minioStorageService.saveMultipartFile(
return savePdf(reportFile.getOriginalFilename(), inputStream, reportFile.getSize()); AiReportFileConst.BIZ_DIR_TEST_DEVICE, null, reportFile, false).getObjectName();
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "保存校准报告失败");
}
} }
public String savePdf(String originalFilename, InputStream inputStream, long size) { public String savePdf(String originalFilename, InputStream inputStream, long size) {
validatePdf(originalFilename, size); validatePdf(originalFilename, size);
try { return minioStorageService.saveInputStream(AiReportFileConst.BIZ_DIR_TEST_DEVICE, null,
Path storageRoot = resolveStorageRoot(); trimFileName(originalFilename), inputStream, size, "application/pdf", false).getObjectName();
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) { public AiReportMinioStorageService.StoredObject getObject(String storedPath) {
if (StrUtil.isBlank(storedPath)) { return minioStorageService.getObject(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) { public String resolveDownloadFileName(String storedPath) {
Path fileName = Paths.get(storedPath).getFileName(); if (StrUtil.isBlank(storedPath)) {
if (fileName == null) {
return "校准报告.pdf"; return "校准报告.pdf";
} }
String name = fileName.toString(); int slashIndex = storedPath.lastIndexOf('/');
int separatorIndex = name.indexOf('-'); String fileName = slashIndex >= 0 ? storedPath.substring(slashIndex + 1) : storedPath;
return separatorIndex >= 0 && separatorIndex + 1 < name.length() ? name.substring(separatorIndex + 1) : name; int separatorIndex = fileName.indexOf('-');
return separatorIndex >= 0 && separatorIndex + 1 < fileName.length() ? fileName.substring(separatorIndex + 1) : fileName;
} }
public void deleteQuietly(String storedPath) { public void deleteQuietly(String storedPath) {
try { minioStorageService.deleteQuietly(storedPath);
if (StrUtil.isNotBlank(storedPath)) {
Files.deleteIfExists(resolveDownloadPath(storedPath));
}
} catch (Exception ignored) {
// 文件清理失败不能覆盖原始业务结果。
}
} }
private void validatePdf(String originalFilename, long size) { private void validatePdf(String originalFilename, long size) {
if (StrUtil.isBlank(originalFilename) || !originalFilename.toLowerCase().endsWith(".pdf")) { String fileName = trimFileName(originalFilename);
if (StrUtil.isBlank(fileName)) {
throw new BusinessException(CommonResponseEnum.FAIL, "校准报告文件名不能为空");
}
String extension = resolveExtension(fileName);
if (!AiReportFileExtensionConst.PDF_EXTENSIONS.contains(extension)) {
throw new BusinessException(CommonResponseEnum.FAIL, "校准报告仅支持PDF文件"); throw new BusinessException(CommonResponseEnum.FAIL, "校准报告仅支持PDF文件");
} }
if (size > MAX_REPORT_SIZE) { if (size > MAX_REPORT_SIZE) {
@@ -102,48 +69,27 @@ public class TestDeviceReportStorageService {
} }
} }
private void copyWithSizeLimit(InputStream inputStream, Path target) throws IOException { private String trimFileName(String originalFilename) {
Path tempFile = Files.createTempFile(target.getParent(), "upload-", ".tmp"); String fileName = originalFilename == null ? null : originalFilename.trim();
long bytes = 0L; if (StrUtil.isBlank(fileName)) {
byte[] buffer = new byte[8192]; return fileName;
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);
} }
} int slashIndex = Math.max(fileName.lastIndexOf('/'), fileName.lastIndexOf('\\'));
String normalizedName = slashIndex >= 0 ? fileName.substring(slashIndex + 1) : fileName;
private Path resolveStorageRoot() { String sanitized = normalizedName.replaceAll("[\\\\/:*?\"<>|]", "_");
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) { if (sanitized.length() > MAX_ORIGINAL_NAME_LENGTH) {
String suffix = sanitized.toLowerCase().endsWith(".pdf") ? ".pdf" : ""; String suffix = sanitized.toLowerCase(Locale.ENGLISH).endsWith(".pdf") ? ".pdf" : "";
int keepLength = MAX_ORIGINAL_NAME_LENGTH - suffix.length(); int keepLength = MAX_ORIGINAL_NAME_LENGTH - suffix.length();
sanitized = sanitized.substring(0, keepLength) + suffix; sanitized = sanitized.substring(0, keepLength) + suffix;
} }
return StrUtil.isBlank(sanitized) ? "report.pdf" : sanitized; return sanitized;
}
private String resolveExtension(String fileName) {
int dotIndex = fileName.lastIndexOf('.');
if (dotIndex < 0 || dotIndex == fileName.length() - 1) {
return "";
}
return fileName.substring(dotIndex + 1).toLowerCase(Locale.ENGLISH);
} }
} }

View File

@@ -7,6 +7,7 @@ import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum; import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException; import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.common.pojo.response.HttpResult; import com.njcn.common.pojo.response.HttpResult;
import com.njcn.gather.aireport.common.storage.AiReportMinioStorageService;
import com.njcn.gather.aireport.testdevice.component.TestDeviceReportStorageService; import com.njcn.gather.aireport.testdevice.component.TestDeviceReportStorageService;
import com.njcn.gather.aireport.testdevice.pojo.param.TestDeviceParam; 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.po.TestDevicePO;
@@ -34,10 +35,7 @@ import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List; import java.util.List;
@Api(tags = "检测设备管理") @Api(tags = "检测设备管理")
@@ -145,19 +143,15 @@ public class TestDeviceController extends BaseController {
if (device == null || device.getValidityReport() == null || device.getValidityReport().trim().isEmpty()) { if (device == null || device.getValidityReport() == null || device.getValidityReport().trim().isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "校准报告文件不存在"); throw new BusinessException(CommonResponseEnum.FAIL, "校准报告文件不存在");
} }
Path filePath = reportStorageService.resolveDownloadPath(device.getValidityReport());
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
throw new BusinessException(CommonResponseEnum.FAIL, "校准报告文件不存在");
}
try { try {
String fileName = reportStorageService.resolveDownloadFileName(device.getValidityReport()); String fileName = reportStorageService.resolveDownloadFileName(device.getValidityReport());
String encodedFileName = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20"); String encodedFileName = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20");
InputStream inputStream = Files.newInputStream(filePath); AiReportMinioStorageService.StoredObject storedObject = reportStorageService.getObject(device.getValidityReport());
return ResponseEntity.ok() return ResponseEntity.ok()
.contentType(preview ? MediaType.APPLICATION_PDF : MediaType.APPLICATION_OCTET_STREAM) .contentType(preview ? MediaType.APPLICATION_PDF : MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, .header(HttpHeaders.CONTENT_DISPOSITION,
(preview ? "inline" : "attachment") + "; filename*=UTF-8''" + encodedFileName) (preview ? "inline" : "attachment") + "; filename*=UTF-8''" + encodedFileName)
.body(new InputStreamResource(inputStream)); .body(new InputStreamResource(storedObject.getInputStream()));
} catch (Exception exception) { } catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "读取校准报告失败"); throw new BusinessException(CommonResponseEnum.FAIL, "读取校准报告失败");
} }

View File

@@ -163,13 +163,23 @@ public class TestDeviceServiceImpl extends ServiceImpl<TestDeviceMapper, TestDev
if (validIds.isEmpty()) { if (validIds.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "检测设备ID不能为空"); throw new BusinessException(CommonResponseEnum.FAIL, "检测设备ID不能为空");
} }
return this.lambdaUpdate() List<TestDevicePO> devices = this.lambdaQuery()
.in(TestDevicePO::getId, validIds)
.eq(TestDevicePO::getStatus, TestDeviceConst.STATUS_NORMAL)
.list();
boolean deleted = this.lambdaUpdate()
.set(TestDevicePO::getStatus, TestDeviceConst.STATUS_DELETED) .set(TestDevicePO::getStatus, TestDeviceConst.STATUS_DELETED)
.set(TestDevicePO::getUpdateBy, resolveCurrentUserId()) .set(TestDevicePO::getUpdateBy, resolveCurrentUserId())
.set(TestDevicePO::getUpdateTime, LocalDateTime.now()) .set(TestDevicePO::getUpdateTime, LocalDateTime.now())
.in(TestDevicePO::getId, validIds) .in(TestDevicePO::getId, validIds)
.eq(TestDevicePO::getStatus, TestDeviceConst.STATUS_NORMAL) .eq(TestDevicePO::getStatus, TestDeviceConst.STATUS_NORMAL)
.update(); .update();
if (deleted) {
for (TestDevicePO device : devices) {
reportStorageService.deleteQuietly(device.getValidityReport());
}
}
return deleted;
} }
@Override @Override

View File

@@ -1,28 +1,34 @@
package com.njcn.gather.aireport.testdevice.component; package com.njcn.gather.aireport.testdevice.component;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.common.storage.AiReportMinioStorageService;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.mock.web.MockMultipartFile; 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 { public class TestDeviceReportStorageServiceTest {
@Test @Test
public void saveUsesTestDeviceReportDirectoryUnderFilesRoot() throws Exception { public void saveShouldDelegateToMinioStorage() {
Path filesRoot = Files.createTempDirectory("files-root"); AiReportMinioStorageService minioStorageService = Mockito.mock(AiReportMinioStorageService.class);
TestDeviceReportStorageService service = new TestDeviceReportStorageService(); TestDeviceReportStorageService service = new TestDeviceReportStorageService(minioStorageService);
ReflectionTestUtils.setField(service, "filesPath", filesRoot.toString()); Mockito.when(minioStorageService.saveMultipartFile(Mockito.anyString(), Mockito.isNull(),
Mockito.any(MockMultipartFile.class), Mockito.eq(false)))
.thenReturn(new AiReportMinioStorageService.StoredFile("cn-tool-ai-report",
"test-device/report-id-report.pdf", "report-id-report.pdf", "report.pdf",
"application/pdf", 8L));
String storedPath = service.save(new MockMultipartFile("reportFile", "../report.pdf", String storedPath = service.save(new MockMultipartFile("reportFile", "../report.pdf",
"application/pdf", "%PDF-1.4".getBytes("UTF-8"))); "application/pdf", "%PDF-1.4".getBytes()));
Path expectedRoot = filesRoot.resolve("test-device-report").toAbsolutePath().normalize(); Assert.assertEquals("test-device/report-id-report.pdf", storedPath);
Path actualPath = service.resolveDownloadPath(storedPath); }
Assert.assertTrue(actualPath.startsWith(expectedRoot));
Assert.assertTrue(storedPath.endsWith("-report.pdf")); @Test(expected = BusinessException.class)
Assert.assertTrue(Files.exists(actualPath)); public void saveShouldRejectNonPdfFile() {
TestDeviceReportStorageService service = new TestDeviceReportStorageService(Mockito.mock(AiReportMinioStorageService.class));
service.save(new MockMultipartFile("reportFile", "report.docx",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", new byte[]{1}));
} }
} }

View File

@@ -1,8 +1,10 @@
package com.njcn.gather.aireport.testdevice.controller; package com.njcn.gather.aireport.testdevice.controller;
import com.njcn.gather.aireport.common.storage.AiReportMinioStorageService;
import com.njcn.gather.aireport.testdevice.component.TestDeviceReportStorageService; import com.njcn.gather.aireport.testdevice.component.TestDeviceReportStorageService;
import com.njcn.gather.aireport.testdevice.pojo.po.TestDevicePO; import com.njcn.gather.aireport.testdevice.pojo.po.TestDevicePO;
import com.njcn.gather.aireport.testdevice.service.TestDeviceService; import com.njcn.gather.aireport.testdevice.service.TestDeviceService;
import io.minio.GetObjectResponse;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.Mockito; import org.mockito.Mockito;
@@ -12,10 +14,6 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
class TestDeviceControllerTest { class TestDeviceControllerTest {
private final TestDeviceService testDeviceService = Mockito.mock(TestDeviceService.class); private final TestDeviceService testDeviceService = Mockito.mock(TestDeviceService.class);
@@ -27,11 +25,11 @@ class TestDeviceControllerTest {
TestDevicePO device = new TestDevicePO(); TestDevicePO device = new TestDevicePO();
device.setId("device-1"); device.setId("device-1");
device.setValidityReport("20260626/report.pdf"); device.setValidityReport("20260626/report.pdf");
Path tempFile = Files.createTempFile("test-device-preview-", ".pdf"); AiReportMinioStorageService.StoredObject storedObject = Mockito.mock(AiReportMinioStorageService.StoredObject.class);
Files.write(tempFile, "%PDF-1.4".getBytes(StandardCharsets.UTF_8));
Mockito.when(testDeviceService.requireNormal("device-1")).thenReturn(device); 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"); Mockito.when(reportStorageService.resolveDownloadFileName("20260626/report.pdf")).thenReturn("report.pdf");
Mockito.when(reportStorageService.getObject("20260626/report.pdf")).thenReturn(storedObject);
Mockito.when(storedObject.getInputStream()).thenReturn(Mockito.mock(GetObjectResponse.class));
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
ResponseEntity<InputStreamResource> response = (ResponseEntity<InputStreamResource>) ResponseEntity<InputStreamResource> response = (ResponseEntity<InputStreamResource>)

View File

@@ -14,6 +14,12 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<dependencies> <dependencies>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>ai-report-common</artifactId>
<version>1.0.0</version>
</dependency>
<dependency> <dependency>
<groupId>com.njcn.gather</groupId> <groupId>com.njcn.gather</groupId>
<artifactId>comservice</artifactId> <artifactId>comservice</artifactId>
@@ -26,6 +32,18 @@
<version>1.0.0</version> <version>1.0.0</version>
</dependency> </dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>client-unit</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.njcn.gather</groupId>
<artifactId>test-device</artifactId>
<version>1.0.0</version>
</dependency>
<dependency> <dependency>
<groupId>com.njcn</groupId> <groupId>com.njcn</groupId>
<artifactId>njcn-common</artifactId> <artifactId>njcn-common</artifactId>
@@ -50,6 +68,12 @@
<version>1.0.0</version> <version>1.0.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>

View File

@@ -0,0 +1,382 @@
package com.njcn.gather.aireport.testreport.component;
import cn.hutool.core.util.StrUtil;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst;
import lombok.Data;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Component
public class TestReportLedgerExcelParser {
private static final DataFormatter FORMATTER = new DataFormatter();
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final List<String> REQUIRED_SHEETS = Arrays.asList(
TestReportConst.LEDGER_POINT_SHEET_NAME);
private static final List<String> OPTIONAL_SHEETS = Arrays.asList(
TestReportConst.LEDGER_GUIDE_SHEET_NAME,
TestReportConst.LEDGER_DEVICE_SHEET_NAME,
TestReportConst.LEDGER_CLIENT_SHEET_NAME);
private static final List<String> POINT_HEADERS = Arrays.asList(
"分组号", "变电站", "监测点名称", "检测设备编号", "委托单位名称", "Excel附件名称", "Word附件名称");
private static final List<String> DEVICE_HEADERS = Arrays.asList(
"设备型号", "设备编号", "标准有效期", "设备状态", "标准报告文件名");
private static final List<String> CLIENT_HEADERS = Arrays.asList(
"单位名称", "联系人", "联系方式", "地址");
public ParsedLedger parse(MultipartFile summaryFile, Map<String, MultipartFile> attachments) {
String fileName = summaryFile == null ? null : StrUtil.trimToNull(summaryFile.getOriginalFilename());
Set<String> attachmentNames = attachments == null ? Collections.<String>emptySet() : attachments.keySet();
try (InputStream inputStream = summaryFile.getInputStream()) {
return parse(fileName, inputStream, attachmentNames);
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段读取台账汇总excel失败");
}
}
public ParsedLedger parse(String fileName, InputStream inputStream, Set<String> attachmentNames) {
ParsedLedger ledger = parseForValidate(fileName, inputStream);
ensureRequiredSheetsPresent(ledger);
validateReferencedAttachments(ledger.getPoints(), attachmentNames);
return ledger;
}
public ParsedLedger parseForValidate(String fileName, InputStream inputStream) {
String normalizedFileName = StrUtil.trimToNull(fileName);
if (!TestReportConst.LEDGER_SUMMARY_FILE_NAME.equals(normalizedFileName)) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:台账汇总文件名必须为台账信息汇总.xlsx");
}
try (Workbook workbook = WorkbookFactory.create(inputStream)) {
ParsedLedger ledger = new ParsedLedger();
ledger.setSummaryFileName(TestReportConst.LEDGER_SUMMARY_FILE_NAME);
ledger.getRequiredSheets().addAll(REQUIRED_SHEETS);
collectSheets(ledger, workbook);
Sheet pointSheet = workbook.getSheet(TestReportConst.LEDGER_POINT_SHEET_NAME);
if (pointSheet != null) {
ledger.setPoints(parsePointSheet(pointSheet));
}
// 检测设备/委托单位 sheet 按需填写,仅在实际存在时才做结构校验和解析。
Sheet deviceSheet = workbook.getSheet(TestReportConst.LEDGER_DEVICE_SHEET_NAME);
if (deviceSheet != null) {
ledger.setDevices(parseDeviceSheet(deviceSheet));
}
Sheet clientSheet = workbook.getSheet(TestReportConst.LEDGER_CLIENT_SHEET_NAME);
if (clientSheet != null) {
ledger.setClients(parseClientSheet(clientSheet));
}
return ledger;
} catch (BusinessException exception) {
throw exception;
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段读取台账汇总excel失败");
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段解析台账汇总excel失败");
}
}
private void collectSheets(ParsedLedger ledger, Workbook workbook) {
for (String sheetName : REQUIRED_SHEETS) {
Sheet sheet = workbook.getSheet(sheetName);
if (sheet == null) {
ledger.getMissingSheets().add(sheetName);
continue;
}
ledger.getExistingSheets().add(sheetName);
}
for (String sheetName : OPTIONAL_SHEETS) {
if (workbook.getSheet(sheetName) != null) {
ledger.getExistingSheets().add(sheetName);
}
}
}
private void ensureRequiredSheetsPresent(ParsedLedger ledger) {
if (!ledger.getMissingSheets().isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL,
"校验文件阶段:缺少" + String.join("", ledger.getMissingSheets()) + "sheet");
}
}
private List<ParsedPointRow> parsePointSheet(Sheet sheet) {
validateHeaders(sheet, POINT_HEADERS, "监测点信息");
List<ParsedPointRow> points = new ArrayList<ParsedPointRow>();
Set<String> pointKeys = new HashSet<String>();
Set<String> excelNames = new HashSet<String>();
Set<String> wordNames = new HashSet<String>();
for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
Row row = sheet.getRow(rowIndex);
if (isEmptyRow(row, POINT_HEADERS.size())) {
continue;
}
ParsedPointRow point = new ParsedPointRow();
point.setRowNo(rowIndex + 1);
point.setGroupNo(parsePositiveInteger(optionalCell(row, 0), "" + (rowIndex + 1) + "行分组号必须为正整数"));
point.setSubstationName(requireCell(row, 1, rowIndex, "变电站"));
point.setPointName(requireCell(row, 2, rowIndex, "监测点名称"));
point.setTestDeviceNo(optionalCell(row, 3));
point.setClientUnitName(optionalCell(row, 4));
point.setExcelAttachmentName(requireCell(row, 5, rowIndex, "Excel附件名称"));
point.setWordAttachmentName(requireCell(row, 6, rowIndex, "Word附件名称"));
validateExtension(point.getExcelAttachmentName(), Arrays.asList(".xls", ".xlsx"),
"" + (rowIndex + 1) + "行Excel附件扩展名仅允许xls/xlsx");
validateExtension(point.getWordAttachmentName(), Arrays.asList(".doc", ".docx"),
"" + (rowIndex + 1) + "行Word附件扩展名仅允许doc/docx");
String pointKey = point.getSubstationName() + "|" + point.getPointName();
if (!pointKeys.add(pointKey)) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:监测点信息中存在重复点位");
}
if (!excelNames.add(point.getExcelAttachmentName())) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段监测点信息中存在重复Excel附件名称");
}
if (!wordNames.add(point.getWordAttachmentName())) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段监测点信息中存在重复Word附件名称");
}
points.add(point);
}
if (points.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段监测点信息sheet不能为空");
}
return points;
}
private List<ParsedDeviceRow> parseDeviceSheet(Sheet sheet) {
validateHeaders(sheet, DEVICE_HEADERS, "检测设备");
List<ParsedDeviceRow> devices = new ArrayList<ParsedDeviceRow>();
Set<String> deviceNos = new HashSet<String>();
for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
Row row = sheet.getRow(rowIndex);
if (isEmptyRow(row, DEVICE_HEADERS.size())) {
continue;
}
ParsedDeviceRow device = new ParsedDeviceRow();
device.setRowNo(rowIndex + 1);
device.setTypeName(optionalCell(row, 0));
device.setNo(optionalCell(row, 1));
device.setValidityPeriod(parseOptionalDate(optionalCell(row, 2), rowIndex));
device.setStateName(optionalCell(row, 3));
device.setValidityReportName(optionalCell(row, 4));
if (StrUtil.isBlank(device.getNo())) {
continue;
}
if (!deviceNos.add(device.getNo())) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段检测设备sheet中存在重复设备编号");
}
devices.add(device);
}
return devices;
}
private List<ParsedClientRow> parseClientSheet(Sheet sheet) {
validateHeaders(sheet, CLIENT_HEADERS, "委托单位");
List<ParsedClientRow> clients = new ArrayList<ParsedClientRow>();
Set<String> clientNames = new HashSet<String>();
for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
Row row = sheet.getRow(rowIndex);
if (isEmptyRow(row, CLIENT_HEADERS.size())) {
continue;
}
ParsedClientRow client = new ParsedClientRow();
client.setRowNo(rowIndex + 1);
client.setName(optionalCell(row, 0));
client.setContactName(optionalCell(row, 1));
client.setPhonenumber(optionalCell(row, 2));
client.setAddress(optionalCell(row, 3));
if (StrUtil.isBlank(client.getName())) {
continue;
}
if (!clientNames.add(client.getName())) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段委托单位sheet中存在重复单位名称");
}
clients.add(client);
}
return clients;
}
private void validateReferencedAttachments(List<ParsedPointRow> points, Set<String> attachmentNames) {
Set<String> referencedNames = new HashSet<String>();
for (ParsedPointRow point : points) {
if (StrUtil.isNotBlank(point.getExcelAttachmentName())) {
referencedNames.add(point.getExcelAttachmentName());
}
if (StrUtil.isNotBlank(point.getWordAttachmentName())) {
referencedNames.add(point.getWordAttachmentName());
}
}
for (ParsedPointRow point : points) {
if (!attachmentNames.contains(point.getExcelAttachmentName())) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:第" + point.getRowNo() + "行缺少Excel附件");
}
if (!attachmentNames.contains(point.getWordAttachmentName())) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:第" + point.getRowNo() + "行缺少Word附件");
}
}
for (String attachmentName : attachmentNames) {
if (!referencedNames.contains(attachmentName)) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段存在未在监测点信息sheet中引用的附件 " + attachmentName);
}
}
}
private void validateHeaders(Sheet sheet, List<String> expectedHeaders, String sheetName) {
Row headerRow = sheet.getRow(0);
if (headerRow == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:" + sheetName + "sheet缺少表头");
}
for (int i = 0; i < expectedHeaders.size(); i++) {
String actualHeader = optionalCell(headerRow, i);
if (!expectedHeaders.get(i).equals(actualHeader)) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:" + sheetName + "sheet表头不符合模板要求");
}
}
}
private boolean isEmptyRow(Row row, int cellCount) {
if (row == null) {
return true;
}
for (int i = 0; i < cellCount; i++) {
if (StrUtil.isNotBlank(optionalCell(row, i))) {
return false;
}
}
return true;
}
private String requireCell(Row row, int cellIndex, int rowIndex, String fieldName) {
String value = optionalCell(row, cellIndex);
if (StrUtil.isBlank(value)) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:第" + (rowIndex + 1) + "行缺少" + fieldName);
}
return value;
}
private String optionalCell(Row row, int cellIndex) {
if (row == null) {
return null;
}
Cell cell = row.getCell(cellIndex);
return cell == null ? null : StrUtil.trimToNull(FORMATTER.formatCellValue(cell));
}
private Integer parsePositiveInteger(String value, String message) {
if (StrUtil.isBlank(value)) {
return null;
}
try {
int parsed = Integer.parseInt(value);
if (parsed <= 0) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:" + message);
}
return parsed;
} catch (NumberFormatException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:" + message);
}
}
private void validateExtension(String fileName, List<String> validExtensions, String message) {
String lowerFileName = fileName.toLowerCase();
for (String validExtension : validExtensions) {
if (lowerFileName.endsWith(validExtension)) {
return;
}
}
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:" + message);
}
private LocalDate parseDate(String value, int rowIndex) {
try {
return LocalDate.parse(value, DATE_FORMATTER);
} catch (DateTimeParseException exception) {
throw new BusinessException(CommonResponseEnum.FAIL,
"校验文件阶段检测设备sheet第" + (rowIndex + 1) + "行标准有效期格式必须为yyyy-MM-dd");
}
}
private LocalDate parseOptionalDate(String value, int rowIndex) {
if (StrUtil.isBlank(value)) {
return null;
}
return parseDate(value, rowIndex);
}
@Data
public static class ParsedLedger {
private String summaryFileName;
private List<String> requiredSheets = new ArrayList<String>();
private List<String> existingSheets = new ArrayList<String>();
private List<String> missingSheets = new ArrayList<String>();
private List<ParsedPointRow> points = new ArrayList<ParsedPointRow>();
private List<ParsedDeviceRow> devices = new ArrayList<ParsedDeviceRow>();
private List<ParsedClientRow> clients = new ArrayList<ParsedClientRow>();
public Map<String, ParsedDeviceRow> toDeviceMap() {
Map<String, ParsedDeviceRow> result = new HashMap<String, ParsedDeviceRow>();
for (ParsedDeviceRow device : devices) {
result.put(device.getNo(), device);
}
return result;
}
public Map<String, ParsedClientRow> toClientMap() {
Map<String, ParsedClientRow> result = new HashMap<String, ParsedClientRow>();
for (ParsedClientRow client : clients) {
result.put(client.getName(), client);
}
return result;
}
}
@Data
public static class ParsedPointRow {
private Integer rowNo;
private Integer groupNo;
private String substationName;
private String pointName;
private String testDeviceNo;
private String clientUnitName;
private String excelAttachmentName;
private String wordAttachmentName;
}
@Data
public static class ParsedDeviceRow {
private Integer rowNo;
private String typeName;
private String no;
private LocalDate validityPeriod;
private String stateName;
private String validityReportName;
}
@Data
public static class ParsedClientRow {
private Integer rowNo;
private String name;
private String contactName;
private String phonenumber;
private String address;
}
}

View File

@@ -0,0 +1,876 @@
package com.njcn.gather.aireport.testreport.component;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.clientunit.mapper.ClientUnitMapper;
import com.njcn.gather.aireport.clientunit.pojo.constant.ClientUnitConst;
import com.njcn.gather.aireport.clientunit.pojo.po.ClientUnitPO;
import com.njcn.gather.aireport.common.storage.AiReportMinioStorageService;
import com.njcn.gather.aireport.testdevice.mapper.TestDeviceMapper;
import com.njcn.gather.aireport.testdevice.pojo.constant.TestDeviceConst;
import com.njcn.gather.aireport.testdevice.pojo.po.TestDevicePO;
import com.njcn.gather.aireport.testdevice.pojo.vo.TestDeviceTypeNameVO;
import com.njcn.gather.aireport.testreport.mapper.TestReportGroupReportMapper;
import com.njcn.gather.aireport.testreport.mapper.TestReportMapper;
import com.njcn.gather.aireport.testreport.mapper.TestReportPointMapper;
import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst;
import com.njcn.gather.aireport.testreport.pojo.po.TestReportGroupReportPO;
import com.njcn.gather.aireport.testreport.pojo.po.TestReportPO;
import com.njcn.gather.aireport.testreport.pojo.po.TestReportPointPO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportLedgerImportResultVO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportLedgerImportStageVO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportLedgerValidateAttachmentVO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportLedgerValidatePointVO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportLedgerValidateResultVO;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@Component
public class TestReportLedgerImportService {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final String BATCH_STATUS_VALIDATED = "validated";
private final TestReportMapper testReportMapper;
private final TestReportPointMapper testReportPointMapper;
private final TestReportGroupReportMapper testReportGroupReportMapper;
private final TestDeviceMapper testDeviceMapper;
private final ClientUnitMapper clientUnitMapper;
private final TestReportLedgerExcelParser testReportLedgerExcelParser;
private final TestReportStorageService testReportStorageService;
public TestReportLedgerImportService(TestReportMapper testReportMapper,
TestReportPointMapper testReportPointMapper,
TestReportGroupReportMapper testReportGroupReportMapper,
TestDeviceMapper testDeviceMapper,
ClientUnitMapper clientUnitMapper,
TestReportLedgerExcelParser testReportLedgerExcelParser,
TestReportStorageService testReportStorageService) {
this.testReportMapper = testReportMapper;
this.testReportPointMapper = testReportPointMapper;
this.testReportGroupReportMapper = testReportGroupReportMapper;
this.testDeviceMapper = testDeviceMapper;
this.clientUnitMapper = clientUnitMapper;
this.testReportLedgerExcelParser = testReportLedgerExcelParser;
this.testReportStorageService = testReportStorageService;
}
public TestReportLedgerValidateResultVO validateLedgerSession(String sessionId, MultipartFile[] files) {
Map<String, MultipartFile> fileMap = normalizeFileMap(files);
TestReportStorageService.UploadedBatch uploadedBatch = testReportStorageService.saveValidateSessionFiles(sessionId, fileMap);
List<String> sessionFileNames = testReportStorageService.listValidateSessionFileNames(sessionId);
Set<String> sessionFileNameSet = new HashSet<String>(sessionFileNames);
TestReportLedgerValidateResultVO result = new TestReportLedgerValidateResultVO();
result.setSessionId(sessionId);
result.setTempStoragePath(uploadedBatch.getTempStoragePath());
result.setUploadedFileCount(sessionFileNames.size());
result.setCanContinueUpload(Boolean.TRUE);
result.setSummaryFileName(TestReportConst.LEDGER_SUMMARY_FILE_NAME);
result.setSummaryFilePresent(sessionFileNameSet.contains(TestReportConst.LEDGER_SUMMARY_FILE_NAME));
if (!Boolean.TRUE.equals(result.getSummaryFilePresent())) {
result.setSuccess(Boolean.FALSE);
result.getFailReasons().add("鏍¢獙鏂囦欢闃舵锛氱己灏戝彴璐︿俊鎭眹鎬?xlsx");
persistValidateSessionResult(sessionId, result);
return result;
}
try (AiReportMinioStorageService.StoredObject storedObject =
testReportStorageService.getValidateSessionObject(sessionId, TestReportConst.LEDGER_SUMMARY_FILE_NAME);
InputStream inputStream = storedObject.getInputStream()) {
TestReportLedgerExcelParser.ParsedLedger ledger = testReportLedgerExcelParser.parseForValidate(
TestReportConst.LEDGER_SUMMARY_FILE_NAME, inputStream);
Map<String, TestDevicePO> dbDeviceMap = loadExistingDevices(collectDeviceNos(ledger));
Map<String, ClientUnitPO> dbClientMap = loadExistingClients(collectClientNames(ledger));
result.getRequiredSheets().addAll(ledger.getRequiredSheets());
result.getExistingSheets().addAll(ledger.getExistingSheets());
result.getMissingSheets().addAll(ledger.getMissingSheets());
fillPointValidationResult(result, ledger, sessionFileNameSet, dbDeviceMap, dbClientMap);
result.getOrphanAttachmentNames().addAll(resolveOrphanAttachmentNames(ledger, sessionFileNameSet));
result.setSuccess(result.getMissingSheets().isEmpty() && result.getMissingAttachmentCount() != null
&& result.getMissingAttachmentCount() == 0 && !hasReferenceErrors(result.getPoints()));
if (!result.getMissingSheets().isEmpty()) {
result.getFailReasons().add("鏍¢獙鏂囦欢闃舵锛歋heet涓嶄笉榻? " + String.join("銆?", result.getMissingSheets()));
}
if (result.getMissingAttachmentCount() != null && result.getMissingAttachmentCount() > 0) {
result.getFailReasons().add("鏍¢獙鏂囦欢闃舵锛氬瓨鍦ㄧ己澶遍檮浠讹紝鍙户缁笂浼犲悗鍐嶆鏍¢獙");
}
appendReferenceFailReasons(result);
persistValidateSessionResult(sessionId, result);
return result;
} catch (BusinessException exception) {
throw exception;
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "鏍¢獙鏂囦欢闃舵锛氳鍙栧彴璐︽眹鎬籩xcel澶辫触");
}
}
public TestReportLedgerImportResultVO validateLedger(TestReportPO report, MultipartFile[] files, String userId) {
Map<String, MultipartFile> fileMap = normalizeFileMap(files);
String batchId = UUID.randomUUID().toString();
TestReportStorageService.UploadedBatch uploadedBatch = testReportStorageService.saveTempBatchFiles(report.getId(), batchId, fileMap);
try {
MultipartFile summaryFile = fileMap.get(TestReportConst.LEDGER_SUMMARY_FILE_NAME);
if (summaryFile == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:缺少台账信息汇总.xlsx");
}
TestReportLedgerExcelParser.ParsedLedger ledger = parseLedgerFromBatch(report.getId(), batchId, fileMap.keySet());
Map<String, TestDevicePO> dbDeviceMap = loadExistingDevices(collectDeviceNos(ledger));
Map<String, ClientUnitPO> dbClientMap = loadExistingClients(collectClientNames(ledger));
validatePointReferences(ledger.getPoints(), dbDeviceMap, dbClientMap, ledger.toDeviceMap(), ledger.toClientMap());
writeValidatedBatchMeta(report.getId(), batchId, uploadedBatch, ledger, userId);
return buildValidateSuccessResult(batchId, uploadedBatch.getTempStoragePath(), ledger, fileMap.size());
} catch (RuntimeException exception) {
testReportStorageService.deleteTempBatchQuietly(report.getId(), batchId);
throw exception;
}
}
public TestReportLedgerImportResultVO importLedger(TestReportPO report, String batchId, String userId) {
BatchMeta batchMeta = loadValidatedBatchMeta(report.getId(), batchId);
TestReportLedgerExcelParser.ParsedLedger ledger = parseLedgerFromBatch(report.getId(), batchId, batchMeta.toFileNameSet());
Map<String, TestDevicePO> dbDeviceMap = loadExistingDevices(collectDeviceNos(ledger));
Map<String, ClientUnitPO> dbClientMap = loadExistingClients(collectClientNames(ledger));
BaseDataSummary baseDataSummary = maintainBaseData(ledger, dbDeviceMap, dbClientMap, userId);
List<TestReportPointPO> oldPoints = loadNormalPoints(report.getId());
List<TestReportGroupReportPO> oldGroupReports = loadNormalGroupReports(report.getId());
deleteOldFiles(oldPoints, oldGroupReports, report.getData());
removeOldRows(report.getId());
String summaryStoragePath = testReportStorageService.saveLedgerSummaryFromTemp(report.getId(), batchId,
batchMeta.getSummaryFileName());
int groupCount = saveNewPoints(report.getId(), batchId, ledger.getPoints(), userId);
LocalDateTime now = LocalDateTime.now();
report.setData(buildSnapshotJson(ledger, summaryStoragePath, baseDataSummary, groupCount));
report.setReportGenerateState(TestReportConst.REPORT_GENERATE_STATE_PENDING);
report.setUpdateBy(userId);
report.setUpdateTime(now);
testReportMapper.updateById(report);
testReportStorageService.deleteTempBatchQuietly(report.getId(), batchId);
return buildImportSuccessResult(batchId, batchMeta.getTempStoragePath(), ledger, baseDataSummary,
batchMeta.getUploadedFileCount(), groupCount);
}
private TestReportLedgerExcelParser.ParsedLedger parseLedgerFromBatch(String reportId, String batchId, Collection<String> fileNames) {
try (AiReportMinioStorageService.StoredObject storedObject =
testReportStorageService.getTempBatchObject(reportId, batchId, TestReportConst.LEDGER_SUMMARY_FILE_NAME);
InputStream inputStream = storedObject.getInputStream()) {
return testReportLedgerExcelParser.parse(TestReportConst.LEDGER_SUMMARY_FILE_NAME, inputStream,
buildAttachmentNameSet(fileNames));
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段读取台账汇总excel失败");
}
}
private Set<String> buildAttachmentNameSet(Collection<String> fileNames) {
Set<String> attachmentNames = new HashSet<String>();
if (fileNames == null || fileNames.isEmpty()) {
return attachmentNames;
}
for (String fileName : fileNames) {
if (StrUtil.isBlank(fileName) || TestReportConst.LEDGER_SUMMARY_FILE_NAME.equals(fileName)) {
continue;
}
attachmentNames.add(fileName);
}
return attachmentNames;
}
private void fillPointValidationResult(TestReportLedgerValidateResultVO result,
TestReportLedgerExcelParser.ParsedLedger ledger,
Set<String> sessionFileNameSet,
Map<String, TestDevicePO> dbDeviceMap,
Map<String, ClientUnitPO> dbClientMap) {
int missingAttachmentCount = 0;
Map<String, TestReportLedgerExcelParser.ParsedDeviceRow> sheetDeviceMap = ledger.toDeviceMap();
Map<String, TestReportLedgerExcelParser.ParsedClientRow> sheetClientMap = ledger.toClientMap();
for (TestReportLedgerExcelParser.ParsedPointRow pointRow : ledger.getPoints()) {
TestReportLedgerValidatePointVO pointVO = new TestReportLedgerValidatePointVO();
pointVO.setRowNo(pointRow.getRowNo());
pointVO.setSubstationName(pointRow.getSubstationName());
pointVO.setPointName(pointRow.getPointName());
pointVO.setPointKey(pointRow.getSubstationName() + "|" + pointRow.getPointName());
TestReportLedgerValidateAttachmentVO excelAttachment = buildAttachmentRow(
pointRow.getExcelAttachmentName(), "excel", sessionFileNameSet.contains(pointRow.getExcelAttachmentName()));
TestReportLedgerValidateAttachmentVO wordAttachment = buildAttachmentRow(
pointRow.getWordAttachmentName(), "word", sessionFileNameSet.contains(pointRow.getWordAttachmentName()));
pointVO.getAttachments().add(excelAttachment);
pointVO.getAttachments().add(wordAttachment);
int pointMissingCount = 0;
if (Boolean.TRUE.equals(excelAttachment.getMissing())) {
pointMissingCount++;
}
if (Boolean.TRUE.equals(wordAttachment.getMissing())) {
pointMissingCount++;
}
pointVO.setMissingAttachmentCount(pointMissingCount);
fillReferenceValidationResult(pointVO, pointRow, dbDeviceMap, dbClientMap, sheetDeviceMap, sheetClientMap);
pointVO.setComplete(pointMissingCount == 0
&& !Boolean.FALSE.equals(pointVO.getTestDeviceNoValid())
&& !Boolean.FALSE.equals(pointVO.getClientUnitNameValid()));
missingAttachmentCount += pointMissingCount;
result.getPoints().add(pointVO);
}
result.setPointCount(result.getPoints().size());
result.setMissingAttachmentCount(missingAttachmentCount);
}
private void fillReferenceValidationResult(TestReportLedgerValidatePointVO pointVO,
TestReportLedgerExcelParser.ParsedPointRow pointRow,
Map<String, TestDevicePO> dbDeviceMap,
Map<String, ClientUnitPO> dbClientMap,
Map<String, TestReportLedgerExcelParser.ParsedDeviceRow> sheetDeviceMap,
Map<String, TestReportLedgerExcelParser.ParsedClientRow> sheetClientMap) {
if (StrUtil.isBlank(pointRow.getTestDeviceNo())) {
pointVO.setTestDeviceNoValid(Boolean.TRUE);
pointVO.setTestDeviceNoMessage("检测设备编号为空,跳过校验");
} else if (dbDeviceMap.containsKey(pointRow.getTestDeviceNo()) || sheetDeviceMap.containsKey(pointRow.getTestDeviceNo())) {
pointVO.setTestDeviceNoValid(Boolean.TRUE);
pointVO.setTestDeviceNoMessage("检测设备编号校验通过");
} else {
pointVO.setTestDeviceNoValid(Boolean.FALSE);
pointVO.setTestDeviceNoMessage("" + pointRow.getRowNo() + "行检测设备编号在数据库和检测设备sheet中都不存在");
}
if (StrUtil.isBlank(pointRow.getClientUnitName())) {
pointVO.setClientUnitNameValid(Boolean.TRUE);
pointVO.setClientUnitNameMessage("委托单位名称为空,跳过校验");
} else if (dbClientMap.containsKey(pointRow.getClientUnitName()) || sheetClientMap.containsKey(pointRow.getClientUnitName())) {
pointVO.setClientUnitNameValid(Boolean.TRUE);
pointVO.setClientUnitNameMessage("委托单位名称校验通过");
} else {
pointVO.setClientUnitNameValid(Boolean.FALSE);
pointVO.setClientUnitNameMessage("" + pointRow.getRowNo() + "行委托单位名称在数据库和委托单位sheet中都不存在");
}
}
private TestReportLedgerValidateAttachmentVO buildAttachmentRow(String attachmentName, String attachmentType,
boolean uploaded) {
TestReportLedgerValidateAttachmentVO attachmentVO = new TestReportLedgerValidateAttachmentVO();
attachmentVO.setAttachmentName(attachmentName);
attachmentVO.setAttachmentType(attachmentType);
attachmentVO.setReferenced(Boolean.TRUE);
attachmentVO.setUploaded(uploaded);
attachmentVO.setMissing(!uploaded);
attachmentVO.setReuploadAllowed(Boolean.TRUE);
attachmentVO.setMessage(uploaded ? "宸蹭笂浼?" : "缂哄皯闄勪欢锛屽彲缁х画涓婁紶");
return attachmentVO;
}
private List<String> resolveOrphanAttachmentNames(TestReportLedgerExcelParser.ParsedLedger ledger, Set<String> sessionFileNameSet) {
Set<String> referencedNames = new HashSet<String>();
for (TestReportLedgerExcelParser.ParsedPointRow pointRow : ledger.getPoints()) {
if (StrUtil.isNotBlank(pointRow.getExcelAttachmentName())) {
referencedNames.add(pointRow.getExcelAttachmentName());
}
if (StrUtil.isNotBlank(pointRow.getWordAttachmentName())) {
referencedNames.add(pointRow.getWordAttachmentName());
}
}
List<String> orphanAttachmentNames = new java.util.ArrayList<String>();
for (String fileName : sessionFileNameSet) {
if (TestReportConst.LEDGER_SUMMARY_FILE_NAME.equals(fileName)) {
continue;
}
if (!referencedNames.contains(fileName)) {
orphanAttachmentNames.add(fileName);
}
}
return orphanAttachmentNames;
}
private void persistValidateSessionResult(String sessionId, TestReportLedgerValidateResultVO result) {
try {
testReportStorageService.writeValidateSessionResult(sessionId, OBJECT_MAPPER.writeValueAsString(result));
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "鏍¢獙鏂囦欢闃舵锛氫繚瀛樻牎楠岀粨鏋滃け璐?");
}
}
private boolean hasReferenceErrors(List<TestReportLedgerValidatePointVO> points) {
if (points == null || points.isEmpty()) {
return false;
}
for (TestReportLedgerValidatePointVO point : points) {
if (point == null) {
continue;
}
if (Boolean.FALSE.equals(point.getTestDeviceNoValid()) || Boolean.FALSE.equals(point.getClientUnitNameValid())) {
return true;
}
}
return false;
}
private void appendReferenceFailReasons(TestReportLedgerValidateResultVO result) {
if (result == null || result.getPoints() == null || result.getPoints().isEmpty()) {
return;
}
boolean hasDeviceError = false;
boolean hasClientError = false;
for (TestReportLedgerValidatePointVO point : result.getPoints()) {
if (point == null) {
continue;
}
if (Boolean.FALSE.equals(point.getTestDeviceNoValid())) {
hasDeviceError = true;
}
if (Boolean.FALSE.equals(point.getClientUnitNameValid())) {
hasClientError = true;
}
}
if (hasDeviceError) {
result.getFailReasons().add("监测点信息中存在检测设备编号未匹配到数据库或检测设备sheet的记录");
}
if (hasClientError) {
result.getFailReasons().add("监测点信息中存在委托单位名称未匹配到数据库或委托单位sheet的记录");
}
}
private void writeValidatedBatchMeta(String reportId, String batchId, TestReportStorageService.UploadedBatch uploadedBatch,
TestReportLedgerExcelParser.ParsedLedger ledger, String userId) {
BatchMeta batchMeta = new BatchMeta();
batchMeta.setBatchId(batchId);
batchMeta.setReportId(reportId);
batchMeta.setStatus(BATCH_STATUS_VALIDATED);
batchMeta.setSummaryFileName(ledger.getSummaryFileName());
batchMeta.setTempStoragePath(uploadedBatch.getTempStoragePath());
batchMeta.setUploadedFileCount(uploadedBatch.getStoredFileMap().size());
batchMeta.setUploadedFileNames(uploadedBatch.getStoredFileMap().keySet().stream().collect(Collectors.toList()));
batchMeta.setValidatedBy(userId);
batchMeta.setValidatedTime(LocalDateTime.now().toString());
try {
testReportStorageService.writeTempBatchMeta(reportId, batchId, OBJECT_MAPPER.writeValueAsString(batchMeta));
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:保存批次元数据失败");
}
}
private BatchMeta loadValidatedBatchMeta(String reportId, String batchId) {
if (StrUtil.isBlank(batchId)) {
throw new BusinessException(CommonResponseEnum.FAIL, "导入批次ID不能为空");
}
String metaText = testReportStorageService.readTempBatchMeta(reportId, batchId);
if (StrUtil.isBlank(metaText)) {
throw new BusinessException(CommonResponseEnum.FAIL, "导入批次不存在或未通过校验");
}
try {
BatchMeta batchMeta = OBJECT_MAPPER.readValue(metaText, BatchMeta.class);
if (!reportId.equals(batchMeta.getReportId()) || !BATCH_STATUS_VALIDATED.equals(batchMeta.getStatus())) {
throw new BusinessException(CommonResponseEnum.FAIL, "导入批次不存在或未通过校验");
}
return batchMeta;
} catch (BusinessException exception) {
throw exception;
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "读取导入批次信息失败");
}
}
private Map<String, MultipartFile> normalizeFileMap(MultipartFile[] files) {
if (files == null || files.length == 0) {
throw new BusinessException(CommonResponseEnum.FAIL, "选择文件阶段:导入文件不能为空");
}
Map<String, MultipartFile> fileMap = new LinkedHashMap<String, MultipartFile>();
for (MultipartFile file : files) {
if (file == null || file.isEmpty()) {
continue;
}
String fileName = normalizeFileName(file.getOriginalFilename());
if (fileName == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "选择文件阶段:文件名不能为空");
}
if (fileMap.containsKey(fileName)) {
throw new BusinessException(CommonResponseEnum.FAIL, "选择文件阶段:存在重复文件名 " + fileName);
}
fileMap.put(fileName, file);
}
if (fileMap.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "选择文件阶段:导入文件不能为空");
}
return fileMap;
}
private void validatePointReferences(List<TestReportLedgerExcelParser.ParsedPointRow> points,
Map<String, TestDevicePO> dbDeviceMap,
Map<String, ClientUnitPO> dbClientMap,
Map<String, TestReportLedgerExcelParser.ParsedDeviceRow> sheetDeviceMap,
Map<String, TestReportLedgerExcelParser.ParsedClientRow> sheetClientMap) {
for (TestReportLedgerExcelParser.ParsedPointRow point : points) {
if (StrUtil.isNotBlank(point.getTestDeviceNo())
&& !dbDeviceMap.containsKey(point.getTestDeviceNo())
&& !sheetDeviceMap.containsKey(point.getTestDeviceNo())) {
throw new BusinessException(CommonResponseEnum.FAIL,
"校验文件阶段:第" + point.getRowNo() + "行检测设备编号在数据库和检测设备sheet中都不存在");
}
if (StrUtil.isNotBlank(point.getClientUnitName())
&& !dbClientMap.containsKey(point.getClientUnitName())
&& !sheetClientMap.containsKey(point.getClientUnitName())) {
throw new BusinessException(CommonResponseEnum.FAIL,
"校验文件阶段:第" + point.getRowNo() + "行委托单位名称在数据库和委托单位sheet中都不存在");
}
}
}
private BaseDataSummary maintainBaseData(TestReportLedgerExcelParser.ParsedLedger ledger,
Map<String, TestDevicePO> dbDeviceMap,
Map<String, ClientUnitPO> dbClientMap,
String userId) {
BaseDataSummary summary = new BaseDataSummary();
Map<String, String> typeNameIdMap = loadDeviceTypeNameIdMap();
LocalDateTime now = LocalDateTime.now();
for (TestReportLedgerExcelParser.ParsedDeviceRow deviceRow : ledger.getDevices()) {
TestDevicePO existingDevice = dbDeviceMap.get(deviceRow.getNo());
if (existingDevice != null) {
summary.setDeviceReuseCount(summary.getDeviceReuseCount() + 1);
continue;
}
String typeName = normalizeFileName(deviceRow.getTypeName());
String typeId = StrUtil.isBlank(typeName) ? null : typeNameIdMap.get(typeName);
if (StrUtil.isNotBlank(typeName) && StrUtil.isBlank(typeId)) {
throw new BusinessException(CommonResponseEnum.FAIL,
"基础信息维护阶段检测设备sheet第" + deviceRow.getRowNo() + "行设备型号不存在");
}
TestDevicePO device = new TestDevicePO();
device.setId(UUID.randomUUID().toString());
device.setType(typeId);
device.setNo(deviceRow.getNo());
device.setValidityPeriod(deviceRow.getValidityPeriod());
device.setValidityReport(deviceRow.getValidityReportName());
device.setState(resolveDeviceState(deviceRow.getStateName(), deviceRow.getRowNo()));
device.setStatus(TestDeviceConst.STATUS_NORMAL);
device.setCreateBy(userId);
device.setCreateTime(now);
device.setUpdateBy(userId);
device.setUpdateTime(now);
testDeviceMapper.insert(device);
dbDeviceMap.put(device.getNo(), device);
summary.setDeviceAddedCount(summary.getDeviceAddedCount() + 1);
}
for (TestReportLedgerExcelParser.ParsedClientRow clientRow : ledger.getClients()) {
ClientUnitPO existingClient = dbClientMap.get(clientRow.getName());
if (existingClient != null) {
summary.setClientReuseCount(summary.getClientReuseCount() + 1);
continue;
}
ClientUnitPO client = new ClientUnitPO();
client.setId(UUID.randomUUID().toString());
client.setName(clientRow.getName());
client.setContactName(defaultEmpty(clientRow.getContactName()));
client.setPhonenumber(defaultEmpty(clientRow.getPhonenumber()));
client.setAddress(clientRow.getAddress());
client.setStatus(ClientUnitConst.STATUS_NORMAL);
client.setCreateBy(userId);
client.setCreateTime(now);
client.setUpdateBy(userId);
client.setUpdateTime(now);
clientUnitMapper.insert(client);
dbClientMap.put(client.getName(), client);
summary.setClientAddedCount(summary.getClientAddedCount() + 1);
}
return summary;
}
private int saveNewPoints(String reportId, String batchId, List<TestReportLedgerExcelParser.ParsedPointRow> points,
String userId) {
LocalDateTime now = LocalDateTime.now();
Set<Integer> groupNos = new HashSet<Integer>();
int sortNo = 1;
for (TestReportLedgerExcelParser.ParsedPointRow pointRow : points) {
TestReportPointPO point = new TestReportPointPO();
point.setId(UUID.randomUUID().toString());
point.setReportId(reportId);
point.setGroupNo(pointRow.getGroupNo());
point.setSubstationName(pointRow.getSubstationName());
point.setPointName(pointRow.getPointName());
point.setExcelAttachmentName(pointRow.getExcelAttachmentName());
point.setExcelStoragePath(testReportStorageService.savePointExcelFromTemp(reportId, batchId,
pointRow.getExcelAttachmentName()));
point.setSortNo(sortNo++);
point.setDeleted(TestReportConst.DELETED_NO);
point.setCreateBy(userId);
point.setCreateTime(now);
point.setUpdateBy(userId);
point.setUpdateTime(now);
testReportPointMapper.insert(point);
if (pointRow.getGroupNo() != null) {
groupNos.add(pointRow.getGroupNo());
}
}
return groupNos.size();
}
private void deleteOldFiles(List<TestReportPointPO> oldPoints, List<TestReportGroupReportPO> oldGroupReports, String data) {
for (TestReportPointPO point : oldPoints) {
testReportStorageService.deleteQuietly(point.getExcelStoragePath());
}
for (TestReportGroupReportPO groupReport : oldGroupReports) {
testReportStorageService.deleteQuietly(groupReport.getReportStoragePath());
}
testReportStorageService.deleteQuietly(resolveSummaryStoragePath(data));
}
private void removeOldRows(String reportId) {
LambdaQueryWrapper<TestReportPointPO> pointWrapper = new LambdaQueryWrapper<TestReportPointPO>();
pointWrapper.eq(TestReportPointPO::getReportId, reportId);
testReportPointMapper.delete(pointWrapper);
LambdaQueryWrapper<TestReportGroupReportPO> groupReportWrapper = new LambdaQueryWrapper<TestReportGroupReportPO>();
groupReportWrapper.eq(TestReportGroupReportPO::getReportId, reportId);
testReportGroupReportMapper.delete(groupReportWrapper);
}
private String buildSnapshotJson(TestReportLedgerExcelParser.ParsedLedger ledger, String summaryStoragePath,
BaseDataSummary summary, int groupCount) {
ObjectNode root = OBJECT_MAPPER.createObjectNode();
root.put("version", "2.0");
root.put("importMode", "summary-excel-with-base-data");
root.put("summaryFileName", ledger.getSummaryFileName());
root.put("summaryStoragePath", summaryStoragePath);
ObjectNode sheetSummary = root.putObject("sheetSummary");
sheetSummary.put("pointSheet", TestReportConst.LEDGER_POINT_SHEET_NAME);
sheetSummary.put("deviceSheetPresent", ledger.getExistingSheets().contains(TestReportConst.LEDGER_DEVICE_SHEET_NAME));
sheetSummary.put("clientSheetPresent", ledger.getExistingSheets().contains(TestReportConst.LEDGER_CLIENT_SHEET_NAME));
root.put("rowCount", ledger.getPoints().size());
root.put("pointCount", ledger.getPoints().size());
root.put("groupCount", groupCount);
ArrayNode groupSummary = root.putArray("groupSummary");
Map<Integer, Long> groupCounter = ledger.getPoints().stream()
.collect(Collectors.groupingBy(TestReportLedgerExcelParser.ParsedPointRow::getGroupNo,
LinkedHashMap::new, Collectors.counting()));
for (Map.Entry<Integer, Long> entry : groupCounter.entrySet()) {
ObjectNode groupNode = groupSummary.addObject();
if (entry.getKey() == null) {
groupNode.putNull("groupNo");
} else {
groupNode.put("groupNo", entry.getKey());
}
groupNode.put("count", entry.getValue());
}
ObjectNode baseDataSummary = root.putObject("baseDataSummary");
baseDataSummary.put("deviceAddedCount", summary.getDeviceAddedCount());
baseDataSummary.put("deviceReuseCount", summary.getDeviceReuseCount());
baseDataSummary.put("clientAddedCount", summary.getClientAddedCount());
baseDataSummary.put("clientReuseCount", summary.getClientReuseCount());
return root.toString();
}
private TestReportLedgerImportResultVO buildValidateSuccessResult(String batchId, String tempStoragePath,
TestReportLedgerExcelParser.ParsedLedger ledger,
int uploadedFileCount) {
TestReportLedgerImportResultVO result = new TestReportLedgerImportResultVO();
result.setBatchId(batchId);
result.setCurrentStage("文件校验");
result.setSuccess(Boolean.TRUE);
result.setSummaryFileName(ledger.getSummaryFileName());
result.setTempStoragePath(tempStoragePath);
result.setUploadedFileCount(uploadedFileCount);
result.setTotalFileCount(uploadedFileCount);
result.setPointCount(ledger.getPoints().size());
result.setExcelAttachmentCount(ledger.getPoints().size());
result.setWordAttachmentCount(ledger.getPoints().size());
result.getStages().add(buildStage("文件选择", "文件选择完成"));
result.getStages().add(buildStage("文件校验", "文件校验通过"));
return result;
}
private TestReportLedgerImportResultVO buildImportSuccessResult(String batchId, String tempStoragePath,
TestReportLedgerExcelParser.ParsedLedger ledger,
BaseDataSummary summary, int uploadedFileCount,
int groupCount) {
TestReportLedgerImportResultVO result = new TestReportLedgerImportResultVO();
result.setBatchId(batchId);
result.setCurrentStage("完成");
result.setSuccess(Boolean.TRUE);
result.setSummaryFileName(ledger.getSummaryFileName());
result.setTempStoragePath(tempStoragePath);
result.setUploadedFileCount(uploadedFileCount);
result.setTotalFileCount(uploadedFileCount);
result.setPointCount(ledger.getPoints().size());
result.setExcelAttachmentCount(ledger.getPoints().size());
result.setWordAttachmentCount(ledger.getPoints().size());
result.setDeviceAddedCount(summary.getDeviceAddedCount());
result.setDeviceReuseCount(summary.getDeviceReuseCount());
result.setClientAddedCount(summary.getClientAddedCount());
result.setClientReuseCount(summary.getClientReuseCount());
result.setGroupCount(groupCount);
result.getStages().add(buildStage("文件选择", "文件选择完成"));
result.getStages().add(buildStage("文件校验", "已复用已校验批次"));
result.getStages().add(buildStage("文件导入", "正式文件和点位数据已导入"));
result.getStages().add(buildStage("基础信息维护", "基础信息维护完成"));
result.getStages().add(buildStage("完成", "导入完成"));
return result;
}
private TestReportLedgerImportStageVO buildStage(String stage, String message) {
TestReportLedgerImportStageVO stageVO = new TestReportLedgerImportStageVO();
stageVO.setStage(stage);
stageVO.setSuccess(Boolean.TRUE);
stageVO.setMessage(message);
return stageVO;
}
private String resolveSummaryStoragePath(String data) {
if (StrUtil.isBlank(data)) {
return null;
}
try {
String summaryStoragePath = OBJECT_MAPPER.readTree(data).path("summaryStoragePath").asText();
return StrUtil.isBlank(summaryStoragePath) ? null : summaryStoragePath;
} catch (Exception exception) {
return null;
}
}
private List<TestReportPointPO> loadNormalPoints(String reportId) {
LambdaQueryWrapper<TestReportPointPO> wrapper = new LambdaQueryWrapper<TestReportPointPO>();
wrapper.eq(TestReportPointPO::getReportId, reportId)
.eq(TestReportPointPO::getDeleted, TestReportConst.DELETED_NO);
return testReportPointMapper.selectList(wrapper);
}
private List<TestReportGroupReportPO> loadNormalGroupReports(String reportId) {
LambdaQueryWrapper<TestReportGroupReportPO> wrapper = new LambdaQueryWrapper<TestReportGroupReportPO>();
wrapper.eq(TestReportGroupReportPO::getReportId, reportId)
.eq(TestReportGroupReportPO::getDeleted, TestReportConst.DELETED_NO);
return testReportGroupReportMapper.selectList(wrapper);
}
private Map<String, TestDevicePO> loadExistingDevices(Collection<String> deviceNos) {
if (deviceNos.isEmpty()) {
return new HashMap<String, TestDevicePO>();
}
LambdaQueryWrapper<TestDevicePO> wrapper = new LambdaQueryWrapper<TestDevicePO>();
wrapper.in(TestDevicePO::getNo, deviceNos)
.eq(TestDevicePO::getStatus, TestDeviceConst.STATUS_NORMAL);
return testDeviceMapper.selectList(wrapper).stream()
.collect(Collectors.toMap(TestDevicePO::getNo, item -> item, (first, second) -> first, HashMap::new));
}
private Map<String, ClientUnitPO> loadExistingClients(Collection<String> clientNames) {
if (clientNames.isEmpty()) {
return new HashMap<String, ClientUnitPO>();
}
LambdaQueryWrapper<ClientUnitPO> wrapper = new LambdaQueryWrapper<ClientUnitPO>();
wrapper.in(ClientUnitPO::getName, clientNames)
.eq(ClientUnitPO::getStatus, ClientUnitConst.STATUS_NORMAL);
return clientUnitMapper.selectList(wrapper).stream()
.collect(Collectors.toMap(ClientUnitPO::getName, item -> item, (first, second) -> first, HashMap::new));
}
private Collection<String> collectDeviceNos(TestReportLedgerExcelParser.ParsedLedger ledger) {
Set<String> deviceNos = new HashSet<String>();
for (TestReportLedgerExcelParser.ParsedPointRow point : ledger.getPoints()) {
if (StrUtil.isNotBlank(point.getTestDeviceNo())) {
deviceNos.add(point.getTestDeviceNo());
}
}
for (TestReportLedgerExcelParser.ParsedDeviceRow device : ledger.getDevices()) {
deviceNos.add(device.getNo());
}
return deviceNos;
}
private Collection<String> collectClientNames(TestReportLedgerExcelParser.ParsedLedger ledger) {
Set<String> clientNames = new HashSet<String>();
for (TestReportLedgerExcelParser.ParsedPointRow point : ledger.getPoints()) {
if (StrUtil.isNotBlank(point.getClientUnitName())) {
clientNames.add(point.getClientUnitName());
}
}
for (TestReportLedgerExcelParser.ParsedClientRow client : ledger.getClients()) {
clientNames.add(client.getName());
}
return clientNames;
}
private Map<String, String> loadDeviceTypeNameIdMap() {
List<TestDeviceTypeNameVO> typeRows = testDeviceMapper.selectNormalDeviceTypes();
if (typeRows == null || typeRows.isEmpty()) {
return Collections.emptyMap();
}
Map<String, String> result = new HashMap<String, String>();
for (TestDeviceTypeNameVO typeRow : typeRows) {
if (typeRow != null && StrUtil.isNotBlank(typeRow.getName()) && StrUtil.isNotBlank(typeRow.getId())) {
result.put(typeRow.getName(), typeRow.getId());
}
}
return result;
}
private String resolveDeviceState(String stateName, Integer rowNo) {
String normalizedState = normalizeFileName(stateName);
if (normalizedState == null) {
return null;
}
if ("01".equals(normalizedState) || "在运".equals(normalizedState)) {
return TestDeviceConst.STATE_RUNNING;
}
if ("02".equals(normalizedState) || "退运".equals(normalizedState)) {
return TestDeviceConst.STATE_RETIRED;
}
throw new BusinessException(CommonResponseEnum.FAIL,
"基础信息维护阶段检测设备sheet第" + rowNo + "行设备状态不正确");
}
private String defaultEmpty(String value) {
return value == null ? "" : value;
}
private String normalizeFileName(String fileName) {
String normalized = fileName == null ? null : fileName.trim();
return StrUtil.isBlank(normalized) ? null : normalized;
}
private static class BaseDataSummary {
private int deviceAddedCount;
private int deviceReuseCount;
private int clientAddedCount;
private int clientReuseCount;
public int getDeviceAddedCount() {
return deviceAddedCount;
}
public void setDeviceAddedCount(int deviceAddedCount) {
this.deviceAddedCount = deviceAddedCount;
}
public int getDeviceReuseCount() {
return deviceReuseCount;
}
public void setDeviceReuseCount(int deviceReuseCount) {
this.deviceReuseCount = deviceReuseCount;
}
public int getClientAddedCount() {
return clientAddedCount;
}
public void setClientAddedCount(int clientAddedCount) {
this.clientAddedCount = clientAddedCount;
}
public int getClientReuseCount() {
return clientReuseCount;
}
public void setClientReuseCount(int clientReuseCount) {
this.clientReuseCount = clientReuseCount;
}
}
public static class BatchMeta {
private String batchId;
private String reportId;
private String status;
private String summaryFileName;
private String tempStoragePath;
private Integer uploadedFileCount;
private List<String> uploadedFileNames;
private String validatedBy;
private String validatedTime;
public Set<String> toFileNameSet() {
return uploadedFileNames == null ? Collections.<String>emptySet() : new HashSet<String>(uploadedFileNames);
}
public String getBatchId() {
return batchId;
}
public void setBatchId(String batchId) {
this.batchId = batchId;
}
public String getReportId() {
return reportId;
}
public void setReportId(String reportId) {
this.reportId = reportId;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getSummaryFileName() {
return summaryFileName;
}
public void setSummaryFileName(String summaryFileName) {
this.summaryFileName = summaryFileName;
}
public String getTempStoragePath() {
return tempStoragePath;
}
public void setTempStoragePath(String tempStoragePath) {
this.tempStoragePath = tempStoragePath;
}
public Integer getUploadedFileCount() {
return uploadedFileCount;
}
public void setUploadedFileCount(Integer uploadedFileCount) {
this.uploadedFileCount = uploadedFileCount;
}
public List<String> getUploadedFileNames() {
return uploadedFileNames;
}
public void setUploadedFileNames(List<String> uploadedFileNames) {
this.uploadedFileNames = uploadedFileNames;
}
public String getValidatedBy() {
return validatedBy;
}
public void setValidatedBy(String validatedBy) {
this.validatedBy = validatedBy;
}
public String getValidatedTime() {
return validatedTime;
}
public void setValidatedTime(String validatedTime) {
this.validatedTime = validatedTime;
}
}
}

View File

@@ -0,0 +1,64 @@
package com.njcn.gather.aireport.testreport.component;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Component;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
@Component
public class TestReportLedgerTemplateService {
public byte[] buildTemplate() {
try (XSSFWorkbook workbook = new XSSFWorkbook();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
writeGuideSheet(workbook.createSheet(TestReportConst.LEDGER_GUIDE_SHEET_NAME));
writeHeaders(workbook, workbook.createSheet(TestReportConst.LEDGER_POINT_SHEET_NAME),
Arrays.asList("分组号", "变电站", "监测点名称", "检测设备编号", "委托单位名称", "Excel附件名称", "Word附件名称"));
writeHeaders(workbook, workbook.createSheet(TestReportConst.LEDGER_DEVICE_SHEET_NAME),
Arrays.asList("设备型号", "设备编号", "标准有效期", "设备状态", "标准报告文件名"));
writeHeaders(workbook, workbook.createSheet(TestReportConst.LEDGER_CLIENT_SHEET_NAME),
Arrays.asList("单位名称", "联系人", "联系方式", "地址"));
workbook.write(outputStream);
return outputStream.toByteArray();
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "下载模板失败");
}
}
private void writeGuideSheet(XSSFSheet sheet) {
Row row0 = sheet.createRow(0);
row0.createCell(0).setCellValue("1. 汇总文件名固定为台账信息汇总.xlsx");
Row row1 = sheet.createRow(1);
row1.createCell(0).setCellValue("2. 监测点信息sheet必填检测设备/委托单位sheet按需填写");
Row row2 = sheet.createRow(2);
row2.createCell(0).setCellValue("3. 每个监测点必须同时填写Excel附件名称和Word附件名称");
Row row3 = sheet.createRow(3);
row3.createCell(0).setCellValue("4. 检测设备编号、委托单位名称优先匹配数据库未命中时再匹配本次sheet");
sheet.setColumnWidth(0, 120 * 256);
}
private void writeHeaders(XSSFWorkbook workbook, XSSFSheet sheet, List<String> headers) {
CellStyle style = workbook.createCellStyle();
Font font = workbook.createFont();
font.setBold(true);
style.setFont(font);
Row headerRow = sheet.createRow(0);
for (int i = 0; i < headers.size(); i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers.get(i));
cell.setCellStyle(style);
sheet.setColumnWidth(i, 24 * 256);
}
}
}

View File

@@ -0,0 +1,108 @@
package com.njcn.gather.aireport.testreport.component;
import cn.hutool.core.util.StrUtil;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import lombok.Data;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.Map;
@Component
public class TestReportPointExcelParser {
private static final String NODE_INFO_SHEET_NAME = "节点信息";
public ParsedPoint parse(String fileName, InputStream inputStream) {
try (Workbook workbook = WorkbookFactory.create(inputStream)) {
Sheet sheet = workbook.getSheet(NODE_INFO_SHEET_NAME);
if (sheet == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "原始Excel缺少节点信息sheet");
}
return parseNodeInfoSheet(fileName, sheet);
} catch (BusinessException exception) {
throw exception;
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "读取原始Excel失败");
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "解析原始Excel失败");
}
}
private ParsedPoint parseNodeInfoSheet(String fileName, Sheet sheet) {
Map<String, String> keyValueMap = new LinkedHashMap<String, String>();
DataFormatter formatter = new DataFormatter();
for (Row row : sheet) {
short lastCellNum = row.getLastCellNum();
if (lastCellNum < 2) {
continue;
}
for (int i = 0; i < lastCellNum - 1; i++) {
String key = trim(formatter.formatCellValue(row.getCell(i)));
String value = trim(formatter.formatCellValue(row.getCell(i + 1)));
if (StrUtil.isNotBlank(key) && StrUtil.isNotBlank(value) && !keyValueMap.containsKey(key)) {
keyValueMap.put(key, value);
}
}
}
ParsedPoint point = new ParsedPoint();
point.setExcelAttachmentName(fileName);
point.setSubstationName(requireAny(keyValueMap, "变电站", "厂站名称", "变电站名称"));
point.setPointName(requireAny(keyValueMap, "监测点名称", "节点名称", "监测点"));
point.setMonitorTime(optionalAny(keyValueMap, "监测时间", "录波时间", "开始时间"));
point.setVoltageLevel(optionalAny(keyValueMap, "电压等级", "电压"));
point.setPtRatio(optionalAny(keyValueMap, "PT变比", "PT"));
point.setCtRatio(optionalAny(keyValueMap, "CT变比", "CT"));
point.setBaseShortCircuitCapacity(optionalAny(keyValueMap, "基准短路容量"));
point.setMinShortCircuitCapacity(optionalAny(keyValueMap, "最小短路容量"));
point.setProtocolCapacity(optionalAny(keyValueMap, "用户协议容量"));
point.setPowerSupplyCapacity(optionalAny(keyValueMap, "供电设备容量"));
return point;
}
private String requireAny(Map<String, String> keyValueMap, String... candidateKeys) {
String value = optionalAny(keyValueMap, candidateKeys);
if (StrUtil.isBlank(value)) {
throw new BusinessException(CommonResponseEnum.FAIL, "节点信息sheet缺少必要字段");
}
return value;
}
private String optionalAny(Map<String, String> keyValueMap, String... candidateKeys) {
for (String candidateKey : candidateKeys) {
String value = keyValueMap.get(candidateKey);
if (StrUtil.isNotBlank(value)) {
return value;
}
}
return null;
}
private String trim(String value) {
return StrUtil.trimToNull(value);
}
@Data
public static class ParsedPoint {
private String excelAttachmentName;
private String substationName;
private String pointName;
private String monitorTime;
private String voltageLevel;
private String ptRatio;
private String ctRatio;
private String baseShortCircuitCapacity;
private String minShortCircuitCapacity;
private String protocolCapacity;
private String powerSupplyCapacity;
}
}

View File

@@ -0,0 +1,335 @@
package com.njcn.gather.aireport.testreport.component;
import cn.hutool.core.util.StrUtil;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.common.constant.AiReportFileConst;
import com.njcn.gather.aireport.common.constant.AiReportFileExtensionConst;
import com.njcn.gather.aireport.common.storage.AiReportMinioStorageService;
import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
@Component
@RequiredArgsConstructor
public class TestReportStorageService {
private final AiReportMinioStorageService minioStorageService;
public String saveSourceExcel(String reportId, MultipartFile file) {
validateMultipartFile(file, AiReportFileExtensionConst.EXCEL_EXTENSIONS, "原始Excel文件不能为空", "原始Excel仅允许xlsx格式");
return minioStorageService.saveMultipartFile(buildBusinessDir(reportId, TestReportConst.STORAGE_SOURCE_EXCEL_DIR),
null, file, false).getObjectName();
}
public String saveLedgerSummaryExcel(String reportId, MultipartFile file) {
validateMultipartFile(file, AiReportFileExtensionConst.EXCEL_EXTENSIONS, "台账汇总文件不能为空", "台账汇总仅允许xlsx格式");
return minioStorageService.saveMultipartFile(buildBusinessDir(reportId, TestReportConst.STORAGE_LEDGER_SUMMARY_DIR),
null, file, false).getObjectName();
}
public String savePointExcelAttachment(String reportId, MultipartFile file) {
validateMultipartFile(file, AiReportFileExtensionConst.EXCEL_EXTENSIONS, "监测点Excel附件不能为空", "监测点Excel附件仅允许xlsx格式");
return minioStorageService.saveMultipartFile(buildBusinessDir(reportId, TestReportConst.STORAGE_POINT_EXCEL_DIR),
null, file, false).getObjectName();
}
public String savePointWordAttachment(String reportId, MultipartFile file) {
validateMultipartFile(file, AiReportFileExtensionConst.WORD_EXTENSIONS, "监测点Word附件不能为空", "监测点Word附件仅允许docx格式");
return minioStorageService.saveMultipartFile(buildBusinessDir(reportId, TestReportConst.STORAGE_POINT_WORD_DIR),
null, file, false).getObjectName();
}
public UploadedBatch saveTempBatchFiles(String reportId, String batchId, Map<String, MultipartFile> fileMap) {
return saveBatchFiles(buildTempUploadDir(reportId, batchId), buildTempBatchRelativePath(reportId, batchId),
batchId, fileMap, "文件上传阶段:保存临时文件失败");
}
public UploadedBatch saveValidateSessionFiles(String sessionId, Map<String, MultipartFile> fileMap) {
return saveBatchFiles(buildValidateSessionUploadDir(sessionId), buildValidateSessionRelativePath(sessionId),
sessionId, fileMap, "校验文件阶段:保存校验会话文件失败");
}
public AiReportMinioStorageService.StoredObject getTempBatchObject(String reportId, String batchId, String fileName) {
return minioStorageService.getObject(buildTempBatchFileObjectName(reportId, batchId, fileName));
}
public AiReportMinioStorageService.StoredObject getValidateSessionObject(String sessionId, String fileName) {
return minioStorageService.getObject(buildValidateSessionFileObjectName(sessionId, fileName));
}
public void writeTempBatchMeta(String reportId, String batchId, String content) {
writeText(buildTempBatchMetaObjectName(reportId, batchId), content, "校验文件阶段:保存批次元数据失败");
}
public void writeValidateSessionResult(String sessionId, String content) {
writeText(buildValidateSessionResultObjectName(sessionId), content, "校验文件阶段:保存校验结果失败");
}
public List<String> listValidateSessionFileNames(String sessionId) {
return minioStorageService.listFileNames(buildValidateSessionUploadDir(sessionId));
}
public String readTempBatchMeta(String reportId, String batchId) {
String objectName = buildTempBatchMetaObjectName(reportId, batchId);
if (!minioStorageService.exists(objectName)) {
return null;
}
return new String(minioStorageService.readBytes(objectName), StandardCharsets.UTF_8);
}
public String saveLedgerSummaryFromTemp(String reportId, String batchId, String fileName) {
return copyTempBatchFile(reportId, batchId, fileName,
buildBusinessDir(reportId, TestReportConst.STORAGE_LEDGER_SUMMARY_DIR),
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "保存台账汇总excel失败");
}
public String savePointExcelFromTemp(String reportId, String batchId, String fileName) {
return copyTempBatchFile(reportId, batchId, fileName,
buildBusinessDir(reportId, TestReportConst.STORAGE_POINT_EXCEL_DIR),
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "保存监测点excel附件失败");
}
public String savePointWordFromTemp(String reportId, String batchId, String fileName) {
return copyTempBatchFile(reportId, batchId, fileName,
buildBusinessDir(reportId, TestReportConst.STORAGE_POINT_WORD_DIR),
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", "保存监测点word附件失败");
}
public String saveGeneratedPlaceholder(String reportId, String fileName, byte[] content) {
validateFileName(fileName, AiReportFileExtensionConst.WORD_EXTENSIONS, "生成报告文件名不能为空", "生成报告仅允许docx格式");
return minioStorageService.saveBytes(buildBusinessDir(reportId, TestReportConst.STORAGE_GENERATED_REPORT_DIR),
null, fileName, content, MediaType.APPLICATION_OCTET_STREAM_VALUE, false).getObjectName();
}
public byte[] readBusinessBytes(String storedPath) {
return minioStorageService.readBytes(storedPath);
}
public void deleteQuietly(String storedPath) {
minioStorageService.deleteQuietly(storedPath);
}
public void deleteTempBatchQuietly(String reportId, String batchId) {
deletePrefixQuietly(buildTempBatchDir(reportId, batchId));
}
private UploadedBatch saveBatchFiles(String bizDir, String tempStoragePath, String batchId,
Map<String, MultipartFile> fileMap, String errorMessage) {
if (fileMap == null || fileMap.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "文件不能为空");
}
Map<String, String> storedFileMap = new LinkedHashMap<String, String>();
try {
for (Map.Entry<String, MultipartFile> entry : fileMap.entrySet()) {
String originalFileName = trimFileName(entry.getKey());
MultipartFile file = entry.getValue();
if (StrUtil.isBlank(originalFileName) || file == null || file.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "文件不能为空");
}
validateAllowedUploadFile(originalFileName);
try (InputStream inputStream = file.getInputStream()) {
AiReportMinioStorageService.StoredFile storedFile = minioStorageService.saveInputStream(
bizDir, null, originalFileName, inputStream, file.getSize(), file.getContentType(), true);
storedFileMap.put(originalFileName, storedFile.getObjectName());
}
}
} catch (BusinessException exception) {
cleanupStoredFiles(storedFileMap);
throw exception;
} catch (Exception exception) {
cleanupStoredFiles(storedFileMap);
throw new BusinessException(CommonResponseEnum.FAIL, errorMessage);
}
UploadedBatch uploadedBatch = new UploadedBatch();
uploadedBatch.setBatchId(batchId);
uploadedBatch.setTempStoragePath(tempStoragePath);
uploadedBatch.setStoredFileMap(storedFileMap);
return uploadedBatch;
}
private void writeText(String objectName, String content, String errorMessage) {
try {
minioStorageService.saveBytes(resolveParentDir(objectName), null, resolveFileName(objectName),
content == null ? new byte[0] : content.getBytes(StandardCharsets.UTF_8),
MediaType.APPLICATION_JSON_VALUE, true);
} catch (BusinessException exception) {
throw exception;
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, errorMessage);
}
}
private String copyTempBatchFile(String reportId, String batchId, String fileName, String targetBizDir,
String contentType, String errorMessage) {
String normalizedFileName = trimFileName(fileName);
if (StrUtil.isBlank(normalizedFileName)) {
return null;
}
try {
byte[] content = minioStorageService.readBytes(buildTempBatchFileObjectName(reportId, batchId, normalizedFileName));
return minioStorageService.saveBytes(targetBizDir, null, normalizedFileName, content, contentType, false)
.getObjectName();
} catch (BusinessException exception) {
throw exception;
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, errorMessage);
}
}
private void deletePrefixQuietly(String prefix) {
List<String> objectNames = minioStorageService.listObjectNames(prefix);
for (String objectName : objectNames) {
minioStorageService.deleteQuietly(objectName);
}
}
private void cleanupStoredFiles(Map<String, String> storedFileMap) {
if (storedFileMap == null || storedFileMap.isEmpty()) {
return;
}
for (String objectName : storedFileMap.values()) {
minioStorageService.deleteQuietly(objectName);
}
}
private void validateMultipartFile(MultipartFile file, Set<String> allowedExtensions,
String emptyMessage, String invalidMessage) {
if (file == null || file.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, emptyMessage);
}
validateFileName(file.getOriginalFilename(), allowedExtensions, emptyMessage, invalidMessage);
}
private void validateAllowedUploadFile(String fileName) {
validateFileName(fileName, AiReportFileExtensionConst.ALLOWED_EXTENSIONS, "文件不能为空",
"仅允许上传docx、xlsx、pdf文件");
}
private void validateFileName(String fileName, Set<String> allowedExtensions,
String emptyMessage, String invalidMessage) {
String normalizedFileName = trimFileName(fileName);
if (StrUtil.isBlank(normalizedFileName)) {
throw new BusinessException(CommonResponseEnum.FAIL, emptyMessage);
}
String extension = resolveExtension(normalizedFileName);
if (!allowedExtensions.contains(extension)) {
throw new BusinessException(CommonResponseEnum.FAIL, invalidMessage);
}
}
private String buildBusinessDir(String reportId, String childDir) {
return AiReportFileConst.BIZ_DIR_TEST_REPORT + "/" + reportId + "/" + childDir;
}
private String buildTempBatchDir(String reportId, String batchId) {
return AiReportFileConst.BIZ_DIR_TEST_REPORT + "/" + reportId + "/"
+ TestReportConst.STORAGE_IMPORT_TEMP_DIR + "/" + batchId;
}
private String buildTempUploadDir(String reportId, String batchId) {
return buildTempBatchDir(reportId, batchId) + "/" + TestReportConst.STORAGE_IMPORT_TEMP_UPLOAD_DIR;
}
private String buildValidateSessionDir(String sessionId) {
return AiReportFileConst.BIZ_DIR_TEST_REPORT + "/" + TestReportConst.STORAGE_VALIDATE_SESSION_DIR + "/" + sessionId;
}
private String buildValidateSessionUploadDir(String sessionId) {
return buildValidateSessionDir(sessionId) + "/" + TestReportConst.STORAGE_IMPORT_TEMP_UPLOAD_DIR;
}
private String buildTempBatchMetaObjectName(String reportId, String batchId) {
return buildTempBatchDir(reportId, batchId) + "/" + TestReportConst.STORAGE_IMPORT_META_FILE_NAME;
}
private String buildValidateSessionResultObjectName(String sessionId) {
return buildValidateSessionDir(sessionId) + "/" + TestReportConst.STORAGE_VALIDATE_RESULT_FILE_NAME;
}
private String buildTempBatchFileObjectName(String reportId, String batchId, String fileName) {
return buildTempUploadDir(reportId, batchId) + "/" + trimFileName(fileName);
}
private String buildValidateSessionFileObjectName(String sessionId, String fileName) {
return buildValidateSessionUploadDir(sessionId) + "/" + trimFileName(fileName);
}
private String buildTempBatchRelativePath(String reportId, String batchId) {
return reportId + "/" + TestReportConst.STORAGE_IMPORT_TEMP_DIR + "/" + batchId;
}
private String buildValidateSessionRelativePath(String sessionId) {
return TestReportConst.STORAGE_VALIDATE_SESSION_DIR + "/" + sessionId;
}
private String resolveParentDir(String objectName) {
int slashIndex = objectName.lastIndexOf('/');
if (slashIndex <= 0) {
throw new BusinessException(CommonResponseEnum.FAIL, "对象目录配置不合法");
}
return objectName.substring(0, slashIndex);
}
private String resolveFileName(String objectName) {
int slashIndex = objectName.lastIndexOf('/');
return slashIndex >= 0 ? objectName.substring(slashIndex + 1) : objectName;
}
private String trimFileName(String fileName) {
String normalized = StrUtil.trimToEmpty(fileName);
if (StrUtil.isBlank(normalized)) {
return null;
}
int slashIndex = Math.max(normalized.lastIndexOf('/'), normalized.lastIndexOf('\\'));
return slashIndex >= 0 ? normalized.substring(slashIndex + 1) : normalized;
}
private String resolveExtension(String fileName) {
int dotIndex = fileName.lastIndexOf('.');
if (dotIndex < 0 || dotIndex == fileName.length() - 1) {
return "";
}
return fileName.substring(dotIndex + 1).toLowerCase(Locale.ENGLISH);
}
public static class UploadedBatch {
private String batchId;
private String tempStoragePath;
private Map<String, String> storedFileMap = new LinkedHashMap<String, String>();
public String getBatchId() {
return batchId;
}
public void setBatchId(String batchId) {
this.batchId = batchId;
}
public String getTempStoragePath() {
return tempStoragePath;
}
public void setTempStoragePath(String tempStoragePath) {
this.tempStoragePath = tempStoragePath;
}
public Map<String, String> getStoredFileMap() {
return storedFileMap;
}
public void setStoredFileMap(Map<String, String> storedFileMap) {
this.storedFileMap = storedFileMap;
}
}
}

View File

@@ -6,7 +6,11 @@ import com.njcn.common.pojo.constant.OperateType;
import com.njcn.common.pojo.enums.common.LogEnum; import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum; import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult; import com.njcn.common.pojo.response.HttpResult;
import com.njcn.gather.aireport.testreport.pojo.param.TestReportGroupSaveParam;
import com.njcn.gather.aireport.testreport.pojo.param.TestReportParam; import com.njcn.gather.aireport.testreport.pojo.param.TestReportParam;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportGroupReportVO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportLedgerImportResultVO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportPointVO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO; import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO;
import com.njcn.gather.aireport.testreport.service.TestReportService; import com.njcn.gather.aireport.testreport.service.TestReportService;
import com.njcn.web.controller.BaseController; import com.njcn.web.controller.BaseController;
@@ -23,7 +27,9 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; 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.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.util.List; import java.util.List;
@@ -92,6 +98,13 @@ public class TestReportController extends BaseController {
return testReportService.previewTemplate(id); return testReportService.previewTemplate(id);
} }
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("导出台账导入模板")
@GetMapping("/ledger/template")
public ResponseEntity<byte[]> downloadLedgerTemplate() {
return testReportService.downloadLedgerTemplate();
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON) @OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("导出普测报告") @ApiOperation("导出普测报告")
@GetMapping("/export") @GetMapping("/export")
@@ -108,4 +121,75 @@ public class TestReportController extends BaseController {
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL, return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe); result, methodDescribe);
} }
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
@ApiOperation("校验台账汇总与监测点附件")
@PostMapping("/{id}/ledger/validate")
public HttpResult<TestReportLedgerImportResultVO> validateLedger(@PathVariable("id") String id,
@RequestParam("files") MultipartFile[] files) {
String methodDescribe = getMethodDescribe("validateLedger");
TestReportLedgerImportResultVO result = testReportService.validateLedger(id, files);
return HttpResultUtil.assembleCommonResponseResult(Boolean.TRUE.equals(result.getSuccess())
? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
@ApiOperation("导入已校验通过的台账批次")
@PostMapping("/{id}/ledger/import")
public HttpResult<TestReportLedgerImportResultVO> importLedger(@PathVariable("id") String id,
@RequestBody @Validated TestReportParam.LedgerImportParam param) {
String methodDescribe = getMethodDescribe("importLedger");
TestReportLedgerImportResultVO result = testReportService.importLedger(id, param);
return HttpResultUtil.assembleCommonResponseResult(Boolean.TRUE.equals(result.getSuccess())
? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询监测点列表")
@GetMapping("/{id}/point/list")
public HttpResult<List<TestReportPointVO>> listPoints(@PathVariable("id") String id) {
String methodDescribe = getMethodDescribe("listPoints");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
testReportService.listPoints(id), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
@ApiOperation("保存监测点分组")
@PostMapping("/{id}/group/save")
public HttpResult<Boolean> saveGroups(@PathVariable("id") String id,
@RequestBody @Validated TestReportGroupSaveParam param) {
String methodDescribe = getMethodDescribe("saveGroups");
boolean result = testReportService.saveGroups(id, param);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询分组报告结果")
@GetMapping("/{id}/group-report/list")
public HttpResult<List<TestReportGroupReportVO>> listGroupReports(@PathVariable("id") String id) {
String methodDescribe = getMethodDescribe("listGroupReports");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
testReportService.listGroupReports(id), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
@ApiOperation("按分组生成报告")
@PostMapping("/{id}/generate")
public HttpResult<Boolean> generateGroupedReports(@PathVariable("id") String id) {
String methodDescribe = getMethodDescribe("generateGroupedReports");
boolean result = testReportService.generateGroupedReports(id);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("下载分组报告")
@ApiImplicitParam(name = "groupReportId", value = "分组报告结果ID", required = true)
@GetMapping("/group-report/{groupReportId}/download")
public ResponseEntity<byte[]> downloadGroupReport(@PathVariable("groupReportId") String groupReportId) {
return testReportService.downloadGroupReport(groupReportId);
}
} }

View File

@@ -0,0 +1,9 @@
package com.njcn.gather.aireport.testreport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.njcn.gather.aireport.testreport.pojo.po.TestReportGroupReportPO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TestReportGroupReportMapper extends BaseMapper<TestReportGroupReportPO> {
}

View File

@@ -18,6 +18,9 @@ public interface TestReportMapper extends BaseMapper<TestReportPO> {
"r.report_model_id as reportModelId, rm.name as reportModelName, r.create_unit as createUnit,", "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,", "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,", "checker.name as checkerName, r.check_time as checkTime, r.check_result as checkResult, r.state,",
"r.report_generate_state as reportGenerateState,",
"(select count(1) from ai_testreport_point p where p.report_id = r.id and p.deleted = 0) as pointCount,",
"(select count(1) from ai_testreport_group_report gr where gr.report_id = r.id and gr.deleted = 0) as groupCount,",
"r.status, r.create_by as createBy, creator.name as createByName, r.create_time as createTime,", "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", "r.update_by as updateBy, updater.name as updateByName, r.update_time as updateTime",
"from ai_testreport r", "from ai_testreport r",
@@ -52,6 +55,9 @@ public interface TestReportMapper extends BaseMapper<TestReportPO> {
"r.report_model_id as reportModelId, rm.name as reportModelName, r.create_unit as createUnit,", "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,", "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,", "checker.name as checkerName, r.check_time as checkTime, r.check_result as checkResult, r.state,",
"r.report_generate_state as reportGenerateState,",
"(select count(1) from ai_testreport_point p where p.report_id = r.id and p.deleted = 0) as pointCount,",
"(select count(1) from ai_testreport_group_report gr where gr.report_id = r.id and gr.deleted = 0) as groupCount,",
"r.status, r.create_by as createBy, creator.name as createByName, r.create_time as createTime,", "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", "r.update_by as updateBy, updater.name as updateByName, r.update_time as updateTime",
"from ai_testreport r", "from ai_testreport r",

View File

@@ -0,0 +1,9 @@
package com.njcn.gather.aireport.testreport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.njcn.gather.aireport.testreport.pojo.po.TestReportPointPO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TestReportPointMapper extends BaseMapper<TestReportPointPO> {
}

View File

@@ -11,6 +11,36 @@ public final class TestReportConst {
public static final Integer STATUS_DELETED = 0; public static final Integer STATUS_DELETED = 0;
public static final Integer STATUS_NORMAL = 1; public static final Integer STATUS_NORMAL = 1;
public static final Integer REPORT_GENERATE_STATE_PENDING = 0;
public static final Integer REPORT_GENERATE_STATE_GENERATING = 1;
public static final Integer REPORT_GENERATE_STATE_FINISHED = 2;
public static final Integer GROUP_REPORT_GENERATE_STATE_PENDING = 0;
public static final Integer GROUP_REPORT_GENERATE_STATE_GENERATING = 1;
public static final Integer GROUP_REPORT_GENERATE_STATE_SUCCESS = 2;
public static final Integer GROUP_REPORT_GENERATE_STATE_FAILED = 3;
public static final Integer DELETED_NO = 0;
public static final Integer DELETED_YES = 1;
public static final String STORAGE_DIR = "test-report";
public static final String STORAGE_SOURCE_EXCEL_DIR = "source-excel";
public static final String STORAGE_LEDGER_SUMMARY_DIR = "ledger-summary";
public static final String STORAGE_POINT_EXCEL_DIR = "point-excel";
public static final String STORAGE_POINT_WORD_DIR = "point-word";
public static final String STORAGE_GENERATED_REPORT_DIR = "generated-report";
public static final String STORAGE_IMPORT_TEMP_DIR = "import-temp";
public static final String STORAGE_IMPORT_TEMP_UPLOAD_DIR = "upload";
public static final String STORAGE_IMPORT_META_FILE_NAME = "batch-meta.json";
public static final String STORAGE_VALIDATE_SESSION_DIR = "validate-session";
public static final String STORAGE_VALIDATE_RESULT_FILE_NAME = "validate-result.json";
public static final String LEDGER_SUMMARY_FILE_NAME = "台账信息汇总.xlsx";
public static final String LEDGER_GUIDE_SHEET_NAME = "填写说明";
public static final String LEDGER_POINT_SHEET_NAME = "监测点信息";
public static final String LEDGER_DEVICE_SHEET_NAME = "检测设备";
public static final String LEDGER_CLIENT_SHEET_NAME = "委托单位";
private TestReportConst() { private TestReportConst() {
} }
} }

View File

@@ -0,0 +1,29 @@
package com.njcn.gather.aireport.testreport.pojo.param;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
@Data
public class TestReportGroupSaveParam {
@ApiModelProperty("分组列表")
@Valid
@NotEmpty(message = "分组列表不能为空")
private List<GroupItem> groups;
@Data
public static class GroupItem {
@ApiModelProperty("组号")
@NotNull(message = "组号不能为空")
private Integer groupNo;
@ApiModelProperty("监测点ID列表")
@NotEmpty(message = "监测点列表不能为空")
private List<String> pointIds;
}
}

View File

@@ -1,83 +1,100 @@
package com.njcn.gather.aireport.testreport.pojo.param; package com.njcn.gather.aireport.testreport.pojo.param;
import com.fasterxml.jackson.databind.JsonNode;
import com.njcn.web.pojo.param.BaseParam; import com.njcn.web.pojo.param.BaseParam;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size; import javax.validation.constraints.Size;
import java.util.List;
public class TestReportParam { public class TestReportParam {
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public static class QueryParam extends BaseParam { 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") @ApiModelProperty("关键字,匹配报告编号、合同编号、委托单位或报告模板")
private String keyword; private String keyword;
@ApiModelProperty("\u521b\u5efa\u5f00\u59cb\u65f6\u95f4\uff0cyyyy-MM-dd HH:mm:ss") @ApiModelProperty("创建开始时间,yyyy-MM-dd HH:mm:ss")
private String searchBeginTime; private String searchBeginTime;
@ApiModelProperty("\u521b\u5efa\u7ed3\u675f\u65f6\u95f4\uff0cyyyy-MM-dd HH:mm:ss") @ApiModelProperty("创建结束时间,yyyy-MM-dd HH:mm:ss")
private String searchEndTime; private String searchEndTime;
@ApiModelProperty("\u62a5\u544a\u72b6\u6001") @ApiModelProperty("报告状态")
private String state; private String state;
} }
@Data @Data
public static class AddParam { public static class AddParam {
@ApiModelProperty("\u666e\u6d4b\u62a5\u544a\u7f16\u53f7") @ApiModelProperty("普测报告ID不传时由后端生成")
@NotBlank(message = "\u666e\u6d4b\u62a5\u544a\u7f16\u53f7\u4e0d\u80fd\u4e3a\u7a7a") private String id;
@Size(max = 32, message = "\u666e\u6d4b\u62a5\u544a\u7f16\u53f7\u4e0d\u80fd\u8d85\u8fc732\u4e2a\u5b57\u7b26")
@ApiModelProperty("普测报告编号")
@NotBlank(message = "普测报告编号不能为空")
@Size(max = 32, message = "普测报告编号不能超过32个字符")
private String no; private String no;
@ApiModelProperty("\u59d4\u6258\u5355\u4f4dID") @ApiModelProperty("委托单位ID")
@NotBlank(message = "\u59d4\u6258\u5355\u4f4d\u4e0d\u80fd\u4e3a\u7a7a") @NotBlank(message = "委托单位不能为空")
private String clientUnitId; private String clientUnitId;
@ApiModelProperty("\u62a5\u544a\u6a21\u677fID") @ApiModelProperty("报告模板ID")
@NotBlank(message = "\u62a5\u544a\u6a21\u677f\u4e0d\u80fd\u4e3a\u7a7a") @NotBlank(message = "报告模板不能为空")
private String reportModelId; private String reportModelId;
@ApiModelProperty("\u68c0\u6d4b\u516c\u53f8ID") @ApiModelProperty("检测公司ID")
@NotBlank(message = "\u68c0\u6d4b\u516c\u53f8\u4e0d\u80fd\u4e3a\u7a7a") @NotBlank(message = "检测公司不能为空")
private String createUnit; private String createUnit;
@ApiModelProperty("\u5408\u540c\u7f16\u53f7") @ApiModelProperty("合同编号")
@NotBlank(message = "\u5408\u540c\u7f16\u53f7\u4e0d\u80fd\u4e3a\u7a7a") @NotBlank(message = "合同编号不能为空")
@Size(max = 32, message = "\u5408\u540c\u7f16\u53f7\u4e0d\u80fd\u8d85\u8fc732\u4e2a\u5b57\u7b26") @Size(max = 32, message = "合同编号不能超过32个字符")
private String contractNumber; private String contractNumber;
@ApiModelProperty("\u68c0\u6d4b\u4f9d\u636e\uff0cJSON\u5b57\u7b26\u4e32\u6570\u7ec4") @ApiModelProperty("检测依据JSON数组")
@NotBlank(message = "\u68c0\u6d4b\u4f9d\u636e\u4e0d\u80fd\u4e3a\u7a7a") @NotEmpty(message = "检测依据不能为空")
private String standard; private List<String> standard;
@ApiModelProperty("\u53d7\u8d44\u6570\u636e\uff0cJSON\u5bf9\u8c61\u6216JSON\u6570\u7ec4") @ApiModelProperty("受资数据JSON对象或JSON数组")
@NotBlank(message = "\u53d7\u8d44\u6570\u636e\u4e0d\u80fd\u4e3a\u7a7a") @NotNull(message = "受资数据不能为空")
private String data; private JsonNode data;
@ApiModelProperty("\u8054\u7cfb\u7535\u8bdd") @ApiModelProperty("台账校验通过后返回的导入批次ID")
@Size(max = 32, message = "\u8054\u7cfb\u7535\u8bdd\u4e0d\u80fd\u8d85\u8fc732\u4e2a\u5b57\u7b26") private String batchId;
@ApiModelProperty("联系电话")
@Size(max = 32, message = "联系电话不能超过32个字符")
private String phonenumber; private String phonenumber;
} }
@Data @Data
public static class UpdateParam extends AddParam { public static class UpdateParam extends AddParam {
@ApiModelProperty("\u666e\u6d4b\u62a5\u544aID") @ApiModelProperty("普测报告ID")
@NotBlank(message = "\u666e\u6d4b\u62a5\u544aID\u4e0d\u80fd\u4e3a\u7a7a") @NotBlank(message = "普测报告ID不能为空")
private String id; private String id;
} }
@Data @Data
public static class SubmitAuditParam { public static class SubmitAuditParam {
@ApiModelProperty("\u666e\u6d4b\u62a5\u544aID") @ApiModelProperty("普测报告ID")
@NotBlank(message = "\u666e\u6d4b\u62a5\u544aID\u4e0d\u80fd\u4e3a\u7a7a") @NotBlank(message = "普测报告ID不能为空")
private String id; private String id;
@ApiModelProperty("\u5ba1\u6838\u610f\u89c1") @ApiModelProperty("审核意见")
@Size(max = 128, message = "\u5ba1\u6838\u610f\u89c1\u4e0d\u80fd\u8d85\u8fc7128\u4e2a\u5b57\u7b26") @Size(max = 128, message = "审核意见不能超过128个字符")
private String checkResult; private String checkResult;
} }
@Data
public static class LedgerImportParam {
@ApiModelProperty("校验成功的导入批次ID")
@NotBlank(message = "导入批次ID不能为空")
private String batchId;
}
} }

View File

@@ -0,0 +1,30 @@
package com.njcn.gather.aireport.testreport.pojo.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ai_testreport_group_report")
public class TestReportGroupReportPO {
@TableId(type = IdType.INPUT)
private String id;
private String reportId;
private Integer groupNo;
private String reportName;
private String reportFileName;
private String reportStoragePath;
private Integer generateState;
private String generateMessage;
private LocalDateTime generateTime;
private Integer sortNo;
private Integer deleted;
private String createBy;
private LocalDateTime createTime;
private String updateBy;
private LocalDateTime updateTime;
}

View File

@@ -30,6 +30,8 @@ public class TestReportPO implements Serializable {
private String standard; private String standard;
@TableField("data") @TableField("data")
private String data; private String data;
@TableField("report_generate_state")
private Integer reportGenerateState;
@TableField("phonenumber") @TableField("phonenumber")
private String phonenumber; private String phonenumber;
@TableField("state") @TableField("state")

View File

@@ -0,0 +1,36 @@
package com.njcn.gather.aireport.testreport.pojo.po;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ai_testreport_point")
public class TestReportPointPO {
@TableId(type = IdType.INPUT)
private String id;
private String reportId;
private Integer groupNo;
private String substationName;
private String pointName;
private String monitorTime;
private String voltageLevel;
private String ptRatio;
private String ctRatio;
private String baseShortCircuitCapacity;
private String minShortCircuitCapacity;
private String protocolCapacity;
private String powerSupplyCapacity;
private String excelAttachmentName;
private String excelStoragePath;
private Integer sortNo;
private Integer deleted;
private String createBy;
private LocalDateTime createTime;
private String updateBy;
private LocalDateTime updateTime;
}

View File

@@ -9,45 +9,45 @@ import java.io.Serializable;
public class TestReportExcelVO implements Serializable { public class TestReportExcelVO implements Serializable {
private static final long serialVersionUID = -3010688465572795334L; private static final long serialVersionUID = -3010688465572795334L;
@Excel(name = "\u666e\u6d4b\u62a5\u544a\u7f16\u53f7", width = 25) @Excel(name = "普测报告编号", width = 25)
private String no; private String no;
@Excel(name = "\u59d4\u6258\u5355\u4f4d", width = 25) @Excel(name = "委托单位", width = 25)
private String clientUnitName; private String clientUnitName;
@Excel(name = "\u62a5\u544a\u6a21\u677f", width = 25) @Excel(name = "报告模板", width = 25)
private String reportModelName; private String reportModelName;
@Excel(name = "\u68c0\u6d4b\u516c\u53f8", width = 20) @Excel(name = "检测公司", width = 20)
private String createUnit; private String createUnit;
@Excel(name = "\u5408\u540c\u7f16\u53f7", width = 20) @Excel(name = "合同编号", width = 20)
private String contractNumber; private String contractNumber;
@Excel(name = "\u68c0\u6d4b\u4f9d\u636e", width = 35) @Excel(name = "检测依据", width = 35)
private String standard; private String standard;
@Excel(name = "\u53d7\u8d44\u6570\u636e", width = 35) @Excel(name = "受资数据", width = 35)
private String data; private String data;
@Excel(name = "\u8054\u7cfb\u7535\u8bdd", width = 20) @Excel(name = "联系电话", width = 20)
private String phonenumber; private String phonenumber;
@Excel(name = "\u5ba1\u6838\u4eba", width = 20) @Excel(name = "审核人", width = 20)
private String checkerName; private String checkerName;
@Excel(name = "\u5ba1\u6838\u65f6\u95f4", width = 20) @Excel(name = "审核时间", width = 20)
private String checkTime; private String checkTime;
@Excel(name = "\u5ba1\u6838\u610f\u89c1", width = 30) @Excel(name = "审核意见", width = 30)
private String checkResult; private String checkResult;
@Excel(name = "\u62a5\u544a\u72b6\u6001", width = 15) @Excel(name = "报告状态", width = 15)
private String state; private String state;
@Excel(name = "\u521b\u5efa\u65f6\u95f4", width = 25) @Excel(name = "创建时间", width = 25)
private String createTime; private String createTime;
@Excel(name = "\u521b\u5efa\u4eba", width = 20) @Excel(name = "创建人", width = 20)
private String createByName; private String createByName;
} }

View File

@@ -0,0 +1,19 @@
package com.njcn.gather.aireport.testreport.pojo.vo;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class TestReportGroupReportVO {
private String id;
private Integer groupNo;
private String reportName;
private String reportFileName;
private String reportStoragePath;
private Integer generateState;
private String generateMessage;
private LocalDateTime generateTime;
private Integer sortNo;
}

View File

@@ -0,0 +1,28 @@
package com.njcn.gather.aireport.testreport.pojo.vo;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class TestReportLedgerImportResultVO {
private String batchId;
private String currentStage;
private Boolean success;
private String summaryFileName;
private String tempStoragePath;
private Integer totalFileCount;
private Integer uploadedFileCount;
private Integer pointCount;
private Integer excelAttachmentCount;
private Integer wordAttachmentCount;
private Integer deviceAddedCount;
private Integer deviceReuseCount;
private Integer clientAddedCount;
private Integer clientReuseCount;
private Integer groupCount;
private List<String> failReasons = new ArrayList<String>();
private List<TestReportLedgerImportStageVO> stages = new ArrayList<TestReportLedgerImportStageVO>();
}

View File

@@ -0,0 +1,11 @@
package com.njcn.gather.aireport.testreport.pojo.vo;
import lombok.Data;
@Data
public class TestReportLedgerImportStageVO {
private String stage;
private Boolean success;
private String message;
}

View File

@@ -0,0 +1,15 @@
package com.njcn.gather.aireport.testreport.pojo.vo;
import lombok.Data;
@Data
public class TestReportLedgerValidateAttachmentVO {
private String attachmentName;
private String attachmentType;
private Boolean referenced;
private Boolean uploaded;
private Boolean missing;
private Boolean reuploadAllowed;
private String message;
}

View File

@@ -0,0 +1,22 @@
package com.njcn.gather.aireport.testreport.pojo.vo;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class TestReportLedgerValidatePointVO {
private Integer rowNo;
private String pointKey;
private String substationName;
private String pointName;
private Boolean complete;
private Integer missingAttachmentCount;
private Boolean testDeviceNoValid;
private String testDeviceNoMessage;
private Boolean clientUnitNameValid;
private String clientUnitNameMessage;
private List<TestReportLedgerValidateAttachmentVO> attachments = new ArrayList<TestReportLedgerValidateAttachmentVO>();
}

View File

@@ -0,0 +1,26 @@
package com.njcn.gather.aireport.testreport.pojo.vo;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class TestReportLedgerValidateResultVO {
private String sessionId;
private Boolean success;
private String summaryFileName;
private Boolean summaryFilePresent;
private String tempStoragePath;
private Integer uploadedFileCount;
private Integer pointCount;
private Integer missingAttachmentCount;
private Boolean canContinueUpload;
private List<String> requiredSheets = new ArrayList<String>();
private List<String> existingSheets = new ArrayList<String>();
private List<String> missingSheets = new ArrayList<String>();
private List<String> orphanAttachmentNames = new ArrayList<String>();
private List<String> failReasons = new ArrayList<String>();
private List<TestReportLedgerValidatePointVO> points = new ArrayList<TestReportLedgerValidatePointVO>();
}

View File

@@ -0,0 +1,14 @@
package com.njcn.gather.aireport.testreport.pojo.vo;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class TestReportPointImportResultVO {
private Integer fileCount;
private Integer pointCount;
private List<String> failReasons = new ArrayList<String>();
}

View File

@@ -0,0 +1,21 @@
package com.njcn.gather.aireport.testreport.pojo.vo;
import lombok.Data;
@Data
public class TestReportPointVO {
private String id;
private Integer groupNo;
private String substationName;
private String pointName;
private String monitorTime;
private String voltageLevel;
private String ptRatio;
private String ctRatio;
private String baseShortCircuitCapacity;
private String minShortCircuitCapacity;
private String protocolCapacity;
private String powerSupplyCapacity;
private String excelAttachmentName;
}

View File

@@ -18,57 +18,63 @@ import java.time.LocalDateTime;
public class TestReportVO { public class TestReportVO {
private static final String DEFAULT_EMPTY_DISPLAY = "-"; private static final String DEFAULT_EMPTY_DISPLAY = "-";
@ApiModelProperty("\u666e\u6d4b\u62a5\u544aID") @ApiModelProperty("普测报告ID")
private String id; private String id;
@ApiModelProperty("\u666e\u6d4b\u62a5\u544a\u7f16\u53f7") @ApiModelProperty("普测报告编号")
private String no; private String no;
@ApiModelProperty("\u59d4\u6258\u5355\u4f4dID") @ApiModelProperty("委托单位ID")
private String clientUnitId; private String clientUnitId;
@ApiModelProperty("\u59d4\u6258\u5355\u4f4d\u540d\u79f0") @ApiModelProperty("委托单位名称")
private String clientUnitName; private String clientUnitName;
@ApiModelProperty("\u62a5\u544a\u6a21\u677fID") @ApiModelProperty("报告模板ID")
private String reportModelId; private String reportModelId;
@ApiModelProperty("\u62a5\u544a\u6a21\u677f\u540d\u79f0") @ApiModelProperty("报告模板名称")
private String reportModelName; private String reportModelName;
@ApiModelProperty("\u68c0\u6d4b\u516c\u53f8ID") @ApiModelProperty("检测公司ID")
private String createUnit; private String createUnit;
@ApiModelProperty("\u5408\u540c\u7f16\u53f7") @ApiModelProperty("合同编号")
private String contractNumber; private String contractNumber;
@ApiModelProperty("\u68c0\u6d4b\u4f9d\u636e") @ApiModelProperty("检测依据")
private String standard; private String standard;
@ApiModelProperty("\u53d7\u8d44\u6570\u636e") @ApiModelProperty("受资数据")
private String data; private String data;
@ApiModelProperty("\u8054\u7cfb\u7535\u8bdd") @ApiModelProperty("联系电话")
private String phonenumber; private String phonenumber;
@ApiModelProperty("\u5ba1\u6838\u4ebaID") @ApiModelProperty("审核人ID")
private String checkId; private String checkId;
@ApiModelProperty("\u5ba1\u6838\u4eba\u540d\u79f0") @ApiModelProperty("审核人名称")
private String checkerName; private String checkerName;
@ApiModelProperty("\u5ba1\u6838\u65f6\u95f4") @ApiModelProperty("审核时间")
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd")
@JsonDeserialize(using = LocalDateDeserializer.class) @JsonDeserialize(using = LocalDateDeserializer.class)
@JsonSerialize(using = LocalDateSerializer.class) @JsonSerialize(using = LocalDateSerializer.class)
private LocalDate checkTime; private LocalDate checkTime;
@ApiModelProperty("\u5ba1\u6838\u610f\u89c1") @ApiModelProperty("审核意见")
private String checkResult; private String checkResult;
@ApiModelProperty("\u62a5\u544a\u72b6\u6001") @ApiModelProperty("报告状态")
private String state; private String state;
@ApiModelProperty("\u903b\u8f91\u72b6\u6001") @ApiModelProperty("报告生成状态")
private Integer reportGenerateState;
@ApiModelProperty("监测点数量")
private Integer pointCount;
@ApiModelProperty("分组数量")
private Integer groupCount;
@ApiModelProperty("逻辑状态")
private Integer status; private Integer status;
@ApiModelProperty("\u521b\u5efa\u4eba\u663e\u793a\u540d") @ApiModelProperty("创建人显示名")
private String createBy; private String createBy;
@ApiModelProperty("\u521b\u5efa\u4eba\u540d\u79f0") @ApiModelProperty("创建人名称")
private String createByName; private String createByName;
@ApiModelProperty("\u521b\u5efa\u65f6\u95f4") @ApiModelProperty("创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class) @JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class) @JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime createTime; private LocalDateTime createTime;
@ApiModelProperty("\u66f4\u65b0\u4eba\u663e\u793a\u540d") @ApiModelProperty("更新人显示名")
private String updateBy; private String updateBy;
@ApiModelProperty("\u66f4\u65b0\u4eba\u540d\u79f0") @ApiModelProperty("更新人名称")
private String updateByName; private String updateByName;
@ApiModelProperty("\u66f4\u65b0\u65f6\u95f4") @ApiModelProperty("更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class) @JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class) @JsonSerialize(using = LocalDateTimeSerializer.class)

View File

@@ -2,10 +2,15 @@ package com.njcn.gather.aireport.testreport.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.gather.aireport.testreport.pojo.param.TestReportGroupSaveParam;
import com.njcn.gather.aireport.testreport.pojo.param.TestReportParam; import com.njcn.gather.aireport.testreport.pojo.param.TestReportParam;
import com.njcn.gather.aireport.testreport.pojo.po.TestReportPO; import com.njcn.gather.aireport.testreport.pojo.po.TestReportPO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportGroupReportVO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportLedgerImportResultVO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportPointVO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO; import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile;
import java.util.List; import java.util.List;
@@ -28,4 +33,20 @@ public interface TestReportService extends IService<TestReportPO> {
void exportTestReports(TestReportParam.QueryParam param); void exportTestReports(TestReportParam.QueryParam param);
ResponseEntity<byte[]> previewTemplate(String id); ResponseEntity<byte[]> previewTemplate(String id);
TestReportLedgerImportResultVO validateLedger(String id, MultipartFile[] files);
TestReportLedgerImportResultVO importLedger(String id, TestReportParam.LedgerImportParam param);
ResponseEntity<byte[]> downloadLedgerTemplate();
List<TestReportPointVO> listPoints(String id);
boolean saveGroups(String id, TestReportGroupSaveParam param);
List<TestReportGroupReportVO> listGroupReports(String id);
boolean generateGroupedReports(String id);
ResponseEntity<byte[]> downloadGroupReport(String groupReportId);
} }

View File

@@ -6,17 +6,30 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.njcn.common.pojo.enums.response.CommonResponseEnum; import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException; import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.reportmodel.component.ReportModelFileStorageService; import com.njcn.gather.aireport.reportmodel.component.ReportModelFileStorageService;
import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO; import com.njcn.gather.aireport.reportmodel.pojo.po.ReportModelPO;
import com.njcn.gather.aireport.reportmodel.service.ReportModelService; import com.njcn.gather.aireport.reportmodel.service.ReportModelService;
import com.njcn.gather.aireport.testreport.component.TestReportPointExcelParser;
import com.njcn.gather.aireport.testreport.component.TestReportLedgerImportService;
import com.njcn.gather.aireport.testreport.component.TestReportLedgerTemplateService;
import com.njcn.gather.aireport.testreport.component.TestReportStorageService;
import com.njcn.gather.aireport.testreport.mapper.TestReportGroupReportMapper;
import com.njcn.gather.aireport.testreport.mapper.TestReportMapper; import com.njcn.gather.aireport.testreport.mapper.TestReportMapper;
import com.njcn.gather.aireport.testreport.mapper.TestReportPointMapper;
import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst; import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst;
import com.njcn.gather.aireport.testreport.pojo.param.TestReportGroupSaveParam;
import com.njcn.gather.aireport.testreport.pojo.param.TestReportParam; import com.njcn.gather.aireport.testreport.pojo.param.TestReportParam;
import com.njcn.gather.aireport.testreport.pojo.po.TestReportGroupReportPO;
import com.njcn.gather.aireport.testreport.pojo.po.TestReportPO; import com.njcn.gather.aireport.testreport.pojo.po.TestReportPO;
import com.njcn.gather.aireport.testreport.pojo.po.TestReportPointPO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportExcelVO; import com.njcn.gather.aireport.testreport.pojo.vo.TestReportExcelVO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportGroupReportVO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportLedgerImportResultVO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportPointVO;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO; import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO;
import com.njcn.gather.aireport.testreport.service.TestReportService; import com.njcn.gather.aireport.testreport.service.TestReportService;
import com.njcn.gather.comservice.filepreview.FilePreviewService; import com.njcn.gather.comservice.filepreview.FilePreviewService;
@@ -30,15 +43,17 @@ import com.njcn.web.factory.PageFactory;
import com.njcn.web.utils.ExcelUtil; import com.njcn.web.utils.ExcelUtil;
import com.njcn.web.utils.RequestUtil; import com.njcn.web.utils.RequestUtil;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
@@ -49,10 +64,13 @@ import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Service @Service
@Slf4j
public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestReportPO> implements TestReportService { 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 DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@@ -88,23 +106,51 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
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_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_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 static final String MSG_ENCODE_FILE_NAME_FAILED = "\u7f16\u7801\u62a5\u544a\u6a21\u677f\u6587\u4ef6\u540d\u5931\u8d25";
private static final String MSG_ONLY_PENDING_CAN_IMPORT = "\u4ec5\u672a\u751f\u6210\u4efb\u52a1\u5141\u8bb8\u5bfc\u5165";
private static final String MSG_IMPORT_FILES_REQUIRED = "\u539f\u59cbExcel\u6587\u4ef6\u4e0d\u80fd\u4e3a\u7a7a";
private static final String MSG_IMPORT_FILE_NAME_DUPLICATE = "\u539f\u59cbExcel\u6587\u4ef6\u540d\u4e0d\u80fd\u91cd\u590d";
private static final String MSG_BATCH_ID_REQUIRED = "\u5bfc\u5165\u6279\u6b21ID\u4e0d\u80fd\u4e3a\u7a7a";
private static final String MSG_GROUPS_REQUIRED = "\u5206\u7ec4\u5217\u8868\u4e0d\u80fd\u4e3a\u7a7a";
private static final String MSG_GROUP_POINT_DUPLICATE = "\u540c\u4e00\u76d1\u6d4b\u70b9\u4e0d\u80fd\u91cd\u590d\u5206\u7ec4";
private static final String MSG_GROUP_POINT_MISSING = "\u6240\u6709\u76d1\u6d4b\u70b9\u90fd\u5fc5\u987b\u5b8c\u6210\u5206\u7ec4";
private static final String MSG_GROUP_NO_INVALID = "\u7ec4\u53f7\u5fc5\u987b\u4e3a\u6b63\u6574\u6570";
private static final String MSG_GROUP_REPORT_NOT_FOUND = "\u5206\u7ec4\u62a5\u544a\u4e0d\u5b58\u5728\u6216\u5df2\u5220\u9664";
private static final String MSG_GROUPS_NOT_READY = "\u8bf7\u5148\u5b8c\u6210\u76d1\u6d4b\u70b9\u5206\u7ec4";
private final ReportModelService reportModelService; private final ReportModelService reportModelService;
private final ReportModelFileStorageService reportModelFileStorageService; private final ReportModelFileStorageService reportModelFileStorageService;
private final FilePreviewService filePreviewService; private final FilePreviewService filePreviewService;
private final IDictTypeService dictTypeService; private final IDictTypeService dictTypeService;
private final IDictDataService dictDataService; private final IDictDataService dictDataService;
private final TestReportPointMapper testReportPointMapper;
private final TestReportGroupReportMapper testReportGroupReportMapper;
private final TestReportPointExcelParser testReportPointExcelParser;
private final TestReportStorageService testReportStorageService;
private final TestReportLedgerImportService testReportLedgerImportService;
private final TestReportLedgerTemplateService testReportLedgerTemplateService;
public TestReportServiceImpl(ReportModelService reportModelService, public TestReportServiceImpl(ReportModelService reportModelService,
ReportModelFileStorageService reportModelFileStorageService, ReportModelFileStorageService reportModelFileStorageService,
FilePreviewService filePreviewService, FilePreviewService filePreviewService,
IDictTypeService dictTypeService, IDictTypeService dictTypeService,
IDictDataService dictDataService) { IDictDataService dictDataService,
TestReportPointMapper testReportPointMapper,
TestReportGroupReportMapper testReportGroupReportMapper,
TestReportPointExcelParser testReportPointExcelParser,
TestReportStorageService testReportStorageService,
TestReportLedgerImportService testReportLedgerImportService,
TestReportLedgerTemplateService testReportLedgerTemplateService) {
this.reportModelService = reportModelService; this.reportModelService = reportModelService;
this.reportModelFileStorageService = reportModelFileStorageService; this.reportModelFileStorageService = reportModelFileStorageService;
this.filePreviewService = filePreviewService; this.filePreviewService = filePreviewService;
this.dictTypeService = dictTypeService; this.dictTypeService = dictTypeService;
this.dictDataService = dictDataService; this.dictDataService = dictDataService;
this.testReportPointMapper = testReportPointMapper;
this.testReportGroupReportMapper = testReportGroupReportMapper;
this.testReportPointExcelParser = testReportPointExcelParser;
this.testReportStorageService = testReportStorageService;
this.testReportLedgerImportService = testReportLedgerImportService;
this.testReportLedgerTemplateService = testReportLedgerTemplateService;
} }
@Override @Override
@@ -130,15 +176,21 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
String userId = resolveCurrentUserId(); String userId = resolveCurrentUserId();
TestReportPO report = new TestReportPO(); TestReportPO report = new TestReportPO();
report.setId(UUID.randomUUID().toString()); report.setId(resolveAddReportId(param));
applyBusinessFields(report, data); applyBusinessFields(report, data);
report.setState(TestReportConst.STATE_EDITING); report.setState(TestReportConst.STATE_EDITING);
report.setReportGenerateState(TestReportConst.REPORT_GENERATE_STATE_PENDING);
report.setStatus(TestReportConst.STATUS_NORMAL); report.setStatus(TestReportConst.STATUS_NORMAL);
report.setCreateBy(userId); report.setCreateBy(userId);
report.setCreateTime(now); report.setCreateTime(now);
report.setUpdateBy(userId); report.setUpdateBy(userId);
report.setUpdateTime(now); report.setUpdateTime(now);
return this.save(report); boolean saved = this.save(report);
if (!saved) {
return false;
}
importLedgerIfNeeded(report, param.getBatchId(), userId);
return true;
} }
@Override @Override
@@ -159,9 +211,15 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
checkStandard(data.getStandard()); checkStandard(data.getStandard());
applyBusinessFields(report, data); applyBusinessFields(report, data);
report.setUpdateBy(resolveCurrentUserId()); String userId = resolveCurrentUserId();
report.setUpdateBy(userId);
report.setUpdateTime(LocalDateTime.now()); report.setUpdateTime(LocalDateTime.now());
return this.updateById(report); boolean updated = this.updateById(report);
if (!updated) {
return false;
}
importLedgerIfNeeded(report, param.getBatchId(), userId);
return true;
} }
@Override @Override
@@ -171,14 +229,28 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
if (validIds.isEmpty()) { if (validIds.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_ID_REQUIRED); throw new BusinessException(CommonResponseEnum.FAIL, MSG_ID_REQUIRED);
} }
return this.lambdaUpdate() List<TestReportPO> reports = this.lambdaQuery()
.in(TestReportPO::getId, validIds)
.eq(TestReportPO::getStatus, TestReportConst.STATUS_NORMAL)
.list();
String userId = resolveCurrentUserId();
LocalDateTime now = LocalDateTime.now();
boolean deleted = this.lambdaUpdate()
.set(TestReportPO::getState, TestReportConst.STATE_DELETED) .set(TestReportPO::getState, TestReportConst.STATE_DELETED)
.set(TestReportPO::getStatus, TestReportConst.STATUS_DELETED) .set(TestReportPO::getStatus, TestReportConst.STATUS_DELETED)
.set(TestReportPO::getUpdateBy, resolveCurrentUserId()) .set(TestReportPO::getUpdateBy, userId)
.set(TestReportPO::getUpdateTime, LocalDateTime.now()) .set(TestReportPO::getUpdateTime, now)
.in(TestReportPO::getId, validIds) .in(TestReportPO::getId, validIds)
.eq(TestReportPO::getStatus, TestReportConst.STATUS_NORMAL) .eq(TestReportPO::getStatus, TestReportConst.STATUS_NORMAL)
.update(); .update();
if (deleted) {
for (TestReportPO report : reports) {
deletePointFiles(loadNormalPoints(report.getId()));
deleteGroupReportFiles(loadNormalGroupReports(report.getId()));
testReportStorageService.deleteQuietly(resolveSummaryStoragePath(report.getData()));
}
}
return deleted;
} }
@Override @Override
@@ -200,6 +272,14 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
.eq(TestReportPO::getStatus, TestReportConst.STATUS_NORMAL) .eq(TestReportPO::getStatus, TestReportConst.STATUS_NORMAL)
.one(); .one();
if (report == null) { if (report == null) {
TestReportPO existingReport = this.getById(normalizedId);
if (existingReport == null) {
log.warn("testreport requireNormal failed: report not found, reportId={}", normalizedId);
} else {
log.warn("testreport requireNormal failed: report status invalid, reportId={}, status={}, state={}, reportGenerateState={}, updateTime={}",
normalizedId, existingReport.getStatus(), existingReport.getState(),
existingReport.getReportGenerateState(), existingReport.getUpdateTime());
}
throw new BusinessException(CommonResponseEnum.FAIL, MSG_REPORT_NOT_FOUND); throw new BusinessException(CommonResponseEnum.FAIL, MSG_REPORT_NOT_FOUND);
} }
return report; return report;
@@ -241,17 +321,135 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
public ResponseEntity<byte[]> previewTemplate(String id) { public ResponseEntity<byte[]> previewTemplate(String id) {
TestReportPO report = requireNormal(id); TestReportPO report = requireNormal(id);
ReportModelPO model = requireReportModel(report.getReportModelId()); ReportModelPO model = requireReportModel(report.getReportModelId());
Path filePath = reportModelFileStorageService.resolveDownloadPath(model.getPath()); String fileName = resolvePreviewFileName(model);
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) { Path tempFile = reportModelFileStorageService.downloadToTempFile(model.getPath(), fileName);
try {
FilePreviewService.PreviewContent previewContent = filePreviewService.preview(tempFile, 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());
} finally {
reportModelFileStorageService.deleteTempFileQuietly(tempFile);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public TestReportLedgerImportResultVO validateLedger(String id, MultipartFile[] files) {
String draftId = requireText(id, MSG_ID_REQUIRED);
String userId = resolveCurrentUserId();
log.info("testreport ledger validate start: draftId={}, userId={}, fileCount={}, fileNames={}",
draftId, userId, files == null ? 0 : files.length, describeUploadedFiles(files));
TestReportPO draftReport = new TestReportPO();
draftReport.setId(draftId);
return testReportLedgerImportService.validateLedger(draftReport, files, userId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public TestReportLedgerImportResultVO importLedger(String id, TestReportParam.LedgerImportParam param) {
TestReportPO report = requireNormal(id);
ensureImportable(report);
if (param == null || StrUtil.isBlank(param.getBatchId())) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_BATCH_ID_REQUIRED);
}
log.info("testreport ledger import start: reportId={}, userId={}, batchId={}",
id, resolveCurrentUserId(), param.getBatchId());
return testReportLedgerImportService.importLedger(report, param.getBatchId(), resolveCurrentUserId());
}
@Override
public ResponseEntity<byte[]> downloadLedgerTemplate() {
byte[] content = testReportLedgerTemplateService.buildTemplate();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename*=UTF-8''" + encodeFileName(TestReportConst.LEDGER_SUMMARY_FILE_NAME))
.contentType(MediaType.parseMediaType(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
.body(content);
}
@Override
public List<TestReportPointVO> listPoints(String id) {
requireNormal(id);
return loadNormalPoints(id).stream().map(this::toPointVO).collect(Collectors.toList());
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean saveGroups(String id, TestReportGroupSaveParam param) {
TestReportPO report = requireNormal(id);
ensureImportable(report);
List<TestReportPointPO> points = loadNormalPoints(id);
validateGroupingPayload(points, param);
String userId = resolveCurrentUserId();
LocalDateTime now = LocalDateTime.now();
Map<String, Integer> pointGroupMap = buildPointGroupMap(param);
for (TestReportPointPO point : points) {
point.setGroupNo(pointGroupMap.get(point.getId()));
point.setUpdateBy(userId);
point.setUpdateTime(now);
testReportPointMapper.updateById(point);
}
rebuildGroupReports(report, param, userId, now);
report.setData(updateImportSnapshotGroupCount(report.getData(), param.getGroups().size()));
report.setUpdateBy(userId);
report.setUpdateTime(now);
return this.updateById(report);
}
@Override
public List<TestReportGroupReportVO> listGroupReports(String id) {
requireNormal(id);
return loadNormalGroupReports(id).stream().map(this::toGroupReportVO).collect(Collectors.toList());
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean generateGroupedReports(String id) {
TestReportPO report = requireNormal(id);
ensureImportable(report);
List<TestReportGroupReportPO> groupReports = loadNormalGroupReports(id);
if (groupReports.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_GROUPS_NOT_READY);
}
report.setReportGenerateState(TestReportConst.REPORT_GENERATE_STATE_GENERATING);
report.setUpdateBy(resolveCurrentUserId());
report.setUpdateTime(LocalDateTime.now());
this.updateById(report);
try {
for (TestReportGroupReportPO groupReport : groupReports) {
generateSingleGroupReport(report, groupReport);
}
report.setReportGenerateState(TestReportConst.REPORT_GENERATE_STATE_FINISHED);
report.setUpdateTime(LocalDateTime.now());
return this.updateById(report);
} catch (RuntimeException exception) {
report.setReportGenerateState(TestReportConst.REPORT_GENERATE_STATE_PENDING);
report.setUpdateTime(LocalDateTime.now());
this.updateById(report);
throw exception;
}
}
@Override
public ResponseEntity<byte[]> downloadGroupReport(String groupReportId) {
TestReportGroupReportPO groupReport = requireNormalGroupReport(groupReportId);
if (StrUtil.isBlank(groupReport.getReportStoragePath())) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_TEMPLATE_FILE_NOT_FOUND);
}
try {
byte[] bytes = testReportStorageService.readBusinessBytes(groupReport.getReportStoragePath());
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename*=UTF-8''" + encodeFileName(groupReport.getReportFileName()))
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(bytes);
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_TEMPLATE_FILE_NOT_FOUND); 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) { private TestReportParam.QueryParam normalizeQueryParam(TestReportParam.QueryParam param) {
@@ -276,7 +474,7 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
data.setReportModelId(requireText(param.getReportModelId(), MSG_REPORT_MODEL_REQUIRED)); data.setReportModelId(requireText(param.getReportModelId(), MSG_REPORT_MODEL_REQUIRED));
data.setCreateUnit(requireText(param.getCreateUnit(), MSG_CREATE_UNIT_REQUIRED)); data.setCreateUnit(requireText(param.getCreateUnit(), MSG_CREATE_UNIT_REQUIRED));
data.setContractNumber(requireText(param.getContractNumber(), MSG_CONTRACT_REQUIRED, 32, MSG_CONTRACT_TOO_LONG)); data.setContractNumber(requireText(param.getContractNumber(), MSG_CONTRACT_REQUIRED, 32, MSG_CONTRACT_TOO_LONG));
data.setStandard(requireText(param.getStandard(), MSG_STANDARD_REQUIRED)); data.setStandard(requireJsonArrayText(param.getStandard(), MSG_STANDARD_REQUIRED, MSG_STANDARD_INVALID));
data.setData(requireJsonObjectOrArrayText(param.getData(), MSG_DATA_REQUIRED, MSG_DATA_INVALID)); data.setData(requireJsonObjectOrArrayText(param.getData(), MSG_DATA_REQUIRED, MSG_DATA_INVALID));
data.setPhonenumber(trimToNull(param.getPhonenumber())); data.setPhonenumber(trimToNull(param.getPhonenumber()));
if (data.getPhonenumber() != null && data.getPhonenumber().length() > 32) { if (data.getPhonenumber() != null && data.getPhonenumber().length() > 32) {
@@ -296,10 +494,328 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
report.setPhonenumber(data.getPhonenumber()); report.setPhonenumber(data.getPhonenumber());
} }
private String resolveAddReportId(TestReportParam.AddParam param) {
String frontendId = param == null ? null : trimToNull(param.getId());
return frontendId == null ? UUID.randomUUID().toString() : frontendId;
}
private boolean allowEdit(String state) { private boolean allowEdit(String state) {
return TestReportConst.STATE_EDITING.equals(state) || TestReportConst.STATE_REJECTED.equals(state); return TestReportConst.STATE_EDITING.equals(state) || TestReportConst.STATE_REJECTED.equals(state);
} }
private void ensureImportable(TestReportPO report) {
Integer reportGenerateState = report.getReportGenerateState();
if (reportGenerateState != null && !TestReportConst.REPORT_GENERATE_STATE_PENDING.equals(reportGenerateState)) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_ONLY_PENDING_CAN_IMPORT);
}
}
private List<MultipartFile> normalizeImportFiles(MultipartFile[] files) {
if (files == null || files.length == 0) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_IMPORT_FILES_REQUIRED);
}
List<MultipartFile> result = new ArrayList<MultipartFile>();
Set<String> fileNames = new HashSet<String>();
for (MultipartFile file : files) {
if (file == null || file.isEmpty()) {
continue;
}
String originalFileName = trimToNull(file.getOriginalFilename());
if (originalFileName == null) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_IMPORT_FILES_REQUIRED);
}
String normalizedFileName = originalFileName.toLowerCase();
if (!normalizedFileName.endsWith(".xlsx")) {
throw new BusinessException(CommonResponseEnum.FAIL, "原始Excel仅允许xlsx格式");
}
if (!fileNames.add(originalFileName)) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_IMPORT_FILE_NAME_DUPLICATE);
}
result.add(file);
}
if (result.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_IMPORT_FILES_REQUIRED);
}
return result;
}
private List<TestReportPointExcelParser.ParsedPoint> parseAllPoints(List<MultipartFile> files) {
List<TestReportPointExcelParser.ParsedPoint> result = new ArrayList<TestReportPointExcelParser.ParsedPoint>();
for (MultipartFile file : files) {
try {
result.add(testReportPointExcelParser.parse(file.getOriginalFilename(), file.getInputStream()));
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "读取原始Excel失败");
}
}
return result;
}
private void overwriteReportPoints(TestReportPO report, List<MultipartFile> files,
List<TestReportPointExcelParser.ParsedPoint> parsedPoints) {
List<TestReportPointPO> oldPoints = loadNormalPoints(report.getId());
List<TestReportGroupReportPO> oldGroupReports = loadNormalGroupReports(report.getId());
deletePointFiles(oldPoints);
deleteGroupReportFiles(oldGroupReports);
removeOldRows(report.getId());
LocalDateTime now = LocalDateTime.now();
String userId = resolveCurrentUserId();
for (int i = 0; i < files.size(); i++) {
MultipartFile file = files.get(i);
TestReportPointExcelParser.ParsedPoint parsedPoint = parsedPoints.get(i);
String storagePath = testReportStorageService.saveSourceExcel(report.getId(), file);
TestReportPointPO point = new TestReportPointPO();
point.setId(UUID.randomUUID().toString());
point.setReportId(report.getId());
point.setGroupNo(null);
point.setSubstationName(parsedPoint.getSubstationName());
point.setPointName(parsedPoint.getPointName());
point.setMonitorTime(parsedPoint.getMonitorTime());
point.setVoltageLevel(parsedPoint.getVoltageLevel());
point.setPtRatio(parsedPoint.getPtRatio());
point.setCtRatio(parsedPoint.getCtRatio());
point.setBaseShortCircuitCapacity(parsedPoint.getBaseShortCircuitCapacity());
point.setMinShortCircuitCapacity(parsedPoint.getMinShortCircuitCapacity());
point.setProtocolCapacity(parsedPoint.getProtocolCapacity());
point.setPowerSupplyCapacity(parsedPoint.getPowerSupplyCapacity());
point.setExcelAttachmentName(parsedPoint.getExcelAttachmentName());
point.setExcelStoragePath(storagePath);
point.setSortNo(i + 1);
point.setDeleted(TestReportConst.DELETED_NO);
point.setCreateBy(userId);
point.setCreateTime(now);
point.setUpdateBy(userId);
point.setUpdateTime(now);
testReportPointMapper.insert(point);
}
report.setData(buildImportSnapshotJson(parsedPoints.size(), 0));
report.setReportGenerateState(TestReportConst.REPORT_GENERATE_STATE_PENDING);
report.setUpdateBy(userId);
report.setUpdateTime(now);
this.updateById(report);
}
private void deletePointFiles(List<TestReportPointPO> oldPoints) {
for (TestReportPointPO oldPoint : oldPoints) {
testReportStorageService.deleteQuietly(oldPoint.getExcelStoragePath());
}
}
private void deleteGroupReportFiles(List<TestReportGroupReportPO> oldGroupReports) {
for (TestReportGroupReportPO oldGroupReport : oldGroupReports) {
testReportStorageService.deleteQuietly(oldGroupReport.getReportStoragePath());
}
}
private String resolveSummaryStoragePath(String data) {
if (StrUtil.isBlank(data)) {
return null;
}
try {
String summaryStoragePath = OBJECT_MAPPER.readTree(data).path("summaryStoragePath").asText();
return StrUtil.isBlank(summaryStoragePath) ? null : summaryStoragePath;
} catch (Exception exception) {
return null;
}
}
private void removeOldRows(String reportId) {
LambdaQueryWrapper<TestReportPointPO> pointWrapper = new LambdaQueryWrapper<TestReportPointPO>();
pointWrapper.eq(TestReportPointPO::getReportId, reportId);
testReportPointMapper.delete(pointWrapper);
LambdaQueryWrapper<TestReportGroupReportPO> groupReportWrapper = new LambdaQueryWrapper<TestReportGroupReportPO>();
groupReportWrapper.eq(TestReportGroupReportPO::getReportId, reportId);
testReportGroupReportMapper.delete(groupReportWrapper);
}
private String buildImportSnapshotJson(int pointCount, int groupCount) {
ObjectNode root = OBJECT_MAPPER.createObjectNode();
root.put("version", "1.0");
root.put("importMode", "multi-source-excel-auto-parse");
root.put("fileCount", pointCount);
root.put("pointCount", pointCount);
root.put("groupCount", groupCount);
return root.toString();
}
private String updateImportSnapshotGroupCount(String data, int groupCount) {
try {
JsonNode jsonNode = StrUtil.isBlank(data) ? OBJECT_MAPPER.createObjectNode() : OBJECT_MAPPER.readTree(data);
ObjectNode root = jsonNode instanceof ObjectNode ? (ObjectNode) jsonNode : OBJECT_MAPPER.createObjectNode();
if (!root.has("version")) {
root.put("version", "1.0");
}
if (!root.has("importMode")) {
root.put("importMode", "multi-source-excel-auto-parse");
}
root.put("groupCount", groupCount);
return root.toString();
} catch (Exception exception) {
return buildImportSnapshotJson(loadNormalPointsCountFromData(data), groupCount);
}
}
private int loadNormalPointsCountFromData(String data) {
try {
if (StrUtil.isBlank(data)) {
return 0;
}
JsonNode jsonNode = OBJECT_MAPPER.readTree(data);
JsonNode pointCountNode = jsonNode.get("pointCount");
return pointCountNode == null ? 0 : pointCountNode.asInt(0);
} catch (Exception exception) {
return 0;
}
}
private List<TestReportPointPO> loadNormalPoints(String reportId) {
LambdaQueryWrapper<TestReportPointPO> wrapper = new LambdaQueryWrapper<TestReportPointPO>();
wrapper.eq(TestReportPointPO::getReportId, reportId)
.eq(TestReportPointPO::getDeleted, TestReportConst.DELETED_NO)
.orderByAsc(TestReportPointPO::getSortNo)
.orderByAsc(TestReportPointPO::getCreateTime);
return testReportPointMapper.selectList(wrapper);
}
private List<TestReportGroupReportPO> loadNormalGroupReports(String reportId) {
LambdaQueryWrapper<TestReportGroupReportPO> wrapper = new LambdaQueryWrapper<TestReportGroupReportPO>();
wrapper.eq(TestReportGroupReportPO::getReportId, reportId)
.eq(TestReportGroupReportPO::getDeleted, TestReportConst.DELETED_NO)
.orderByAsc(TestReportGroupReportPO::getSortNo)
.orderByAsc(TestReportGroupReportPO::getGroupNo);
return testReportGroupReportMapper.selectList(wrapper);
}
private TestReportGroupReportPO requireNormalGroupReport(String groupReportId) {
LambdaQueryWrapper<TestReportGroupReportPO> wrapper = new LambdaQueryWrapper<TestReportGroupReportPO>();
wrapper.eq(TestReportGroupReportPO::getId, requireText(groupReportId, MSG_ID_REQUIRED))
.eq(TestReportGroupReportPO::getDeleted, TestReportConst.DELETED_NO);
TestReportGroupReportPO groupReport = testReportGroupReportMapper.selectOne(wrapper);
if (groupReport == null) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_GROUP_REPORT_NOT_FOUND);
}
return groupReport;
}
private void validateGroupingPayload(List<TestReportPointPO> points, TestReportGroupSaveParam param) {
if (param == null || param.getGroups() == null || param.getGroups().isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_GROUPS_REQUIRED);
}
Set<String> pointIds = points.stream().map(TestReportPointPO::getId).collect(Collectors.toSet());
Set<String> payloadPointIds = new HashSet<String>();
for (TestReportGroupSaveParam.GroupItem group : param.getGroups()) {
if (group.getGroupNo() == null || group.getGroupNo() <= 0) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_GROUP_NO_INVALID);
}
for (String pointId : group.getPointIds()) {
if (!payloadPointIds.add(pointId)) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_GROUP_POINT_DUPLICATE);
}
}
}
if (!payloadPointIds.equals(pointIds)) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_GROUP_POINT_MISSING);
}
}
private Map<String, Integer> buildPointGroupMap(TestReportGroupSaveParam param) {
Map<String, Integer> result = new LinkedHashMap<String, Integer>();
for (TestReportGroupSaveParam.GroupItem group : param.getGroups()) {
for (String pointId : group.getPointIds()) {
result.put(pointId, group.getGroupNo());
}
}
return result;
}
private void rebuildGroupReports(TestReportPO report, TestReportGroupSaveParam param, String userId, LocalDateTime now) {
deleteGroupReportFiles(loadNormalGroupReports(report.getId()));
LambdaQueryWrapper<TestReportGroupReportPO> wrapper = new LambdaQueryWrapper<TestReportGroupReportPO>();
wrapper.eq(TestReportGroupReportPO::getReportId, report.getId());
testReportGroupReportMapper.delete(wrapper);
int sortNo = 1;
for (TestReportGroupSaveParam.GroupItem group : param.getGroups()) {
TestReportGroupReportPO groupReport = new TestReportGroupReportPO();
groupReport.setId(UUID.randomUUID().toString());
groupReport.setReportId(report.getId());
groupReport.setGroupNo(group.getGroupNo());
groupReport.setGenerateState(TestReportConst.GROUP_REPORT_GENERATE_STATE_PENDING);
groupReport.setSortNo(sortNo++);
groupReport.setDeleted(TestReportConst.DELETED_NO);
groupReport.setCreateBy(userId);
groupReport.setCreateTime(now);
groupReport.setUpdateBy(userId);
groupReport.setUpdateTime(now);
testReportGroupReportMapper.insert(groupReport);
}
}
private void generateSingleGroupReport(TestReportPO report, TestReportGroupReportPO groupReport) {
try {
groupReport.setGenerateState(TestReportConst.GROUP_REPORT_GENERATE_STATE_GENERATING);
groupReport.setGenerateMessage(null);
groupReport.setUpdateBy(resolveCurrentUserId());
groupReport.setUpdateTime(LocalDateTime.now());
testReportGroupReportMapper.updateById(groupReport);
String reportName = report.getNo() + "-第" + groupReport.getGroupNo() + "组检测报告";
String reportFileName = ExportFileNameUtil.appendToday(reportName + ".docx");
String relativePath = testReportStorageService.saveGeneratedPlaceholder(report.getId(), reportFileName,
("group:" + groupReport.getGroupNo()).getBytes(StandardCharsets.UTF_8));
groupReport.setReportName(reportName);
groupReport.setReportFileName(reportFileName);
groupReport.setReportStoragePath(relativePath);
groupReport.setGenerateState(TestReportConst.GROUP_REPORT_GENERATE_STATE_SUCCESS);
groupReport.setGenerateTime(LocalDateTime.now());
groupReport.setUpdateTime(LocalDateTime.now());
testReportGroupReportMapper.updateById(groupReport);
} catch (RuntimeException exception) {
groupReport.setGenerateState(TestReportConst.GROUP_REPORT_GENERATE_STATE_FAILED);
groupReport.setGenerateMessage(exception.getMessage());
groupReport.setUpdateTime(LocalDateTime.now());
testReportGroupReportMapper.updateById(groupReport);
throw exception;
}
}
private TestReportPointVO toPointVO(TestReportPointPO point) {
TestReportPointVO vo = new TestReportPointVO();
vo.setId(point.getId());
vo.setGroupNo(point.getGroupNo());
vo.setSubstationName(defaultDash(point.getSubstationName()));
vo.setPointName(defaultDash(point.getPointName()));
vo.setMonitorTime(defaultDash(point.getMonitorTime()));
vo.setVoltageLevel(defaultDash(point.getVoltageLevel()));
vo.setPtRatio(defaultDash(point.getPtRatio()));
vo.setCtRatio(defaultDash(point.getCtRatio()));
vo.setBaseShortCircuitCapacity(defaultDash(point.getBaseShortCircuitCapacity()));
vo.setMinShortCircuitCapacity(defaultDash(point.getMinShortCircuitCapacity()));
vo.setProtocolCapacity(defaultDash(point.getProtocolCapacity()));
vo.setPowerSupplyCapacity(defaultDash(point.getPowerSupplyCapacity()));
vo.setExcelAttachmentName(defaultDash(point.getExcelAttachmentName()));
return vo;
}
private TestReportGroupReportVO toGroupReportVO(TestReportGroupReportPO groupReport) {
TestReportGroupReportVO vo = new TestReportGroupReportVO();
vo.setId(groupReport.getId());
vo.setGroupNo(groupReport.getGroupNo());
vo.setReportName(defaultDash(groupReport.getReportName()));
vo.setReportFileName(defaultDash(groupReport.getReportFileName()));
vo.setReportStoragePath(defaultDash(groupReport.getReportStoragePath()));
vo.setGenerateState(groupReport.getGenerateState());
vo.setGenerateMessage(defaultDash(groupReport.getGenerateMessage()));
vo.setGenerateTime(groupReport.getGenerateTime());
vo.setSortNo(groupReport.getSortNo());
return vo;
}
private String defaultDash(String value) {
return StrUtil.isBlank(value) ? "-" : value;
}
private void checkNoUnique(String no, String excludeId) { private void checkNoUnique(String no, String excludeId) {
LambdaQueryWrapper<TestReportPO> wrapper = new LambdaQueryWrapper<TestReportPO>(); LambdaQueryWrapper<TestReportPO> wrapper = new LambdaQueryWrapper<TestReportPO>();
wrapper.eq(TestReportPO::getNo, no) wrapper.eq(TestReportPO::getNo, no)
@@ -479,14 +995,38 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
} }
} }
private String requireJsonObjectOrArrayText(String value, String blankMessage, String invalidMessage) { private String requireJsonArrayText(List<String> value, String blankMessage, String invalidMessage) {
String text = requireText(value, blankMessage); if (value == null || value.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, blankMessage);
}
try { try {
JsonNode jsonNode = OBJECT_MAPPER.readTree(text); ArrayList<String> normalizedItems = new ArrayList<String>();
if (jsonNode == null || !(jsonNode.isObject() || jsonNode.isArray())) { for (String item : value) {
String normalizedItem = trimToNull(item);
if (normalizedItem != null) {
normalizedItems.add(normalizedItem);
}
}
if (normalizedItems.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, invalidMessage); throw new BusinessException(CommonResponseEnum.FAIL, invalidMessage);
} }
return text; return OBJECT_MAPPER.writeValueAsString(normalizedItems);
} catch (BusinessException exception) {
throw exception;
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, invalidMessage);
}
}
private String requireJsonObjectOrArrayText(JsonNode value, String blankMessage, String invalidMessage) {
if (value == null || value.isNull()) {
throw new BusinessException(CommonResponseEnum.FAIL, blankMessage);
}
try {
if (!(value.isObject() || value.isArray())) {
throw new BusinessException(CommonResponseEnum.FAIL, invalidMessage);
}
return OBJECT_MAPPER.writeValueAsString(value);
} catch (BusinessException exception) { } catch (BusinessException exception) {
throw exception; throw exception;
} catch (Exception exception) { } catch (Exception exception) {
@@ -588,6 +1128,29 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
return StrUtil.isBlank(userId) ? null : userId; return StrUtil.isBlank(userId) ? null : userId;
} }
private List<String> describeUploadedFiles(MultipartFile[] files) {
if (files == null || files.length == 0) {
return Collections.emptyList();
}
List<String> fileNames = new ArrayList<String>();
for (MultipartFile file : files) {
if (file == null) {
fileNames.add("null");
continue;
}
String fileName = trimToNull(file.getOriginalFilename());
fileNames.add(fileName == null ? "(blank)" : fileName);
}
return fileNames;
}
private void importLedgerIfNeeded(TestReportPO report, String batchId, String userId) {
if (report == null || StrUtil.isBlank(batchId)) {
return;
}
testReportLedgerImportService.importLedger(report, batchId, userId);
}
private String trimToNull(String value) { private String trimToNull(String value) {
if (value == null) { if (value == null) {
return null; return null;

View File

@@ -0,0 +1,4 @@
<?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.testreport.mapper.TestReportGroupReportMapper">
</mapper>

View File

@@ -0,0 +1,4 @@
<?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.testreport.mapper.TestReportPointMapper">
</mapper>

View File

@@ -0,0 +1,234 @@
package com.njcn.gather.aireport.testreport.component;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst;
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.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayOutputStream;
import java.util.LinkedHashMap;
import java.util.Map;
class TestReportLedgerExcelParserTest {
@Test
void shouldParseValidLedgerWorkbook() throws Exception {
TestReportLedgerExcelParser parser = new TestReportLedgerExcelParser();
MockMultipartFile summaryFile = new MockMultipartFile("files", TestReportConst.LEDGER_SUMMARY_FILE_NAME,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buildWorkbookBytes(true));
Map<String, MultipartFile> attachments = new LinkedHashMap<String, MultipartFile>();
attachments.put("point-a.xlsx", new MockMultipartFile("files", "point-a.xlsx", "application/octet-stream", new byte[]{1}));
attachments.put("point-a.docx", new MockMultipartFile("files", "point-a.docx", "application/octet-stream", new byte[]{1}));
TestReportLedgerExcelParser.ParsedLedger ledger = parser.parse(summaryFile, attachments);
Assertions.assertEquals(1, ledger.getPoints().size());
Assertions.assertEquals(1, ledger.getDevices().size());
Assertions.assertEquals(1, ledger.getClients().size());
Assertions.assertEquals("点位A", ledger.getPoints().get(0).getPointName());
}
@Test
void shouldAllowSummaryFileNameWithSurroundingSpaces() throws Exception {
TestReportLedgerExcelParser parser = new TestReportLedgerExcelParser();
MockMultipartFile summaryFile = new MockMultipartFile("files", " " + TestReportConst.LEDGER_SUMMARY_FILE_NAME + " ",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buildWorkbookBytes(true));
Map<String, MultipartFile> attachments = new LinkedHashMap<String, MultipartFile>();
attachments.put("point-a.xlsx", new MockMultipartFile("files", "point-a.xlsx", "application/octet-stream", new byte[]{1}));
attachments.put("point-a.docx", new MockMultipartFile("files", "point-a.docx", "application/octet-stream", new byte[]{1}));
TestReportLedgerExcelParser.ParsedLedger ledger = parser.parse(summaryFile, attachments);
Assertions.assertEquals(TestReportConst.LEDGER_SUMMARY_FILE_NAME, ledger.getSummaryFileName());
Assertions.assertEquals(1, ledger.getPoints().size());
}
@Test
void shouldRejectMissingPointSheet() throws Exception {
TestReportLedgerExcelParser parser = new TestReportLedgerExcelParser();
MockMultipartFile summaryFile = new MockMultipartFile("files", TestReportConst.LEDGER_SUMMARY_FILE_NAME,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buildWorkbookBytes(false));
BusinessException exception = Assertions.assertThrows(BusinessException.class,
() -> parser.parse(summaryFile, new LinkedHashMap<String, MultipartFile>()));
Assertions.assertTrue(exception.getMessage().contains("监测点信息"));
}
@Test
void shouldRejectDuplicatePointBySubstationAndPointName() throws Exception {
TestReportLedgerExcelParser parser = new TestReportLedgerExcelParser();
MockMultipartFile summaryFile = new MockMultipartFile("files", TestReportConst.LEDGER_SUMMARY_FILE_NAME,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buildDuplicatePointWorkbookBytes());
Map<String, MultipartFile> attachments = new LinkedHashMap<String, MultipartFile>();
attachments.put("point-a.xlsx", new MockMultipartFile("files", "point-a.xlsx", "application/octet-stream", new byte[]{1}));
attachments.put("point-a.docx", new MockMultipartFile("files", "point-a.docx", "application/octet-stream", new byte[]{1}));
attachments.put("point-b.xlsx", new MockMultipartFile("files", "point-b.xlsx", "application/octet-stream", new byte[]{1}));
attachments.put("point-b.docx", new MockMultipartFile("files", "point-b.docx", "application/octet-stream", new byte[]{1}));
BusinessException exception = Assertions.assertThrows(BusinessException.class,
() -> parser.parse(summaryFile, attachments));
Assertions.assertTrue(exception.getMessage().contains("重复点位"));
}
@Test
void shouldAllowMissingOptionalSheets() throws Exception {
TestReportLedgerExcelParser parser = new TestReportLedgerExcelParser();
MockMultipartFile summaryFile = new MockMultipartFile("files", TestReportConst.LEDGER_SUMMARY_FILE_NAME,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
buildWorkbookBytes(true, false, false, false));
Map<String, MultipartFile> attachments = new LinkedHashMap<String, MultipartFile>();
attachments.put("point-a.xlsx", new MockMultipartFile("files", "point-a.xlsx", "application/octet-stream", new byte[]{1}));
attachments.put("point-a.docx", new MockMultipartFile("files", "point-a.docx", "application/octet-stream", new byte[]{1}));
TestReportLedgerExcelParser.ParsedLedger ledger = parser.parse(summaryFile, attachments);
Assertions.assertEquals(1, ledger.getPoints().size());
Assertions.assertTrue(ledger.getDevices().isEmpty());
Assertions.assertTrue(ledger.getClients().isEmpty());
Assertions.assertTrue(ledger.getMissingSheets().isEmpty());
}
@Test
void shouldAllowBlankOptionalSheetContent() throws Exception {
TestReportLedgerExcelParser parser = new TestReportLedgerExcelParser();
MockMultipartFile summaryFile = new MockMultipartFile("files", TestReportConst.LEDGER_SUMMARY_FILE_NAME,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buildWorkbookWithBlankOptionalRows());
Map<String, MultipartFile> attachments = new LinkedHashMap<String, MultipartFile>();
attachments.put("point-a.xlsx", new MockMultipartFile("files", "point-a.xlsx", "application/octet-stream", new byte[]{1}));
attachments.put("point-a.docx", new MockMultipartFile("files", "point-a.docx", "application/octet-stream", new byte[]{1}));
TestReportLedgerExcelParser.ParsedLedger ledger = parser.parse(summaryFile, attachments);
Assertions.assertEquals(1, ledger.getPoints().size());
Assertions.assertTrue(ledger.getDevices().isEmpty());
Assertions.assertTrue(ledger.getClients().isEmpty());
}
private byte[] buildWorkbookBytes(boolean includePointSheet) throws Exception {
return buildWorkbookBytes(includePointSheet, true, true, true);
}
private byte[] buildWorkbookBytes(boolean includePointSheet, boolean includeGuideSheet,
boolean includeDeviceSheet, boolean includeClientSheet) throws Exception {
try (XSSFWorkbook workbook = new XSSFWorkbook(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
if (includeGuideSheet) {
workbook.createSheet(TestReportConst.LEDGER_GUIDE_SHEET_NAME);
}
if (includePointSheet) {
XSSFSheet pointSheet = workbook.createSheet(TestReportConst.LEDGER_POINT_SHEET_NAME);
pointSheet.createRow(0).createCell(0).setCellValue("分组号");
pointSheet.getRow(0).createCell(1).setCellValue("变电站");
pointSheet.getRow(0).createCell(2).setCellValue("监测点名称");
pointSheet.getRow(0).createCell(3).setCellValue("检测设备编号");
pointSheet.getRow(0).createCell(4).setCellValue("委托单位名称");
pointSheet.getRow(0).createCell(5).setCellValue("Excel附件名称");
pointSheet.getRow(0).createCell(6).setCellValue("Word附件名称");
pointSheet.createRow(1).createCell(0).setCellValue("1");
pointSheet.getRow(1).createCell(1).setCellValue("站点A");
pointSheet.getRow(1).createCell(2).setCellValue("点位A");
pointSheet.getRow(1).createCell(3).setCellValue(includeDeviceSheet ? "DEV-001" : "");
pointSheet.getRow(1).createCell(4).setCellValue(includeClientSheet ? "单位A" : "");
pointSheet.getRow(1).createCell(5).setCellValue("point-a.xlsx");
pointSheet.getRow(1).createCell(6).setCellValue("point-a.docx");
}
if (includeDeviceSheet) {
XSSFSheet deviceSheet = workbook.createSheet(TestReportConst.LEDGER_DEVICE_SHEET_NAME);
deviceSheet.createRow(0).createCell(0).setCellValue("设备型号");
deviceSheet.getRow(0).createCell(1).setCellValue("设备编号");
deviceSheet.getRow(0).createCell(2).setCellValue("标准有效期");
deviceSheet.getRow(0).createCell(3).setCellValue("设备状态");
deviceSheet.getRow(0).createCell(4).setCellValue("标准报告文件名");
deviceSheet.createRow(1).createCell(0).setCellValue("型号A");
deviceSheet.getRow(1).createCell(1).setCellValue("DEV-001");
deviceSheet.getRow(1).createCell(2).setCellValue("2026-07-01");
deviceSheet.getRow(1).createCell(3).setCellValue("在运");
deviceSheet.getRow(1).createCell(4).setCellValue("report-a.pdf");
}
if (includeClientSheet) {
XSSFSheet clientSheet = workbook.createSheet(TestReportConst.LEDGER_CLIENT_SHEET_NAME);
clientSheet.createRow(0).createCell(0).setCellValue("单位名称");
clientSheet.getRow(0).createCell(1).setCellValue("联系人");
clientSheet.getRow(0).createCell(2).setCellValue("联系方式");
clientSheet.getRow(0).createCell(3).setCellValue("地址");
clientSheet.createRow(1).createCell(0).setCellValue("单位A");
clientSheet.getRow(1).createCell(1).setCellValue("张三");
clientSheet.getRow(1).createCell(2).setCellValue("13800000000");
clientSheet.getRow(1).createCell(3).setCellValue("南京");
}
workbook.write(outputStream);
return outputStream.toByteArray();
}
}
private byte[] buildDuplicatePointWorkbookBytes() throws Exception {
try (XSSFWorkbook workbook = new XSSFWorkbook(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
workbook.createSheet(TestReportConst.LEDGER_GUIDE_SHEET_NAME);
XSSFSheet pointSheet = workbook.createSheet(TestReportConst.LEDGER_POINT_SHEET_NAME);
pointSheet.createRow(0).createCell(0).setCellValue("分组号");
pointSheet.getRow(0).createCell(1).setCellValue("变电站");
pointSheet.getRow(0).createCell(2).setCellValue("监测点名称");
pointSheet.getRow(0).createCell(3).setCellValue("检测设备编号");
pointSheet.getRow(0).createCell(4).setCellValue("委托单位名称");
pointSheet.getRow(0).createCell(5).setCellValue("Excel附件名称");
pointSheet.getRow(0).createCell(6).setCellValue("Word附件名称");
pointSheet.createRow(1).createCell(0).setCellValue("1");
pointSheet.getRow(1).createCell(1).setCellValue("站点A");
pointSheet.getRow(1).createCell(2).setCellValue("点位A");
pointSheet.getRow(1).createCell(3).setCellValue("DEV-001");
pointSheet.getRow(1).createCell(4).setCellValue("单位A");
pointSheet.getRow(1).createCell(5).setCellValue("point-a.xlsx");
pointSheet.getRow(1).createCell(6).setCellValue("point-a.docx");
pointSheet.createRow(2).createCell(0).setCellValue("2");
pointSheet.getRow(2).createCell(1).setCellValue("站点A");
pointSheet.getRow(2).createCell(2).setCellValue("点位A");
pointSheet.getRow(2).createCell(3).setCellValue("DEV-002");
pointSheet.getRow(2).createCell(4).setCellValue("单位B");
pointSheet.getRow(2).createCell(5).setCellValue("point-b.xlsx");
pointSheet.getRow(2).createCell(6).setCellValue("point-b.docx");
workbook.write(outputStream);
return outputStream.toByteArray();
}
}
private byte[] buildWorkbookWithBlankOptionalRows() throws Exception {
try (XSSFWorkbook workbook = new XSSFWorkbook(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
XSSFSheet pointSheet = workbook.createSheet(TestReportConst.LEDGER_POINT_SHEET_NAME);
pointSheet.createRow(0).createCell(0).setCellValue("分组号");
pointSheet.getRow(0).createCell(1).setCellValue("变电站");
pointSheet.getRow(0).createCell(2).setCellValue("监测点名称");
pointSheet.getRow(0).createCell(3).setCellValue("检测设备编号");
pointSheet.getRow(0).createCell(4).setCellValue("委托单位名称");
pointSheet.getRow(0).createCell(5).setCellValue("Excel附件名称");
pointSheet.getRow(0).createCell(6).setCellValue("Word附件名称");
pointSheet.createRow(1).createCell(0).setCellValue("1");
pointSheet.getRow(1).createCell(1).setCellValue("站点A");
pointSheet.getRow(1).createCell(2).setCellValue("点位A");
pointSheet.getRow(1).createCell(5).setCellValue("point-a.xlsx");
pointSheet.getRow(1).createCell(6).setCellValue("point-a.docx");
XSSFSheet deviceSheet = workbook.createSheet(TestReportConst.LEDGER_DEVICE_SHEET_NAME);
deviceSheet.createRow(0).createCell(0).setCellValue("设备型号");
deviceSheet.getRow(0).createCell(1).setCellValue("设备编号");
deviceSheet.getRow(0).createCell(2).setCellValue("标准有效期");
deviceSheet.getRow(0).createCell(3).setCellValue("设备状态");
deviceSheet.getRow(0).createCell(4).setCellValue("标准报告文件名");
deviceSheet.createRow(1).createCell(0).setCellValue("型号A");
XSSFSheet clientSheet = workbook.createSheet(TestReportConst.LEDGER_CLIENT_SHEET_NAME);
clientSheet.createRow(0).createCell(0).setCellValue("单位名称");
clientSheet.getRow(0).createCell(1).setCellValue("联系人");
clientSheet.getRow(0).createCell(2).setCellValue("联系方式");
clientSheet.getRow(0).createCell(3).setCellValue("地址");
clientSheet.createRow(1).createCell(1).setCellValue("张三");
workbook.write(outputStream);
return outputStream.toByteArray();
}
}
}

View File

@@ -0,0 +1,151 @@
//package com.njcn.gather.aireport.testreport.component;
//
//import com.njcn.common.pojo.exception.BusinessException;
//import com.njcn.gather.aireport.clientunit.mapper.ClientUnitMapper;
//import com.njcn.gather.aireport.clientunit.pojo.po.ClientUnitPO;
//import com.njcn.gather.aireport.testdevice.mapper.TestDeviceMapper;
//import com.njcn.gather.aireport.testdevice.pojo.constant.TestDeviceConst;
//import com.njcn.gather.aireport.testdevice.pojo.po.TestDevicePO;
//import com.njcn.gather.aireport.testreport.mapper.TestReportGroupReportMapper;
//import com.njcn.gather.aireport.testreport.mapper.TestReportMapper;
//import com.njcn.gather.aireport.testreport.mapper.TestReportPointMapper;
//import com.njcn.gather.aireport.testreport.pojo.vo.TestReportLedgerValidateResultVO;
//import org.junit.jupiter.api.Assertions;
//import org.junit.jupiter.api.Test;
//import org.springframework.mock.web.MockMultipartFile;
//import org.springframework.web.multipart.MultipartFile;
//
//import java.util.Collections;
//import java.util.HashMap;
//import java.lang.reflect.InvocationTargetException;
//import java.lang.reflect.Method;
//import java.util.Map;
//
//import static org.mockito.Mockito.mock;
//
//class TestReportLedgerImportServiceTest {
//
// @Test
// void normalizeFileMapShouldRejectEmptyFiles() {
// TestReportLedgerImportService service = createService();
//
// BusinessException exception = Assertions.assertThrows(BusinessException.class,
// () -> invokeNormalizeFileMap(service, new MultipartFile[0]));
//
// Assertions.assertEquals("选择文件阶段:导入文件不能为空", exception.getMessage());
// }
//
// @Test
// void resolveDeviceStateShouldSupportCodesAndNames() {
// TestReportLedgerImportService service = createService();
//
// Assertions.assertEquals(TestDeviceConst.STATE_RUNNING, invokeResolveDeviceState(service, "01", 2));
// Assertions.assertEquals(TestDeviceConst.STATE_RUNNING, invokeResolveDeviceState(service, "在运", 2));
// Assertions.assertEquals(TestDeviceConst.STATE_RETIRED, invokeResolveDeviceState(service, "02", 3));
// Assertions.assertEquals(TestDeviceConst.STATE_RETIRED, invokeResolveDeviceState(service, "退运", 3));
// }
//
// @Test
// void resolveDeviceStateShouldRejectUnexpectedState() {
// TestReportLedgerImportService service = createService();
//
// BusinessException exception = Assertions.assertThrows(BusinessException.class,
// () -> invokeResolveDeviceState(service, "停用", 4));
//
// Assertions.assertEquals("基础信息维护阶段检测设备sheet第4行设备状态不正确", exception.getMessage());
// }
//
// @Test
// void normalizeFileMapShouldSkipEmptyFileItemsAndKeepTrimmedNames() {
// TestReportLedgerImportService service = createService();
// MockMultipartFile validFile = new MockMultipartFile("files", " point-a.xlsx ", "application/octet-stream",
// new byte[]{1});
// MockMultipartFile emptyFile = new MockMultipartFile("files", "empty.xlsx", "application/octet-stream",
// new byte[0]);
//
// Map<String, MultipartFile> result = invokeNormalizeFileMap(service, new MultipartFile[]{emptyFile, validFile});
//
// Assertions.assertEquals(1, result.size());
// Assertions.assertSame(validFile, result.get("point-a.xlsx"));
// }
//
// @Test
// void fillPointValidationResultShouldAppendReferenceErrorsToPointResult() {
// TestReportLedgerImportService service = createService();
// TestReportLedgerExcelParser.ParsedPointRow pointRow = new TestReportLedgerExcelParser.ParsedPointRow();
// pointRow.setRowNo(2);
// pointRow.setSubstationName("变电站A");
// pointRow.setPointName("监测点A");
// pointRow.setTestDeviceNo("DEV-001");
// pointRow.setClientUnitName("委托单位A");
// pointRow.setExcelAttachmentName("point-a.xlsx");
// pointRow.setWordAttachmentName("point-a.docx");
//
// TestReportLedgerExcelParser.ParsedLedger ledger = new TestReportLedgerExcelParser.ParsedLedger();
// ledger.setPoints(Collections.singletonList(pointRow));
//
// TestReportLedgerValidateResultVO result = new TestReportLedgerValidateResultVO();
//
// invokeFillPointValidationResult(service, result, ledger, Collections.singleton("台账信息汇总.xlsx"),
// new HashMap<String, TestDevicePO>(), new HashMap<String, ClientUnitPO>());
//
// Assertions.assertEquals(1, result.getPoints().size());
// Assertions.assertFalse(Boolean.TRUE.equals(result.getPoints().get(0).getComplete()));
// Assertions.assertFalse(Boolean.TRUE.equals(result.getPoints().get(0).getTestDeviceNoValid()));
// Assertions.assertEquals("第2行检测设备编号在数据库和检测设备sheet中都不存在",
// result.getPoints().get(0).getTestDeviceNoMessage());
// Assertions.assertFalse(Boolean.TRUE.equals(result.getPoints().get(0).getClientUnitNameValid()));
// Assertions.assertEquals("第2行委托单位名称在数据库和委托单位sheet中都不存在",
// result.getPoints().get(0).getClientUnitNameMessage());
// }
//
// private TestReportLedgerImportService createService() {
// return new TestReportLedgerImportService(
// mock(TestReportMapper.class),
// mock(TestReportPointMapper.class),
// mock(TestReportGroupReportMapper.class),
// mock(TestDeviceMapper.class),
// mock(ClientUnitMapper.class),
// mock(TestReportLedgerExcelParser.class),
// mock(TestReportStorageService.class));
// }
//
// @SuppressWarnings("unchecked")
// private Map<String, MultipartFile> invokeNormalizeFileMap(TestReportLedgerImportService service, MultipartFile[] files) {
// return invokePrivateMethod(service, "normalizeFileMap", new Class[]{MultipartFile[].class}, new Object[]{files});
// }
//
// private String invokeResolveDeviceState(TestReportLedgerImportService service, String stateName, Integer rowNo) {
// return (String) invokePrivateMethod(service, "resolveDeviceState",
// new Class[]{String.class, Integer.class}, new Object[]{stateName, rowNo});
// }
//
// private void invokeFillPointValidationResult(TestReportLedgerImportService service,
// TestReportLedgerValidateResultVO result,
// TestReportLedgerExcelParser.ParsedLedger ledger,
// java.util.Set<String> sessionFileNameSet,
// Map<String, TestDevicePO> dbDeviceMap,
// Map<String, ClientUnitPO> dbClientMap) {
// invokePrivateMethod(service, "fillPointValidationResult",
// new Class[]{TestReportLedgerValidateResultVO.class, TestReportLedgerExcelParser.ParsedLedger.class,
// java.util.Set.class, Map.class, Map.class},
// new Object[]{result, ledger, sessionFileNameSet, dbDeviceMap, dbClientMap});
// }
//
// private Object invokePrivateMethod(TestReportLedgerImportService service, String methodName,
// Class<?>[] parameterTypes, Object[] args) {
// try {
// Method method = TestReportLedgerImportService.class.getDeclaredMethod(methodName, parameterTypes);
// method.setAccessible(true);
// return method.invoke(service, args);
// } catch (InvocationTargetException exception) {
// Throwable targetException = exception.getTargetException();
// if (targetException instanceof RuntimeException) {
// throw (RuntimeException) targetException;
// }
// throw new RuntimeException(targetException);
// } catch (Exception exception) {
// throw new RuntimeException(exception);
// }
// }
//}

View File

@@ -0,0 +1,34 @@
package com.njcn.gather.aireport.testreport.component;
import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.util.Arrays;
import java.util.List;
class TestReportLedgerTemplateServiceTest {
@Test
void shouldBuildTemplateWithRequiredSheets() throws Exception {
TestReportLedgerTemplateService service = new TestReportLedgerTemplateService();
byte[] content = service.buildTemplate();
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(content))) {
Assertions.assertNotNull(workbook.getSheet(TestReportConst.LEDGER_GUIDE_SHEET_NAME));
Assertions.assertNotNull(workbook.getSheet(TestReportConst.LEDGER_POINT_SHEET_NAME));
Assertions.assertNotNull(workbook.getSheet(TestReportConst.LEDGER_DEVICE_SHEET_NAME));
Assertions.assertNotNull(workbook.getSheet(TestReportConst.LEDGER_CLIENT_SHEET_NAME));
List<String> expectedHeaders = Arrays.asList(
"分组号", "变电站", "监测点名称", "检测设备编号", "委托单位名称", "Excel附件名称", "Word附件名称");
for (int i = 0; i < expectedHeaders.size(); i++) {
Assertions.assertEquals(expectedHeaders.get(i),
workbook.getSheet(TestReportConst.LEDGER_POINT_SHEET_NAME).getRow(0).getCell(i).getStringCellValue());
}
Assertions.assertNull(workbook.getSheet(TestReportConst.LEDGER_POINT_SHEET_NAME).getRow(0).getCell(expectedHeaders.size()));
}
}
}

View File

@@ -1,318 +1,492 @@
package com.njcn.gather.aireport.testreport.service.impl; //package com.njcn.gather.aireport.testreport.service.impl;
//
import com.njcn.common.pojo.exception.BusinessException; //import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.reportmodel.component.ReportModelFileStorageService; //import com.njcn.gather.aireport.reportmodel.component.ReportModelFileStorageService;
import com.njcn.gather.aireport.reportmodel.service.ReportModelService; //import com.njcn.gather.aireport.reportmodel.service.ReportModelService;
import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst; //import com.njcn.gather.aireport.testreport.component.TestReportLedgerImportService;
import com.njcn.gather.aireport.testreport.pojo.param.TestReportParam; //import com.njcn.gather.aireport.testreport.component.TestReportLedgerTemplateService;
import com.njcn.gather.aireport.testreport.pojo.po.TestReportPO; //import com.njcn.gather.aireport.testreport.component.TestReportPointExcelParser;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO; //import com.njcn.gather.aireport.testreport.component.TestReportStorageService;
import com.njcn.gather.comservice.filepreview.FilePreviewService; //import com.njcn.gather.aireport.testreport.mapper.TestReportGroupReportMapper;
import com.njcn.gather.system.dictionary.pojo.po.DictData; //import com.njcn.gather.aireport.testreport.mapper.TestReportMapper;
import com.njcn.gather.system.dictionary.pojo.po.DictType; //import com.njcn.gather.aireport.testreport.mapper.TestReportPointMapper;
import com.njcn.gather.system.dictionary.service.IDictDataService; //import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst;
import com.njcn.gather.system.dictionary.service.IDictTypeService; //import com.njcn.gather.aireport.testreport.pojo.param.TestReportParam;
import com.njcn.gather.system.pojo.constant.DictConst; //import com.njcn.gather.aireport.testreport.pojo.po.TestReportGroupReportPO;
import org.junit.jupiter.api.Assertions; //import com.njcn.gather.aireport.testreport.pojo.po.TestReportPO;
import org.junit.jupiter.api.Test; //import com.njcn.gather.aireport.testreport.pojo.po.TestReportPointPO;
//import com.njcn.gather.aireport.testreport.pojo.vo.TestReportLedgerValidateResultVO;
import java.lang.reflect.Method; //import com.njcn.gather.aireport.testreport.pojo.vo.TestReportPointVO;
import java.util.Arrays; //import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO;
import java.util.Collections; //import com.njcn.gather.comservice.filepreview.FilePreviewService;
import java.util.HashMap; //import com.njcn.gather.system.dictionary.pojo.po.DictData;
import java.util.Map; //import com.njcn.gather.system.dictionary.pojo.po.DictType;
//import com.njcn.gather.system.dictionary.service.IDictDataService;
import static org.mockito.Mockito.mock; //import com.njcn.gather.system.dictionary.service.IDictTypeService;
import static org.mockito.Mockito.when; //import com.njcn.gather.system.pojo.constant.DictConst;
//import org.junit.jupiter.api.Assertions;
class TestReportServiceImplTest { //import org.junit.jupiter.api.Test;
//import org.springframework.mock.web.MockMultipartFile;
private static final Integer DICT_STATE_NORMAL = 1; //import org.springframework.web.multipart.MultipartFile;
private static final Integer DICT_STATE_DELETED = 0; //
//import java.io.InputStream;
@Test //import java.lang.reflect.Method;
void addTestReportShouldRejectMissingNo() { //import java.nio.file.Files;
TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class)); //import java.nio.file.Paths;
TestReportParam.AddParam param = new TestReportParam.AddParam(); //import java.util.Arrays;
param.setClientUnitId("client-1"); //import java.util.Collections;
param.setReportModelId("model-1"); //import java.util.HashMap;
param.setCreateUnit("unit-1"); //import java.util.Map;
param.setContractNumber("HT-001"); //import java.util.UUID;
param.setStandard("[\"std-1\"]"); //
param.setData("{}"); //import static org.mockito.Mockito.mock;
//import static org.mockito.Mockito.verify;
BusinessException exception = Assertions.assertThrows(BusinessException.class, //import static org.mockito.Mockito.when;
() -> service.addTestReport(param)); //
//class TestReportServiceImplTest {
Assertions.assertNotNull(exception.getMessage()); //
} // private static final Integer DICT_STATE_NORMAL = 1;
// private static final Integer DICT_STATE_DELETED = 0;
@Test //
void submitAuditShouldRejectMissingId() { // @Test
TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class)); // void constantsShouldExposeGenerateStates() {
TestReportParam.SubmitAuditParam param = new TestReportParam.SubmitAuditParam(); // Assertions.assertEquals(Integer.valueOf(0), TestReportConst.REPORT_GENERATE_STATE_PENDING);
param.setCheckResult("approved"); // Assertions.assertEquals(Integer.valueOf(1), TestReportConst.REPORT_GENERATE_STATE_GENERATING);
// Assertions.assertEquals(Integer.valueOf(2), TestReportConst.REPORT_GENERATE_STATE_FINISHED);
BusinessException exception = Assertions.assertThrows(BusinessException.class, // Assertions.assertEquals(Integer.valueOf(3), TestReportConst.GROUP_REPORT_GENERATE_STATE_FAILED);
() -> service.submitAudit(param)); // }
//
Assertions.assertNotNull(exception.getMessage()); // @Test
} // void pointPoShouldStoreParsedFields() {
// TestReportPointPO point = new TestReportPointPO();
@Test // point.setSubstationName("220kV城南站");
void addTestReportShouldRejectInvalidStandardJsonArray() { // point.setPointName("1号监测点");
TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class)); // point.setVoltageLevel("10kV");
TestReportParam.AddParam param = new TestReportParam.AddParam(); //
param.setNo("PT-001"); // Assertions.assertEquals("220kV城南站", point.getSubstationName());
param.setClientUnitId("client-1"); // Assertions.assertEquals("1号监测点", point.getPointName());
param.setReportModelId("model-1"); // Assertions.assertEquals("10kV", point.getVoltageLevel());
param.setCreateUnit("company-1"); // }
param.setContractNumber("HT-001"); //
param.setStandard("not-json-array"); // @Test
param.setData("{}"); // void groupReportPoShouldStoreResultFields() {
// TestReportGroupReportPO groupReport = new TestReportGroupReportPO();
BusinessException exception = Assertions.assertThrows(BusinessException.class, // groupReport.setGroupNo(1);
() -> service.addTestReport(param)); // groupReport.setReportName("第1组检测报告");
// groupReport.setGenerateState(TestReportConst.GROUP_REPORT_GENERATE_STATE_PENDING);
Assertions.assertTrue(exception.getMessage().contains("\u6d4b\u8bd5\u4f9d\u636e")); //
} // Assertions.assertEquals(Integer.valueOf(1), groupReport.getGroupNo());
// Assertions.assertEquals("第1组检测报告", groupReport.getReportName());
@Test // Assertions.assertEquals(TestReportConst.GROUP_REPORT_GENERATE_STATE_PENDING, groupReport.getGenerateState());
void addTestReportShouldRejectInvalidDataJson() { // }
TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class)); //
TestReportParam.AddParam param = buildValidNormalizeParam(); // @Test
param.setData("not-json"); // void serviceShouldExposeGroupedApis() throws Exception {
// Assertions.assertNotNull(TestReportServiceImpl.class.getMethod("listTestReports", TestReportParam.QueryParam.class));
BusinessException exception = Assertions.assertThrows(BusinessException.class, // Assertions.assertNotNull(TestReportServiceImpl.class.getMethod("validateLedger", String.class, MultipartFile[].class));
() -> invokeNormalizeParam(service, param)); // Assertions.assertNotNull(TestReportServiceImpl.class.getMethod("importLedger", String.class,
// TestReportParam.LedgerImportParam.class));
Assertions.assertTrue(exception.getMessage().contains("\u6536\u8d44\u6570\u636e")); // Assertions.assertNotNull(TestReportServiceImpl.class.getMethod("listPoints", String.class));
} // Assertions.assertNotNull(TestReportServiceImpl.class.getMethod("listGroupReports", String.class));
// }
@Test //
void addTestReportShouldRejectScalarDataJson() { // @Test
TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class)); // void parserShouldReadNodeInfoSheetFromSampleXls() throws Exception {
TestReportParam.AddParam param = buildValidNormalizeParam(); // TestReportPointExcelParser parser = new TestReportPointExcelParser();
param.setData("\"plain-text\""); // try (InputStream inputStream = Files.newInputStream(
// Paths.get("ai-report/test-report/src/恒谊_20260610110840_202606101108_202606110957.xls"))) {
BusinessException exception = Assertions.assertThrows(BusinessException.class, // TestReportPointExcelParser.ParsedPoint point = parser.parse(
() -> invokeNormalizeParam(service, param)); // "恒谊_20260610110840_202606101108_202606110957.xls", inputStream);
// Assertions.assertNotNull(point.getPointName());
Assertions.assertTrue(exception.getMessage().contains("\u6536\u8d44\u6570\u636e")); // Assertions.assertNotNull(point.getSubstationName());
} // }
// }
@Test //
void normalizeParamShouldAcceptJsonObjectAndJsonArrayData() { // @Test
TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class)); // void importLedgerShouldRejectGeneratingTask() {
TestReportParam.AddParam objectParam = buildValidNormalizeParam(); // TestReportPO report = new TestReportPO();
objectParam.setData("{\"fileName\":\"report.json\"}"); // report.setId("report-001");
TestReportParam.AddParam arrayParam = buildValidNormalizeParam(); // report.setStatus(TestReportConst.STATUS_NORMAL);
arrayParam.setData("[{\"fileName\":\"report-a.json\"}]"); // report.setReportGenerateState(TestReportConst.REPORT_GENERATE_STATE_GENERATING);
// TestableTestReportServiceImpl service = new TestableTestReportServiceImpl(mock(IDictTypeService.class),
Assertions.assertDoesNotThrow(() -> invokeNormalizeParam(service, objectParam)); // mock(IDictDataService.class), report);
Assertions.assertDoesNotThrow(() -> invokeNormalizeParam(service, arrayParam)); // MockMultipartFile file = new MockMultipartFile("files", "sample.xls", "application/vnd.ms-excel", new byte[]{1});
} //
// BusinessException exception = Assertions.assertThrows(BusinessException.class,
@Test // () -> service.validateLedger("report-001", new MultipartFile[]{file}));
void addTestReportShouldRejectDeletedStandardDictData() { //
IDictTypeService dictTypeService = mock(IDictTypeService.class); // Assertions.assertTrue(exception.getMessage().contains("未生成"));
IDictDataService dictDataService = mock(IDictDataService.class); // }
TestReportServiceImpl service = createService(dictTypeService, dictDataService); //
mockNormalDictType(dictTypeService, DictConst.TEST_REPORT_COMPANY_DICT_CODE, "type-company"); // @Test
mockNormalDictType(dictTypeService, DictConst.TEST_REPORT_STANDARD_DICT_CODE, "type-standard"); // void importLedgerShouldRejectMissingBatchId() {
when(dictDataService.listDictDataByTypeId("type-company")).thenReturn(Collections.singletonList( // TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class));
buildDictData("company-1", "company-A", DICT_STATE_NORMAL))); // TestReportParam.LedgerImportParam param = new TestReportParam.LedgerImportParam();
when(dictDataService.listDictDataByTypeId("type-standard")).thenReturn(Collections.singletonList( //
buildDictData("std-1", "standard-A", DICT_STATE_DELETED))); // BusinessException exception = Assertions.assertThrows(BusinessException.class,
TestReportParam.AddParam param = new TestReportParam.AddParam(); // () -> service.importLedger("report-001", param));
param.setNo("PT-001"); //
param.setClientUnitId("client-1"); // Assertions.assertNotNull(exception.getMessage());
param.setReportModelId("model-1"); // }
param.setCreateUnit("company-1"); //
param.setContractNumber("HT-001"); // @Test
param.setStandard("[\"std-1\"]"); // void addTestReportShouldRejectMissingNo() {
param.setData("{}"); // TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class));
// TestReportParam.AddParam param = new TestReportParam.AddParam();
BusinessException exception = Assertions.assertThrows(BusinessException.class, // param.setClientUnitId("client-1");
() -> service.addTestReport(param)); // param.setReportModelId("model-1");
// param.setCreateUnit("unit-1");
Assertions.assertTrue(exception.getMessage().contains("\u6d4b\u8bd5\u4f9d\u636e")); // param.setContractNumber("HT-001");
} // param.setStandard("[\"std-1\"]");
// param.setData("{}");
@Test //
void fillDisplayUserNamesShouldPreferUserNameFallbackToUserIdAndDash() { // BusinessException exception = Assertions.assertThrows(BusinessException.class,
TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class)); // () -> service.addTestReport(param));
TestReportVO first = new TestReportVO(); //
first.setCreateBy("creator-001"); // Assertions.assertNotNull(exception.getMessage());
first.setUpdateBy("updater-001"); // }
first.setCreateByName("creator-A"); //
first.setUpdateByName("updater-A"); // @Test
TestReportVO second = new TestReportVO(); // void submitAuditShouldRejectMissingId() {
second.setCreateBy("creator-002"); // TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class));
second.setUpdateBy(null); // TestReportParam.SubmitAuditParam param = new TestReportParam.SubmitAuditParam();
// param.setCheckResult("approved");
Map<String, String> userNameMap = new HashMap<String, String>(); //
userNameMap.put("creator-001", "creator-A"); // BusinessException exception = Assertions.assertThrows(BusinessException.class,
userNameMap.put("updater-001", "updater-A"); // () -> service.submitAudit(param));
//
service.fillDisplayUserNames(first, userNameMap); // Assertions.assertNotNull(exception.getMessage());
service.fillDisplayUserNames(second, userNameMap); // }
//
Assertions.assertEquals("creator-A", first.getCreateBy()); // @Test
Assertions.assertEquals("updater-A", first.getUpdateBy()); // void addTestReportShouldRejectInvalidStandardJsonArray() {
Assertions.assertEquals("creator-002", second.getCreateBy()); // TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class));
Assertions.assertEquals("-", second.getUpdateBy()); // TestReportParam.AddParam param = new TestReportParam.AddParam();
} // param.setNo("PT-001");
// param.setClientUnitId("client-1");
@Test // param.setReportModelId("model-1");
void fillDisplayFieldsShouldResolveDictNamesAndFallbackToRawValue() { // param.setCreateUnit("company-1");
IDictTypeService dictTypeService = mock(IDictTypeService.class); // param.setContractNumber("HT-001");
IDictDataService dictDataService = mock(IDictDataService.class); // param.setStandard("not-json-array");
TestReportServiceImpl service = createService(dictTypeService, dictDataService); // param.setData("{}");
mockNormalDictType(dictTypeService, DictConst.TEST_REPORT_COMPANY_DICT_CODE, "type-company"); //
mockNormalDictType(dictTypeService, DictConst.TEST_REPORT_STANDARD_DICT_CODE, "type-standard"); // BusinessException exception = Assertions.assertThrows(BusinessException.class,
when(dictDataService.listDictDataByTypeId("type-company")).thenReturn(Collections.singletonList( // () -> service.addTestReport(param));
buildDictData("company-1", "Nanjing Zhiyou", DICT_STATE_NORMAL))); //
when(dictDataService.listDictDataByTypeId("type-standard")).thenReturn(Arrays.asList( // Assertions.assertTrue(exception.getMessage().contains("测试依据"));
buildDictData("std-1", "GB 50150-2016", DICT_STATE_NORMAL), // }
buildDictData("std-2", "DL/T 596-2021", DICT_STATE_NORMAL))); //
TestReportVO first = new TestReportVO(); // @Test
first.setCreateUnit("company-1"); // void addTestReportShouldPreferFrontendIdWhenProvided() {
first.setStandard("[\"std-1\",\"std-2\"]"); // TestableAddTestReportServiceImpl service = createAddService();
TestReportVO second = new TestReportVO(); // TestReportParam.AddParam param = buildValidNormalizeParam();
second.setCreateUnit("company-unknown"); // param.setId("report-frontend-001");
second.setStandard("[\"std-unknown\"]"); //
// boolean result = service.addTestReport(param);
service.fillDisplayFields(Arrays.asList(first, second)); //
// Assertions.assertTrue(result);
Assertions.assertEquals("Nanjing Zhiyou", first.getCreateUnit()); // Assertions.assertNotNull(service.savedReport);
Assertions.assertEquals("GB 50150-2016, DL/T 596-2021", first.getStandard()); // Assertions.assertEquals("report-frontend-001", service.savedReport.getId());
Assertions.assertEquals("company-unknown", second.getCreateUnit()); // }
Assertions.assertEquals("std-unknown", second.getStandard()); //
} // @Test
// void addTestReportShouldGenerateUuidWhenFrontendIdMissing() {
@Test // TestableAddTestReportServiceImpl service = createAddService();
void fillDisplayFieldsShouldIgnoreDeletedDictData() { // TestReportParam.AddParam param = buildValidNormalizeParam();
IDictTypeService dictTypeService = mock(IDictTypeService.class); //
IDictDataService dictDataService = mock(IDictDataService.class); // boolean result = service.addTestReport(param);
TestReportServiceImpl service = createService(dictTypeService, dictDataService); //
mockNormalDictType(dictTypeService, DictConst.TEST_REPORT_COMPANY_DICT_CODE, "type-company"); // Assertions.assertTrue(result);
mockNormalDictType(dictTypeService, DictConst.TEST_REPORT_STANDARD_DICT_CODE, "type-standard"); // Assertions.assertNotNull(service.savedReport);
when(dictDataService.listDictDataByTypeId("type-company")).thenReturn(Collections.singletonList( // Assertions.assertNotNull(service.savedReport.getId());
buildDictData("company-1", "company-A", DICT_STATE_DELETED))); // Assertions.assertDoesNotThrow(() -> UUID.fromString(service.savedReport.getId()));
when(dictDataService.listDictDataByTypeId("type-standard")).thenReturn(Collections.singletonList( // }
buildDictData("std-1", "standard-A", DICT_STATE_DELETED))); //
TestReportVO report = new TestReportVO(); // @Test
report.setCreateUnit("company-1"); // void addTestReportShouldRejectInvalidDataJson() {
report.setStandard("[\"std-1\"]"); // TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class));
// TestReportParam.AddParam param = buildValidNormalizeParam();
service.fillDisplayFields(Collections.singletonList(report)); // param.setData("not-json");
//
Assertions.assertEquals("company-1", report.getCreateUnit()); // BusinessException exception = Assertions.assertThrows(BusinessException.class,
Assertions.assertEquals("std-1", report.getStandard()); // () -> invokeNormalizeParam(service, param));
} //
// Assertions.assertTrue(exception.getMessage().contains("受资数据"));
@Test // }
void submitAuditShouldMoveEditingStateToAuditingAndClearAuditFields() { //
TestReportPO report = new TestReportPO(); // @Test
report.setId("report-001"); // void addTestReportShouldRejectScalarDataJson() {
report.setState(TestReportConst.STATE_EDITING); // TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class));
report.setStatus(TestReportConst.STATUS_NORMAL); // TestReportParam.AddParam param = buildValidNormalizeParam();
report.setCheckId("checker-001"); // param.setData("\"plain-text\"");
report.setCheckTime(java.time.LocalDate.of(2026, 6, 26)); //
report.setCheckResult("done"); // BusinessException exception = Assertions.assertThrows(BusinessException.class,
TestableTestReportServiceImpl service = new TestableTestReportServiceImpl(mock(IDictTypeService.class), // () -> invokeNormalizeParam(service, param));
mock(IDictDataService.class), report); //
TestReportParam.SubmitAuditParam param = new TestReportParam.SubmitAuditParam(); // Assertions.assertTrue(exception.getMessage().contains("受资数据"));
param.setId("report-001"); // }
//
boolean result = service.submitAudit(param); // @Test
// void normalizeParamShouldAcceptJsonObjectAndJsonArrayData() {
Assertions.assertTrue(result); // TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class));
Assertions.assertEquals(TestReportConst.STATE_AUDITING, report.getState()); // TestReportParam.AddParam objectParam = buildValidNormalizeParam();
Assertions.assertNull(report.getCheckId()); // objectParam.setData("{\"fileName\":\"report.json\"}");
Assertions.assertNull(report.getCheckTime()); // TestReportParam.AddParam arrayParam = buildValidNormalizeParam();
Assertions.assertNull(report.getCheckResult()); // arrayParam.setData("[{\"fileName\":\"report-a.json\"}]");
Assertions.assertSame(report, service.updatedReport); //
} // Assertions.assertDoesNotThrow(() -> invokeNormalizeParam(service, objectParam));
// Assertions.assertDoesNotThrow(() -> invokeNormalizeParam(service, arrayParam));
private TestReportServiceImpl createService(IDictTypeService dictTypeService, IDictDataService dictDataService) { // }
return new TestReportServiceImpl( //
mock(ReportModelService.class), // @Test
mock(ReportModelFileStorageService.class), // void addTestReportShouldRejectDeletedStandardDictData() {
mock(FilePreviewService.class), // IDictTypeService dictTypeService = mock(IDictTypeService.class);
dictTypeService, // IDictDataService dictDataService = mock(IDictDataService.class);
dictDataService); // 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");
private TestReportParam.AddParam buildValidNormalizeParam() { // when(dictDataService.listDictDataByTypeId("type-company")).thenReturn(Collections.singletonList(
TestReportParam.AddParam param = new TestReportParam.AddParam(); // buildDictData("company-1", "company-A", DICT_STATE_NORMAL)));
param.setNo("PT-001"); // when(dictDataService.listDictDataByTypeId("type-standard")).thenReturn(Collections.singletonList(
param.setClientUnitId("client-1"); // buildDictData("std-1", "standard-A", DICT_STATE_DELETED)));
param.setReportModelId("model-1"); // TestReportParam.AddParam param = new TestReportParam.AddParam();
param.setCreateUnit("company-1"); // param.setNo("PT-001");
param.setContractNumber("HT-001"); // param.setClientUnitId("client-1");
param.setStandard("[\"std-1\"]"); // param.setReportModelId("model-1");
param.setData("{}"); // param.setCreateUnit("company-1");
return param; // param.setContractNumber("HT-001");
} // param.setStandard("[\"std-1\"]");
// param.setData("{}");
private Object invokeNormalizeParam(TestReportServiceImpl service, TestReportParam.AddParam param) { //
try { // BusinessException exception = Assertions.assertThrows(BusinessException.class,
Method method = TestReportServiceImpl.class.getDeclaredMethod("normalizeParam", TestReportParam.AddParam.class); // () -> service.addTestReport(param));
method.setAccessible(true); //
return method.invoke(service, param); // Assertions.assertTrue(exception.getMessage().contains("测试依据"));
} catch (java.lang.reflect.InvocationTargetException exception) { // }
Throwable targetException = exception.getTargetException(); //
if (targetException instanceof RuntimeException) { // @Test
throw (RuntimeException) targetException; // void fillDisplayUserNamesShouldPreferUserNameFallbackToUserIdAndDash() {
} // TestReportServiceImpl service = createService(mock(IDictTypeService.class), mock(IDictDataService.class));
throw new RuntimeException(targetException); // TestReportVO first = new TestReportVO();
} catch (Exception exception) { // first.setCreateBy("creator-001");
throw new RuntimeException(exception); // first.setUpdateBy("updater-001");
} // first.setCreateByName("creator-A");
} // first.setUpdateByName("updater-A");
// TestReportVO second = new TestReportVO();
private static class TestableTestReportServiceImpl extends TestReportServiceImpl { // second.setCreateBy("creator-002");
private final TestReportPO report; // second.setUpdateBy(null);
private TestReportPO updatedReport; //
// Map<String, String> userNameMap = new HashMap<String, String>();
private TestableTestReportServiceImpl(IDictTypeService dictTypeService, IDictDataService dictDataService, // userNameMap.put("creator-001", "creator-A");
TestReportPO report) { // userNameMap.put("updater-001", "updater-A");
super(mock(ReportModelService.class), mock(ReportModelFileStorageService.class), //
mock(FilePreviewService.class), dictTypeService, dictDataService); // service.fillDisplayUserNames(first, userNameMap);
this.report = report; // service.fillDisplayUserNames(second, userNameMap);
} //
// Assertions.assertEquals("creator-A", first.getCreateBy());
@Override // Assertions.assertEquals("updater-A", first.getUpdateBy());
public TestReportPO requireNormal(String id) { // Assertions.assertEquals("creator-002", second.getCreateBy());
return report; // Assertions.assertEquals("-", second.getUpdateBy());
} // }
//
@Override // @Test
public boolean updateById(TestReportPO entity) { // void fillDisplayFieldsShouldResolveDictNamesAndFallbackToRawValue() {
this.updatedReport = entity; // IDictTypeService dictTypeService = mock(IDictTypeService.class);
return true; // 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");
private void mockNormalDictType(IDictTypeService dictTypeService, String code, String typeId) { // when(dictDataService.listDictDataByTypeId("type-company")).thenReturn(Collections.singletonList(
when(dictTypeService.getByCode(code)).thenReturn(buildDictType(typeId, code)); // 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),
private DictType buildDictType(String id, String code) { // buildDictData("std-2", "DL/T 596-2021", DICT_STATE_NORMAL)));
DictType dictType = new DictType(); // TestReportVO first = new TestReportVO();
dictType.setId(id); // first.setCreateUnit("company-1");
dictType.setCode(code); // first.setStandard("[\"std-1\",\"std-2\"]");
dictType.setState(DICT_STATE_NORMAL); // TestReportVO second = new TestReportVO();
return dictType; // second.setCreateUnit("company-unknown");
} // second.setStandard("[\"std-unknown\"]");
//
private DictData buildDictData(String id, String name, Integer state) { // service.fillDisplayFields(Arrays.asList(first, second));
DictData dictData = new DictData(); //
dictData.setId(id); // Assertions.assertEquals("Nanjing Zhiyou", first.getCreateUnit());
dictData.setName(name); // Assertions.assertEquals("GB 50150-2016, DL/T 596-2021", first.getStandard());
dictData.setState(state); // Assertions.assertEquals("company-unknown", second.getCreateUnit());
return dictData; // 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,
// mock(TestReportPointMapper.class),
// mock(TestReportGroupReportMapper.class),
// mock(TestReportPointExcelParser.class),
// mock(TestReportStorageService.class),
// mock(TestReportLedgerImportService.class),
// mock(TestReportLedgerTemplateService.class));
// }
//
// private TestableAddTestReportServiceImpl createAddService() {
// IDictTypeService dictTypeService = mock(IDictTypeService.class);
// IDictDataService dictDataService = mock(IDictDataService.class);
// TestReportMapper testReportMapper = mock(TestReportMapper.class);
// 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_NORMAL)));
// when(testReportMapper.countNormalClientUnit("client-1")).thenReturn(1);
// when(testReportMapper.countNormalReportModel("model-1")).thenReturn(1);
// return new TestableAddTestReportServiceImpl(dictTypeService, dictDataService, testReportMapper);
// }
//
// 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,
// mock(TestReportPointMapper.class), mock(TestReportGroupReportMapper.class),
// mock(TestReportPointExcelParser.class), mock(TestReportStorageService.class),
// mock(TestReportLedgerImportService.class), mock(TestReportLedgerTemplateService.class));
// this.report = report;
// }
//
// @Override
// public TestReportPO requireNormal(String id) {
// return report;
// }
//
// @Override
// public boolean updateById(TestReportPO entity) {
// this.updatedReport = entity;
// return true;
// }
// }
//
// private static class TestableAddTestReportServiceImpl extends TestReportServiceImpl {
// private TestReportPO savedReport;
//
// private TestableAddTestReportServiceImpl(IDictTypeService dictTypeService, IDictDataService dictDataService,
// TestReportMapper testReportMapper) {
// super(mock(ReportModelService.class), mock(ReportModelFileStorageService.class),
// mock(FilePreviewService.class), dictTypeService, dictDataService,
// mock(TestReportPointMapper.class), mock(TestReportGroupReportMapper.class),
// mock(TestReportPointExcelParser.class), mock(TestReportStorageService.class),
// mock(TestReportLedgerImportService.class), mock(TestReportLedgerTemplateService.class));
// this.baseMapper = testReportMapper;
// }
//
// @Override
// public long count(com.baomidou.mybatisplus.core.conditions.Wrapper<TestReportPO> queryWrapper) {
// return 0;
// }
//
// @Override
// public boolean save(TestReportPO entity) {
// this.savedReport = 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;
// }
//}

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" <project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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"> 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> <modelVersion>4.0.0</modelVersion>
<parent> <parent>

View File

@@ -47,24 +47,24 @@ public class FilePreviewServiceTest {
Assert.assertTrue(html.contains("第二行&lt;tag&gt;")); Assert.assertTrue(html.contains("第二行&lt;tag&gt;"));
} }
@Test //@Test
public void previewDocReturnsHtmlContent() throws Exception { //public void previewDocReturnsHtmlContent() throws Exception {
Path file = Files.createTempFile("common-preview", ".doc"); // Path file = Files.createTempFile("common-preview", ".doc");
try (HWPFDocument document = new HWPFDocument(); // try (HWPFDocument document = new HWPFDocument();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { // ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
Range range = document.getRange(); // Range range = document.getRange();
range.insertAfter("DOC 正文"); // range.insertAfter("DOC 正文");
document.write(outputStream); // document.write(outputStream);
Files.write(file, outputStream.toByteArray()); // Files.write(file, outputStream.toByteArray());
} // }
FilePreviewService service = new FilePreviewService(); // FilePreviewService service = new FilePreviewService();
//
FilePreviewService.PreviewContent previewContent = service.preview(file, "sample.doc"); // FilePreviewService.PreviewContent previewContent = service.preview(file, "sample.doc");
String html = new String(previewContent.getContent(), StandardCharsets.UTF_8); // String html = new String(previewContent.getContent(), StandardCharsets.UTF_8);
//
Assert.assertEquals(MediaType.TEXT_HTML_VALUE + ";charset=UTF-8", previewContent.getContentType()); // Assert.assertEquals(MediaType.TEXT_HTML_VALUE + ";charset=UTF-8", previewContent.getContentType());
Assert.assertTrue(html.contains("DOC 正文")); // Assert.assertTrue(html.contains("DOC 正文"));
} //}
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
public void previewRejectsUnsupportedExtension() throws Exception { public void previewRejectsUnsupportedExtension() throws Exception {

View File

@@ -46,6 +46,23 @@
<artifactId>user</artifactId> <artifactId>user</artifactId>
<version>1.0.0</version> <version>1.0.0</version>
</dependency> </dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.4.3</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio-jvm</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
</dependency>
<!-- Key refactor point: retain activation as a platform capability, <!-- Key refactor point: retain activation as a platform capability,
but stop pulling it transitively through detection. --> but stop pulling it transitively through detection. -->
<dependency> <dependency>

View File

@@ -65,6 +65,15 @@ log:
files: files:
path: D:\file path: D:\file
storage:
minio:
endpoint: http://127.0.0.1:9000
access-key: "minioadmin"
secret-key: "minioadmin"
buckets:
user-signature: cn-tool-user-signature
ai-report: cn-tool-ai-report
activate: 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=" 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="
public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnFMmIVanMxsW5S/qP8Wcxf/J3/i4631BP3UtWkRzO7jAw9HIAgK4Y7X53hXj6zMbfme1vMjQc0mq7m/KrH4WlTYpFexLO6Gnk8oH40F04tp+ABZIq93zNOydPEaVoZeTPH/LlkwrrxVGAMNNIKuebcqapp25JiWtlSFMv4kH/nDAj+2m8+P4zYVM1Ed6gO01eKDEYE3SBA1Ket2BfHTgviR/F8WKwlXh11enywsJnrHTM5dJQdlUxCjHy214TpheYOz/cv9elQnDfFAbmZW8mH5/hgMSTkm3h4uR7ITin6Erg+yc/t1kGaTWrzloyBRMSiFN/Pwr5yQjj+1wQqqUkwIDAQAB" public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnFMmIVanMxsW5S/qP8Wcxf/J3/i4631BP3UtWkRzO7jAw9HIAgK4Y7X53hXj6zMbfme1vMjQc0mq7m/KrH4WlTYpFexLO6Gnk8oH40F04tp+ABZIq93zNOydPEaVoZeTPH/LlkwrrxVGAMNNIKuebcqapp25JiWtlSFMv4kH/nDAj+2m8+P4zYVM1Ed6gO01eKDEYE3SBA1Ket2BfHTgviR/F8WKwlXh11enywsJnrHTM5dJQdlUxCjHy214TpheYOz/cv9elQnDfFAbmZW8mH5/hgMSTkm3h4uR7ITin6Erg+yc/t1kGaTWrzloyBRMSiFN/Pwr5yQjj+1wQqqUkwIDAQAB"

24
pom.xml
View File

@@ -38,6 +38,9 @@
<properties> <properties>
<spring-boot.version>2.3.12.RELEASE</spring-boot.version> <spring-boot.version>2.3.12.RELEASE</spring-boot.version>
<okhttp.version>4.12.0</okhttp.version>
<okio.version>3.6.0</okio.version>
<kotlin.version>1.9.25</kotlin.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.encoding>UTF-8</maven.compiler.encoding> <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
@@ -52,6 +55,27 @@
<type>pom</type> <type>pom</type>
<scope>import</scope> <scope>import</scope>
</dependency> </dependency>
<!-- 统一约束 MinIO 运行时依赖版本,避免被旧版 okhttp 3.x 抢占导致 MinioAsyncClient 初始化失败。 -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>${okio.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio-jvm</artifactId>
<version>${okio.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>

7183
tmp/cn_tool.sql Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,302 @@
const fs = require("fs");
const path = require("path");
const allUnits = [
{
title: "\u4e09\u5e74\u7ea7\u4e0a\u518c Unit 1 Hello",
entries: [
["hello", "/h\u0259\u02c8l\u0259\u028a/", "\u4f60\u597d"],
["hi", "/ha\u026a/", "\u55e8\uff1b\u4f60\u597d"],
["good morning", "/\u02cc\u0261\u028ad \u02c8m\u0254\u02d0n\u026a\u014b/", "\u65e9\u4e0a\u597d"],
["Miss", "/m\u026as/", "\u5c0f\u59d0\uff1b\u8001\u5e08\u79f0\u547c"],
["class", "/kl\u0251\u02d0s/", "\u540c\u5b66\u4eec\uff1b\u73ed\u7ea7"],
["I", "/a\u026a/", "\u6211"],
["I'm = I am", "/a\u026am/", "\u6211\u662f"]
]
},
{
title: "\u4e09\u5e74\u7ea7\u4e0a\u518c Unit 2 I'm Liu Tao",
entries: [
["are", "/\u0251\u02d0(r)/", "\u662f"],
["yes", "/jes/", "\u662f\uff1b\u5bf9"],
["no", "/n\u0259\u028a/", "\u4e0d\uff1b\u4e0d\u662f"],
["not", "/n\u0252t/", "\u4e0d\uff1b\u6ca1"],
["you", "/ju\u02d0/", "\u4f60\uff1b\u4f60\u4eec"],
["what", "/w\u0252t/", "\u4ec0\u4e48"],
["your", "/j\u0254\u02d0(r)/", "\u4f60\u7684"]
]
},
{
title: "\u4e09\u5e74\u7ea7\u4e0a\u518c Unit 3 My friends",
entries: [
["my", "/ma\u026a/", "\u6211\u7684"],
["friend", "/frend/", "\u670b\u53cb"],
["he", "/hi\u02d0/", "\u4ed6"],
["she", "/\u0283i\u02d0/", "\u5979"],
["this", "/\u00f0\u026as/", "\u8fd9\uff1b\u8fd9\u4e2a"],
["is", "/\u026az/", "\u662f"],
["too", "/tu\u02d0/", "\u4e5f"]
]
},
{
title: "\u4e09\u5e74\u7ea7\u4e0a\u518c Unit 4 My family",
entries: [
["family", "/\u02c8f\u00e6m\u0259li/", "\u5bb6\u5ead\uff1b\u5bb6\u4eba"],
["father", "/\u02c8f\u0251\u02d0\u00f0\u0259(r)/", "\u7236\u4eb2"],
["mother", "/\u02c8m\u028c\u00f0\u0259(r)/", "\u6bcd\u4eb2"],
["brother", "/\u02c8br\u028c\u00f0\u0259(r)/", "\u5144\uff1b\u5f1f"],
["sister", "/\u02c8s\u026ast\u0259(r)/", "\u59d0\uff1b\u59b9"],
["grandpa", "/\u02c8\u0261r\u00e6n(d)p\u0251\u02d0/", "\u7237\u7237\uff1b\u5916\u516c"],
["grandma", "/\u02c8\u0261r\u00e6n(d)m\u0251\u02d0/", "\u5976\u5976\uff1b\u5916\u5a46"],
["me", "/mi\u02d0/", "\u6211"]
]
},
{
title: "\u4e09\u5e74\u7ea7\u4e0a\u518c Unit 5 Look at me",
entries: [
["look at", "/l\u028ak \u00e6t/", "\u770b"],
["T-shirt", "/\u02c8ti\u02d0 \u0283\u025c\u02d0t/", "T\u6064\u886b"],
["skirt", "/sk\u025c\u02d0t/", "\u88d9\u5b50"],
["cap", "/k\u00e6p/", "\u5e3d\u5b50"],
["jacket", "/\u02c8d\u0292\u00e6k\u026at/", "\u5939\u514b\u886b"],
["nice", "/na\u026as/", "\u597d\u770b\u7684\uff1b\u597d\u7684"],
["great", "/\u0261re\u026at/", "\u597d\u6781\u4e86"]
]
},
{
title: "\u4e09\u5e74\u7ea7\u4e0a\u518c Unit 6 Colours",
entries: [
["red", "/red/", "\u7ea2\u8272"],
["orange", "/\u02c8\u0252r\u026and\u0292/", "\u6a59\u8272"],
["yellow", "/\u02c8jel\u0259\u028a/", "\u9ec4\u8272"],
["green", "/\u0261ri\u02d0n/", "\u7eff\u8272"],
["blue", "/blu\u02d0/", "\u84dd\u8272"],
["brown", "/bra\u028an/", "\u68d5\u8272"],
["white", "/wa\u026at/", "\u767d\u8272"],
["black", "/bl\u00e6k/", "\u9ed1\u8272"],
["and", "/\u00e6nd/", "\u548c"],
["now", "/na\u028a/", "\u73b0\u5728"]
]
},
{
title: "\u4e09\u5e74\u7ea7\u4e0a\u518c Unit 7 Would you like a pie",
entries: [
["would like", "/w\u028ad la\u026ak/", "\u60f3\u8981"],
["pie", "/pa\u026a/", "\u9985\u997c"],
["cake", "/ke\u026ak/", "\u86cb\u7cd5"],
["egg", "/e\u0261/", "\u9e21\u86cb"],
["sweet", "/swi\u02d0t/", "\u7cd6\u679c\uff1b\u751c\u7684"],
["ice cream", "/\u02cca\u026as \u02c8kri\u02d0m/", "\u51b0\u6fc0\u51cc"],
["hot dog", "/\u02c8h\u0252t d\u0252\u0261/", "\u70ed\u72d7"],
["what about", "/w\u0252t \u0259\u02c8ba\u028at/", "\u2026\u2026\u600e\u4e48\u6837"]
]
},
{
title: "\u4e09\u5e74\u7ea7\u4e0a\u518c Unit 8 Happy New Year",
entries: [
["happy", "/\u02c8h\u00e6pi/", "\u9ad8\u5174\u7684\uff1b\u5feb\u4e50\u7684"],
["new", "/nju\u02d0/", "\u65b0\u7684"],
["year", "/j\u026a\u0259(r)/", "\u5e74"],
["Happy New Year", "/\u02cch\u00e6pi nju\u02d0 \u02c8j\u026a\u0259(r)/", "\u65b0\u5e74\u5feb\u4e50"],
["uncle", "/\u02c8\u028c\u014bkl/", "\u53d4\u53d4\uff1b\u8205\u8205"],
["aunt", "/\u0251\u02d0nt/", "\u963f\u59e8\uff1b\u59d1\u5988"],
["doll", "/d\u0252l/", "\u73a9\u5177\u5a03\u5a03"],
["ball", "/b\u0254\u02d0l/", "\u7403"],
["robot", "/\u02c8r\u0259\u028ab\u0252t/", "\u673a\u5668\u4eba"],
["toy car", "/t\u0254\u026a k\u0251\u02d0(r)/", "\u73a9\u5177\u5c0f\u6c7d\u8f66"]
]
},
{
title: "\u4e09\u5e74\u7ea7\u4e0b\u518c Unit 1 In class",
entries: [
["in class", "/\u026an kl\u0251\u02d0s/", "\u5728\u4e0a\u8bfe"],
["open", "/\u02c8\u0259\u028ap\u0259n/", "\u6253\u5f00"],
["the", "/\u00f0\u0259/", "\u8fd9\uff1b\u90a3\uff08\u5b9a\u51a0\u8bcd\uff09"],
["door", "/d\u0254\u02d0(r)/", "\u95e8"],
["window", "/\u02c8w\u026and\u0259\u028a/", "\u7a97\u6237"],
["book", "/b\u028ak/", "\u4e66"],
["stand up", "/st\u00e6nd \u028cp/", "\u8d77\u7acb"],
["sit down", "/s\u026at da\u028an/", "\u5750\u4e0b"],
["please", "/pli\u02d0z/", "\u8bf7"],
["come in", "/k\u028cm \u026an/", "\u8fdb\u6765"],
["close", "/kl\u0259\u028az/", "\u5173\u4e0a"],
["blackboard", "/\u02c8bl\u00e6kb\u0254\u02d0d/", "\u9ed1\u677f"]
]
},
{
title: "\u4e09\u5e74\u7ea7\u4e0b\u518c Unit 2 In the library",
entries: [
["library", "/\u02c8la\u026abr\u0259ri/", "\u56fe\u4e66\u9986"],
["shout", "/\u0283a\u028at/", "\u558a\uff1b\u53eb"],
["eat", "/i\u02d0t/", "\u5403"],
["run", "/r\u028cn/", "\u8dd1"],
["talk", "/t\u0254\u02d0k/", "\u8bf4\u8bdd"],
["sleep", "/sli\u02d0p/", "\u7761\u89c9"],
["drink", "/dr\u026a\u014bk/", "\u559d"],
["here", "/h\u026a\u0259(r)/", "\u8fd9\u91cc"],
["English", "/\u02c8\u026a\u014b\u0261l\u026a\u0283/", "\u82f1\u8bed"],
["milk", "/m\u026alk/", "\u725b\u5976"]
]
},
{
title: "\u4e09\u5e74\u7ea7\u4e0b\u518c Unit 3 Is this your pencil",
entries: [
["pencil", "/\u02c8pensl/", "\u94c5\u7b14"],
["pen", "/pen/", "\u94a2\u7b14"],
["ruler", "/\u02c8ru\u02d0l\u0259(r)/", "\u5c3a"],
["rubber", "/\u02c8r\u028cb\u0259(r)/", "\u6a61\u76ae"],
["crayon", "/\u02c8kre\u026a\u0259n/", "\u8721\u7b14"],
["schoolbag", "/\u02c8sku\u02d0lb\u00e6\u0261/", "\u4e66\u5305"],
["pencil case", "/\u02c8pensl ke\u026as/", "\u94c5\u7b14\u76d2"],
["lunch box", "/\u02c8l\u028cnt\u0283 b\u0252ks/", "\u5348\u9910\u76d2"],
["where", "/we\u0259(r)/", "\u5728\u54ea\u91cc"],
["over there", "/\u02c8\u0259\u028av\u0259(r) \u00f0e\u0259(r)/", "\u5728\u90a3\u91cc"]
]
},
{
title: "\u4e09\u5e74\u7ea7\u4e0b\u518c Unit 4 Where's the bird",
entries: [
["bird", "/b\u025c\u02d0d/", "\u9e1f"],
["under", "/\u02c8\u028cnd\u0259(r)/", "\u5728\u2026\u2026\u4e0b\u9762"],
["behind", "/b\u026a\u02c8ha\u026and/", "\u5728\u2026\u2026\u540e\u9762"],
["on", "/\u0252n/", "\u5728\u2026\u2026\u4e0a\u9762"],
["desk", "/desk/", "\u4e66\u684c"],
["chair", "/t\u0283e\u0259(r)/", "\u6905\u5b50"],
["in", "/\u026an/", "\u5728\u2026\u2026\u91cc\u9762"],
["tree", "/tri\u02d0/", "\u6811"],
["beautiful", "/\u02c8bju\u02d0t\u026afl/", "\u6f02\u4eae\u7684"]
]
},
{
title: "\u4e09\u5e74\u7ea7\u4e0b\u518c Unit 5 How old are you",
entries: [
["one", "/w\u028cn/", "\u4e00"],
["two", "/tu\u02d0/", "\u4e8c"],
["three", "/\u03b8ri\u02d0/", "\u4e09"],
["four", "/f\u0254\u02d0(r)/", "\u56db"],
["five", "/fa\u026av/", "\u4e94"],
["six", "/s\u026aks/", "\u516d"],
["seven", "/\u02c8sevn/", "\u4e03"],
["eight", "/e\u026at/", "\u516b"],
["nine", "/na\u026an/", "\u4e5d"],
["ten", "/ten/", "\u5341"],
["how old", "/ha\u028a \u0259\u028ald/", "\u51e0\u5c81"],
["right", "/ra\u026at/", "\u5bf9\u7684\uff1b\u6b63\u786e\u7684"]
]
},
{
title: "\u4e09\u5e74\u7ea7\u4e0b\u518c Unit 6 What time is it",
entries: [
["what time", "/w\u0252t ta\u026am/", "\u51e0\u70b9"],
["wake up", "/we\u026ak \u028cp/", "\u9192\uff1b\u9192\u6765"],
["o'clock", "/\u0259\u02c8kl\u0252k/", "\u2026\u2026\u70b9\u949f"],
["breakfast", "/\u02c8brekf\u0259st/", "\u65e9\u9910"],
["lunch", "/l\u028cnt\u0283/", "\u5348\u9910"],
["dinner", "/\u02c8d\u026an\u0259(r)/", "\u665a\u9910"],
["class", "/kl\u0251\u02d0s/", "\u8bfe\uff1b\u4e0a\u8bfe"],
["bed", "/bed/", "\u5e8a"],
["OK", "/\u02cc\u0259\u028a\u02c8ke\u026a/", "\u597d\uff1b\u53ef\u4ee5\u4e86"]
]
},
{
title: "\u4e09\u5e74\u7ea7\u4e0b\u518c Unit 7 On the farm",
entries: [
["farm", "/f\u0251\u02d0m/", "\u519c\u573a"],
["they", "/\u00f0e\u026a/", "\u5b83\u4eec\uff1b\u4ed6\u4eec"],
["pig", "/p\u026a\u0261/", "\u732a"],
["cow", "/ka\u028a/", "\u5976\u725b"],
["chicken", "/\u02c8t\u0283\u026ak\u026an/", "\u9e21"],
["duck", "/d\u028ck/", "\u9e2d"],
["pear", "/pe\u0259(r)/", "\u68a8"],
["apple", "/\u02c8\u00e6pl/", "\u82f9\u679c"],
["orange", "/\u02c8\u0252r\u026and\u0292/", "\u6a59\u5b50"],
["those", "/\u00f0\u0259\u028az/", "\u90a3\u4e9b"]
]
},
{
title: "\u4e09\u5e74\u7ea7\u4e0b\u518c Unit 8 We're twins",
entries: [
["we", "/wi\u02d0/", "\u6211\u4eec"],
["we're = we are", "/w\u026a\u0259(r)/", "\u6211\u4eec\u662f"],
["twin", "/tw\u026an/", "\u53cc\u80de\u80ce\u4e4b\u4e00"],
["man", "/m\u00e6n/", "\u7537\u4eba"],
["woman", "/\u02c8w\u028am\u0259n/", "\u5973\u4eba"],
["boy", "/b\u0254\u026a/", "\u7537\u5b69"],
["girl", "/\u0261\u025c\u02d0l/", "\u5973\u5b69"],
["baby", "/\u02c8be\u026abi/", "\u5a74\u513f"],
["cousin", "/\u02c8k\u028czn/", "\u5802\uff08\u8868\uff09\u5144\u5f1f\u59d0\u59b9"],
["name", "/ne\u026am/", "\u540d\u5b57"]
]
}
];
function buildHtml(title, note, units) {
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>${title}</title>
<style>
body { font-family: "Microsoft YaHei", "SimSun", sans-serif; margin: 28px; line-height: 1.65; }
h1 { color: #4f378b; font-size: 20pt; margin: 0 0 12px; }
h2 { color: #2b4c7e; font-size: 14pt; margin: 18px 0 8px; border-bottom: 1px solid #d9e2f3; padding-bottom: 4px; }
p.note { color: #444; margin: 0 0 18px; font-size: 10.5pt; }
table { width: 100%; border-collapse: collapse; margin-bottom: 14px; }
td { border-bottom: 1px solid #eee; padding: 6px 4px; vertical-align: top; font-size: 11pt; }
td.word { color: #c00000; width: 28%; font-weight: 700; }
td.phonetic { color: #0066cc; width: 24%; }
td.meaning { color: #008000; width: 48%; }
</style>
</head>
<body>
<h1>${title}</h1>
<p class="note">${note}</p>
${units.map((unit) => `
<h2>${unit.title}</h2>
<table>
${unit.entries.map((entry) => `
<tr>
<td class="word">${entry[0]}</td>
<td class="phonetic">${entry[1]}</td>
<td class="meaning">${entry[2]}</td>
</tr>
`).join("")}
</table>
`).join("")}
</body>
</html>`;
}
function writeDoc(fileName, title, note, units) {
const outputPath = path.join(process.cwd(), fileName);
const html = buildHtml(title, note, units);
fs.writeFileSync(outputPath, "\uFEFF" + html, "utf8");
return outputPath;
}
const upperUnits = allUnits.filter((unit) => unit.title.includes("\u4e09\u5e74\u7ea7\u4e0a\u518c"));
const lowerUnits = allUnits.filter((unit) => unit.title.includes("\u4e09\u5e74\u7ea7\u4e0b\u518c"));
const outputs = [
writeDoc(
"\u6c5f\u82cf\u4e09\u5e74\u7ea7\u82f1\u8bed\u8bd1\u6797\u7248\u5355\u8bcd_\u4e0a\u4e0b\u518c_20260706.doc",
"\u6c5f\u82cf\u4e09\u5e74\u7ea7\u82f1\u8bed\u8bd1\u6797\u7248\u5355\u8bcd\u6c47\u603b",
"\u6574\u7406\u4e09\u5e74\u7ea7\u4e0a\u518c + \u4e0b\u518c\u3002\u82f1\u6587\u5355\u8bcd\u4e3a\u7ea2\u8272\uff0c\u97f3\u6807\u4e3a\u84dd\u8272\uff0c\u4e2d\u6587\u91ca\u4e49\u4e3a\u7eff\u8272\u3002",
allUnits
),
writeDoc(
"\u6c5f\u82cf\u4e09\u5e74\u7ea7\u82f1\u8bed\u8bd1\u6797\u7248\u4e0a\u518c\u5355\u8bcd_20260706.doc",
"\u6c5f\u82cf\u4e09\u5e74\u7ea7\u82f1\u8bed\u8bd1\u6797\u7248\u4e0a\u518c\u5355\u8bcd",
"\u4ec5\u5305\u542b\u4e09\u5e74\u7ea7\u4e0a\u518c\u3002\u82f1\u6587\u5355\u8bcd\u4e3a\u7ea2\u8272\uff0c\u97f3\u6807\u4e3a\u84dd\u8272\uff0c\u4e2d\u6587\u91ca\u4e49\u4e3a\u7eff\u8272\u3002",
upperUnits
),
writeDoc(
"\u6c5f\u82cf\u4e09\u5e74\u7ea7\u82f1\u8bed\u8bd1\u6797\u7248\u4e0b\u518c\u5355\u8bcd_20260706.doc",
"\u6c5f\u82cf\u4e09\u5e74\u7ea7\u82f1\u8bed\u8bd1\u6797\u7248\u4e0b\u518c\u5355\u8bcd",
"\u4ec5\u5305\u542b\u4e09\u5e74\u7ea7\u4e0b\u518c\u3002\u82f1\u6587\u5355\u8bcd\u4e3a\u7ea2\u8272\uff0c\u97f3\u6807\u4e3a\u84dd\u8272\uff0c\u4e2d\u6587\u91ca\u4e49\u4e3a\u7eff\u8272\u3002",
lowerUnits
)
];
outputs.forEach((outputPath) => console.log(outputPath));

View File

@@ -108,6 +108,12 @@ public class JsonToXmlConversionService {
String iedName = mappingDocument.getIed(); String iedName = mappingDocument.getIed();
String ldPrefix = mappingDocument.getLd(); String ldPrefix = mappingDocument.getLd();
String waveTimeFlag = mappingDocument.getWaveTimeFlag();
if (waveTimeFlag.startsWith("U") || waveTimeFlag.startsWith("u")){
waveTimeFlag= waveTimeFlag.toUpperCase();
}else{
waveTimeFlag= waveTimeFlag.toLowerCase();
}
if (iedName != null && !iedName.isEmpty()) { if (iedName != null && !iedName.isEmpty()) {
xmlContent = xmlContent.replaceAll( xmlContent = xmlContent.replaceAll(
@@ -123,6 +129,14 @@ public class JsonToXmlConversionService {
); );
} }
// 从 JSON 中读取 WaveTimeFlag 替换模板中的硬编码值
if (waveTimeFlag != null && !waveTimeFlag.isEmpty()) {
xmlContent = xmlContent.replaceAll(
"<ComtradeFile\\s+WaveTimeFlag=\"[^\"]*\"",
"<ComtradeFile WaveTimeFlag=\"" + escapeXml(waveTimeFlag) + "\""
);
}
return xmlContent; return xmlContent;
} }
@@ -174,12 +188,15 @@ public class JsonToXmlConversionService {
String name = reportItem.getName(); String name = reportItem.getName();
if (name.contains("urcbFluc") || if (name.contains("urcbFluc") ||
name.contains("urcbStHarm") || name.contains("brcbFluc") ||
name.contains("urcbStIHarm") || name.contains("urcbRtHarm") ||
name.contains("urcbStMMXU") || name.contains("urcbRtIHarm") ||
name.contains("urcbStMSQI") || name.contains("urcbRtMMXU") ||
name.contains("urcbRtMSQI") ||
name.contains("urcbPLT") || name.contains("urcbPLT") ||
name.contains("urcbPST") || name.contains("urcbPST") ||
name.contains("brcbPLT") ||
name.contains("brcbPST") ||
name.contains("urcbFlicker") || name.contains("urcbFlicker") ||
name.contains("urcbStatistic")|| name.contains("urcbStatistic")||
name.contains("urcbRealData")) { name.contains("urcbRealData")) {
@@ -246,11 +263,20 @@ public class JsonToXmlConversionService {
}else{ }else{
sb.append(",60"); sb.append(",60");
} }
sb.append(",1"); if(rc.getName().contains("urcb")){
sb.append(",0"); sb.append(",0");
sb.append(",0"); sb.append(",0");
sb.append(",0"); sb.append(",0");
sb.append(",0"); sb.append(",1");
sb.append(",0");
}else{
sb.append(",1");
sb.append(",0");
sb.append(",0");
sb.append(",0");
sb.append(",0");
}
sb.append(",yes"); sb.append(",yes");
sb.append(",1"); sb.append(",1");
sb.append(",1"); sb.append(",1");
@@ -748,6 +774,7 @@ public class JsonToXmlConversionService {
if(2==configType && fullKey.contains("线电压总有效值")){ if(2==configType && fullKey.contains("线电压总有效值")){
daPath = "1_" + daPath; daPath = "1_" + daPath;
} }
int offset=calculateOffset(sdiItem.getName(), typeItem.getName(), doiItem);
MetricInfo info = new MetricInfo(); MetricInfo info = new MetricInfo();
info.setLnClass(dataSetGroup.getLnClass()); info.setLnClass(dataSetGroup.getLnClass());
info.setLnInst(instItem.getInst()); info.setLnInst(instItem.getInst());
@@ -758,6 +785,7 @@ public class JsonToXmlConversionService {
info.setStart(doiItem.getStart()); info.setStart(doiItem.getStart());
info.setEnd(doiItem.getEnd()); info.setEnd(doiItem.getEnd());
info.setBaseFlag(doiItem.getBaseflag()); info.setBaseFlag(doiItem.getBaseflag());
info.setOffset(offset);
metrics.put(fullKey.toLowerCase(), info); metrics.put(fullKey.toLowerCase(), info);
} }
} }
@@ -806,10 +834,15 @@ public class JsonToXmlConversionService {
int start = doiItem.getStart(); int start = doiItem.getStart();
if (baseflag == 1) { if (baseflag == 1) {
boolean isInterHarmonic = isInterHarmonicMetric(doiItem); boolean isInterHarmonic = isInterHarmonicMetric(doiItem);//间谐波判断
int m = isInterHarmonic ? 0 : 2; int m = isInterHarmonic ? 0 : 2;
int i = m - start; int i = m - start;
sb.append("[%-").append(i).append("]"); //判断是Offset是否需要
if(i<0){
sb.append("[%-0]");
}else {
sb.append("[%-").append(i).append("]");
}
} else { } else {
sb.append("[").append(start).append("]"); sb.append("[").append(start).append("]");
} }
@@ -825,6 +858,28 @@ public class JsonToXmlConversionService {
} }
return sb.toString(); return sb.toString();
} }
private int calculateOffset(String sdiName, String typeName, DoiItem doiItem) {
int offset=0;
if (sdiName != null && !sdiName.isEmpty()){
if (sdiName.endsWith("Har") && doiItem != null) {
int baseflag = doiItem.getBaseflag();
int start = doiItem.getStart();
if (baseflag == 1) {
boolean isInterHarmonic = isInterHarmonicMetric(doiItem);//间谐波判断
int m = isInterHarmonic ? 0 : 2;
int i = m - start;
//判断是Offset是否需要
if(i<0){
offset = i;
}
}
}
}
return offset;
}
/** /**
* 标准化相别名称,将具体相别转换为通配符格式 * 标准化相别名称,将具体相别转换为通配符格式
@@ -883,6 +938,8 @@ public class JsonToXmlConversionService {
MetricInfo metric = mappingMetrics.get(rule.getName()); MetricInfo metric = mappingMetrics.get(rule.getName());
rule.setDoPath(metric.getDoPath()); rule.setDoPath(metric.getDoPath());
rule.setDaPath(metric.getDaPath()); rule.setDaPath(metric.getDaPath());
// 传递offset值将Integer转换为String
rule.setOffset(String.valueOf(metric.getOffset()));
applicable.put(ruleKey, rule); applicable.put(ruleKey, rule);
matched = true; matched = true;
addMethodDescribe(methodDescribeList, "✓ 规则匹配成功: " + ruleKey + " -> DO=" + rule.getDoPath() + ", DA=" + rule.getDaPath()); addMethodDescribe(methodDescribeList, "✓ 规则匹配成功: " + ruleKey + " -> DO=" + rule.getDoPath() + ", DA=" + rule.getDaPath());
@@ -1043,10 +1100,11 @@ public class JsonToXmlConversionService {
RuleBasedXmlMappingService.ValueRule matchedRule = applicableRules.get(desc); RuleBasedXmlMappingService.ValueRule matchedRule = applicableRules.get(desc);
if (matchedRule != null) { if (matchedRule != null) {
StringBuilder valueNode = new StringBuilder("<Value "); StringBuilder valueNode = new StringBuilder("<Value ");
valueNode.append("name=\"").append(escapeXml(name)).append("\" "); // 如果offset<0需要调整指标名中的%int或int%格式
String adjustedName = adjustPathWithOffset(name, matchedRule.getOffset());
valueNode.append("name=\"").append(escapeXml(adjustedName)).append("\" ");
valueNode.append("desc=\"").append(escapeXml(desc)).append("\" "); valueNode.append("desc=\"").append(escapeXml(desc)).append("\" ");
valueNode.append("type=\"").append(type).append("\" "); valueNode.append("type=\"").append(type).append("\" ");
if (matchedRule.getDoPath() != null && !matchedRule.getDoPath().isEmpty()) { if (matchedRule.getDoPath() != null && !matchedRule.getDoPath().isEmpty()) {
valueNode.append("DO=\"").append(escapeXml(matchedRule.getDoPath())).append("\" "); valueNode.append("DO=\"").append(escapeXml(matchedRule.getDoPath())).append("\" ");
} }
@@ -1065,7 +1123,7 @@ public class JsonToXmlConversionService {
if (matchedRule.getCoefficient() != null && !matchedRule.getCoefficient().isEmpty()) { if (matchedRule.getCoefficient() != null && !matchedRule.getCoefficient().isEmpty()) {
valueNode.append("Coefficient=\"").append(escapeXml(matchedRule.getCoefficient())).append("\" "); valueNode.append("Coefficient=\"").append(escapeXml(matchedRule.getCoefficient())).append("\" ");
} }
if (matchedRule.getOffset() != null && !matchedRule.getOffset().isEmpty()) { if (matchedRule.getOffset() != null && !matchedRule.getOffset().isEmpty() && !matchedRule.getOffset().equals("0")) {
valueNode.append("Offset=\"").append(escapeXml(matchedRule.getOffset())).append("\" "); valueNode.append("Offset=\"").append(escapeXml(matchedRule.getOffset())).append("\" ");
} }
if (matchedRule.getPltFlag() != null && !matchedRule.getPltFlag().isEmpty()) { if (matchedRule.getPltFlag() != null && !matchedRule.getPltFlag().isEmpty()) {
@@ -1092,6 +1150,48 @@ public class JsonToXmlConversionService {
.replace("\"", "&quot;") .replace("\"", "&quot;")
.replace("'", "&apos;"); .replace("'", "&apos;");
} }
/**
* 根据offset调整路径DO或DA
* 当offset<0时对路径中的[%-int]或[int%]格式增加-offset的值
* @param path 原始路径DO或DA
* @param offsetStr offset字符串值
* @return 调整后的路径
*/
private String adjustPathWithOffset(String path, String offsetStr) {
if (path == null || offsetStr == null || offsetStr.isEmpty()) {
return path;
}
try {
int offset = Integer.parseInt(offsetStr);
// 只有当offset<0时才需要调整
if (offset >= 0) {
return path;
}
int adjustment = -offset;
// 匹配 %数字,数字% 格式并调整(如 %0,49%
java.util.regex.Pattern pattern3 = java.util.regex.Pattern.compile("%(\\d+),(\\d+)%");
java.util.regex.Matcher matcher3 = pattern3.matcher(path);
StringBuffer result3 = new StringBuffer();
while (matcher3.find()) {
int startVal = Integer.parseInt(matcher3.group(1));
int endVal = Integer.parseInt(matcher3.group(2));
int newStart = startVal + adjustment;
int newEnd = endVal + adjustment;
matcher3.appendReplacement(result3, "%"+newStart+","+newEnd+"%");
}
matcher3.appendTail(result3);
path = result3.toString();
return path;
} catch (NumberFormatException e) {
// 如果offset无法解析为整数返回原始路径
return path;
}
}
private void addMethodDescribe(List<String> methodDescribeList, String message) { private void addMethodDescribe(List<String> methodDescribeList, String message) {
if (methodDescribeList == null || message == null || message.trim().isEmpty()) { if (methodDescribeList == null || message == null || message.trim().isEmpty()) {
@@ -1208,7 +1308,9 @@ public class JsonToXmlConversionService {
private int end; private int end;
private int baseCount; private int baseCount;
private int baseFlag; private int baseFlag;
private int offset;
public int getOffset() {return offset;}
public void setOffset(int offset) {this.offset = offset;}
public String getLnClass() { return lnClass; } public String getLnClass() { return lnClass; }
public void setLnClass(String lnClass) { this.lnClass = lnClass; } public void setLnClass(String lnClass) { this.lnClass = lnClass; }
public String getLnInst() { return lnInst; } public String getLnInst() { return lnInst; }

View File

@@ -83,7 +83,7 @@ public class RuleBasedXmlMappingService {
public List<InputStream> loadDefaultRuleFile( ) throws Exception { public List<InputStream> loadDefaultRuleFile( ) throws Exception {
ClassPathResource ruleResource = new ClassPathResource("template/默认规则.txt"); ClassPathResource ruleResource = new ClassPathResource("template/DefaultRule.txt");
if ( !ruleResource.exists()) { if ( !ruleResource.exists()) {
return null; return null;
} }

View File

@@ -32,6 +32,6 @@ public class MappingModuleConfig {
private String jsonToXmlTemplatePath; private String jsonToXmlTemplatePath;
/** JSON转XML使用的默认规则文件路径。 */ /** JSON转XML使用的默认规则文件路径。 */
@Value("${icd.mapping.json-to-xml-rule-path:template/默认规则.txt}") @Value("${icd.mapping.json-to-xml-rule-path:template/DefaultRule.txt}")
private String jsonToXmlRulePath; private String jsonToXmlRulePath;
} }

View File

@@ -32,7 +32,8 @@
"Select": "FlickerFileMap", "Select": "FlickerFileMap",
"DataSetList": [ "DataSetList": [
"dsFlickerData", "dsFlickerData",
"dsPST" "dsPST",
"dsPLT"
], ],
"LnInstList": [ "LnInstList": [
"波动闪变值" "波动闪变值"

View File

@@ -43,10 +43,10 @@
<Value name="间谐波电压有效值序列间谐波平均值值" desc="间谐波电压含有率(f25-2475)平均值" type="6" DO="MHAI8$MX$HPhV" DA="phs*Har[%-0]$mag$f" /> <Value name="间谐波电压有效值序列间谐波平均值值" desc="间谐波电压含有率(f25-2475)平均值" type="6" DO="MHAI8$MX$HPhV" DA="phs*Har[%-0]$mag$f" />
<Value name="间谐波电压有效值序列间谐波最大值值" desc="间谐波电压含有率(f25-2475)最大值" type="6" DO="MHAI9$MX$HPhV" DA="phs*Har[%-0]$mag$f" /> <Value name="间谐波电压有效值序列间谐波最大值值" desc="间谐波电压含有率(f25-2475)最大值" type="6" DO="MHAI9$MX$HPhV" DA="phs*Har[%-0]$mag$f" />
<Value name="间谐波电压有效值序列间谐波最小值值" desc="间谐波电压含有率(f25-2475)最小值" type="6" DO="MHAI10$MX$HPhV" DA="phs*Har[%-0]$mag$f" /> <Value name="间谐波电压有效值序列间谐波最小值值" desc="间谐波电压含有率(f25-2475)最小值" type="6" DO="MHAI10$MX$HPhV" DA="phs*Har[%-0]$mag$f" />
<Value name="间谐波电压有效值序列间谐波95值值" desc="间谐波电压含有率(f25-2475)95值" type="6" DO="MHAI8$MX$HPhV" DA="phs*Har[%-0]$mag$f" /> <Value name="相电压间谐波含有率序列间谐波95值值" desc="间谐波电压含有率(f25-2475)95值" type="6" DO="MHAI8$MX$HPhV" DA="phs*Har[%-0]$mag$f" />
<Value name="间谐波电压有效值序列间谐波平均值值" desc="间谐波电压含有率(f25-2475)平均值" type="6" DO="MHAI8$MX$HPhV" DA="phs*Har[%-0]$mag$f" /> <Value name="相电压间谐波含有率序列间谐波平均值值" desc="间谐波电压含有率(f25-2475)平均值" type="6" DO="MHAI8$MX$HPhV" DA="phs*Har[%-0]$mag$f" />
<Value name="间谐波电压有效值序列间谐波最大值值" desc="间谐波电压含有率(f25-2475)最大值" type="6" DO="MHAI9$MX$HPhV" DA="phs*Har[%-0]$mag$f" /> <Value name="相电压间谐波含有率序列间谐波最大值值" desc="间谐波电压含有率(f25-2475)最大值" type="6" DO="MHAI9$MX$HPhV" DA="phs*Har[%-0]$mag$f" />
<Value name="间谐波电压有效值序列间谐波最小值值" desc="间谐波电压含有率(f25-2475)最小值" type="6" DO="MHAI10$MX$HPhV" DA="phs*Har[%-0]$mag$f" /> <Value name="相电压间谐波含有率序列间谐波最小值值" desc="间谐波电压含有率(f25-2475)最小值" type="6" DO="MHAI10$MX$HPhV" DA="phs*Har[%-0]$mag$f" />
<Value name="相电压谐波含有率序列95值值" desc="谐波电压含有率(2-50)95值" type="6" DO="MHAI2$MX$HPhV" DA="phs*Har[%-2]$mag$f" /> <Value name="相电压谐波含有率序列95值值" desc="谐波电压含有率(2-50)95值" type="6" DO="MHAI2$MX$HPhV" DA="phs*Har[%-2]$mag$f" />
<Value name="相电压谐波含有率序列平均值值" desc="谐波电压含有率(2-50)平均值" type="6" DO="MHAI2$MX$HPhV" DA="phs*Har[%-2]$mag$f" /> <Value name="相电压谐波含有率序列平均值值" desc="谐波电压含有率(2-50)平均值" type="6" DO="MHAI2$MX$HPhV" DA="phs*Har[%-2]$mag$f" />
@@ -281,6 +281,7 @@
<Value name="相电压基波有效值实时数据值" desc="实时基波电压有效值" type="6" DO="MHAI$MX$FundPhV" DA="phs*$cVal$mag$f" Coefficient="1" /> <Value name="相电压基波有效值实时数据值" desc="实时基波电压有效值" type="6" DO="MHAI$MX$FundPhV" DA="phs*$cVal$mag$f" Coefficient="1" />
<Value name="相电压谐波含有率序列实时数据值" desc="实时2-50次谐波电压含有率" type="6" DO="MHAI$MX$HPhV" DA="phs*Har[%-2]$mag$f" Coefficient="1" Offset="0" /> <Value name="相电压谐波含有率序列实时数据值" desc="实时2-50次谐波电压含有率" type="6" DO="MHAI$MX$HPhV" DA="phs*Har[%-2]$mag$f" Coefficient="1" Offset="0" />
<Value name="间谐波电压有效值序列间谐波实时数据值" desc="实时1-50次间谐波电压含有率" type="6" DO="MHAI$MX$HPhV" DA="phs*Har[%-0]$mag$f" Coefficient="1" Offset="0" /> <Value name="间谐波电压有效值序列间谐波实时数据值" desc="实时1-50次间谐波电压含有率" type="6" DO="MHAI$MX$HPhV" DA="phs*Har[%-0]$mag$f" Coefficient="1" Offset="0" />
<Value name="相电压间谐波含有率序列间谐波实时数据值" desc="实时1-50次间谐波电压含有率" type="6" DO="MHAI$MX$HPhV" DA="phs*Har[%-0]$mag$f" Coefficient="1" Offset="0" />
<Value name="谐波电压有效值序列实时数据角度" desc="实时谐波电压相角(2-50)" type="6" DO="MHAI$MX$HPhV" DA="phs*Har[%-2]$ang$f" /> <Value name="谐波电压有效值序列实时数据角度" desc="实时谐波电压相角(2-50)" type="6" DO="MHAI$MX$HPhV" DA="phs*Har[%-2]$ang$f" />
<Value name="相电压谐波含有率序列实时数据角度" desc="实时谐波电压相角(2-50)" type="6" DO="MHAI$MX$HPhV" DA="phs*Har[%-2]$ang$f" /> <Value name="相电压谐波含有率序列实时数据角度" desc="实时谐波电压相角(2-50)" type="6" DO="MHAI$MX$HPhV" DA="phs*Har[%-2]$ang$f" />
<Value name="线电压偏差实时数据值" desc="实时线电压偏差" type="6" DO="MMXU$MX$LnVolDev" DA="phs*$cVal$mag$f" Coefficient="1"/> <Value name="线电压偏差实时数据值" desc="实时线电压偏差" type="6" DO="MMXU$MX$LnVolDev" DA="phs*$cVal$mag$f" Coefficient="1"/>

View File

@@ -8,11 +8,11 @@
<!--角型接线时相角为0不再进行DO、DA路径值扩展--> <!--角型接线时相角为0不再进行DO、DA路径值扩展-->
<JSConfigTemplate version="2021-12-15" author="ww" SelectStat="JiangSu" SelectReal="Kafka Producer" desc="昆明长水机场增加谐波类数据配置"> <JSConfigTemplate version="2021-12-15" author="ww" SelectStat="JiangSu" SelectReal="Kafka Producer" desc="昆明长水机场增加谐波类数据配置">
<!--注:暂态事件解析规则配置 Flag0-不分相 1-分相 如果Flag=0 ABC配置成一样如果Flag=1ABC根据实际配置--> <!--注:暂态事件解析规则配置 Flag0-不分相 1-分相 如果Flag=0 ABC配置成一样如果Flag=1ABC根据实际配置-->
<WavePhasic Flag="1" A="QVVR1" B="QVVR2" C="QVVR3" /> <WavePhasic Flag="1" A="QVVR0" B="QVVR1" C="QVVR2" />
<!--暂态事件持续事件单位0-毫秒 1---> <!--暂态事件持续事件单位0-毫秒 1--->
<UnitOfTime Unit="0" /> <UnitOfTime Unit="0" />
<!--上送值的时间UTC-UTC时间 beijing-北京时间--> <!--上送值的时间UTC-UTC时间 beijing-北京时间-->
<ValueOfTime Unit="utc" /> <ValueOfTime Unit="UTC" />
<!--录波文件的时间UTC-UTC时间 beijing-北京时间--> <!--录波文件的时间UTC-UTC时间 beijing-北京时间-->
<ComtradeFile WaveTimeFlag="beijing" /> <ComtradeFile WaveTimeFlag="beijing" />
<IED name="PQMonitor" desc="电能质量监测装置" /> <IED name="PQMonitor" desc="电能质量监测装置" />
@@ -217,14 +217,14 @@
<Value name="SI_%0,49%" desc="间谐波电流幅值(f25-2475)平均值" type="6" DO="" DA="" /> <Value name="SI_%0,49%" desc="间谐波电流幅值(f25-2475)平均值" type="6" DO="" DA="" />
<Value name="MAX_SI_%0,49%" desc="间谐波电流幅值(f25-2475)最大值" type="6" DO="" DA="" /> <Value name="MAX_SI_%0,49%" desc="间谐波电流幅值(f25-2475)最大值" type="6" DO="" DA="" />
<Value name="MIN_SI_%0,49%" desc="间谐波电流幅值(f25-2475)最小值" type="6" DO="" DA="" /> <Value name="MIN_SI_%0,49%" desc="间谐波电流幅值(f25-2475)最小值" type="6" DO="" DA="" />
<Value name="G_I%1,50%" desc="谐波电流幅值(2-50)95值" type="6" DO="" DA="" /> <Value name="G_I%2,50%" desc="谐波电流幅值(2-50)95值" type="6" DO="" DA="" />
<Value name="I%1,50%" desc="谐波电流幅值(2-50)平均值" type="6" DO="" DA="" /> <Value name="I%2,50%" desc="谐波电流幅值(2-50)平均值" type="6" DO="" DA="" />
<Value name="MAX_I%1,50%" desc="谐波电流幅值(2-50)最大值" type="6" DO="" DA="" /> <Value name="MAX_I%2,50%" desc="谐波电流幅值(2-50)最大值" type="6" DO="" DA="" />
<Value name="MIN_I%1,50%" desc="谐波电流幅值(2-50)最小值" type="6" DO="" DA="" /> <Value name="MIN_I%2,50%" desc="谐波电流幅值(2-50)最小值" type="6" DO="" DA="" />
<Value name="G_IA%1,50%" desc="谐波电流相角(2-50)95值" type="6" DO="" DA="" /> <Value name="G_IA%2,50%" desc="谐波电流相角(2-50)95值" type="6" DO="" DA="" />
<Value name="IA%1,50%" desc="谐波电流相角(2-50)平均值" type="6" DO="" DA="" /> <Value name="IA%2,50%" desc="谐波电流相角(2-50)平均值" type="6" DO="" DA="" />
<Value name="MAX_IA%1,50%" desc="谐波电流相角(2-50)最大值" type="6" DO="" DA="" /> <Value name="MAX_IA%2,50%" desc="谐波电流相角(2-50)最大值" type="6" DO="" DA="" />
<Value name="MIN_IA%1,50%" desc="谐波电流相角(2-50)最小值" type="6" DO="" DA="" /> <Value name="MIN_IA%2,50%" desc="谐波电流相角(2-50)最小值" type="6" DO="" DA="" />
<Value name="ITHD" desc="电流总谐波畸变率平均值" type="6" DO="" DA="" Coefficient="1" /> <Value name="ITHD" desc="电流总谐波畸变率平均值" type="6" DO="" DA="" Coefficient="1" />
<Value name="MAX_ITHD" desc="电流总谐波畸变率最大值" type="6" DO="" DA="" Coefficient="1" /> <Value name="MAX_ITHD" desc="电流总谐波畸变率最大值" type="6" DO="" DA="" Coefficient="1" />
<Value name="MIN_ITHD" desc="电流总谐波畸变率最小值" type="6" DO="" DA="" Coefficient="1" /> <Value name="MIN_ITHD" desc="电流总谐波畸变率最小值" type="6" DO="" DA="" Coefficient="1" />
@@ -244,8 +244,8 @@
<Value name="IZSEQ" desc="零序电流平均值" type="6" DO="" DA="" /> <Value name="IZSEQ" desc="零序电流平均值" type="6" DO="" DA="" />
<Value name="MAX_IZSEQ" desc="零序电流最大值" type="6" DO="" DA="" /> <Value name="MAX_IZSEQ" desc="零序电流最大值" type="6" DO="" DA="" />
<Value name="MIN_IZSEQ" desc="零序电流最小值" type="6" DO="" DA="" /> <Value name="MIN_IZSEQ" desc="零序电流最小值" type="6" DO="" DA="" />
<Value name="G_VZSEQ_UNBAN" desc="零序电流不平衡95值" type="6" DO="" DA="" /> <Value name="G_IZSEQ_UNBAN" desc="零序电流不平衡95值" type="6" DO="" DA="" />
<Value name="VZSEQ_UNBAN" desc="零序电流不平衡平均值" type="6" DO="" DA="" /> <Value name="IZSEQ_UNBAN" desc="零序电流不平衡平均值" type="6" DO="" DA="" />
<Value name="MAX_IZSEQ_UNBAN" desc="零序电流不平衡最大值" type="6" DO="" DA="" /> <Value name="MAX_IZSEQ_UNBAN" desc="零序电流不平衡最大值" type="6" DO="" DA="" />
<Value name="MIN_IZSEQ_UNBAN" desc="零序电流不平衡最小值" type="6" DO="" DA="" /> <Value name="MIN_IZSEQ_UNBAN" desc="零序电流不平衡最小值" type="6" DO="" DA="" />
<Value name="G_IPSEQ" desc="正序电流95值" type="6" DO="" DA="" /> <Value name="G_IPSEQ" desc="正序电流95值" type="6" DO="" DA="" />
@@ -277,18 +277,18 @@
<Value name="Q" desc="无功功率平均值" type="6" DO="" DA="" /> <Value name="Q" desc="无功功率平均值" type="6" DO="" DA="" />
<Value name="MAX_Q" desc="无功功率最大值" type="6" DO="" DA="" /> <Value name="MAX_Q" desc="无功功率最大值" type="6" DO="" DA="" />
<Value name="MIN_Q" desc="无功功率最小值" type="6" DO="" DA="" /> <Value name="MIN_Q" desc="无功功率最小值" type="6" DO="" DA="" />
<Value name="G_P%1,50%" desc="谐波有功功率(2-50)95值" type="6" DO="" DA="" /> <Value name="G_P%2,50%" desc="谐波有功功率(2-50)95值" type="6" DO="" DA="" />
<Value name="P%1,50%" desc="谐波有功功率(2-50)平均值" type="6" DO="" DA="" /> <Value name="P%2,50%" desc="谐波有功功率(2-50)平均值" type="6" DO="" DA="" />
<Value name="MAX_P%1,50%" desc="谐波有功功率(2-50)最大值" type="6" DO="" DA="" /> <Value name="MAX_P%2,50%" desc="谐波有功功率(2-50)最大值" type="6" DO="" DA="" />
<Value name="MIN_P%1,50%" desc="谐波有功功率(2-50)最小值" type="6" DO="" DA="" /> <Value name="MIN_P%2,50%" desc="谐波有功功率(2-50)最小值" type="6" DO="" DA="" />
<Value name="G_Q%1,50%" desc="谐波无功功率(2-50)95值" type="6" DO="" DA="" /> <Value name="G_Q%2,50%" desc="谐波无功功率(2-50)95值" type="6" DO="" DA="" />
<Value name="Q%1,50%" desc="谐波无功功率(2-50)平均值" type="6" DO="" DA="" /> <Value name="Q%2,50%" desc="谐波无功功率(2-50)平均值" type="6" DO="" DA="" />
<Value name="MAX_Q%1,50%" desc="谐波无功功率(2-50)最大值" type="6" DO="" DA="" /> <Value name="MAX_Q%2,50%" desc="谐波无功功率(2-50)最大值" type="6" DO="" DA="" />
<Value name="MIN_Q%1,50%" desc="谐波无功功率(2-50)最小值" type="6" DO="" DA="" /> <Value name="MIN_Q%2,50%" desc="谐波无功功率(2-50)最小值" type="6" DO="" DA="" />
<Value name="G_S%1,50%" desc="谐波视在功率(2-50)95值" type="6" DO="" DA="" /> <Value name="G_S%2,50%" desc="谐波视在功率(2-50)95值" type="6" DO="" DA="" />
<Value name="S%1,50%" desc="谐波视在功率(2-50)平均值" type="6" DO="" DA="" /> <Value name="S%2,50%" desc="谐波视在功率(2-50)平均值" type="6" DO="" DA="" />
<Value name="MAX_S%1,50%" desc="谐波视在功率(2-50)最大值" type="6" DO="" DA="" /> <Value name="MAX_S%2,50%" desc="谐波视在功率(2-50)最大值" type="6" DO="" DA="" />
<Value name="MIN_S%1,50%" desc="谐波视在功率(2-50)最小值" type="6" DO="" DA="" /> <Value name="MIN_S%2,50%" desc="谐波视在功率(2-50)最小值" type="6" DO="" DA="" />
<Value name="G_P" desc="有功功率95值" type="6" DO="" DA="" /> <Value name="G_P" desc="有功功率95值" type="6" DO="" DA="" />
<Value name="P" desc="有功功率平均值" type="6" DO="" DA="" /> <Value name="P" desc="有功功率平均值" type="6" DO="" DA="" />
<Value name="MAX_P" desc="有功功率最大值" type="6" DO="" DA="" /> <Value name="MAX_P" desc="有功功率最大值" type="6" DO="" DA="" />

View File

@@ -37,6 +37,11 @@
<version>1.2.83</version> <version>1.2.83</version>
</dependency> </dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.4.3</version>
</dependency>
</dependencies> </dependencies>

View File

@@ -25,7 +25,9 @@ public enum UserResponseEnum {
SUPER_ADMIN_REPEAT("A010013","超级管理员已存在,请勿重复添加" ), SUPER_ADMIN_REPEAT("A010013","超级管理员已存在,请勿重复添加" ),
RSA_DECRYT_ERROR("A010014","RSA解密失败" ), RSA_DECRYT_ERROR("A010014","RSA解密失败" ),
PASSWORD_SAME("A010015", "新密码不能与旧密码相同"), PASSWORD_SAME("A010015", "新密码不能与旧密码相同"),
OLD_PASSWORD_ERROR("A010016", "旧密码错误"), ; OLD_PASSWORD_ERROR("A010016", "旧密码错误"),
SIGNATURE_FILE_NAME_EMPTY("A010017", "签名文件名不能为空"),
SIGNATURE_FILE_TYPE_ERROR("A010018", "签名文件格式仅支持 png、jpg、jpeg、bmp、webp"), ;
private String code; private String code;
private String message; private String message;

View File

@@ -0,0 +1,200 @@
package com.njcn.gather.user.user.component;
import cn.hutool.core.util.StrUtil;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.user.user.config.StorageMinioProperties;
import io.minio.BucketExistsArgs;
import io.minio.GetObjectArgs;
import io.minio.GetObjectResponse;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.Locale;
/**
* MinIO 通用文件存储服务。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class SysFileMinioStorageService {
private final StorageMinioProperties properties;
public StoredObjectInfo save(String bucketName, String bizDir, String ownerId, String fileId, MultipartFile file) {
MultipartFile normalizedFile = requireFile(file);
validateConfig();
String originalFileName = normalizeFileName(normalizedFile.getOriginalFilename());
String extension = resolveExtension(originalFileName);
String objectName = buildObjectName(bizDir, ownerId, fileId, extension);
String contentType = resolveContentType(normalizedFile, originalFileName);
try (InputStream inputStream = normalizedFile.getInputStream()) {
MinioClient client = buildClient();
ensureBucket(client, bucketName);
client.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.stream(inputStream, normalizedFile.getSize(), -1)
.contentType(contentType)
.build());
return new StoredObjectInfo(originalFileName, bucketName, objectName, contentType, normalizedFile.getSize());
} catch (BusinessException exception) {
throw exception;
} catch (Exception exception) {
log.error("upload file to minio failed, bucketName={}, objectName={}", bucketName, objectName, exception);
throw new BusinessException(CommonResponseEnum.FAIL, "上传文件失败");
}
}
public StoredObject getObject(String bucketName, String objectName) {
validateConfig();
if (StrUtil.hasBlank(bucketName, objectName)) {
throw new BusinessException(CommonResponseEnum.FAIL, "文件不存在");
}
try {
GetObjectResponse response = buildClient().getObject(GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
return new StoredObject(response);
} catch (BusinessException exception) {
throw exception;
} catch (Exception exception) {
log.error("read file from minio failed, bucketName={}, objectName={}", bucketName, objectName, exception);
throw new BusinessException(CommonResponseEnum.FAIL, "读取文件失败");
}
}
public void deleteQuietly(String bucketName, String objectName) {
if (StrUtil.hasBlank(bucketName, objectName)) {
return;
}
try {
validateConfig();
buildClient().removeObject(RemoveObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build());
} catch (Exception exception) {
log.warn("delete file from minio failed, bucketName={}, objectName={}", bucketName, objectName, exception);
}
}
private MultipartFile requireFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "上传文件不能为空");
}
return file;
}
private void validateConfig() {
if (StrUtil.hasBlank(properties.getEndpoint(), properties.getAccessKey(), properties.getSecretKey())) {
throw new BusinessException(CommonResponseEnum.FAIL, "MinIO配置不完整");
}
}
private MinioClient buildClient() {
return MinioClient.builder()
.endpoint(properties.getEndpoint())
.credentials(properties.getAccessKey(), properties.getSecretKey())
.build();
}
private void ensureBucket(MinioClient client, String bucketName) throws Exception {
boolean exists = client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!exists) {
client.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
}
}
private String normalizeFileName(String originalFileName) {
String fileName = StrUtil.trimToEmpty(originalFileName);
if (StrUtil.isBlank(fileName)) {
throw new BusinessException(CommonResponseEnum.FAIL, "文件名不能为空");
}
int slashIndex = Math.max(fileName.lastIndexOf('/'), fileName.lastIndexOf('\\'));
return slashIndex >= 0 ? fileName.substring(slashIndex + 1) : fileName;
}
private String resolveExtension(String originalFileName) {
int dotIndex = originalFileName.lastIndexOf('.');
if (dotIndex < 0 || dotIndex == originalFileName.length() - 1) {
return "bin";
}
return originalFileName.substring(dotIndex + 1).toLowerCase(Locale.ENGLISH);
}
private String buildObjectName(String bizDir, String ownerId, String fileId, String extension) {
StringBuilder builder = new StringBuilder();
builder.append(trimPathSegment(bizDir));
if (StrUtil.isNotBlank(ownerId)) {
builder.append("/").append(trimPathSegment(ownerId));
}
builder.append("/").append(fileId);
if (StrUtil.isNotBlank(extension)) {
builder.append(".").append(extension);
}
return builder.toString();
}
private String trimPathSegment(String value) {
String text = StrUtil.trimToEmpty(value);
String normalized = text.replace("\\", "/");
while (normalized.startsWith("/")) {
normalized = normalized.substring(1);
}
while (normalized.endsWith("/")) {
normalized = normalized.substring(0, normalized.length() - 1);
}
if (StrUtil.isBlank(normalized)) {
throw new BusinessException(CommonResponseEnum.FAIL, "对象目录配置不合法");
}
return normalized;
}
private String resolveContentType(MultipartFile file, String originalFileName) {
if (StrUtil.isNotBlank(file.getContentType())) {
return file.getContentType();
}
return MediaTypeFactory.getMediaType(originalFileName)
.map(MediaType::toString)
.orElse(MediaType.APPLICATION_OCTET_STREAM_VALUE);
}
@Getter
public static class StoredObjectInfo {
private final String fileName;
private final String bucketName;
private final String objectName;
private final String contentType;
private final long fileSize;
public StoredObjectInfo(String fileName, String bucketName, String objectName, String contentType, long fileSize) {
this.fileName = fileName;
this.bucketName = bucketName;
this.objectName = objectName;
this.contentType = contentType;
this.fileSize = fileSize;
}
}
@Getter
public static class StoredObject {
private final GetObjectResponse inputStream;
public StoredObject(GetObjectResponse inputStream) {
this.inputStream = inputStream;
}
}
}

View File

@@ -0,0 +1,48 @@
package com.njcn.gather.user.user.config;
import cn.hutool.core.util.StrUtil;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* MinIO 通用配置。
*/
@Data
@Component
@ConfigurationProperties(prefix = "storage.minio")
public class StorageMinioProperties {
/**
* MinIO 服务地址。
*/
private String endpoint;
/**
* MinIO 访问账号。
*/
private String accessKey;
/**
* MinIO 访问密码。
*/
private String secretKey;
/**
* 业务桶配置。
*/
private Map<String, String> buckets = new LinkedHashMap<String, String>();
public String requireBucket(String bucketKey) {
String bucketName = buckets.get(bucketKey);
if (StrUtil.isBlank(bucketName)) {
throw new BusinessException(CommonResponseEnum.FAIL, "MinIO桶配置缺失" + bucketKey);
}
return bucketName;
}
}

View File

@@ -7,24 +7,35 @@ import com.njcn.common.pojo.constant.OperateType;
import com.njcn.common.pojo.enums.common.DataStateEnum; import com.njcn.common.pojo.enums.common.DataStateEnum;
import com.njcn.common.pojo.enums.common.LogEnum; import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum; 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.common.pojo.response.HttpResult;
import com.njcn.common.utils.LogUtil; import com.njcn.common.utils.LogUtil;
import com.njcn.gather.user.user.pojo.param.SysUserParam; import com.njcn.gather.user.user.pojo.param.SysUserParam;
import com.njcn.gather.user.user.pojo.po.SysFile;
import com.njcn.gather.user.user.pojo.po.SysRole; import com.njcn.gather.user.user.pojo.po.SysRole;
import com.njcn.gather.user.user.pojo.po.SysUser; import com.njcn.gather.user.user.pojo.po.SysUser;
import com.njcn.gather.user.user.pojo.po.SysUserSignature;
import com.njcn.gather.user.user.service.ISysFileService;
import com.njcn.gather.user.user.service.ISysUserRoleService; import com.njcn.gather.user.user.service.ISysUserRoleService;
import com.njcn.gather.user.user.service.ISysUserSignatureService;
import com.njcn.gather.user.user.service.ISysUserService; import com.njcn.gather.user.user.service.ISysUserService;
import com.njcn.gather.user.user.component.SysFileMinioStorageService;
import com.njcn.web.controller.BaseController; import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.HttpResultUtil; import com.njcn.web.utils.HttpResultUtil;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
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.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.net.URLEncoder;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -42,6 +53,9 @@ public class SysUserController extends BaseController {
private final ISysUserService sysUserService; private final ISysUserService sysUserService;
private final ISysUserRoleService sysUserRoleService; private final ISysUserRoleService sysUserRoleService;
private final ISysUserSignatureService sysUserSignatureService;
private final ISysFileService sysFileService;
private final SysFileMinioStorageService sysFileMinioStorageService;
@OperateInfo(info = LogEnum.SYSTEM_COMMON) @OperateInfo(info = LogEnum.SYSTEM_COMMON)
@PostMapping("/list") @PostMapping("/list")
@@ -55,13 +69,14 @@ public class SysUserController extends BaseController {
} }
@OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.ADD) @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.ADD)
@PostMapping("/add") @PostMapping(value = "/add", consumes = {"multipart/form-data"})
@ApiOperation("新增用户") @ApiOperation("新增用户")
@ApiImplicitParam(name = "addUserParam", value = "新增用户", required = true) @ApiImplicitParam(name = "addUserParam", value = "新增用户", required = true)
public HttpResult<Boolean> add(@RequestBody @Validated SysUserParam.SysUserAddParam addUserParam) { public HttpResult<Boolean> add(@RequestPart("request") @Validated SysUserParam.SysUserAddParam addUserParam,
@RequestPart(value = "signatureFile", required = false) MultipartFile signatureFile) {
String methodDescribe = getMethodDescribe("add"); String methodDescribe = getMethodDescribe("add");
LogUtil.njcnDebug(log, "{},用户数据为:{}", methodDescribe, addUserParam); LogUtil.njcnDebug(log, "{},用户数据为:{}", methodDescribe, addUserParam);
boolean result = sysUserService.addUser(addUserParam); boolean result = sysUserService.addUser(addUserParam, signatureFile);
if (result) { if (result) {
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe);
} else { } else {
@@ -70,13 +85,14 @@ public class SysUserController extends BaseController {
} }
@OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.UPDATE) @OperateInfo(info = LogEnum.SYSTEM_COMMON, operateType = OperateType.UPDATE)
@PostMapping("/update") @PostMapping(value = "/update", consumes = {"multipart/form-data"})
@ApiOperation("修改用户") @ApiOperation("修改用户")
@ApiImplicitParam(name = "updateUserParam", value = "修改用户", required = true) @ApiImplicitParam(name = "updateUserParam", value = "修改用户", required = true)
public HttpResult<Boolean> update(@RequestBody @Validated SysUserParam.SysUserUpdateParam updateUserParam) { public HttpResult<Boolean> update(@RequestPart("request") @Validated SysUserParam.SysUserUpdateParam updateUserParam,
@RequestPart(value = "signatureFile", required = false) MultipartFile signatureFile) {
String methodDescribe = getMethodDescribe("update"); String methodDescribe = getMethodDescribe("update");
LogUtil.njcnDebug(log, "{},用户数据为:{}", methodDescribe, updateUserParam); LogUtil.njcnDebug(log, "{},用户数据为:{}", methodDescribe, updateUserParam);
boolean result = sysUserService.updateUser(updateUserParam); boolean result = sysUserService.updateUser(updateUserParam, signatureFile);
if (result) { if (result) {
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe); return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, true, methodDescribe);
} else { } else {
@@ -128,7 +144,43 @@ public class SysUserController extends BaseController {
user.setRoleCodes(sysRoles.stream().map(SysRole::getCode).collect(Collectors.toList())); user.setRoleCodes(sysRoles.stream().map(SysRole::getCode).collect(Collectors.toList()));
user.setRoleNames(sysRoles.stream().map(SysRole::getName).collect(Collectors.toList())); user.setRoleNames(sysRoles.stream().map(SysRole::getName).collect(Collectors.toList()));
}); });
sysUserSignatureService.fillSignatureInfo(result);
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe); return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS, result, methodDescribe);
} }
@OperateInfo(info = LogEnum.SYSTEM_COMMON)
@GetMapping("/{id}/signature")
@ApiOperation("预览用户签名")
@ApiImplicitParam(name = "id", value = "用户id", required = true)
public ResponseEntity<InputStreamResource> signature(@PathVariable("id") String id) {
SysUserSignature signature = sysUserSignatureService.getActiveByUserId(id);
if (signature == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "用户签名不存在");
}
SysFile sysFile = sysFileService.getActiveById(signature.getFileId());
if (sysFile == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "用户签名文件不存在");
}
SysFileMinioStorageService.StoredObject storedObject =
sysFileMinioStorageService.getObject(sysFile.getBucketName(), sysFile.getObjectName());
String encodedFileName;
try {
encodedFileName = URLEncoder.encode(sysFile.getFileName(), "UTF-8")
.replace("+", "%20");
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "读取用户签名失败");
}
MediaType mediaType;
try {
mediaType = sysFile.getContentType() == null ? MediaType.APPLICATION_OCTET_STREAM
: MediaType.parseMediaType(sysFile.getContentType());
} catch (Exception exception) {
mediaType = MediaType.APPLICATION_OCTET_STREAM;
}
return ResponseEntity.ok()
.contentType(mediaType)
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename*=UTF-8''" + encodedFileName)
.body(new InputStreamResource(storedObject.getInputStream()));
}
} }

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.user.user.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.njcn.gather.user.user.pojo.po.SysFile;
/**
* 通用文件 Mapper。
*/
public interface SysFileMapper extends BaseMapper<SysFile> {
}

View File

@@ -0,0 +1,10 @@
package com.njcn.gather.user.user.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.njcn.gather.user.user.pojo.po.SysUserSignature;
/**
* 用户签名 Mapper。
*/
public interface SysUserSignatureMapper extends BaseMapper<SysUserSignature> {
}

View File

@@ -0,0 +1,19 @@
package com.njcn.gather.user.user.pojo.constant;
/**
* 通用文件常量。
*/
public final class SysFileConst {
private SysFileConst() {
}
public static final Integer STATUS_DELETED = 0;
public static final Integer STATUS_NORMAL = 1;
public static final String STORAGE_TYPE_MINIO = "MINIO";
public static final String BIZ_TYPE_USER_SIGNATURE = "USER_SIGNATURE";
public static final String BIZ_DIR_USER_SIGNATURE = "user-signature";
public static final String BUCKET_KEY_USER_SIGNATURE = "user-signature";
}

View File

@@ -0,0 +1,43 @@
package com.njcn.gather.user.user.pojo.constant;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* 通用文件扩展名常量。
*/
public final class SysFileExtensionConst {
private SysFileExtensionConst() {
}
/**
* 图片文件扩展名,适用于签名、图片附件等场景。
*/
public static final Set<String> IMAGE_EXTENSIONS = unmodifiableSet(
"png", "jpg", "jpeg", "bmp", "webp");
/**
* 常见文档文件扩展名,适用于文档、台账、说明文件等场景。
*/
public static final Set<String> DOCUMENT_EXTENSIONS = unmodifiableSet(
"doc", "docx", "xls", "xlsx", "txt", "pdf");
/**
* 通用上传扩展名,供后续文件业务按需复用。
*/
public static final Set<String> COMMON_UPLOAD_EXTENSIONS = merge(IMAGE_EXTENSIONS, DOCUMENT_EXTENSIONS);
private static Set<String> merge(Set<String> left, Set<String> right) {
LinkedHashSet<String> merged = new LinkedHashSet<String>();
merged.addAll(left);
merged.addAll(right);
return Collections.unmodifiableSet(merged);
}
private static Set<String> unmodifiableSet(String... values) {
return Collections.unmodifiableSet(new LinkedHashSet<String>(Arrays.asList(values)));
}
}

View File

@@ -0,0 +1,48 @@
package com.njcn.gather.user.user.pojo.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.njcn.db.mybatisplus.bo.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 通用文件记录。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_file")
public class SysFile extends BaseEntity implements Serializable {
private static final long serialVersionUID = -5516228278748156912L;
@TableId("id")
private String id;
@TableField("biz_type")
private String bizType;
@TableField("file_name")
private String fileName;
@TableField("bucket_name")
private String bucketName;
@TableField("object_name")
private String objectName;
@TableField("content_type")
private String contentType;
@TableField("file_size")
private Long fileSize;
@TableField("storage_type")
private String storageType;
@TableField("status")
private Integer status;
}

View File

@@ -94,5 +94,11 @@ public class SysUser extends BaseEntity implements Serializable {
@TableField(exist = false) @TableField(exist = false)
private List<String> roleNames; private List<String> roleNames;
@TableField(exist = false)
private String signatureFileId;
@TableField(exist = false)
private String signatureFileName;
} }

View File

@@ -0,0 +1,36 @@
package com.njcn.gather.user.user.pojo.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.njcn.db.mybatisplus.bo.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* 用户签名记录。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_user_signature")
public class SysUserSignature extends BaseEntity implements Serializable {
private static final long serialVersionUID = 5765362638635953846L;
@TableId("id")
private String id;
@TableField("user_id")
private String userId;
@TableField("user_name")
private String userName;
@TableField("file_id")
private String fileId;
@TableField("status")
private Integer status;
}

View File

@@ -0,0 +1,55 @@
package com.njcn.gather.user.user.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.gather.user.user.pojo.po.SysFile;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/**
* 通用文件服务。
*/
public interface ISysFileService extends IService<SysFile> {
/**
* 上传并保存 MinIO 文件记录。
*
* @param bizType 业务类型
* @param bucketKey 桶配置键
* @param bizDir MinIO 目录
* @param ownerId 业务归属ID
* @param file 上传文件
* @return 文件记录
*/
SysFile saveMinioFile(String bizType, String bucketKey, String bizDir, String ownerId, MultipartFile file);
/**
* 根据 ID 查询有效文件。
*
* @param fileId 文件ID
* @return 文件记录
*/
SysFile getActiveById(String fileId);
/**
* 按文件 ID 列表查询有效文件。
*
* @param fileIds 文件ID列表
* @return 文件记录
*/
List<SysFile> listActiveByIds(List<String> fileIds);
/**
* 逻辑删除文件记录。
*
* @param file 文件记录
*/
void markDeleted(SysFile file);
/**
* 删除 MinIO 对象。
*
* @param file 文件记录
*/
void deleteObjectQuietly(SysFile file);
}

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.gather.user.user.pojo.param.SysUserParam; import com.njcn.gather.user.user.pojo.param.SysUserParam;
import com.njcn.gather.user.user.pojo.po.SysUser; import com.njcn.gather.user.user.pojo.po.SysUser;
import org.springframework.web.multipart.MultipartFile;
import java.util.List; import java.util.List;
@@ -64,7 +65,7 @@ public interface ISysUserService extends IService<SysUser> {
* @param addUserParam 新增用户参数 * @param addUserParam 新增用户参数
* @return 结果true表示新增成功false表示新增失败 * @return 结果true表示新增成功false表示新增失败
*/ */
boolean addUser(SysUserParam.SysUserAddParam addUserParam); boolean addUser(SysUserParam.SysUserAddParam addUserParam, MultipartFile signatureFile);
/** /**
* 更新用户 * 更新用户
@@ -72,7 +73,7 @@ public interface ISysUserService extends IService<SysUser> {
* @param updateUserParam 更新用户参数 * @param updateUserParam 更新用户参数
* @return 结果true表示更新成功false表示更新失败 * @return 结果true表示更新成功false表示更新失败
*/ */
boolean updateUser(SysUserParam.SysUserUpdateParam updateUserParam); boolean updateUser(SysUserParam.SysUserUpdateParam updateUserParam, MultipartFile signatureFile);
/** /**
* 修改密码 * 修改密码

View File

@@ -0,0 +1,61 @@
package com.njcn.gather.user.user.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.gather.user.user.pojo.po.SysUser;
import com.njcn.gather.user.user.pojo.po.SysUserSignature;
import java.util.List;
/**
* 用户签名服务。
*/
public interface ISysUserSignatureService extends IService<SysUserSignature> {
/**
* 回填用户签名展示信息。
*
* @param users 用户列表
*/
void fillSignatureInfo(List<SysUser> users);
/**
* 查询用户当前签名。
*
* @param userId 用户ID
* @return 签名记录
*/
SysUserSignature getActiveByUserId(String userId);
/**
* 保存或更新签名元数据。
*
* @param userId 用户ID
* @param userName 用户名
* @param fileId 通用文件ID
* @return 旧签名记录,若不存在则返回 null
*/
SysUserSignature saveOrUpdateSignature(String userId, String userName, String fileId);
/**
* 同步签名记录中的用户名。
*
* @param userId 用户ID
* @param userName 用户名
*/
void syncUserName(String userId, String userName);
/**
* 查询用户签名列表。
*
* @param userIds 用户ID列表
* @return 签名记录
*/
List<SysUserSignature> listActiveByUserIds(List<String> userIds);
/**
* 逻辑删除用户签名记录。
*
* @param signatures 签名记录
*/
void markDeleted(List<SysUserSignature> signatures);
}

View File

@@ -0,0 +1,98 @@
package com.njcn.gather.user.user.service.impl;
import cn.hutool.core.collection.CollUtil;
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.user.user.component.SysFileMinioStorageService;
import com.njcn.gather.user.user.config.StorageMinioProperties;
import com.njcn.gather.user.user.mapper.SysFileMapper;
import com.njcn.gather.user.user.pojo.constant.SysFileConst;
import com.njcn.gather.user.user.pojo.po.SysFile;
import com.njcn.gather.user.user.service.ISysFileService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
/**
* 通用文件服务实现。
*/
@Service
@RequiredArgsConstructor
public class SysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFile> implements ISysFileService {
private final StorageMinioProperties storageMinioProperties;
private final SysFileMinioStorageService sysFileMinioStorageService;
@Override
public SysFile saveMinioFile(String bizType, String bucketKey, String bizDir, String ownerId, MultipartFile file) {
String fileId = UUID.randomUUID().toString().replace("-", "");
String bucketName = storageMinioProperties.requireBucket(bucketKey);
SysFileMinioStorageService.StoredObjectInfo storedObjectInfo =
sysFileMinioStorageService.save(bucketName, bizDir, ownerId, fileId, file);
SysFile sysFile = new SysFile();
sysFile.setId(fileId);
sysFile.setBizType(bizType);
sysFile.setFileName(storedObjectInfo.getFileName());
sysFile.setBucketName(storedObjectInfo.getBucketName());
sysFile.setObjectName(storedObjectInfo.getObjectName());
sysFile.setContentType(storedObjectInfo.getContentType());
sysFile.setFileSize(storedObjectInfo.getFileSize());
sysFile.setStorageType(SysFileConst.STORAGE_TYPE_MINIO);
sysFile.setStatus(SysFileConst.STATUS_NORMAL);
try {
boolean saved = this.save(sysFile);
if (!saved) {
sysFileMinioStorageService.deleteQuietly(sysFile.getBucketName(), sysFile.getObjectName());
throw new BusinessException(CommonResponseEnum.FAIL, "保存文件记录失败");
}
return sysFile;
} catch (RuntimeException exception) {
sysFileMinioStorageService.deleteQuietly(sysFile.getBucketName(), sysFile.getObjectName());
throw exception;
}
}
@Override
public SysFile getActiveById(String fileId) {
if (fileId == null || fileId.trim().isEmpty()) {
return null;
}
return this.lambdaQuery()
.eq(SysFile::getId, fileId)
.eq(SysFile::getStatus, SysFileConst.STATUS_NORMAL)
.one();
}
@Override
public List<SysFile> listActiveByIds(List<String> fileIds) {
if (CollUtil.isEmpty(fileIds)) {
return Collections.emptyList();
}
return this.lambdaQuery()
.in(SysFile::getId, fileIds)
.eq(SysFile::getStatus, SysFileConst.STATUS_NORMAL)
.list();
}
@Override
public void markDeleted(SysFile file) {
if (file == null) {
return;
}
file.setStatus(SysFileConst.STATUS_DELETED);
this.updateById(file);
}
@Override
public void deleteObjectQuietly(SysFile file) {
if (file == null) {
return;
}
sysFileMinioStorageService.deleteQuietly(file.getBucketName(), file.getObjectName());
}
}

View File

@@ -13,35 +13,43 @@ import com.njcn.db.mybatisplus.constant.DbConstant;
import com.njcn.gather.user.pojo.constant.RoleConst; import com.njcn.gather.user.pojo.constant.RoleConst;
import com.njcn.gather.user.pojo.constant.UserConst; import com.njcn.gather.user.pojo.constant.UserConst;
import com.njcn.gather.user.pojo.enums.UserResponseEnum; import com.njcn.gather.user.pojo.enums.UserResponseEnum;
import com.njcn.gather.user.user.pojo.constant.SysFileExtensionConst;
import com.njcn.gather.user.user.mapper.SysUserMapper; import com.njcn.gather.user.user.mapper.SysUserMapper;
import com.njcn.gather.user.user.pojo.constant.SysFileConst;
import com.njcn.gather.user.user.pojo.param.SysUserParam; import com.njcn.gather.user.user.pojo.param.SysUserParam;
import com.njcn.gather.user.user.pojo.po.SysFile;
import com.njcn.gather.user.user.pojo.po.SysRole; import com.njcn.gather.user.user.pojo.po.SysRole;
import com.njcn.gather.user.user.pojo.po.SysUser; import com.njcn.gather.user.user.pojo.po.SysUser;
import com.njcn.gather.user.user.pojo.po.SysUserSignature;
import com.njcn.gather.user.user.service.ISysFileService;
import com.njcn.gather.user.user.service.ISysUserRoleService; import com.njcn.gather.user.user.service.ISysUserRoleService;
import com.njcn.gather.user.user.service.ISysUserSignatureService;
import com.njcn.gather.user.user.service.ISysUserService; import com.njcn.gather.user.user.service.ISysUserService;
import com.njcn.web.factory.PageFactory; import com.njcn.web.factory.PageFactory;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Locale;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* @author caozehui * @author caozehui
* @date 2024-11-08 * @date 2024-11-08
*/ */
@Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService { public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService {
private final ISysUserRoleService sysUserRoleService; private final ISysUserRoleService sysUserRoleService;
private final ISysFileService sysFileService;
private final ISysUserSignatureService sysUserSignatureService;
@Override @Override
public Page<SysUser> listUser(SysUserParam.SysUserQueryParam queryParam) { public Page<SysUser> listUser(SysUserParam.SysUserQueryParam queryParam) {
@@ -65,6 +73,7 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
sysUser.setRoleIds(sysRoles.stream().map(SysRole::getId).collect(Collectors.toList())); sysUser.setRoleIds(sysRoles.stream().map(SysRole::getId).collect(Collectors.toList()));
sysUser.setRoleNames(sysRoles.stream().map(SysRole::getName).collect(Collectors.toList())); sysUser.setRoleNames(sysRoles.stream().map(SysRole::getName).collect(Collectors.toList()));
}); });
sysUserSignatureService.fillSignatureInfo(page.getRecords());
return page; return page;
} }
@@ -105,7 +114,7 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
@Override @Override
@Transactional @Transactional
public boolean addUser(SysUserParam.SysUserAddParam addUserParam) { public boolean addUser(SysUserParam.SysUserAddParam addUserParam, MultipartFile signatureFile) {
addUserParam.setName(addUserParam.getName().trim()); addUserParam.setName(addUserParam.getName().trim());
addUserParam.setLoginName(addUserParam.getLoginName().trim()); addUserParam.setLoginName(addUserParam.getLoginName().trim());
if (UserConst.SUPER_ADMIN.equals(addUserParam.getLoginName())) { if (UserConst.SUPER_ADMIN.equals(addUserParam.getLoginName())) {
@@ -117,26 +126,79 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
checkRepeat(addUserParam, false, null); checkRepeat(addUserParam, false, null);
SysUser sysUser = new SysUser(); SysUser sysUser = new SysUser();
BeanUtils.copyProperties(addUserParam, sysUser); BeanUtils.copyProperties(addUserParam, sysUser);
sysUser.setId(UUID.randomUUID().toString().replace("-", ""));
String secretkey = Sm4Utils.globalSecretKey; String secretkey = Sm4Utils.globalSecretKey;
Sm4Utils sm4 = new Sm4Utils(secretkey); Sm4Utils sm4 = new Sm4Utils(secretkey);
sysUser.setPassword(sm4.encryptData_ECB(sysUser.getPassword())); sysUser.setPassword(sm4.encryptData_ECB(sysUser.getPassword()));
sysUser.setLoginTime(LocalDateTimeUtil.now()); sysUser.setLoginTime(LocalDateTimeUtil.now());
sysUser.setLoginErrorTimes(0); sysUser.setLoginErrorTimes(0);
sysUser.setState(UserConst.STATE_ENABLE); sysUser.setState(UserConst.STATE_ENABLE);
boolean result = this.save(sysUser); MultipartFile normalizedSignature = normalizeSignatureFile(signatureFile);
sysUserRoleService.addUserRole(sysUser.getId(), addUserParam.getRoleIds()); SysFile sysFile = null;
return result; if (normalizedSignature != null) {
sysFile = sysFileService.saveMinioFile(SysFileConst.BIZ_TYPE_USER_SIGNATURE,
SysFileConst.BUCKET_KEY_USER_SIGNATURE, SysFileConst.BIZ_DIR_USER_SIGNATURE,
sysUser.getId(), normalizedSignature);
}
try {
boolean result = this.save(sysUser);
if (result) {
sysUserRoleService.addUserRole(sysUser.getId(), addUserParam.getRoleIds());
if (sysFile != null) {
sysUserSignatureService.saveOrUpdateSignature(sysUser.getId(), sysUser.getName(), sysFile.getId());
}
}
if (!result && sysFile != null) {
cleanupNewFile(sysFile);
}
return result;
} catch (RuntimeException exception) {
if (sysFile != null) {
cleanupNewFile(sysFile);
}
throw exception;
}
} }
@Override @Override
@Transactional @Transactional
public boolean updateUser(SysUserParam.SysUserUpdateParam updateUserParam) { public boolean updateUser(SysUserParam.SysUserUpdateParam updateUserParam, MultipartFile signatureFile) {
updateUserParam.setName(updateUserParam.getName().trim()); updateUserParam.setName(updateUserParam.getName().trim());
checkRepeat(updateUserParam, true, updateUserParam.getId()); checkRepeat(updateUserParam, true, updateUserParam.getId());
SysUser sysUser = new SysUser(); SysUser sysUser = new SysUser();
BeanUtils.copyProperties(updateUserParam, sysUser); BeanUtils.copyProperties(updateUserParam, sysUser);
sysUserRoleService.updateUserRole(sysUser.getId(), updateUserParam.getRoleIds()); MultipartFile normalizedSignature = normalizeSignatureFile(signatureFile);
return this.updateById(sysUser); SysFile newSysFile = null;
if (normalizedSignature != null) {
newSysFile = sysFileService.saveMinioFile(SysFileConst.BIZ_TYPE_USER_SIGNATURE,
SysFileConst.BUCKET_KEY_USER_SIGNATURE, SysFileConst.BIZ_DIR_USER_SIGNATURE,
sysUser.getId(), normalizedSignature);
}
try {
boolean updated = this.updateById(sysUser);
if (!updated) {
if (newSysFile != null) {
cleanupNewFile(newSysFile);
}
return false;
}
sysUserRoleService.updateUserRole(sysUser.getId(), updateUserParam.getRoleIds());
if (newSysFile != null) {
SysUserSignature oldSignature = sysUserSignatureService.saveOrUpdateSignature(
sysUser.getId(), sysUser.getName(), newSysFile.getId());
if (oldSignature != null) {
cleanupOldFile(oldSignature.getFileId());
}
} else {
sysUserSignatureService.syncUserName(sysUser.getId(), sysUser.getName());
}
return true;
} catch (RuntimeException exception) {
if (newSysFile != null) {
cleanupNewFile(newSysFile);
}
throw exception;
}
} }
@Override @Override
@@ -170,12 +232,18 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
} }
} }
} }
List<SysUserSignature> signatures = sysUserSignatureService.listActiveByUserIds(ids);
// 删除用户角色关联数据 // 删除用户角色关联数据
sysUserRoleService.deleteUserRoleByUserIds(ids); sysUserRoleService.deleteUserRoleByUserIds(ids);
return this.lambdaUpdate() boolean result = this.lambdaUpdate()
.set(SysUser::getState, UserConst.STATE_DELETE) .set(SysUser::getState, UserConst.STATE_DELETE)
.in(SysUser::getId, ids) .in(SysUser::getId, ids)
.update(); .update();
if (result) {
sysUserSignatureService.markDeleted(signatures);
signatures.forEach(signature -> cleanupOldFile(signature.getFileId()));
}
return result;
} }
@Override @Override
@@ -210,4 +278,37 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
throw new BusinessException(UserResponseEnum.REGISTER_EMAIL_FAIL); throw new BusinessException(UserResponseEnum.REGISTER_EMAIL_FAIL);
} }
} }
private MultipartFile normalizeSignatureFile(MultipartFile signatureFile) {
if (signatureFile == null || signatureFile.isEmpty()) {
return null;
}
String originalFileName = signatureFile.getOriginalFilename();
if (originalFileName == null || originalFileName.trim().isEmpty()) {
throw new BusinessException(UserResponseEnum.SIGNATURE_FILE_NAME_EMPTY);
}
int dotIndex = originalFileName.lastIndexOf('.');
if (dotIndex < 0 || dotIndex == originalFileName.length() - 1) {
throw new BusinessException(UserResponseEnum.SIGNATURE_FILE_TYPE_ERROR);
}
String extension = originalFileName.substring(dotIndex + 1).toLowerCase(Locale.ENGLISH);
if (!SysFileExtensionConst.IMAGE_EXTENSIONS.contains(extension)) {
throw new BusinessException(UserResponseEnum.SIGNATURE_FILE_TYPE_ERROR);
}
return signatureFile;
}
private void cleanupNewFile(SysFile sysFile) {
sysFileService.markDeleted(sysFile);
sysFileService.deleteObjectQuietly(sysFile);
}
private void cleanupOldFile(String fileId) {
SysFile oldFile = sysFileService.getActiveById(fileId);
if (oldFile == null) {
return;
}
sysFileService.markDeleted(oldFile);
sysFileService.deleteObjectQuietly(oldFile);
}
} }

View File

@@ -0,0 +1,134 @@
package com.njcn.gather.user.user.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.gather.user.user.mapper.SysUserSignatureMapper;
import com.njcn.gather.user.user.pojo.constant.SysFileConst;
import com.njcn.gather.user.user.pojo.po.SysFile;
import com.njcn.gather.user.user.pojo.po.SysUser;
import com.njcn.gather.user.user.pojo.po.SysUserSignature;
import com.njcn.gather.user.user.service.ISysFileService;
import com.njcn.gather.user.user.service.ISysUserSignatureService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
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
@RequiredArgsConstructor
public class SysUserSignatureServiceImpl extends ServiceImpl<SysUserSignatureMapper, SysUserSignature>
implements ISysUserSignatureService {
private final ISysFileService sysFileService;
@Override
public void fillSignatureInfo(List<SysUser> users) {
if (CollUtil.isEmpty(users)) {
return;
}
Map<String, SysUserSignature> signatureMap = new LinkedHashMap<String, SysUserSignature>();
List<String> userIds = users.stream().map(SysUser::getId).collect(Collectors.toList());
for (SysUserSignature signature : listActiveByUserIds(userIds)) {
signatureMap.put(signature.getUserId(), signature);
}
Map<String, SysFile> sysFileMap = new LinkedHashMap<String, SysFile>();
List<String> fileIds = signatureMap.values().stream().map(SysUserSignature::getFileId).collect(Collectors.toList());
for (SysFile sysFile : sysFileService.listActiveByIds(fileIds)) {
sysFileMap.put(sysFile.getId(), sysFile);
}
for (SysUser user : users) {
SysUserSignature signature = signatureMap.get(user.getId());
if (signature == null) {
user.setSignatureFileId(null);
user.setSignatureFileName(null);
continue;
}
user.setSignatureFileId(signature.getFileId());
SysFile sysFile = sysFileMap.get(signature.getFileId());
user.setSignatureFileName(sysFile == null ? null : sysFile.getFileName());
}
}
@Override
public SysUserSignature getActiveByUserId(String userId) {
if (StrUtil.isBlank(userId)) {
return null;
}
return this.lambdaQuery()
.eq(SysUserSignature::getUserId, userId)
.eq(SysUserSignature::getStatus, SysFileConst.STATUS_NORMAL)
.one();
}
@Override
public SysUserSignature saveOrUpdateSignature(String userId, String userName, String fileId) {
SysUserSignature existing = getActiveByUserId(userId);
if (existing == null) {
SysUserSignature signature = new SysUserSignature();
signature.setId(UUID.randomUUID().toString().replace("-", ""));
signature.setUserId(userId);
signature.setUserName(userName);
signature.setFileId(fileId);
signature.setStatus(SysFileConst.STATUS_NORMAL);
this.save(signature);
return null;
}
SysUserSignature oldSignature = copySignature(existing);
existing.setUserName(userName);
existing.setFileId(fileId);
existing.setStatus(SysFileConst.STATUS_NORMAL);
this.updateById(existing);
return oldSignature;
}
@Override
public void syncUserName(String userId, String userName) {
SysUserSignature signature = getActiveByUserId(userId);
if (signature == null || StrUtil.equals(signature.getUserName(), userName)) {
return;
}
signature.setUserName(userName);
this.updateById(signature);
}
@Override
public List<SysUserSignature> listActiveByUserIds(List<String> userIds) {
if (CollUtil.isEmpty(userIds)) {
return Collections.emptyList();
}
return this.lambdaQuery()
.in(SysUserSignature::getUserId, userIds)
.eq(SysUserSignature::getStatus, SysFileConst.STATUS_NORMAL)
.list();
}
@Override
public void markDeleted(List<SysUserSignature> signatures) {
if (CollUtil.isEmpty(signatures)) {
return;
}
for (SysUserSignature signature : signatures) {
signature.setStatus(SysFileConst.STATUS_DELETED);
this.updateById(signature);
}
}
private SysUserSignature copySignature(SysUserSignature source) {
SysUserSignature copy = new SysUserSignature();
copy.setId(source.getId());
copy.setUserId(source.getUserId());
copy.setUserName(source.getUserName());
copy.setFileId(source.getFileId());
copy.setStatus(source.getStatus());
return copy;
}
}