情景状态闭环
This commit is contained in:
parent
50fc25abbc
commit
d102de849b
@ -0,0 +1,16 @@
|
|||||||
|
package com.yfd.business.css.common.exception;
|
||||||
|
|
||||||
|
public class ScenarioInferException extends RuntimeException {
|
||||||
|
|
||||||
|
private final String failDetailJson;
|
||||||
|
|
||||||
|
public ScenarioInferException(String message, String failDetailJson) {
|
||||||
|
super(message);
|
||||||
|
this.failDetailJson = failDetailJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFailDetailJson() {
|
||||||
|
return failDetailJson;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -2,18 +2,23 @@ package com.yfd.business.css.controller;
|
|||||||
|
|
||||||
import com.yfd.business.css.build.SimBuilder;
|
import com.yfd.business.css.build.SimBuilder;
|
||||||
import com.yfd.business.css.domain.Scenario;
|
import com.yfd.business.css.domain.Scenario;
|
||||||
|
import com.yfd.business.css.domain.ScenarioResult;
|
||||||
import com.yfd.business.css.facade.SimDataFacade;
|
import com.yfd.business.css.facade.SimDataFacade;
|
||||||
import com.yfd.business.css.model.*;
|
import com.yfd.business.css.model.*;
|
||||||
|
import com.yfd.business.css.service.ScenarioResultService;
|
||||||
import com.yfd.business.css.service.MaterialService;
|
import com.yfd.business.css.service.MaterialService;
|
||||||
import com.yfd.business.css.service.ScenarioService;
|
import com.yfd.business.css.service.ScenarioService;
|
||||||
import com.yfd.business.css.service.SimService;
|
import com.yfd.business.css.service.SimService;
|
||||||
import com.yfd.business.css.service.SimInferService;
|
import com.yfd.business.css.service.SimInferService;
|
||||||
import com.yfd.platform.config.ResponseResult;
|
import com.yfd.platform.config.ResponseResult;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import com.yfd.platform.annotation.Log;
|
import com.yfd.platform.annotation.Log;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@ -31,6 +36,8 @@ public class SimController {
|
|||||||
@Autowired private MaterialService materialService;
|
@Autowired private MaterialService materialService;
|
||||||
@Autowired private SimInferService simInferService;
|
@Autowired private SimInferService simInferService;
|
||||||
@Autowired private ScenarioService scenarioService;
|
@Autowired private ScenarioService scenarioService;
|
||||||
|
@Autowired private ScenarioResultService scenarioResultService;
|
||||||
|
@Autowired private ObjectMapper objectMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行仿真计算
|
* 执行仿真计算
|
||||||
@ -44,9 +51,24 @@ public class SimController {
|
|||||||
String projectId = (String) req.get("projectId");
|
String projectId = (String) req.get("projectId");
|
||||||
String scenarioId = (String) req.get("scenarioId");
|
String scenarioId = (String) req.get("scenarioId");
|
||||||
int steps = req.containsKey("steps") ? (int) req.get("steps") : 10;
|
int steps = req.containsKey("steps") ? (int) req.get("steps") : 10;
|
||||||
|
boolean clearResult = Boolean.TRUE.equals(req.get("clearResult"));
|
||||||
|
|
||||||
|
Scenario sc = scenarioService.getById(scenarioId);
|
||||||
|
if (sc != null) {
|
||||||
|
String st = sc.getStatus();
|
||||||
|
if ("1".equals(st)) {
|
||||||
|
return ResponseResult.error("情景正在运行中,请稍后重试");
|
||||||
|
}
|
||||||
|
if (("2".equals(st) || "3".equals(st)) && !clearResult) {
|
||||||
|
return ResponseResult.error("请先清空历史结果后再重新模拟");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (clearResult) {
|
||||||
|
scenarioResultService.remove(new QueryWrapper<ScenarioResult>().eq("scenario_id", scenarioId));
|
||||||
|
}
|
||||||
|
|
||||||
// 0. Update Status: 更新情景状态为进行中
|
// 0. Update Status: 更新情景状态为进行中
|
||||||
updateScenarioStatus(scenarioId, "1"); // 1: 进行中
|
updateScenarioStatus(scenarioId, "1", null); // 1: 进行中
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Load Data: 获取项目、设备和事件数据
|
// 1. Load Data: 获取项目、设备和事件数据
|
||||||
@ -75,16 +97,30 @@ public class SimController {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
// 仿真计算失败,更新状态为失败 (3: 失败)
|
// 仿真计算失败,更新状态为失败 (3: 失败)
|
||||||
updateScenarioStatus(scenarioId, "3");
|
String failDetail = null;
|
||||||
|
try {
|
||||||
|
Map<String, Object> fd = new HashMap<>();
|
||||||
|
fd.put("code", "SIMULATION_FAILED");
|
||||||
|
fd.put("message", "仿真计算失败");
|
||||||
|
fd.put("detail", e.getMessage() == null ? "" : e.getMessage());
|
||||||
|
fd.put("suggestions", List.of(
|
||||||
|
"请检查情景配置、事件配置与拓扑数据是否完整",
|
||||||
|
"请检查输入参数 steps 与相关数据是否符合预期"
|
||||||
|
));
|
||||||
|
failDetail = objectMapper.writeValueAsString(fd);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
updateScenarioStatus(scenarioId, "3", failDetail);
|
||||||
return ResponseResult.error("Simulation failed: " + e.getMessage());
|
return ResponseResult.error("Simulation failed: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateScenarioStatus(String scenarioId, String status) {
|
private void updateScenarioStatus(String scenarioId, String status, String failDetail) {
|
||||||
try {
|
try {
|
||||||
Scenario scenario = new Scenario();
|
Scenario scenario = new Scenario();
|
||||||
scenario.setScenarioId(scenarioId);
|
scenario.setScenarioId(scenarioId);
|
||||||
scenario.setStatus(status);
|
scenario.setStatus(status);
|
||||||
|
scenario.setFailDetail(failDetail);
|
||||||
scenario.setUpdatedAt(LocalDateTime.now());
|
scenario.setUpdatedAt(LocalDateTime.now());
|
||||||
scenarioService.updateById(scenario);
|
scenarioService.updateById(scenario);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@ -54,4 +54,7 @@ public class Scenario implements Serializable {
|
|||||||
*/
|
*/
|
||||||
@TableField("device_algo_config")
|
@TableField("device_algo_config")
|
||||||
private String deviceAlgoConfig;
|
private String deviceAlgoConfig;
|
||||||
|
|
||||||
|
@TableField("fail_detail")
|
||||||
|
private String failDetail;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.JsonNode;
|
|||||||
import com.yfd.business.css.domain.Scenario;
|
import com.yfd.business.css.domain.Scenario;
|
||||||
import com.yfd.business.css.domain.ScenarioResult;
|
import com.yfd.business.css.domain.ScenarioResult;
|
||||||
import com.yfd.business.css.domain.AlgorithmModel;
|
import com.yfd.business.css.domain.AlgorithmModel;
|
||||||
|
import com.yfd.business.css.common.exception.ScenarioInferException;
|
||||||
import com.yfd.business.css.model.DeviceStepInfo;
|
import com.yfd.business.css.model.DeviceStepInfo;
|
||||||
import com.yfd.business.css.model.InferRequest;
|
import com.yfd.business.css.model.InferRequest;
|
||||||
import com.yfd.business.css.model.InferResponse;
|
import com.yfd.business.css.model.InferResponse;
|
||||||
@ -52,6 +53,8 @@ public class DeviceInferService {
|
|||||||
// 增加标志位,记录是否至少成功执行了一次推理
|
// 增加标志位,记录是否至少成功执行了一次推理
|
||||||
boolean hasAnySuccess = false;
|
boolean hasAnySuccess = false;
|
||||||
boolean hasAnyError = false;
|
boolean hasAnyError = false;
|
||||||
|
List<Map<String, Object>> missingModels = new ArrayList<>();
|
||||||
|
List<String> errorMessages = new ArrayList<>();
|
||||||
|
|
||||||
// 1. 获取情景配置信息
|
// 1. 获取情景配置信息
|
||||||
Scenario scenario = scenarioService.getById(scenarioId);
|
Scenario scenario = scenarioService.getById(scenarioId);
|
||||||
@ -117,6 +120,11 @@ public class DeviceInferService {
|
|||||||
if (model == null || model.getModelPath() == null) {
|
if (model == null || model.getModelPath() == null) {
|
||||||
log.error("Model path not found for algorithmType: {}, deviceType: {}, materialType: {}", currentAlgoType, deviceType, currentMaterialType);
|
log.error("Model path not found for algorithmType: {}, deviceType: {}, materialType: {}", currentAlgoType, deviceType, currentMaterialType);
|
||||||
hasAnyError = true;
|
hasAnyError = true;
|
||||||
|
missingModels.add(Map.of(
|
||||||
|
"algorithmType", currentAlgoType,
|
||||||
|
"deviceType", deviceType,
|
||||||
|
"materialType", currentMaterialType
|
||||||
|
));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,6 +194,9 @@ public class DeviceInferService {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("推理异常: {}", e.getMessage(), e);
|
log.error("推理异常: {}", e.getMessage(), e);
|
||||||
hasAnyError = true;
|
hasAnyError = true;
|
||||||
|
if (e.getMessage() != null && !e.getMessage().isBlank()) {
|
||||||
|
errorMessages.add(e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -193,9 +204,43 @@ public class DeviceInferService {
|
|||||||
|
|
||||||
// 5. 最终检查:如果没有任何成功的推理,且发生过错误,抛出异常以通知上层
|
// 5. 最终检查:如果没有任何成功的推理,且发生过错误,抛出异常以通知上层
|
||||||
if (!hasAnySuccess && hasAnyError) {
|
if (!hasAnySuccess && hasAnyError) {
|
||||||
throw new RuntimeException("所有设备推理均失败或未找到模型");
|
Map<String, Object> failDetail = new HashMap<>();
|
||||||
|
if (!missingModels.isEmpty()) {
|
||||||
|
failDetail.put("code", "MODEL_NOT_FOUND");
|
||||||
|
failDetail.put("message", "缺少可用模型");
|
||||||
|
failDetail.put("missingModels", dedupeMissingModels(missingModels));
|
||||||
|
failDetail.put("suggestions", List.of(
|
||||||
|
"请在模型管理中训练并激活缺失模型(algorithmType/deviceType/materialType 对应组合)",
|
||||||
|
"检查材料类型来源是否正确,必要时启用材料类型回退/默认映射策略"
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
failDetail.put("code", "INFER_FAILED");
|
||||||
|
failDetail.put("message", "推理失败");
|
||||||
|
if (!errorMessages.isEmpty()) {
|
||||||
|
failDetail.put("errors", errorMessages.size() > 50 ? errorMessages.subList(0, 50) : errorMessages);
|
||||||
|
}
|
||||||
|
failDetail.put("suggestions", List.of(
|
||||||
|
"请检查 Python 推理服务是否可用以及请求参数是否完整",
|
||||||
|
"请检查模型是否已训练并激活,算法类型/设备类型/材料类型是否匹配"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
String json = null;
|
||||||
|
try {
|
||||||
|
json = objectMapper.writeValueAsString(failDetail);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
throw new ScenarioInferException("推理失败", json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<Map<String, Object>> dedupeMissingModels(List<Map<String, Object>> missingModels) {
|
||||||
|
LinkedHashMap<String, Map<String, Object>> m = new LinkedHashMap<>();
|
||||||
|
for (Map<String, Object> x : missingModels) {
|
||||||
|
String k = String.valueOf(x.get("algorithmType")) + "|" + String.valueOf(x.get("deviceType")) + "|" + String.valueOf(x.get("materialType"));
|
||||||
|
m.putIfAbsent(k, x);
|
||||||
|
}
|
||||||
|
return new ArrayList<>(m.values());
|
||||||
|
}
|
||||||
|
|
||||||
private InferRequest buildInferenceRequest(String deviceType,List<DeviceStepInfo> devices,String modelPath, List<String> requiredFeatures) {
|
private InferRequest buildInferenceRequest(String deviceType,List<DeviceStepInfo> devices,String modelPath, List<String> requiredFeatures) {
|
||||||
InferRequest request = new InferRequest();
|
InferRequest request = new InferRequest();
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package com.yfd.business.css.service;
|
package com.yfd.business.css.service;
|
||||||
|
|
||||||
import com.yfd.business.css.domain.Scenario;
|
import com.yfd.business.css.domain.Scenario;
|
||||||
|
import com.yfd.business.css.common.exception.ScenarioInferException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.yfd.business.css.model.*;
|
import com.yfd.business.css.model.*;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@ -24,6 +26,9 @@ public class SimInferService {
|
|||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ScenarioService scenarioService;
|
private ScenarioService scenarioService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
@Async
|
@Async
|
||||||
public void asyncInferAndSave(String projectId, String scenarioId, SimContext context, List<SimUnit> units) {
|
public void asyncInferAndSave(String projectId, String scenarioId, SimContext context, List<SimUnit> units) {
|
||||||
@ -83,7 +88,7 @@ public class SimInferService {
|
|||||||
if (groupedDevices.isEmpty()) {
|
if (groupedDevices.isEmpty()) {
|
||||||
log.warn("No device data found for inference. scenarioId: {}", scenarioId);
|
log.warn("No device data found for inference. scenarioId: {}", scenarioId);
|
||||||
// 即使没有数据,也应该更新状态为完成,或者视为正常结束
|
// 即使没有数据,也应该更新状态为完成,或者视为正常结束
|
||||||
updateScenarioStatus(scenarioId, "2");
|
updateScenarioStatus(scenarioId, "2", null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,21 +103,39 @@ public class SimInferService {
|
|||||||
// 但由于是 void 方法,无法直接得知。
|
// 但由于是 void 方法,无法直接得知。
|
||||||
// 简单改进:如果 groupedDevices 非空,但所有组都因为找不到模型而跳过,应该视为失败吗?
|
// 简单改进:如果 groupedDevices 非空,但所有组都因为找不到模型而跳过,应该视为失败吗?
|
||||||
// 目前策略:只要没有抛出未捕获异常,就视为 Success。
|
// 目前策略:只要没有抛出未捕获异常,就视为 Success。
|
||||||
updateScenarioStatus(scenarioId, "2");
|
updateScenarioStatus(scenarioId, "2", null);
|
||||||
log.info("Async inference completed successfully. scenarioId: {}", scenarioId);
|
log.info("Async inference completed successfully. scenarioId: {}", scenarioId);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Async inference failed. scenarioId: {}, error: {}", scenarioId, e.getMessage(), e);
|
log.error("Async inference failed. scenarioId: {}, error: {}", scenarioId, e.getMessage(), e);
|
||||||
// 5. 更新状态为失败 (假设 3 代表失败)
|
// 5. 更新状态为失败 (假设 3 代表失败)
|
||||||
updateScenarioStatus(scenarioId, "3");
|
String failDetail = null;
|
||||||
|
if (e instanceof ScenarioInferException) {
|
||||||
|
failDetail = ((ScenarioInferException) e).getFailDetailJson();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
Map<String, Object> fd = new HashMap<>();
|
||||||
|
fd.put("code", "INFER_FAILED");
|
||||||
|
fd.put("message", "推理失败");
|
||||||
|
fd.put("detail", e.getMessage() == null ? "" : e.getMessage());
|
||||||
|
fd.put("suggestions", List.of(
|
||||||
|
"请检查模型是否已训练并激活,算法类型/设备类型/材料类型是否匹配",
|
||||||
|
"请检查 Python 推理服务是否可用以及请求参数是否完整"
|
||||||
|
));
|
||||||
|
failDetail = objectMapper.writeValueAsString(fd);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateScenarioStatus(scenarioId, "3", failDetail);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateScenarioStatus(String scenarioId, String status) {
|
private void updateScenarioStatus(String scenarioId, String status, String failDetail) {
|
||||||
try {
|
try {
|
||||||
Scenario scenario = new Scenario();
|
Scenario scenario = new Scenario();
|
||||||
scenario.setScenarioId(scenarioId);
|
scenario.setScenarioId(scenarioId);
|
||||||
scenario.setStatus(status);
|
scenario.setStatus(status);
|
||||||
|
scenario.setFailDetail(failDetail);
|
||||||
scenario.setUpdatedAt(LocalDateTime.now());
|
scenario.setUpdatedAt(LocalDateTime.now());
|
||||||
scenarioService.updateById(scenario);
|
scenarioService.updateById(scenario);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@ -24,49 +24,43 @@ public class AlgorithmModelServiceImpl extends ServiceImpl<AlgorithmModelMapper,
|
|||||||
@Override
|
@Override
|
||||||
public String getCurrentModelPath(String algorithmType, String deviceType, String materialType) {
|
public String getCurrentModelPath(String algorithmType, String deviceType, String materialType) {
|
||||||
log.debug("Querying current model path for algorithmType: {}, deviceType: {}, materialType: {}", algorithmType, deviceType, materialType);
|
log.debug("Querying current model path for algorithmType: {}, deviceType: {}, materialType: {}", algorithmType, deviceType, materialType);
|
||||||
QueryWrapper<AlgorithmModel> queryWrapper = new QueryWrapper<>();
|
AlgorithmModel model = getCurrentModel(algorithmType, deviceType, materialType);
|
||||||
queryWrapper.eq("algorithm_type", algorithmType)
|
if (model == null) {
|
||||||
.eq("device_type", deviceType)
|
|
||||||
.eq("is_current", 1); // 当前激活版本
|
|
||||||
|
|
||||||
if (materialType != null && !materialType.isEmpty()) {
|
|
||||||
queryWrapper.eq("material_type", materialType);
|
|
||||||
} else {
|
|
||||||
// 如果未指定材料类型,优先匹配无材料类型的通用模型,或者返回任意匹配(兼容旧逻辑)
|
|
||||||
// 这里为了稳健,如果 materialType 为 null,暂不添加 material_type 的过滤条件,或者显式匹配 null
|
|
||||||
// 建议:如果 materialType 为空,尝试匹配 material_type is null or ''
|
|
||||||
// queryWrapper.and(w -> w.isNull("material_type").or().eq("material_type", ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
List<AlgorithmModel> models = list(queryWrapper);
|
|
||||||
if (models != null && !models.isEmpty()) {
|
|
||||||
AlgorithmModel model = models.get(0);
|
|
||||||
log.debug("Found model: {}", model.getModelPath());
|
|
||||||
return model.getModelPath();
|
|
||||||
} else {
|
|
||||||
log.warn("Model not found in database for algorithmType: {}, deviceType: {}, materialType: {}", algorithmType, deviceType, materialType);
|
log.warn("Model not found in database for algorithmType: {}, deviceType: {}, materialType: {}", algorithmType, deviceType, materialType);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
log.debug("Found model: {}", model.getModelPath());
|
||||||
|
return model.getModelPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AlgorithmModel getCurrentModel(String algorithmType, String deviceType, String materialType) {
|
public AlgorithmModel getCurrentModel(String algorithmType, String deviceType, String materialType) {
|
||||||
log.debug("Querying current model for algorithmType: {}, deviceType: {}, materialType: {}", algorithmType, deviceType, materialType);
|
log.debug("Querying current model for algorithmType: {}, deviceType: {}, materialType: {}", algorithmType, deviceType, materialType);
|
||||||
QueryWrapper<AlgorithmModel> queryWrapper = new QueryWrapper<>();
|
String mt = materialType == null ? null : materialType.trim();
|
||||||
queryWrapper.eq("algorithm_type", algorithmType)
|
if (mt != null && mt.isEmpty()) mt = null;
|
||||||
.eq("device_type", deviceType)
|
|
||||||
.eq("is_current", 1); // 当前激活版本
|
QueryWrapper<AlgorithmModel> qw = new QueryWrapper<>();
|
||||||
|
qw.eq("algorithm_type", algorithmType)
|
||||||
if (materialType != null && !materialType.isEmpty()) {
|
.eq("device_type", deviceType)
|
||||||
queryWrapper.eq("material_type", materialType);
|
.eq("is_current", 1);
|
||||||
}
|
if (mt != null) {
|
||||||
|
qw.eq("material_type", mt);
|
||||||
List<AlgorithmModel> models = list(queryWrapper);
|
|
||||||
if (models != null && !models.isEmpty()) {
|
|
||||||
return models.get(0);
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
qw.and(w -> w.isNull("material_type").or().eq("material_type", ""));
|
||||||
}
|
}
|
||||||
|
List<AlgorithmModel> models = list(qw);
|
||||||
|
if (models != null && !models.isEmpty()) return models.get(0);
|
||||||
|
|
||||||
|
if (mt != null) {
|
||||||
|
QueryWrapper<AlgorithmModel> fallback = new QueryWrapper<>();
|
||||||
|
fallback.eq("algorithm_type", algorithmType)
|
||||||
|
.eq("device_type", deviceType)
|
||||||
|
.eq("is_current", 1)
|
||||||
|
.and(w -> w.isNull("material_type").or().eq("material_type", ""));
|
||||||
|
List<AlgorithmModel> generic = list(fallback);
|
||||||
|
if (generic != null && !generic.isEmpty()) return generic.get(0);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user