Merge remote-tracking branch 'origin/main'

This commit is contained in:
2025-12-08 16:09:43 +08:00
10 changed files with 220 additions and 125 deletions

View File

@@ -4,4 +4,11 @@ import lombok.Data;
@Data
public class ExportWordScriptExecuteConfig extends BaseExecuteConfig {
// 输入节点id
private String beforeNodeId;
// 文件正则表达式,用于匹配输入文件夹下的文件名
private String fileRegularStr="^aa\\.xml$";
// 导出脚本文件id
private String exportScriptFileId;
}

View File

@@ -158,6 +158,17 @@ public class DataClientFeignClientImpl implements IDataFeignClient {
}
}
@Override
public SdmResponse downloadFolderToLocal(Long downloadDirId, String basePath, String fileRegularStr) {
try {
dataClient.downloadFolderToLocal(downloadDirId,basePath,fileRegularStr);
return SdmResponse.success();
} catch (Exception e) {
log.error("下载文件响应", e);
return SdmResponse.failed("下载文件响应");
}
}
@Override
public SdmResponse<List<BatchAddFileInfoResp>> batchAddFileInfo(UploadFilesReq req) {
SdmResponse<List<BatchAddFileInfoResp>> response;

View File

@@ -61,6 +61,11 @@ public interface IDataFeignClient {
@PostMapping("/data/downloadFileToLocal")
void downloadFileToLocal(@RequestParam(value = "fileId") @Validated Long fileId, @RequestParam(value = "path") @Validated String path);
@PostMapping("/data/downloadFolderToLocal")
SdmResponse downloadFolderToLocal(@RequestParam(value = "downloadDirId") @Validated Long downloadDirId,
@RequestParam(value = "basePath") @Validated String basePath,
@RequestParam(value = "fileRegularStr", required = false) String fileRegularStr) throws Exception;
@PostMapping("/data/batchAddFileInfo")
SdmResponse<List<BatchAddFileInfoResp>> batchAddFileInfo(@RequestBody UploadFilesReq req);

View File

@@ -440,6 +440,13 @@ public class DataFileController implements IDataFeignClient {
IDataFileService.downloadFileToLocal(fileId,path);
}
@PostMapping("/downloadFolderToLocal")
public SdmResponse downloadFolderToLocal(@RequestParam(value = "downloadDirId") @Validated Long downloadDirId,
@RequestParam(value = "basePath") @Validated String basePath,
@RequestParam(value = "fileRegularStr", required = false) String fileRegularStr) throws Exception {
return IDataFileService.downloadFolderToLocal(downloadDirId, basePath, fileRegularStr);
}
/**
* 导出知识库
* @param knowledgeExportExcelFormat

View File

@@ -337,8 +337,9 @@ public interface IDataFileService {
* 批量下载指定文件夹到本地目录
* @param downloadDirId
* @param basePath
* @param fileRegularStr 用于过滤文件的正则表达式
*/
default void downloadFolderToLocal(Long downloadDirId, String basePath) throws Exception {}
default SdmResponse downloadFolderToLocal(Long downloadDirId, String basePath, String fileRegularStr) throws Exception {return null;}
/**
* 导出知识库

View File

@@ -76,6 +76,7 @@ import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -2523,7 +2524,7 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
}
}
public void downloadFolderToLocal(Long downloadDirId, String basePath) throws Exception {
public SdmResponse downloadFolderToLocal(Long downloadDirId, String basePath, String fileRegularStr) throws Exception {
if (downloadDirId == null || basePath == null) {
throw new IllegalArgumentException("downloadDirId 和 basePath 不能为空");
}
@@ -2565,11 +2566,19 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
throw new RuntimeException("无法创建本地目录: " + fullLocalBase, e);
}
// 3. 列出 MinIO 中该前缀下的所有对象(递归)
Iterable<Result<Item>> results = minioService.listObjects(folderObjectKey);
// 编译正则表达式(如果提供)
Pattern pattern = null;
if (fileRegularStr != null && !fileRegularStr.isEmpty()) {
try {
pattern = Pattern.compile(fileRegularStr);
} catch (Exception e) {
throw new RuntimeException("无效的正则表达式: " + fileRegularStr, e);
}
}
// 4. 遍历并下载每个对象(任一失败立即抛出 RuntimeException
for (Result<Item> result : results) {
Item item = result.get();
@@ -2580,6 +2589,15 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
continue;
}
// 如果提供了正则表达式,则进行过滤
if (pattern != null) {
String fileName = Paths.get(objectKey).getFileName().toString();
if (!pattern.matcher(fileName).matches()) {
// 不匹配正则表达式的文件跳过下载
continue;
}
}
// 构建本地文件路径basePath + objectKey
Path localFilePath = localBaseDir.resolve(objectKey).normalize();
@@ -2606,6 +2624,8 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
throw new RuntimeException("下载对象失败: " + objectKey, e);
}
}
return SdmResponse.success();
}
@Override

View File

@@ -14,7 +14,6 @@ import com.sdm.flowable.service.IAsyncTaskRecordService;
import com.sdm.flowable.service.IProcessNodeParamService;
import com.sdm.flowable.util.FlowNodeIdUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.delegate.DelegateExecution;
@@ -22,9 +21,6 @@ import org.flowable.engine.delegate.JavaDelegate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.Map;
@@ -80,7 +76,7 @@ public class UniversalDelegate implements JavaDelegate {
throw new RuntimeException("当前节点未查询到输入文件夹");
}
String objectKey = fileBaseInfoResp.getData().getObjectKey();
prepareLocalDir(objectKey);
FlowNodeIdUtils.prepareLocalDir(objectKey);
// 检查是否有扩展元素配置
if (execution.getCurrentFlowElement().getExtensionElements() != null &&
@@ -119,37 +115,6 @@ public class UniversalDelegate implements JavaDelegate {
}
}
/**
* 准备本地目录:如果目录已存在,则清空其内容;否则创建新目录。
* 流程实例启动后,需要在本地准备一个目录,用于存储节点计算结果。
* 如果同一个流程二次启动,每个节点会使用同一个文件夹,二次启动的时候,
* 如果清空,上一次流程实例运行结果相关文件也会在这个文件夹中,影响这次运行流程的结果文件
*
* @param objectKey MinIO 的对象路径,将作为本地目录路径的一部分
*/
private void prepareLocalDir(String objectKey) {
String simulationBaseDir = FlowableConfig.FLOWABLE_SIMULATION_BASEDIR;
Path localBaseDir = Paths.get(simulationBaseDir).toAbsolutePath().normalize();
Path fullLocalPath = localBaseDir.resolve(objectKey).normalize();
// 安全校验:防止路径穿越
if (!fullLocalPath.startsWith(localBaseDir)) {
throw new RuntimeException("非法文件夹路径,可能包含路径穿越: " + objectKey);
}
try {
if (Files.exists(fullLocalPath)) {
//直接删除整个目录
log.info("本地目录已存在,将删除并重新创建: {}", fullLocalPath);
FileUtils.deleteDirectory(fullLocalPath.toFile());
}
log.info("创建本地目录: {}", fullLocalPath);
Files.createDirectories(fullLocalPath);
} catch (Exception e) {
throw new RuntimeException("无法准备本地目录: " + fullLocalPath, e);
}
}
/**
* 处理任务执行失败的情况

View File

@@ -1,10 +1,17 @@
package com.sdm.flowable.delegate.handler;
import com.alibaba.fastjson2.JSONObject;
import com.sdm.common.common.SdmResponse;
import com.sdm.common.entity.flowable.executeConfig.ExportWordScriptExecuteConfig;
import com.sdm.common.entity.req.data.GetFileBaseInfoReq;
import com.sdm.common.entity.req.data.UploadFilesReq;
import com.sdm.common.entity.resp.data.FileMetadataInfoResp;
import com.sdm.common.feign.inter.data.IDataFeignClient;
import com.sdm.common.utils.RandomUtil;
import com.sdm.common.entity.flowable.executeConfig.BaseExecuteConfig;
import com.sdm.flowable.constants.FlowableConfig;
import com.sdm.flowable.entity.ProcessNodeParam;
import com.sdm.flowable.service.IProcessNodeParamService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.flowable.engine.delegate.DelegateExecution;
@@ -29,6 +36,9 @@ public class ExportWordScriptHandler implements ExecutionHandler<Map<String, Obj
@Autowired
private IDataFeignClient dataFeignClient;
@Autowired
private IProcessNodeParamService processNodeParamService;
private static final String TEMP_REPORT_PATH = "/opt/report/";
// todo 用户需要上传脚本文件到当前算列的导出报告节点输入文件夹下,params需要记下脚本文件id此处暂时写死
@@ -38,57 +48,125 @@ public class ExportWordScriptHandler implements ExecutionHandler<Map<String, Obj
@Override
public void execute(DelegateExecution execution, Map<String, Object> params, ExportWordScriptExecuteConfig config) {
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");
try {
// 获取前置节点参数
String beforeNodeId = config.getBeforeNodeId();
String currentNodeId =execution.getCurrentActivityId();
String fileRegularStr = config.getFileRegularStr();
} catch (Exception ex) {
log.error("生成自动化报告失败:{}", ex.getMessage(), ex);
throw new RuntimeException("生成自动化报告失败");
// 获取当前流程实例参数
String runId = (String) execution.getVariable("runId");
String processDefinitionId = execution.getProcessDefinitionId();
// 获取前置节点的输出文件夹信息
ProcessNodeParam beforeProcessNodeParam = processNodeParamService.lambdaQuery()
.eq(ProcessNodeParam::getRunId, runId)
.eq(ProcessNodeParam::getNodeId, beforeNodeId)
.eq(ProcessNodeParam::getProcessDefinitionId, processDefinitionId)
.one();
ProcessNodeParam currentProcessNodeParam = processNodeParamService.lambdaQuery()
.eq(ProcessNodeParam::getRunId, runId)
.eq(ProcessNodeParam::getNodeId, currentNodeId)
.eq(ProcessNodeParam::getProcessDefinitionId, processDefinitionId)
.one();
if (beforeProcessNodeParam == null || currentProcessNodeParam == null) {
log.error(" 获取节点参数失败runId:{}, beforeNodeId:{}, currentNodeId:{}", runId, beforeNodeId, currentNodeId);
throw new RuntimeException("获取节点参数失败");
}
// 删除临时路径
log.info("删除临时路径:{},中。。。。。。", randomId);
deleteFolder(new File(TEMP_REPORT_PATH + randomId));
// 获取前置节点输出文件夹信息
String beforeNodeParamJson = beforeProcessNodeParam.getParamJson();
JSONObject beforeParamJsonObject = JSONObject.parseObject(beforeNodeParamJson);
Long beforeNodeOutputDirId = beforeParamJsonObject.getLong("outputDirId");
FileMetadataInfoResp beforeNodeFileMetadataInfoResp = getFileBaseInfo(beforeNodeOutputDirId);
String beforeNodeObjectKey = beforeNodeFileMetadataInfoResp.getObjectKey();
// 获取当前节点输出文件夹信息
String currentNodeParamJson = currentProcessNodeParam.getParamJson();
JSONObject currentParamJsonObject = JSONObject.parseObject(currentNodeParamJson);
Long currentNodeOutputDirId = currentParamJsonObject.getLong("outputDirId");
FileMetadataInfoResp currentNodeFileMetadataInfoResp = getFileBaseInfo(currentNodeOutputDirId);
String currentNodeObjectKey = currentNodeFileMetadataInfoResp.getObjectKey();
// 前置节点输出文件夹的所有文件通过正则过滤后下载到当前脚本节点的输出文件夹,并使用正则表达式过滤文件
String basePath = FlowableConfig.FLOWABLE_SIMULATION_BASEDIR + currentNodeObjectKey;
dataFeignClient.downloadFolderToLocal(beforeNodeOutputDirId, basePath, fileRegularStr);
// 下载处理脚本文件到本地
String exportScriptFileId = config.getExportScriptFileId();
if (exportScriptFileId != null && !exportScriptFileId.isEmpty()) {
dataFeignClient.downloadFileToLocal(Long.valueOf(exportScriptFileId),
FlowableConfig.FLOWABLE_SIMULATION_BASEDIR + beforeNodeObjectKey + "/exportScript.py");
}
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) {
log.error("执行ExportWordScript失败", e);
throw new RuntimeException("执行ExportWordScript失败: " + e.getMessage(), e);
}
}
private FileMetadataInfoResp getFileBaseInfo(Long outputDirId) {
GetFileBaseInfoReq getFileBaseInfoReq = new GetFileBaseInfoReq();
getFileBaseInfoReq.setFileId(outputDirId);
SdmResponse<FileMetadataInfoResp> fileBaseInfoResp = dataFeignClient.getFileBaseInfo(getFileBaseInfoReq);
if (!fileBaseInfoResp.isSuccess() || fileBaseInfoResp.getData() == null) {
log.warn("getFileBaseInfo failed, outputDirId:{}", outputDirId);
throw new RuntimeException("上一节点信息查询失败");
}
return fileBaseInfoResp.getData();
}
private void uploadResultFileToMinio(String resultFilePath) {
try {
File resultFile = new File(resultFilePath);

View File

@@ -5,21 +5,15 @@ import com.sdm.common.common.SdmResponse;
import com.sdm.common.entity.req.data.GetFileBaseInfoReq;
import com.sdm.common.entity.resp.data.FileMetadataInfoResp;
import com.sdm.common.feign.inter.data.IDataFeignClient;
import com.sdm.flowable.constants.FlowableConfig;
import com.sdm.flowable.service.IProcessNodeParamService;
import com.sdm.flowable.util.FlowNodeIdUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.ExecutionListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
/**
* UserTask 启动时准备本地输出目录的监听器
*/
@@ -54,37 +48,6 @@ public class UserTaskDirectoryPreparationListener implements ExecutionListener {
throw new RuntimeException("当前节点未查询到输入文件夹");
}
String objectKey = fileBaseInfoResp.getData().getObjectKey();
prepareLocalDir(objectKey);
}
/**
* 准备本地目录:如果目录已存在,则清空其内容;否则创建新目录。
* 流程实例启动后,需要在本地准备一个目录,用于存储节点计算结果。
* 如果同一个流程二次启动,每个节点会使用同一个文件夹,二次启动的时候,
* 如果清空,上一次流程实例运行结果相关文件也会在这个文件夹中,影响这次运行流程的结果文件
*
* @param objectKey MinIO 的对象路径,将作为本地目录路径的一部分
*/
private void prepareLocalDir(String objectKey) {
String simulationBaseDir = FlowableConfig.FLOWABLE_SIMULATION_BASEDIR;
Path localBaseDir = Paths.get(simulationBaseDir).toAbsolutePath().normalize();
Path fullLocalPath = localBaseDir.resolve(objectKey).normalize();
// 安全校验:防止路径穿越
if (!fullLocalPath.startsWith(localBaseDir)) {
throw new RuntimeException("非法文件夹路径,可能包含路径穿越: " + objectKey);
}
try {
if (Files.exists(fullLocalPath)) {
//直接删除整个目录
log.info("本地目录已存在,将删除并重新创建: {}", fullLocalPath);
FileUtils.deleteDirectory(fullLocalPath.toFile());
}
log.info("创建本地目录: {}", fullLocalPath);
Files.createDirectories(fullLocalPath);
} catch (Exception e) {
throw new RuntimeException("无法准备本地目录: " + fullLocalPath, e);
}
FlowNodeIdUtils.prepareLocalDir(objectKey);
}
}

