diff --git a/src/main/java/com/bipt/intelligentapplicationorchestrationservice/controller/AlgorithmInfoController.java b/src/main/java/com/bipt/intelligentapplicationorchestrationservice/controller/AlgorithmInfoController.java index 9d4c466..9cec50f 100644 --- a/src/main/java/com/bipt/intelligentapplicationorchestrationservice/controller/AlgorithmInfoController.java +++ b/src/main/java/com/bipt/intelligentapplicationorchestrationservice/controller/AlgorithmInfoController.java @@ -107,35 +107,60 @@ public class AlgorithmInfoController { */ @PostMapping("/run/{id}") @Operation(summary = "运行算法") - public OptResult run(@PathVariable Long id, @RequestBody String param) { - log.info("运行算法 ID: {}", id); + public OptResult run(@PathVariable Long id, @RequestBody Map paramMap) { + log.info("运行算法 ID: {}, 参数: {}", id, paramMap); try { AlgorithmInfo algorithm = algorithmInfoService.getById(id); if (algorithm == null) { return OptResult.error("算法不存在"); } - // 1. 解析前端传入的参数(JSON格式) - Map paramMap = objectMapper.readValue(param, Map.class); - - // 2. 从参数中提取实际需要传递给Python脚本的参数列表 - // 示例:假设前端传入 {"args": [3, 0, 8, 7, 2, 1, 9, 4]} - List args = new ArrayList<>(); - if (paramMap.containsKey("args")) { - List argList = (List) paramMap.get("args"); - for (Object arg : argList) { - args.add(arg.toString()); - } + // 验证算法文件路径 + if (algorithm.getAlgorithmFile() == null || algorithm.getAlgorithmFile().isEmpty()) { + return OptResult.error("算法文件路径不存在"); } - // 3. 调用Service执行Python脚本并获取结果 - String result = algorithmInfoService.run(algorithm.getFilePath(), args); + // 提取并转换参数为字符串列表,适配Python脚本参数格式 + List args = new ArrayList<>(); - // 4. 返回结构化结果 - return OptResult.success("运行结果"+result); + // 处理多种参数传递方式 + if (paramMap.containsKey("param")) { + // 处理前端直接传递的单个参数 + Object paramValue = paramMap.get("param"); + if (paramValue != null) { + args.add(paramValue.toString()); + } + } else if (paramMap.containsKey("args")) { + // 处理参数列表 + Object argsObj = paramMap.get("args"); + if (argsObj instanceof List) { + ((List) argsObj).forEach(arg -> { + if (arg != null) { + args.add(arg.toString()); + } + }); + } else if (argsObj != null) { + args.add(argsObj.toString()); + } + } else { + // 将所有键值对作为参数传递 (key=value格式) + paramMap.forEach((key, value) -> { + if (value != null) { + args.add(key + "=" + value.toString()); + } + }); + } + + log.info("解析后的算法参数: {}", args); + + // 调用Service执行Python脚本并获取结果 + String result = algorithmInfoService.run(algorithm.getAlgorithmFile(), args); + + // 返回结构化结果 + return OptResult.success(result); } catch (Exception e) { log.error("算法运行失败", e); - return OptResult.error("算法运行失败: " + e.getMessage()); + return OptResult.error("算法运行失败: " + e.getMessage() + " (" + e.getLocalizedMessage() + ")"); } } /** diff --git a/src/main/java/com/bipt/intelligentapplicationorchestrationservice/service/Impl/AlgorithmInfoServiceImpl.java b/src/main/java/com/bipt/intelligentapplicationorchestrationservice/service/Impl/AlgorithmInfoServiceImpl.java index 7c3ce18..05a385c 100644 --- a/src/main/java/com/bipt/intelligentapplicationorchestrationservice/service/Impl/AlgorithmInfoServiceImpl.java +++ b/src/main/java/com/bipt/intelligentapplicationorchestrationservice/service/Impl/AlgorithmInfoServiceImpl.java @@ -13,14 +13,14 @@ import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; import java.io.*; +import java.net.URL; 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.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; +import java.util.*; @Service @Slf4j @@ -104,7 +104,8 @@ public class AlgorithmInfoServiceImpl implements AlgorithmInfoService { // 生成唯一文件名,避免冲突 String fileName = UUID.randomUUID().toString() + "_" + originalFilename; - // 构建相对路径(相对于项目根目录) + // 使用Paths.get()构建路径,自动处理不同系统的文件分隔符 + // 假设uploadDir是一个相对路径字符串,如"uploads/algorithms" Path relativePath = Paths.get(uploadDir, fileName); // 获取当前应用的运行目录(兼容开发和部署环境) @@ -121,8 +122,9 @@ public class AlgorithmInfoServiceImpl implements AlgorithmInfoService { // 保存文件到指定路径 file.transferTo(absolutePath); - // 存储相对路径到数据库 - algorithmInfo.setAlgorithmFile(relativePath.toString()); + // 存储相对路径到数据库,使用toString()会自动使用系统默认分隔符 + // 统一数据库中的格式,我们可以使用Unix风格的分隔符'/' + algorithmInfo.setAlgorithmFile(relativePath.toString().replace(File.separator, "/")); // 设置文件大小 algorithmInfo.setFileSize(Files.size(absolutePath)); @@ -146,73 +148,198 @@ public class AlgorithmInfoServiceImpl implements AlgorithmInfoService { /** * 执行Python算法脚本并返回结果 - * @param scriptPath Python脚本路径(数据库中存储的相对路径) + * 先将文件下载到本地临时目录,再执行脚本 + * @param scriptUrl Python脚本的URL路径(可以是远程URL或本地文件路径) * @param args 命令行参数列表 * @return 脚本执行结果 */ - public String run(String scriptPath, List args) throws IOException, InterruptedException { - if (scriptPath == null || scriptPath.isEmpty()) { + public String run(String scriptUrl, List args) throws IOException, InterruptedException { + if (scriptUrl == null || scriptUrl.isEmpty()) { throw new IllegalArgumentException("脚本路径不能为空"); } - // 获取当前应用的运行目录(兼容开发和部署环境) - Path basePath = Paths.get("").toAbsolutePath(); - Path absoluteScriptPath = basePath.resolve(scriptPath); + // 1. 创建临时目录用于存放下载的脚本文件 + Path tempDir = Files.createTempDirectory("algorithm_scripts_"); + log.info("创建临时目录用于存放脚本: {}", tempDir.toAbsolutePath()); - // 验证文件是否存在 - if (!Files.exists(absoluteScriptPath)) { - throw new FileNotFoundException("脚本文件不存在: " + absoluteScriptPath); + try { + // 2. 下载文件到临时目录 + Path localScriptPath = downloadFileToLocal(scriptUrl, tempDir); + + // 3. 验证文件是否存在且可读 + if (!Files.exists(localScriptPath)) { + throw new FileNotFoundException("下载的脚本文件不存在: " + localScriptPath); + } + if (!Files.isReadable(localScriptPath)) { + throw new IOException("下载的脚本文件不可读: " + localScriptPath); + } + + // 4. 检测系统中可用的Python命令 + String pythonCommand = findPythonCommand(); + log.info("使用Python命令: " + pythonCommand); + + // 5. 构建命令:python [本地脚本路径] [参数1] [参数2] ... + List command = new ArrayList<>(); + command.add(pythonCommand); + command.add(localScriptPath.toString()); + command.addAll(args); + + // 打印完整命令(用于调试) + log.info("执行命令: {}", String.join(" ", command)); + + // 6. 创建进程并执行命令 + ProcessBuilder processBuilder = new ProcessBuilder(command); + + // 设置工作目录为临时目录 + processBuilder.directory(tempDir.toFile()); + + // 设置环境变量 + Map env = processBuilder.environment(); + env.put("PATH", System.getenv("PATH")); + + processBuilder.redirectErrorStream(true); + Process process = processBuilder.start(); + + // 7. 读取脚本输出 + StringBuilder output = new StringBuilder(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + } + } + + // 8. 读取错误输出 + StringBuilder errorOutput = new StringBuilder(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + errorOutput.append(line).append("\n"); + } + } + + // 9. 等待进程执行完成并检查退出码 + int exitCode = process.waitFor(); + + if (exitCode != 0) { + String errorMsg = "脚本执行失败,退出码: " + exitCode + + "\n命令: " + String.join(" ", command) + + "\n标准输出: " + output.toString() + + "\n错误输出: " + errorOutput.toString(); + log.error(errorMsg); + throw new RuntimeException(errorMsg); + } + + if (!errorOutput.isEmpty()) { + log.warn("脚本执行成功,但有错误输出: " + errorOutput.toString()); + } + + return output.toString(); + } finally { + // 10. 清理临时文件和目录 + deleteDirectory(tempDir.toFile()); + log.info("已清理临时目录: {}", tempDir.toAbsolutePath()); + } + } + + /** + * 下载文件到本地目录 + * @param fileUrl 文件URL(支持本地文件路径和远程HTTP/HTTPS URL) + * @param targetDir 目标目录 + * @return 本地文件路径 + */ + private Path downloadFileToLocal(String fileUrl, Path targetDir) throws IOException { + // 生成唯一的文件名,保留原始文件扩展名 + String originalFileName = new File(fileUrl).getName(); + String fileExtension = ""; + int dotIndex = originalFileName.lastIndexOf('.'); + if (dotIndex > 0) { + fileExtension = originalFileName.substring(dotIndex); + } + String uniqueFileName = UUID.randomUUID().toString() + fileExtension; + Path targetPath = targetDir.resolve(uniqueFileName); + + // 判断是本地文件还是远程URL + if (fileUrl.startsWith("http://") || fileUrl.startsWith("https://")) { + // 远程URL,通过网络下载 + log.info("从URL下载文件: {} 到 {}", fileUrl, targetPath); + try (InputStream in = new URL(fileUrl).openStream()) { + Files.copy(in, targetPath, StandardCopyOption.REPLACE_EXISTING); + } + } else { + // 本地文件,直接复制 + Path sourcePath = Paths.get(fileUrl).toAbsolutePath().normalize(); + log.info("从本地复制文件: {} 到 {}", sourcePath, targetPath); + Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING); } - // 验证文件是否可执行(针对Python脚本) - if (!Files.isReadable(absoluteScriptPath)) { - throw new IOException("脚本文件不可读: " + absoluteScriptPath); + return targetPath; + } + + /** + * 递归删除目录及其内容 + */ + private void deleteDirectory(File directory) { + if (directory == null || !directory.exists()) { + return; } - // 构建命令:python [脚本绝对路径] [参数1] [参数2] ... - List command = new ArrayList<>(); - command.add("python"); // Python解释器路径,可配置在application.properties中 - command.add(absoluteScriptPath.toString()); // 使用绝对路径执行脚本 - command.addAll(args); // 添加所有参数 + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + deleteDirectory(file); + } else { + file.delete(); + } + } + } + directory.delete(); + } - // 打印完整命令(用于调试) - log.info("执行命令: {}", String.join(" ", command)); + /** + * 查找系统中可用的Python命令 + * 优先使用python,找不到再尝试其他可能的命令 + */ + private String findPythonCommand() { + // 要尝试的Python命令列表,按优先级排序 + List possibleCommands = Arrays.asList("python", "python3", "py"); - // 创建进程并执行命令 - ProcessBuilder processBuilder = new ProcessBuilder(command); - - // 设置工作目录为脚本所在目录 - processBuilder.directory(absoluteScriptPath.getParent().toFile()); - - processBuilder.redirectErrorStream(true); // 将错误输出合并到标准输出 - Process process = processBuilder.start(); - - // 读取脚本输出(使用UTF-8编码,避免中文乱码) - StringBuilder output = new StringBuilder(); - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { - String line; - while ((line = reader.readLine()) != null) { - output.append(line).append("\n"); + for (String cmd : possibleCommands) { + if (isCommandAvailable(cmd)) { + return cmd; } } - // 等待进程执行完成并获取退出码 - int exitCode = process.waitFor(); - - // 检查脚本是否成功执行 - if (exitCode != 0) { - // 捕获详细的错误信息 - String errorMsg = "脚本执行失败,退出码: " + exitCode + - "\n命令: " + String.join(" ", command) + - "\n输出: " + output.toString(); - log.error(errorMsg); - throw new RuntimeException(errorMsg); - } - - return output.toString(); + // 如果都找不到,尝试直接返回python(让系统抛出更明确的错误) + return "python"; } + /** + * 检查系统是否存在指定的命令 + */ + private boolean isCommandAvailable(String command) { + ProcessBuilder processBuilder = new ProcessBuilder(); + try { + String osName = System.getProperty("os.name").toLowerCase(); + if (osName.contains("win")) { + // Windows系统使用where命令检查 + processBuilder.command("cmd", "/c", "where", command); + } else { + // Linux或Mac系统使用which命令检查 + processBuilder.command("which", command); + } + Process process = processBuilder.start(); + // 等待命令执行完成,0表示成功找到命令 + return process.waitFor() == 0; + } catch (IOException | InterruptedException e) { + return false; + } + } + + @Override public List getAllNames() { return algorithmInfoMapper.getAllNames();