生成报告脚本

This commit is contained in:
2025-12-08 16:29:47 +08:00
parent 6c4b082d3e
commit 072f043c00
3 changed files with 77 additions and 88 deletions

View File

@@ -10,5 +10,5 @@ public class ExportWordScriptExecuteConfig extends BaseExecuteConfig {
private String fileRegularStr="^aa\\.xml$"; private String fileRegularStr="^aa\\.xml$";
// 导出脚本文件id // 导出脚本文件id
private String exportScriptFileId; private Long exportScriptFileId;
} }

View File

@@ -2537,7 +2537,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
throw new RuntimeException("文件夹不存在ID: " + downloadDirId); throw new RuntimeException("文件夹不存在ID: " + downloadDirId);
} }
if(!Objects.equals(DataTypeEnum.DIRECTORY.getValue(),folderInfo.getFileType())){ if (!Objects.equals(DataTypeEnum.DIRECTORY.getValue(), folderInfo.getFileType())) {
throw new RuntimeException("指定 ID 不是文件夹类型: " + downloadDirId); throw new RuntimeException("指定 ID 不是文件夹类型: " + downloadDirId);
} }
@@ -2551,19 +2551,12 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
folderObjectKey += "/"; folderObjectKey += "/";
} }
// 2. 构建本地基础路径(直接使用 basePath + folderObjectKey // 2. 构建本地基础路径basePath 就是最终根目录
Path localBaseDir = Paths.get(basePath).toAbsolutePath().normalize(); Path localBaseDir = Paths.get(basePath).toAbsolutePath().normalize();
Path fullLocalBase = localBaseDir.resolve(folderObjectKey).normalize();
// 安全校验:确保 fullLocalBase 确实在 basePath 下
if (!fullLocalBase.startsWith(localBaseDir)) {
throw new RuntimeException("非法文件夹路径,可能包含路径穿越: " + folderObjectKey);
}
try { try {
Files.createDirectories(fullLocalBase); Files.createDirectories(localBaseDir); // 确保 basePath 存在
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("无法创建本地目录: " + fullLocalBase, e); throw new RuntimeException("无法创建本地基础目录: " + localBaseDir, e);
} }
// 3. 列出 MinIO 中该前缀下的所有对象(递归) // 3. 列出 MinIO 中该前缀下的所有对象(递归)
@@ -2579,7 +2572,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
} }
} }
// 4. 遍历并下载每个对象(任一失败立即抛出 RuntimeException // 4. 遍历并下载每个对象
for (Result<Item> result : results) { for (Result<Item> result : results) {
Item item = result.get(); Item item = result.get();
String objectKey = item.objectName(); String objectKey = item.objectName();
@@ -2589,19 +2582,29 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
continue; continue;
} }
// 如果提供了正则表达式,则进行过滤 // 理论上 listObjects(folderObjectKey) 返回的都以 folderObjectKey 开头,但加个校验更安全
if (!objectKey.startsWith(folderObjectKey)) {
continue;
}
// 剥离文件夹前缀,得到相对路径
String relativePath = objectKey.substring(folderObjectKey.length());
if (relativePath.isEmpty()) {
continue; // 忽略空路径(不应发生)
}
// 如果提供了正则表达式,则只匹配文件名(不是完整路径)
if (pattern != null) { if (pattern != null) {
String fileName = Paths.get(objectKey).getFileName().toString(); String fileName = Paths.get(relativePath).getFileName().toString();
if (!pattern.matcher(fileName).matches()) { if (!pattern.matcher(fileName).matches()) {
// 不匹配正则表达式的文件跳过下载 continue; // 跳过不匹配的文件
continue;
} }
} }
// 构建本地文件路径basePath + objectKey // 构建本地文件路径basePath + 相对路径
Path localFilePath = localBaseDir.resolve(objectKey).normalize(); Path localFilePath = localBaseDir.resolve(relativePath).normalize();
// 二次安全校验:防止 objectKey 含 ../ 导致越界 // 安全校验:防止路径穿越(例如 relativePath 含 ../../
if (!localFilePath.startsWith(localBaseDir)) { if (!localFilePath.startsWith(localBaseDir)) {
throw new RuntimeException("检测到非法对象路径,拒绝下载: " + objectKey); throw new RuntimeException("检测到非法对象路径,拒绝下载: " + objectKey);
} }

View File

