3 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
30 changed files with 2113 additions and 912 deletions

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

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

View File

@@ -10,6 +10,7 @@ import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.clientunit.mapper.ClientUnitMapper; import com.njcn.gather.aireport.clientunit.mapper.ClientUnitMapper;
import com.njcn.gather.aireport.clientunit.pojo.constant.ClientUnitConst; import com.njcn.gather.aireport.clientunit.pojo.constant.ClientUnitConst;
import com.njcn.gather.aireport.clientunit.pojo.po.ClientUnitPO; 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.mapper.TestDeviceMapper;
import com.njcn.gather.aireport.testdevice.pojo.constant.TestDeviceConst; 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.po.TestDevicePO;
@@ -30,7 +31,6 @@ import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Files;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@@ -91,8 +91,9 @@ public class TestReportLedgerImportService {
persistValidateSessionResult(sessionId, result); persistValidateSessionResult(sessionId, result);
return result; return result;
} }
try (InputStream inputStream = Files.newInputStream( try (AiReportMinioStorageService.StoredObject storedObject =
testReportStorageService.resolveValidateSessionFile(sessionId, TestReportConst.LEDGER_SUMMARY_FILE_NAME))) { testReportStorageService.getValidateSessionObject(sessionId, TestReportConst.LEDGER_SUMMARY_FILE_NAME);
InputStream inputStream = storedObject.getInputStream()) {
TestReportLedgerExcelParser.ParsedLedger ledger = testReportLedgerExcelParser.parseForValidate( TestReportLedgerExcelParser.ParsedLedger ledger = testReportLedgerExcelParser.parseForValidate(
TestReportConst.LEDGER_SUMMARY_FILE_NAME, inputStream); TestReportConst.LEDGER_SUMMARY_FILE_NAME, inputStream);
Map<String, TestDevicePO> dbDeviceMap = loadExistingDevices(collectDeviceNos(ledger)); Map<String, TestDevicePO> dbDeviceMap = loadExistingDevices(collectDeviceNos(ledger));
@@ -136,6 +137,7 @@ public class TestReportLedgerImportService {
writeValidatedBatchMeta(report.getId(), batchId, uploadedBatch, ledger, userId); writeValidatedBatchMeta(report.getId(), batchId, uploadedBatch, ledger, userId);
return buildValidateSuccessResult(batchId, uploadedBatch.getTempStoragePath(), ledger, fileMap.size()); return buildValidateSuccessResult(batchId, uploadedBatch.getTempStoragePath(), ledger, fileMap.size());
} catch (RuntimeException exception) { } catch (RuntimeException exception) {
testReportStorageService.deleteTempBatchQuietly(report.getId(), batchId);
throw exception; throw exception;
} }
} }
@@ -168,11 +170,12 @@ public class TestReportLedgerImportService {
} }
private TestReportLedgerExcelParser.ParsedLedger parseLedgerFromBatch(String reportId, String batchId, Collection<String> fileNames) { private TestReportLedgerExcelParser.ParsedLedger parseLedgerFromBatch(String reportId, String batchId, Collection<String> fileNames) {
try (InputStream inputStream = java.nio.file.Files.newInputStream( try (AiReportMinioStorageService.StoredObject storedObject =
testReportStorageService.resolveTempBatchFile(reportId, batchId, TestReportConst.LEDGER_SUMMARY_FILE_NAME))) { testReportStorageService.getTempBatchObject(reportId, batchId, TestReportConst.LEDGER_SUMMARY_FILE_NAME);
InputStream inputStream = storedObject.getInputStream()) {
return testReportLedgerExcelParser.parse(TestReportConst.LEDGER_SUMMARY_FILE_NAME, inputStream, return testReportLedgerExcelParser.parse(TestReportConst.LEDGER_SUMMARY_FILE_NAME, inputStream,
buildAttachmentNameSet(fileNames)); buildAttachmentNameSet(fileNames));
} catch (java.io.IOException exception) { } catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段读取台账汇总excel失败"); throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段读取台账汇总excel失败");
} }
} }
@@ -501,14 +504,9 @@ public class TestReportLedgerImportService {
point.setGroupNo(pointRow.getGroupNo()); point.setGroupNo(pointRow.getGroupNo());
point.setSubstationName(pointRow.getSubstationName()); point.setSubstationName(pointRow.getSubstationName());
point.setPointName(pointRow.getPointName()); point.setPointName(pointRow.getPointName());
point.setTestDeviceNo(pointRow.getTestDeviceNo());
point.setClientUnitName(pointRow.getClientUnitName());
point.setExcelAttachmentName(pointRow.getExcelAttachmentName()); point.setExcelAttachmentName(pointRow.getExcelAttachmentName());
point.setExcelStoragePath(testReportStorageService.savePointExcelFromTemp(reportId, batchId, point.setExcelStoragePath(testReportStorageService.savePointExcelFromTemp(reportId, batchId,
pointRow.getExcelAttachmentName())); pointRow.getExcelAttachmentName()));
point.setWordAttachmentName(pointRow.getWordAttachmentName());
point.setWordStoragePath(testReportStorageService.savePointWordFromTemp(reportId, batchId,
pointRow.getWordAttachmentName()));
point.setSortNo(sortNo++); point.setSortNo(sortNo++);
point.setDeleted(TestReportConst.DELETED_NO); point.setDeleted(TestReportConst.DELETED_NO);
point.setCreateBy(userId); point.setCreateBy(userId);
@@ -526,7 +524,6 @@ public class TestReportLedgerImportService {
private void deleteOldFiles(List<TestReportPointPO> oldPoints, List<TestReportGroupReportPO> oldGroupReports, String data) { private void deleteOldFiles(List<TestReportPointPO> oldPoints, List<TestReportGroupReportPO> oldGroupReports, String data) {
for (TestReportPointPO point : oldPoints) { for (TestReportPointPO point : oldPoints) {
testReportStorageService.deleteQuietly(point.getExcelStoragePath()); testReportStorageService.deleteQuietly(point.getExcelStoragePath());
testReportStorageService.deleteQuietly(point.getWordStoragePath());
} }
for (TestReportGroupReportPO groupReport : oldGroupReports) { for (TestReportGroupReportPO groupReport : oldGroupReports) {
testReportStorageService.deleteQuietly(groupReport.getReportStoragePath()); testReportStorageService.deleteQuietly(groupReport.getReportStoragePath());

View File

@@ -3,326 +3,304 @@ package com.njcn.gather.aireport.testreport.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 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 com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst;
import org.springframework.beans.factory.annotation.Value; import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
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.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.Set;
@Component @Component
@RequiredArgsConstructor
public class TestReportStorageService { public class TestReportStorageService {
@Value("${files.path}") private final AiReportMinioStorageService minioStorageService;
private String filesRootPath;
public String saveSourceExcel(String reportId, MultipartFile file) { public String saveSourceExcel(String reportId, MultipartFile file) {
return saveMultipartFile(reportId, TestReportConst.STORAGE_SOURCE_EXCEL_DIR, file, "淇濆瓨鍘熷Excel澶辫触"); 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) { public String saveLedgerSummaryExcel(String reportId, MultipartFile file) {
return saveMultipartFile(reportId, TestReportConst.STORAGE_LEDGER_SUMMARY_DIR, file, "淇濆瓨鍙拌处姹囨€籈xcel澶辫触"); 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) { public String savePointExcelAttachment(String reportId, MultipartFile file) {
return saveMultipartFile(reportId, TestReportConst.STORAGE_POINT_EXCEL_DIR, file, "淇濆瓨鐩戞祴鐐笶xcel闄勪欢澶辫触"); 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) { public String savePointWordAttachment(String reportId, MultipartFile file) {
return saveMultipartFile(reportId, TestReportConst.STORAGE_POINT_WORD_DIR, file, "淇濆瓨鐩戞祴鐐筗ord闄勪欢澶辫触"); 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) { public UploadedBatch saveTempBatchFiles(String reportId, String batchId, Map<String, MultipartFile> fileMap) {
if (fileMap == null || fileMap.isEmpty()) { return saveBatchFiles(buildTempUploadDir(reportId, batchId), buildTempBatchRelativePath(reportId, batchId),
throw new BusinessException(CommonResponseEnum.FAIL, "文件不能为空"); batchId, fileMap, "文件上传阶段:保存临时文件失败");
}
Map<String, String> storedFileMap = new LinkedHashMap<String, String>();
for (Map.Entry<String, MultipartFile> entry : fileMap.entrySet()) {
String originalFileName = entry.getKey();
MultipartFile file = entry.getValue();
if (file == null || file.isEmpty() || StrUtil.isBlank(originalFileName)) {
throw new BusinessException(CommonResponseEnum.FAIL, "文件不能为空");
}
String safeFileName = Paths.get(originalFileName).getFileName().toString();
Path targetPath = resolveTempUploadDir(reportId, batchId).resolve(safeFileName);
try {
Files.createDirectories(targetPath.getParent());
file.transferTo(targetPath.toFile());
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "文件上传阶段:保存临时文件失败");
}
storedFileMap.put(originalFileName, buildTempUploadRelativePath(reportId, batchId, safeFileName));
}
UploadedBatch uploadedBatch = new UploadedBatch();
uploadedBatch.setBatchId(batchId);
uploadedBatch.setTempStoragePath(buildTempBatchRelativePath(reportId, batchId));
uploadedBatch.setStoredFileMap(storedFileMap);
return uploadedBatch;
} }
public UploadedBatch saveValidateSessionFiles(String sessionId, Map<String, MultipartFile> fileMap) { 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()) { if (fileMap == null || fileMap.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "鏂囦欢涓嶈兘涓虹┖"); throw new BusinessException(CommonResponseEnum.FAIL, "文件不能为空");
} }
Map<String, String> storedFileMap = new LinkedHashMap<String, String>(); Map<String, String> storedFileMap = new LinkedHashMap<String, String>();
for (Map.Entry<String, MultipartFile> entry : fileMap.entrySet()) { try {
String originalFileName = entry.getKey(); for (Map.Entry<String, MultipartFile> entry : fileMap.entrySet()) {
MultipartFile file = entry.getValue(); String originalFileName = trimFileName(entry.getKey());
if (file == null || file.isEmpty() || StrUtil.isBlank(originalFileName)) { MultipartFile file = entry.getValue();
throw new BusinessException(CommonResponseEnum.FAIL, "鏂囦欢涓嶈兘涓虹┖"); 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());
}
} }
String safeFileName = Paths.get(originalFileName).getFileName().toString(); } catch (BusinessException exception) {
Path targetPath = resolveValidateSessionUploadDir(sessionId).resolve(safeFileName); cleanupStoredFiles(storedFileMap);
try { throw exception;
Files.createDirectories(targetPath.getParent()); } catch (Exception exception) {
file.transferTo(targetPath.toFile()); cleanupStoredFiles(storedFileMap);
} catch (IOException exception) { throw new BusinessException(CommonResponseEnum.FAIL, errorMessage);
throw new BusinessException(CommonResponseEnum.FAIL, "鏂囦欢涓婁紶闃舵锛氫繚瀛樻牎楠屼細璇濇枃浠跺け璐?");
}
storedFileMap.put(originalFileName, buildValidateSessionUploadRelativePath(sessionId, safeFileName));
} }
UploadedBatch uploadedBatch = new UploadedBatch(); UploadedBatch uploadedBatch = new UploadedBatch();
uploadedBatch.setBatchId(sessionId); uploadedBatch.setBatchId(batchId);
uploadedBatch.setTempStoragePath(buildValidateSessionRelativePath(sessionId)); uploadedBatch.setTempStoragePath(tempStoragePath);
uploadedBatch.setStoredFileMap(storedFileMap); uploadedBatch.setStoredFileMap(storedFileMap);
return uploadedBatch; return uploadedBatch;
} }
public Path resolveTempBatchFile(String reportId, String batchId, String fileName) { private void writeText(String objectName, String content, String errorMessage) {
return resolveTempUploadDir(reportId, batchId).resolve(Paths.get(fileName).getFileName().toString()).normalize();
}
public void writeTempBatchMeta(String reportId, String batchId, String content) {
Path metaPath = resolveTempBatchMetaPath(reportId, batchId);
try { try {
Files.createDirectories(metaPath.getParent()); minioStorageService.saveBytes(resolveParentDir(objectName), null, resolveFileName(objectName),
Files.write(metaPath, content.getBytes(StandardCharsets.UTF_8)); content == null ? new byte[0] : content.getBytes(StandardCharsets.UTF_8),
} catch (IOException exception) { MediaType.APPLICATION_JSON_VALUE, true);
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:保存批次元数据失败"); } catch (BusinessException exception) {
throw exception;
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, errorMessage);
} }
} }
public void writeValidateSessionResult(String sessionId, String content) { private String copyTempBatchFile(String reportId, String batchId, String fileName, String targetBizDir,
Path resultPath = resolveValidateSessionResultPath(sessionId); String contentType, String errorMessage) {
try { String normalizedFileName = trimFileName(fileName);
Files.createDirectories(resultPath.getParent()); if (StrUtil.isBlank(normalizedFileName)) {
Files.write(resultPath, content.getBytes(StandardCharsets.UTF_8));
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "鏍¢獙鏂囦欢闃舵锛氫繚瀛樻牎楠岀粨鏋滃け璐?");
}
}
public Path resolveValidateSessionFile(String sessionId, String fileName) {
return resolveValidateSessionUploadDir(sessionId).resolve(Paths.get(fileName).getFileName().toString()).normalize();
}
public List<String> listValidateSessionFileNames(String sessionId) {
Path uploadDir = resolveValidateSessionUploadDir(sessionId);
if (!Files.exists(uploadDir) || !Files.isDirectory(uploadDir)) {
return new ArrayList<String>();
}
try {
List<String> result = new ArrayList<String>();
Files.list(uploadDir)
.filter(Files::isRegularFile)
.forEach(path -> result.add(path.getFileName().toString()));
return result;
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "鏍¢獙鏂囦欢闃舵锛氳鍙栦細璇濇枃浠跺垪琛ㄥけ璐?");
}
}
public String readTempBatchMeta(String reportId, String batchId) {
Path metaPath = resolveTempBatchMetaPath(reportId, batchId);
if (!Files.exists(metaPath) || !Files.isRegularFile(metaPath)) {
return null; return null;
} }
try { try {
return new String(Files.readAllBytes(metaPath), StandardCharsets.UTF_8); byte[] content = minioStorageService.readBytes(buildTempBatchFileObjectName(reportId, batchId, normalizedFileName));
} catch (IOException exception) { return minioStorageService.saveBytes(targetBizDir, null, normalizedFileName, content, contentType, false)
throw new BusinessException(CommonResponseEnum.FAIL, "璇诲彇瀵煎叆鎵规淇℃伅澶辫触"); .getObjectName();
} } catch (BusinessException exception) {
} throw exception;
} catch (Exception exception) {
public String saveLedgerSummaryFromTemp(String reportId, String batchId, String fileName) {
return savePathFile(reportId, TestReportConst.STORAGE_LEDGER_SUMMARY_DIR,
resolveTempBatchFile(reportId, batchId, fileName), fileName, "淇濆瓨鍙拌处姹囨€籈xcel澶辫触");
}
public String savePointExcelFromTemp(String reportId, String batchId, String fileName) {
return savePathFile(reportId, TestReportConst.STORAGE_POINT_EXCEL_DIR,
resolveTempBatchFile(reportId, batchId, fileName), fileName, "淇濆瓨鐩戞祴鐐笶xcel闄勪欢澶辫触");
}
public String savePointWordFromTemp(String reportId, String batchId, String fileName) {
return savePathFile(reportId, TestReportConst.STORAGE_POINT_WORD_DIR,
resolveTempBatchFile(reportId, batchId, fileName), fileName, "淇濆瓨鐩戞祴鐐筗ord闄勪欢澶辫触");
}
public String saveGeneratedPlaceholder(String reportId, String fileName, byte[] content) {
Path targetPath = resolveReportDir(reportId, TestReportConst.STORAGE_GENERATED_REPORT_DIR).resolve(fileName);
try {
Files.createDirectories(targetPath.getParent());
Files.write(targetPath, content);
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "淇濆瓨鐢熸垚鎶ュ憡澶辫触");
}
return buildRelativePath(reportId, TestReportConst.STORAGE_GENERATED_REPORT_DIR, fileName);
}
public Path resolveBusinessPath(String relativePath) {
return Paths.get(filesRootPath, TestReportConst.STORAGE_DIR).resolve(relativePath).normalize();
}
public void deleteQuietly(String relativePath) {
if (StrUtil.isBlank(relativePath)) {
return;
}
try {
Files.deleteIfExists(resolveBusinessPath(relativePath));
} catch (IOException ignored) {
// 鏂囦欢娓呯悊澶辫触涓嶅奖鍝嶄富娴佺▼
}
}
public void deleteTempBatchQuietly(String reportId, String batchId) {
if (StrUtil.isBlank(reportId) || StrUtil.isBlank(batchId)) {
return;
}
deletePathQuietly(resolveTempBatchDir(reportId, batchId));
}
private String saveMultipartFile(String reportId, String childDir, MultipartFile file, String errorMessage) {
String originalFileName = file == null ? null : file.getOriginalFilename();
if (file == null || file.isEmpty() || StrUtil.isBlank(originalFileName)) {
throw new BusinessException(CommonResponseEnum.FAIL, "文件不能为空");
}
return savePathFile(reportId, childDir, toTempTransferredPath(file), originalFileName, errorMessage);
}
private Path toTempTransferredPath(MultipartFile file) {
try {
Path tempFile = Files.createTempFile("test-report-", ".upload");
file.transferTo(tempFile.toFile());
return tempFile;
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "文件上传失败");
}
}
private String savePathFile(String reportId, String childDir, Path sourcePath, String originalFileName, String errorMessage) {
String normalizedFileName = originalFileName == null ? null : Paths.get(originalFileName).getFileName().toString();
if (sourcePath == null || StrUtil.isBlank(normalizedFileName) || !Files.exists(sourcePath)) {
throw new BusinessException(CommonResponseEnum.FAIL, "文件不能为空");
}
String fileName = UUID.randomUUID().toString().replace("-", "") + "-" + normalizedFileName;
Path targetPath = resolveReportDir(reportId, childDir).resolve(fileName);
try {
Files.createDirectories(targetPath.getParent());
Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, errorMessage); throw new BusinessException(CommonResponseEnum.FAIL, errorMessage);
} finally {
deletePathQuietlyIfTemp(sourcePath);
} }
return buildRelativePath(reportId, childDir, fileName);
} }
private void deletePathQuietlyIfTemp(Path path) { private void deletePrefixQuietly(String prefix) {
if (path == null) { 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; return;
} }
String fileName = path.getFileName() == null ? null : path.getFileName().toString(); for (String objectName : storedFileMap.values()) {
if (fileName != null && fileName.startsWith("test-report-")) { minioStorageService.deleteQuietly(objectName);
deletePathQuietly(path);
} }
} }
private void deletePathQuietly(Path path) { private void validateMultipartFile(MultipartFile file, Set<String> allowedExtensions,
if (path == null || !Files.exists(path)) { String emptyMessage, String invalidMessage) {
return; if (file == null || file.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, emptyMessage);
} }
try { validateFileName(file.getOriginalFilename(), allowedExtensions, emptyMessage, invalidMessage);
if (Files.isDirectory(path)) { }
Files.walk(path)
.sorted((left, right) -> right.getNameCount() - left.getNameCount()) private void validateAllowedUploadFile(String fileName) {
.forEach(this::deleteSingleQuietly); validateFileName(fileName, AiReportFileExtensionConst.ALLOWED_EXTENSIONS, "文件不能为空",
return; "仅允许上传docx、xlsx、pdf文件");
} }
Files.deleteIfExists(path);
} catch (IOException ignored) { 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 void deleteSingleQuietly(Path path) { private String buildBusinessDir(String reportId, String childDir) {
try { return AiReportFileConst.BIZ_DIR_TEST_REPORT + "/" + reportId + "/" + childDir;
Files.deleteIfExists(path);
} catch (IOException ignored) {
// ignore
}
} }
private Path resolveReportDir(String reportId, String childDir) { private String buildTempBatchDir(String reportId, String batchId) {
return Paths.get(filesRootPath, TestReportConst.STORAGE_DIR, reportId, childDir).normalize(); return AiReportFileConst.BIZ_DIR_TEST_REPORT + "/" + reportId + "/"
+ TestReportConst.STORAGE_IMPORT_TEMP_DIR + "/" + batchId;
} }
private Path resolveTempBatchDir(String reportId, String batchId) { private String buildTempUploadDir(String reportId, String batchId) {
return Paths.get(filesRootPath, TestReportConst.STORAGE_DIR, reportId, return buildTempBatchDir(reportId, batchId) + "/" + TestReportConst.STORAGE_IMPORT_TEMP_UPLOAD_DIR;
TestReportConst.STORAGE_IMPORT_TEMP_DIR, batchId).normalize();
} }
private Path resolveTempUploadDir(String reportId, String batchId) { private String buildValidateSessionDir(String sessionId) {
return resolveTempBatchDir(reportId, batchId).resolve(TestReportConst.STORAGE_IMPORT_TEMP_UPLOAD_DIR); return AiReportFileConst.BIZ_DIR_TEST_REPORT + "/" + TestReportConst.STORAGE_VALIDATE_SESSION_DIR + "/" + sessionId;
} }
private Path resolveTempBatchMetaPath(String reportId, String batchId) { private String buildValidateSessionUploadDir(String sessionId) {
return resolveTempBatchDir(reportId, batchId).resolve(TestReportConst.STORAGE_IMPORT_META_FILE_NAME); return buildValidateSessionDir(sessionId) + "/" + TestReportConst.STORAGE_IMPORT_TEMP_UPLOAD_DIR;
} }
private Path resolveValidateSessionDir(String sessionId) { private String buildTempBatchMetaObjectName(String reportId, String batchId) {
return Paths.get(filesRootPath, TestReportConst.STORAGE_DIR, return buildTempBatchDir(reportId, batchId) + "/" + TestReportConst.STORAGE_IMPORT_META_FILE_NAME;
TestReportConst.STORAGE_VALIDATE_SESSION_DIR, sessionId).normalize();
} }
private Path resolveValidateSessionUploadDir(String sessionId) { private String buildValidateSessionResultObjectName(String sessionId) {
return resolveValidateSessionDir(sessionId).resolve(TestReportConst.STORAGE_IMPORT_TEMP_UPLOAD_DIR); return buildValidateSessionDir(sessionId) + "/" + TestReportConst.STORAGE_VALIDATE_RESULT_FILE_NAME;
} }
private Path resolveValidateSessionResultPath(String sessionId) { private String buildTempBatchFileObjectName(String reportId, String batchId, String fileName) {
return resolveValidateSessionDir(sessionId).resolve(TestReportConst.STORAGE_VALIDATE_RESULT_FILE_NAME); return buildTempUploadDir(reportId, batchId) + "/" + trimFileName(fileName);
} }
private String buildRelativePath(String reportId, String childDir, String fileName) { private String buildValidateSessionFileObjectName(String sessionId, String fileName) {
return reportId + "/" + childDir + "/" + fileName; return buildValidateSessionUploadDir(sessionId) + "/" + trimFileName(fileName);
} }
private String buildTempBatchRelativePath(String reportId, String batchId) { private String buildTempBatchRelativePath(String reportId, String batchId) {
return reportId + "/" + TestReportConst.STORAGE_IMPORT_TEMP_DIR + "/" + batchId; return reportId + "/" + TestReportConst.STORAGE_IMPORT_TEMP_DIR + "/" + batchId;
} }
private String buildTempUploadRelativePath(String reportId, String batchId, String fileName) {
return buildTempBatchRelativePath(reportId, batchId) + "/"
+ TestReportConst.STORAGE_IMPORT_TEMP_UPLOAD_DIR + "/" + fileName;
}
private String buildValidateSessionRelativePath(String sessionId) { private String buildValidateSessionRelativePath(String sessionId) {
return TestReportConst.STORAGE_VALIDATE_SESSION_DIR + "/" + sessionId; return TestReportConst.STORAGE_VALIDATE_SESSION_DIR + "/" + sessionId;
} }
private String buildValidateSessionUploadRelativePath(String sessionId, String fileName) { private String resolveParentDir(String objectName) {
return buildValidateSessionRelativePath(sessionId) + "/" int slashIndex = objectName.lastIndexOf('/');
+ TestReportConst.STORAGE_IMPORT_TEMP_UPLOAD_DIR + "/" + fileName; 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 { public static class UploadedBatch {

View File

@@ -1,12 +1,16 @@
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 {
@@ -32,65 +36,65 @@ public class TestReportParam {
private String id; private String id;
@ApiModelProperty("普测报告编号") @ApiModelProperty("普测报告编号")
@NotBlank(message = "\u666e\u6d4b\u62a5\u544a\u7f16\u53f7\u4e0d\u80fd\u4e3a\u7a7a") @NotBlank(message = "普测报告编号不能为空")
@Size(max = 32, message = "\u666e\u6d4b\u62a5\u544a\u7f16\u53f7\u4e0d\u80fd\u8d85\u8fc732\u4e2a\u5b57\u7b26") @Size(max = 32, message = "普测报告编号不能超过32个字符")
private String no; private String no;
@ApiModelProperty("委托单位ID") @ApiModelProperty("委托单位ID")
@NotBlank(message = "\u59d4\u6258\u5355\u4f4d\u4e0d\u80fd\u4e3a\u7a7a") @NotBlank(message = "委托单位不能为空")
private String clientUnitId; private String clientUnitId;
@ApiModelProperty("报告模板ID") @ApiModelProperty("报告模板ID")
@NotBlank(message = "\u62a5\u544a\u6a21\u677f\u4e0d\u80fd\u4e3a\u7a7a") @NotBlank(message = "报告模板不能为空")
private String reportModelId; private String reportModelId;
@ApiModelProperty("检测公司ID") @ApiModelProperty("检测公司ID")
@NotBlank(message = "\u68c0\u6d4b\u516c\u53f8\u4e0d\u80fd\u4e3a\u7a7a") @NotBlank(message = "检测公司不能为空")
private String createUnit; private String createUnit;
@ApiModelProperty("合同编号") @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("检测依据JSON字符串数组") @ApiModelProperty("检测依据JSON数组")
@NotBlank(message = "\u68c0\u6d4b\u4f9d\u636e\u4e0d\u80fd\u4e3a\u7a7a") @NotEmpty(message = "检测依据不能为空")
private String standard; private List<String> standard;
@ApiModelProperty("受资数据JSON对象或JSON数组") @ApiModelProperty("受资数据JSON对象或JSON数组")
@NotBlank(message = "\u53d7\u8d44\u6570\u636e\u4e0d\u80fd\u4e3a\u7a7a") @NotNull(message = "受资数据不能为空")
private String data; private JsonNode data;
@ApiModelProperty("台账校验通过后返回的导入批次ID") @ApiModelProperty("台账校验通过后返回的导入批次ID")
private String batchId; private String batchId;
@ApiModelProperty("联系电话") @ApiModelProperty("联系电话")
@Size(max = 32, message = "\u8054\u7cfb\u7535\u8bdd\u4e0d\u80fd\u8d85\u8fc732\u4e2a\u5b57\u7b26") @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("普测报告ID") @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("普测报告ID") @ApiModelProperty("普测报告ID")
@NotBlank(message = "\u666e\u6d4b\u62a5\u544aID\u4e0d\u80fd\u4e3a\u7a7a") @NotBlank(message = "普测报告ID不能为空")
private String id; private String id;
@ApiModelProperty("审核意见") @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 @Data
public static class LedgerImportParam { public static class LedgerImportParam {
@ApiModelProperty("校验成功的导入批次ID") @ApiModelProperty("校验成功的导入批次ID")
@NotBlank(message = "\u5bfc\u5165\u6279\u6b21ID\u4e0d\u80fd\u4e3a\u7a7a") @NotBlank(message = "导入批次ID不能为空")
private String batchId; private String batchId;
} }
} }

View File

@@ -16,10 +16,7 @@ public class TestReportPointPO {
private String reportId; private String reportId;
private Integer groupNo; private Integer groupNo;
private String substationName; private String substationName;
private String deviceName;
private String pointName; private String pointName;
private String testDeviceNo;
private String clientUnitName;
private String monitorTime; private String monitorTime;
private String voltageLevel; private String voltageLevel;
private String ptRatio; private String ptRatio;
@@ -30,8 +27,6 @@ public class TestReportPointPO {
private String powerSupplyCapacity; private String powerSupplyCapacity;
private String excelAttachmentName; private String excelAttachmentName;
private String excelStoragePath; private String excelStoragePath;
private String wordAttachmentName;
private String wordStoragePath;
private Integer sortNo; private Integer sortNo;
private Integer deleted; private Integer deleted;
private String createBy; private String createBy;

View File

@@ -8,10 +8,7 @@ public class TestReportPointVO {
private String id; private String id;
private Integer groupNo; private Integer groupNo;
private String substationName; private String substationName;
private String deviceName;
private String pointName; private String pointName;
private String testDeviceNo;
private String clientUnitName;
private String monitorTime; private String monitorTime;
private String voltageLevel; private String voltageLevel;
private String ptRatio; private String ptRatio;
@@ -21,5 +18,4 @@ public class TestReportPointVO {
private String protocolCapacity; private String protocolCapacity;
private String powerSupplyCapacity; private String powerSupplyCapacity;
private String excelAttachmentName; private String excelAttachmentName;
private String wordAttachmentName;
} }

View File

@@ -54,7 +54,6 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.IOException; 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;
@@ -230,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
@@ -308,17 +321,18 @@ 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());
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_TEMPLATE_FILE_NOT_FOUND);
}
String fileName = resolvePreviewFileName(model); String fileName = resolvePreviewFileName(model);
FilePreviewService.PreviewContent previewContent = filePreviewService.preview(filePath, fileName); Path tempFile = reportModelFileStorageService.downloadToTempFile(model.getPath(), fileName);
return ResponseEntity.ok() try {
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename*=UTF-8''" + encodeFileName(fileName)) FilePreviewService.PreviewContent previewContent = filePreviewService.preview(tempFile, fileName);
.header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate") return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(previewContent.getContentType())) .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename*=UTF-8''" + encodeFileName(fileName))
.body(previewContent.getContent()); .header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate")
.contentType(MediaType.parseMediaType(previewContent.getContentType()))
.body(previewContent.getContent());
} finally {
reportModelFileStorageService.deleteTempFileQuietly(tempFile);
}
} }
@Override @Override
@@ -427,8 +441,7 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
throw new BusinessException(CommonResponseEnum.FAIL, MSG_TEMPLATE_FILE_NOT_FOUND); throw new BusinessException(CommonResponseEnum.FAIL, MSG_TEMPLATE_FILE_NOT_FOUND);
} }
try { try {
Path filePath = testReportStorageService.resolveBusinessPath(groupReport.getReportStoragePath()); byte[] bytes = testReportStorageService.readBusinessBytes(groupReport.getReportStoragePath());
byte[] bytes = Files.readAllBytes(filePath);
return ResponseEntity.ok() return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, .header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename*=UTF-8''" + encodeFileName(groupReport.getReportFileName())) "attachment; filename*=UTF-8''" + encodeFileName(groupReport.getReportFileName()))
@@ -461,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) {
@@ -512,8 +525,8 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
throw new BusinessException(CommonResponseEnum.FAIL, MSG_IMPORT_FILES_REQUIRED); throw new BusinessException(CommonResponseEnum.FAIL, MSG_IMPORT_FILES_REQUIRED);
} }
String normalizedFileName = originalFileName.toLowerCase(); String normalizedFileName = originalFileName.toLowerCase();
if (!normalizedFileName.endsWith(".xls") && !normalizedFileName.endsWith(".xlsx")) { if (!normalizedFileName.endsWith(".xlsx")) {
throw new BusinessException(CommonResponseEnum.FAIL, "原始Excel仅允许xls或xlsx格式"); throw new BusinessException(CommonResponseEnum.FAIL, "原始Excel仅允许xlsx格式");
} }
if (!fileNames.add(originalFileName)) { if (!fileNames.add(originalFileName)) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_IMPORT_FILE_NAME_DUPLICATE); throw new BusinessException(CommonResponseEnum.FAIL, MSG_IMPORT_FILE_NAME_DUPLICATE);
@@ -595,6 +608,18 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
} }
} }
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) { private void removeOldRows(String reportId) {
LambdaQueryWrapper<TestReportPointPO> pointWrapper = new LambdaQueryWrapper<TestReportPointPO>(); LambdaQueryWrapper<TestReportPointPO> pointWrapper = new LambdaQueryWrapper<TestReportPointPO>();
pointWrapper.eq(TestReportPointPO::getReportId, reportId); pointWrapper.eq(TestReportPointPO::getReportId, reportId);
@@ -760,10 +785,7 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
vo.setId(point.getId()); vo.setId(point.getId());
vo.setGroupNo(point.getGroupNo()); vo.setGroupNo(point.getGroupNo());
vo.setSubstationName(defaultDash(point.getSubstationName())); vo.setSubstationName(defaultDash(point.getSubstationName()));
vo.setDeviceName(defaultDash(point.getDeviceName()));
vo.setPointName(defaultDash(point.getPointName())); vo.setPointName(defaultDash(point.getPointName()));
vo.setTestDeviceNo(defaultDash(point.getTestDeviceNo()));
vo.setClientUnitName(defaultDash(point.getClientUnitName()));
vo.setMonitorTime(defaultDash(point.getMonitorTime())); vo.setMonitorTime(defaultDash(point.getMonitorTime()));
vo.setVoltageLevel(defaultDash(point.getVoltageLevel())); vo.setVoltageLevel(defaultDash(point.getVoltageLevel()));
vo.setPtRatio(defaultDash(point.getPtRatio())); vo.setPtRatio(defaultDash(point.getPtRatio()));
@@ -773,7 +795,6 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
vo.setProtocolCapacity(defaultDash(point.getProtocolCapacity())); vo.setProtocolCapacity(defaultDash(point.getProtocolCapacity()));
vo.setPowerSupplyCapacity(defaultDash(point.getPowerSupplyCapacity())); vo.setPowerSupplyCapacity(defaultDash(point.getPowerSupplyCapacity()));
vo.setExcelAttachmentName(defaultDash(point.getExcelAttachmentName())); vo.setExcelAttachmentName(defaultDash(point.getExcelAttachmentName()));
vo.setWordAttachmentName(defaultDash(point.getWordAttachmentName()));
return vo; return vo;
} }
@@ -974,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) {

View File

@@ -91,11 +91,6 @@
// } // }
// //
// @Test // @Test
// void pointVoShouldExposeDeviceName() throws Exception {
// Assertions.assertNotNull(TestReportPointVO.class.getMethod("getDeviceName"));
// }
//
// @Test
// void parserShouldReadNodeInfoSheetFromSampleXls() throws Exception { // void parserShouldReadNodeInfoSheetFromSampleXls() throws Exception {
// TestReportPointExcelParser parser = new TestReportPointExcelParser(); // TestReportPointExcelParser parser = new TestReportPointExcelParser();
// try (InputStream inputStream = Files.newInputStream( // try (InputStream inputStream = Files.newInputStream(

View File

@@ -72,6 +72,7 @@ storage:
secret-key: "minioadmin" secret-key: "minioadmin"
buckets: buckets:
user-signature: cn-tool-user-signature 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="

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +0,0 @@
DROP TABLE IF EXISTS `sys_file`;
CREATE TABLE `sys_file` (
`id` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '文件ID',
`biz_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '业务类型',
`file_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '原始文件名',
`bucket_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'MinIO桶名称',
`object_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT 'MinIO对象名称',
`content_type` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '文件类型',
`file_size` bigint(20) NULL DEFAULT NULL COMMENT '文件大小(字节)',
`storage_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '存储类型',
`status` tinyint(1) NOT NULL COMMENT '状态(0删除 1正常)',
`create_by` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '创建用户',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_by` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '更新用户',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_sys_file_bucket_object` (`bucket_name`,`object_name`) USING BTREE,
KEY `idx_sys_file_biz_type` (`biz_type`) USING BTREE,
KEY `idx_sys_file_status` (`status`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '通用文件表' ROW_FORMAT = DYNAMIC;

View File

@@ -1,16 +0,0 @@
DROP TABLE IF EXISTS `sys_user_signature`;
CREATE TABLE `sys_user_signature` (
`id` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '用户签名ID',
`user_id` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '用户ID',
`user_name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '用户名',
`file_id` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '通用文件ID',
`status` tinyint(1) NOT NULL COMMENT '状态(0删除 1正常)',
`create_by` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '创建用户',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_by` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '更新用户',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_sys_user_signature_user_id` (`user_id`) USING BTREE,
UNIQUE KEY `uk_sys_user_signature_file_id` (`file_id`) USING BTREE,
KEY `idx_sys_user_signature_status` (`status`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '用户签名表' ROW_FORMAT = DYNAMIC;

View File

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