Compare commits
1 Commits
main
...
xiaohucodi
Author | SHA1 | Date | |
---|---|---|---|
d2043feda5 |
@ -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<String, Object> paramMap) {
|
||||
log.info("运行算法 ID: {}, 参数: {}", id, paramMap);
|
||||
try {
|
||||
AlgorithmInfo algorithm = algorithmInfoService.getById(id);
|
||||
if (algorithm == null) {
|
||||
return OptResult.error("算法不存在");
|
||||
}
|
||||
|
||||
// 1. 解析前端传入的参数(JSON格式)
|
||||
Map<String, Object> paramMap = objectMapper.readValue(param, Map.class);
|
||||
// 验证算法文件路径
|
||||
if (algorithm.getAlgorithmFile() == null || algorithm.getAlgorithmFile().isEmpty()) {
|
||||
return OptResult.error("算法文件路径不存在");
|
||||
}
|
||||
|
||||
// 2. 从参数中提取实际需要传递给Python脚本的参数列表
|
||||
// 示例:假设前端传入 {"args": [3, 0, 8, 7, 2, 1, 9, 4]}
|
||||
// 提取并转换参数为字符串列表,适配Python脚本参数格式
|
||||
List<String> args = new ArrayList<>();
|
||||
if (paramMap.containsKey("args")) {
|
||||
List<Object> argList = (List<Object>) paramMap.get("args");
|
||||
for (Object arg : argList) {
|
||||
|
||||
// 处理多种参数传递方式
|
||||
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());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 调用Service执行Python脚本并获取结果
|
||||
String result = algorithmInfoService.run(algorithm.getFilePath(), args);
|
||||
log.info("解析后的算法参数: {}", args);
|
||||
|
||||
// 4. 返回结构化结果
|
||||
return OptResult.success("运行结果"+result);
|
||||
// 调用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() + ")");
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
@ -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,48 +148,59 @@ public class AlgorithmInfoServiceImpl implements AlgorithmInfoService {
|
||||
|
||||
/**
|
||||
* 执行Python算法脚本并返回结果
|
||||
* @param scriptPath Python脚本路径(数据库中存储的相对路径)
|
||||
* 先将文件下载到本地临时目录,再执行脚本
|
||||
* @param scriptUrl Python脚本的URL路径(可以是远程URL或本地文件路径)
|
||||
* @param args 命令行参数列表
|
||||
* @return 脚本执行结果
|
||||
*/
|
||||
public String run(String scriptPath, List<String> args) throws IOException, InterruptedException {
|
||||
if (scriptPath == null || scriptPath.isEmpty()) {
|
||||
public String run(String scriptUrl, List<String> 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);
|
||||
}
|
||||
|
||||
// 验证文件是否可执行(针对Python脚本)
|
||||
if (!Files.isReadable(absoluteScriptPath)) {
|
||||
throw new IOException("脚本文件不可读: " + absoluteScriptPath);
|
||||
}
|
||||
// 4. 检测系统中可用的Python命令
|
||||
String pythonCommand = findPythonCommand();
|
||||
log.info("使用Python命令: " + pythonCommand);
|
||||
|
||||
// 构建命令:python [脚本绝对路径] [参数1] [参数2] ...
|
||||
// 5. 构建命令:python [本地脚本路径] [参数1] [参数2] ...
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("python"); // Python解释器路径,可配置在application.properties中
|
||||
command.add(absoluteScriptPath.toString()); // 使用绝对路径执行脚本
|
||||
command.addAll(args); // 添加所有参数
|
||||
command.add(pythonCommand);
|
||||
command.add(localScriptPath.toString());
|
||||
command.addAll(args);
|
||||
|
||||
// 打印完整命令(用于调试)
|
||||
log.info("执行命令: {}", String.join(" ", command));
|
||||
|
||||
// 创建进程并执行命令
|
||||
// 6. 创建进程并执行命令
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
||||
|
||||
// 设置工作目录为脚本所在目录
|
||||
processBuilder.directory(absoluteScriptPath.getParent().toFile());
|
||||
// 设置工作目录为临时目录
|
||||
processBuilder.directory(tempDir.toFile());
|
||||
|
||||
processBuilder.redirectErrorStream(true); // 将错误输出合并到标准输出
|
||||
// 设置环境变量
|
||||
Map<String, String> env = processBuilder.environment();
|
||||
env.put("PATH", System.getenv("PATH"));
|
||||
|
||||
processBuilder.redirectErrorStream(true);
|
||||
Process process = processBuilder.start();
|
||||
|
||||
// 读取脚本输出(使用UTF-8编码,避免中文乱码)
|
||||
// 7. 读取脚本输出
|
||||
StringBuilder output = new StringBuilder();
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
@ -197,22 +210,136 @@ public class AlgorithmInfoServiceImpl implements AlgorithmInfoService {
|
||||
}
|
||||
}
|
||||
|
||||
// 等待进程执行完成并获取退出码
|
||||
// 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标准输出: " + output.toString() +
|
||||
"\n错误输出: " + errorOutput.toString();
|
||||
log.error(errorMsg);
|
||||
throw new RuntimeException(errorMsg);
|
||||
}
|
||||
|
||||
return output.toString();
|
||||
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);
|
||||
}
|
||||
|
||||
return targetPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归删除目录及其内容
|
||||
*/
|
||||
private void deleteDirectory(File directory) {
|
||||
if (directory == null || !directory.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
File[] files = directory.listFiles();
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
deleteDirectory(file);
|
||||
} else {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
directory.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找系统中可用的Python命令
|
||||
* 优先使用python,找不到再尝试其他可能的命令
|
||||
*/
|
||||
private String findPythonCommand() {
|
||||
// 要尝试的Python命令列表,按优先级排序
|
||||
List<String> possibleCommands = Arrays.asList("python", "python3", "py");
|
||||
|
||||
for (String cmd : possibleCommands) {
|
||||
if (isCommandAvailable(cmd)) {
|
||||
return cmd;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果都找不到,尝试直接返回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<String> getAllNames() {
|
||||
return algorithmInfoMapper.getAllNames();
|
||||
|
Reference in New Issue
Block a user