feat(ai-report): 新增MinIO文件存储服务

- 添加AiReportFileConst和AiReportFileExtensionConst常量类
- 实现AiReportMinioStorageService存储服务
- 添加AiReportMinioProperties配置类
- 集成MinIO依赖到ai-report-common模块
- 配置应用yml中的ai-report桶设置
- 更新报告模型控制器使用MinIO存储服务
- 修改报告模型文件存储组件集成MinIO
- 添加相关单元测试验证功能
This commit is contained in:
dk
2026-07-14 16:38:11 +08:00
parent aac21e5f73
commit 55a74815b2
29 changed files with 2111 additions and 911 deletions

View File

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

View File

@@ -3,326 +3,304 @@ package com.njcn.gather.aireport.testreport.component;
import cn.hutool.core.util.StrUtil;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.common.constant.AiReportFileConst;
import com.njcn.gather.aireport.common.constant.AiReportFileExtensionConst;
import com.njcn.gather.aireport.common.storage.AiReportMinioStorageService;
import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst;
import org.springframework.beans.factory.annotation.Value;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.Set;
@Component
@RequiredArgsConstructor
public class TestReportStorageService {
@Value("${files.path}")
private String filesRootPath;
private final AiReportMinioStorageService minioStorageService;
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) {
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) {
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) {
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) {
if (fileMap == null || fileMap.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "文件不能为空");
}
Map<String, String> storedFileMap = new LinkedHashMap<String, String>();
for (Map.Entry<String, MultipartFile> entry : fileMap.entrySet()) {
String originalFileName = entry.getKey();
MultipartFile file = entry.getValue();
if (file == null || file.isEmpty() || StrUtil.isBlank(originalFileName)) {
throw new BusinessException(CommonResponseEnum.FAIL, "文件不能为空");
}
String safeFileName = Paths.get(originalFileName).getFileName().toString();
Path targetPath = resolveTempUploadDir(reportId, batchId).resolve(safeFileName);
try {
Files.createDirectories(targetPath.getParent());
file.transferTo(targetPath.toFile());
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "文件上传阶段:保存临时文件失败");
}
storedFileMap.put(originalFileName, buildTempUploadRelativePath(reportId, batchId, safeFileName));
}
UploadedBatch uploadedBatch = new UploadedBatch();
uploadedBatch.setBatchId(batchId);
uploadedBatch.setTempStoragePath(buildTempBatchRelativePath(reportId, batchId));
uploadedBatch.setStoredFileMap(storedFileMap);
return uploadedBatch;
return saveBatchFiles(buildTempUploadDir(reportId, batchId), buildTempBatchRelativePath(reportId, batchId),
batchId, fileMap, "文件上传阶段:保存临时文件失败");
}
public UploadedBatch saveValidateSessionFiles(String sessionId, Map<String, MultipartFile> fileMap) {
return saveBatchFiles(buildValidateSessionUploadDir(sessionId), buildValidateSessionRelativePath(sessionId),
sessionId, fileMap, "校验文件阶段:保存校验会话文件失败");
}
public AiReportMinioStorageService.StoredObject getTempBatchObject(String reportId, String batchId, String fileName) {
return minioStorageService.getObject(buildTempBatchFileObjectName(reportId, batchId, fileName));
}
public AiReportMinioStorageService.StoredObject getValidateSessionObject(String sessionId, String fileName) {
return minioStorageService.getObject(buildValidateSessionFileObjectName(sessionId, fileName));
}
public void writeTempBatchMeta(String reportId, String batchId, String content) {
writeText(buildTempBatchMetaObjectName(reportId, batchId), content, "校验文件阶段:保存批次元数据失败");
}
public void writeValidateSessionResult(String sessionId, String content) {
writeText(buildValidateSessionResultObjectName(sessionId), content, "校验文件阶段:保存校验结果失败");
}
public List<String> listValidateSessionFileNames(String sessionId) {
return minioStorageService.listFileNames(buildValidateSessionUploadDir(sessionId));
}
public String readTempBatchMeta(String reportId, String batchId) {
String objectName = buildTempBatchMetaObjectName(reportId, batchId);
if (!minioStorageService.exists(objectName)) {
return null;
}
return new String(minioStorageService.readBytes(objectName), StandardCharsets.UTF_8);
}
public String saveLedgerSummaryFromTemp(String reportId, String batchId, String fileName) {
return copyTempBatchFile(reportId, batchId, fileName,
buildBusinessDir(reportId, TestReportConst.STORAGE_LEDGER_SUMMARY_DIR),
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "保存台账汇总excel失败");
}
public String savePointExcelFromTemp(String reportId, String batchId, String fileName) {
return copyTempBatchFile(reportId, batchId, fileName,
buildBusinessDir(reportId, TestReportConst.STORAGE_POINT_EXCEL_DIR),
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "保存监测点excel附件失败");
}
public String savePointWordFromTemp(String reportId, String batchId, String fileName) {
return copyTempBatchFile(reportId, batchId, fileName,
buildBusinessDir(reportId, TestReportConst.STORAGE_POINT_WORD_DIR),
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", "保存监测点word附件失败");
}
public String saveGeneratedPlaceholder(String reportId, String fileName, byte[] content) {
validateFileName(fileName, AiReportFileExtensionConst.WORD_EXTENSIONS, "生成报告文件名不能为空", "生成报告仅允许docx格式");
return minioStorageService.saveBytes(buildBusinessDir(reportId, TestReportConst.STORAGE_GENERATED_REPORT_DIR),
null, fileName, content, MediaType.APPLICATION_OCTET_STREAM_VALUE, false).getObjectName();
}
public byte[] readBusinessBytes(String storedPath) {
return minioStorageService.readBytes(storedPath);
}
public void deleteQuietly(String storedPath) {
minioStorageService.deleteQuietly(storedPath);
}
public void deleteTempBatchQuietly(String reportId, String batchId) {
deletePrefixQuietly(buildTempBatchDir(reportId, batchId));
}
private UploadedBatch saveBatchFiles(String bizDir, String tempStoragePath, String batchId,
Map<String, MultipartFile> fileMap, String errorMessage) {
if (fileMap == null || fileMap.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "鏂囦欢涓嶈兘涓虹┖");
throw new BusinessException(CommonResponseEnum.FAIL, "文件不能为空");
}
Map<String, String> storedFileMap = new LinkedHashMap<String, String>();
for (Map.Entry<String, MultipartFile> entry : fileMap.entrySet()) {
String originalFileName = entry.getKey();
MultipartFile file = entry.getValue();
if (file == null || file.isEmpty() || StrUtil.isBlank(originalFileName)) {
throw new BusinessException(CommonResponseEnum.FAIL, "鏂囦欢涓嶈兘涓虹┖");
try {
for (Map.Entry<String, MultipartFile> entry : fileMap.entrySet()) {
String originalFileName = trimFileName(entry.getKey());
MultipartFile file = entry.getValue();
if (StrUtil.isBlank(originalFileName) || file == null || file.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "文件不能为空");
}
validateAllowedUploadFile(originalFileName);
try (InputStream inputStream = file.getInputStream()) {
AiReportMinioStorageService.StoredFile storedFile = minioStorageService.saveInputStream(
bizDir, null, originalFileName, inputStream, file.getSize(), file.getContentType(), true);
storedFileMap.put(originalFileName, storedFile.getObjectName());
}
}
String safeFileName = Paths.get(originalFileName).getFileName().toString();
Path targetPath = resolveValidateSessionUploadDir(sessionId).resolve(safeFileName);
try {
Files.createDirectories(targetPath.getParent());
file.transferTo(targetPath.toFile());
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "鏂囦欢涓婁紶闃舵锛氫繚瀛樻牎楠屼細璇濇枃浠跺け璐?");
}
storedFileMap.put(originalFileName, buildValidateSessionUploadRelativePath(sessionId, safeFileName));
} catch (BusinessException exception) {
cleanupStoredFiles(storedFileMap);
throw exception;
} catch (Exception exception) {
cleanupStoredFiles(storedFileMap);
throw new BusinessException(CommonResponseEnum.FAIL, errorMessage);
}
UploadedBatch uploadedBatch = new UploadedBatch();
uploadedBatch.setBatchId(sessionId);
uploadedBatch.setTempStoragePath(buildValidateSessionRelativePath(sessionId));
uploadedBatch.setBatchId(batchId);
uploadedBatch.setTempStoragePath(tempStoragePath);
uploadedBatch.setStoredFileMap(storedFileMap);
return uploadedBatch;
}
public Path resolveTempBatchFile(String reportId, String batchId, String fileName) {
return resolveTempUploadDir(reportId, batchId).resolve(Paths.get(fileName).getFileName().toString()).normalize();
}
public void writeTempBatchMeta(String reportId, String batchId, String content) {
Path metaPath = resolveTempBatchMetaPath(reportId, batchId);
private void writeText(String objectName, String content, String errorMessage) {
try {
Files.createDirectories(metaPath.getParent());
Files.write(metaPath, content.getBytes(StandardCharsets.UTF_8));
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段:保存批次元数据失败");
minioStorageService.saveBytes(resolveParentDir(objectName), null, resolveFileName(objectName),
content == null ? new byte[0] : content.getBytes(StandardCharsets.UTF_8),
MediaType.APPLICATION_JSON_VALUE, true);
} catch (BusinessException exception) {
throw exception;
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, errorMessage);
}
}
public void writeValidateSessionResult(String sessionId, String content) {
Path resultPath = resolveValidateSessionResultPath(sessionId);
try {
Files.createDirectories(resultPath.getParent());
Files.write(resultPath, content.getBytes(StandardCharsets.UTF_8));
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "鏍¢獙鏂囦欢闃舵锛氫繚瀛樻牎楠岀粨鏋滃け璐?");
}
}
public Path resolveValidateSessionFile(String sessionId, String fileName) {
return resolveValidateSessionUploadDir(sessionId).resolve(Paths.get(fileName).getFileName().toString()).normalize();
}
public List<String> listValidateSessionFileNames(String sessionId) {
Path uploadDir = resolveValidateSessionUploadDir(sessionId);
if (!Files.exists(uploadDir) || !Files.isDirectory(uploadDir)) {
return new ArrayList<String>();
}
try {
List<String> result = new ArrayList<String>();
Files.list(uploadDir)
.filter(Files::isRegularFile)
.forEach(path -> result.add(path.getFileName().toString()));
return result;
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "鏍¢獙鏂囦欢闃舵锛氳鍙栦細璇濇枃浠跺垪琛ㄥけ璐?");
}
}
public String readTempBatchMeta(String reportId, String batchId) {
Path metaPath = resolveTempBatchMetaPath(reportId, batchId);
if (!Files.exists(metaPath) || !Files.isRegularFile(metaPath)) {
private String copyTempBatchFile(String reportId, String batchId, String fileName, String targetBizDir,
String contentType, String errorMessage) {
String normalizedFileName = trimFileName(fileName);
if (StrUtil.isBlank(normalizedFileName)) {
return null;
}
try {
return new String(Files.readAllBytes(metaPath), StandardCharsets.UTF_8);
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "璇诲彇瀵煎叆鎵规淇℃伅澶辫触");
}
}
public String saveLedgerSummaryFromTemp(String reportId, String batchId, String fileName) {
return savePathFile(reportId, TestReportConst.STORAGE_LEDGER_SUMMARY_DIR,
resolveTempBatchFile(reportId, batchId, fileName), fileName, "淇濆瓨鍙拌处姹囨€籈xcel澶辫触");
}
public String savePointExcelFromTemp(String reportId, String batchId, String fileName) {
return savePathFile(reportId, TestReportConst.STORAGE_POINT_EXCEL_DIR,
resolveTempBatchFile(reportId, batchId, fileName), fileName, "淇濆瓨鐩戞祴鐐笶xcel闄勪欢澶辫触");
}
public String savePointWordFromTemp(String reportId, String batchId, String fileName) {
return savePathFile(reportId, TestReportConst.STORAGE_POINT_WORD_DIR,
resolveTempBatchFile(reportId, batchId, fileName), fileName, "淇濆瓨鐩戞祴鐐筗ord闄勪欢澶辫触");
}
public String saveGeneratedPlaceholder(String reportId, String fileName, byte[] content) {
Path targetPath = resolveReportDir(reportId, TestReportConst.STORAGE_GENERATED_REPORT_DIR).resolve(fileName);
try {
Files.createDirectories(targetPath.getParent());
Files.write(targetPath, content);
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "淇濆瓨鐢熸垚鎶ュ憡澶辫触");
}
return buildRelativePath(reportId, TestReportConst.STORAGE_GENERATED_REPORT_DIR, fileName);
}
public Path resolveBusinessPath(String relativePath) {
return Paths.get(filesRootPath, TestReportConst.STORAGE_DIR).resolve(relativePath).normalize();
}
public void deleteQuietly(String relativePath) {
if (StrUtil.isBlank(relativePath)) {
return;
}
try {
Files.deleteIfExists(resolveBusinessPath(relativePath));
} catch (IOException ignored) {
// 鏂囦欢娓呯悊澶辫触涓嶅奖鍝嶄富娴佺▼
}
}
public void deleteTempBatchQuietly(String reportId, String batchId) {
if (StrUtil.isBlank(reportId) || StrUtil.isBlank(batchId)) {
return;
}
deletePathQuietly(resolveTempBatchDir(reportId, batchId));
}
private String saveMultipartFile(String reportId, String childDir, MultipartFile file, String errorMessage) {
String originalFileName = file == null ? null : file.getOriginalFilename();
if (file == null || file.isEmpty() || StrUtil.isBlank(originalFileName)) {
throw new BusinessException(CommonResponseEnum.FAIL, "文件不能为空");
}
return savePathFile(reportId, childDir, toTempTransferredPath(file), originalFileName, errorMessage);
}
private Path toTempTransferredPath(MultipartFile file) {
try {
Path tempFile = Files.createTempFile("test-report-", ".upload");
file.transferTo(tempFile.toFile());
return tempFile;
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "文件上传失败");
}
}
private String savePathFile(String reportId, String childDir, Path sourcePath, String originalFileName, String errorMessage) {
String normalizedFileName = originalFileName == null ? null : Paths.get(originalFileName).getFileName().toString();
if (sourcePath == null || StrUtil.isBlank(normalizedFileName) || !Files.exists(sourcePath)) {
throw new BusinessException(CommonResponseEnum.FAIL, "文件不能为空");
}
String fileName = UUID.randomUUID().toString().replace("-", "") + "-" + normalizedFileName;
Path targetPath = resolveReportDir(reportId, childDir).resolve(fileName);
try {
Files.createDirectories(targetPath.getParent());
Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException exception) {
byte[] content = minioStorageService.readBytes(buildTempBatchFileObjectName(reportId, batchId, normalizedFileName));
return minioStorageService.saveBytes(targetBizDir, null, normalizedFileName, content, contentType, false)
.getObjectName();
} catch (BusinessException exception) {
throw exception;
} catch (Exception exception) {
throw new BusinessException(CommonResponseEnum.FAIL, errorMessage);
} finally {
deletePathQuietlyIfTemp(sourcePath);
}
return buildRelativePath(reportId, childDir, fileName);
}
private void deletePathQuietlyIfTemp(Path path) {
if (path == null) {
private void deletePrefixQuietly(String prefix) {
List<String> objectNames = minioStorageService.listObjectNames(prefix);
for (String objectName : objectNames) {
minioStorageService.deleteQuietly(objectName);
}
}
private void cleanupStoredFiles(Map<String, String> storedFileMap) {
if (storedFileMap == null || storedFileMap.isEmpty()) {
return;
}
String fileName = path.getFileName() == null ? null : path.getFileName().toString();
if (fileName != null && fileName.startsWith("test-report-")) {
deletePathQuietly(path);
for (String objectName : storedFileMap.values()) {
minioStorageService.deleteQuietly(objectName);
}
}
private void deletePathQuietly(Path path) {
if (path == null || !Files.exists(path)) {
return;
private void validateMultipartFile(MultipartFile file, Set<String> allowedExtensions,
String emptyMessage, String invalidMessage) {
if (file == null || file.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, emptyMessage);
}
try {
if (Files.isDirectory(path)) {
Files.walk(path)
.sorted((left, right) -> right.getNameCount() - left.getNameCount())
.forEach(this::deleteSingleQuietly);
return;
}
Files.deleteIfExists(path);
} catch (IOException ignored) {
// 涓存椂鐩綍娓呯悊澶辫触涓嶅奖鍝嶄富娴佺▼
validateFileName(file.getOriginalFilename(), allowedExtensions, emptyMessage, invalidMessage);
}
private void validateAllowedUploadFile(String fileName) {
validateFileName(fileName, AiReportFileExtensionConst.ALLOWED_EXTENSIONS, "文件不能为空",
"仅允许上传docx、xlsx、pdf文件");
}
private void validateFileName(String fileName, Set<String> allowedExtensions,
String emptyMessage, String invalidMessage) {
String normalizedFileName = trimFileName(fileName);
if (StrUtil.isBlank(normalizedFileName)) {
throw new BusinessException(CommonResponseEnum.FAIL, emptyMessage);
}
String extension = resolveExtension(normalizedFileName);
if (!allowedExtensions.contains(extension)) {
throw new BusinessException(CommonResponseEnum.FAIL, invalidMessage);
}
}
private void deleteSingleQuietly(Path path) {
try {
Files.deleteIfExists(path);
} catch (IOException ignored) {
// ignore
}
private String buildBusinessDir(String reportId, String childDir) {
return AiReportFileConst.BIZ_DIR_TEST_REPORT + "/" + reportId + "/" + childDir;
}
private Path resolveReportDir(String reportId, String childDir) {
return Paths.get(filesRootPath, TestReportConst.STORAGE_DIR, reportId, childDir).normalize();
private String buildTempBatchDir(String reportId, String batchId) {
return AiReportFileConst.BIZ_DIR_TEST_REPORT + "/" + reportId + "/"
+ TestReportConst.STORAGE_IMPORT_TEMP_DIR + "/" + batchId;
}
private Path resolveTempBatchDir(String reportId, String batchId) {
return Paths.get(filesRootPath, TestReportConst.STORAGE_DIR, reportId,
TestReportConst.STORAGE_IMPORT_TEMP_DIR, batchId).normalize();
private String buildTempUploadDir(String reportId, String batchId) {
return buildTempBatchDir(reportId, batchId) + "/" + TestReportConst.STORAGE_IMPORT_TEMP_UPLOAD_DIR;
}
private Path resolveTempUploadDir(String reportId, String batchId) {
return resolveTempBatchDir(reportId, batchId).resolve(TestReportConst.STORAGE_IMPORT_TEMP_UPLOAD_DIR);
private String buildValidateSessionDir(String sessionId) {
return AiReportFileConst.BIZ_DIR_TEST_REPORT + "/" + TestReportConst.STORAGE_VALIDATE_SESSION_DIR + "/" + sessionId;
}
private Path resolveTempBatchMetaPath(String reportId, String batchId) {
return resolveTempBatchDir(reportId, batchId).resolve(TestReportConst.STORAGE_IMPORT_META_FILE_NAME);
private String buildValidateSessionUploadDir(String sessionId) {
return buildValidateSessionDir(sessionId) + "/" + TestReportConst.STORAGE_IMPORT_TEMP_UPLOAD_DIR;
}
private Path resolveValidateSessionDir(String sessionId) {
return Paths.get(filesRootPath, TestReportConst.STORAGE_DIR,
TestReportConst.STORAGE_VALIDATE_SESSION_DIR, sessionId).normalize();
private String buildTempBatchMetaObjectName(String reportId, String batchId) {
return buildTempBatchDir(reportId, batchId) + "/" + TestReportConst.STORAGE_IMPORT_META_FILE_NAME;
}
private Path resolveValidateSessionUploadDir(String sessionId) {
return resolveValidateSessionDir(sessionId).resolve(TestReportConst.STORAGE_IMPORT_TEMP_UPLOAD_DIR);
private String buildValidateSessionResultObjectName(String sessionId) {
return buildValidateSessionDir(sessionId) + "/" + TestReportConst.STORAGE_VALIDATE_RESULT_FILE_NAME;
}
private Path resolveValidateSessionResultPath(String sessionId) {
return resolveValidateSessionDir(sessionId).resolve(TestReportConst.STORAGE_VALIDATE_RESULT_FILE_NAME);
private String buildTempBatchFileObjectName(String reportId, String batchId, String fileName) {
return buildTempUploadDir(reportId, batchId) + "/" + trimFileName(fileName);
}
private String buildRelativePath(String reportId, String childDir, String fileName) {
return reportId + "/" + childDir + "/" + fileName;
private String buildValidateSessionFileObjectName(String sessionId, String fileName) {
return buildValidateSessionUploadDir(sessionId) + "/" + trimFileName(fileName);
}
private String buildTempBatchRelativePath(String reportId, String batchId) {
return reportId + "/" + TestReportConst.STORAGE_IMPORT_TEMP_DIR + "/" + batchId;
}
private String buildTempUploadRelativePath(String reportId, String batchId, String fileName) {
return buildTempBatchRelativePath(reportId, batchId) + "/"
+ TestReportConst.STORAGE_IMPORT_TEMP_UPLOAD_DIR + "/" + fileName;
}
private String buildValidateSessionRelativePath(String sessionId) {
return TestReportConst.STORAGE_VALIDATE_SESSION_DIR + "/" + sessionId;
}
private String buildValidateSessionUploadRelativePath(String sessionId, String fileName) {
return buildValidateSessionRelativePath(sessionId) + "/"
+ TestReportConst.STORAGE_IMPORT_TEMP_UPLOAD_DIR + "/" + fileName;
private String resolveParentDir(String objectName) {
int slashIndex = objectName.lastIndexOf('/');
if (slashIndex <= 0) {
throw new BusinessException(CommonResponseEnum.FAIL, "对象目录配置不合法");
}
return objectName.substring(0, slashIndex);
}
private String resolveFileName(String objectName) {
int slashIndex = objectName.lastIndexOf('/');
return slashIndex >= 0 ? objectName.substring(slashIndex + 1) : objectName;
}
private String trimFileName(String fileName) {
String normalized = StrUtil.trimToEmpty(fileName);
if (StrUtil.isBlank(normalized)) {
return null;
}
int slashIndex = Math.max(normalized.lastIndexOf('/'), normalized.lastIndexOf('\\'));
return slashIndex >= 0 ? normalized.substring(slashIndex + 1) : normalized;
}
private String resolveExtension(String fileName) {
int dotIndex = fileName.lastIndexOf('.');
if (dotIndex < 0 || dotIndex == fileName.length() - 1) {
return "";
}
return fileName.substring(dotIndex + 1).toLowerCase(Locale.ENGLISH);
}
public static class UploadedBatch {

View File

@@ -1,12 +1,16 @@
package com.njcn.gather.aireport.testreport.pojo.param;
import com.fasterxml.jackson.databind.JsonNode;
import com.njcn.web.pojo.param.BaseParam;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.List;
public class TestReportParam {
@@ -32,65 +36,65 @@ public class TestReportParam {
private String id;
@ApiModelProperty("普测报告编号")
@NotBlank(message = "\u666e\u6d4b\u62a5\u544a\u7f16\u53f7\u4e0d\u80fd\u4e3a\u7a7a")
@Size(max = 32, message = "\u666e\u6d4b\u62a5\u544a\u7f16\u53f7\u4e0d\u80fd\u8d85\u8fc732\u4e2a\u5b57\u7b26")
@NotBlank(message = "普测报告编号不能为空")
@Size(max = 32, message = "普测报告编号不能超过32个字符")
private String no;
@ApiModelProperty("委托单位ID")
@NotBlank(message = "\u59d4\u6258\u5355\u4f4d\u4e0d\u80fd\u4e3a\u7a7a")
@NotBlank(message = "委托单位不能为空")
private String clientUnitId;
@ApiModelProperty("报告模板ID")
@NotBlank(message = "\u62a5\u544a\u6a21\u677f\u4e0d\u80fd\u4e3a\u7a7a")
@NotBlank(message = "报告模板不能为空")
private String reportModelId;
@ApiModelProperty("检测公司ID")
@NotBlank(message = "\u68c0\u6d4b\u516c\u53f8\u4e0d\u80fd\u4e3a\u7a7a")
@NotBlank(message = "检测公司不能为空")
private String createUnit;
@ApiModelProperty("合同编号")
@NotBlank(message = "\u5408\u540c\u7f16\u53f7\u4e0d\u80fd\u4e3a\u7a7a")
@Size(max = 32, message = "\u5408\u540c\u7f16\u53f7\u4e0d\u80fd\u8d85\u8fc732\u4e2a\u5b57\u7b26")
@NotBlank(message = "合同编号不能为空")
@Size(max = 32, message = "合同编号不能超过32个字符")
private String contractNumber;
@ApiModelProperty("检测依据JSON字符串数组")
@NotBlank(message = "\u68c0\u6d4b\u4f9d\u636e\u4e0d\u80fd\u4e3a\u7a7a")
private String standard;
@ApiModelProperty("检测依据JSON数组")
@NotEmpty(message = "检测依据不能为空")
private List<String> standard;
@ApiModelProperty("受资数据JSON对象或JSON数组")
@NotBlank(message = "\u53d7\u8d44\u6570\u636e\u4e0d\u80fd\u4e3a\u7a7a")
private String data;
@NotNull(message = "受资数据不能为空")
private JsonNode data;
@ApiModelProperty("台账校验通过后返回的导入批次ID")
private String batchId;
@ApiModelProperty("联系电话")
@Size(max = 32, message = "\u8054\u7cfb\u7535\u8bdd\u4e0d\u80fd\u8d85\u8fc732\u4e2a\u5b57\u7b26")
@Size(max = 32, message = "联系电话不能超过32个字符")
private String phonenumber;
}
@Data
public static class UpdateParam extends AddParam {
@ApiModelProperty("普测报告ID")
@NotBlank(message = "\u666e\u6d4b\u62a5\u544aID\u4e0d\u80fd\u4e3a\u7a7a")
@NotBlank(message = "普测报告ID不能为空")
private String id;
}
@Data
public static class SubmitAuditParam {
@ApiModelProperty("普测报告ID")
@NotBlank(message = "\u666e\u6d4b\u62a5\u544aID\u4e0d\u80fd\u4e3a\u7a7a")
@NotBlank(message = "普测报告ID不能为空")
private String id;
@ApiModelProperty("审核意见")
@Size(max = 128, message = "\u5ba1\u6838\u610f\u89c1\u4e0d\u80fd\u8d85\u8fc7128\u4e2a\u5b57\u7b26")
@Size(max = 128, message = "审核意见不能超过128个字符")
private String checkResult;
}
@Data
public static class LedgerImportParam {
@ApiModelProperty("校验成功的导入批次ID")
@NotBlank(message = "\u5bfc\u5165\u6279\u6b21ID\u4e0d\u80fd\u4e3a\u7a7a")
@NotBlank(message = "导入批次ID不能为空")
private String batchId;
}
}

View File

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

View File

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

View File

@@ -54,7 +54,6 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@@ -230,14 +229,28 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
if (validIds.isEmpty()) {
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::getStatus, TestReportConst.STATUS_DELETED)
.set(TestReportPO::getUpdateBy, resolveCurrentUserId())
.set(TestReportPO::getUpdateTime, LocalDateTime.now())
.set(TestReportPO::getUpdateBy, userId)
.set(TestReportPO::getUpdateTime, now)
.in(TestReportPO::getId, validIds)
.eq(TestReportPO::getStatus, TestReportConst.STATUS_NORMAL)
.update();
if (deleted) {
for (TestReportPO report : reports) {
deletePointFiles(loadNormalPoints(report.getId()));
deleteGroupReportFiles(loadNormalGroupReports(report.getId()));
testReportStorageService.deleteQuietly(resolveSummaryStoragePath(report.getData()));
}
}
return deleted;
}
@Override
@@ -308,17 +321,18 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
public ResponseEntity<byte[]> previewTemplate(String id) {
TestReportPO report = requireNormal(id);
ReportModelPO model = requireReportModel(report.getReportModelId());
Path filePath = reportModelFileStorageService.resolveDownloadPath(model.getPath());
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_TEMPLATE_FILE_NOT_FOUND);
}
String fileName = resolvePreviewFileName(model);
FilePreviewService.PreviewContent previewContent = filePreviewService.preview(filePath, fileName);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename*=UTF-8''" + encodeFileName(fileName))
.header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate")
.contentType(MediaType.parseMediaType(previewContent.getContentType()))
.body(previewContent.getContent());
Path tempFile = reportModelFileStorageService.downloadToTempFile(model.getPath(), fileName);
try {
FilePreviewService.PreviewContent previewContent = filePreviewService.preview(tempFile, fileName);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename*=UTF-8''" + encodeFileName(fileName))
.header(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate")
.contentType(MediaType.parseMediaType(previewContent.getContentType()))
.body(previewContent.getContent());
} finally {
reportModelFileStorageService.deleteTempFileQuietly(tempFile);
}
}
@Override
@@ -427,8 +441,7 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
throw new BusinessException(CommonResponseEnum.FAIL, MSG_TEMPLATE_FILE_NOT_FOUND);
}
try {
Path filePath = testReportStorageService.resolveBusinessPath(groupReport.getReportStoragePath());
byte[] bytes = Files.readAllBytes(filePath);
byte[] bytes = testReportStorageService.readBusinessBytes(groupReport.getReportStoragePath());
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"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.setCreateUnit(requireText(param.getCreateUnit(), MSG_CREATE_UNIT_REQUIRED));
data.setContractNumber(requireText(param.getContractNumber(), MSG_CONTRACT_REQUIRED, 32, MSG_CONTRACT_TOO_LONG));
data.setStandard(requireText(param.getStandard(), MSG_STANDARD_REQUIRED));
data.setStandard(requireJsonArrayText(param.getStandard(), MSG_STANDARD_REQUIRED, MSG_STANDARD_INVALID));
data.setData(requireJsonObjectOrArrayText(param.getData(), MSG_DATA_REQUIRED, MSG_DATA_INVALID));
data.setPhonenumber(trimToNull(param.getPhonenumber()));
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);
}
String normalizedFileName = originalFileName.toLowerCase();
if (!normalizedFileName.endsWith(".xls") && !normalizedFileName.endsWith(".xlsx")) {
throw new BusinessException(CommonResponseEnum.FAIL, "原始Excel仅允许xls或xlsx格式");
if (!normalizedFileName.endsWith(".xlsx")) {
throw new BusinessException(CommonResponseEnum.FAIL, "原始Excel仅允许xlsx格式");
}
if (!fileNames.add(originalFileName)) {
throw new BusinessException(CommonResponseEnum.FAIL, MSG_IMPORT_FILE_NAME_DUPLICATE);
@@ -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) {
LambdaQueryWrapper<TestReportPointPO> pointWrapper = new LambdaQueryWrapper<TestReportPointPO>();
pointWrapper.eq(TestReportPointPO::getReportId, reportId);
@@ -760,10 +785,7 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
vo.setId(point.getId());
vo.setGroupNo(point.getGroupNo());
vo.setSubstationName(defaultDash(point.getSubstationName()));
vo.setDeviceName(defaultDash(point.getDeviceName()));
vo.setPointName(defaultDash(point.getPointName()));
vo.setTestDeviceNo(defaultDash(point.getTestDeviceNo()));
vo.setClientUnitName(defaultDash(point.getClientUnitName()));
vo.setMonitorTime(defaultDash(point.getMonitorTime()));
vo.setVoltageLevel(defaultDash(point.getVoltageLevel()));
vo.setPtRatio(defaultDash(point.getPtRatio()));
@@ -773,7 +795,6 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
vo.setProtocolCapacity(defaultDash(point.getProtocolCapacity()));
vo.setPowerSupplyCapacity(defaultDash(point.getPowerSupplyCapacity()));
vo.setExcelAttachmentName(defaultDash(point.getExcelAttachmentName()));
vo.setWordAttachmentName(defaultDash(point.getWordAttachmentName()));
return vo;
}
@@ -974,14 +995,38 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
}
}
private String requireJsonObjectOrArrayText(String value, String blankMessage, String invalidMessage) {
String text = requireText(value, blankMessage);
private String requireJsonArrayText(List<String> value, String blankMessage, String invalidMessage) {
if (value == null || value.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, blankMessage);
}
try {
JsonNode jsonNode = OBJECT_MAPPER.readTree(text);
if (jsonNode == null || !(jsonNode.isObject() || jsonNode.isArray())) {
ArrayList<String> normalizedItems = new ArrayList<String>();
for (String item : value) {
String normalizedItem = trimToNull(item);
if (normalizedItem != null) {
normalizedItems.add(normalizedItem);
}
}
if (normalizedItems.isEmpty()) {
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) {
throw exception;
} catch (Exception exception) {

View File

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