整体需求完善

This commit is contained in:
2025-08-01 20:04:41 +08:00
parent 44b18c3689
commit d2043feda5
2 changed files with 225 additions and 73 deletions

View File

@ -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);
// 2. 从参数中提取实际需要传递给Python脚本的参数列表
// 示例:假设前端传入 {"args": [3, 0, 8, 7, 2, 1, 9, 4]}
List<String> args = new ArrayList<>();
if (paramMap.containsKey("args")) {
List<Object> argList = (List<Object>) 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<String> 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() + ")");
}
}
/**

View File

@ -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<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);
}
// 4. 检测系统中可用的Python命令
String pythonCommand = findPythonCommand();
log.info("使用Python命令: " + pythonCommand);
// 5. 构建命令python [本地脚本路径] [参数1] [参数2] ...
List<String> 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<String, String> 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<String> 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<String> 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<String> getAllNames() {
return algorithmInfoMapper.getAllNames();