feat(testreport): 添加多模板普测报告生成功能

- 创建多模板普测任务表(ai_testreport_multi)用于管理批量任务
- 扩展测试报告表添加多模板关联字段和模板目录路径信息
- 添加测试报告点表的Word附件相关字段用于文件存储
- 实现多模板批任务控制器提供压缩包校验和任务创建功能
- 开发多模板批任务服务接口支持任务管理和报告生成
- 添加多模板批任务存储服务实现文件上传和元数据管理
- 创建多模板常量类定义任务状态和存储路径常量
- 实现多模板批任务参数验证和数据传输对象
- 添加多模板批任务验证结果和模板目录汇总视图对象
- 完善测试报告台账导入服务删除旧文件时处理Word存储路径
This commit is contained in:
dk
2026-07-21 15:47:58 +08:00
parent 598ff9ea81
commit 664703f139
22 changed files with 2607 additions and 23 deletions

View File

@@ -562,6 +562,7 @@ 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

@@ -6,6 +6,7 @@ import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.common.constant.AiReportFileConst; import com.njcn.gather.aireport.common.constant.AiReportFileConst;
import com.njcn.gather.aireport.common.constant.AiReportFileExtensionConst; import com.njcn.gather.aireport.common.constant.AiReportFileExtensionConst;
import com.njcn.gather.aireport.common.storage.AiReportMinioStorageService; import com.njcn.gather.aireport.common.storage.AiReportMinioStorageService;
import com.njcn.gather.aireport.testreport.mapper.TestReportMapper;
import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst; import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -34,6 +35,7 @@ public class TestReportStorageService {
new LinkedHashSet<String>(Arrays.asList("doc", "docx", "xls", "xlsx", "pdf"))); new LinkedHashSet<String>(Arrays.asList("doc", "docx", "xls", "xlsx", "pdf")));
private final AiReportMinioStorageService minioStorageService; private final AiReportMinioStorageService minioStorageService;
private final TestReportMapper testReportMapper;
public String saveSourceExcel(String reportId, MultipartFile file) { public String saveSourceExcel(String reportId, MultipartFile file) {
validateMultipartFile(file, AiReportFileExtensionConst.EXCEL_EXTENSIONS, "原始Excel文件不能为空", "原始Excel仅允许xls/xlsx格式"); validateMultipartFile(file, AiReportFileExtensionConst.EXCEL_EXTENSIONS, "原始Excel文件不能为空", "原始Excel仅允许xls/xlsx格式");
@@ -59,6 +61,42 @@ public class TestReportStorageService {
null, file, false).getObjectName(); null, file, false).getObjectName();
} }
public String saveLedgerSummaryBytes(String reportId, String fileName, byte[] content) {
validateFileName(fileName, AiReportFileExtensionConst.EXCEL_EXTENSIONS, "台账汇总文件不能为空", "台账汇总仅允许xls/xlsx格式");
return minioStorageService.saveBytes(buildBusinessDir(reportId, TestReportConst.STORAGE_LEDGER_SUMMARY_DIR),
null, fileName, content, resolveExcelContentType(fileName), false).getObjectName();
}
public String saveLedgerSummaryBytes(String reportId, String multiId, String fileName, byte[] content) {
validateFileName(fileName, AiReportFileExtensionConst.EXCEL_EXTENSIONS, "台账汇总文件不能为空", "台账汇总仅允许xls/xlsx格式");
return minioStorageService.saveBytes(buildBusinessDir(reportId, multiId, TestReportConst.STORAGE_LEDGER_SUMMARY_DIR),
null, fileName, content, resolveExcelContentType(fileName), false).getObjectName();
}
public String savePointExcelBytes(String reportId, String fileName, byte[] content) {
validateFileName(fileName, AiReportFileExtensionConst.EXCEL_EXTENSIONS, "监测点Excel附件不能为空", "监测点Excel附件仅允许xls/xlsx格式");
return minioStorageService.saveBytes(buildBusinessDir(reportId, TestReportConst.STORAGE_POINT_EXCEL_DIR),
null, fileName, content, resolveExcelContentType(fileName), false).getObjectName();
}
public String savePointExcelBytes(String reportId, String multiId, String fileName, byte[] content) {
validateFileName(fileName, AiReportFileExtensionConst.EXCEL_EXTENSIONS, "监测点Excel附件不能为空", "监测点Excel附件仅允许xls/xlsx格式");
return minioStorageService.saveBytes(buildBusinessDir(reportId, multiId, TestReportConst.STORAGE_POINT_EXCEL_DIR),
null, fileName, content, resolveExcelContentType(fileName), false).getObjectName();
}
public String savePointWordBytes(String reportId, String fileName, byte[] content) {
validateFileName(fileName, POINT_WORD_ATTACHMENT_EXTENSIONS, "监测点Word附件不能为空", "监测点Word附件仅允许doc/docx格式");
return minioStorageService.saveBytes(buildBusinessDir(reportId, TestReportConst.STORAGE_POINT_WORD_DIR),
null, fileName, content, resolveWordContentType(fileName), false).getObjectName();
}
public String savePointWordBytes(String reportId, String multiId, String fileName, byte[] content) {
validateFileName(fileName, POINT_WORD_ATTACHMENT_EXTENSIONS, "监测点Word附件不能为空", "监测点Word附件仅允许doc/docx格式");
return minioStorageService.saveBytes(buildBusinessDir(reportId, multiId, TestReportConst.STORAGE_POINT_WORD_DIR),
null, fileName, content, resolveWordContentType(fileName), false).getObjectName();
}
public UploadedBatch saveTempBatchFiles(String reportId, String batchId, Map<String, MultipartFile> fileMap) { public UploadedBatch saveTempBatchFiles(String reportId, String batchId, Map<String, MultipartFile> fileMap) {
return saveBatchFiles(buildTempUploadDir(reportId, batchId), buildTempBatchRelativePath(reportId, batchId), return saveBatchFiles(buildTempUploadDir(reportId, batchId), buildTempBatchRelativePath(reportId, batchId),
batchId, fileMap, "文件上传阶段:保存临时文件失败"); batchId, fileMap, "文件上传阶段:保存临时文件失败");
@@ -121,6 +159,12 @@ public class TestReportStorageService {
null, fileName, content, MediaType.APPLICATION_OCTET_STREAM_VALUE, false).getObjectName(); null, fileName, content, MediaType.APPLICATION_OCTET_STREAM_VALUE, false).getObjectName();
} }
public String saveGeneratedPlaceholder(String reportId, String multiId, String fileName, byte[] content) {
validateFileName(fileName, AiReportFileExtensionConst.WORD_EXTENSIONS, "生成报告文件名不能为空", "生成报告仅允许docx格式");
return minioStorageService.saveBytes(buildBusinessDir(reportId, multiId, TestReportConst.STORAGE_GENERATED_REPORT_DIR),
null, fileName, content, MediaType.APPLICATION_OCTET_STREAM_VALUE, false).getObjectName();
}
public byte[] readBusinessBytes(String storedPath) { public byte[] readBusinessBytes(String storedPath) {
return minioStorageService.readBytes(storedPath); return minioStorageService.readBytes(storedPath);
} }
@@ -254,10 +298,22 @@ public class TestReportStorageService {
} }
private String buildBusinessDir(String reportId, String childDir) { private String buildBusinessDir(String reportId, String childDir) {
return buildBusinessDir(reportId, resolveReportMultiId(reportId), childDir);
}
private String buildBusinessDir(String reportId, String multiId, String childDir) {
if (StrUtil.isNotBlank(multiId)) {
return AiReportFileConst.BIZ_DIR_TEST_REPORT + "/multi-task/" + multiId + "/child-report/" + reportId + "/" + childDir;
}
return AiReportFileConst.BIZ_DIR_TEST_REPORT + "/" + reportId + "/" + childDir; return AiReportFileConst.BIZ_DIR_TEST_REPORT + "/" + reportId + "/" + childDir;
} }
private String buildTempBatchDir(String reportId, String batchId) { private String buildTempBatchDir(String reportId, String batchId) {
String multiId = resolveReportMultiId(reportId);
if (StrUtil.isNotBlank(multiId)) {
return AiReportFileConst.BIZ_DIR_TEST_REPORT + "/multi-task/" + multiId + "/child-report/" + reportId + "/"
+ TestReportConst.STORAGE_IMPORT_TEMP_DIR + "/" + batchId;
}
return AiReportFileConst.BIZ_DIR_TEST_REPORT + "/" + reportId + "/" return AiReportFileConst.BIZ_DIR_TEST_REPORT + "/" + reportId + "/"
+ TestReportConst.STORAGE_IMPORT_TEMP_DIR + "/" + batchId; + TestReportConst.STORAGE_IMPORT_TEMP_DIR + "/" + batchId;
} }
@@ -291,9 +347,21 @@ public class TestReportStorageService {
} }
private String buildTempBatchRelativePath(String reportId, String batchId) { private String buildTempBatchRelativePath(String reportId, String batchId) {
String multiId = resolveReportMultiId(reportId);
if (StrUtil.isNotBlank(multiId)) {
return "multi-task/" + multiId + "/child-report/" + reportId + "/" + TestReportConst.STORAGE_IMPORT_TEMP_DIR + "/" + batchId;
}
return reportId + "/" + TestReportConst.STORAGE_IMPORT_TEMP_DIR + "/" + batchId; return reportId + "/" + TestReportConst.STORAGE_IMPORT_TEMP_DIR + "/" + batchId;
} }
private String resolveReportMultiId(String reportId) {
String normalizedReportId = trimFileName(reportId);
if (StrUtil.isBlank(normalizedReportId) || testReportMapper == null) {
return null;
}
return StrUtil.trimToNull(testReportMapper.selectReportMultiId(normalizedReportId));
}
private String buildValidateSessionRelativePath(String sessionId) { private String buildValidateSessionRelativePath(String sessionId) {
return TestReportConst.STORAGE_VALIDATE_SESSION_DIR + "/" + sessionId; return TestReportConst.STORAGE_VALIDATE_SESSION_DIR + "/" + sessionId;
} }

