From 95c3030b91faffb148f0975fda83be4693e94d6d Mon Sep 17 00:00:00 2001 From: lixiaolong <702156524@qq.com> Date: Wed, 16 Sep 2026 17:26:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(ti):=20=E5=AE=8C=E5=96=84=E8=A7=A6?= =?UTF-8?q?=E6=8E=A7=E4=BA=A4=E4=BA=92=E9=A1=B9=E7=9B=AE=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E4=B8=8E=E8=AF=84=E4=BC=B0=E9=97=AD=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 接入真实流程事件,记录检测结果、录像及执行统计。 区分 VI/TI 运行模式,支持执行实例查询和简易报告,并补充单元测试及投标建设方案。 --- .../controller/ti/TiProjectController.java | 12 +- .../evaluation/model/vo/AeViEvaluationVO.java | 25 ++ .../dispatcher/FlowEndNodeHandler.java | 2 +- .../runtime/engine/FlowTaskRuntimeEntry.java | 9 +- .../event/FlowExecutionModuleCodes.java | 10 + .../test/model/vo/TeTaskExecuteProjectVO.java | 7 + cmvr-iot-ti/pom.xml | 9 +- .../ti/listener/TiFlowExecutionListener.java | 35 ++ .../cmvr/ti/service/ITiProjectService.java | 4 +- .../ti/service/impl/TiProjectServiceImpl.java | 228 +++++----- .../cmvr/ti/support/TiEvaluationRecorder.java | 413 ++++++++++++++++++ .../ti/support/TiEvaluationRecorderTest.java | 193 ++++++++ .../vi/service/impl/ViProjectServiceImpl.java | 5 +- ...™º能座舱触控交互视频检测平台投标建设方案.md | 350 +++++++++++++++ 14 files changed, 1171 insertions(+), 131 deletions(-) create mode 100644 cmvr-iot-ti/src/main/java/com/cmvr/ti/listener/TiFlowExecutionListener.java create mode 100644 cmvr-iot-ti/src/main/java/com/cmvr/ti/support/TiEvaluationRecorder.java create mode 100644 cmvr-iot-ti/src/test/java/com/cmvr/ti/support/TiEvaluationRecorderTest.java create mode 100644 doc/智能座舱触控交互视频检测平台投标建设方案.md diff --git a/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/ti/TiProjectController.java b/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/ti/TiProjectController.java index 1383675..4f446e6 100644 --- a/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/ti/TiProjectController.java +++ b/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/ti/TiProjectController.java @@ -85,16 +85,10 @@ public class TiProjectController extends BaseController { return AjaxResult.ok(tiProjectService.queryExecuteInstId(projectId)); } - @ApiOperation("临时执行项目评估") - @PostMapping("/evaluation") - public AjaxResult evaluation(@RequestBody List funcIds) { - tiProjectService.executeTiEvaluation(funcIds); - return AjaxResult.ok(); - } - @ApiOperation("评估查询") @GetMapping("/evaluation/{projectId}") - public AjaxResult queryEvaluation(@PathVariable("projectId") String projectId) { - return AjaxResult.ok(tiProjectService.queryEvaluation(projectId)); + public AjaxResult queryEvaluation(@PathVariable("projectId") String projectId, + @RequestParam(value = "instId", required = false) String instId) { + return AjaxResult.ok(tiProjectService.queryEvaluation(projectId, instId)); } } diff --git a/cmvr-iot-evaluation/src/main/java/com/cmvr/evaluation/model/vo/AeViEvaluationVO.java b/cmvr-iot-evaluation/src/main/java/com/cmvr/evaluation/model/vo/AeViEvaluationVO.java index c5f4e39..39d661f 100644 --- a/cmvr-iot-evaluation/src/main/java/com/cmvr/evaluation/model/vo/AeViEvaluationVO.java +++ b/cmvr-iot-evaluation/src/main/java/com/cmvr/evaluation/model/vo/AeViEvaluationVO.java @@ -6,11 +6,36 @@ import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.util.List; +import java.util.Date; @Data @ApiModel("语音交互评估") public class AeViEvaluationVO { + @ApiModelProperty(value = "项目ID") + private String projectId; + + @ApiModelProperty(value = "流程执行实例ID") + private String instId; + + @ApiModelProperty(value = "执行状态") + private String executionStatus; + + @ApiModelProperty(value = "执行开始时间") + private Date startTime; + + @ApiModelProperty(value = "执行结束/最后更新时间") + private Date endTime; + + @ApiModelProperty(value = "测试项总数") + private Integer totalCount; + + @ApiModelProperty(value = "通过数") + private Integer successCount; + + @ApiModelProperty(value = "失败数") + private Integer failureCount; + @ApiModelProperty(value = "方案ID") private String schemeId; diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/dispatcher/FlowEndNodeHandler.java b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/dispatcher/FlowEndNodeHandler.java index ab24237..c624271 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/dispatcher/FlowEndNodeHandler.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/dispatcher/FlowEndNodeHandler.java @@ -54,7 +54,7 @@ public class FlowEndNodeHandler implements FlowNodeTypeHandler { jsonObject.put("instId", instId); jsonObject.put("taskId", taskId); jsonObject.put("itemId", itemId); - jsonObject.put("aeType", RunModeEnum.VI_PROJECT.name()); + jsonObject.put("aeType", runMode == null ? RunModeEnum.VI_PROJECT.name() : runMode.name()); executor.submit(() -> { Thread.currentThread().setName("evaluation-thread-" + Thread.currentThread().getId()); diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/engine/FlowTaskRuntimeEntry.java b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/engine/FlowTaskRuntimeEntry.java index 7711ed9..d7a839f 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/engine/FlowTaskRuntimeEntry.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/engine/FlowTaskRuntimeEntry.java @@ -163,13 +163,18 @@ public class FlowTaskRuntimeEntry implements FlowTaskRuntimeService { @Transactional(rollbackFor = Exception.class) public String executeProjectTask(TeTaskExecuteProjectVO taskExecuteProjectVO) { + RunModeEnum runMode = taskExecuteProjectVO.getRunMode() == null + ? RunModeEnum.VI_PROJECT : taskExecuteProjectVO.getRunMode(); + if (runMode != RunModeEnum.VI_PROJECT && runMode != RunModeEnum.TI_PROJECT) { + throw new GlobalException("项目执行仅支持语音交互或触控交互模式"); + } ResolvedRuntimeResources resources = resolveResources( taskExecuteProjectVO.getRobotId(), taskExecuteProjectVO.getRuntimeAssignments()); return executeTaskInternal( resources, taskExecuteProjectVO.getProjectId(), - null, - RunModeEnum.VI_PROJECT, + taskExecuteProjectVO.getModuleCode(), + runMode, taskExecuteProjectVO.getRunParams(), taskExecuteProjectVO ); diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/event/FlowExecutionModuleCodes.java b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/event/FlowExecutionModuleCodes.java index edef23b..544ea54 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/event/FlowExecutionModuleCodes.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/flow/runtime/event/FlowExecutionModuleCodes.java @@ -17,6 +17,16 @@ public final class FlowExecutionModuleCodes { */ public static final String AIMA = "AIMA"; + /** + * 语音交互项目模块 + */ + public static final String VI = "VI"; + + /** + * 触控交互项目模块 + */ + public static final String TI = "TI"; + private FlowExecutionModuleCodes() { // 防止实例化 } diff --git a/cmvr-iot-test/src/main/java/com/cmvr/test/model/vo/TeTaskExecuteProjectVO.java b/cmvr-iot-test/src/main/java/com/cmvr/test/model/vo/TeTaskExecuteProjectVO.java index f896c90..fa3fab4 100644 --- a/cmvr-iot-test/src/main/java/com/cmvr/test/model/vo/TeTaskExecuteProjectVO.java +++ b/cmvr-iot-test/src/main/java/com/cmvr/test/model/vo/TeTaskExecuteProjectVO.java @@ -1,6 +1,7 @@ package com.cmvr.test.model.vo; import com.alibaba.fastjson2.JSONObject; +import com.cmvr.test.enums.RunModeEnum; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; @@ -26,4 +27,10 @@ public class TeTaskExecuteProjectVO { @ApiModelProperty(value = "运行时参数") private JSONObject runParams = new JSONObject(); + + @ApiModelProperty(value = "项目运行模式,由项目服务端设置", hidden = true) + private RunModeEnum runMode; + + @ApiModelProperty(value = "流程事件模块编码,由项目服务端设置", hidden = true) + private String moduleCode; } diff --git a/cmvr-iot-ti/pom.xml b/cmvr-iot-ti/pom.xml index 452f5be..53985e3 100644 --- a/cmvr-iot-ti/pom.xml +++ b/cmvr-iot-ti/pom.xml @@ -33,6 +33,13 @@ com.cmvr cmvr-iot-evaluation + + + junit + junit + 4.13.2 + test + - \ No newline at end of file + diff --git a/cmvr-iot-ti/src/main/java/com/cmvr/ti/listener/TiFlowExecutionListener.java b/cmvr-iot-ti/src/main/java/com/cmvr/ti/listener/TiFlowExecutionListener.java new file mode 100644 index 0000000..038e76b --- /dev/null +++ b/cmvr-iot-ti/src/main/java/com/cmvr/ti/listener/TiFlowExecutionListener.java @@ -0,0 +1,35 @@ +package com.cmvr.ti.listener; + +import com.cmvr.test.flow.runtime.event.FlowExecutionEvent; +import com.cmvr.test.flow.runtime.event.FlowExecutionListener; +import com.cmvr.test.flow.runtime.event.FlowExecutionModuleCodes; +import com.cmvr.ti.support.TiEvaluationRecorder; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** Records touch-project workflow events for status and simple reporting. */ +@Slf4j +@Component +@RequiredArgsConstructor +public class TiFlowExecutionListener implements FlowExecutionListener { + + private final TiEvaluationRecorder evaluationRecorder; + + @Override + public void onEvent(FlowExecutionEvent event) { + try { + evaluationRecorder.record(event); + } catch (RuntimeException exception) { + // Reporting must not interfere with robot execution or device cleanup. + log.error("触控项目执行结果记录失败: instId={}, eventType={}", + event == null ? null : event.getInstId(), + event == null ? null : event.getEventType(), exception); + } + } + + @Override + public String getModuleCode() { + return FlowExecutionModuleCodes.TI; + } +} diff --git a/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/ITiProjectService.java b/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/ITiProjectService.java index e863f78..e7057b4 100644 --- a/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/ITiProjectService.java +++ b/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/ITiProjectService.java @@ -30,7 +30,7 @@ public interface ITiProjectService extends IService { public boolean updateProjectStatus(String projectId, Integer status); - public void executeTiEvaluation(List funcIds); - public AeViEvaluationVO queryEvaluation(String projectId); + + public AeViEvaluationVO queryEvaluation(String projectId, String instId); } diff --git a/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/impl/TiProjectServiceImpl.java b/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/impl/TiProjectServiceImpl.java index 0940965..0dcaa85 100644 --- a/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/impl/TiProjectServiceImpl.java +++ b/cmvr-iot-ti/src/main/java/com/cmvr/ti/service/impl/TiProjectServiceImpl.java @@ -1,9 +1,7 @@ package com.cmvr.ti.service.impl; -import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import com.alibaba.fastjson2.JSON; @@ -11,7 +9,6 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.spring.service.impl.ServiceImpl; -import com.cmvr.common.core.domain.entity.SysDictData; import com.cmvr.common.exception.GlobalException; import com.cmvr.evaluation.model.domain.AeEvaluation; import com.cmvr.evaluation.model.domain.AeIndicatorEntity; @@ -19,10 +16,10 @@ import com.cmvr.evaluation.model.dto.AeIResultDTO; import com.cmvr.evaluation.model.vo.AeViEvaluationVO; import com.cmvr.evaluation.service.AeIndicatorService; import com.cmvr.evaluation.service.IAeEvaluationService; -import com.cmvr.system.service.ISysDictTypeService; import com.cmvr.test.enums.FlowSceneTypeEnum; import com.cmvr.test.enums.RunModeEnum; import com.cmvr.test.flow.runtime.engine.FlowTaskRuntimeService; +import com.cmvr.test.flow.runtime.event.FlowExecutionModuleCodes; import com.cmvr.test.model.domain.TeDetectionItem; import com.cmvr.test.model.domain.TeTaskConfigInfo; import com.cmvr.test.model.domain.TeTaskInst; @@ -31,14 +28,15 @@ import com.cmvr.test.model.vo.TeTaskExecuteProjectVO; import com.cmvr.test.model.vo.TeTaskOrchestraVO; import com.cmvr.test.service.ITeDetectionItemService; import com.cmvr.test.service.ITeTaskConfigInfoService; +import com.cmvr.test.service.ITeTaskInstService; import com.cmvr.test.service.ITeTaskOrchestrationService; import com.cmvr.ti.mapper.TiProjectMapper; import com.cmvr.ti.model.domain.TiProject; import com.cmvr.ti.model.domain.TiVehicleFunction; import com.cmvr.ti.service.ITiProjectService; import com.cmvr.ti.service.ITiVehicleFunctionService; +import com.cmvr.ti.support.TiEvaluationRecorder; import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -46,11 +44,7 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -59,13 +53,12 @@ public class TiProjectServiceImpl extends ServiceImpl() -// .eq(TiProject::getProjectId, taskExecuteProjectVO.getProjectId()) -// .set(TiProject::getStatus, "0") -// ); + normalVO.setRuntimeAssignments(taskExecuteProjectVO.getRuntimeAssignments()); + normalVO.setRunMode(RunModeEnum.TI_PROJECT); + normalVO.setModuleCode(FlowExecutionModuleCodes.TI); + updateProjectStatus(taskExecuteProjectVO.getProjectId(), 1); + String instId; + try { + instId = flowTaskRuntimeService.executeProjectTask(normalVO); + } catch (RuntimeException exception) { + updateProjectStatus(taskExecuteProjectVO.getProjectId(), 0); + throw exception; + } + evaluationRecorder.initialize(taskExecuteProjectVO.getProjectId(), instId); return instId; } @Override public TeTaskInst getTeTaskInst(String projectId) { - return null; + TiProject project = getById(projectId); + if (project == null) { + throw new GlobalException("触控项目不存在:" + projectId); + } + return taskInstService.getOne( + Wrappers.lambdaQuery(TeTaskInst.class) + .eq(TeTaskInst::getTaskId, projectId) + .eq(TeTaskInst::getRunMode, RunModeEnum.TI_PROJECT.name()) + .orderByDesc(TeTaskInst::getCreateTime) + .last("LIMIT 1") + ); } @Override public String queryExecuteInstId(String projectId) { - return ""; + TeTaskInst taskInst = getTeTaskInst(projectId); + if (taskInst == null) { + throw new GlobalException("触控项目尚未执行:" + projectId); + } + return taskInst.getId(); } @Override @@ -167,136 +183,115 @@ public class TiProjectServiceImpl extends ServiceImpl 音频地址 - private final Map AUDIO_PATH_MAP = new HashMap() {{ - put("8", "http://192.168.0.100:9000/cmvr-iot/AUDIO/20260310/1773108837127.wav"); // 播放音乐 - put("12", "http://192.168.0.100:9000/cmvr-iot/AUDIO/20260310/1773108916488.wav"); // 打开导航 - put("9", "http://192.168.0.100:9000/cmvr-iot/AUDIO/20260310/1773108933968.wav"); // 能量回收 - }}; - - // 功能ID -> 视频地址 - private final Map VIDEO_PATH_MAP = new HashMap() {{ - put("8", "http://192.168.0.100:9000/cmvr-iot/VIDEO/20260310/1773108955700.mp4"); // 播放音乐 - put("12", "http://192.168.0.100:9000/cmvr-iot/VIDEO/20260310/1773108967164.mp4"); // 打开导航 - put("9", "http://192.168.0.100:9000/cmvr-iot/VIDEO/20260310/1773108978124.mp4"); // 能量回收 - }}; - @Override - public void executeTiEvaluation(List funcIds) { - AeEvaluation aeEvaluation = new AeEvaluation(); - aeEvaluation.setAeType(RunModeEnum.TI_PROJECT.name()); - aeEvaluation.setInstId(IdUtil.fastSimpleUUID()); - aeEvaluation.setTaskId("aadb21a32e7990456b866e844a7d4a54"); - aeEvaluation.setItemId(IdUtil.fastSimpleUUID()); - - // todo 触控交互评估 - String taskId = aeEvaluation.getTaskId(); - updateProjectStatus(taskId, 1); // 执行中 - // 模拟流程运行 -// try { -// Thread.sleep(30000); -// } catch (InterruptedException e) { -// throw new RuntimeException(e); -// } - - - // 查字典 - List tiFunctionConfig = dictTypeService.selectDictDataByType("ti_function_config"); - Map functionMap = tiFunctionConfig.stream() - .collect(Collectors.toMap( - SysDictData::getDictValue, - SysDictData::getDictLabel - )); - - // 构建评估任务列表 - List evaluations = new LinkedList<>(); - for (Long funcId : funcIds) { - TiVehicleFunction byId = vehicleFunctionService.getById(funcId); - String funcName = functionMap.get(byId.getFuncKey()); - AeEvaluation eval = new AeEvaluation(); - BeanUtil.copyProperties(aeEvaluation, eval); - eval.setAeId(IdUtil.fastSimpleUUID()); - eval.setStatus(0); // 待评估 - eval.setAvContent(funcName); - eval.setAudioPath(AUDIO_PATH_MAP.get(byId.getFuncKey())); - eval.setVideoPath(VIDEO_PATH_MAP.get(byId.getFuncKey())); - evaluations.add(eval); - } - - try { - // 入库 - aeEvaluationService.saveBatch(evaluations); - - // 提交评估 - for (AeEvaluation e : evaluations) { - redisTemplate.opsForList() - .rightPush("ae:evaluation:queue", JSON.toJSONString(e)); - } - } catch (Exception e) { - throw new RuntimeException(e); - } + public AeViEvaluationVO queryEvaluation(String projectId) { + return queryEvaluation(projectId, null); } @Override - public AeViEvaluationVO queryEvaluation(String projectId) { - // 1. 查询项目 + public AeViEvaluationVO queryEvaluation(String projectId, String instId) { TiProject project = getById(projectId); - - // 2. 查询评估数据(允许不完整) - List evaluations = aeEvaluationService.queryEvaluationByProjectId(projectId); - if (CollUtil.isEmpty(evaluations)) { - throw new GlobalException(StrUtil.format("暂无项目id为{}的数据", projectId)); + if (project == null) { + throw new GlobalException("触控项目不存在:" + projectId); } - // 3. 获取指标定义 - List indicators = - aeIndicatorService.getChildrenByParentIdWithLevelLimit( - project.getIndicatorId(), 4, 1 - ); + TeTaskInst taskInst; + if (StrUtil.isBlank(instId)) { + taskInst = getTeTaskInst(projectId); + } else { + taskInst = taskInstService.getById(instId); + if (taskInst != null && (!StrUtil.equals(projectId, taskInst.getTaskId()) + || !StrUtil.equals(RunModeEnum.TI_PROJECT.name(), taskInst.getRunMode()))) { + throw new GlobalException("执行实例不属于当前触控项目:" + instId); + } + } + if (taskInst == null) { + throw new GlobalException("触控项目尚未执行:" + projectId); + } + + List evaluations = aeEvaluationService.queryEvaluation(taskInst.getId()); + if (CollUtil.isEmpty(evaluations)) { + throw new GlobalException(StrUtil.format("项目 {} 的执行实例 {} 暂无结果数据", + projectId, taskInst.getId())); + } + + List indicators = project.getIndicatorId() == null + ? Collections.emptyList() + : aeIndicatorService.getChildrenByParentIdWithLevelLimit(project.getIndicatorId(), 4, 1); List itemDetails = new ArrayList<>(); - + int successCount = 0; int failureCount = 0; + long responseTimeSum = 0L; + int responseTimeCount = 0; for (AeEvaluation evaluation : evaluations) { AeViEvaluationVO.AeItemDetail itemDto = new AeViEvaluationVO.AeItemDetail(); itemDto.setAeId(evaluation.getAeId()); itemDto.setContent(evaluation.getAvContent()); - AeIResultDTO dto = JSON.parseObject(evaluation.getResult(), AeIResultDTO.class); + AeIResultDTO dto = parseEvaluationResult(evaluation.getResult()); itemDto.setItemResponseTime(dto.getResponseTime()); itemDto.setStatus(evaluation.getStatus()); - boolean success = "成功".equals(dto.getOverallResult()); - - if (!success) { - itemDto.setFailureReason(dto.getFailureReason()); + if (evaluation.getStatus() != null && evaluation.getStatus() == 2) { + successCount++; + } else if (evaluation.getStatus() != null && evaluation.getStatus() >= 3) { failureCount++; } + if (dto.getResponseTime() != null) { + responseTimeSum += dto.getResponseTime(); + responseTimeCount++; + } - itemDto.setVideoPath(dto.getVideoPath()); + itemDto.setFailureReason(dto.getFailureReason()); + itemDto.setVideoPath(StrUtil.blankToDefault(dto.getVideoPath(), evaluation.getVideoPath())); itemDto.setExecutionDetail(dto.getExecutionDetail()); - itemDetails.add(itemDto); } - // 计算成功率 - Long successRate = (long) (((evaluations.size() - failureCount) * 100.0) / evaluations.size()); - - //成功率得分 + Long successRate = evaluations.isEmpty() ? 0L : successCount * 100L / evaluations.size(); Long score = calculateSuccessRateScore(successRate, indicators); - AeViEvaluationVO vo = new AeViEvaluationVO(); + vo.setProjectId(projectId); + vo.setInstId(taskInst.getId()); + vo.setExecutionStatus(taskInst.getStatus()); + vo.setStartTime(taskInst.getCreateTime()); + vo.setEndTime(taskInst.getUpdateTime()); + vo.setSchemeId(taskInst.getId()); + vo.setSchemeName(project.getProjectName()); + vo.setTotalCount(evaluations.size()); + vo.setSuccessCount(successCount); + vo.setFailureCount(failureCount); + vo.setResponseTime(responseTimeCount == 0 ? null : responseTimeSum / responseTimeCount); vo.setItemDetails(itemDetails); vo.setSuccessRate(successRate); vo.setSuccessRateScore(score); return vo; } + private AeIResultDTO parseEvaluationResult(String resultJson) { + if (StrUtil.isBlank(resultJson)) { + return new AeIResultDTO(); + } + try { + AeIResultDTO result = JSON.parseObject(resultJson, AeIResultDTO.class); + return result == null ? new AeIResultDTO() : result; + } catch (RuntimeException exception) { + AeIResultDTO result = new AeIResultDTO(); + result.setFailureReason("结果数据格式异常"); + result.setExecutionDetail(StrUtil.sub(resultJson, 0, 500)); + return result; + } + } + private Long calculateSuccessRateScore(Long successRate, List indicators) { + if (CollUtil.isEmpty(indicators)) { + return 0L; + } // 查找成功率指标 AeIndicatorEntity successRateIndicator = findIndicatorByName(indicators, "成功率"); - if (successRateIndicator == null) { + if (successRateIndicator == null || CollUtil.isEmpty(successRateIndicator.getChildren())) { return 0L; // 如果没有匹配的成功率指标,返回 0 分 } List children = successRateIndicator.getChildren(); @@ -318,8 +313,11 @@ public class TiProjectServiceImpl extends ServiceImpl indicators, String indicatorName) { + if (CollUtil.isEmpty(indicators)) { + return null; + } for (AeIndicatorEntity indicator : indicators) { - if (indicator.getName().equals(indicatorName)) { + if (indicator != null && StrUtil.equals(indicator.getName(), indicatorName)) { return indicator; } } diff --git a/cmvr-iot-ti/src/main/java/com/cmvr/ti/support/TiEvaluationRecorder.java b/cmvr-iot-ti/src/main/java/com/cmvr/ti/support/TiEvaluationRecorder.java new file mode 100644 index 0000000..769bf2e --- /dev/null +++ b/cmvr-iot-ti/src/main/java/com/cmvr/ti/support/TiEvaluationRecorder.java @@ -0,0 +1,413 @@ +package com.cmvr.ti.support; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.cmvr.common.core.domain.entity.SysDictData; +import com.cmvr.evaluation.model.domain.AeEvaluation; +import com.cmvr.evaluation.model.dto.AeIResultDTO; +import com.cmvr.evaluation.service.IAeEvaluationService; +import com.cmvr.system.service.ISysDictTypeService; +import com.cmvr.test.enums.ActionEnum; +import com.cmvr.test.enums.RunModeEnum; +import com.cmvr.test.flow.runtime.event.FlowExecutionEvent; +import com.cmvr.test.flow.runtime.operator.FlowMediaParamResolver; +import com.cmvr.ti.mapper.TiProjectMapper; +import com.cmvr.ti.model.domain.TiProject; +import com.cmvr.ti.model.domain.TiVehicleFunction; +import com.cmvr.ti.service.ITiVehicleFunctionService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 将触控流程的真实执行事件整理为项目内的简易报告数据。 + * + *

该组件不调用模型评估,也不制造演示音视频。结果只来自流程节点状态、 + * 边端返回以及相机停止录像节点产生的真实 videoUrl。

+ */ +@Slf4j +@Component +@RequiredArgsConstructor +public class TiEvaluationRecorder { + + private static final int STATUS_PENDING = 0; + private static final int STATUS_RUNNING = 1; + private static final int STATUS_SUCCESS = 2; + private static final int STATUS_FAILED = 3; + + private final TiProjectMapper projectMapper; + private final ITiVehicleFunctionService vehicleFunctionService; + private final ISysDictTypeService dictTypeService; + private final IAeEvaluationService evaluationService; + + private final ConcurrentMap initializationLocks = new ConcurrentHashMap<>(); + private final ConcurrentMap startedAt = new ConcurrentHashMap<>(); + private final ConcurrentMap latestVideoUrls = new ConcurrentHashMap<>(); + private final ConcurrentMap businessFailures = new ConcurrentHashMap<>(); + private final ConcurrentMap> executionDetails = new ConcurrentHashMap<>(); + + /** Creates one report row for each function selected by the touch project. */ + public void initialize(String projectId, String instId) { + if (StrUtil.isBlank(projectId) || StrUtil.isBlank(instId)) { + return; + } + Object lock = initializationLocks.computeIfAbsent(instId, key -> new Object()); + synchronized (lock) { + try { + if (CollUtil.isNotEmpty(evaluationService.queryEvaluation(instId))) { + return; + } + TiProject project = projectMapper.selectById(projectId); + if (project == null) { + log.warn("触控执行结果初始化失败,项目不存在: projectId={}, instId={}", projectId, instId); + return; + } + + List functionIds = parseFunctionIds(project.getTestFuncIds()); + List functionList = functionIds.isEmpty() + ? Collections.emptyList() : vehicleFunctionService.queryByIds(functionIds); + Map functions = functionList == null + ? Collections.emptyMap() + : functionList.stream().collect(Collectors.toMap( + TiVehicleFunction::getId, Function.identity(), (left, right) -> left)); + Map functionNames = loadFunctionNames(); + List evaluations = new ArrayList<>(); + + for (Long functionId : functionIds) { + TiVehicleFunction function = functions.get(functionId); + AeEvaluation evaluation = new AeEvaluation(); + evaluation.setAeId(IdUtil.fastSimpleUUID()); + evaluation.setAeType(RunModeEnum.TI_PROJECT.name()); + evaluation.setInstId(instId); + evaluation.setTaskId(projectId); + evaluation.setItemId(String.valueOf(functionId)); + evaluation.setAvContent(resolveFunctionName(functionId, function, functionNames)); + evaluation.setStatus(STATUS_PENDING); + evaluations.add(evaluation); + } + + if (evaluations.isEmpty()) { + AeEvaluation evaluation = new AeEvaluation(); + evaluation.setAeId(IdUtil.fastSimpleUUID()); + evaluation.setAeType(RunModeEnum.TI_PROJECT.name()); + evaluation.setInstId(instId); + evaluation.setTaskId(projectId); + evaluation.setItemId("project"); + evaluation.setAvContent("触控交互测试"); + evaluation.setStatus(STATUS_PENDING); + evaluations.add(evaluation); + } + evaluationService.saveBatch(evaluations); + startedAt.putIfAbsent(instanceKey(instId), System.currentTimeMillis()); + } finally { + initializationLocks.remove(instId, lock); + } + } + } + + public void record(FlowExecutionEvent event) { + if (event == null || StrUtil.isBlank(event.getInstId()) || StrUtil.isBlank(event.getTaskId())) { + return; + } + initialize(event.getTaskId(), event.getInstId()); + startedAt.putIfAbsent(instanceKey(event.getInstId()), System.currentTimeMillis()); + + switch (event.getEventType()) { + case NODE_STARTED: + updateProjectStatus(event.getTaskId(), "1"); + recordNodeEvent(event, "开始执行"); + markRunning(resolveEvaluation(event)); + break; + case NODE_COMPLETED: + recordNodeEvent(event, "执行完成"); + handleNodeCompleted(event); + break; + case NODE_FAILED: + recordNodeEvent(event, "执行失败:" + StrUtil.blankToDefault(event.getErrorMessage(), "未知异常")); + failEvaluation(resolveEvaluation(event), event, + StrUtil.blankToDefault(event.getErrorMessage(), "节点执行失败")); + break; + case TASK_COMPLETED: + String businessFailure = businessFailures.get(event.getInstId()); + finishPending(event, StrUtil.isBlank(businessFailure), businessFailure); + updateProjectStatus(event.getTaskId(), "2"); + clearRuntimeCache(event.getInstId()); + break; + case TASK_FAILED: + finishPending(event, false, + StrUtil.blankToDefault(event.getErrorMessage(), "任务执行失败")); + updateProjectStatus(event.getTaskId(), "2"); + clearRuntimeCache(event.getInstId()); + break; + case TASK_STOPPED: + finishPending(event, false, "任务已终止"); + updateProjectStatus(event.getTaskId(), "2"); + clearRuntimeCache(event.getInstId()); + break; + case TASK_PAUSED: + case TASK_RESUMED: + default: + break; + } + } + + private void handleNodeCompleted(FlowExecutionEvent event) { + AeEvaluation evaluation = resolveEvaluation(event); + String businessFailure = resolveBusinessFailure(event.getOutputParams()); + if (StrUtil.isNotBlank(businessFailure)) { + if (evaluation == null) { + businessFailures.putIfAbsent(event.getInstId(), businessFailure); + } else { + completeEvaluation(evaluation, event, false, businessFailure); + } + } + String videoUrl = FlowMediaParamResolver.lastUrl(event.getOutputParams(), "videoUrl"); + if (StrUtil.isNotBlank(videoUrl)) { + if (evaluation != null) { + evaluation.setVideoPath(videoUrl); + evaluationService.updateById(evaluation); + } else { + // A recording outside the function loop is evidence for the entire run. + latestVideoUrls.put(event.getInstId(), videoUrl); + } + } + if (StrUtil.isBlank(businessFailure) + && event.getAction() == ActionEnum.CAMERA_RECORDING_STOP && evaluation != null) { + completeEvaluation(evaluation, event, true, null); + } + } + + private void markRunning(AeEvaluation evaluation) { + if (evaluation == null || evaluation.getStatus() == null || evaluation.getStatus() != STATUS_PENDING) { + return; + } + evaluation.setStatus(STATUS_RUNNING); + evaluationService.updateById(evaluation); + startedAt.putIfAbsent(evaluationKey(evaluation), System.currentTimeMillis()); + } + + private void failEvaluation(AeEvaluation evaluation, FlowExecutionEvent event, String reason) { + if (evaluation != null) { + completeEvaluation(evaluation, event, false, reason); + } + } + + private void finishPending(FlowExecutionEvent event, boolean success, String failureReason) { + List evaluations = evaluationService.queryEvaluation(event.getInstId()); + for (AeEvaluation evaluation : evaluations) { + if (evaluation.getStatus() != null + && (evaluation.getStatus() == STATUS_SUCCESS || evaluation.getStatus() == STATUS_FAILED)) { + continue; + } + completeEvaluation(evaluation, event, success, failureReason); + } + } + + private void completeEvaluation(AeEvaluation evaluation, FlowExecutionEvent event, + boolean success, String failureReason) { + if (evaluation == null) { + return; + } + // A later successful task event must not hide an earlier node failure. + if (success && evaluation.getStatus() != null && evaluation.getStatus() == STATUS_FAILED) { + return; + } + + String videoUrl = StrUtil.blankToDefault(evaluation.getVideoPath(), + latestVideoUrls.get(event.getInstId())); + Long start = startedAt.get(evaluationKey(evaluation)); + if (start == null) { + start = startedAt.get(instanceKey(event.getInstId())); + } + + AeIResultDTO result = new AeIResultDTO(); + result.setOverallResult(success ? "成功" : "失败"); + result.setFailureReason(success ? null : StrUtil.blankToDefault(failureReason, "执行失败")); + result.setResponseTime(start == null ? null : Math.max(0L, System.currentTimeMillis() - start)); + result.setVideoPath(videoUrl); + result.setExecutionDetail(buildExecutionDetail(event.getInstId(), evaluation.getAeId())); + + evaluation.setStatus(success ? STATUS_SUCCESS : STATUS_FAILED); + evaluation.setVideoPath(videoUrl); + evaluation.setResult(JSON.toJSONString(result)); + evaluationService.updateById(evaluation); + } + + private AeEvaluation resolveEvaluation(FlowExecutionEvent event) { + List evaluations = evaluationService.queryEvaluation(event.getInstId()); + if (CollUtil.isEmpty(evaluations)) { + return null; + } + + String outputFunctionId = resolveOutputFunctionId(event.getOutputParams()); + if (StrUtil.isNotBlank(outputFunctionId)) { + AeEvaluation matched = findEvaluation(evaluations, outputFunctionId); + if (matched != null) { + return matched; + } + } + + TiProject project = projectMapper.selectById(event.getTaskId()); + List functionIds = project == null + ? Collections.emptyList() : parseFunctionIds(project.getTestFuncIds()); + if (CollUtil.isNotEmpty(event.getIterations())) { + int index = event.getIterations().get(0) - 1; + if (index >= 0 && index < functionIds.size()) { + AeEvaluation matched = findEvaluation(evaluations, String.valueOf(functionIds.get(index))); + if (matched != null) { + return matched; + } + } + } + return evaluations.size() == 1 ? evaluations.get(0) : null; + } + + private AeEvaluation findEvaluation(List evaluations, String functionId) { + return evaluations.stream() + .filter(item -> StrUtil.equals(item.getItemId(), functionId)) + .findFirst() + .orElse(null); + } + + private String resolveOutputFunctionId(JSONObject output) { + if (output == null) { + return null; + } + JSONObject source = output.getJSONObject("result"); + if (source == null) { + source = output; + } + String functionId = source.getString("functionId"); + if (StrUtil.isBlank(functionId)) { + functionId = source.getString("funcId"); + } + return functionId; + } + + private String resolveBusinessFailure(JSONObject output) { + if (output == null) { + return null; + } + JSONObject source = output.getJSONObject("result"); + if (source == null) { + source = output; + } + Object success = source.containsKey("success") ? source.get("success") + : source.containsKey("passed") ? source.get("passed") : source.get("pass"); + String overallResult = source.getString("overallResult"); + boolean explicitlyFailed = Boolean.FALSE.equals(success) + || (success instanceof String && "false".equalsIgnoreCase((String) success)) + || "失败".equals(overallResult) + || "不通过".equals(overallResult) + || "failed".equalsIgnoreCase(overallResult) + || "failure".equalsIgnoreCase(overallResult); + if (!explicitlyFailed) { + return null; + } + String reason = source.getString("failureReason"); + if (StrUtil.isBlank(reason)) { + reason = source.getString("errorMessage"); + } + if (StrUtil.isBlank(reason)) { + reason = source.getString("message"); + } + return StrUtil.blankToDefault(reason, "边端脚本返回失败"); + } + + private void recordNodeEvent(FlowExecutionEvent event, String statusText) { + AeEvaluation evaluation = resolveEvaluation(event); + String key = detailKey(event.getInstId(), evaluation == null ? "*" : evaluation.getAeId()); + String nodeName = StrUtil.blankToDefault(event.getNodeName(), event.getNodeId()); + String detail = StrUtil.blankToDefault(nodeName, "流程节点") + statusText; + executionDetails.computeIfAbsent(key, ignored -> new ConcurrentLinkedQueue<>()).add(detail); + } + + private String buildExecutionDetail(String instId, String evaluationId) { + List details = new ArrayList<>(); + details.addAll(executionDetails.getOrDefault(detailKey(instId, "*"), new ConcurrentLinkedQueue<>())); + details.addAll(executionDetails.getOrDefault(detailKey(instId, evaluationId), new ConcurrentLinkedQueue<>())); + if (details.isEmpty()) { + return "流程执行完成"; + } + int fromIndex = Math.max(0, details.size() - 20); + return StrUtil.sub(String.join(";", details.subList(fromIndex, details.size())), 0, 2000); + } + + private List parseFunctionIds(String json) { + if (StrUtil.isBlank(json)) { + return Collections.emptyList(); + } + try { + List ids = JSON.parseArray(json, Long.class); + return ids == null ? Collections.emptyList() : ids; + } catch (RuntimeException exception) { + log.warn("触控项目功能列表格式错误: {}", json); + return Collections.emptyList(); + } + } + + private Map loadFunctionNames() { + List dictData = dictTypeService.selectDictDataByType("ti_function_config"); + if (dictData == null) { + return Collections.emptyMap(); + } + return dictData.stream().collect(Collectors.toMap( + SysDictData::getDictValue, + SysDictData::getDictLabel, + (left, right) -> left, + LinkedHashMap::new + )); + } + + private String resolveFunctionName(Long functionId, TiVehicleFunction function, + Map functionNames) { + if (function == null) { + return "功能 " + functionId; + } + return functionNames.getOrDefault(function.getFuncKey(), + StrUtil.blankToDefault(function.getFuncKey(), "功能 " + functionId)); + } + + private void updateProjectStatus(String projectId, String status) { + TiProject project = projectMapper.selectById(projectId); + if (project == null || StrUtil.equals(project.getStatus(), status)) { + return; + } + project.setStatus(status); + projectMapper.updateById(project); + } + + private void clearRuntimeCache(String instId) { + startedAt.keySet().removeIf(key -> key.startsWith(instId + "|")); + executionDetails.keySet().removeIf(key -> key.startsWith(instId + "|")); + latestVideoUrls.remove(instId); + businessFailures.remove(instId); + initializationLocks.remove(instId); + } + + private String evaluationKey(AeEvaluation evaluation) { + return evaluation.getInstId() + "|eval|" + evaluation.getAeId(); + } + + private String instanceKey(String instId) { + return instId + "|instance"; + } + + private String detailKey(String instId, String evaluationId) { + return instId + "|detail|" + evaluationId; + } +} diff --git a/cmvr-iot-ti/src/test/java/com/cmvr/ti/support/TiEvaluationRecorderTest.java b/cmvr-iot-ti/src/test/java/com/cmvr/ti/support/TiEvaluationRecorderTest.java new file mode 100644 index 0000000..6b21c39 --- /dev/null +++ b/cmvr-iot-ti/src/test/java/com/cmvr/ti/support/TiEvaluationRecorderTest.java @@ -0,0 +1,193 @@ +package com.cmvr.ti.support; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.cmvr.common.core.domain.entity.SysDictData; +import com.cmvr.evaluation.model.domain.AeEvaluation; +import com.cmvr.evaluation.model.dto.AeIResultDTO; +import com.cmvr.evaluation.service.IAeEvaluationService; +import com.cmvr.system.service.ISysDictTypeService; +import com.cmvr.test.enums.ActionEnum; +import com.cmvr.test.flow.runtime.event.FlowExecutionEvent; +import com.cmvr.ti.mapper.TiProjectMapper; +import com.cmvr.ti.model.domain.TiProject; +import com.cmvr.ti.model.domain.TiVehicleFunction; +import com.cmvr.ti.service.ITiVehicleFunctionService; +import org.junit.Test; + +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class TiEvaluationRecorderTest { + + @Test + public void recordsActualVideoAndSuccessfulFunctionResult() { + Fixture fixture = fixture("[8]"); + + fixture.recorder.initialize("project-1", "inst-1"); + fixture.recorder.record(nodeEvent(FlowExecutionEvent.EventType.NODE_STARTED, + ActionEnum.TOUCH, null, List.of(1), null)); + fixture.recorder.record(nodeEvent(FlowExecutionEvent.EventType.NODE_COMPLETED, + ActionEnum.CAMERA_RECORDING_STOP, + new JSONObject().fluentPut("videoUrl", List.of("http://files/touch-1.mp4")), + List.of(1), null)); + fixture.recorder.record(taskEvent(FlowExecutionEvent.EventType.TASK_COMPLETED, null)); + + assertEquals(1, fixture.evaluations.size()); + AeEvaluation evaluation = fixture.evaluations.get(0); + assertEquals(Integer.valueOf(2), evaluation.getStatus()); + assertEquals("http://files/touch-1.mp4", evaluation.getVideoPath()); + AeIResultDTO result = JSON.parseObject(evaluation.getResult(), AeIResultDTO.class); + assertEquals("成功", result.getOverallResult()); + assertEquals("http://files/touch-1.mp4", result.getVideoPath()); + assertTrue(result.getExecutionDetail().contains("结束录像执行完成")); + assertEquals("2", fixture.project.getStatus()); + } + + @Test + public void doesNotReusePreviousFunctionVideoWhenNextFunctionFails() { + Fixture fixture = fixture("[8,9]"); + + fixture.recorder.initialize("project-1", "inst-1"); + fixture.recorder.record(nodeEvent(FlowExecutionEvent.EventType.NODE_COMPLETED, + ActionEnum.CAMERA_RECORDING_STOP, + new JSONObject().fluentPut("videoUrl", List.of("http://files/first.mp4")), + List.of(1), null)); + fixture.recorder.record(taskEvent(FlowExecutionEvent.EventType.TASK_FAILED, "机械臂执行失败")); + + AeEvaluation first = findByItemId(fixture.evaluations, "8"); + AeEvaluation second = findByItemId(fixture.evaluations, "9"); + assertEquals(Integer.valueOf(2), first.getStatus()); + assertEquals("http://files/first.mp4", first.getVideoPath()); + assertEquals(Integer.valueOf(3), second.getStatus()); + assertNull(second.getVideoPath()); + AeIResultDTO failure = JSON.parseObject(second.getResult(), AeIResultDTO.class); + assertEquals("机械臂执行失败", failure.getFailureReason()); + } + + @Test + public void treatsHttpBusinessFailureAsFailedResult() { + Fixture fixture = fixture("[8]"); + JSONObject edgeResult = new JSONObject() + .fluentPut("success", false) + .fluentPut("failureReason", "未识别到目标控件"); + + fixture.recorder.record(nodeEvent(FlowExecutionEvent.EventType.NODE_COMPLETED, + ActionEnum.HTTP, new JSONObject().fluentPut("result", edgeResult), + List.of(1), null)); + fixture.recorder.record(taskEvent(FlowExecutionEvent.EventType.TASK_COMPLETED, null)); + + AeEvaluation evaluation = fixture.evaluations.get(0); + assertEquals(Integer.valueOf(3), evaluation.getStatus()); + AeIResultDTO result = JSON.parseObject(evaluation.getResult(), AeIResultDTO.class); + assertEquals("失败", result.getOverallResult()); + assertEquals("未识别到目标控件", result.getFailureReason()); + } + + private Fixture fixture(String testFunctionIds) { + TiProject project = new TiProject(); + project.setProjectId("project-1"); + project.setTestFuncIds(testFunctionIds); + project.setStatus("0"); + + List evaluations = new ArrayList<>(); + TiProjectMapper projectMapper = proxy(TiProjectMapper.class, (method, args) -> { + if ("selectById".equals(method)) return project; + if ("updateById".equals(method)) return 1; + return defaultValue(method); + }); + ITiVehicleFunctionService functions = proxy(ITiVehicleFunctionService.class, (method, args) -> { + if ("queryByIds".equals(method)) { + Collection ids = (Collection) args[0]; + return ids.stream().map(id -> { + TiVehicleFunction function = new TiVehicleFunction(); + function.setId(Long.valueOf(String.valueOf(id))); + function.setFuncKey(String.valueOf(id)); + return function; + }).toList(); + } + return defaultValue(method); + }); + ISysDictTypeService dictionaries = proxy(ISysDictTypeService.class, (method, args) -> { + if ("selectDictDataByType".equals(method)) { + SysDictData first = new SysDictData(); + first.setDictValue("8"); + first.setDictLabel("播放音乐"); + SysDictData second = new SysDictData(); + second.setDictValue("9"); + second.setDictLabel("能量回收"); + return List.of(first, second); + } + return defaultValue(method); + }); + IAeEvaluationService evaluationService = proxy(IAeEvaluationService.class, (method, args) -> { + if ("queryEvaluation".equals(method)) { + String instId = String.valueOf(args[0]); + return evaluations.stream().filter(item -> instId.equals(item.getInstId())).toList(); + } + if ("saveBatch".equals(method)) { + evaluations.addAll((Collection) args[0]); + return true; + } + if ("updateById".equals(method)) return true; + return defaultValue(method); + }); + + TiEvaluationRecorder recorder = new TiEvaluationRecorder( + projectMapper, functions, dictionaries, evaluationService); + return new Fixture(recorder, project, evaluations); + } + + private FlowExecutionEvent nodeEvent(FlowExecutionEvent.EventType type, ActionEnum action, + JSONObject output, List iterations, String error) { + return FlowExecutionEvent.builder() + .eventType(type) + .instId("inst-1") + .taskId("project-1") + .itemId("touch-flow") + .nodeId(action.getAction()) + .nodeName(action == ActionEnum.CAMERA_RECORDING_STOP ? "结束录像" : "执行触控") + .action(action) + .outputParams(output) + .iterations(iterations) + .errorMessage(error) + .build(); + } + + private FlowExecutionEvent taskEvent(FlowExecutionEvent.EventType type, String error) { + return FlowExecutionEvent.builder() + .eventType(type) + .instId("inst-1") + .taskId("project-1") + .errorMessage(error) + .build(); + } + + private AeEvaluation findByItemId(List evaluations, String itemId) { + return evaluations.stream().filter(item -> itemId.equals(item.getItemId())).findFirst().orElseThrow(); + } + + @SuppressWarnings("unchecked") + private T proxy(Class type, Invocation invocation) { + return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type}, + (proxy, method, args) -> invocation.invoke(method.getName(), args)); + } + + private Object defaultValue(String methodName) { + return null; + } + + private interface Invocation { + Object invoke(String method, Object[] args); + } + + private record Fixture(TiEvaluationRecorder recorder, TiProject project, + List evaluations) { + } +} diff --git a/cmvr-iot-vi/src/main/java/com/cmvr/vi/service/impl/ViProjectServiceImpl.java b/cmvr-iot-vi/src/main/java/com/cmvr/vi/service/impl/ViProjectServiceImpl.java index 613fc5e..22187b6 100644 --- a/cmvr-iot-vi/src/main/java/com/cmvr/vi/service/impl/ViProjectServiceImpl.java +++ b/cmvr-iot-vi/src/main/java/com/cmvr/vi/service/impl/ViProjectServiceImpl.java @@ -18,6 +18,7 @@ import com.cmvr.evaluation.service.IAeEvaluationService; import com.cmvr.test.enums.FlowSceneTypeEnum; import com.cmvr.test.enums.RunModeEnum; import com.cmvr.test.flow.runtime.engine.FlowTaskRuntimeService; +import com.cmvr.test.flow.runtime.event.FlowExecutionModuleCodes; import com.cmvr.test.model.domain.TeDetectionItem; import com.cmvr.test.model.domain.TeTaskConfigInfo; import com.cmvr.test.model.domain.TeTaskInst; @@ -136,7 +137,9 @@ public class ViProjectServiceImpl extends ServiceImpl