View File

@@ -1,7 +1,14 @@
package com.sdm.flowable.util;
import com.sdm.flowable.constants.FlowableConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@Slf4j
public class FlowNodeIdUtils {
private static final String JOIN_GATEWAY_PREFIX = FlowableConfig.JOIN_GATEWAY_PREFIX;
private static final String SPLIT_GATEWAY_PREFIX = FlowableConfig.SPLIT_GATEWAY_PREFIX;
@@ -65,4 +72,35 @@ public class FlowNodeIdUtils {
public static String getRetryTaskId() {
return FlowableConfig.RETRY_TASK_ID;
}
/**
* 准备本地目录:如果目录已存在,则清空其内容;否则创建新目录。
* 流程实例启动后,需要在本地准备一个目录,用于存储节点计算结果。
* 如果同一个流程二次启动,每个节点会使用同一个文件夹,二次启动的时候,
* 如果清空,上一次流程实例运行结果相关文件也会在这个文件夹中,影响这次运行流程的结果文件
*
* @param objectKey MinIO 的对象路径,将作为本地目录路径的一部分
*/
public static void prepareLocalDir(String objectKey) {
String simulationBaseDir = FlowableConfig.FLOWABLE_SIMULATION_BASEDIR;
Path localBaseDir = Paths.get(simulationBaseDir).toAbsolutePath().normalize();
Path fullLocalPath = localBaseDir.resolve(objectKey).normalize();
// 安全校验:防止路径穿越
if (!fullLocalPath.startsWith(localBaseDir)) {
throw new RuntimeException("非法文件夹路径,可能包含路径穿越: " + objectKey);
}
try {
if (Files.exists(fullLocalPath)) {
//直接删除整个目录
log.info("本地目录已存在,将删除并重新创建: {}", fullLocalPath);
FileUtils.deleteDirectory(fullLocalPath.toFile());
}
log.info("创建本地目录: {}", fullLocalPath);
Files.createDirectories(fullLocalPath);
} catch (Exception e) {
throw new RuntimeException("无法准备本地目录: " + fullLocalPath, e);
}
}
}