View File

@@ -14,8 +14,9 @@ public interface TestReportMapper extends BaseMapper<TestReportPO> {
@Select({ @Select({
"<script>", "<script>",
"select r.id, r.no, r.client_unit_id as clientUnitId, cu.name as clientUnitName,", "select r.id, r.multi_id as multiId, r.no, r.client_unit_id as clientUnitId, cu.name as clientUnitName,",
"r.report_model_id as reportModelId, rm.name as reportModelName, r.create_unit as createUnit,", "r.report_model_id as reportModelId, rm.name as reportModelName, r.template_dir_name as templateDirName,",
"r.template_dir_path as templateDirPath, r.sort_no as sortNo, r.create_unit as createUnit,",
"r.contract_number as contractNumber, r.standard, r.data, r.phonenumber, r.check_id as checkId,", "r.contract_number as contractNumber, r.standard, r.data, r.phonenumber, r.check_id as checkId,",
"checker.name as checkerName, r.check_time as checkTime, r.check_result as checkResult, r.state,", "checker.name as checkerName, r.check_time as checkTime, r.check_result as checkResult, r.state,",
"r.report_generate_state as reportGenerateState,", "r.report_generate_state as reportGenerateState,",
@@ -51,8 +52,9 @@ public interface TestReportMapper extends BaseMapper<TestReportPO> {
Page<TestReportVO> selectReportPage(Page<TestReportVO> page, @Param("param") TestReportParam.QueryParam param); Page<TestReportVO> selectReportPage(Page<TestReportVO> page, @Param("param") TestReportParam.QueryParam param);
@Select({ @Select({
"select r.id, r.no, r.client_unit_id as clientUnitId, cu.name as clientUnitName,", "select r.id, r.multi_id as multiId, r.no, r.client_unit_id as clientUnitId, cu.name as clientUnitName,",
"r.report_model_id as reportModelId, rm.name as reportModelName, r.create_unit as createUnit,", "r.report_model_id as reportModelId, rm.name as reportModelName, r.template_dir_name as templateDirName,",
"r.template_dir_path as templateDirPath, r.sort_no as sortNo, r.create_unit as createUnit,",
"r.contract_number as contractNumber, r.standard, r.data, r.phonenumber, r.check_id as checkId,", "r.contract_number as contractNumber, r.standard, r.data, r.phonenumber, r.check_id as checkId,",
"checker.name as checkerName, r.check_time as checkTime, r.check_result as checkResult, r.state,", "checker.name as checkerName, r.check_time as checkTime, r.check_result as checkResult, r.state,",
"r.report_generate_state as reportGenerateState,", "r.report_generate_state as reportGenerateState,",
@@ -75,4 +77,29 @@ public interface TestReportMapper extends BaseMapper<TestReportPO> {
@Select("select count(1) from ai_reportmodel where id = #{id} and status = 1") @Select("select count(1) from ai_reportmodel where id = #{id} and status = 1")
int countNormalReportModel(@Param("id") String id); int countNormalReportModel(@Param("id") String id);
@Select("select multi_id from ai_testreport where id = #{id} limit 1")
String selectReportMultiId(@Param("id") String id);
@Select({
"select r.id, r.multi_id as multiId, r.no, r.client_unit_id as clientUnitId, cu.name as clientUnitName,",
"r.report_model_id as reportModelId, rm.name as reportModelName, r.template_dir_name as templateDirName,",
"r.template_dir_path as templateDirPath, r.sort_no as sortNo, r.create_unit as createUnit,",
"r.contract_number as contractNumber, r.standard, r.data, r.phonenumber, r.check_id as checkId,",
"checker.name as checkerName, r.check_time as checkTime, r.check_result as checkResult, r.state,",
"r.report_generate_state as reportGenerateState,",
"(select count(1) from ai_testreport_point p where p.report_id = r.id and p.deleted = 0) as pointCount,",
"(select count(1) from ai_testreport_group_report gr where gr.report_id = r.id and gr.deleted = 0) as groupCount,",
"r.status, r.create_by as createBy, creator.name as createByName, r.create_time as createTime,",
"r.update_by as updateBy, updater.name as updateByName, r.update_time as updateTime",
"from ai_testreport r",
"left join ai_clientunit cu on r.client_unit_id = cu.id",
"left join ai_reportmodel rm on r.report_model_id = rm.id",
"left join sys_user creator on r.create_by = creator.id",
"left join sys_user updater on r.update_by = updater.id",
"left join sys_user checker on r.check_id = checker.id",
"where r.multi_id = #{multiId} and r.status = 1",
"order by r.sort_no asc, r.create_time asc"
})
java.util.List<TestReportVO> selectByMultiId(@Param("multiId") String multiId);
} }

View File

@@ -16,12 +16,20 @@ public class TestReportPO implements Serializable {
@TableId("id") @TableId("id")
private String id; private String id;
@TableField("multi_id")
private String multiId;
@TableField("no") @TableField("no")
private String no; private String no;
@TableField("client_unit_id") @TableField("client_unit_id")
private String clientUnitId; private String clientUnitId;
@TableField("report_model_id") @TableField("report_model_id")
private String reportModelId; private String reportModelId;
@TableField("template_dir_name")
private String templateDirName;
@TableField("template_dir_path")
private String templateDirPath;
@TableField("sort_no")
private Integer sortNo;
@TableField("create_unit") @TableField("create_unit")
private String createUnit; private String createUnit;
@TableField("contract_number") @TableField("contract_number")

View File

@@ -26,7 +26,11 @@ public class TestReportPointPO {
private String protocolCapacity; private String protocolCapacity;
private String powerSupplyCapacity; private String powerSupplyCapacity;
private String excelAttachmentName; private String excelAttachmentName;
private String excelAttachmentRelativePath;
private String excelStoragePath; private String excelStoragePath;
private String wordAttachmentName;
private String wordAttachmentRelativePath;
private String wordStoragePath;
private Integer sortNo; private Integer sortNo;
private Integer deleted; private Integer deleted;
private String createBy; private String createBy;

View File

@@ -18,4 +18,7 @@ public class TestReportPointVO {
private String protocolCapacity; private String protocolCapacity;
private String powerSupplyCapacity; private String powerSupplyCapacity;
private String excelAttachmentName; private String excelAttachmentName;
private String excelAttachmentRelativePath;
private String wordAttachmentName;
private String wordAttachmentRelativePath;
} }

View File

@@ -20,6 +20,8 @@ public class TestReportVO {
@ApiModelProperty("普测报告ID") @ApiModelProperty("普测报告ID")
private String id; private String id;
@ApiModelProperty("所属多模板任务ID")
private String multiId;
@ApiModelProperty("普测报告编号") @ApiModelProperty("普测报告编号")
private String no; private String no;
@ApiModelProperty("委托单位ID") @ApiModelProperty("委托单位ID")
@@ -30,6 +32,12 @@ public class TestReportVO {
private String reportModelId; private String reportModelId;
@ApiModelProperty("报告模板名称") @ApiModelProperty("报告模板名称")
private String reportModelName; private String reportModelName;
@ApiModelProperty("压缩包一级模板目录名称")
private String templateDirName;
@ApiModelProperty("压缩包一级模板目录相对路径")
private String templateDirPath;
@ApiModelProperty("父任务内排序号")
private Integer sortNo;
@ApiModelProperty("检测公司ID") @ApiModelProperty("检测公司ID")
private String createUnit; private String createUnit;
@ApiModelProperty("合同编号") @ApiModelProperty("合同编号")
@@ -88,6 +96,10 @@ public class TestReportVO {
return defaultDisplay(no); return defaultDisplay(no);
} }
public String getMultiId() {
return defaultDisplay(multiId);
}
public String getClientUnitId() { public String getClientUnitId() {
return defaultDisplay(clientUnitId); return defaultDisplay(clientUnitId);
} }
@@ -104,6 +116,14 @@ public class TestReportVO {
return defaultDisplay(reportModelName); return defaultDisplay(reportModelName);
} }
public String getTemplateDirName() {
return defaultDisplay(templateDirName);
}
public String getTemplateDirPath() {
return defaultDisplay(templateDirPath);
}
public String getCreateUnit() { public String getCreateUnit() {
return defaultDisplay(createUnit); return defaultDisplay(createUnit);
} }

View File

@@ -599,6 +599,7 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
private void deletePointFiles(List<TestReportPointPO> oldPoints) { private void deletePointFiles(List<TestReportPointPO> oldPoints) {
for (TestReportPointPO oldPoint : oldPoints) { for (TestReportPointPO oldPoint : oldPoints) {
testReportStorageService.deleteQuietly(oldPoint.getExcelStoragePath()); testReportStorageService.deleteQuietly(oldPoint.getExcelStoragePath());
testReportStorageService.deleteQuietly(oldPoint.getWordStoragePath());
} }
} }
@@ -762,7 +763,7 @@ public class TestReportServiceImpl extends ServiceImpl<TestReportMapper, TestRep
String reportName = report.getNo() + "-第" + groupReport.getGroupNo() + "组检测报告"; String reportName = report.getNo() + "-第" + groupReport.getGroupNo() + "组检测报告";
String reportFileName = ExportFileNameUtil.appendToday(reportName + ".docx"); String reportFileName = ExportFileNameUtil.appendToday(reportName + ".docx");
String relativePath = testReportStorageService.saveGeneratedPlaceholder(report.getId(), reportFileName, String relativePath = testReportStorageService.saveGeneratedPlaceholder(report.getId(), report.getMultiId(), reportFileName,
("group:" + groupReport.getGroupNo()).getBytes(StandardCharsets.UTF_8)); ("group:" + groupReport.getGroupNo()).getBytes(StandardCharsets.UTF_8));
groupReport.setReportName(reportName); groupReport.setReportName(reportName);
groupReport.setReportFileName(reportFileName); groupReport.setReportFileName(reportFileName);
@@ -795,6 +796,9 @@ 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.setExcelAttachmentRelativePath(defaultDash(point.getExcelAttachmentRelativePath()));
vo.setWordAttachmentName(defaultDash(point.getWordAttachmentName()));
vo.setWordAttachmentRelativePath(defaultDash(point.getWordAttachmentRelativePath()));
return vo; return vo;
} }

View File

@@ -0,0 +1,169 @@
package com.njcn.gather.aireport.testreportmulti.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.storage.AiReportMinioStorageService;
import com.njcn.gather.aireport.testreportmulti.pojo.constant.TestReportMultiConst;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.nio.charset.StandardCharsets;
import java.util.List;
@Component
@RequiredArgsConstructor
public class TestReportMultiStorageService {
private final AiReportMinioStorageService minioStorageService;
public AiReportMinioStorageService.StoredFile saveSessionZip(String sessionId, MultipartFile file) {
validateZipFile(file);
return minioStorageService.saveMultipartFile(buildSessionZipDir(sessionId), null, file, false);
}
public void saveSessionExtractedFile(String sessionId, String relativePath, byte[] content, String contentType) {
String normalizedRelativePath = normalizeRelativePath(relativePath);
minioStorageService.saveBytes(buildSessionExtractDir(sessionId, resolveParentPath(normalizedRelativePath)),
null, resolveFileName(normalizedRelativePath), content, contentType, true);
}
public byte[] readSessionExtractedBytes(String sessionId, String relativePath) {
return minioStorageService.readBytes(buildSessionExtractObjectName(sessionId, relativePath));
}
public void writeSessionMeta(String sessionId, String content) {
minioStorageService.saveBytes(buildSessionDir(sessionId), null, TestReportMultiConst.STORAGE_SESSION_META_FILE_NAME,
content == null ? new byte[0] : content.getBytes(StandardCharsets.UTF_8), MediaType.APPLICATION_JSON_VALUE, true);
}
public String readSessionMeta(String sessionId) {
String objectName = buildSessionMetaObjectName(sessionId);
if (!minioStorageService.exists(objectName)) {
return null;
}
return new String(minioStorageService.readBytes(objectName), StandardCharsets.UTF_8);
}
public byte[] readSessionZipBytes(String zipStoragePath) {
return minioStorageService.readBytes(zipStoragePath);
}
public String saveTaskZipBytes(String multiId, String fileName, byte[] content) {
return minioStorageService.saveBytes(buildTaskDir(multiId, TestReportMultiConst.STORAGE_MULTI_ZIP_DIR),
null, fileName, content, "application/zip", false).getObjectName();
}
public String saveTaskSummaryBytes(String multiId, String fileName, byte[] content) {
return minioStorageService.saveBytes(buildTaskDir(multiId, TestReportMultiConst.STORAGE_MULTI_SUMMARY_DIR),
null, fileName, content, resolveExcelContentType(fileName), false).getObjectName();
}
public List<String> listTaskChildReportObjectNames(String multiId) {
return minioStorageService.listObjectNames(buildTaskChildReportDir(multiId));
}
public List<String> listTaskSummaryObjectNames(String multiId) {
return minioStorageService.listObjectNames(buildTaskSummaryDir(multiId));
}
public byte[] readStoredBytes(String objectName) {
return minioStorageService.readBytes(objectName);
}
public String buildTaskChildReportDir(String multiId) {
return buildTaskDir(multiId, TestReportMultiConst.STORAGE_MULTI_CHILD_REPORT_DIR);
}
public String buildTaskSummaryDir(String multiId) {
return buildTaskDir(multiId, TestReportMultiConst.STORAGE_MULTI_SUMMARY_DIR);
}
public void deleteSessionQuietly(String sessionId) {
deletePrefixQuietly(buildSessionDir(sessionId));
}
public void deleteQuietly(String storedPath) {
minioStorageService.deleteQuietly(storedPath);
}
private void validateZipFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "压缩包不能为空");
}
String fileName = file.getOriginalFilename();
if (StrUtil.isBlank(fileName) || !fileName.trim().toLowerCase().endsWith(".zip")) {
throw new BusinessException(CommonResponseEnum.FAIL, "仅支持上传zip压缩包");
}
}
private void deletePrefixQuietly(String prefix) {
List<String> objectNames = minioStorageService.listObjectNames(prefix);
for (String objectName : objectNames) {
minioStorageService.deleteQuietly(objectName);
}
}
private String resolveExcelContentType(String fileName) {
return fileName != null && fileName.toLowerCase().endsWith(".xls")
? "application/vnd.ms-excel"
: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
}
private String buildSessionDir(String sessionId) {
return AiReportFileConst.BIZ_DIR_TEST_REPORT + "/" + TestReportMultiConst.STORAGE_MULTI_SESSION_DIR + "/" + sessionId;
}
private String buildSessionZipDir(String sessionId) {
return buildSessionDir(sessionId) + "/" + TestReportMultiConst.STORAGE_SESSION_ZIP_DIR;
}
private String buildSessionExtractDir(String sessionId, String parentPath) {
String dir = buildSessionDir(sessionId) + "/" + TestReportMultiConst.STORAGE_SESSION_EXTRACT_DIR;
return StrUtil.isBlank(parentPath) ? dir : dir + "/" + parentPath;
}
private String buildSessionExtractObjectName(String sessionId, String relativePath) {
return buildSessionDir(sessionId) + "/" + TestReportMultiConst.STORAGE_SESSION_EXTRACT_DIR + "/" + normalizeRelativePath(relativePath);
}
private String buildSessionMetaObjectName(String sessionId) {
return buildSessionDir(sessionId) + "/" + TestReportMultiConst.STORAGE_SESSION_META_FILE_NAME;
}
private String buildTaskDir(String multiId, String childDir) {
return AiReportFileConst.BIZ_DIR_TEST_REPORT + "/" + TestReportMultiConst.STORAGE_MULTI_TASK_DIR + "/" + multiId + "/" + childDir;
}
private String resolveParentPath(String relativePath) {
int index = relativePath.lastIndexOf('/');
return index < 0 ? "" : relativePath.substring(0, index);
}
private String resolveFileName(String relativePath) {
int index = relativePath.lastIndexOf('/');
return index < 0 ? relativePath : relativePath.substring(index + 1);
}
private String normalizeRelativePath(String relativePath) {
String normalized = StrUtil.trimToEmpty(relativePath).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, "压缩包内文件路径不能为空");
}
for (String segment : normalized.split("/")) {
if (StrUtil.isBlank(segment) || ".".equals(segment) || "..".equals(segment)) {
throw new BusinessException(CommonResponseEnum.FAIL, "压缩包内文件路径不合法");
}
}
return normalized;
}
}

View File

@@ -0,0 +1,344 @@
package com.njcn.gather.aireport.testreportmulti.component;
import cn.hutool.core.util.StrUtil;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.testreport.component.TestReportLedgerExcelParser;
import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst;
import com.njcn.gather.aireport.testreportmulti.pojo.vo.TestReportMultiTemplateDirVO;
import lombok.Data;
import lombok.RequiredArgsConstructor;
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.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@Component
@RequiredArgsConstructor
public class TestReportMultiZipParser {
private static final Set<String> SUPPORTED_EXTENSIONS = new LinkedHashSet<String>();
static {
SUPPORTED_EXTENSIONS.add("xls");
SUPPORTED_EXTENSIONS.add("xlsx");
SUPPORTED_EXTENSIONS.add("doc");
SUPPORTED_EXTENSIONS.add("docx");
}
private final TestReportLedgerExcelParser testReportLedgerExcelParser;
public ParsedArchive parse(MultipartFile zipFile) {
if (zipFile == null || zipFile.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "压缩包不能为空");
}
String zipFileName = requireZipFileName(zipFile.getOriginalFilename());
Map<String, byte[]> rawEntries = readZipEntries(zipFile);
Map<String, byte[]> normalizedEntries = stripSingleWrapperDir(rawEntries);
String summaryPath = resolveSummaryPath(normalizedEntries.keySet());
if (summaryPath == null) {
throw new BusinessException(CommonResponseEnum.FAIL, "压缩包根目录缺少台账信息汇总.xls或台账信息汇总.xlsx");
}
byte[] summaryBytes = normalizedEntries.get(summaryPath);
TestReportLedgerExcelParser.ParsedLedger ledger = testReportLedgerExcelParser.parseForValidate(
resolveFileName(summaryPath), new ByteArrayInputStream(summaryBytes));
ParsedArchive archive = new ParsedArchive();
archive.setZipFileName(zipFileName);
archive.setSummaryFileName(resolveFileName(summaryPath));
archive.setSummaryRelativePath(summaryPath);
archive.setSummaryFileBytes(summaryBytes);
archive.setLedger(ledger);
archive.setFileContentMap(normalizedEntries);
archive.setPointCount(ledger.getPoints().size());
archive.setExcelAttachmentCount(ledger.getPoints().size());
Map<String, TemplateDirSummary> dirSummaryMap = new LinkedHashMap<String, TemplateDirSummary>();
Set<String> referencedPaths = new HashSet<String>();
int wordAttachmentCount = 0;
for (TestReportLedgerExcelParser.ParsedPointRow pointRow : ledger.getPoints()) {
String excelRelativePath = normalizeRelativePath(pointRow.getExcelAttachmentName());
String templateDirPath = resolveTopLevelDir(excelRelativePath);
if (templateDirPath == null) {
archive.getFailReasons().add("" + pointRow.getRowNo() + "行Excel附件名称必须包含一级模板目录");
continue;
}
if (!normalizedEntries.containsKey(excelRelativePath)) {
archive.getFailReasons().add("" + pointRow.getRowNo() + "行缺少Excel附件" + excelRelativePath);
}
referencedPaths.add(excelRelativePath);
String wordRelativePath = null;
if (StrUtil.isNotBlank(pointRow.getWordAttachmentName())) {
wordAttachmentCount++;
wordRelativePath = normalizeRelativePath(pointRow.getWordAttachmentName());
if (!normalizedEntries.containsKey(wordRelativePath)) {
archive.getFailReasons().add("" + pointRow.getRowNo() + "行缺少Word附件" + wordRelativePath);
}
if (!templateDirPath.equals(resolveTopLevelDir(wordRelativePath))) {
archive.getFailReasons().add("" + pointRow.getRowNo() + "行Word附件与Excel附件不在同一一级目录");
}
referencedPaths.add(wordRelativePath);
}
TemplateDirSummary dirSummary = dirSummaryMap.computeIfAbsent(templateDirPath, this::newTemplateDirSummary);
dirSummary.setPointCount(dirSummary.getPointCount() + 1);
dirSummary.setExcelAttachmentCount(dirSummary.getExcelAttachmentCount() + 1);
if (StrUtil.isNotBlank(wordRelativePath)) {
dirSummary.setWordAttachmentCount(dirSummary.getWordAttachmentCount() + 1);
}
if (pointRow.getGroupNo() != null) {
dirSummary.getGroupNos().add(pointRow.getGroupNo());
}
ParsedPointBinding pointBinding = new ParsedPointBinding();
pointBinding.setRowNo(pointRow.getRowNo());
pointBinding.setGroupNo(pointRow.getGroupNo());
pointBinding.setSubstationName(pointRow.getSubstationName());
pointBinding.setPointName(pointRow.getPointName());
pointBinding.setTemplateDirName(dirSummary.getDirName());
pointBinding.setTemplateDirPath(dirSummary.getDirPath());
pointBinding.setExcelAttachmentName(resolveFileName(excelRelativePath));
pointBinding.setExcelAttachmentRelativePath(excelRelativePath);
pointBinding.setWordAttachmentName(StrUtil.isBlank(wordRelativePath) ? null : resolveFileName(wordRelativePath));
pointBinding.setWordAttachmentRelativePath(wordRelativePath);
archive.getPoints().add(pointBinding);
}
archive.setWordAttachmentCount(wordAttachmentCount);
for (TemplateDirSummary dirSummary : dirSummaryMap.values()) {
TestReportMultiTemplateDirVO dirVO = new TestReportMultiTemplateDirVO();
dirVO.setDirName(dirSummary.getDirName());
dirVO.setDirPath(dirSummary.getDirPath());
dirVO.setPointCount(dirSummary.getPointCount());
dirVO.setGroupCount(dirSummary.getGroupNos().size());
dirVO.setExcelAttachmentCount(dirSummary.getExcelAttachmentCount());
dirVO.setWordAttachmentCount(dirSummary.getWordAttachmentCount());
archive.getTemplateDirs().add(dirVO);
}
for (String relativePath : normalizedEntries.keySet()) {
if (summaryPath.equals(relativePath)) {
continue;
}
if (!referencedPaths.contains(relativePath)) {
archive.getOrphanAttachmentPaths().add(relativePath);
}
}
if (!archive.getOrphanAttachmentPaths().isEmpty()) {
archive.getFailReasons().add("压缩包存在未被台账引用的附件");
}
archive.setDirCount(archive.getTemplateDirs().size());
archive.setChildReportCount(archive.getTemplateDirs().size());
archive.setSuccess(archive.getFailReasons().isEmpty());
return archive;
}
private Map<String, byte[]> readZipEntries(MultipartFile zipFile) {
Map<String, byte[]> result = new LinkedHashMap<String, byte[]>();
Set<String> duplicateCheck = new HashSet<String>();
try (ZipInputStream zipInputStream = new ZipInputStream(zipFile.getInputStream())) {
ZipEntry entry;
while ((entry = zipInputStream.getNextEntry()) != null) {
if (entry.isDirectory()) {
continue;
}
String normalizedPath = normalizeRelativePath(entry.getName());
if (isIgnoredEntry(normalizedPath)) {
continue;
}
validateEntryExtension(normalizedPath);
String duplicateKey = normalizedPath.toLowerCase(Locale.ENGLISH);
if (!duplicateCheck.add(duplicateKey)) {
throw new BusinessException(CommonResponseEnum.FAIL, "压缩包中存在重复文件路径:" + normalizedPath);
}
result.put(normalizedPath, readEntryBytes(zipInputStream));
}
} catch (BusinessException exception) {
throw exception;
} catch (IOException exception) {
throw new BusinessException(CommonResponseEnum.FAIL, "读取压缩包失败");
}
if (result.isEmpty()) {
throw new BusinessException(CommonResponseEnum.FAIL, "压缩包中没有可用文件");
}
return result;
}
private Map<String, byte[]> stripSingleWrapperDir(Map<String, byte[]> rawEntries) {
if (rawEntries.isEmpty()) {
return Collections.emptyMap();
}
String wrapper = null;
for (String path : rawEntries.keySet()) {
int slashIndex = path.indexOf('/');
if (slashIndex <= 0) {
return rawEntries;
}
String currentWrapper = path.substring(0, slashIndex);
if (wrapper == null) {
wrapper = currentWrapper;
continue;
}
if (!wrapper.equals(currentWrapper)) {
return rawEntries;
}
}
Map<String, byte[]> result = new LinkedHashMap<String, byte[]>();
for (Map.Entry<String, byte[]> entry : rawEntries.entrySet()) {
String strippedPath = entry.getKey().substring(wrapper.length() + 1);
if (StrUtil.isBlank(strippedPath)) {
continue;
}
result.put(strippedPath, entry.getValue());
}
return result.isEmpty() ? rawEntries : result;
}
private String resolveSummaryPath(Set<String> relativePaths) {
String matchedPath = null;
for (String relativePath : relativePaths) {
if (!TestReportConst.isLedgerSummaryFileName(resolveFileName(relativePath))) {
continue;
}
if (relativePath.contains("/")) {
throw new BusinessException(CommonResponseEnum.FAIL, "台账信息汇总文件必须位于压缩包根目录");
}
if (matchedPath != null) {
throw new BusinessException(CommonResponseEnum.FAIL, "压缩包根目录仅允许存在一个台账信息汇总文件");
}
matchedPath = relativePath;
}
return matchedPath;
}
private TemplateDirSummary newTemplateDirSummary(String dirPath) {
TemplateDirSummary summary = new TemplateDirSummary();
summary.setDirPath(dirPath);
summary.setDirName(resolveFileName(dirPath));
return summary;
}
private void validateEntryExtension(String path) {
String extension = resolveExtension(path);
if (!SUPPORTED_EXTENSIONS.contains(extension)) {
throw new BusinessException(CommonResponseEnum.FAIL, "压缩包中存在不支持的文件类型:" + path);
}
}
private boolean isIgnoredEntry(String path) {
String lowerPath = path.toLowerCase(Locale.ENGLISH);
return lowerPath.startsWith("__macosx/") || lowerPath.endsWith("/.ds_store") || lowerPath.equals(".ds_store");
}
private byte[] readEntryBytes(ZipInputStream zipInputStream) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[8192];
int length;
while ((length = zipInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
return outputStream.toByteArray();
}
private String requireZipFileName(String fileName) {
String normalized = StrUtil.trimToNull(fileName);
if (normalized == null || !normalized.toLowerCase(Locale.ENGLISH).endsWith(".zip")) {
throw new BusinessException(CommonResponseEnum.FAIL, "仅支持上传zip压缩包");
}
return normalized;
}
private String resolveTopLevelDir(String relativePath) {
int slashIndex = relativePath.indexOf('/');
return slashIndex <= 0 ? null : relativePath.substring(0, slashIndex);
}
private String resolveFileName(String relativePath) {
int slashIndex = relativePath.lastIndexOf('/');
return slashIndex < 0 ? relativePath : relativePath.substring(slashIndex + 1);
}
private String resolveExtension(String filePath) {
int dotIndex = filePath.lastIndexOf('.');
return dotIndex < 0 || dotIndex == filePath.length() - 1 ? "" : filePath.substring(dotIndex + 1).toLowerCase(Locale.ENGLISH);
}
private String normalizeRelativePath(String relativePath) {
String normalized = StrUtil.trimToEmpty(relativePath).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, "压缩包内文件路径不能为空");
}
for (String segment : normalized.split("/")) {
if (StrUtil.isBlank(segment) || ".".equals(segment) || "..".equals(segment)) {
throw new BusinessException(CommonResponseEnum.FAIL, "压缩包内文件路径不合法:" + relativePath);
}
}
return normalized;
}
@Data
public static class ParsedArchive {
private Boolean success = Boolean.FALSE;
private String zipFileName;
private String summaryFileName;
private String summaryRelativePath;
private byte[] summaryFileBytes;
private Integer dirCount = 0;
private Integer childReportCount = 0;
private Integer pointCount = 0;
private Integer excelAttachmentCount = 0;
private Integer wordAttachmentCount = 0;
private List<String> orphanAttachmentPaths = new ArrayList<String>();
private List<String> failReasons = new ArrayList<String>();
private List<TestReportMultiTemplateDirVO> templateDirs = new ArrayList<TestReportMultiTemplateDirVO>();
private List<ParsedPointBinding> points = new ArrayList<ParsedPointBinding>();
private Map<String, byte[]> fileContentMap = new LinkedHashMap<String, byte[]>();
private TestReportLedgerExcelParser.ParsedLedger ledger;
}
@Data
public static class ParsedPointBinding {
private Integer rowNo;
private Integer groupNo;
private String substationName;
private String pointName;
private String templateDirName;
private String templateDirPath;
private String excelAttachmentName;
private String excelAttachmentRelativePath;
private String wordAttachmentName;
private String wordAttachmentRelativePath;
}
@Data
private static class TemplateDirSummary {
private String dirName;
private String dirPath;
private int pointCount;
private int excelAttachmentCount;
private int wordAttachmentCount;
private Set<Integer> groupNos = new LinkedHashSet<Integer>();
}
}

View File

@@ -0,0 +1,119 @@
package com.njcn.gather.aireport.testreportmulti.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.common.pojo.annotation.OperateInfo;
import com.njcn.common.pojo.constant.OperateType;
import com.njcn.common.pojo.enums.common.LogEnum;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.response.HttpResult;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO;
import com.njcn.gather.aireport.testreportmulti.pojo.param.TestReportMultiParam;
import com.njcn.gather.aireport.testreportmulti.pojo.vo.TestReportMultiVO;
import com.njcn.gather.aireport.testreportmulti.pojo.vo.TestReportMultiValidateResultVO;
import com.njcn.gather.aireport.testreportmulti.service.TestReportMultiService;
import com.njcn.web.controller.BaseController;
import com.njcn.web.utils.HttpResultUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@Api(tags = "多模板普测批任务")
@RestController
@RequestMapping("/api/test-report-multi")
@RequiredArgsConstructor
public class TestReportMultiController extends BaseController {
private final TestReportMultiService testReportMultiService;
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询多模板批任务列表")
@PostMapping("/list")
public HttpResult<Page<TestReportMultiVO>> list(@RequestBody(required = false) TestReportMultiParam.QueryParam param) {
String methodDescribe = getMethodDescribe("list");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
testReportMultiService.list(param), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
@ApiOperation("校验多模板批任务压缩包")
@PostMapping("/zip/validate")
public HttpResult<TestReportMultiValidateResultVO> validateZip(@RequestParam("file") MultipartFile file) {
String methodDescribe = getMethodDescribe("validateZip");
TestReportMultiValidateResultVO result = testReportMultiService.validateZip(file);
return HttpResultUtil.assembleCommonResponseResult(Boolean.TRUE.equals(result.getSuccess())
? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.ADD)
@ApiOperation("创建多模板批任务")
@PostMapping("/create")
public HttpResult<Boolean> create(@RequestBody @Validated TestReportMultiParam.CreateParam param) {
String methodDescribe = getMethodDescribe("create");
boolean result = testReportMultiService.create(param);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询多模板批任务详情")
@ApiImplicitParam(name = "id", value = "多模板批任务ID", required = true)
@GetMapping("/{id}")
public HttpResult<TestReportMultiVO> detail(@PathVariable("id") String id) {
String methodDescribe = getMethodDescribe("detail");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
testReportMultiService.detail(id), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("查询子报告列表")
@ApiImplicitParam(name = "id", value = "多模板批任务ID", required = true)
@GetMapping("/{id}/child-report/list")
public HttpResult<List<TestReportVO>> listChildReports(@PathVariable("id") String id) {
String methodDescribe = getMethodDescribe("listChildReports");
return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.SUCCESS,
testReportMultiService.listChildReports(id), methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON)
@ApiOperation("下载多模板批任务结果压缩包")
@ApiImplicitParam(name = "id", value = "多模板批任务ID", required = true)
@GetMapping("/{id}/download")
public ResponseEntity<byte[]> download(@PathVariable("id") String id) {
return testReportMultiService.download(id);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DELETE)
@ApiOperation("删除多模板批任务")
@PostMapping("/delete")
public HttpResult<Boolean> delete(@RequestBody List<String> ids) {
String methodDescribe = getMethodDescribe("delete");
boolean result = testReportMultiService.delete(ids);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.UPDATE)
@ApiOperation("执行多模板批任务生成")
@ApiImplicitParam(name = "id", value = "多模板批任务ID", required = true)
@PostMapping("/{id}/generate")
public HttpResult<Boolean> generate(@PathVariable("id") String id) {
String methodDescribe = getMethodDescribe("generate");
boolean result = testReportMultiService.generate(id);
return HttpResultUtil.assembleCommonResponseResult(result ? CommonResponseEnum.SUCCESS : CommonResponseEnum.FAIL,
result, methodDescribe);
}
}

View File

@@ -0,0 +1,57 @@
package com.njcn.gather.aireport.testreportmulti.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.gather.aireport.testreportmulti.pojo.param.TestReportMultiParam;
import com.njcn.gather.aireport.testreportmulti.pojo.po.TestReportMultiPO;
import com.njcn.gather.aireport.testreportmulti.pojo.vo.TestReportMultiVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface TestReportMultiMapper extends BaseMapper<TestReportMultiPO> {
@Select({
"<script>",
"select m.id, m.no, m.zip_file_name as zipFileName, m.summary_file_name as summaryFileName,",
"m.dir_count as dirCount, m.child_report_count as childReportCount, m.point_count as pointCount,",
"m.import_state as importState, m.generate_state as generateState, m.status,",
"m.create_by as createBy, creator.name as createByName, m.create_time as createTime,",
"m.update_by as updateBy, updater.name as updateByName, m.update_time as updateTime",
"from ai_testreport_multi m",
"left join sys_user creator on m.create_by = creator.id",
"left join sys_user updater on m.update_by = updater.id",
"where m.status = 1",
"<if test='param.keyword != null and param.keyword != \"\"'>",
"and (m.no like concat('%', #{param.keyword}, '%')",
"or m.zip_file_name like concat('%', #{param.keyword}, '%')",
"or m.summary_file_name like concat('%', #{param.keyword}, '%'))",
"</if>",
"<if test='param.searchBeginTime != null and param.searchBeginTime != \"\"'>",
"and m.create_time &gt;= #{param.searchBeginTime}",
"</if>",
"<if test='param.searchEndTime != null and param.searchEndTime != \"\"'>",
"and m.create_time &lt;= #{param.searchEndTime}",
"</if>",
"<if test='param.generateState != null'>",
"and m.generate_state = #{param.generateState}",
"</if>",
"order by m.create_time desc",
"</script>"
})
Page<TestReportMultiVO> selectPage(Page<TestReportMultiVO> page, @Param("param") TestReportMultiParam.QueryParam param);
@Select({
"select m.id, m.no, m.zip_file_name as zipFileName, m.summary_file_name as summaryFileName,",
"m.dir_count as dirCount, m.child_report_count as childReportCount, m.point_count as pointCount,",
"m.import_state as importState, m.generate_state as generateState, m.status,",
"m.create_by as createBy, creator.name as createByName, m.create_time as createTime,",
"m.update_by as updateBy, updater.name as updateByName, m.update_time as updateTime",
"from ai_testreport_multi m",
"left join sys_user creator on m.create_by = creator.id",
"left join sys_user updater on m.update_by = updater.id",
"where m.id = #{id} and m.status = 1"
})
TestReportMultiVO selectDetail(@Param("id") String id);
}

View File

@@ -0,0 +1,31 @@
package com.njcn.gather.aireport.testreportmulti.pojo.constant;
public final class TestReportMultiConst {
public static final Integer STATUS_DELETED = 0;
public static final Integer STATUS_NORMAL = 1;
public static final Integer IMPORT_STATE_PENDING = 0;
public static final Integer IMPORT_STATE_UPLOADED = 1;
public static final Integer IMPORT_STATE_VALIDATED = 2;
public static final Integer IMPORT_STATE_CREATED = 3;
public static final Integer IMPORT_STATE_FAILED = 4;
public static final Integer GENERATE_STATE_PENDING = 0;
public static final Integer GENERATE_STATE_PARTIAL_SUCCESS = 1;
public static final Integer GENERATE_STATE_ALL_SUCCESS = 2;
public static final Integer GENERATE_STATE_GENERATING = 3;
public static final Integer GENERATE_STATE_FAILED = 4;
public static final String STORAGE_MULTI_SESSION_DIR = "multi-session";
public static final String STORAGE_MULTI_TASK_DIR = "multi-task";
public static final String STORAGE_SESSION_ZIP_DIR = "zip";
public static final String STORAGE_SESSION_EXTRACT_DIR = "extract";
public static final String STORAGE_SESSION_META_FILE_NAME = "session-meta.json";
public static final String STORAGE_MULTI_ZIP_DIR = "zip";
public static final String STORAGE_MULTI_SUMMARY_DIR = "summary";
public static final String STORAGE_MULTI_CHILD_REPORT_DIR = "child-report";
private TestReportMultiConst() {
}
}

View File

@@ -0,0 +1,89 @@
package com.njcn.gather.aireport.testreportmulti.pojo.param;
import com.njcn.web.pojo.param.BaseParam;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
import java.util.List;
public class TestReportMultiParam {
@Data
@EqualsAndHashCode(callSuper = true)
public static class QueryParam extends BaseParam {
@ApiModelProperty("关键字,匹配批任务编号、压缩包文件名或台账汇总文件名")
private String keyword;
@ApiModelProperty("创建开始时间yyyy-MM-dd HH:mm:ss")
private String searchBeginTime;
@ApiModelProperty("创建结束时间yyyy-MM-dd HH:mm:ss")
private String searchEndTime;
@ApiModelProperty("整体生成状态")
private Integer generateState;
}
@Data
public static class CreateParam {
@ApiModelProperty("多模板批任务编号")
@NotBlank(message = "批任务编号不能为空")
@Size(max = 64, message = "批任务编号不能超过64个字符")
private String no;
@ApiModelProperty("校验通过的会话ID")
@NotBlank(message = "会话ID不能为空")
private String sessionId;
@ApiModelProperty("子报告配置列表")
@NotEmpty(message = "子报告配置不能为空")
private List<ChildReportItem> childReports;
}
@Data
public static class ChildReportItem {
@ApiModelProperty("压缩包一级模板目录名称")
@NotBlank(message = "模板目录名称不能为空")
@Size(max = 128, message = "模板目录名称不能超过128个字符")
private String templateDirName;
@ApiModelProperty("压缩包一级模板目录相对路径")
@NotBlank(message = "模板目录路径不能为空")
@Size(max = 255, message = "模板目录路径不能超过255个字符")
private String templateDirPath;
@ApiModelProperty("普测报告编号")
@NotBlank(message = "普测报告编号不能为空")
@Size(max = 32, message = "普测报告编号不能超过32个字符")
private String no;
@ApiModelProperty("委托单位ID")
@NotBlank(message = "委托单位不能为空")
private String clientUnitId;
@ApiModelProperty("报告模板ID")
@NotBlank(message = "报告模板不能为空")
private String reportModelId;
@ApiModelProperty("检测公司ID")
@NotBlank(message = "检测公司不能为空")
private String createUnit;
@ApiModelProperty("合同编号")
@NotBlank(message = "合同编号不能为空")
@Size(max = 32, message = "合同编号不能超过32个字符")
private String contractNumber;
@ApiModelProperty("检测依据ID列表")
@NotEmpty(message = "检测依据不能为空")
private List<String> standard;
@ApiModelProperty("联系电话")
@Size(max = 32, message = "联系电话不能超过32个字符")
private String phonenumber;
}
}

View File

@@ -0,0 +1,50 @@
package com.njcn.gather.aireport.testreportmulti.pojo.po;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@TableName("ai_testreport_multi")
public class TestReportMultiPO implements Serializable {
private static final long serialVersionUID = 1303351823037028556L;
@TableId("id")
private String id;
@TableField("no")
private String no;
@TableField("zip_file_name")
private String zipFileName;
@TableField("zip_storage_path")
private String zipStoragePath;
@TableField("summary_file_name")
private String summaryFileName;
@TableField("summary_storage_path")
private String summaryStoragePath;
@TableField("dir_count")
private Integer dirCount;
@TableField("child_report_count")
private Integer childReportCount;
@TableField("point_count")
private Integer pointCount;
@TableField("import_state")
private Integer importState;
@TableField("generate_state")
private Integer generateState;
@TableField("data")
private String data;
@TableField("status")
private Integer status;
@TableField("create_by")
private String createBy;
@TableField("create_time")
private LocalDateTime createTime;
@TableField("update_by")
private String updateBy;
@TableField("update_time")
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,21 @@
package com.njcn.gather.aireport.testreportmulti.pojo.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
public class TestReportMultiTemplateDirVO {
@ApiModelProperty("一级模板目录名称")
private String dirName;
@ApiModelProperty("一级模板目录相对路径")
private String dirPath;
@ApiModelProperty("监测点数")
private Integer pointCount;
@ApiModelProperty("分组数")
private Integer groupCount;
@ApiModelProperty("Excel附件数")
private Integer excelAttachmentCount;
@ApiModelProperty("Word附件数")
private Integer wordAttachmentCount;
}

View File

@@ -0,0 +1,92 @@
package com.njcn.gather.aireport.testreportmulti.pojo.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
@Data
public class TestReportMultiVO {
private static final String DEFAULT_EMPTY_DISPLAY = "-";
@ApiModelProperty("多模板批任务ID")
private String id;
@ApiModelProperty("批任务编号")
private String no;
@ApiModelProperty("压缩包文件名")
private String zipFileName;
@ApiModelProperty("台账汇总文件名")
private String summaryFileName;
@ApiModelProperty("模板目录数量")
private Integer dirCount;
@ApiModelProperty("子报告数量")
private Integer childReportCount;
@ApiModelProperty("监测点总数")
private Integer pointCount;
@ApiModelProperty("导入状态")
private Integer importState;
@ApiModelProperty("整体生成状态")
private Integer generateState;
@ApiModelProperty("逻辑状态")
private Integer status;
@ApiModelProperty("创建人")
private String createBy;
@ApiModelProperty("创建人名称")
private String createByName;
@ApiModelProperty("创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime createTime;
@ApiModelProperty("更新人")
private String updateBy;
@ApiModelProperty("更新人名称")
private String updateByName;
@ApiModelProperty("更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
@JsonSerialize(using = LocalDateTimeSerializer.class)
private LocalDateTime updateTime;
public String getId() {
return defaultDisplay(id);
}
public String getNo() {
return defaultDisplay(no);
}
public String getZipFileName() {
return defaultDisplay(zipFileName);
}
public String getSummaryFileName() {
return defaultDisplay(summaryFileName);
}
public String getCreateBy() {
return defaultDisplay(createBy);
}
public String getCreateByName() {
return defaultDisplay(createByName);
}
public String getUpdateBy() {
return defaultDisplay(updateBy);
}
public String getUpdateByName() {
return defaultDisplay(updateByName);
}
private String defaultDisplay(String value) {
return StringUtils.hasText(value) ? value : DEFAULT_EMPTY_DISPLAY;
}
}

View File

@@ -0,0 +1,36 @@
package com.njcn.gather.aireport.testreportmulti.pojo.vo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class TestReportMultiValidateResultVO {
@ApiModelProperty("校验是否通过")
private Boolean success = Boolean.FALSE;
@ApiModelProperty("校验通过的会话ID")
private String sessionId;
@ApiModelProperty("压缩包文件名")
private String zipFileName;
@ApiModelProperty("台账汇总文件名")
private String summaryFileName;
@ApiModelProperty("模板目录数量")
private Integer dirCount = 0;
@ApiModelProperty("子报告数量")
private Integer childReportCount = 0;
@ApiModelProperty("监测点总数")
private Integer pointCount = 0;
@ApiModelProperty("Excel附件总数")
private Integer excelAttachmentCount = 0;
@ApiModelProperty("Word附件总数")
private Integer wordAttachmentCount = 0;
@ApiModelProperty("孤儿附件相对路径")
private List<String> orphanAttachmentPaths = new ArrayList<String>();
@ApiModelProperty("失败原因")
private List<String> failReasons = new ArrayList<String>();
@ApiModelProperty("模板目录汇总")
private List<TestReportMultiTemplateDirVO> templateDirs = new ArrayList<TestReportMultiTemplateDirVO>();
}

View File

@@ -0,0 +1,32 @@
package com.njcn.gather.aireport.testreportmulti.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.gather.aireport.testreport.pojo.vo.TestReportVO;
import com.njcn.gather.aireport.testreportmulti.pojo.param.TestReportMultiParam;
import com.njcn.gather.aireport.testreportmulti.pojo.po.TestReportMultiPO;
import com.njcn.gather.aireport.testreportmulti.pojo.vo.TestReportMultiVO;
import com.njcn.gather.aireport.testreportmulti.pojo.vo.TestReportMultiValidateResultVO;
import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
public interface TestReportMultiService extends IService<TestReportMultiPO> {
Page<TestReportMultiVO> list(TestReportMultiParam.QueryParam param);
TestReportMultiValidateResultVO validateZip(MultipartFile file);
boolean create(TestReportMultiParam.CreateParam param);
TestReportMultiVO detail(String id);
List<TestReportVO> listChildReports(String id);
ResponseEntity<byte[]> download(String id);
boolean delete(List<String> ids);
boolean generate(String id);
}

View File

@@ -0,0 +1,37 @@
SET NAMES utf8mb4;
CREATE TABLE `ai_testreport_multi` (
`id` varchar(36) NOT NULL COMMENT '多模板普测任务IDGUID',
`no` varchar(64) NOT NULL COMMENT '批任务编号',
`zip_file_name` varchar(255) NOT NULL COMMENT '上传压缩包文件名',
`zip_storage_path` varchar(500) NOT NULL COMMENT '上传压缩包存储路径',
`summary_file_name` varchar(255) NOT NULL COMMENT '台账汇总文件名',
`summary_storage_path` varchar(500) NOT NULL COMMENT '台账汇总存储路径',
`dir_count` int NOT NULL DEFAULT 0 COMMENT '模板目录数',
`child_report_count` int NOT NULL DEFAULT 0 COMMENT '子报告数',
`point_count` int NOT NULL DEFAULT 0 COMMENT '监测点总数',
`import_state` tinyint(1) NOT NULL DEFAULT 0 COMMENT '导入状态0待上传 1已上传待解析 2解析通过待创建 3已创建 4创建失败',
`generate_state` tinyint(1) NOT NULL DEFAULT 0 COMMENT '整体生成状态0未生成 1部分生成成功 2全部生成成功 3生成中 4生成失败',
`data` json DEFAULT NULL COMMENT '批任务快照数据',
`status` tinyint(1) NOT NULL COMMENT '状态(0删除 1正常)',
`create_by` varchar(32) DEFAULT NULL COMMENT '创建人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_by` varchar(32) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
INDEX `idx_ai_testreport_multi_no` (`no`),
INDEX `idx_ai_testreport_multi_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='多模板普测任务表';
ALTER TABLE `ai_testreport`
ADD COLUMN `multi_id` varchar(36) DEFAULT NULL COMMENT '所属多模板普测任务IDGUID' AFTER `id`,
ADD COLUMN `template_dir_name` varchar(128) DEFAULT NULL COMMENT '压缩包一级模板目录名称' AFTER `report_model_id`,
ADD COLUMN `template_dir_path` varchar(255) DEFAULT NULL COMMENT '压缩包一级模板目录相对路径' AFTER `template_dir_name`,
ADD COLUMN `sort_no` int NOT NULL DEFAULT 0 COMMENT '父任务内排序号' AFTER `template_dir_path`,
ADD INDEX `idx_ai_testreport_multi_id` (`multi_id`);
ALTER TABLE `ai_testreport_point`
ADD COLUMN `excel_attachment_relative_path` varchar(500) DEFAULT NULL COMMENT 'Excel附件相对路径' AFTER `excel_attachment_name`,
ADD COLUMN `word_attachment_name` varchar(255) DEFAULT NULL COMMENT 'Word附件文件名' AFTER `excel_storage_path`,
ADD COLUMN `word_attachment_relative_path` varchar(500) DEFAULT NULL COMMENT 'Word附件相对路径' AFTER `word_attachment_name`,
ADD COLUMN `word_storage_path` varchar(500) DEFAULT NULL COMMENT 'Word附件存储路径' AFTER `word_attachment_relative_path`;

File diff suppressed because it is too large Load Diff