@@ -14,6 +14,7 @@ import com.sdm.flowable.entity.ProcessNodeParam;
import com.sdm.flowable.service.IProcessNodeParamService; import com.sdm.flowable.service.IProcessNodeParamService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.delegate.DelegateExecution;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -58,6 +59,8 @@ public class ExportWordScriptHandler implements ExecutionHandler<Map<String, Obj
String runId = (String) execution.getVariable("runId"); String runId = (String) execution.getVariable("runId");
String processDefinitionId = execution.getProcessDefinitionId(); String processDefinitionId = execution.getProcessDefinitionId();
log.info("ExportWordScriptHandler 开始执行 runId:{}, beforeNodeId:{}, currentNodeId:{},fileRegularStr:{}", runId, beforeNodeId, currentNodeId,fileRegularStr);
// 获取前置节点的输出文件夹信息 // 获取前置节点的输出文件夹信息
ProcessNodeParam beforeProcessNodeParam = processNodeParamService.lambdaQuery() ProcessNodeParam beforeProcessNodeParam = processNodeParamService.lambdaQuery()
.eq(ProcessNodeParam::getRunId, runId) .eq(ProcessNodeParam::getRunId, runId)
@@ -82,6 +85,7 @@ public class ExportWordScriptHandler implements ExecutionHandler<Map<String, Obj
Long beforeNodeOutputDirId = beforeParamJsonObject.getLong("outputDirId"); Long beforeNodeOutputDirId = beforeParamJsonObject.getLong("outputDirId");
FileMetadataInfoResp beforeNodeFileMetadataInfoResp = getFileBaseInfo(beforeNodeOutputDirId); FileMetadataInfoResp beforeNodeFileMetadataInfoResp = getFileBaseInfo(beforeNodeOutputDirId);
String beforeNodeObjectKey = beforeNodeFileMetadataInfoResp.getObjectKey(); String beforeNodeObjectKey = beforeNodeFileMetadataInfoResp.getObjectKey();
log.info("前置节点配置参数:{}", beforeNodeParamJson);
// 获取当前节点输出文件夹信息 // 获取当前节点输出文件夹信息
String currentNodeParamJson = currentProcessNodeParam.getParamJson(); String currentNodeParamJson = currentProcessNodeParam.getParamJson();
@@ -89,67 +93,61 @@ public class ExportWordScriptHandler implements ExecutionHandler<Map<String, Obj
Long currentNodeOutputDirId = currentParamJsonObject.getLong("outputDirId"); Long currentNodeOutputDirId = currentParamJsonObject.getLong("outputDirId");
FileMetadataInfoResp currentNodeFileMetadataInfoResp = getFileBaseInfo(currentNodeOutputDirId); FileMetadataInfoResp currentNodeFileMetadataInfoResp = getFileBaseInfo(currentNodeOutputDirId);
String currentNodeObjectKey = currentNodeFileMetadataInfoResp.getObjectKey(); String currentNodeObjectKey = currentNodeFileMetadataInfoResp.getObjectKey();
log.info("当前节点配置参数:{}", currentNodeParamJson);
// 前置节点输出文件夹的所有文件通过正则过滤后下载到当前脚本节点的输出文件夹,并使用正则表达式过滤文件 // 前置节点输出文件夹的所有文件通过正则过滤后下载到当前脚本节点的输出文件夹,并使用正则表达式过滤文件
String basePath = FlowableConfig.FLOWABLE_SIMULATION_BASEDIR + currentNodeObjectKey; String basePath = FlowableConfig.FLOWABLE_SIMULATION_BASEDIR + currentNodeObjectKey;
dataFeignClient.downloadFolderToLocal(beforeNodeOutputDirId, basePath, fileRegularStr); dataFeignClient.downloadFolderToLocal(beforeNodeOutputDirId, basePath, fileRegularStr);
// 下载处理脚本文件到本地 // 下载处理脚本文件到本地
String exportScriptFileId = config.getExportScriptFileId(); Long exportScriptFileId = config.getExportScriptFileId();
if (exportScriptFileId != null && !exportScriptFileId.isEmpty()) { if (ObjectUtils.isEmpty(exportScriptFileId)) {
dataFeignClient.downloadFileToLocal(Long.valueOf(exportScriptFileId), log.error("未设置导出报告脚本文件");
FlowableConfig.FLOWABLE_SIMULATION_BASEDIR + beforeNodeObjectKey + "/exportScript.py"); throw new RuntimeException("未设置导出报告脚本文件");
}
dataFeignClient.downloadFileToLocal(exportScriptFileId, basePath);
FileMetadataInfoResp scriptFileBaseInfo = getFileBaseInfo(exportScriptFileId);
String scriptPath = basePath + scriptFileBaseInfo.getOriginalName();
log.info("脚本文件路径:{}", scriptPath);
// 调用脚本
log.info("调用脚本中。。。。。。");
String commands = "python" + " " + scriptPath + " " + basePath;
log.info("command:" + commands);
List<String> result = new ArrayList<>();
int runningStatus = -1;
try {
log.info("开始同步执行脚本");
Process process = Runtime.getRuntime().exec(commands);
log.info("准备获取脚本输出");
log.info("开始获取脚本输出");
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
log.info("executePython" + line);
result.add(line);
}
log.info("脚本执行完成");
runningStatus = process.waitFor();
log.info("脚本运行状态:" + runningStatus);
} catch (IOException | InterruptedException e) {
log.error("执行脚本失败:" + e);
return;
}
if (runningStatus != 0) {
log.error("执行脚本失败");
return;
} else {
log.info(commands + "执行脚本完成!");
}
try {
// 获取临时路径中脚本生成的报告
uploadResultFileToMinio(basePath + "report.docx",currentNodeOutputDirId);
} catch (Exception ex) {
log.error("生成自动化报告失败:{}", ex.getMessage(), ex);
throw new RuntimeException("生成自动化报告失败");
} }
List<Long> imageFileIdList = new ArrayList<>();
if (CollectionUtils.isNotEmpty(imageFileIdList)) {
String randomId = RandomUtil.generateString(16);
log.info("临时路径为:{}", randomId);
for (Long fileId : imageFileIdList) {
dataFeignClient.downloadFileToLocal(fileId, TEMP_REPORT_PATH + randomId);
}
// 调用脚本
log.info("调用脚本中。。。。。。");
String commands = "python /opt/script/exportWord.py " + TEMP_REPORT_PATH + randomId + File.separator;
log.info("command:" + commands);
List<String> result = new ArrayList<>();
int runningStatus = -1;
try {
log.info("开始同步执行脚本");
Process process = Runtime.getRuntime().exec(commands);
log.info("准备获取脚本输出");
log.info("开始获取脚本输出");
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
log.info("executePython" + line);
result.add(line);
}
log.info("脚本执行完成");
runningStatus = process.waitFor();
log.info("脚本运行状态:" + runningStatus);
} catch (IOException | InterruptedException e) {
log.error("执行脚本失败:" + e);
return;
}
if (runningStatus != 0) {
log.error("执行脚本失败");
return;
} else {
log.info(commands + "执行脚本完成!");
}
try {
// 获取临时路径中脚本生成的报告
uploadResultFileToMinio(TEMP_REPORT_PATH + randomId + File.separator + "report.docx");
} catch (Exception ex) {
log.error("生成自动化报告失败:{}", ex.getMessage(), ex);
throw new RuntimeException("生成自动化报告失败");
}
// 删除临时路径
log.info("删除临时路径:{},中。。。。。。", randomId);
deleteFolder(new File(TEMP_REPORT_PATH + randomId));
}
} catch (Exception e) { } catch (Exception e) {
log.error("执行ExportWordScript失败", e); log.error("执行ExportWordScript失败", e);
throw new RuntimeException("执行ExportWordScript失败: " + e.getMessage(), e); throw new RuntimeException("执行ExportWordScript失败: " + e.getMessage(), e);
@@ -167,7 +165,7 @@ public class ExportWordScriptHandler implements ExecutionHandler<Map<String, Obj
return fileBaseInfoResp.getData(); return fileBaseInfoResp.getData();
} }
private void uploadResultFileToMinio(String resultFilePath) { private void uploadResultFileToMinio(String resultFilePath,Long currentNodeOutputDirId) {
try { try {
File resultFile = new File(resultFilePath); File resultFile = new File(resultFilePath);
if (!resultFile.exists()) { if (!resultFile.exists()) {
@@ -185,7 +183,7 @@ public class ExportWordScriptHandler implements ExecutionHandler<Map<String, Obj
// 上传到MinIO // 上传到MinIO
UploadFilesReq req = new UploadFilesReq(); UploadFilesReq req = new UploadFilesReq();
req.setDirId(1L); req.setDirId(currentNodeOutputDirId);
req.setFile(multipartFile); req.setFile(multipartFile);
// 调用上传文件的方法 // 调用上传文件的方法
// 注意:这里应该处理返回值 // 注意:这里应该处理返回值
@@ -196,16 +194,4 @@ public class ExportWordScriptHandler implements ExecutionHandler<Map<String, Obj
throw new RuntimeException("上传结果文件到MinIO失败: " + e.getMessage(), e); throw new RuntimeException("上传结果文件到MinIO失败: " + e.getMessage(), e);
} }
} }
public static void deleteFolder(File folder) {
if (folder.isDirectory()) {
File[] files = folder.listFiles();
if (files != null) {
for (File file : files) {
deleteFolder(file);
}
}
}
folder.delete();
}
} }