Compare commits

...

28 Commits
dev ... lxl-dev

Author SHA1 Message Date
61bb2b4a59 feat(service): 升级AI分析服务支持图像分析和视频详细输出
- 移除Ollama相关配置和回退机制,统一使用SGLang作为视觉模型提供商
- 添加IMAGE_ANALYSIS类型支持,允许对多张图片进行分析
- 实现视频分析的详细输出模式,支持紧凑和详细两种结果格式
- 更新环境变量配置,添加VIDEO_MODEL_FRAME_LIMIT和MAX_IMAGE_BYTES
- 修改compose配置文件中的上下文长度和内存分配参数
- 重构视频采样逻辑,限制单次请求帧数以优化显存使用
- 更新API接口文档,添加mediaUrls参数和详细输出选项说明
- 添加图像分析相关的依赖库opencv-python-headless
- 实现结构化JSON响应格式验证和重试机制
2026-08-21 16:19:34 +08:00
0948d73a62 feat(flow): 添加子流程功能支持和循环迭代追踪
- 新增 SUB_FLOW 类型枚举值用于标识子流程节点
- 实现子流程异步媒体分析协调器管理机制
- 添加循环迭代路径追踪到执行上下文日志
- 增强流程控制服务支持嵌套子流程暂停恢复
- 实现子流程作用域内异步任务等待和取消功能
- 优化循环体多入口独立执行线程调度机制
- 添加节点参数循环引用检测验证机制
- 支持遗留循环参数值解析兼容性处理
- 配置虚拟机器人ID列表绕过在线检查机制
2026-08-21 11:48:52 +08:00
6e01f6888b feat(workflow): 实现工作流版本管理功能
- 将检测项流程数据存储分离为草稿和发布版本两个概念
- 添加 TeDetectionItemVersion 实体用于存储不可变的工作流发布版本
- 实现工作流草稿保存、版本发布、版本回退等功能
- 在任务执行时验证检测项是否已发布,防止使用未发布流程
- 添加 TeTaskInstFlowVersion 表记录任务实例关联的具体版本清单
- 重构 TeFlowController 控制器接口,支持草稿保存和版本管理操作
- 更新数据库表结构,添加版本控制相关字段和约束
2026-08-20 14:04:21 +08:00
05f695c93c feat(agv): 添加AGV重定位功能并扩展导航选项
- 新增AgvRelocalizeCommand消息定义,支持在当前活动地图中重新设置AGV定位位姿
- 在AgvService服务中添加relocalize RPC方法,调用前需确保AGV已停止
- 生成相应的Java gRPC服务端和客户端代码实现
- 扩展AgvMotionOptions添加blocked_timeout_ms参数,用于设置连续遇障时最大等待时间
- 更新导航等待工具类支持传入运动选项参数
2026-08-20 12:31:29 +08:00
53ed826480 feat(llm): 添加通用图片分析功能并优化边缘系统安全状态恢复
- 新增 IMAGE_ANALYZE 动作类型支持通用图片分析
- 实现图片分析操作服务支持单张及多图片分析
- 添加图片分析的提示词、参考图片和调优参数配置
- 集成 MinIO 服务上传功能支持唯一文件名生成
- 添加 FlowMediaParamResolver 支持 URL 数组解析
- 实现边缘系统恢复运行状态的新 API 接口
- 标记旧的安全状态恢复接口为已弃用
- 更新媒体分析操作服务支持详细的输出选项
- 添加图片分析相关的单元测试验证功能
2026-08-19 15:00:49 +08:00
937426fcfc feat(edge): 新增AGV平移和机械臂使能控制功能
- 新增AGV固定距离平移功能,支持指定速度和距离移动
- 新增机械臂使能开启/关闭控制,独立于移动操作
- 新增机械臂厂商扩展JSON指令执行接口
- 新增边缘系统安全状态查询和恢复功能
- 优化任务执行监听器,支持任务终态消息推送和错误处理
- 重构音频事件分类逻辑,支持预期标签匹配验证
- 新增任务日志状态管理和终端消息推送机制
2026-08-18 14:43:43 +08:00
0b05438712 feat(flow): 添加流程失败清理服务和机械臂使能枚举
- 新增 FlowFailureCleanupService 用于清理设备端和后端媒体状态
- 添加 ARM_ENABLE 和 ARM_DISABLE 枚举值支持机械臂使能控制
- 在 TaskInstHolder 中集成失败清理逻辑
- 实现机器人录音、录像和系统状态的异常清理功能
- 添加 FlowFailureCleanupService 单元测试验证清理逻辑
- 注入 FlowFailureCleanupService 并在任务失败时执行清理操作
2026-08-18 10:19:25 +08:00
d3498e0178 feat(test): 完善测试任务控制与AI测试日志记录功能
- 在AimaTestLog中添加caseName字段并实现获取方法
- 扩展grpc协议定义包括CommandReasonCode和CommandExecutionState枚举
- 在系统命令中添加安全状态相关字段和操作接口
- 将AGV导航命令改为同步执行模式以提高可靠性
- 添加摄像头和麦克风服务的录像录音中止功能
- 实现测试任务终止时先清理媒体会话再停止设备的安全流程
- 为LLM媒体分析操作添加视频分析模式验证和调优参数支持
- 增加视频分析参数范围校验和测试用例覆盖
2026-08-17 16:58:31 +08:00
861252f066 fix(flow): 修复流程执行异常处理机制
- 在FlowItemExecutor中添加GlobalException导入并修改分支匹配失败处理逻辑
- 将分支节点未匹配到路径的警告改为抛出异常
- 在FlowTaskAsyncDispatcher中添加异常处理相关依赖注入
- 实现更完善的异常捕获和任务失败标记机制
- 优化中断异常处理确保线程状态正确维护
- 添加任务上下文检查避免重复失败标记
2026-08-14 15:12:11 +08:00
c4a1266335 fix(flow): 修复任务终止逻辑并实现AGV导航等待功能
- 修复任务实例终止时对STOPPED状态的处理,添加PAUSED状态检查
- 实现AGV导航等待功能,支持超时控制和状态轮询
- 添加应用启动时遗留任务清理机制
- 优化任务终止时对空任务实例ID的检查
- 添加未完成节点记录的状态更新功能
- 实现AGV导航超时后的自动取消机制
2026-08-14 12:37:08 +08:00
eff8fdd4b6 fix(flow): 修复流程调度器缓存清理逻辑
- 修改 FlowSchedulerCache 中的分隔符从下划线改为冒号
- 在 FlowTaskEngine 中为每个编排项添加调度器去重逻辑
- 移除 TeTaskOrchestrationController 中不必要的分页初始化
- 添加 FlowSchedulerCacheTest 测试用例验证复制节点ID的调度功能
2026-08-14 12:09:02 +08:00
54220ccaac fix(flow): 修复边缘操作服务空指针异常
- 在executeEdgeOperateAction方法中添加edgeOperateServices为空检查
- 添加回退逻辑以支持Spring容器外的手动构造调用者
- 添加cmvr-iot-grpc-lib依赖以支持grpc功能
- 移除TaskNodeExecuteMessage中resolvedDeviceBindings的初始化赋值
2026-08-14 09:26:54 +08:00
1eb1a88870 remove(layout): 删除布局相关组件和配置文件
- 删除了 .gitignore 文件中的构建配置
- 移除了 401 和 404 错误页面组件
- 清理了图标资源文件
- 删除了应用主入口 App.vue 文件
- 移除了应用主视图组件 AppMain.vue
- 清理了设备 API 相关文件
- 删除了流程组件 Aside.vue
- 移除了音频播放器组件 AudioPlayer.vue
- 清理了应用状态管理模块 app.js
2026-08-14 09:10:19 +08:00
519e4de846 feat(aima): 添加爱玛任务和测试用例复制功能
- 在AimaTaskController中添加/copy/{id}端点支持任务复制
- 在AimaTestCaseController中添加/copy/{id}端点支持用例复制
- 实现AimaTaskServiceImpl.copyAimaTask方法完成任务及关联配置复制
- 实现AimaTestCaseServiceImpl.copyAimaTestCase方法完成用例及检测项复制
- 添加EntityCopyUtils工具类处理复制时的名称和编码生成
- 更新服务接口定义IAimaTaskService和IAimaTestCaseService
- 实现完整的事务性复制逻辑包括关联数据的复制
- 添加复制操作的权限验证和日志记录功能
2026-08-14 09:08:18 +08:00
da679363a7 feat(task): 添加任务实例重新执行功能
- 在AimaTaskInstanceController中添加rerun接口支持爱玛任务重新执行
- 在InspectionTaskInstanceController中添加rerun接口支持巡检任务重新执行
- 在TeTaskInstController中添加rerun接口支持测试任务重新执行
- 实现AimaTaskInstanceService的rerunInstance方法处理爱玛任务重跑逻辑
- 实现InspectionTaskInstanceService的rerunInstance方法处理巡检任务重跑逻辑
- 扩展FlowTaskRuntimeService添加loadExecutionRequest和rerunTask方法
- 修改createAndSaveTaskInstance方法保存执行请求快照到任务实例配置中
- 添加单元测试验证执行请求快照的恢复和向后兼容功能
2026-08-13 17:55:38 +08:00
c4a9f29780 feat(llm): 添加媒体分析功能支持音视频分类处理
- 新增 LLMMediaAnalysisOperateService 实现 LLM 操作接口
- 实现对音频事件分类和视频分析操作的支持
- 集成 MediaAnalysisClient 客户端进行媒体分析请求
- 添加媒体参数解析器提取音频/视频 URL 地址
- 实现请求上下文构建包含任务ID、节点ID等信息
- 支持视频分析模式设置和指令传递
- 添加分析结果处理和状态回传机制
- 新增 MediaAnalysisClient 接口定义和实现类
- 实现 HTTP 请求封装和认证信息处理
- 添加 API 密钥标准化和错误处理逻辑
- 配置媒体分析服务连接超时和读取超时参数
- 编写单元测试验证音频数组输入映射功能
- 添加视频
2026-08-13 16:37:51 +08:00
6265db01c7 fix(flow): 解决任务暂停时设备停止和节点退出的同步问题
- 在EdgeSystemServiceImpl中添加响应验证和异常处理
- 实现暂停操作的阻塞等待机制确保设备完全停止
- 添加节点执行完成标记和等待功能
- 优化任务上下文中的暂停状态管理
- 防止恢复操作时新旧节点上下文冲突
- 添加超时控制和异常重试机制
2026-08-13 10:12:14 +08:00
05992d1980 feat(aima): 实现智能测试任务执行队列和媒体录制功能
- 新增 SYSTEM_EXECUTE_ACTION_QUEUE 枚举值支持边缘设备动作队列执行
- 修改 aima_task_test_case 表主键为 task_id 和 sort_order 组合
- 添加 AimaFlowExecutionListener 监听器实现任务执行进度跟踪
- 引入并发安全的 Map 结构存储任务执行状态避免并发问题
- 实现测试用例绑定顺序管理和执行进度计算逻辑
- 集成 FlowMediaParamResolver 处理图像和视频媒体参数
- 完善 AimaTaskInstanceService 支持测试用例顺序执行
- 添加 AimaTestLog 记录实际执行结果、截图和视频链接
- 更新 EdgeCameraService 支持工作流录像会话管理
- 实现后端本地录像功能包括开始/停止录制操作
- 添加定时清理过期录像会话和 WebSocket 实时推送支持
2026-08-12 17:58:44 +08:00
18c4d3c0d7 fix(flow): 修复边缘节点执行目标配置验证和解析问题
- 在 EdgeCameraOperateService 中启用视频录制功能并移除硬编码路径
- 为 FlowRuntimeAssignmentResolver 添加节点类型检查避免非边缘节点处理
- 当执行目标为空且运行时分配存在时抛出异常提供明确错误信息
- 在 FlowRuntimeDefinitionValidator 中增强节点验证逻辑确保边缘节点配置完整性
- 添加单元测试验证遗留机器人配置和非机器人节点处理场景
- 修复测试用例中的节点类型配置确保测试准确性
2026-08-11 17:32:10 +08:00
34ff31366d feat(task): 支持多机器人角色分配的任务执行功能
- 在任务启动接口中增加 AimaTaskStartVO 和 InspectionTaskStartVO 参数支持
- 实现多机器人角色分配机制,支持运行时动态分配机器人和设备资源
- 添加 FlowRuntimeAssignmentResolver 组件处理节点执行目标解析
- 更新任务执行引擎以支持多机器人并发控制和资源锁定
- 修改数据库约束将机器人设备关联改为唯一索引防止重复绑定
- 增强流程定义验证器确保运行时分配配置的完整性
- 扩展任务上下文管理支持多机器人 ID 和锁键跟踪
- 优化试运行和项目任务执行流程集成新的资源分配逻辑
2026-08-11 16:54:01 +08:00
1543a6fb9c feat(agv): 添加AGV固定距离平移功能
- 定义AgvTranslateCommand消息结构,包含请求和反馈
- 添加AgvTranslationMode枚举,支持里程和定位两种距离参考模式
- 定义AgvTranslation消息,包含距离、速度和平移模式参数
- 在AgvService服务中添加translate RPC方法
- 生成Java gRPC客户端和服务端代码实现
- 更新protobuf描述符以包含新的平移命令定义
2026-08-11 15:16:15 +08:00
c51823801d fix(flow): 修复任务暂停终止时的节点执行异常处理
- 在 FlowAfterInterceptor 中提前检查任务暂停终止状态并返回 null
- 将 TaskContext 获取移至条件判断前避免空指针异常
- 在 FlowExceptionInterceptor 中处理中断异常和暂停终止状态
- 添加对空节点动作和不支持操作类型的校验
- 在 FlowItemExecutor 中增加任务状态检查防止继续执行
- 实现 handleUnsuccessfulResult 方法统一处理执行失败情况
- 添加 notifyFinished 方法确保流程等待器正确释放
- 在 FlowTaskEngine 中增加任务状态检查和资源清理
- 优化 TaskInstHolder 中的状态同步逻辑
- 添加 FlowFunctionNodeHandler 单元测试验证异常处理逻辑
2026-08-07 16:48:58 +08:00
54df691ea6 feat(inspection): 新增扬声器播放音频功能并优化机器人状态同步机制
- 在 ActionEnum 中添加 SPEAKER_PLAYAUDIO 操作类型
- 配置机器人状态检查任务的启用和时间间隔参数
- 移除应用配置中的废弃TTS外部接口配置
- 实现 EdgeSpeakerOperateService 中的音频播放功能和参数验证
- 重构 FlowActionExecutorService 以支持多种操作类型的服务分发
- 更新数据库表结构中机器人状态字段的含义和注释说明
- 修改 InspectionRobot 实体类中的状态定义和业务编号描述
- 扩展 InspectionRobotMapper 接口以支持运行时状态更新和候选查询
- 实现 InspectionRobotService 中的 AGV 请求构建和状态同步逻辑
- 添加 RobotRuntimeStateRefreshTask 定时任务来同步机器人运行状态
- 修复 InspectionTaskInstanceService 中的重复机器人ID设置问题
- 优化 RobotQuicStateService 中的设备状态判断逻辑
2026-08-07 16:17:12 +08:00
86a8e02d85 refactor(audio): 将terminalId替换为robotId以统一机器人标识
- 修改AudioService中terminalId为robotId,包括变量声明、参数传递和日志输出
- 更新BaseChannelSubscribeHandler中的注释和参数解析逻辑
- 修改CameraStreamTicketService中的方法参数和记录类定义
- 更新DeRobotConfig文档中的术语描述
- 替换DeviceListController和EdgeAgvController中的terminalId参数为robotId
- 修改EdgeAgvServiceImpl中所有方法的参数引用
- 更新EdgeArmController中所有终端ID参数为机器人ID
- 修改EdgeArmServiceImpl中所有gRPC客户端调用的参数
- 调整EdgeArmTeleopOpenVO中的字段定义顺序
2026-08-05 11:10:19 +08:00
7278d2c0f0 feat(edge): 添加机械臂关节空间运动功能和QUIC连接机器人标识支持
- 新增ARM_MOVE_TO_J动作类型支持关节空间运动
- 在QUIC协议中添加robotId字段用于机器人识别
- 扩展ArmService添加ExecuteJsonCommand RPC方法
- 更新设备列表获取逻辑使用新API接口
- 优化协议注释文档和设备类型定义
- 重构机械臂操作服务实现关节空间运动功能
2026-08-04 15:40:35 +08:00
c8b2c39d74 feat(agv): 添加AGV移动到指定站点功能和设备通用指令支持
- 新增 AGV_MOVE_TO_STATION 操作类型用于移动到指定站点
- 添加 DEVICE_EXECUTE_JSON_COMMAND 类型用于执行设备通用指令
- 移除独立的 agv.proto 文件并将相关内容合并到 agv_utils.proto
- 更新 AGV 命令协议导入路径指向新的工具协议文件
- 优化 AGV 运动选项注释说明同步阻塞行为默认设置
- 为路径导航命令添加执行选项参数支持
- 新增臂式机器人远程操作服务协议定义
- 实现臂式机器人远程操作服务的gRPC接口代码
2026-08-04 09:22:04 +08:00
e62f445013 refactor(flow): 重构循环处理逻辑并完善自动填充功能
- 在BaseEntity中添加MyBatis Plus字段自动填充注解
- 实现FlowLoopValueResolver工具类统一处理循环数据源
- 简化FlowGetCurrentObjNodeHandler中的循环对象获取逻辑
- 优化MyMetaObjectHandler中的时间字段填充方法
- 在多个服务实现中添加实体对象参数确保更新操作正确性
- 为SysFileInfo启用创建时间和创建者的自动填充功能
- 添加FlowLoopValueResolver的单元测试确保功能正确性
2026-07-30 08:51:19 +08:00
bdb6640ad8 feat: migrate backend to Spring Boot 4 2026-07-28 16:33:32 +08:00
802 changed files with 153667 additions and 13435 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
custom: http://doc.ruoyi.vip/ruoyi-vue/other/donate.html

11
.gitignore vendored
View File

@ -46,11 +46,6 @@ nbdist/
!*/build/*.html
!*/build/*.xml
bin/
ry.bat
ry.sh
run.sh
package.bat
sql/quartz.sql
**/application-local.yml
# Local verification tests
/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/operator/edge/EdgeAgvOperateServiceTest.java
/cmvr-iot-test/src/test/java/com/cmvr/test/flow/runtime/operator/edge/EdgeArmOperateServiceTest.java

View File

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<trees>
<tree path="/cmvr-iot-system" title="系统模块"/>
<tree path="/cmvr-iot-admin" title="后台服务"/>
<tree path="/cmvr-iot-common" title="工具类"/>
<tree path="/cmvr-iot-framework" title="框架核心组件"/>
<tree path="/cmvr-iot-generator" title="代码生成"/>
<tree path="/cmvr-iot-quartz" title="定时任务"/>
<tree path="/cmvr-iot-common/src/main/java/com/cmvr/common/annotation" title="自定义注解"/>
<tree path="/cmvr-iot-common/src/main/java/com/cmvr/common/config" title="全局配置"/>
<tree path="/cmvr-iot-common/src/main/java/com/cmvr/common/constant" title="通用常量"/>
<tree path="/cmvr-iot-common/src/main/java/com/cmvr/common/core" title="核心控制"/>
<tree path="/cmvr-iot-common/src/main/java/com/cmvr/common/enums" title="通用枚举"/>
<tree path="/cmvr-iot-common/src/main/java/com/cmvr/common/exception" title="通用异常"/>
<tree path="/cmvr-iot-common/src/main/java/com/cmvr/common/filter" title="过滤器处理"/>
<tree path="/cmvr-iot-common/src/main/java/com/cmvr/common/utils" title="通用类处理"/>
<tree path="/cmvr-iot-common/src/main/java/com/cmvr/common/xss" title="自定义xss校验注解"/>
<tree path="/cmvr-iot-framework/src/main/java/com/cmvr/framework/aspectj" title="注解实现"/>
<tree path="/cmvr-iot-framework/src/main/java/com/cmvr/framework/config" title="系统配置"/>
<tree path="/cmvr-iot-framework/src/main/java/com/cmvr/framework/datasource" title="数据权限"/>
<tree path="/cmvr-iot-framework/src/main/java/com/cmvr/framework/interceptor" title="拦截器"/>
<tree path="/cmvr-iot-framework/src/main/java/com/cmvr/framework/manager" title="异步处理"/>
<tree path="/cmvr-iot-framework/src/main/java/com/cmvr/framework/security" title="权限控制"/>
<tree path="/cmvr-iot-framework/src/main/java/com/cmvr/framework/web" title="前端控制"/>
<tree path="/cmvr-iot-device" title="设备管理"/>
</trees>

View File

@ -1,50 +0,0 @@
# ====================== 构建阶段 ======================
FROM maven:3.9.9-eclipse-temurin-8 AS builder
WORKDIR /app
# 缓存pom
COPY pom.xml .
COPY cmvr-iot-admin/pom.xml cmvr-iot-admin/
COPY cmvr-iot-framework/pom.xml cmvr-iot-framework/
COPY cmvr-iot-system/pom.xml cmvr-iot-system/
COPY cmvr-iot-quartz/pom.xml cmvr-iot-quartz/
COPY cmvr-iot-generator/pom.xml cmvr-iot-generator/
COPY cmvr-iot-common/pom.xml cmvr-iot-common/
COPY cmvr-iot-device/pom.xml cmvr-iot-device/
COPY cmvr-iot-test/pom.xml cmvr-iot-test/
COPY cmvr-iot-api/pom.xml cmvr-iot-api/
COPY cmvr-iot-vi/pom.xml cmvr-iot-vi/
COPY cmvr-iot-ti/pom.xml cmvr-iot-ti/
COPY cmvr-iot-evaluation/pom.xml cmvr-iot-evaluation/
COPY cmvr-iot-api/cmvr-iot-llm/pom.xml cmvr-iot-api/cmvr-iot-llm/
COPY cmvr-iot-api/cmvr-iot-edge/pom.xml cmvr-iot-api/cmvr-iot-edge/
COPY cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/pom.xml cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/
COPY cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/pom.xml cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/
RUN mvn dependency:go-offline -B --no-transfer-progress
COPY . .
RUN mvn clean package -DskipTests -pl cmvr-iot-admin -am --no-transfer-progress
# ====================== 运行阶段 ======================
FROM openjdk:8-jdk-alpine
WORKDIR /app
# -------------------------- 【唯一正确写法】 --------------------------
# 1. 先把所有jar复制进来
COPY --from=builder /app/cmvr-iot-admin/target/*.jar ./
# 2. 自动删除不能运行的小jar(只保留可执行jar)
RUN find . -name "*.jar" -type f -size -20000k -delete
# 3. 重命名为app.jar
RUN mv *.jar app.jar
ENTRYPOINT ["java", "-jar", \
"-Dspring.profiles.active=dev", \
"-Djava.security.egd=file:/dev/./urandom", \
"-XX:+UseG1GC", \
"-Xms512m", "-Xmx1024m", \
"app.jar"]

43
Jenkinsfile vendored
View File

@ -1,43 +0,0 @@
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'mvn clean install -Dmaven.test.skip=true'
}
}
stage('Deploy') {
parallel {
stage('Deploy to DEV') {
steps {
sh './ry.sh start dev'
}
}
stage('Deploy to TEST') {
steps {
sh './ry.sh start test'
}
}
stage('Deploy to PROD') {
steps {
sh './ry.sh start prod'
}
}
}
}
}
post {
always {
archiveArtifacts artifacts: '**/*.jar', allowEmptyArchive: true
}
success {
echo 'Pipeline completed successfully.'
}
failure {
echo 'Pipeline failed.'
}
}
}

335
README.md
View File

@ -1,249 +1,156 @@
# cmvr-iot
# CMVR IoT 物联网平台
cmvr-iot 是一个基于 Java 8 和 Spring Boot 2.5 的多模块后端工程,提供设备管理、边缘机器人控制、测试编排、巡检、TTS、AIMA、VI/TI 等业务能力。工程入口在 `cmvr-iot-admin`,其余模块按业务和基础能力拆分。
CMVR IoT 是招商车研机器人及智能设备管理平台。本项目以 RuoYi-Vue 3.9.2 为基础升级到 Spring Boot 4,保留原有 REST、前端和 gRPC 对外接口,并将 QUIC Gateway 集成到主应用进程中。
Java 包名、Maven 坐标统一使用 `com.cmvr`。旧项目 `cmvr-iot` 仅作为迁移来源,不参与本项目构建。
## 主要功能
- 系统管理:用户、角色、部门、菜单、字典、参数、日志和通知。
- 机器人设备:以边缘端上报的永久 `robotId` 作为唯一业务身份,自动同步机器人地址、在线状态和设备清单。
- 测试业务:任务配置、任务编排、任务实例、检测项、语音唤醒、Flowise 和 AI 评估。
- 智能巡检:机器人、地图、点位、任务、执行实例、巡检结果和告警。
- 智能座舱:语音交互、触控交互、TTS、语料库和车机操作配置。
- 专项业务:爱玛电动车测试、评价指标库和大模型服务。
- 边缘控制:通过 gRPC 调用 AGV、相机、机械臂、灵巧手、机器人头部、麦克风和扬声器。
- QUIC 接入:节点注册、心跳、设备快照、媒体 DATAGRAM 和 gRPC 地址同步。
- 平台能力:Quartz 定时任务、代码生成、WebSocket、MinIO 文件服务和 Knife4j 接口文档。
## 技术栈
- Java 8
- Spring Boot 2.5.15
- Spring Security
- MyBatis Plus / MyBatis Plus Join
- MySQL / Redis
- Druid
- Quartz
- Knife4j / Swagger
- gRPC Java 1.58.0
- Protobuf / protoc-maven-plugin
- MinIO
| 组件 | 版本或实现 |
| --- | --- |
| Java | 17 |
| Spring Boot | 4.0.6 |
| Spring Security | JWT 认证 |
| MyBatis | MyBatis-Plus 3.5.17、MPJ 1.5.9 |
| 数据库 | MySQL |
| 缓存 | Redis |
| RPC | gRPC 1.58.0、Protobuf 3.25.5 |
| QUIC | Netty QUIC 4.2.16.Final |
| API 文档 | Springdoc 3.0.3、Knife4j 4.5.0 |
| 前端 | Vue 3.4、Element Plus 2.9、Vite 5.3 |
## 模块说明
## 工程结构
| 模块 | 说明 |
| --- | --- |
| `cmvr-iot-admin` | Web 启动模块,包含 REST Controller、启动类和运行配置 |
| `cmvr-iot-framework` | Web、安全、配置、通用框架能力 |
| `cmvr-iot-common` | 公共工具、通用响应、基础常量和通用能力 |
| `cmvr-iot-system` | 系统管理、用户、角色、菜单、字典等基础业务 |
| `cmvr-iot-quartz` | 定时任务模块 |
| `cmvr-iot-generator` | 代码生成模块 |
| `cmvr-iot-device` | 设备领域模型与设备相关能力 |
| `cmvr-iot-test` | 测试任务、编排、执行实例等业务 |
| `cmvr-iot-vi` | VI 相关业务 |
| `cmvr-iot-ti` | TI 相关业务 |
| `cmvr-iot-evaluation` | 评测相关业务 |
| `cmvr-iot-inspection` | 巡检机器人、地图、任务、告警等业务 |
| `cmvr-iot-aima` | AIMA 相关业务 |
| `cmvr-iot-tts` | TTS 语音合成相关业务 |
| `cmvr-iot-api` | API 聚合模块 |
| `cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib` | gRPC proto 与生成代码模块 |
| `cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client` | 边缘设备 gRPC 客户端、设备控制服务和请求 VO |
| `cmvr-iot-admin` | Spring Boot 启动入口、REST 控制器和运行配置 |
| `cmvr-iot-framework` | 安全、Web、MyBatis、WebSocket 等框架配置 |
| `cmvr-iot-common` | 公共模型、注解、异常和工具类 |
| `cmvr-iot-system` | 用户、角色、菜单、字典、参数和通用分组 |
| `cmvr-iot-quartz` | Quartz 任务调度和任务日志 |
| `cmvr-iot-generator` | 代码生成 |
| `cmvr-iot-device` | 设备协议模型、机器人配置和巡检告警监听 |
| `cmvr-iot-test` | 测试任务、流程、检测项和执行实例 |
| `cmvr-iot-vi` | 语音交互业务 |
| `cmvr-iot-ti` | 触控交互业务 |
| `cmvr-iot-evaluation` | 评价指标业务 |
| `cmvr-iot-inspection` | 智能巡检业务 |
| `cmvr-iot-aima` | 爱玛电动车专项业务 |
| `cmvr-iot-tts` | TTS 任务和车载语料 |
| `cmvr-iot-api` | 边缘服务和大模型 API 聚合模块 |
| `cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib` | 原有 gRPC 协议及生成代码 |
| `cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client` | 原有机器人 gRPC 客户端 |
| `cmvr-iot-quic-contract` | QUIC 控制、节点事件和媒体数据协议 |
| `cmvr-iot-quic` | 与主应用同进程运行的 QUIC Gateway |
| `cmvr-iot-ui-lxl-dev-worktree` | Vue 3 管理控制台(同级独立前端仓库) |
## 目录结构
## 环境准备
```text
cmvr-iot
├── cmvr-iot-admin # Spring Boot 启动模块
├── cmvr-iot-common # 公共模块
├── cmvr-iot-framework # 框架模块
├── cmvr-iot-system # 系统管理模块
├── cmvr-iot-device # 设备模块
├── cmvr-iot-test # 测试业务模块
├── cmvr-iot-api
│ └── cmvr-iot-edge
│ ├── cmvr-iot-grpc-lib # proto 与 gRPC 生成代码
│ └── cmvr-iot-grpc-client # gRPC 客户端与边缘控制服务
├── sql # 增量 SQL
├── Dockerfile
├── Jenkinsfile
└── pom.xml # Maven 父工程
```
## 环境要求
- JDK 1.8
- Maven 3.6+
- MySQL 5.7+/8.x
- JDK 17
- Maven 3.6.3 或更高版本
- MySQL 5.7/8.0
- Redis
- 可访问的 gRPC 机器人/边缘终端服务
- 可选:MinIO、外部大模型/TTS/评测服务
- Node.js 18/20 LTS 和 pnpm 9
- MinIO,仅使用文件、语料或媒体存储功能时需要
注意:`application-*.yml` 中包含环境相关地址、账号和密钥配置。实际部署或本地开发时,应使用本机配置、环境变量或配置中心覆盖,不要直接复用生产/测试环境敏感配置。
默认激活 `test` profile,后端端口为 `13080`。运行前检查以下配置:
## 配置文件
- `cmvr-iot-admin/src/main/resources/application-test.yml`:MySQL、Redis、MinIO、gRPC 和第三方服务。
- `cmvr-iot-admin/src/main/resources/application.yml`:上传目录、JWT、MyBatis、Knife4j 和 QUIC。
- 不要把生产密码、访问密钥或 TLS 私钥提交到仓库。
主配置文件位于:
## 数据库初始化
```text
cmvr-iot-admin/src/main/resources/application.yml
```
新数据库以 `sql/ry_20260417.sql` 为基础脚本。按实际启用模块检查并执行以下补充脚本:
环境配置文件位于:
- `sql/quartz.sql`:Quartz 表结构。
- `sql/aima_module.sql`:爱玛业务。
- `sql/inspection_module.sql`、`inspection_result.sql`、`inspection_detection_alert.sql`:巡检业务。
- `sql/tts_corpus_module.sql`:TTS 和语料业务。
- `sql/robot_quic_identity_migration.sql`:机器人 QUIC 身份、运行状态和设备快照表。
- `sql/robot_id_only_migration.sql`:移除巡检机器人和任务实例中的历史终端 ID 字段。
```text
cmvr-iot-admin/src/main/resources/application-dev.yml
cmvr-iot-admin/src/main/resources/application-test.yml
cmvr-iot-admin/src/main/resources/application-prod.yml
```
补充脚本可能面向不同历史库版本。已有数据库升级前应先备份,并确认字段或索引尚未存在。
当前 `application.yml` 默认激活:
```yaml
spring:
profiles:
active: test
```
如需切换环境,可以修改 `spring.profiles.active`,或启动时指定:
```bash
java -jar cmvr-iot-admin-dev.jar --spring.profiles.active=dev
```
## 构建
## 后端运行
在项目根目录执行:
```bash
mvn clean package -DskipTests
java -jar cmvr-iot-admin/target/cmvr-iot-admin.jar
```
只编译启动模块及其依赖:
也可以在 IDEA 中直接运行:
```text
com.cmvr.CmvrIotApplication
```
DevTools 自动重启已关闭。修改代码后需要完整停止并重新启动应用,避免 MPJ 保留已经关闭的 Spring 上下文。
常用地址:
- 后端 API:`http://localhost:13080`
- Knife4j 文档:`http://localhost:13080/doc.html`
- OpenAPI JSON:`http://localhost:13080/v3/api-docs/00-all`
Knife4j 只展示带原有 `@ApiOperation` 注解的正式接口,并沿用中文 `@Api(tags=...)` 业务分类。
## 前端运行
```bash
mvn -pl cmvr-iot-admin -am compile -DskipTests
cd ../cmvr-iot-ui-lxl-dev-worktree
pnpm install
pnpm dev
```
只重新生成并编译 gRPC 代码:
默认开发端口为 `80`。`.env.development` 中的 `VITE_API_URL` 必须与后端地址一致;本项目后端默认是 `http://localhost:13080`。更多说明见 [ruoyi-ui/README.md](ruoyi-ui/README.md)。
## gRPC 与 QUIC
原有机器人 gRPC 客户端与 proto 均保留,但业务调用只传永久 `robotId`。平台根据 `inspection_robot.robot_id` 查找 QUIC 最近同步的 `ip_address` 和 `port`,不再使用终端 ID 或终端配置解析 gRPC 地址。
边缘端通过 QUIC 上报设备快照后,平台按 `robotId + deviceId` 写入 `inspection_robot_device`。前端选择机器人后只展示该机器人当前在线且启用的设备;单一匹配设备会自动选中并锁定。
QUIC 不是独立服务,`cmvr-iot-quic` 已作为 `cmvr-iot-admin` 的运行时依赖。启用时主应用同时启动:
- QUIC:默认 `0.0.0.0:4433/udp`
- 节点和媒体事件:通过 Spring 单例 `NodeEventBroker` 在进程内直接分发,不占用内部端口
设置 `cmvr.quic.enabled=true` 前必须配置有效的 TLS 证书和私钥,否则应用会拒绝启动。详细配置见 [cmvr-iot-quic/README.md](cmvr-iot-quic/README.md)。
## 构建与验证
```bash
mvn -pl cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib -am compile -DskipTests
```
# 运行测试
mvn test
## 启动
启动类:
```text
cmvr-iot-admin/src/main/java/com/cmvr/CmvrIotApplication.java
```
IDE 中直接运行 `CmvrIotApplication` 即可。
打包后运行:
```bash
java -jar cmvr-iot-admin/target/cmvr-iot-admin-dev.jar --spring.profiles.active=dev
```
默认开发端口以当前 profile 配置为准,例如 `application-dev.yml` 中为:
```yaml
server:
port: 13080
```
## 接口文档
项目集成 Knife4j / Swagger。服务启动后可访问:
```text
http://localhost:13080/doc.html
```
如果端口或上下文路径被 profile 覆盖,请以实际配置为准。
## 数据库脚本
增量脚本位于:
```text
sql/
```
当前包含:
- `aima_module.sql`
- `inspection_module.sql`
- `tts_corpus_module.sql`
初始化数据库时,需要结合目标环境的基础库结构和增量脚本执行。
## gRPC 与边缘设备
边缘设备相关代码集中在:
```text
cmvr-iot-api/cmvr-iot-edge/
```
proto 文件位于:
```text
cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/
```
gRPC 客户端服务位于:
```text
cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/
```
边缘控制器位于:
```text
cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/
```
新增或修改 proto 后,执行 Maven compile 会触发 `protoc-maven-plugin` 生成 Java 和 gRPC Stub 代码。业务代码应复用现有 gRPC 客户端管理工具获取 Stub,避免在业务层重复创建底层 `ManagedChannel`。
## 实时语音对讲
当前实时语音对讲链路为:
```text
浏览器 WebSocket Binary PCM <-> Java 后端 <-> gRPC 双向流 <-> 机器人终端
```
约定音频格式:
- PCM_S16LE
- 48000 Hz
- 单声道
- 16 bit
- 10 ms 一帧
- 960 bytes/帧
相关文件:
- WebSocket 控制器:`cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/WebRtcSignalingController.java`
- 业务会话管理:`cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/AudioService.java`
- gRPC 音频适配:`cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/adapter/RobotAudioGrpcAdapter.java`
- proto:`cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/robot_audio.proto`
说明:历史 WebRTC 相关类可能仍保留在源码中,但当前浏览器音频传输链路使用 WebSocket 二进制 PCM。
## 开发约定
- Controller 层只做参数接收、基础校验和响应封装。
- POST 控制类接口优先使用单个 `@RequestBody VO` 承载完整请求参数。
- GET 查询类接口可以使用 query 参数绑定 VO。
- 不建议在同一个接口中同时声明通用 VO 和零散业务参数。
- gRPC 调用统一通过现有客户端管理类获取 Stub。
- proto 修改后必须重新编译并确认生成代码已更新。
- 不要提交本地日志、崩溃 dump、临时音频文件和包含真实密钥的配置。
## 常用命令
```bash
# 编译全部模块
mvn clean compile -DskipTests
# 编译启动模块及依赖
mvn -pl cmvr-iot-admin -am compile -DskipTests
# 打包
# 构建全部后端模块
mvn clean package -DskipTests
# 运行指定 profile
java -jar cmvr-iot-admin/target/cmvr-iot-admin-dev.jar --spring.profiles.active=dev
# 构建前端生产包
cd ../cmvr-iot-ui-lxl-dev-worktree
pnpm build:prod
```
Windows 下也可以使用 `bin/package.bat`、`bin/run.bat` 和 `bin/clean.bat`。
## 排查提示
- `ApplicationContext ... has been closed already`:彻底停止旧 Java 进程后重新启动,不要使用 DevTools 热重启。
- 找不到机器人或机器人离线:确认机器人表已登记相同的永久 `robotId`,并检查 QUIC 注册日志、`inspection_robot` 的地址和连接状态。
- QUIC 启动失败:检查证书、私钥、ALPN、UDP 端口占用和 native library 对应的操作系统架构。
- 数据库提示缺少字段:确认基础脚本版本及对应模块迁移脚本已经执行。

12
bin/clean.bat Normal file
View File

@ -0,0 +1,12 @@
@echo off
echo.
echo [信息] 清理工程target生成路径。
echo.
%~d0
cd %~dp0
cd ..
call mvn clean
pause

12
bin/package.bat Normal file
View File

@ -0,0 +1,12 @@
@echo off
echo.
echo [信息] 打包Web工程,生成war/jar包文件。
echo.
%~d0
cd %~dp0
cd ..
call mvn clean package -Dmaven.test.skip=true
pause

14
bin/run.bat Normal file
View File

@ -0,0 +1,14 @@
@echo off
echo.
echo [<5B><>Ϣ] ʹ<><CAB9>Jar<61><72><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Web<65><62><EFBFBD>̡<EFBFBD>
echo.
cd %~dp0
cd ../cmvr-iot-admin/target
set JAVA_OPTS=-Xms256m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m
java -jar %JAVA_OPTS% cmvr-iot-admin.jar
cd bin
pause

25
certs/cmvr-quic-ca.crt Normal file
View File

@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIEKzCCApOgAwIBAgIUTtZCyKM8INYxZKkfwbKdpHKLjn4wDQYJKoZIhvcNAQEL
BQAwHTEbMBkGA1UEAwwSQ01WUiBRVUlDIExvY2FsIENBMB4XDTI2MDcyNDA2NDQy
MFoXDTM2MDcyMTA2NDQyMFowHTEbMBkGA1UEAwwSQ01WUiBRVUlDIExvY2FsIENB
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAmq2rHldOobaemqNfWggS
OVj3inKy6AYjfgtcXUfKs48DDbpZ9gyEd/YPJXA8C2jGPXpxzgnc7a4UCUVQZ8ah
ddoJtFcC+Q6BgjeMVqUdUubu5Y9HpkfU3lvnp4KhzvOeFnkKtrCzYIPa2nK3zLc7
uCiuLlB+91KQSRXPFbc6N7H/EAfGmUHIwlZGysAkRN7b2TAoR4C7E96JLVtuUQsS
VtlEGpunSfuefFzeeZCMS6avLbB+a8Q6yUzLt6pqnheNsDB+jCCXodlJs5XS1AOB
W2GOpGFMj7dLoTD+eBIlAlrhWFcwKjzFmtp6LGl/Jy0O+E99X4TL72oNSOdTrO2O
iavXABz9IvWR2BrAyo5AKlTJqO6tmZw77iVti8jYi+HsXIVGQKMYwvv5k0jdHeIZ
FaDToUbFPP/zj0m8ZraMy+8eNAhScnx6Zs56fcncBuDti6pT+zKisjV1rH/sFvZY
wO1UvJlOEbvXrfYLPp58Aqe/toG2nV45a2Q4+x6AETV1AgMBAAGjYzBhMB0GA1Ud
DgQWBBRdHz+g2vVmqghDZi2kEHtZfoyHZzAfBgNVHSMEGDAWgBRdHz+g2vVmqghD
Zi2kEHtZfoyHZzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkq
hkiG9w0BAQsFAAOCAYEASuEoMFiVcg4iTMxO2kshFTJ6LIrqqGXBn+1j+yQ1ennG
mqPMo5fBOe/Kp3YWCnREQWu0+EEPEC9qWgIDOIm3v7ch4mZiW31GUOqae6bjprBe
er7ySElKGZ5GefKAq++we19A6WHnNxtNAT9BE1VSKUmkxEsnIkuwd+QMmQ9eaIRm
8RvzshWdUcyiBJg07sI3rPzPD/YpnfcYlAa2a0+oXJ3o3zlhdbs2S+9fIl+Y/zFN
wBaUV6ZNk0RFzOCA+jUq2jU5Y1pODcot3Mp2jCHzp4uTFyVqsoqc5USRB2TUILCP
acSbEjD9GnKaNU4miTcftnC85fnBAiZ5btZkXIs72ij+J3VW7QnbcFQG8bElngoR
mWizh6ByPQrsvyT0evJjdjpuOzILsVTAn64m0cYpnlR4g5orcEkIJHv4DzMhpXc2
CvpcWwk2vJrUuws/LGxDATTAcqtIUIxAkxffqUsdB/6j8fdykNCvXFwNh/uPcawd
4kzh6dr+RFzHSMQ6oN16
-----END CERTIFICATE-----

40
certs/cmvr-quic-ca.key Normal file
View File

@ -0,0 +1,40 @@
-----BEGIN PRIVATE KEY-----
MIIG/wIBADANBgkqhkiG9w0BAQEFAASCBukwggblAgEAAoIBgQCaraseV06htp6a
o19aCBI5WPeKcrLoBiN+C1xdR8qzjwMNuln2DIR39g8lcDwLaMY9enHOCdztrhQJ
RVBnxqF12gm0VwL5DoGCN4xWpR1S5u7lj0emR9TeW+engqHO854WeQq2sLNgg9ra
crfMtzu4KK4uUH73UpBJFc8Vtzo3sf8QB8aZQcjCVkbKwCRE3tvZMChHgLsT3okt
W25RCxJW2UQam6dJ+558XN55kIxLpq8tsH5rxDrJTMu3qmqeF42wMH6MIJeh2Umz
ldLUA4FbYY6kYUyPt0uhMP54EiUCWuFYVzAqPMWa2nosaX8nLQ74T31fhMvvag1I
51Os7Y6Jq9cAHP0i9ZHYGsDKjkAqVMmo7q2ZnDvuJW2LyNiL4exchUZAoxjC+/mT
SN0d4hkVoNOhRsU8//OPSbxmtozL7x40CFJyfHpmznp9ydwG4O2LqlP7MqKyNXWs
f+wW9ljA7VS8mU4Ru9et9gs+nnwCp7+2gbadXjlrZDj7HoARNXUCAwEAAQKCAYAi
e49/pNqWghoTItM9xLldWAhdcMsSH1Y3wgweDoRxqbL2U0I9cFZy0OPZBo+YQowZ
RgwLcRbz1MBKPc3aSMWTep95uQEkaVe1YjFS2p3yLqH5AstoFjDuPmJjLWPpuVVX
sLXS+wsOO+7lDriLdpjlagpEsHTRqbIZXPeM4YtkwbV5SyZ64ZfCPU4sYo/jW6R6
43nDUP9Dw3Nk7XJnNlbpDigY33T4sRPIqUJ+qtsf/WGlx6gzWax6Vni+8gqxQlIe
80XG6dHCM3UGFEdGF64PB2FuprsPWBSSau7L7I/gagiIuQbhs9VYXOanz1MQizMU
ZdK8cqmG/bpP+uY4VQLJ5Lxu87boEllGFHBOYTCjWq0zatzGMt1/DeggLnjgfC90
5kOzlxl403j+4K8nqiHoKpAbOatwTxFz1up6XSecsarE1RKsCq8sIH+FjjCfO1Db
hBfv2a7/XemdcxHtl9Q/EOSDaAFOUQfFMDAc3rCH7/vonTzaaZq5ucWmjlBsv2EC
gcEAz+cH+3x09pXVOnM7zlWrGAQk69Fj/0BGs7g3GFf8ZvI/kkbez+9J1QGItxj+
7Dm4Ay0kgxYi3DxFWbPLHTbFpVg2CyL08IQZSXJzokwz2V/HTd1a+OgzgB/dBNDc
V+XnxJI89+SI3VhOQZJG625l//lhHWCy2u0ULE7ZlK3ao95L86bSXQHI4dZ2nWXl
0f4ISJmm4qkEJAY+DkQeF3UZLumd1NRxy0LBdbYVM/wZIN+sGsRdQqQ9f+ik6+aX
YrjhAoHBAL52dJzIPIN1Z/Y9qxUrVLPE7K8Mm8KLaFIl/ztl3TFqaUvErHi66nx3
MTwEnrQIBI31OKxB6Or2JLxYMvDTSVRV1K2tY53+K6Q8giWU4x3zFc/svyl35gkd
U1bLdfxXqKMLrvlmVWj6/RMoH7vd/aTcidYVvwOZgtL+6RahS4LfZ9qXAyREFAJa
VSI0HW5LLiOBLTegztT1CBVe7xb78bWFMiUkxre4XsLZYwFPzXzVPh6HloDcACDL
FPwLrfJrFQKBwQCvGDl90UzEnF4v0vsshLQLDvp1bS1VvTGOjPhB1WBq510o+e0P
nM1Gyvr0keWo19elPTDCAjOr3kreCHFpEkcVQRyK9o7pvad6Vx0SNDF6wpKdfm7u
sMknAC7prmnU0XkH8c3NTTkDiiqmSObXw2u+UK48ysL3ZLIXuvS+pkk8t6yp8Pa8
hBNGOJQ/baFH4TXixx1pScWF/YfoBfB9+w4Rl4loxN9tu7QpSgfDd29GY3qUNIsC
5EYzYqD7WIJpD6ECgcEAkdIfdenYas14yw5r7ck/EGO00lDU8B3LwRlWUCOtNihC
dcAeTFDPNnwLNehTmYKJ+iXFPh04Nqw9c/YTCk651dfg/RfDLTNsNlIdUqirOkLi
cE7SDO2/MTtCkzEzI//5HNvVGx0+RyHioMgXg75yc8ZlwYLku9zMTL7dtnXHWmux
F6qGvT1iFGsUwxsjbU4iBQzhkbWMpX70sWf9pZs/c7qGqel+OyrtYkENi/ONYAXj
iXxFvmKxtmnFpzNJ+lABAoHBAJjs8jfEXJnJP80t93Bxg6NmfKhxUEm4ovcIeR8U
lEfb8Ind1ICrBWGVGOAkirTFZgFAAlEjne+lT1FYgf19oC+XbxFdl/e02M8KfflS
01+0/t6lEpefiU0dfW0dWtXOPaAVvpNgMkeMgBzB2Yz7ZNUrMypfCGk289qveWxB
8UbO+QLjTI+cFvPsmR95Eti/nAjd2fXGeqbR+z+DdQnz4BvXLSCQCw4on2+T+mpw
BprrEQmcdeZA8LE1zgcKtkZR1w==
-----END PRIVATE KEY-----

1
certs/cmvr-quic-ca.srl Normal file
View File

@ -0,0 +1 @@
2C945D70B02014891B6E09D57E377CEFB6D18498

22
certs/quic-gateway.crt Normal file
View File

@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDuzCCAiOgAwIBAgIULJRdcLAgFIkbbgnVfjd877bRhJgwDQYJKoZIhvcNAQEL
BQAwHTEbMBkGA1UEAwwSQ01WUiBRVUlDIExvY2FsIENBMB4XDTI2MDcyNDA2NDQz
NloXDTI4MTAyNjA2NDQzNlowGDEWMBQGA1UEAwwNMTkyLjE2OC4wLjIyMjCCASIw
DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKdM3i1FYFKqNWJzhfhsD9nRUAuK
pzilz5uqCKAt8lKYYC9WnLHOYdiEjcHGnGr02yd6sWFH/LBbxNhzx8M7h4S4izuO
bhSlG1EIhkMiojzVD1e3P7YzXdEoVxTCfmMgBZQJG63GNOfzRawFYtEeGv7ndFVw
kitCYlyTza5KlBNlWpiNOPmmx4dLTdGLUk8a5TUm0zJ+b/LyzVsWUtr9sxKmKeG8
0/77AeiL0hQE3xUt5QROTZjRTVhNHowv410dFMJyIfY4sab8ndc4SIwE9PCKg068
4705vbFInBS3eTvQur5VLSZLPatnXGKzCXjci1lIQX2p/QIqADMGFWIN9OECAwEA
AaN4MHYwDwYDVR0RBAgwBocEwKgA3jAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAww
CgYIKwYBBQUHAwEwHQYDVR0OBBYEFLmoEtsglxm3mXh4l8OF31bt2ZhnMB8GA1Ud
IwQYMBaAFF0fP6Da9WaqCENmLaQQe1l+jIdnMA0GCSqGSIb3DQEBCwUAA4IBgQAR
eK1mD9rJkzHe4OusimQfcuDQW+0J32e4T/34RHlW+lIj7botFaElXIzO9S80tDwq
4d4ozNPKysqgJN9hv/BBMzJpZLwP2XozPaGLTNl1jRTCc9UhFUPrUeu0LbpQGBfC
6Ghq42V94zPAw4lnMujnkq8botk21hclbJORQ9kblXP31IdWCgiKSFLy1NTBmQmc
IxmR+SldMWYrWGWv/0I85AeMu6HR3+NKHmzDblm1HUHFekyC1f7sypNG+D1r8ab2
GSijoCMKHSEOm81Vl/j5bgWQygnnIOhsLOUf2DZO6jC+VZmTKpMEJNHgwk23WBlu
FYz/X9p5z9ZucL8aBxegj7G1fI5Ik2O05+LLeqJMfspAm6ZcnRDxCVZWQ+K/dAle
fz+gTASzhHTsjEBeiX46LP0L2PVyBiPvNtD66e52LoM5ZigechZq91niLO+ipWdh
xq7ryJzmaAEQKPuYaoswriWzJam07ywX9yupGHzBA6vVgGy0jPAsnGCuZXTfDyA=
-----END CERTIFICATE-----

17
certs/quic-gateway.csr Normal file
View File

@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE REQUEST-----
MIICpDCCAYwCAQAwGDEWMBQGA1UEAwwNMTkyLjE2OC4wLjIyMjCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBAKdM3i1FYFKqNWJzhfhsD9nRUAuKpzilz5uq
CKAt8lKYYC9WnLHOYdiEjcHGnGr02yd6sWFH/LBbxNhzx8M7h4S4izuObhSlG1EI
hkMiojzVD1e3P7YzXdEoVxTCfmMgBZQJG63GNOfzRawFYtEeGv7ndFVwkitCYlyT
za5KlBNlWpiNOPmmx4dLTdGLUk8a5TUm0zJ+b/LyzVsWUtr9sxKmKeG80/77AeiL
0hQE3xUt5QROTZjRTVhNHowv410dFMJyIfY4sab8ndc4SIwE9PCKg0684705vbFI
nBS3eTvQur5VLSZLPatnXGKzCXjci1lIQX2p/QIqADMGFWIN9OECAwEAAaBHMEUG
CSqGSIb3DQEJDjE4MDYwDwYDVR0RBAgwBocEwKgA3jAOBgNVHQ8BAf8EBAMCBaAw
EwYDVR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZIhvcNAQELBQADggEBAGIcLeCE344z
PENI1/oONVHBzMMt5VN0P8jbkJOFgZ3a6AUhfqAmDNBr+8SBym+cX2Y9Q2BsaWAu
TKOBN+fs+fh5/NqF0hTNvmXzp89NFK5SlsTjoC21HJvK1HTNuNW8drOxNfWgFW3/
gSCutcsWS9hVtYrV2FHQzOvVXvXKfmuTkZE8g92P3BCkvRm+ORxI0QfVS81/Ibnv
Yl+t/o2QCphdP+1OWzU6+Pccqi2xCyIc6jqCmh0b01zg265PTTOfICO6WQNtYcOn
308avDDpTneGl5vWRVdmmiXPqkc+dCesoguIEDUlerFpDKKY81P2GTpfjki93K7Z
2DJPs4P1cU0=
-----END CERTIFICATE REQUEST-----

28
certs/quic-gateway.key Normal file
View File

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCnTN4tRWBSqjVi
c4X4bA/Z0VALiqc4pc+bqgigLfJSmGAvVpyxzmHYhI3Bxpxq9NsnerFhR/ywW8TY
c8fDO4eEuIs7jm4UpRtRCIZDIqI81Q9Xtz+2M13RKFcUwn5jIAWUCRutxjTn80Ws
BWLRHhr+53RVcJIrQmJck82uSpQTZVqYjTj5pseHS03Ri1JPGuU1JtMyfm/y8s1b
FlLa/bMSpinhvNP++wHoi9IUBN8VLeUETk2Y0U1YTR6ML+NdHRTCciH2OLGm/J3X
OEiMBPTwioNOvOO9Ob2xSJwUt3k70Lq+VS0mSz2rZ1xiswl43ItZSEF9qf0CKgAz
BhViDfThAgMBAAECggEAC3+johwrdIcNFAMRZUuFE+EYZbCuEV1JHX90aBD8X5uo
Obyz2J8xF6z1osjEMbA6W1ZqGF/2oMbv6robc+JUrEU0UZ+2lm3qKJnDw5u+55wz
uaFHTF4vr6tcf9llzymRZJUzLTCLLJmKc3orymhtrdEp7GDoNPezM753Y6gt0P7Z
AzePK3d7YmEAcRkIDVtqQXyma4CPziItJrcF4Lumww6PxRzqYNb6jB8wsmf0B91U
iTtnJQoJfkOGTl5OyPp2s7A+qXqToIqAbIlzcF1Q7i0kohmhZHHIB8Qdk9O8IGb6
kZuLbTIdIhTG2vyi8nqz+oMh6D+AJBy+GTLlK0LWCQKBgQDVIrGu9J76R7D3npRy
3okc6mVHo2InZaOeai4ZAe/CFVPcHecq7+bx06MxTqge0XYJd5UbO8qfRL5mRc8g
JwjUrnCzGvJHFavc3Xs221JgWAaNQ0+PtBKCLHQwbmu8V3NZHXcjyonMuQ2g2oOm
TYY7Adedci0SgSFlUy9TjifH6QKBgQDI8lcoojzmYkj0O8oO3dr01XbAEl5pxjyB
IsEpal6J8gdbbj8vzdWaGoHs/IoLPtwWbQGx7gUJfYtKp/DORw0WDFJmqrm/PGfh
RrLKasjp3GC06BU3GmnoKZxQM8PzY0ELWZsQM0fa+ROC1vMrNcHx5nZvcBwrkKXu
CEw2G4miOQKBgDMcUsfF2anjL0jyOl8hhnfsrcT0JUGVWhtPEybkE/7S5n1OnnTp
etRbEDNuzVo2mLvEdXlGaVywO4iDDRyEv9Vgz53jxF3PkJsyca3CAosgENInZhjr
8dRNnL9Qe3166ypfqoZmt4zkg6QjQpbK2HmmA3K6QpzBYqTNwfeji16hAoGBAKO3
pkFp5TpC63NpXxe7s4/0dxrIHHoLU/JWcZAfpMS5Fztr2DlHd3dlj2OY4ZOF9GSl
DVN9PEdeLOV3bJ9zjfrfsScKg+mF+pC1PCfk1qqKrZPW3qoVcCIB/OPvOQAMZssr
h0Wssid69WWNYXNcChbfELF8Yscvw7Dqu8T9rKYpAoGAByn1Vfu95lAxQ36fxXnk
3CC9FkGcdfKGTqkFZHJWiJqaLL6eAbCwO38kywYZLANoSz03Qu3MmD0Gt0wyeP6I
a3o0goisV8s5W9q1P1i1K73xlbAPf54QsqoPNuXR8n67I9IzzL7EkPJiUsHuqNHW
Ffn4lzHEAxaTksNIaC6oIlo=
-----END PRIVATE KEY-----

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>cmvr-iot</artifactId>
<groupId>com.cmvr</groupId>
<version>3.8.9</version>
<version>3.9.2</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
@ -24,16 +24,20 @@
<optional>true</optional> <!-- 表示依赖不会传递 -->
</dependency>
<!-- Mysql驱动包 -->
<!-- spring-doc -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-ui</artifactId>
</dependency>
<!-- 测试管理-->
<!-- Mysql驱动包 -->
<dependency>
<groupId>com.cmvr</groupId>
<artifactId>cmvr-iot-test</artifactId>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- 核心模块-->
@ -54,73 +58,72 @@
<artifactId>cmvr-iot-generator</artifactId>
</dependency>
<!-- 设备管理-->
<dependency>
<groupId>com.cmvr</groupId>
<artifactId>cmvr-iot-test</artifactId>
</dependency>
<dependency>
<groupId>com.cmvr</groupId>
<artifactId>cmvr-iot-device</artifactId>
</dependency>
<!-- 语音交互-->
<dependency>
<groupId>com.cmvr</groupId>
<artifactId>cmvr-iot-vi</artifactId>
</dependency>
<!-- 触控交互-->
<dependency>
<groupId>com.cmvr</groupId>
<artifactId>cmvr-iot-ti</artifactId>
</dependency>
<!-- AI评估-->
<dependency>
<groupId>com.cmvr</groupId>
<artifactId>cmvr-iot-evaluation</artifactId>
</dependency>
<!-- 智能巡检-->
<dependency>
<groupId>com.cmvr</groupId>
<artifactId>cmvr-iot-inspection</artifactId>
</dependency>
<!-- 爱玛电动车测试-->
<dependency>
<groupId>com.cmvr</groupId>
<artifactId>cmvr-iot-aima</artifactId>
</dependency>
<!-- 智能座舱语音语料采集-->
<dependency>
<groupId>com.cmvr</groupId>
<artifactId>cmvr-iot-tts</artifactId>
</dependency>
<dependency>
<groupId>com.cmvr</groupId>
<artifactId>cmvr-iot-quic</artifactId>
</dependency>
</dependencies>
<properties>
<env>dev</env>
</properties>
<build>
<plugins>
<!-- Spring Boot Maven 插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.5.15</version>
<configuration>
<mainClass>com.cmvr.CmvrIotApplication</mainClass>
<fork>true</fork> <!-- 如果没有该配置, devtools 不会生效 -->
<addResources>true</addResources>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<finalName>${project.artifactId}-${spring.profiles.active}</finalName>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
<warName>${project.artifactId}</warName>
</configuration>
</plugin>
</plugins>
<finalName>${project.artifactId}</finalName>
</build>
</project>

View File

@ -1,9 +1,8 @@
package com.cmvr;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
@ -12,13 +11,13 @@ import org.springframework.scheduling.annotation.EnableScheduling;
* @author cmvr-iot
*/
@EnableScheduling
@MapperScan("com.cmvr.**.mapper")
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
public class CmvrIotApplication
{
public static void main(String[] args)
{
// System.setProperty("spring.devtools.restart.enabled", "false");
// MPJ keeps its Spring context in a static holder that is not refreshed by DevTools.
System.setProperty("spring.devtools.restart.enabled", "false");
SpringApplication.run(CmvrIotApplication.class, args);
System.out.println("招商车研物联网平台启动成功");
}

View File

@ -0,0 +1,77 @@
package com.cmvr.web.config;
import com.cmvr.aima.domain.AimaTaskInstance;
import com.cmvr.aima.enums.AimaTaskStatusEnum;
import com.cmvr.aima.service.IAimaTaskInstanceService;
import com.cmvr.inspection.domain.InspectionTaskInstance;
import com.cmvr.inspection.service.IInspectionTaskInstanceService;
import com.cmvr.test.enums.TaskStatusEnum;
import com.cmvr.test.flow.control.FlowControlService;
import com.cmvr.test.model.domain.TeTaskInst;
import com.cmvr.test.service.ITeTaskInstService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
public class FlowTaskStartupRecovery {
private final ITeTaskInstService taskInstService;
private final FlowControlService flowControlService;
private final IAimaTaskInstanceService aimaTaskInstanceService;
private final IInspectionTaskInstanceService inspectionTaskInstanceService;
private final Date processStartedAt = new Date();
@EventListener(ApplicationReadyEvent.class)
@Transactional(rollbackFor = Exception.class)
public void stopOrphanedTasks() {
List<String> orphanedInstIds = taskInstService.lambdaQuery()
.in(TeTaskInst::getStatus,
String.valueOf(TaskStatusEnum.RUNNING.getCode()),
String.valueOf(TaskStatusEnum.PAUSED.getCode()))
.lt(TeTaskInst::getCreateTime, processStartedAt)
.list()
.stream()
.map(TeTaskInst::getId)
.toList();
if (orphanedInstIds.isEmpty()) {
return;
}
Date now = new Date();
for (String instId : orphanedInstIds) {
flowControlService.stopOrphaned(instId);
}
aimaTaskInstanceService.lambdaUpdate()
.in(AimaTaskInstance::getTaskInsId, orphanedInstIds)
.in(AimaTaskInstance::getStatus,
AimaTaskStatusEnum.RUNNING.getCode(), AimaTaskStatusEnum.PAUSED.getCode())
.set(AimaTaskInstance::getStatus, AimaTaskStatusEnum.STOPPED.getCode())
.set(AimaTaskInstance::getEndTime, now)
.set(AimaTaskInstance::getUpdateTime, now)
.update();
inspectionTaskInstanceService.lambdaUpdate()
.in(InspectionTaskInstance::getTaskInsId, orphanedInstIds)
.in(InspectionTaskInstance::getStatus,
com.cmvr.inspection.enums.TaskStatusEnum.RUNNING.getCode(),
com.cmvr.inspection.enums.TaskStatusEnum.PAUSED.getCode())
.set(InspectionTaskInstance::getStatus,
com.cmvr.inspection.enums.TaskStatusEnum.STOPPED.getCode())
.set(InspectionTaskInstance::getEndTime, now)
.set(InspectionTaskInstance::getUpdateTime, now)
.update();
log.warn("应用启动时自动终止遗留流程任务: count={}", orphanedInstIds.size());
log.debug("应用启动时自动终止遗留流程任务明细: instIds={}", orphanedInstIds);
}
}

View File

@ -1,7 +1,7 @@
package com.cmvr.web.controller.aima;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;

View File

@ -1,7 +1,7 @@
package com.cmvr.web.controller.aima;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;

View File

@ -1,7 +1,7 @@
package com.cmvr.web.controller.aima;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
@ -58,6 +58,14 @@ public class AimaTaskController extends BaseController {
return toAjax(aimaTaskService.insertAimaTask(aimaTask));
}
@ApiOperation("复制爱玛任务")
@PreAuthorize("@ss.hasPermi('aima:task:add')")
@Log(title = "爱玛任务管理", businessType = BusinessType.INSERT)
@PostMapping("/copy/{id}")
public AjaxResult copy(@PathVariable String id) {
return success(aimaTaskService.copyAimaTask(id));
}
@ApiOperation("修改任务")
@PreAuthorize("@ss.hasPermi('aima:task:edit')")
@Log(title = "任务管理", businessType = BusinessType.UPDATE)

View File

@ -1,7 +1,7 @@
package com.cmvr.web.controller.aima;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
@ -14,6 +14,7 @@ import com.cmvr.common.enums.BusinessType;
import com.cmvr.aima.domain.AimaTaskInstance;
import com.cmvr.aima.domain.dto.AimaTaskInstanceQuery;
import com.cmvr.aima.domain.vo.AimaTaskInstanceVo;
import com.cmvr.aima.domain.vo.AimaTaskStartVO;
import com.cmvr.aima.service.IAimaTaskInstanceService;
import com.cmvr.common.utils.poi.ExcelUtil;
import com.cmvr.common.core.page.TableDataInfo;
@ -82,8 +83,17 @@ public class AimaTaskInstanceController extends BaseController {
@PreAuthorize("@ss.hasPermi('aima:taskinstance:edit')")
@Log(title = "任务执行实例", businessType = BusinessType.UPDATE)
@PutMapping("/start/{id}")
public AjaxResult start(@PathVariable("id") String id) {
return toAjax(aimaTaskInstanceService.startInstance(id));
public AjaxResult start(@PathVariable("id") String id,
@RequestBody(required = false) AimaTaskStartVO startVO) {
return toAjax(aimaTaskInstanceService.startInstance(id, startVO));
}
@ApiOperation("再次执行已完成或失败的爱玛任务")
@PreAuthorize("@ss.hasPermi('aima:taskinstance:edit')")
@Log(title = "任务执行实例", businessType = BusinessType.INSERT)
@PostMapping("/rerun/{id}")
public AjaxResult rerun(@PathVariable("id") String id) {
return success(aimaTaskInstanceService.rerunInstance(id));
}
/**

View File

@ -1,7 +1,7 @@
package com.cmvr.web.controller.aima;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
@ -58,6 +58,14 @@ public class AimaTestCaseController extends BaseController {
return toAjax(aimaTestCaseService.insertAimaTestCase(aimaTestCase));
}
@ApiOperation("复制测试用例")
@PreAuthorize("@ss.hasPermi('aima:testcase:add')")
@Log(title = "测试用例管理", businessType = BusinessType.INSERT)
@PostMapping("/copy/{id}")
public AjaxResult copy(@PathVariable String id) {
return success(aimaTestCaseService.copyAimaTestCase(id));
}
@ApiOperation("修改测试用例")
@PreAuthorize("@ss.hasPermi('aima:testcase:edit')")
@Log(title = "测试用例管理", businessType = BusinessType.UPDATE)

View File

@ -1,7 +1,7 @@
package com.cmvr.web.controller.aima;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;

View File

@ -1,7 +1,7 @@
package com.cmvr.web.controller.aima;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;

View File

@ -28,9 +28,9 @@ public class DeviceListController {
@PreAuthorize("@ss.hasPermi('device:profile:list')")
@GetMapping("/addList")
@ApiOperation("同步设备信息列表")
public int addDeviceList(@RequestParam String terminalId){
public int addDeviceList(@RequestParam String robotId){
List<DeDeviceRegistration> list = systemService.deviceList(terminalId);
List<DeDeviceRegistration> list = systemService.deviceList(robotId);
return registrationService.addDeviceList(list);
}

View File

@ -1,23 +1,27 @@
package com.cmvr.web.controller.api;
import cmvr.msgs.Agv;
import cmvr.msgs.AgvUtils;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.edge.client.model.EdgeCommonVO;
import com.cmvr.edge.client.model.agv.EdgeAgvMapNameVO;
import com.cmvr.edge.client.model.agv.EdgeAgvNavigateToPoseVO;
import com.cmvr.edge.client.model.agv.EdgeAgvNavigateToStationVO;
import com.cmvr.edge.client.model.agv.EdgeAgvRelocalizeVO;
import com.cmvr.edge.client.model.agv.EdgeAgvStartMappingVO;
import com.cmvr.edge.client.model.agv.EdgeAgvUploadMapVO;
import com.cmvr.edge.client.model.agv.EdgeAgvVelocityVO;
import com.cmvr.edge.client.model.agv.EdgeAgvTranslateVO;
import com.cmvr.edge.client.service.EdgeAgvService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
@ -48,8 +52,13 @@ public class EdgeAgvController {
*/
@ApiOperation("获取AGV运行时状态")
@GetMapping("/getRuntimeState")
public AjaxResult getRuntimeState(@Validated EdgeCommonVO vo) {
Agv.AgvRuntimeState state = edgeAgvService.getRuntimeState(vo);
public AjaxResult getRuntimeState(
@ApiParam(value = "机器人永久唯一ID", required = true)
@RequestParam("robotId") String robotId,
@ApiParam(value = "设备ID", required = true)
@RequestParam("deviceId") String deviceId) {
EdgeCommonVO vo = commonVo(robotId, deviceId);
AgvUtils.AgvRuntimeState state = edgeAgvService.getRuntimeState(vo);
return AjaxResult.ok(toRuntimeStateMap(state));
}
@ -58,8 +67,13 @@ public class EdgeAgvController {
*/
@ApiOperation("获取导航状态")
@GetMapping("/getNavigationStatus")
public AjaxResult getNavigationStatus(@Validated EdgeCommonVO vo) {
Agv.AgvNavigationStatus status = edgeAgvService.getNavigationStatus(vo);
public AjaxResult getNavigationStatus(
@ApiParam(value = "机器人永久唯一ID", required = true)
@RequestParam("robotId") String robotId,
@ApiParam(value = "设备ID", required = true)
@RequestParam("deviceId") String deviceId) {
EdgeCommonVO vo = commonVo(robotId, deviceId);
AgvUtils.AgvNavigationStatus status = edgeAgvService.getNavigationStatus(vo);
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("state", status.getState());
resultMap.put("type", status.getType());
@ -88,18 +102,52 @@ public class EdgeAgvController {
return AjaxResult.ok();
}
/**
* 在当前活动地图中重新设置 AGV 定位位姿。
*/
@ApiOperation("AGV重定位")
@PostMapping("/relocalize")
public AjaxResult relocalize(@RequestBody @Validated EdgeAgvRelocalizeVO vo) {
AgvUtils.AgvPose2d pose = AgvUtils.AgvPose2d.newBuilder()
.setX(vo.getX())
.setY(vo.getY())
.setTheta(vo.getTheta())
.build();
edgeAgvService.relocalize(vo, pose);
return AjaxResult.ok();
}
@ApiOperation("固定距离平移")
@PostMapping("/translate")
public AjaxResult translate(@RequestBody @Validated EdgeAgvTranslateVO vo) {
AgvUtils.AgvTranslationMode mode = vo.getMode() == null
? AgvUtils.AgvTranslationMode.AGV_TRANSLATION_MODE_ODOMETRY
: AgvUtils.AgvTranslationMode.forNumber(vo.getMode());
if (mode == null) {
return AjaxResult.error("距离参考模式不合法,允许值:0=里程,1=定位");
}
AgvUtils.AgvTranslation translation = AgvUtils.AgvTranslation.newBuilder()
.setDistance(vo.getDistance())
.setVx(vo.getVx())
.setVy(vo.getVy())
.setMode(mode)
.build();
edgeAgvService.translate(vo, translation);
return AjaxResult.ok();
}
/**
* 导航到指定坐标。
*/
@ApiOperation("导航到指定位置")
@PostMapping("/navigateToPose")
public AjaxResult navigateToPose(@RequestBody @Validated EdgeAgvNavigateToPoseVO vo) {
Agv.AgvPose2d pose = Agv.AgvPose2d.newBuilder()
AgvUtils.AgvPose2d pose = AgvUtils.AgvPose2d.newBuilder()
.setX(vo.getX())
.setY(vo.getY())
.setTheta(vo.getTheta())
.build();
edgeAgvService.navigateToPose(vo, pose);
edgeAgvService.navigateToPose(vo, pose, navigationOptions(vo.getBlockedTimeoutMs()));
return AjaxResult.ok();
}
@ -109,7 +157,7 @@ public class EdgeAgvController {
@ApiOperation("导航到站点")
@PostMapping("/navigateToStation")
public AjaxResult navigateToStation(@RequestBody @Validated EdgeAgvNavigateToStationVO vo) {
edgeAgvService.navigateToStation(vo, vo.getStationId());
edgeAgvService.navigateToStation(vo, vo.getStationId(), navigationOptions(vo.getBlockedTimeoutMs()));
return AjaxResult.ok();
}
@ -149,7 +197,7 @@ public class EdgeAgvController {
@ApiOperation("设置速度")
@PostMapping("/setVelocity")
public AjaxResult setVelocity(@RequestBody @Validated EdgeAgvVelocityVO vo) {
Agv.AgvVelocity velocity = Agv.AgvVelocity.newBuilder()
AgvUtils.AgvVelocity velocity = AgvUtils.AgvVelocity.newBuilder()
.setVx(vo.getVx())
.setVy(vo.getVy())
.setWz(vo.getWz())
@ -173,7 +221,12 @@ public class EdgeAgvController {
*/
@ApiOperation("列出所有地图")
@GetMapping("/listMaps")
public AjaxResult listMaps(@Validated EdgeCommonVO vo) {
public AjaxResult listMaps(
@ApiParam(value = "机器人永久唯一ID", required = true)
@RequestParam("robotId") String robotId,
@ApiParam(value = "设备ID", required = true)
@RequestParam("deviceId") String deviceId) {
EdgeCommonVO vo = commonVo(robotId, deviceId);
List<String> maps = edgeAgvService.listMaps(vo);
return AjaxResult.ok(maps);
}
@ -183,10 +236,15 @@ public class EdgeAgvController {
*/
@ApiOperation("列出所有站点")
@GetMapping("/listStations")
public AjaxResult listStations(@Validated EdgeCommonVO vo) {
List<Agv.AgvStation> stations = edgeAgvService.listStations(vo);
public AjaxResult listStations(
@ApiParam(value = "机器人永久ID", required = true)
@RequestParam("robotId") String robotId,
@ApiParam(value = "设备ID", required = true)
@RequestParam("deviceId") String deviceId) {
EdgeCommonVO vo = commonVo(robotId, deviceId);
List<AgvUtils.AgvStation> stations = edgeAgvService.listStations(vo);
List<Map<String, Object>> stationList = new ArrayList<>();
for (Agv.AgvStation station : stations) {
for (AgvUtils.AgvStation station : stations) {
stationList.add(toStationMap(station));
}
return AjaxResult.ok(stationList);
@ -217,7 +275,17 @@ public class EdgeAgvController {
*/
@ApiOperation("下载地图")
@GetMapping("/downloadMap")
public AjaxResult downloadMap(@Validated EdgeAgvMapNameVO vo) {
public AjaxResult downloadMap(
@ApiParam(value = "机器人永久唯一ID", required = true)
@RequestParam("robotId") String robotId,
@ApiParam(value = "设备ID", required = true)
@RequestParam("deviceId") String deviceId,
@ApiParam(value = "地图名称", required = true)
@RequestParam("mapName") String mapName) {
EdgeAgvMapNameVO vo = new EdgeAgvMapNameVO();
vo.setRobotId(robotId);
vo.setDeviceId(deviceId);
vo.setMapName(mapName);
String content = edgeAgvService.downloadMap(vo, vo.getMapName());
return AjaxResult.ok(content);
}
@ -228,7 +296,7 @@ public class EdgeAgvController {
@ApiOperation("开始建图")
@PostMapping("/startMapping")
public AjaxResult startMapping(@RequestBody @Validated EdgeAgvStartMappingVO vo) {
Agv.AgvMapDimension mapDimension = Agv.AgvMapDimension.forNumber(vo.getDimension());
AgvUtils.AgvMapDimension mapDimension = AgvUtils.AgvMapDimension.forNumber(vo.getDimension());
if (mapDimension == null) {
return AjaxResult.error("建图维度不合法,允许值:0=未指定,1=2D,2=3D,3=2D+3D");
}
@ -252,7 +320,23 @@ public class EdgeAgvController {
/**
* 将 protobuf 运行时状态转换为前端友好的 Map,避免直接暴露 protobuf 对象结构。
*/
private Map<String, Object> toRuntimeStateMap(Agv.AgvRuntimeState state) {
private EdgeCommonVO commonVo(String robotId, String deviceId) {
EdgeCommonVO vo = new EdgeCommonVO();
vo.setRobotId(robotId);
vo.setDeviceId(deviceId);
return vo;
}
private AgvUtils.AgvMotionOptions navigationOptions(Integer blockedTimeoutMs) {
int timeout = blockedTimeoutMs == null
? EdgeAgvService.DEFAULT_BLOCKED_TIMEOUT_MS : blockedTimeoutMs;
return AgvUtils.AgvMotionOptions.newBuilder()
.setAsynchronous(false)
.setBlockedTimeoutMs(timeout)
.build();
}
private Map<String, Object> toRuntimeStateMap(AgvUtils.AgvRuntimeState state) {
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("timestamp", state.getTimestamp());
resultMap.put("mode", state.getMode());
@ -291,7 +375,7 @@ public class EdgeAgvController {
/**
* 将 protobuf 站点对象转换为普通 Map。
*/
private Map<String, Object> toStationMap(Agv.AgvStation station) {
private Map<String, Object> toStationMap(AgvUtils.AgvStation station) {
Map<String, Object> stationMap = new HashMap<>();
stationMap.put("id", station.getId());
stationMap.put("type", station.getType());
@ -305,7 +389,7 @@ public class EdgeAgvController {
/**
* 将 protobuf 二维位姿转换为普通 Map。
*/
private Map<String, Object> toPoseMap(Agv.AgvPose2d pose) {
private Map<String, Object> toPoseMap(AgvUtils.AgvPose2d pose) {
Map<String, Object> poseMap = new HashMap<>();
poseMap.put("x", pose.getX());
poseMap.put("y", pose.getY());

View File

@ -4,15 +4,18 @@ import cmvr.api.ArmCommand;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.edge.client.model.EdgeCommonVO;
import com.cmvr.edge.client.model.arm.*;
import com.cmvr.edge.client.model.system.EdgeSystemJsonCommandVO;
import com.cmvr.edge.client.service.EdgeArmService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
@ -31,14 +34,20 @@ public class EdgeArmController {
@ApiOperation("关闭力矩")
@GetMapping("/torqueOff")
public AjaxResult torqueOff(EdgeCommonVO vo) {
public AjaxResult torqueOff(
@ApiParam(value = "机器人永久唯一ID", required = true) @RequestParam("robotId") String robotId,
@ApiParam(value = "设备ID", required = true) @RequestParam("deviceId") String deviceId) {
EdgeCommonVO vo = commonVo(robotId, deviceId);
edgeArmService.torqueOff(vo);
return AjaxResult.ok();
}
@ApiOperation("开启力矩")
@GetMapping("/torqueOn")
public AjaxResult torqueOn(EdgeCommonVO vo) {
public AjaxResult torqueOn(
@ApiParam(value = "机器人永久唯一ID", required = true) @RequestParam("robotId") String robotId,
@ApiParam(value = "设备ID", required = true) @RequestParam("deviceId") String deviceId) {
EdgeCommonVO vo = commonVo(robotId, deviceId);
edgeArmService.torqueOn(vo);
return AjaxResult.ok();
}
@ -48,11 +57,21 @@ public class EdgeArmController {
*/
@ApiOperation("清除故障")
@GetMapping("/clearFault")
public AjaxResult clearFault(EdgeCommonVO vo) {
public AjaxResult clearFault(
@ApiParam(value = "机器人永久唯一ID", required = true) @RequestParam("robotId") String robotId,
@ApiParam(value = "设备ID", required = true) @RequestParam("deviceId") String deviceId) {
EdgeCommonVO vo = commonVo(robotId, deviceId);
edgeArmService.clearFault(vo);
return AjaxResult.ok();
}
@ApiOperation("执行机械臂厂商扩展JSON指令")
@PostMapping("/executeJsonCommand")
public AjaxResult executeJsonCommand(@RequestBody @Validated EdgeSystemJsonCommandVO request) {
return AjaxResult.ok(com.alibaba.fastjson2.JSON.toJSONString(
edgeArmService.executeJsonCommand(request)));
}
@ApiOperation("关节空间运动")
@PostMapping("/moveJ")
public AjaxResult moveJ(@RequestBody EdgeArmMoveJVO moveJVO) {
@ -126,14 +145,20 @@ public class EdgeArmController {
@ApiOperation("停止运动")
@GetMapping("/stopMotion")
public AjaxResult stopMotion(EdgeCommonVO vo) {
public AjaxResult stopMotion(
@ApiParam(value = "机器人永久唯一ID", required = true) @RequestParam("robotId") String robotId,
@ApiParam(value = "设备ID", required = true) @RequestParam("deviceId") String deviceId) {
EdgeCommonVO vo = commonVo(robotId, deviceId);
edgeArmService.stopMotion(vo);
return AjaxResult.ok();
}
@ApiOperation("获取关节状态")
@GetMapping("/getJointState")
public AjaxResult getJointState(EdgeCommonVO vo) {
public AjaxResult getJointState(
@ApiParam(value = "机器人永久唯一ID", required = true) @RequestParam("robotId") String robotId,
@ApiParam(value = "设备ID", required = true) @RequestParam("deviceId") String deviceId) {
EdgeCommonVO vo = commonVo(robotId, deviceId);
ArmCommand.JointResponse result = edgeArmService.getJointState(vo);
ArmCommand.JointState state = result.getState();
@ -150,7 +175,12 @@ public class EdgeArmController {
@ApiOperation("获取末端位姿")
@GetMapping("/getPose")
public AjaxResult getPose(@Validated EdgeArmPoseQueryVO vo) {
public AjaxResult getPose(
@ApiParam(value = "机器人永久唯一ID", required = true) @RequestParam("robotId") String robotId,
@ApiParam(value = "设备ID", required = true) @RequestParam("deviceId") String deviceId,
@ApiParam("基座连杆名称") @RequestParam(value = "baseLink", required = false) String baseLink,
@ApiParam("末端执行器连杆名称") @RequestParam(value = "eeLink", required = false) String eeLink) {
EdgeArmPoseQueryVO vo = poseQueryVo(robotId, deviceId, baseLink, eeLink);
ArmCommand.GetPose.Response result = edgeArmService.getPose(vo, vo.getBaseLink(), vo.getEeLink());
ArmCommand.CartesianPose pose = result.getPose();
@ -168,14 +198,26 @@ public class EdgeArmController {
@ApiOperation("标定零点")
@GetMapping("/calibrateZeroQ")
public AjaxResult calibrateZeroQ(@Validated EdgeArmCalibrateZeroQVO vo) {
public AjaxResult calibrateZeroQ(
@ApiParam(value = "机器人永久唯一ID", required = true) @RequestParam("robotId") String robotId,
@ApiParam(value = "设备ID", required = true) @RequestParam("deviceId") String deviceId,
@ApiParam(value = "关节名称", required = true) @RequestParam("jointName") String jointName) {
EdgeArmCalibrateZeroQVO vo = new EdgeArmCalibrateZeroQVO();
vo.setRobotId(robotId);
vo.setDeviceId(deviceId);
vo.setJointName(jointName);
edgeArmService.calibrateZeroQ(vo, vo.getJointName());
return AjaxResult.ok();
}
@ApiOperation("获取位姿矩阵")
@GetMapping("/getPoseMatrix")
public AjaxResult getPoseMatrix(@Validated EdgeArmPoseQueryVO vo) {
public AjaxResult getPoseMatrix(
@ApiParam(value = "机器人永久唯一ID", required = true) @RequestParam("robotId") String robotId,
@ApiParam(value = "设备ID", required = true) @RequestParam("deviceId") String deviceId,
@ApiParam("基座连杆名称") @RequestParam(value = "baseLink", required = false) String baseLink,
@ApiParam("末端执行器连杆名称") @RequestParam(value = "eeLink", required = false) String eeLink) {
EdgeArmPoseQueryVO vo = poseQueryVo(robotId, deviceId, baseLink, eeLink);
ArmCommand.GetPoseMatrix.Response result = edgeArmService.getPoseMatrix(vo, vo.getBaseLink(), vo.getEeLink());
// 将TransformMatrix4x4转换为Map
ArmCommand.TransformMatrix4x4 matrix = result.getMatrix();
@ -229,4 +271,20 @@ public class EdgeArmController {
matrixMap.put("m33", matrix.getM33());
return AjaxResult.ok(matrixMap);
}
private EdgeCommonVO commonVo(String robotId, String deviceId) {
EdgeCommonVO vo = new EdgeCommonVO();
vo.setRobotId(robotId);
vo.setDeviceId(deviceId);
return vo;
}
private EdgeArmPoseQueryVO poseQueryVo(String robotId, String deviceId, String baseLink, String eeLink) {
EdgeArmPoseQueryVO vo = new EdgeArmPoseQueryVO();
vo.setRobotId(robotId);
vo.setDeviceId(deviceId);
vo.setBaseLink(baseLink);
vo.setEeLink(eeLink);
return vo;
}
}

View File

@ -0,0 +1,62 @@
package com.cmvr.web.controller.api;
import com.alibaba.fastjson2.JSON;
import com.cmvr.common.core.controller.BaseController;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopHeartbeatVO;
import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopOpenVO;
import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopSetpointVO;
import com.cmvr.edge.client.model.armteleop.EdgeArmTeleopStopVO;
import com.cmvr.edge.client.service.EdgeArmTeleopService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Api(tags = "Edge - Arm teleoperation")
@RestController
@RequestMapping("/api/edge/armTeleop")
@RequiredArgsConstructor
public class EdgeArmTeleopController extends BaseController {
private final EdgeArmTeleopService edgeArmTeleopService;
@ApiOperation("Open an arm teleoperation session")
@PostMapping("/open")
public AjaxResult open(@RequestBody EdgeArmTeleopOpenVO request) {
return success(edgeArmTeleopService.open(request));
}
@ApiOperation("Send an arm joint setpoint")
@PostMapping("/setpoint")
public AjaxResult setpoint(@RequestBody EdgeArmTeleopSetpointVO request) {
edgeArmTeleopService.sendSetpoint(request);
return success();
}
@ApiOperation("Send a teleoperation heartbeat")
@PostMapping("/heartbeat")
public AjaxResult heartbeat(@RequestBody EdgeArmTeleopHeartbeatVO request) {
edgeArmTeleopService.heartbeat(request);
return success();
}
@ApiOperation("Get the latest teleoperation state")
@GetMapping("/status")
public AjaxResult status(@RequestParam("sessionId") String sessionId) {
Object status = edgeArmTeleopService.getStatus(sessionId);
return success(status == null ? null : JSON.toJSONString(status));
}
@ApiOperation("Stop an arm teleoperation session")
@PostMapping("/stop")
public AjaxResult stop(@RequestBody EdgeArmTeleopStopVO request) {
edgeArmTeleopService.stop(request);
return success();
}
}

View File

@ -4,13 +4,24 @@ import com.alibaba.fastjson2.JSON;
import com.cmvr.common.core.controller.BaseController;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.edge.client.model.EdgeCommonVO;
import com.cmvr.edge.client.model.camera.EdgeCameraPtzVO;
import com.cmvr.edge.client.service.EdgeCameraService;
import com.cmvr.web.service.CameraStreamTicketService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
@Api(tags = "边缘--相机")
@RestController
@ -19,46 +30,127 @@ import org.springframework.web.bind.annotation.RestController;
public class EdgeCameraController extends BaseController {
private final EdgeCameraService edgeCameraService;
private final CameraStreamTicketService cameraStreamTicketService;
@ApiOperation("获取相机状态")
@GetMapping("/status")
public AjaxResult status(EdgeCommonVO edgeCommonVO) {
public AjaxResult status(
@ApiParam(value = "机器人永久唯一ID", required = true) @RequestParam("robotId") String robotId,
@ApiParam(value = "设备ID", required = true) @RequestParam("deviceId") String deviceId) {
EdgeCommonVO edgeCommonVO = commonVo(robotId, deviceId);
return success(JSON.toJSONString(edgeCameraService.status(edgeCommonVO)));
}
@ApiOperation("开启相机")
@GetMapping("/start")
public AjaxResult start(EdgeCommonVO edgeCommonVO) {
public AjaxResult start(
@ApiParam(value = "机器人永久唯一ID", required = true) @RequestParam("robotId") String robotId,
@ApiParam(value = "设备ID", required = true) @RequestParam("deviceId") String deviceId) {
EdgeCommonVO edgeCommonVO = commonVo(robotId, deviceId);
return success(edgeCameraService.start(edgeCommonVO));
}
@ApiOperation("关闭相机")
@GetMapping("/stop")
public AjaxResult stop(EdgeCommonVO edgeCommonVO) {
public AjaxResult stop(
@ApiParam(value = "机器人永久唯一ID", required = true) @RequestParam("robotId") String robotId,
@ApiParam(value = "设备ID", required = true) @RequestParam("deviceId") String deviceId) {
EdgeCommonVO edgeCommonVO = commonVo(robotId, deviceId);
return success(edgeCameraService.stop(edgeCommonVO));
}
@ApiOperation("获取图片")
@GetMapping("/getRGBImage")
public AjaxResult getRGBImage(EdgeCommonVO edgeCommonVO) {
public AjaxResult getRGBImage(
@ApiParam(value = "机器人永久唯一ID", required = true) @RequestParam("robotId") String robotId,
@ApiParam(value = "设备ID", required = true) @RequestParam("deviceId") String deviceId) {
EdgeCommonVO edgeCommonVO = commonVo(robotId, deviceId);
return success(edgeCameraService.getRGBImage(edgeCommonVO));
}
@ApiOperation("Get one depth image")
@GetMapping("/getDepthImage")
public AjaxResult getDepthImage(
@RequestParam("robotId") String robotId,
@RequestParam("deviceId") String deviceId) {
return success(edgeCameraService.getDepthImage(commonVo(robotId, deviceId)));
}
@ApiOperation("Control camera PTZ")
@PostMapping("/controlPtz")
public AjaxResult controlPtz(@RequestBody EdgeCameraPtzVO request) {
return success(edgeCameraService.controlPtz(request));
}
@ApiOperation("Get a short-lived RGB video stream URL")
@GetMapping("/rgb-stream-url")
public AjaxResult rgbStreamUrl(
@RequestParam("robotId") String robotId,
@RequestParam("deviceId") String deviceId) {
String ticket = cameraStreamTicketService.issue(robotId, deviceId);
return success((Object) ("/api/edge/camera/rgb-stream?ticket=" + ticket));
}
@ApiOperation("RGB fragmented MP4 live stream")
@GetMapping(value = "/rgb-stream", produces = "video/mp4")
public ResponseEntity<StreamingResponseBody> rgbStream(@RequestParam("ticket") String ticket) {
CameraStreamTicketService.StreamTarget target = cameraStreamTicketService.consume(ticket);
if (target == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
return videoStreamResponse(target.robotId(), target.deviceId());
}
@ApiOperation("Public RGB fragmented MP4 live stream")
@GetMapping(value = "/rgb-stream/public", produces = "video/mp4")
public ResponseEntity<StreamingResponseBody> publicRgbStream(
@RequestParam("robotId") String robotId,
@RequestParam("deviceId") String deviceId) {
return videoStreamResponse(robotId, deviceId);
}
private ResponseEntity<StreamingResponseBody> videoStreamResponse(String robotId, String deviceId) {
StreamingResponseBody body = outputStream -> edgeCameraService.writeRGBVideoStream(
robotId, deviceId, outputStream);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("video/mp4"))
.cacheControl(CacheControl.noStore())
.header("X-Accel-Buffering", "no")
.header("X-Content-Type-Options", "nosniff")
.body(body);
}
@ApiOperation("开始录制视频")
@GetMapping("/startRecording")
public AjaxResult startRecording(EdgeCommonVO edgeCommonVO) {
public AjaxResult startRecording(
@ApiParam(value = "机器人永久唯一ID", required = true) @RequestParam("robotId") String robotId,
@ApiParam(value = "设备ID", required = true) @RequestParam("deviceId") String deviceId) {
EdgeCommonVO edgeCommonVO = commonVo(robotId, deviceId);
return success(edgeCameraService.startRecording(edgeCommonVO));
}
@ApiOperation("停止录制视频")
@GetMapping("/stopRecording")
public AjaxResult stopRecording(EdgeCommonVO edgeCommonVO) {
public AjaxResult stopRecording(
@ApiParam(value = "机器人永久唯一ID", required = true) @RequestParam("robotId") String robotId,
@ApiParam(value = "设备ID", required = true) @RequestParam("deviceId") String deviceId) {
EdgeCommonVO edgeCommonVO = commonVo(robotId, deviceId);
return success(edgeCameraService.stopRecording(edgeCommonVO));
}
@ApiOperation("获取图片和视频")
@GetMapping("/getRGBDImages")
public AjaxResult getRGBDImages(EdgeCommonVO edgeCommonVO) {
public AjaxResult getRGBDImages(
@ApiParam(value = "机器人永久唯一ID", required = true) @RequestParam("robotId") String robotId,
@ApiParam(value = "设备ID", required = true) @RequestParam("deviceId") String deviceId) {
EdgeCommonVO edgeCommonVO = commonVo(robotId, deviceId);
return success(edgeCameraService.getRGBDImages(edgeCommonVO));
}
private EdgeCommonVO commonVo(String robotId, String deviceId) {
EdgeCommonVO vo = new EdgeCommonVO();
vo.setRobotId(robotId);
vo.setDeviceId(deviceId);
return vo;
}
}

View File

@ -25,9 +25,9 @@ public class EdgeDexHandController extends BaseController {
@GetMapping("/status")
public AjaxResult status(
@RequestParam("deviceId") String deviceId,
@RequestParam("terminalId") String terminalId
@RequestParam("robotId") String robotId
) {
return success(JSON.parseObject(JSON.toJSONString(edgeDexHandService.getStatus(terminalId, deviceId)), Map.class));
return success(JSON.parseObject(JSON.toJSONString(edgeDexHandService.getStatus(robotId, deviceId)), Map.class));
}
@ -35,11 +35,11 @@ public class EdgeDexHandController extends BaseController {
@GetMapping("/angle")
public AjaxResult setAngle(
@RequestParam("deviceId") String deviceId,
@RequestParam("terminalId") String terminalId,
@RequestParam("robotId") String robotId,
@RequestParam("id") int id,
@RequestParam("value") float value
) {
edgeDexHandService.setDexHandAngle(terminalId, deviceId, id, value);
edgeDexHandService.setDexHandAngle(robotId, deviceId, id, value);
return success("ok");
}
@ -48,8 +48,8 @@ public class EdgeDexHandController extends BaseController {
@GetMapping("/sensor")
public AjaxResult sensor(
@RequestParam("deviceId") String deviceId,
@RequestParam("terminalId") String terminalId
@RequestParam("robotId") String robotId
) {
return success(JSON.parseObject(edgeDexHandService.getSensorData(terminalId, deviceId), Map.class));
return success(JSON.parseObject(edgeDexHandService.getSensorData(robotId, deviceId), Map.class));
}
}

View File

@ -25,9 +25,9 @@ public class EdgeMicrophoneController {
@ApiOperation("获取麦克风状态")
@PreAuthorize("@ss.hasPermi('system:microphone:query')")
@GetMapping("/status")
public AjaxResult getStatus(@ApiParam(value = "终端设备ID", required = true) @RequestParam String terminalId, @ApiParam (value = "设备ID", required = true) @RequestParam String deviceId) {
public AjaxResult getStatus(@ApiParam(value = "机器人永久唯一ID", required = true) @RequestParam String robotId, @ApiParam (value = "设备ID", required = true) @RequestParam String deviceId) {
EdgeCommonVO edgeCommonVO = new EdgeCommonVO();
edgeCommonVO.setTerminalId(terminalId);
edgeCommonVO.setRobotId(robotId);
edgeCommonVO.setDeviceId(deviceId);
return AjaxResult.success(microphoneService.getStatus(edgeCommonVO));
}
@ -44,8 +44,7 @@ public class EdgeMicrophoneController {
@PreAuthorize("@ss.hasPermi('system:microphone:stop')")
@PostMapping("/stop")
public AjaxResult stopRecord(@RequestBody EdgeCommonVO edgeCommonVO) {
microphoneService.stopRecord(edgeCommonVO);
return AjaxResult.success();
return AjaxResult.success(microphoneService.stopRecord(edgeCommonVO));
}
@ApiOperation("暂停录音")
@ -75,9 +74,9 @@ public class EdgeMicrophoneController {
@ApiOperation("获取音量")
@PreAuthorize("@ss.hasPermi('system:microphone:volume')")
@GetMapping("/volume")
public AjaxResult getVolume(@ApiParam(value = "终端设备ID", required = true) @RequestParam String terminalId, @ApiParam (value = "设备ID", required = true) @RequestParam String deviceId) {
public AjaxResult getVolume(@ApiParam(value = "机器人永久唯一ID", required = true) @RequestParam String robotId, @ApiParam (value = "设备ID", required = true) @RequestParam String deviceId) {
EdgeCommonVO edgeCommonVO = new EdgeCommonVO();
edgeCommonVO.setTerminalId(terminalId);
edgeCommonVO.setRobotId(robotId);
edgeCommonVO.setDeviceId(deviceId);
return AjaxResult.success(microphoneService.getVolume(edgeCommonVO));
}

View File

@ -0,0 +1,114 @@
package com.cmvr.web.controller.api;
import com.alibaba.fastjson2.JSON;
import com.cmvr.common.core.controller.BaseController;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.edge.client.model.motor.EdgeMotorCyclicOpenVO;
import com.cmvr.edge.client.model.motor.EdgeMotorCyclicSetpointVO;
import com.cmvr.edge.client.model.motor.EdgeMotorEnabledVO;
import com.cmvr.edge.client.model.motor.EdgeMotorMotionVO;
import com.cmvr.edge.client.model.motor.EdgeMotorTargetVO;
import com.cmvr.edge.client.service.EdgeMotorService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Api(tags = "Edge - Motor")
@RestController
@RequestMapping("/api/edge/motor")
@RequiredArgsConstructor
public class EdgeMotorController extends BaseController {
private final EdgeMotorService edgeMotorService;
@ApiOperation("Set the current motor position as zero")
@PostMapping("/setZero")
public AjaxResult setZero(@RequestBody EdgeMotorTargetVO request) {
return protobuf(edgeMotorService.setZero(request));
}
@ApiOperation("Move motor to zero")
@PostMapping("/moveToZero")
public AjaxResult moveToZero(@RequestBody EdgeMotorMotionVO request) {
return protobuf(edgeMotorService.moveToZero(request));
}
@ApiOperation("Execute profile position control")
@PostMapping("/profilePosition")
public AjaxResult profilePosition(@RequestBody EdgeMotorMotionVO request) {
return protobuf(edgeMotorService.profilePosition(request));
}
@ApiOperation("Execute profile velocity control")
@PostMapping("/profileVelocity")
public AjaxResult profileVelocity(@RequestBody EdgeMotorMotionVO request) {
return protobuf(edgeMotorService.profileVelocity(request));
}
@ApiOperation("Emergency stop a motor")
@PostMapping("/emergencyStop")
public AjaxResult emergencyStop(@RequestBody EdgeMotorTargetVO request) {
return protobuf(edgeMotorService.emergencyStop(request));
}
@ApiOperation("Get motor status")
@PostMapping("/status")
public AjaxResult status(@RequestBody EdgeMotorTargetVO request) {
return protobuf(edgeMotorService.getStatus(request));
}
@ApiOperation("Enable or disable a motor")
@PostMapping("/enabled")
public AjaxResult enabled(@RequestBody EdgeMotorEnabledVO request) {
return protobuf(edgeMotorService.setEnabled(request));
}
@ApiOperation("Open cyclic position stream")
@PostMapping("/cyclicPosition/open")
public AjaxResult openCyclicPosition(@RequestBody EdgeMotorCyclicOpenVO request) {
return success(edgeMotorService.openCyclicPosition(request));
}
@ApiOperation("Send cyclic position setpoint")
@PostMapping("/cyclicPosition/setpoint")
public AjaxResult cyclicPosition(@RequestBody EdgeMotorCyclicSetpointVO request) {
edgeMotorService.sendCyclicPosition(request);
return success();
}
@ApiOperation("Open cyclic velocity stream")
@PostMapping("/cyclicVelocity/open")
public AjaxResult openCyclicVelocity(@RequestBody EdgeMotorCyclicOpenVO request) {
return success(edgeMotorService.openCyclicVelocity(request));
}
@ApiOperation("Send cyclic velocity setpoint")
@PostMapping("/cyclicVelocity/setpoint")
public AjaxResult cyclicVelocity(@RequestBody EdgeMotorCyclicSetpointVO request) {
edgeMotorService.sendCyclicVelocity(request);
return success();
}
@ApiOperation("Get the latest cyclic stream response")
@GetMapping("/stream/status")
public AjaxResult streamStatus(@RequestParam("sessionId") String sessionId) {
return protobuf(edgeMotorService.getStreamStatus(sessionId));
}
@ApiOperation("Close a cyclic stream")
@PostMapping("/stream/close")
public AjaxResult closeStream(@RequestParam("sessionId") String sessionId) {
edgeMotorService.closeStream(sessionId);
return success();
}
private AjaxResult protobuf(Object value) {
return success(value == null ? null : JSON.toJSONString(value));
}
}

View File

@ -33,13 +33,13 @@ public class EdgeSpeakerController {
@PreAuthorize("@ss.hasPermi('system:speaker:query')")
@GetMapping("/status")
public AjaxResult getStatus(
@ApiParam(value = "终端设备ID", required = true)
@RequestParam String terminalId,
@ApiParam(value = "机器人永久唯一ID", required = true)
@RequestParam String robotId,
@ApiParam(value = "设备ID", required = true)
@RequestParam String deviceId) {
try {
// 获取扬声器状态
SpeakerCommand.GetSpeakerStateCommand.Feedback feedback = speakerService.getStatus(terminalId, deviceId);
SpeakerCommand.GetSpeakerStateCommand.Feedback feedback = speakerService.getStatus(robotId, deviceId);
SpeakerCommand.SpeakerState state = feedback.getState();
// 构建返回结果
@ -73,7 +73,7 @@ public class EdgeSpeakerController {
@PreAuthorize("@ss.hasPermi('system:speaker:play')")
@PostMapping("/play")
public AjaxResult playAudio(@RequestBody EdgeSpeakerPlayAudioVO edgeSpeakerPlayAudioVO) {
speakerService.playAudio(edgeSpeakerPlayAudioVO.getTerminalId(), edgeSpeakerPlayAudioVO.getDeviceId(), edgeSpeakerPlayAudioVO.getAudioPath());
speakerService.playAudio(edgeSpeakerPlayAudioVO.getRobotId(), edgeSpeakerPlayAudioVO.getDeviceId(), edgeSpeakerPlayAudioVO.getAudioPath());
return AjaxResult.success();
}
@ -84,7 +84,7 @@ public class EdgeSpeakerController {
@PreAuthorize("@ss.hasPermi('system:speaker:stop')")
@PostMapping("/stop")
public AjaxResult stopPlayback(@RequestBody EdgeCommonVO edgeCommonVO) {
speakerService.stopPlayback(edgeCommonVO.getTerminalId(), edgeCommonVO.getDeviceId());
speakerService.stopPlayback(edgeCommonVO.getRobotId(), edgeCommonVO.getDeviceId());
return AjaxResult.success();
}
@ -95,7 +95,7 @@ public class EdgeSpeakerController {
@PreAuthorize("@ss.hasPermi('system:speaker:pause')")
@PostMapping("/pause")
public AjaxResult pausePlayback(@RequestBody EdgeCommonVO edgeCommonVO) {
speakerService.pausePlayback(edgeCommonVO.getTerminalId(), edgeCommonVO.getDeviceId());
speakerService.pausePlayback(edgeCommonVO.getRobotId(), edgeCommonVO.getDeviceId());
return AjaxResult.success();
}
@ -106,7 +106,7 @@ public class EdgeSpeakerController {
@PreAuthorize("@ss.hasPermi('system:speaker:resume')")
@PostMapping("/resume")
public AjaxResult resumePlayback(@RequestBody EdgeCommonVO edgeCommonVO) {
speakerService.resumePlayback(edgeCommonVO.getTerminalId(), edgeCommonVO.getDeviceId());
speakerService.resumePlayback(edgeCommonVO.getRobotId(), edgeCommonVO.getDeviceId());
return AjaxResult.success();
}
@ -117,7 +117,7 @@ public class EdgeSpeakerController {
@PreAuthorize("@ss.hasPermi('system:speaker:volume')")
@PostMapping("/volume")
public AjaxResult setVolume(@RequestBody EdgeMicrophoneVolumeVO edgeMicrophoneVolumeVO) {
speakerService.setVolume(edgeMicrophoneVolumeVO.getTerminalId(), edgeMicrophoneVolumeVO.getDeviceId(), edgeMicrophoneVolumeVO.getVolume());
speakerService.setVolume(edgeMicrophoneVolumeVO.getRobotId(), edgeMicrophoneVolumeVO.getDeviceId(), edgeMicrophoneVolumeVO.getVolume());
return AjaxResult.success();
}
@ -128,11 +128,11 @@ public class EdgeSpeakerController {
@PreAuthorize("@ss.hasPermi('system:speaker:volume')")
@GetMapping("/volume")
public AjaxResult getVolume(
@ApiParam(value = "终端设备ID", required = true)
@RequestParam String terminalId,
@ApiParam(value = "机器人永久唯一ID", required = true)
@RequestParam String robotId,
@ApiParam(value = "设备ID", required = true)
@RequestParam String deviceId) {
return AjaxResult.success(speakerService.getVolume(terminalId, deviceId));
return AjaxResult.success(speakerService.getVolume(robotId, deviceId));
}
@ApiOperation("获取所有音频")

View File

@ -1,14 +1,23 @@
package com.cmvr.web.controller.api;
import com.alibaba.fastjson2.JSON;
import com.cmvr.common.core.controller.BaseController;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.edge.client.model.system.EdgeSystemJsonCommandVO;
import com.cmvr.edge.client.model.system.EdgeSystemUpdateParamsVO;
import com.cmvr.edge.client.model.system.EdgeActionQueueVO;
import com.cmvr.edge.client.model.system.EdgeSafetyRecoveryVO;
import com.cmvr.edge.client.service.EdgeSystemService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.validation.annotation.Validated;
@Api(tags = "边缘--系统功能")
@RestController
@ -18,9 +27,63 @@ public class EdgeSystemController extends BaseController {
private final EdgeSystemService edgeSystemService;
@ApiOperation("Get edge system information")
@GetMapping("/info")
public AjaxResult info(@RequestParam("robotId") String robotId) {
return success(JSON.toJSONString(edgeSystemService.getSystemInfo(robotId)));
}
@ApiOperation("Get edge system runtime status")
@GetMapping("/status")
public AjaxResult status(@RequestParam("robotId") String robotId) {
return success(JSON.toJSONString(edgeSystemService.getSystemStatus(robotId)));
}
@ApiOperation("Update edge device parameters")
@PostMapping("/params")
public AjaxResult updateParams(@RequestBody EdgeSystemUpdateParamsVO request) {
return success(JSON.toJSONString(edgeSystemService.updateParams(request)));
}
@ApiOperation("Execute a JSON device command")
@PostMapping("/executeJsonCommand")
public AjaxResult executeJsonCommand(@RequestBody EdgeSystemJsonCommandVO request) {
return success(JSON.toJSONString(edgeSystemService.executeJsonCommand(request)));
}
@ApiOperation("Execute an edge-local serial action queue")
@PostMapping("/actionQueue")
public AjaxResult executeActionQueue(@RequestBody EdgeActionQueueVO request) {
return success(JSON.toJSONString(edgeSystemService.executeActionQueue(request)));
}
@ApiOperation("Get edge safety state")
@GetMapping("/safetyState")
public AjaxResult safetyState(@RequestParam("robotId") String robotId) {
return success(JSON.toJSONString(edgeSystemService.getSafetyState(robotId)));
}
@ApiOperation("Restore edge operational state")
@PostMapping("/restoreOperationalState")
public AjaxResult restoreOperationalState(@RequestBody @Validated EdgeSafetyRecoveryVO request) {
return success(JSON.toJSONString(edgeSystemService.restoreOperationalState(
request.getRobotId(), request.getReason(), request.getTimeoutMs())));
}
/**
* @deprecated Use /restoreOperationalState.
*/
@Deprecated
@ApiOperation("[Deprecated] Clear recoverable edge safety errors")
@PostMapping("/recoverSafetyState")
public AjaxResult recoverSafetyState(@RequestBody @Validated EdgeSafetyRecoveryVO request) {
return success(JSON.toJSONString(edgeSystemService.recoverSafetyState(
request.getRobotId(), request.getReason(), request.getTimeoutMs())));
}
@ApiOperation("停止所有设备")
@GetMapping("/stopAll")
public AjaxResult stopAll(String terminalId) {
return AjaxResult.ok(edgeSystemService.stopAll(terminalId));
public AjaxResult stopAll(@RequestParam("robotId") String robotId) {
return AjaxResult.ok(edgeSystemService.stopAll(robotId));
}
}

View File

@ -1,8 +1,8 @@
package com.cmvr.web.controller.api;
import com.cmvr.edge.client.service.AudioService;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.node.ObjectNode;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;

View File

@ -3,9 +3,9 @@ package com.cmvr.web.controller.common;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import javax.annotation.Resource;
import jakarta.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.FastByteArrayOutputStream;
import org.springframework.web.bind.annotation.GetMapping;
@ -23,7 +23,7 @@ import com.cmvr.system.service.ISysConfigService;
/**
* 验证码操作处理
*
* @author cmvr-iot
* @author ruoyi
*/
@RestController
public class CaptchaController

View File

@ -1,49 +1,54 @@
package com.cmvr.web.controller.common;
import com.cmvr.common.config.CmvrIotConfig;
import com.cmvr.common.constant.Constants;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.common.utils.StringUtils;
import com.cmvr.common.utils.file.FileUploadUtils;
import com.cmvr.common.utils.file.FileUtils;
import com.cmvr.framework.config.ServerConfig;
import io.swagger.annotations.Api;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.cmvr.common.config.CmvrIotConfig;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.common.utils.StringUtils;
import com.cmvr.common.utils.file.FileUploadUtils;
import com.cmvr.common.utils.file.FileUtils;
import com.cmvr.framework.config.ServerConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
@Api(tags = "通用请求处理")
/**
* 通用请求处理
*
* @author ruoyi
*/
@RestController
@RequestMapping("/common")
@RequiredArgsConstructor
public class CommonController {
public class CommonController
{
private static final Logger log = LoggerFactory.getLogger(CommonController.class);
private final ServerConfig serverConfig;
@Autowired
private ServerConfig serverConfig;
private static final String FILE_DELIMETER = ",";
private static final String FILE_DELIMITER = ",";
/**
* 通用下载请求
*
*
* @param fileName 文件名称
* @param delete 是否删除
* @param delete 是否删除
*/
@GetMapping("/download")
public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request) {
try {
if (!FileUtils.checkAllowDownload(fileName)) {
public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request)
{
try
{
if (!FileUtils.checkAllowDownload(fileName))
{
throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
}
String realFileName = System.currentTimeMillis() + fileName.substring(fileName.indexOf("_") + 1);
@ -52,10 +57,13 @@ public class CommonController {
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
FileUtils.setAttachmentResponseHeader(response, realFileName);
FileUtils.writeBytes(filePath, response.getOutputStream());
if (delete) {
if (delete)
{
FileUtils.deleteFile(filePath);
}
} catch (Exception e) {
}
catch (Exception e)
{
log.error("下载文件失败", e);
}
}
@ -64,8 +72,10 @@ public class CommonController {
* 通用上传请求(单个)
*/
@PostMapping("/upload")
public AjaxResult uploadFile(MultipartFile file) throws Exception {
try {
public AjaxResult uploadFile(MultipartFile file) throws Exception
{
try
{
// 上传文件路径
String filePath = CmvrIotConfig.getUploadPath();
// 上传并返回新文件名称
@ -77,7 +87,9 @@ public class CommonController {
ajax.put("newFileName", FileUtils.getName(fileName));
ajax.put("originalFilename", file.getOriginalFilename());
return ajax;
} catch (Exception e) {
}
catch (Exception e)
{
return AjaxResult.error(e.getMessage());
}
}
@ -86,15 +98,18 @@ public class CommonController {
* 通用上传请求(多个)
*/
@PostMapping("/uploads")
public AjaxResult uploadFiles(List<MultipartFile> files) throws Exception {
try {
public AjaxResult uploadFiles(List<MultipartFile> files) throws Exception
{
try
{
// 上传文件路径
String filePath = CmvrIotConfig.getUploadPath();
List<String> urls = new ArrayList<String>();
List<String> fileNames = new ArrayList<String>();
List<String> newFileNames = new ArrayList<String>();
List<String> originalFilenames = new ArrayList<String>();
for (MultipartFile file : files) {
for (MultipartFile file : files)
{
// 上传并返回新文件名称
String fileName = FileUploadUtils.upload(filePath, file);
String url = serverConfig.getUrl() + fileName;
@ -104,12 +119,14 @@ public class CommonController {
originalFilenames.add(file.getOriginalFilename());
}
AjaxResult ajax = AjaxResult.success();
ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER));
ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER));
ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER));
ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER));
ajax.put("urls", StringUtils.join(urls, FILE_DELIMITER));
ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMITER));
ajax.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMITER));
ajax.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMITER));
return ajax;
} catch (Exception e) {
}
catch (Exception e)
{
return AjaxResult.error(e.getMessage());
}
}
@ -119,21 +136,26 @@ public class CommonController {
*/
@GetMapping("/download/resource")
public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
throws Exception {
try {
if (!FileUtils.checkAllowDownload(resource)) {
throws Exception
{
try
{
if (!FileUtils.checkAllowDownload(resource))
{
throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource));
}
// 本地资源路径
String localPath = CmvrIotConfig.getProfile();
// 数据库资源地址
String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX);
String downloadPath = localPath + FileUtils.stripPrefix(resource);
// 下载名称
String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
FileUtils.setAttachmentResponseHeader(response, downloadName);
FileUtils.writeBytes(downloadPath, response.getOutputStream());
} catch (Exception e) {
}
catch (Exception e)
{
log.error("下载文件失败", e);
}
}

View File

@ -1,7 +1,7 @@
package com.cmvr.web.controller.device;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import com.cmvr.edge.client.service.EdgeSystemService;
import io.swagger.annotations.Api;

View File

@ -15,7 +15,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
/**

View File

@ -1,7 +1,7 @@
package com.cmvr.web.controller.device;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import com.cmvr.common.annotation.Log;
import com.cmvr.common.core.controller.BaseController;

View File

@ -1,59 +1,59 @@
package com.cmvr.web.controller.evaluation;
import com.cmvr.common.core.controller.BaseController;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.evaluation.model.dto.AeIndicatorCreateDTO;
import com.cmvr.evaluation.model.dto.AeIndicatorUpdateDTO;
import com.cmvr.evaluation.service.AeIndicatorService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@Api(tags = "评价管理--指标库")
@RestController
@RequestMapping("/evaluate/indicator")
@RequiredArgsConstructor
public class AeIndicatorController extends BaseController {
private final AeIndicatorService indicatorService;
@ApiOperation("根据父id查询指标")
@GetMapping("/getIndicatorByParentId")
public AjaxResult getIndicatorByParentId(@RequestParam Long parentId, @RequestParam Integer type) {
if (parentId == null || type == null) {
return AjaxResult.error("参数不能为空");
}
return success(indicatorService.selectIndicatorByParentId(parentId, type));
}
@ApiOperation("新增指标")
@PostMapping("/indicator/insert")
public AjaxResult insertIndicator(@Valid @RequestBody AeIndicatorCreateDTO createDTO) {
return success(indicatorService.saveIndicator(createDTO));
}
@ApiOperation("根据ID级联删除:删除当前指标及其所有后代指标(子/孙级)")
@DeleteMapping("/{id}")
public AjaxResult removeIndicator(@PathVariable Long id) {
if (id == null) {
return AjaxResult.error("id不能为空");
}
return toAjax(indicatorService.removeIndicator(id));
}
@ApiOperation("修改指标")
@PutMapping
public AjaxResult updateIndicator(@Valid @RequestBody AeIndicatorUpdateDTO updateDTO) {
return toAjax(indicatorService.updateIndicator(updateDTO));
}
@ApiOperation("获取指定parentId下指定level层级的指标")
@GetMapping("/getChildrenByParentIdWithLevelLimit")
public AjaxResult getChildrenByParentIdWithLevelLimit(@RequestParam Long parentId, @RequestParam Integer maxLevel, @RequestParam Integer type) {
return success(indicatorService.getChildrenByParentIdWithLevelLimit(parentId, maxLevel, type));
}
}
package com.cmvr.web.controller.evaluation;
import com.cmvr.common.core.controller.BaseController;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.evaluation.model.dto.AeIndicatorCreateDTO;
import com.cmvr.evaluation.model.dto.AeIndicatorUpdateDTO;
import com.cmvr.evaluation.service.AeIndicatorService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
@Api(tags = "评价管理--指标库")
@RestController
@RequestMapping("/evaluate/indicator")
@RequiredArgsConstructor
public class AeIndicatorController extends BaseController {
private final AeIndicatorService indicatorService;
@ApiOperation("根据父id查询指标")
@GetMapping("/getIndicatorByParentId")
public AjaxResult getIndicatorByParentId(@RequestParam Long parentId, @RequestParam Integer type) {
if (parentId == null || type == null) {
return AjaxResult.error("参数不能为空");
}
return success(indicatorService.selectIndicatorByParentId(parentId, type));
}
@ApiOperation("新增指标")
@PostMapping("/indicator/insert")
public AjaxResult insertIndicator(@Valid @RequestBody AeIndicatorCreateDTO createDTO) {
return success(indicatorService.saveIndicator(createDTO));
}
@ApiOperation("根据ID级联删除:删除当前指标及其所有后代指标(子/孙级)")
@DeleteMapping("/{id}")
public AjaxResult removeIndicator(@PathVariable Long id) {
if (id == null) {
return AjaxResult.error("id不能为空");
}
return toAjax(indicatorService.removeIndicator(id));
}
@ApiOperation("修改指标")
@PutMapping
public AjaxResult updateIndicator(@Valid @RequestBody AeIndicatorUpdateDTO updateDTO) {
return toAjax(indicatorService.updateIndicator(updateDTO));
}
@ApiOperation("获取指定parentId下指定level层级的指标")
@GetMapping("/getChildrenByParentIdWithLevelLimit")
public AjaxResult getChildrenByParentIdWithLevelLimit(@RequestParam Long parentId, @RequestParam Integer maxLevel, @RequestParam Integer type) {
return success(indicatorService.getChildrenByParentIdWithLevelLimit(parentId, maxLevel, type));
}
}

View File

@ -1,7 +1,7 @@
package com.cmvr.web.controller.inspection;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;

View File

@ -5,7 +5,7 @@ import com.cmvr.inspection.domain.dto.alert.AlertEnvelope;
import com.cmvr.inspection.exception.DetectionAlertBadRequestException;
import com.cmvr.inspection.service.IInspectionDetectionAlertService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectMapper;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
@ -18,8 +18,8 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

View File

@ -1,7 +1,7 @@
package com.cmvr.web.controller.inspection;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;

View File

@ -28,7 +28,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
/**

View File

@ -1,9 +1,9 @@
package com.cmvr.web.controller.inspection;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import cmvr.msgs.Agv;
import cmvr.msgs.AgvUtils;
import com.cmvr.inspection.domain.vo.InspectionRobotVo;
import io.swagger.annotations.*;
import org.springframework.security.access.prepost.PreAuthorize;
@ -70,6 +70,25 @@ public class InspectionRobotController extends BaseController
return success(inspectionRobotService.selectInspectionRobotById(id));
}
@ApiOperation("查询机器人自动发现的设备")
@PreAuthorize("@ss.hasPermi('inspection:robot:query')")
@GetMapping("/{id}/devices")
public AjaxResult devices(@PathVariable("id") String id)
{
return success(inspectionRobotService.selectRobotDevices(id));
}
@ApiOperation("按机器人ID查询自动发现的设备")
@PreAuthorize("@ss.hasPermi('inspection:robot:query')")
@GetMapping("/devices")
public AjaxResult devicesByRobotId(
@RequestParam @ApiParam(value = "机器人永久唯一ID", required = true) String robotId,
@RequestParam(required = false) @ApiParam("设备类型") Integer deviceKind,
@RequestParam(defaultValue = "true") @ApiParam("是否只查询在线且启用的设备") boolean onlineOnly)
{
return success(inspectionRobotService.selectRobotDevicesByRobotId(robotId, deviceKind, onlineOnly));
}
/**
* 新增巡检机器人
*/
@ -180,7 +199,7 @@ public class InspectionRobotController extends BaseController
@GetMapping("/{robotId}/location")
public AjaxResult getRobotRealtimeLocation(@PathVariable String robotId)
{
Agv.AgvPose2d location = inspectionRobotService.getRobotRealtimeLocation(robotId);
AgvUtils.AgvPose2d location = inspectionRobotService.getRobotRealtimeLocation(robotId);
return success(location);
}

View File

@ -1,7 +1,7 @@
package com.cmvr.web.controller.inspection;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
@ -89,6 +89,15 @@ public class InspectionTaskController extends BaseController
return toAjax(inspectionTaskService.insertInspectionTask(inspectionTask));
}
@ApiOperation("复制巡检任务")
@PreAuthorize("@ss.hasPermi('inspection:task:add')")
@Log(title = "巡检任务", businessType = BusinessType.INSERT)
@PostMapping("/copy/{id}")
public AjaxResult copy(@PathVariable String id)
{
return success(inspectionTaskService.copyInspectionTask(id));
}
/**
* 修改巡检任务
*/

View File

@ -1,7 +1,7 @@
package com.cmvr.web.controller.inspection;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import com.cmvr.inspection.domain.dto.InspectionTaskInstanceQuery;
import io.swagger.annotations.Api;
@ -21,6 +21,7 @@ import com.cmvr.common.core.controller.BaseController;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.common.enums.BusinessType;
import com.cmvr.inspection.domain.InspectionTaskInstance;
import com.cmvr.inspection.domain.vo.InspectionTaskStartVO;
import com.cmvr.inspection.domain.vo.InspectionTaskInstanceVo;
import com.cmvr.inspection.service.IInspectionTaskInstanceService;
import com.cmvr.common.utils.poi.ExcelUtil;
@ -121,9 +122,19 @@ public class InspectionTaskInstanceController extends BaseController
@PreAuthorize("@ss.hasPermi('inspection:taskInstance:edit')")
@Log(title = "巡检任务执行实例", businessType = BusinessType.UPDATE)
@PutMapping("/start/{id}")
public AjaxResult start(@PathVariable("id") String id)
public AjaxResult start(@PathVariable("id") String id,
@RequestBody(required = false) InspectionTaskStartVO startVO)
{
return toAjax(inspectionTaskInstanceService.startInstance(id));
return toAjax(inspectionTaskInstanceService.startInstance(id, startVO));
}
@ApiOperation("再次执行已完成或失败的巡检任务")
@PreAuthorize("@ss.hasPermi('inspection:taskInstance:edit')")
@Log(title = "巡检任务执行实例", businessType = BusinessType.INSERT)
@PostMapping("/rerun/{id}")
public AjaxResult rerun(@PathVariable("id") String id)
{
return success(inspectionTaskInstanceService.rerunInstance(id));
}
/**

View File

@ -1,7 +1,7 @@
package com.cmvr.web.controller.inspection;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
@ -88,6 +88,15 @@ public class InspectionWaypointController extends BaseController
return toAjax(inspectionWaypointService.insertInspectionWaypoint(inspectionWaypoint));
}
@ApiOperation("复制巡检点位")
@PreAuthorize("@ss.hasPermi('inspection:waypoint:add')")
@Log(title = "巡检点位", businessType = BusinessType.INSERT)
@PostMapping("/copy/{id}")
public AjaxResult copy(@PathVariable String id)
{
return success(inspectionWaypointService.copyInspectionWaypoint(id));
}
/**
* 修改巡检点位
*/

View File

@ -25,7 +25,7 @@ import com.cmvr.system.domain.SysCache;
/**
* 缓存监控
*
* @author cmvr-iot
* @author ruoyi
*/
@RestController
@RequestMapping("/monitor/cache")
@ -45,6 +45,7 @@ public class CacheController
caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数"));
}
@SuppressWarnings("deprecation")
@PreAuthorize("@ss.hasPermi('monitor:cache:list')")
@GetMapping()
public AjaxResult getInfo() throws Exception

View File

@ -10,7 +10,7 @@ import com.cmvr.framework.web.domain.Server;
/**
* 服务器监控
*
* @author cmvr-iot
* @author ruoyi
*/
@RestController
@RequestMapping("/monitor/server")

View File

@ -1,7 +1,7 @@
package com.cmvr.web.controller.monitor;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
@ -23,7 +23,7 @@ import com.cmvr.system.service.ISysLogininforService;
/**
* 系统访问记录
*
* @author cmvr-iot
* @author ruoyi
*/
@RestController
@RequestMapping("/monitor/logininfor")

View File

@ -1,7 +1,7 @@
package com.cmvr.web.controller.monitor;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
@ -22,7 +22,7 @@ import com.cmvr.system.service.ISysOperLogService;
/**
* 操作日志记录
*
* @author cmvr-iot
* @author ruoyi
*/
@RestController
@RequestMapping("/monitor/operlog")

View File

@ -26,7 +26,7 @@ import com.cmvr.system.service.ISysUserOnlineService;
/**
* 在线用户监控
*
* @author cmvr-iot
* @author ruoyi
*/
@RestController
@RequestMapping("/monitor/online")

View File

@ -23,7 +23,7 @@ import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.Valid;
import jakarta.validation.Valid;
import java.io.IOException;
@Api(tags = "演示--智能座舱")

View File

@ -1,7 +1,7 @@
package com.cmvr.web.controller.system;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
@ -25,7 +25,7 @@ import com.cmvr.system.service.ISysConfigService;
/**
* 参数配置 信息操作处理
*
* @author cmvr-iot
* @author ruoyi
*/
@RestController
@RequestMapping("/system/config")

View File

@ -1,6 +1,7 @@
package com.cmvr.web.controller.system;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
@ -25,7 +26,7 @@ import com.cmvr.system.service.ISysDeptService;
/**
* 部门信息
*
* @author cmvr-iot
* @author ruoyi
*/
@RestController
@RequestMapping("/system/dept")
@ -110,6 +111,20 @@ public class SysDeptController extends BaseController
return toAjax(deptService.updateDept(dept));
}
/**
* 保存部门排序
*/
@PreAuthorize("@ss.hasPermi('system:dept:edit')")
@Log(title = "保存部门排序", businessType = BusinessType.UPDATE)
@PutMapping("/updateSort")
public AjaxResult updateSort(@RequestBody Map<String, String> params)
{
String[] deptIds = params.get("deptIds").split(",");
String[] orderNums = params.get("orderNums").split(",");
deptService.updateDeptSort(deptIds, orderNums);
return success();
}
/**
* 删除部门
*/

View File

@ -2,7 +2,7 @@ package com.cmvr.web.controller.system;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
@ -28,7 +28,7 @@ import com.cmvr.system.service.ISysDictTypeService;
/**
* 数据字典信息
*
* @author cmvr-iot
* @author ruoyi
*/
@RestController
@RequestMapping("/system/dict/data")

View File

@ -1,7 +1,7 @@
package com.cmvr.web.controller.system;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
@ -25,7 +25,7 @@ import com.cmvr.system.service.ISysDictTypeService;
/**
* 数据字典信息
*
* @author cmvr-iot
* @author ruoyi
*/
@RestController
@RequestMapping("/system/dict/type")

View File

@ -1,22 +1,32 @@
package com.cmvr.web.controller.system;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.cmvr.common.config.CmvrIotConfig;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.common.core.domain.entity.SysUser;
import com.cmvr.common.utils.SecurityUtils;
import com.cmvr.common.utils.StringUtils;
import com.cmvr.system.service.ISysUserService;
/**
* 首页
*
* @author cmvr-iot
* @author ruoyi
*/
@RestController
public class SysIndexController
{
/** 系统基础配置 */
@Autowired
private CmvrIotConfig cmvrIotConfig;
private CmvrIotConfig ruoyiConfig;
@Autowired
private ISysUserService userService;
/**
* 访问首页,提示语
@ -24,6 +34,31 @@ public class SysIndexController
@RequestMapping("/")
public String index()
{
return StringUtils.format("欢迎使用{}后台管理框架,当前版本:v{},请通过前端地址访问。", cmvrIotConfig.getName(), cmvrIotConfig.getVersion());
return StringUtils.format("欢迎使用{}后台管理框架,当前版本:v{},请通过前端地址访问。", ruoyiConfig.getName(), ruoyiConfig.getVersion());
}
/**
* 解锁屏幕
*/
@PostMapping("/unlockscreen")
public AjaxResult unlockScreen(@RequestBody Map<String, String> body)
{
String password = body.get("password");
if (StringUtils.isEmpty(password))
{
return AjaxResult.error("密码不能为空");
}
String username = SecurityUtils.getUsername();
SysUser user = userService.selectUserByUserName(username);
if (user == null)
{
return AjaxResult.error("服务器超时,请重新登录");
}
if (!SecurityUtils.matchesPassword(password, user.getPassword()))
{
return AjaxResult.error("密码错误,请重新输入");
}
return AjaxResult.success("解锁成功");
}
}

View File

@ -1,10 +1,8 @@
package com.cmvr.web.controller.system;
import java.util.Date;
import java.util.List;
import java.util.Set;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@ -16,18 +14,21 @@ import com.cmvr.common.core.domain.entity.SysMenu;
import com.cmvr.common.core.domain.entity.SysUser;
import com.cmvr.common.core.domain.model.LoginBody;
import com.cmvr.common.core.domain.model.LoginUser;
import com.cmvr.common.core.text.Convert;
import com.cmvr.common.utils.DateUtils;
import com.cmvr.common.utils.SecurityUtils;
import com.cmvr.common.utils.StringUtils;
import com.cmvr.framework.web.service.SysLoginService;
import com.cmvr.framework.web.service.SysPermissionService;
import com.cmvr.framework.web.service.TokenService;
import com.cmvr.system.service.ISysConfigService;
import com.cmvr.system.service.ISysMenuService;
/**
* 登录验证
*
* @author cmvr-iot
* @author ruoyi
*/
@Api(tags = "通用--登录验证")
@RestController
public class SysLoginController
{
@ -43,13 +44,15 @@ public class SysLoginController
@Autowired
private TokenService tokenService;
@Autowired
private ISysConfigService configService;
/**
* 登录方法
*
* @param loginBody 登录信息
* @return 结果
*/
@ApiOperation("登录")
@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
@ -84,6 +87,9 @@ public class SysLoginController
ajax.put("user", user);
ajax.put("roles", roles);
ajax.put("permissions", permissions);
ajax.put("pwdChrtype", getSysAccountChrtype());
ajax.put("isDefaultModifyPwd", initPasswordIsModify(user.getPwdUpdateDate()));
ajax.put("isPasswordExpired", passwordIsExpiration(user.getPwdUpdateDate()));
return ajax;
}
@ -99,4 +105,34 @@ public class SysLoginController
List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
return AjaxResult.success(menuService.buildMenus(menus));
}
// 获取用户密码自定义配置规则
public String getSysAccountChrtype()
{
return Convert.toStr(configService.selectConfigByKey("sys.account.chrtype"), "0");
}
// 检查初始密码是否提醒修改
public boolean initPasswordIsModify(Date pwdUpdateDate)
{
Integer initPasswordModify = Convert.toInt(configService.selectConfigByKey("sys.account.initPasswordModify"));
return initPasswordModify != null && initPasswordModify == 1 && pwdUpdateDate == null;
}
// 检查密码是否过期
public boolean passwordIsExpiration(Date pwdUpdateDate)
{
Integer passwordValidateDays = Convert.toInt(configService.selectConfigByKey("sys.account.passwordValidateDays"));
if (passwordValidateDays != null && passwordValidateDays > 0)
{
if (StringUtils.isNull(pwdUpdateDate))
{
// 如果从未修改过初始密码,直接提醒过期
return true;
}
Date nowDate = DateUtils.getNowDate();
return DateUtils.differentDaysByMillisecond(nowDate, pwdUpdateDate) > passwordValidateDays;
}
return false;
}
}

View File

@ -1,6 +1,7 @@
package com.cmvr.web.controller.system;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
@ -24,7 +25,7 @@ import com.cmvr.system.service.ISysMenuService;
/**
* 菜单信息
*
* @author cmvr-iot
* @author ruoyi
*/
@RestController
@RequestMapping("/system/menu")
@ -93,6 +94,10 @@ public class SysMenuController extends BaseController
{
return error("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
}
else if (!menuService.checkRouteConfigUnique(menu))
{
return error("新增菜单'" + menu.getMenuName() + "'失败,路由名称或地址已存在");
}
menu.setCreateBy(getUsername());
return toAjax(menuService.insertMenu(menu));
}
@ -117,10 +122,28 @@ public class SysMenuController extends BaseController
{
return error("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己");
}
else if (!menuService.checkRouteConfigUnique(menu))
{
return error("修改菜单'" + menu.getMenuName() + "'失败,路由名称或地址已存在");
}
menu.setUpdateBy(getUsername());
return toAjax(menuService.updateMenu(menu));
}
/**
* 保存菜单排序
*/
@PreAuthorize("@ss.hasPermi('system:menu:edit')")
@Log(title = "保存菜单排序", businessType = BusinessType.UPDATE)
@PutMapping("/updateSort")
public AjaxResult updateSort(@RequestBody Map<String, String> params)
{
String[] menuIds = params.get("menuIds").split(",");
String[] orderNums = params.get("orderNums").split(",");
menuService.updateMenuSort(menuIds, orderNums);
return success();
}
/**
* 删除菜单
*/
@ -139,4 +162,4 @@ public class SysMenuController extends BaseController
}
return toAjax(menuService.deleteMenuById(menuId));
}
}
}

View File

@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.cmvr.common.annotation.Log;
import com.cmvr.common.core.controller.BaseController;
@ -18,12 +19,11 @@ import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.common.core.page.TableDataInfo;
import com.cmvr.common.enums.BusinessType;
import com.cmvr.system.domain.SysNotice;
import com.cmvr.system.domain.SysNoticeRead;
import com.cmvr.system.service.ISysNoticeService;
/**
* 公告 信息操作处理
*
* @author cmvr-iot
* 公告信息操作处理。
*/
@RestController
@RequestMapping("/system/notice")
@ -32,9 +32,6 @@ public class SysNoticeController extends BaseController
@Autowired
private ISysNoticeService noticeService;
/**
* 获取通知公告列表
*/
@PreAuthorize("@ss.hasPermi('system:notice:list')")
@GetMapping("/list")
public TableDataInfo list(SysNotice notice)
@ -44,9 +41,37 @@ public class SysNoticeController extends BaseController
return getDataTable(list);
}
/**
* 根据通知公告编号获取详细信息
*/
@GetMapping("/listTop")
public AjaxResult listTop()
{
Long userId = getUserId();
AjaxResult result = success(noticeService.selectNoticeTop(userId));
result.put("unreadCount", noticeService.countUnreadNotice(userId));
return result;
}
@PostMapping("/markRead")
public AjaxResult markRead(@RequestParam Long noticeId)
{
noticeService.markNoticeRead(noticeId, getUserId());
return success();
}
@PostMapping("/markReadAll")
public AjaxResult markReadAll(@RequestParam Long[] ids)
{
noticeService.markNoticeReadAll(ids, getUserId());
return success();
}
@PreAuthorize("@ss.hasPermi('system:notice:list')")
@GetMapping("/readUsers/list")
public TableDataInfo readUsers(SysNoticeRead noticeRead)
{
startPage();
return getDataTable(noticeService.selectNoticeReadUsers(noticeRead));
}
@PreAuthorize("@ss.hasPermi('system:notice:query')")
@GetMapping(value = "/{noticeId}")
public AjaxResult getInfo(@PathVariable Long noticeId)
@ -54,9 +79,6 @@ public class SysNoticeController extends BaseController
return success(noticeService.selectNoticeById(noticeId));
}
/**
* 新增通知公告
*/
@PreAuthorize("@ss.hasPermi('system:notice:add')")
@Log(title = "通知公告", businessType = BusinessType.INSERT)
@PostMapping
@ -66,9 +88,6 @@ public class SysNoticeController extends BaseController
return toAjax(noticeService.insertNotice(notice));
}
/**
* 修改通知公告
*/
@PreAuthorize("@ss.hasPermi('system:notice:edit')")
@Log(title = "通知公告", businessType = BusinessType.UPDATE)
@PutMapping
@ -78,9 +97,6 @@ public class SysNoticeController extends BaseController
return toAjax(noticeService.updateNotice(notice));
}
/**
* 删除通知公告
*/
@PreAuthorize("@ss.hasPermi('system:notice:remove')")
@Log(title = "通知公告", businessType = BusinessType.DELETE)
@DeleteMapping("/{noticeIds}")

View File

@ -1,7 +1,7 @@
package com.cmvr.web.controller.system;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
@ -25,7 +25,7 @@ import com.cmvr.system.service.ISysPostService;
/**
* 岗位信息操作处理
*
* @author cmvr-iot
* @author ruoyi
*/
@RestController
@RequestMapping("/system/post")

View File

@ -17,9 +17,11 @@ import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.common.core.domain.entity.SysUser;
import com.cmvr.common.core.domain.model.LoginUser;
import com.cmvr.common.enums.BusinessType;
import com.cmvr.common.utils.DateUtils;
import com.cmvr.common.utils.SecurityUtils;
import com.cmvr.common.utils.StringUtils;
import com.cmvr.common.utils.file.FileUploadUtils;
import com.cmvr.common.utils.file.FileUtils;
import com.cmvr.common.utils.file.MimeTypeUtils;
import com.cmvr.framework.web.service.TokenService;
import com.cmvr.system.service.ISysUserService;
@ -27,7 +29,7 @@ import com.cmvr.system.service.ISysUserService;
/**
* 个人信息 业务处理
*
* @author cmvr-iot
* @author ruoyi
*/
@RestController
@RequestMapping("/system/user/profile")
@ -93,8 +95,9 @@ public class SysProfileController extends BaseController
String oldPassword = params.get("oldPassword");
String newPassword = params.get("newPassword");
LoginUser loginUser = getLoginUser();
String userName = loginUser.getUsername();
String password = loginUser.getPassword();
Long userId = loginUser.getUserId();
SysUser user = userService.selectUserById(userId);
String password = user.getPassword();
if (!SecurityUtils.matchesPassword(oldPassword, password))
{
return error("修改密码失败,旧密码错误");
@ -104,9 +107,10 @@ public class SysProfileController extends BaseController
return error("新密码不能与旧密码相同");
}
newPassword = SecurityUtils.encryptPassword(newPassword);
if (userService.resetUserPwd(userName, newPassword) > 0)
if (userService.resetUserPwd(userId, newPassword) > 0)
{
// 更新缓存用户密码
// 更新缓存用户密码&密码最后更新时间
loginUser.getUser().setPwdUpdateDate(DateUtils.getNowDate());
loginUser.getUser().setPassword(newPassword);
tokenService.setLoginUser(loginUser);
return success();
@ -124,9 +128,14 @@ public class SysProfileController extends BaseController
if (!file.isEmpty())
{
LoginUser loginUser = getLoginUser();
String avatar = FileUploadUtils.upload(CmvrIotConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION);
if (userService.updateUserAvatar(loginUser.getUsername(), avatar))
String avatar = FileUploadUtils.upload(CmvrIotConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION, true);
if (userService.updateUserAvatar(loginUser.getUserId(), avatar))
{
String oldAvatar = loginUser.getUser().getAvatar();
if (StringUtils.isNotEmpty(oldAvatar))
{
FileUtils.deleteFile(CmvrIotConfig.getProfile() + FileUtils.stripPrefix(oldAvatar));
}
AjaxResult ajax = AjaxResult.success();
ajax.put("imgUrl", avatar);
// 更新缓存用户头像

View File

@ -14,7 +14,7 @@ import com.cmvr.system.service.ISysConfigService;
/**
* 注册验证
*
* @author cmvr-iot
* @author ruoyi
*/
@RestController
public class SysRegisterController extends BaseController

View File

@ -1,7 +1,7 @@
package com.cmvr.web.controller.system;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
@ -19,10 +19,8 @@ import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.common.core.domain.entity.SysDept;
import com.cmvr.common.core.domain.entity.SysRole;
import com.cmvr.common.core.domain.entity.SysUser;
import com.cmvr.common.core.domain.model.LoginUser;
import com.cmvr.common.core.page.TableDataInfo;
import com.cmvr.common.enums.BusinessType;
import com.cmvr.common.utils.StringUtils;
import com.cmvr.common.utils.poi.ExcelUtil;
import com.cmvr.framework.web.service.SysPermissionService;
import com.cmvr.framework.web.service.TokenService;
@ -34,7 +32,7 @@ import com.cmvr.system.service.ISysUserService;
/**
* 角色信息
*
* @author cmvr-iot
* @author ruoyi
*/
@RestController
@RequestMapping("/system/role")
@ -128,14 +126,8 @@ public class SysRoleController extends BaseController
if (roleService.updateRole(role) > 0)
{
// 更新缓存用户权限
LoginUser loginUser = getLoginUser();
if (StringUtils.isNotNull(loginUser.getUser()) && !loginUser.getUser().isAdmin())
{
loginUser.setUser(userService.selectUserByUserName(loginUser.getUser().getUserName()));
loginUser.setPermissions(permissionService.getMenuPermission(loginUser.getUser()));
tokenService.setLoginUser(loginUser);
}
// 刷新所有持有该角色的在线用户权限
tokenService.refreshPermissionByRoleId(role.getRoleId(), permissionService);
return success();
}
return error("修改角色'" + role.getRoleName() + "'失败,请联系管理员");

View File

@ -2,7 +2,7 @@ package com.cmvr.web.controller.system;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
@ -35,7 +35,7 @@ import com.cmvr.system.service.ISysUserService;
/**
* 用户信息
*
* @author cmvr-iot
* @author ruoyi
*/
@RestController
@RequestMapping("/system/user")
@ -111,7 +111,7 @@ public class SysUserController extends BaseController
ajax.put("roleIds", sysUser.getRoles().stream().map(SysRole::getRoleId).collect(Collectors.toList()));
}
List<SysRole> roles = roleService.selectRoleAll();
ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList()));
ajax.put("roles", SecurityUtils.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList()));
ajax.put("posts", postService.selectPostAll());
return ajax;
}
@ -226,7 +226,7 @@ public class SysUserController extends BaseController
SysUser user = userService.selectUserById(userId);
List<SysRole> roles = roleService.selectRolesByUserId(userId);
ajax.put("user", user);
ajax.put("roles", SysUser.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList()));
ajax.put("roles", SecurityUtils.isAdmin(userId) ? roles : roles.stream().filter(r -> !r.isAdmin()).collect(Collectors.toList()));
return ajax;
}

View File

@ -15,7 +15,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
@Api(tags = "测试--检测项配置")
@ -60,6 +60,14 @@ public class TeDetectionItemController extends BaseController {
return toAjax(rows);
}
@ApiOperation("复制检测项配置")
@PreAuthorize("@ss.hasPermi('test:detect:add')")
@Log(title = "检测项配置", businessType = BusinessType.INSERT)
@PostMapping("/copy/{id}")
public AjaxResult copy(@PathVariable String id) {
return success(teDetectionItemService.copyTeDetectionItem(id));
}
@ApiOperation("修改检测项配置")
@PreAuthorize("@ss.hasPermi('test:detect:edit')")
@Log(title = "检测项配置", businessType = BusinessType.UPDATE)

View File

@ -10,14 +10,18 @@ import com.cmvr.llm.service.LLMAiAgentPlatformService;
import com.cmvr.test.flow.context.TaskContextManager;
import com.cmvr.test.flow.control.FlowControlService;
import com.cmvr.test.flow.runtime.engine.FlowTaskRuntimeService;
import com.cmvr.test.flow.runtime.subflow.FlowSubFlowDefinitionService;
import com.cmvr.test.flow.runtime.operator.edge.ti.TiTouchOperateService;
import com.cmvr.test.model.domain.TeDetectionItem;
import com.cmvr.test.model.vo.FlowActionRequestVO;
import com.cmvr.test.model.vo.TeDetectItemDeployFlowVO;
import com.cmvr.test.model.vo.TeFlowPublishVO;
import com.cmvr.test.model.vo.TeFlowVersionActionVO;
import com.cmvr.test.model.vo.TeFlowViewVO;
import com.cmvr.test.model.vo.TeTaskExecuteNormalVO;
import com.cmvr.test.model.vo.TeTaskExecuteTrailVO;
import com.cmvr.test.service.FlowActionExecutorService;
import com.cmvr.test.service.ITeDetectionItemService;
import com.cmvr.test.service.ITeDetectionItemVersionService;
import com.cmvr.test.service.ITeNodeInstService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@ -29,9 +33,10 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.PutMapping;
import javax.annotation.PreDestroy;
import javax.validation.Valid;
import jakarta.annotation.PreDestroy;
import jakarta.validation.Valid;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CancellationException;
@ -62,6 +67,8 @@ public class TeFlowController extends BaseController {
private static final String AI_AGENT_API_KEY = "d9etum54shheenol3apg";
private final ITeDetectionItemService teDetectionItemService;
private final ITeDetectionItemVersionService detectionItemVersionService;
private final FlowSubFlowDefinitionService subFlowDefinitionService;
private final ITeNodeInstService nodeInstService;
private final FlowTaskRuntimeService flowTaskRuntimeService;
private final TaskContextManager taskContextManager;
@ -87,8 +94,48 @@ public class TeFlowController extends BaseController {
@ApiOperation("流程发布")
@PostMapping("/publish")
public AjaxResult publish(@RequestBody TeDetectionItem detectionItem) {
return toAjax(teDetectionItemService.publish(detectionItem.getId()));
public AjaxResult publish(@Valid @RequestBody TeFlowPublishVO request) {
return success(detectionItemVersionService.publish(request, getUsername()));
}
@ApiOperation("保存工作流草稿")
@PutMapping("/draft")
public AjaxResult saveDraft(@Valid @RequestBody TeDetectItemDeployFlowVO request) {
return success(teDetectionItemService.saveDraft(request, getUsername()));
}
@ApiOperation("查询工作流发布记录")
@GetMapping("/versions/{itemId}")
public AjaxResult versions(@PathVariable String itemId) {
return success(detectionItemVersionService.listVersions(itemId));
}
@ApiOperation("查询当前项目可用的已发布子流程")
@GetMapping("/subflows")
public AjaxResult subFlows(@RequestParam String currentItemId) {
return success(subFlowDefinitionService.listOptions(currentItemId));
}
@ApiOperation("查看工作流发布版本")
@GetMapping("/version/{versionId}")
public AjaxResult version(@PathVariable String versionId) {
return success(detectionItemVersionService.getVersion(versionId));
}
@ApiOperation("将发布版本恢复为草稿")
@PostMapping("/version/{versionId}/restore-draft")
public AjaxResult restoreDraft(@PathVariable String versionId,
@RequestBody(required = false) TeFlowVersionActionVO request) {
Integer revision = request == null ? null : request.getDraftRevision();
return success(detectionItemVersionService.restoreDraft(versionId, revision, getUsername()));
}
@ApiOperation("回退到指定发布版本")
@PostMapping("/version/{versionId}/rollback")
public AjaxResult rollback(@PathVariable String versionId,
@RequestBody(required = false) TeFlowVersionActionVO request) {
String note = request == null ? null : request.getPublishNote();
return success(detectionItemVersionService.rollback(versionId, note, getUsername()));
}
@ApiOperation("演示任务执行入口")
@ -157,7 +204,8 @@ public class TeFlowController extends BaseController {
@PostMapping("/action")
@ApiOperation(value = "单个节点执行",
notes = "巡检仪表节点action=INSPECTION_METER_RECOGNIZE,payload使用InspectionMeterRecognizeConfigVO;"
notes = "设备通用指令action=DEVICE_EXECUTE_JSON_COMMAND,payload需要robotId、deviceId、requestJson;"
+ "巡检仪表节点action=INSPECTION_METER_RECOGNIZE,payload使用InspectionMeterRecognizeConfigVO;"
+ "人工判断节点action=INSPECTION_MANUAL_REVIEW_CREATE,payload使用InspectionManualReviewConfigVO。"
+ "该接口用于试调节点,不会生成正式巡检任务结果")
public AjaxResult actionExecute(

View File

@ -16,6 +16,7 @@ import com.google.gson.JsonObject;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@ -50,8 +51,9 @@ public class TeFlowiseController extends BaseController {
.put("test", request.getQuestion());
runParams.put("5bb26d52e1861c6936f3c3149eaf17e8", runParams1);
TeTaskExecuteNormalVO taskExecuteNormalVO = TeTaskExecuteNormalVO.builder()
.taskId("b6ab5ffff783d9b25d0dc34f13be222d")
.terminalId("4ed1246c465b97975f96c9ef8371a3bd")
.taskId(StringUtils.defaultIfBlank(request.getTaskId(), "b6ab5ffff783d9b25d0dc34f13be222d"))
.robotId(StringUtils.defaultIfBlank(request.getRobotId(), "4ed1246c465b97975f96c9ef8371a3bd"))
.runtimeAssignments(request.getRuntimeAssignments())
.runParams(JSONObject.from(runParams)).build();
if (!taskInstIdList.isEmpty()) {
try {
@ -112,4 +114,4 @@ public class TeFlowiseController extends BaseController {
public AjaxResult actionStop() {
return AjaxResult.ok(flowiseActionService.actionStop());
}
}
}

View File

@ -48,6 +48,14 @@ public class TeTaskConfigInfoController extends BaseController {
return toAjax(teTaskConfigInfoService.insertTeTaskConfigInfo(teTaskConfigInfo));
}
@ApiOperation("复制任务配置")
@PreAuthorize("@ss.hasPermi('test:config:add')")
@Log(title = "任务配置", businessType = BusinessType.INSERT)
@PostMapping("/copy/{id}")
public AjaxResult copy(@PathVariable String id) {
return success(teTaskConfigInfoService.copyTeTaskConfigInfo(id));
}
@ApiOperation("修改任务配置信息")
@PreAuthorize("@ss.hasPermi('test:config:edit')")
@Log(title = "任务配置信息", businessType = BusinessType.UPDATE)

View File

@ -6,6 +6,7 @@ import com.cmvr.test.model.domain.TeNodeInst;
import com.cmvr.test.model.vo.TeQueryTaskInstVO;
import com.cmvr.test.model.vo.TeTaskInstVO;
import com.cmvr.test.service.ITeTaskInstService;
import com.cmvr.test.flow.runtime.engine.FlowTaskRuntimeService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
@ -14,6 +15,9 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import com.cmvr.common.core.domain.AjaxResult;
import java.util.List;
@ -24,6 +28,7 @@ import java.util.List;
public class TeTaskInstController extends BaseController {
private final ITeTaskInstService teTaskInstService;
private final FlowTaskRuntimeService flowTaskRuntimeService;
@ApiOperation("查询任务实例列表")
@PreAuthorize("@ss.hasPermi('test:inst:list')")
@ -42,4 +47,11 @@ public class TeTaskInstController extends BaseController {
return getDataTable(list);
}
}
@ApiOperation("再次执行已完成或失败的任务实例")
@PreAuthorize("@ss.hasPermi('test:config:execute')")
@PostMapping("/rerun/{id}")
public AjaxResult rerun(@PathVariable("id") String id) {
return success(flowTaskRuntimeService.rerunTask(id));
}
}

View File

@ -14,8 +14,8 @@ import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import java.util.List;
@Api(tags = "测试--任务编排")
@ -33,7 +33,6 @@ class TeTaskOrchestrationController extends BaseController {
@NotEmpty(message = "任务ID不能为空")
@RequestParam("taskId") String taskId
) {
startPage();
List<TeQueryTaskOrchestraItemVO> list = teTaskOrchestrationService.queryByTaskId(taskId);
return getDataTable(list);
}

View File

@ -15,7 +15,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Map;
/**

View File

@ -18,7 +18,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
/**

View File

@ -0,0 +1,207 @@
package com.cmvr.web.core.config;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import org.springdoc.core.models.GroupedOpenApi;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.customizers.PropertyCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
/** OpenAPI metadata, authorization and business-domain groups. */
@Configuration
public class OpenApiConfig
{
private static final String AUTHORIZATION = "Authorization";
@Bean
public OpenAPI cmvrOpenApi()
{
SecurityScheme bearerAuth = new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT");
return new OpenAPI()
.info(new Info()
.title("CMVR IoT API")
.description("招商车研物联网平台接口文档")
.version("1.0.0"))
.components(new Components().addSecuritySchemes(AUTHORIZATION, bearerAuth))
.addSecurityItem(new SecurityRequirement().addList(AUTHORIZATION));
}
@Bean
public GroupedOpenApi allApi()
{
return group("00-all", "全部接口", "/**");
}
private GroupedOpenApi group(String id, String displayName, String... paths)
{
return GroupedOpenApi.builder()
.group(id)
.displayName(displayName)
.pathsToMatch(paths)
// Keep the same visibility rule as the legacy Swagger Docket.
.addOpenApiMethodFilter(method -> method.isAnnotationPresent(ApiOperation.class))
.addOperationCustomizer(legacySwaggerOperationCustomizer())
.addOpenApiCustomizer(legacyTagOrderCustomizer())
.build();
}
private OpenApiCustomizer legacyTagOrderCustomizer()
{
return openApi -> {
Set<String> tagNames = new TreeSet<>();
if (openApi.getPaths() != null)
{
openApi.getPaths().values().forEach(pathItem -> pathItem.readOperations().forEach(operation -> {
if (operation.getTags() != null)
{
tagNames.addAll(operation.getTags());
}
}));
}
openApi.setTags(tagNames.stream()
.map(name -> new io.swagger.v3.oas.models.tags.Tag().name(name))
.collect(Collectors.toList()));
};
}
@Bean
public OperationCustomizer legacySwaggerOperationCustomizer()
{
return this::customizeLegacyOperation;
}
@Bean
public PropertyCustomizer legacySwaggerPropertyCustomizer()
{
return (schema, annotatedType) -> {
ApiModelProperty property = findAnnotation(annotatedType, ApiModelProperty.class);
if (property != null)
{
if (StringUtils.hasText(property.value()))
{
schema.setDescription(property.value());
}
if (StringUtils.hasText(property.example()))
{
schema.setExample(property.example());
}
}
return schema;
};
}
private Operation customizeLegacyOperation(Operation operation, HandlerMethod handlerMethod)
{
Api api = handlerMethod.getBeanType().getAnnotation(Api.class);
if (api != null)
{
List<String> tags = Arrays.stream(api.tags())
.filter(StringUtils::hasText)
.collect(Collectors.toList());
if (tags.isEmpty() && StringUtils.hasText(api.value()))
{
tags.add(api.value());
}
if (!tags.isEmpty())
{
operation.setTags(tags);
}
}
ApiOperation apiOperation = handlerMethod.getMethodAnnotation(ApiOperation.class);
if (apiOperation != null)
{
if (StringUtils.hasText(apiOperation.value()))
{
operation.setSummary(apiOperation.value());
}
if (StringUtils.hasText(apiOperation.notes()))
{
operation.setDescription(apiOperation.notes());
}
}
applyLegacyParameterDescriptions(operation, handlerMethod);
return operation;
}
private void applyLegacyParameterDescriptions(Operation operation, HandlerMethod handlerMethod)
{
if (operation.getParameters() == null || operation.getParameters().isEmpty())
{
return;
}
List<Parameter> parameters = operation.getParameters();
int fallbackIndex = 0;
for (MethodParameter methodParameter : handlerMethod.getMethodParameters())
{
ApiParam apiParam = methodParameter.getParameterAnnotation(ApiParam.class);
if (apiParam == null)
{
continue;
}
String parameterName = StringUtils.hasText(apiParam.name())
? apiParam.name() : methodParameter.getParameterName();
Parameter parameter = parameters.stream()
.filter(item -> parameterName != null && parameterName.equals(item.getName()))
.findFirst()
.orElse(fallbackIndex < parameters.size() ? parameters.get(fallbackIndex++) : null);
if (parameter != null)
{
if (StringUtils.hasText(apiParam.value()))
{
parameter.setDescription(apiParam.value());
}
if (apiParam.required())
{
parameter.setRequired(true);
}
if (StringUtils.hasText(apiParam.example()))
{
parameter.setExample(apiParam.example());
}
}
}
}
private <A extends Annotation> A findAnnotation(AnnotatedType annotatedType, Class<A> annotationType)
{
Annotation[] annotations = annotatedType.getCtxAnnotations();
if (annotations == null)
{
return null;
}
for (Annotation annotation : annotations)
{
if (annotationType.isInstance(annotation))
{
return annotationType.cast(annotation);
}
}
return null;
}
}

View File

@ -1,140 +0,0 @@
package com.cmvr.web.core.config;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.cmvr.common.config.CmvrIotConfig;
import com.cmvr.test.model.vo.inspection.InspectionAlarmRuleVO;
import com.cmvr.test.model.vo.inspection.InspectionManualReviewConfigVO;
import com.cmvr.test.model.vo.inspection.InspectionMediaVO;
import com.cmvr.test.model.vo.inspection.InspectionMeterRecognizeConfigVO;
import com.fasterxml.classmate.TypeResolver;
import io.swagger.annotations.ApiOperation;
import io.swagger.models.auth.In;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.Contact;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.service.SecurityScheme;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
/**
* Swagger2的接口配置
*
* @author cmvr-iot
*/
@Configuration
public class SwaggerConfig
{
/** 系统基础配置 */
@Autowired
private CmvrIotConfig cmvrIotConfig;
/** 用于将动态工作流payload对应的参数模型显式加入Swagger文档。 */
@Autowired
private TypeResolver typeResolver;
/** 是否开启swagger */
@Value("${swagger.enabled}")
private boolean enabled;
/** 设置请求的统一前缀 */
@Value("${swagger.pathMapping}")
private String pathMapping;
/**
* 创建API
*/
@Bean
public Docket createRestApi()
{
return new Docket(DocumentationType.OAS_30)
// 是否启用Swagger
.enable(enabled)
// 用来创建该API的基本信息,展示在文档的页面中(自定义展示的信息)
.apiInfo(apiInfo())
// 设置哪些接口暴露给Swagger展示
.select()
// 扫描所有有注解的api,用这种方式更灵活
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
// 扫描指定包中的swagger注解
// .apis(RequestHandlerSelectors.basePackage("com.cmvr.project.tool.swagger"))
// 扫描所有 .apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build()
// 工作流单节点接口使用JSONObject作为payload,需显式注册巡检节点参数模型。
.additionalModels(
typeResolver.resolve(InspectionMeterRecognizeConfigVO.class),
typeResolver.resolve(InspectionAlarmRuleVO.class),
typeResolver.resolve(InspectionManualReviewConfigVO.class),
typeResolver.resolve(InspectionMediaVO.class))
/* 设置安全模式,swagger可以设置访问token */
.securitySchemes(securitySchemes())
.securityContexts(securityContexts())
.pathMapping(pathMapping);
}
/**
* 安全模式,这里指定token通过Authorization头请求头传递
*/
private List<SecurityScheme> securitySchemes()
{
List<SecurityScheme> apiKeyList = new ArrayList<SecurityScheme>();
apiKeyList.add(new ApiKey("Authorization", "Authorization", In.HEADER.toValue()));
return apiKeyList;
}
/**
* 安全上下文
*/
private List<SecurityContext> securityContexts()
{
List<SecurityContext> securityContexts = new ArrayList<>();
securityContexts.add(
SecurityContext.builder()
.securityReferences(defaultAuth())
.operationSelector(o -> o.requestMappingPattern().matches("/.*"))
.build());
return securityContexts;
}
/**
* 默认的安全上引用
*/
private List<SecurityReference> defaultAuth()
{
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
List<SecurityReference> securityReferences = new ArrayList<>();
securityReferences.add(new SecurityReference("Authorization", authorizationScopes));
return securityReferences;
}
/**
* 添加摘要信息
*/
private ApiInfo apiInfo()
{
// 用ApiInfoBuilder进行定制
return new ApiInfoBuilder()
// 设置标题
.title("标题:招商车研物联网平台管理系统_接口文档")
// 描述
.description("描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...")
// 作者信息
.contact(new Contact(cmvrIotConfig.getName(), null, null))
// 版本
.version("版本号:" + cmvrIotConfig.getVersion())
.build();
}
}

View File

@ -0,0 +1,38 @@
package com.cmvr.web.service;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class CameraStreamTicketService {
private static final long TICKET_LIFETIME_MILLIS = Duration.ofSeconds(30).toMillis();
private final Map<String, StreamTarget> tickets = new ConcurrentHashMap<>();
public String issue(String robotId, String deviceId) {
long now = System.currentTimeMillis();
tickets.entrySet().removeIf(entry -> entry.getValue().expiresAt() < now);
String ticket = UUID.randomUUID().toString().replace("-", "");
tickets.put(ticket, new StreamTarget(robotId, deviceId, now + TICKET_LIFETIME_MILLIS));
return ticket;
}
public StreamTarget consume(String ticket) {
if (ticket == null || ticket.isBlank()) {
return null;
}
StreamTarget target = tickets.get(ticket);
if (target == null || target.expiresAt() < System.currentTimeMillis()) {
tickets.remove(ticket);
return null;
}
return target;
}
public record StreamTarget(String robotId, String deviceId, long expiresAt) {
}
}

View File

@ -68,7 +68,7 @@ spring:
# 热部署开关
enabled: true
# redis 配置
redis:
data.redis:
# 地址
host: 192.168.28.10
# 端口,默认为6379
@ -133,7 +133,8 @@ flowise:
query: http://192.168.0.108:3000/api/v1/executions/
api-key: pg61JW6W_GXyqmqSoURJ4mlSRrCMRzGLBJli_w3BFkg
# TTS外部接口配置
tts:
external-api:
url: http://192.168.0.102:9003/generate_advanced_audio
media-analysis:
base-url: ${MEDIA_ANALYSIS_BASE_URL:http://192.168.28.10:14080}
api-key: ${MEDIA_ANALYSIS_API_KEY:}
connect-timeout-ms: 5000
read-timeout-ms: 600000

View File

@ -68,7 +68,7 @@ spring:
# 热部署开关
enabled: true
# redis 配置
redis:
data.redis:
# 地址
host: 192.168.0.100
# 端口,默认为6379
@ -131,4 +131,4 @@ flowise:
start: http://192.168.0.108:3000/api/v1/prediction/f99329e9-b33d-437d-90ed-69eaa8a05418
abort: http://192.168.0.108:3000/api/v1/chatmessage/abort/f99329e9-b33d-437d-90ed-69eaa8a05418/
query: http://192.168.0.108:3000/api/v1/executions/
api-key: pg61JW6W_GXyqmqSoURJ4mlSRrCMRzGLBJli_w3BFkg
api-key: pg61JW6W_GXyqmqSoURJ4mlSRrCMRzGLBJli_w3BFkg

View File

@ -0,0 +1,61 @@
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
# 主库数据源
master:
url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: password
# 从库数据源
slave:
# 从数据源开关/默认关闭
enabled: false
url:
username:
password:
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置连接超时时间
connectTimeout: 30000
# 配置网络超时时间
socketTimeout: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
webStatFilter:
enabled: true
statViewServlet:
enabled: true
# 设置白名单,不填则允许所有访问
allow:
url-pattern: /druid/*
# 控制台管理用户名和密码
login-username: ruoyi
login-password: 123456
filter:
stat:
enabled: true
# 慢SQL记录
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true

View File

@ -68,7 +68,7 @@ spring:
# 热部署开关
enabled: true
# redis 配置
redis:
data.redis:
# 地址
host: 192.168.28.10
# 端口,默认为6379
@ -131,4 +131,10 @@ flowise:
start: http://192.168.0.108:3000/api/v1/prediction/f99329e9-b33d-437d-90ed-69eaa8a05418
abort: http://192.168.0.108:3000/api/v1/chatmessage/abort/f99329e9-b33d-437d-90ed-69eaa8a05418/
query: http://192.168.0.108:3000/api/v1/executions/
api-key: pg61JW6W_GXyqmqSoURJ4mlSRrCMRzGLBJli_w3BFkg
api-key: pg61JW6W_GXyqmqSoURJ4mlSRrCMRzGLBJli_w3BFkg
media-analysis:
base-url: ${MEDIA_ANALYSIS_BASE_URL:http://192.168.28.10:14080}
api-key: ${MEDIA_ANALYSIS_API_KEY:}
connect-timeout-ms: 5000
read-timeout-ms: 600000

View File

@ -68,7 +68,7 @@ spring:
# 热部署开关
enabled: true
# redis 配置
redis:
data.redis:
# 地址
host: redis
# 端口,默认为6379

View File

@ -68,7 +68,7 @@ spring:
# 热部署开关
enabled: true
# redis 配置
redis:
data.redis:
# 地址
host: 192.168.28.10
# 端口,默认为6379
@ -131,4 +131,10 @@ flowise:
start: http://192.168.0.108:3000/api/v1/prediction/f99329e9-b33d-437d-90ed-69eaa8a05418
abort: http://192.168.0.108:3000/api/v1/chatmessage/abort/f99329e9-b33d-437d-90ed-69eaa8a05418/
query: http://192.168.0.108:3000/api/v1/executions/
api-key: pg61JW6W_GXyqmqSoURJ4mlSRrCMRzGLBJli_w3BFkg
api-key: pg61JW6W_GXyqmqSoURJ4mlSRrCMRzGLBJli_w3BFkg
media-analysis:
base-url: ${MEDIA_ANALYSIS_BASE_URL:http://192.168.28.10:14080}
api-key: ${MEDIA_ANALYSIS_API_KEY:cmvr-analysis-2026-c65ec181d5ab4a49aef34e6fd1be6704}
connect-timeout-ms: 5000
read-timeout-ms: 600000

View File

@ -48,6 +48,13 @@ user:
# Spring配置
spring:
jackson:
date-format: "yyyy-MM-dd HH:mm:ss"
time-zone: Asia/Shanghai
sql:
init:
mode: always
schema-locations: classpath:db/notice-schema.sql
# 资源信息
messages:
# 国际化资源文件路径
@ -99,13 +106,15 @@ swagger:
# 请求前缀
pathMapping: /
knife4j:
# 开启屏蔽文档资源
# 生产环境可开启屏蔽文档资源
production: false
# 是否开启Knife4j(swagger 增强版)
# 是否开启Knife4j增强界面
enable: true
setting:
language: zh-CN
enableRequestCache: true
enableSwaggerModels: true
swaggerModelName: 数据模型
# 防止XSS攻击
xss:
@ -116,4 +125,43 @@ xss:
# 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/*,/device/*,/test/*,/api/*
# Integrated QUIC gateway and in-process node synchronization.
cmvr:
inspection:
robot-state:
enabled: true
initial-delay-ms: 3000
refresh-ms: 1000
heartbeat-check-ms: 5000
heartbeat-timeout-ms: ${CMVR_ROBOT_HEARTBEAT_TIMEOUT_MS:${cmvr.quic.heartbeat-timeout-ms:15000}}
# Comma-separated IDs for pure logical workflows. These IDs bypass QUIC online checks.
virtual-robot-ids: CN-CMVR-MBLRV1-TEST-20260821-001
quic:
enabled: true
bind-host: 0.0.0.0
port: 4433
grpc-host-source: observed-source
alpn: cmvr-quic-edge/1
allowed-node-ids: ""
tls:
certificate: "certs/quic-gateway.crt"
private-key: "certs/quic-gateway.key"
require-client-certificate: false
client-ca: ""
heartbeat-interval-ms: 5000
heartbeat-timeout-ms: 15000
idle-timeout-ms: 30000
maximum-control-frame-bytes: 1048576
datagram-queue-length: 1024
maximum-frame-bytes: 8388608
maximum-reassembly-bytes: 67108864
maximum-reassembly-frames: 256
reassembly-timeout-ms: 2000
media-subscriber-queue-capacity: 64
springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
enabled: true
path: /swagger-ui.html

View File

@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS sys_notice_read (
read_id BIGINT NOT NULL AUTO_INCREMENT,
notice_id INT NOT NULL,
user_id BIGINT NOT NULL,
read_time DATETIME NOT NULL,
PRIMARY KEY (read_id),
UNIQUE KEY uk_user_notice (user_id, notice_id),
KEY idx_notice_id (notice_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Notice read records';

View File

@ -6,7 +6,7 @@
<parent>
<groupId>com.cmvr</groupId>
<artifactId>cmvr-iot</artifactId>
<version>3.8.9</version>
<version>3.9.2</version>
</parent>
<artifactId>cmvr-iot-aima</artifactId>

View File

@ -2,6 +2,7 @@ package com.cmvr.aima.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;
@ -46,6 +47,18 @@ public class AimaTestLog extends BaseEntity
@ApiModelProperty("测试用例ID")
private String testCaseId;
/** 工作流检测项 ID,仅用于接口和 WebSocket 展示,不落日志表。 */
@TableField(exist = false)
private String itemId;
/** 当前绑定项的执行位置,从 1 开始。 */
@TableField(exist = false)
private Integer itemOrder;
/** 同一检测项在任务中第几次出现。 */
@TableField(exist = false)
private Integer itemOccurrence;
@Excel(name = "用例名称")
@ApiModelProperty("用例名称")
private String caseName;

View File

@ -0,0 +1,24 @@
package com.cmvr.aima.domain.vo;
import com.alibaba.fastjson2.JSONObject;
import com.cmvr.test.model.vo.ResourceRuntimeAssignmentVO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
@ApiModel("爱玛任务启动参数")
public class AimaTaskStartVO {
@ApiModelProperty("兼容单机器人执行ID")
private String robotId;
@ApiModelProperty("机器人角色运行分配")
private List<ResourceRuntimeAssignmentVO> runtimeAssignments = new ArrayList<>();
@ApiModelProperty("按检测项和出现序号组织的本次运行参数")
private JSONObject runParams = new JSONObject();
}

View File

@ -1,16 +1,21 @@
package com.cmvr.aima.listener;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSONObject;
import com.cmvr.common.utils.DateUtils;
import com.cmvr.framework.websocket.service.MessagePushService;
import com.cmvr.aima.domain.AimaTaskInstance;
import com.cmvr.aima.domain.AimaTestLog;
import com.cmvr.aima.enums.AimaTaskStatusEnum;
import com.cmvr.aima.service.IAimaTaskInstanceService;
import com.cmvr.aima.service.IAimaTaskService;
import com.cmvr.aima.service.IAimaTestLogService;
import com.cmvr.aima.support.AimaLogExecutionContext;
import com.cmvr.aima.support.AimaAnalysisVerdict;
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.test.flow.runtime.operator.FlowMediaParamResolver;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@ -28,18 +33,23 @@ import java.util.*;
public class AimaFlowExecutionListener implements FlowExecutionListener {
private final IAimaTaskInstanceService aimaTaskInstanceService;
private final IAimaTestLogService aimaTestLogService;
private final IAimaTaskService aimaTaskService;
private final MessagePushService messagePushService;
/** key:itemId, value:已完成节点集合 */
private final Map<String, Set<String>> itemNodeExecutMap = new HashMap<>();
private final Map<String, Set<String>> itemNodeExecutMap = new java.util.concurrent.ConcurrentHashMap<>();
/** key:taskInsId(流程实例id), value:已执行item集合 */
private final Map<String, Set<String>> taskItemExecutMap = new HashMap<>();
private final Map<String, Set<String>> taskItemExecutMap = new java.util.concurrent.ConcurrentHashMap<>();
/** key: flow instance ID, value: test case IDs in binding order. */
private final Map<String, List<String>> taskCaseBindingMap = new java.util.concurrent.ConcurrentHashMap<>();
public AimaFlowExecutionListener(IAimaTaskInstanceService aimaTaskInstanceService,
IAimaTestLogService aimaTestLogService,
IAimaTaskService aimaTaskService,
MessagePushService messagePushService) {
this.aimaTaskInstanceService = aimaTaskInstanceService;
this.aimaTestLogService = aimaTestLogService;
this.aimaTaskService = aimaTaskService;
this.messagePushService = messagePushService;
}
@ -60,17 +70,17 @@ public class AimaFlowExecutionListener implements FlowExecutionListener {
// 1.节点完成才记录当前item下node
if (StrUtil.isNotBlank(itemId) && StrUtil.isNotBlank(nodeId)
&& FlowExecutionEvent.EventType.NODE_COMPLETED == event.getEventType()) {
itemNodeExecutMap.computeIfAbsent(itemId, k -> new HashSet<>()).add(nodeId);
itemNodeExecutMap.computeIfAbsent(itemExecutionKey(instId, event),
k -> java.util.concurrent.ConcurrentHashMap.newKeySet()).add(nodeId);
}
// 任意事件:当前任务绑定item(item一启动就入Map)
if (StrUtil.isNotBlank(instId) && StrUtil.isNotBlank(itemId)) {
taskItemExecutMap.computeIfAbsent(instId, k -> new HashSet<>()).add(itemId);
taskItemExecutMap.computeIfAbsent(instId,
k -> java.util.concurrent.ConcurrentHashMap.newKeySet()).add(itemExecutionKey(instId, event));
}
// 查询任务实例
AimaTaskInstance taskInstance = aimaTaskInstanceService.lambdaQuery()
.eq(AimaTaskInstance::getTaskInsId, instId)
.one();
AimaTaskInstance taskInstance = findTaskInstance(instId);
if (taskInstance == null) {
log.warn("未查询到爱玛测试实例,taskInsId:{}", instId);
return;
@ -78,6 +88,7 @@ public class AimaFlowExecutionListener implements FlowExecutionListener {
int targetStatus = AimaTaskStatusEnum.RUNNING.getCode();
boolean needUpdateDb = false;
String terminalMessage = null;
Date now = DateUtils.getNowDate();
switch (event.getEventType()) {
@ -85,12 +96,20 @@ public class AimaFlowExecutionListener implements FlowExecutionListener {
targetStatus = AimaTaskStatusEnum.SUCCESS.getCode();
needUpdateDb = true;
clearCache(instId);
pushCompleteMsg(taskInstance.getId(), now, targetStatus);
terminalMessage = "任务执行完成";
break;
case TASK_FAILED:
targetStatus = AimaTaskStatusEnum.FAILED.getCode();
needUpdateDb = true;
clearCache(instId);
terminalMessage = StrUtil.isBlank(event.getErrorMessage())
? "任务执行失败" : "任务执行失败:" + event.getErrorMessage();
break;
case TASK_STOPPED:
targetStatus = AimaTaskStatusEnum.STOPPED.getCode();
needUpdateDb = true;
clearCache(instId);
terminalMessage = "任务已终止";
break;
case NODE_COMPLETED:
default:
@ -102,6 +121,10 @@ public class AimaFlowExecutionListener implements FlowExecutionListener {
taskInstance.setEndTime(now);
aimaTaskInstanceService.updateById(taskInstance);
}
if (terminalMessage != null) {
pushTerminalMsg(taskInstance.getId(), now, targetStatus, terminalMessage,
event.getErrorMessage());
}
// 节点推送 - 记录日志
if (StrUtil.isNotBlank(nodeId)) {
@ -114,11 +137,14 @@ public class AimaFlowExecutionListener implements FlowExecutionListener {
int finishedItemNum = itemTotal - 1;
// 当前item已完成节点数
Set<String> finishedNodeSet = itemNodeExecutMap.getOrDefault(itemId, new HashSet<>());
Set<String> finishedNodeSet = itemNodeExecutMap.getOrDefault(itemExecutionKey(instId, event), new HashSet<>());
int finishedNode = finishedNodeSet.size();
int totalNode = event.getNodeCount() == 0 ? 1 : event.getNodeCount();
// 已完整跑完用例占比
if (event.getItemOrder() != null && event.getItemOrder() > 0) {
finishedItemNum = event.getItemOrder() - 1;
}
double itemTotalRatio = finishedItemNum / (double) totalCases;
// 当前运行item内部节点占比
double innerRatio = finishedNode / (double) totalNode / totalCases;
@ -130,24 +156,52 @@ public class AimaFlowExecutionListener implements FlowExecutionListener {
progress = (itemTotalRatio + innerRatio) * 100;
}
String info = event.getEventType() == FlowExecutionEvent.EventType.NODE_STARTED ? "开始执行" : "执行完成";
String info = event.getEventType() == FlowExecutionEvent.EventType.NODE_STARTED
? "开始执行"
: event.getEventType() == FlowExecutionEvent.EventType.NODE_FAILED
? "执行失败" : "执行完成";
int nodeLogStatus = event.getEventType() == FlowExecutionEvent.EventType.NODE_FAILED
? AimaTaskStatusEnum.FAILED.getCode() : targetStatus;
String logId = UUID.randomUUID().toString().replace("-", "");
// 四舍五入保留2位小数
progress = Math.round(progress * 100) / 100.0;
JSONObject outputParams = event.getOutputParams();
boolean nodeFailed = event.getEventType() == FlowExecutionEvent.EventType.NODE_FAILED;
String nodeError = nodeFailed ? StrUtil.trim(event.getErrorMessage()) : null;
JSONObject logOutputParams = outputParams == null
? new JSONObject() : JSONObject.parseObject(outputParams.toJSONString());
if (StrUtil.isNotBlank(nodeError)) {
logOutputParams.put("errorMessage", nodeError);
}
String imageUrl = FlowMediaParamResolver.lastUrl(outputParams, "imageUrl");
String videoUrl = FlowMediaParamResolver.lastUrl(outputParams, "videoUrl");
String testCaseId = resolveTestCaseId(instId, taskInstance.getTaskId(), event);
// 获取casename
String casename = aimaTaskService.getCaseName(testCaseId);
// 保存日志到数据库
AimaTestLog testLog = AimaTestLog.builder()
.id(logId)
.taskInstanceId(taskInstance.getId())
.taskId(taskInstance.getTaskId())
.testCaseId(itemId)
.logLevel("INFO")
.logType("执行日志")
.logContent("节点【" + event.getNodeName() + "】" + info)
.testCaseId(testCaseId)
.caseName(casename)
.itemId(itemId)
.itemOrder(event.getItemOrder())
.itemOccurrence(event.getItemOccurrence())
.logLevel(nodeFailed ? "ERROR" : "INFO")
.logType(nodeFailed ? "异常日志" : "执行日志")
.logContent("节点【" + event.getNodeName() + "】" + info
+ (StrUtil.isBlank(nodeError) ? "" : ":" + StrUtil.sub(nodeError, 0, 300)))
.actualResult(AimaLogExecutionContext.attach(logOutputParams, itemId,
event.getItemOrder(), event.getItemOccurrence(),
event.getEventType().name(), event.getNodeType(), event.getIterations()))
.screenshotUrl(imageUrl)
.videoUrl(videoUrl)
.errorStack(nodeError)
.logTime(now)
.status(targetStatus)
.status(nodeLogStatus)
.executionStep(event.getNodeName())
.testStatus("0") // 0未执行
.testStatus(AimaAnalysisVerdict.resolve(event.getAction(), outputParams))
.durationMs(0L)
.progress( progress)
.build();
@ -172,30 +226,68 @@ public class AimaFlowExecutionListener implements FlowExecutionListener {
Set<String> itemList = taskItemExecutMap.getOrDefault(taskInsId, new HashSet<>());
itemList.forEach(itemNodeExecutMap::remove);
taskItemExecutMap.remove(taskInsId);
taskCaseBindingMap.remove(taskInsId);
}
private String itemExecutionKey(String taskInsId, FlowExecutionEvent event) {
if (event.getItemOrder() != null && event.getItemOrder() > 0) {
return taskInsId + "|" + event.getItemOrder();
}
return taskInsId + "|" + event.getItemId();
}
private AimaTaskInstance findTaskInstance(String taskInsId) {
for (int attempt = 0; attempt < 5; attempt++) {
AimaTaskInstance instance = aimaTaskInstanceService.lambdaQuery()
.eq(AimaTaskInstance::getTaskInsId, taskInsId)
.one();
if (instance != null) {
return instance;
}
try {
Thread.sleep(50L);
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
return null;
}
}
return null;
}
private String resolveTestCaseId(String taskInsId, String taskId, FlowExecutionEvent event) {
if (event.getItemOrder() == null || event.getItemOrder() < 1) {
return event.getItemId();
}
List<String> testCaseIds = taskCaseBindingMap.computeIfAbsent(taskInsId,
key -> aimaTaskService.getTestCaseIds(taskId).stream().map(testCase -> testCase.getId()).toList());
int index = event.getItemOrder() - 1;
return index < testCaseIds.size() ? testCaseIds.get(index) : event.getItemId();
}
/**
* 推送完成任务信息
* 保存并推送任务终态信息
*/
public void pushCompleteMsg(String dbInsId, Date now, int targetStatus) {
public void pushTerminalMsg(String dbInsId, Date now, int targetStatus,
String message, String errorMessage) {
String logId = UUID.randomUUID().toString().replace("-", "");
// 保存完成日志
// 保存终态日志
AimaTestLog testLog = AimaTestLog.builder()
.id(logId)
.taskInstanceId(dbInsId)
.logLevel("INFO")
.logType("系统日志")
.logContent("任务执行完成")
.logContent(message)
.errorStack(errorMessage)
.logTime(now)
.status(targetStatus)
.build();
try {
aimaTestLogService.insertAimaTestLog(testLog);
log.info("任务完成日志保存成功,logId:{},taskInstanceId:{}", logId, dbInsId);
log.info("任务终态日志保存成功,logId:{},taskInstanceId:{},status:{}", logId, dbInsId, targetStatus);
} catch (Exception e) {
log.error("任务完成日志保存失败,taskInstanceId:{}", dbInsId, e);
log.error("任务终态日志保存失败,taskInstanceId:{},status:{}", dbInsId, targetStatus, e);
}
try {
messagePushService.pushToChannel("AimaTaskInstance", testLog);

View File

@ -5,6 +5,7 @@ import java.util.List;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Param;
import com.cmvr.aima.domain.AimaTaskTestCase;
import com.cmvr.aima.domain.AimaTestCase;
/**
* 爱玛任务与测试用例关联Mapper接口
@ -38,6 +39,9 @@ public interface AimaTaskTestCaseMapper extends MPJBaseMapper<AimaTaskTestCase>
*/
List<String> selectTestCaseIdsByTaskId(String taskId);
/** 按任务中的位置查询用例,重复用例会返回多条独立记录。 */
List<AimaTestCase> selectTestCasesByTaskId(String taskId);
/**
* 批量删除任务-测试用例关联
*

View File

@ -2,10 +2,11 @@ package com.cmvr.aima.service;
import java.util.List;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.spring.service.IService;
import com.cmvr.aima.domain.AimaTaskInstance;
import com.cmvr.aima.domain.dto.AimaTaskInstanceQuery;
import com.cmvr.aima.domain.vo.AimaTaskInstanceVo;
import com.cmvr.aima.domain.vo.AimaTaskStartVO;
/**
* 爱玛任务执行实例Service接口
@ -28,7 +29,10 @@ public interface IAimaTaskInstanceService extends IService<AimaTaskInstance> {
* @param id 任务实例ID
* @return 结果
*/
int startInstance(String id);
int startInstance(String id, AimaTaskStartVO startVO);
/** 按历史执行参数创建并启动一个新的爱玛任务实例。 */
String rerunInstance(String id);
/**
* 暂停任务实例

View File

@ -16,6 +16,7 @@ public interface IAimaTaskService {
List<AimaTask> selectAimaTaskList(AimaTask aimaTask);
List<AimaTaskVo> selectAimaTaskVoList(AimaTask aimaTask);
int insertAimaTask(AimaTask aimaTask);
String copyAimaTask(String id);
int updateAimaTask(AimaTask aimaTask);
int deleteAimaTaskByIds(String[] ids);
int deleteAimaTaskById(String id);
@ -35,4 +36,6 @@ public interface IAimaTaskService {
* @return 测试用例ID列表
*/
List<AimaTestCase> getTestCaseIds(String taskId);
String getCaseName(String testCaseId);
}

View File

@ -43,6 +43,9 @@ public interface IAimaTestCaseService {
*/
int insertAimaTestCase(AimaTestCase aimaTestCase);
/** Copy a test case and its independent detection item. */
String copyAimaTestCase(String id);
/**
* 修改爱玛测试用例
*

View File

@ -2,7 +2,7 @@ package com.cmvr.aima.service.impl;
import java.util.List;
import java.util.UUID;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.spring.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.cmvr.common.utils.DateUtils;

View File

@ -2,7 +2,7 @@ package com.cmvr.aima.service.impl;
import java.util.List;
import java.util.UUID;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.spring.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.cmvr.common.utils.DateUtils;

View File

@ -2,16 +2,19 @@ package com.cmvr.aima.service.impl;
import java.util.List;
import java.util.UUID;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.spring.service.impl.ServiceImpl;
import com.cmvr.aima.domain.dto.AimaTaskInstanceQuery;
import com.cmvr.aima.domain.vo.AimaTaskInstanceVo;
import com.cmvr.aima.domain.vo.AimaTaskStartVO;
import com.cmvr.common.exception.ServiceException;
import com.cmvr.common.utils.SecurityUtils;
import com.cmvr.test.flow.control.FlowControlService;
import com.cmvr.test.flow.runtime.engine.FlowTaskRuntimeService;
import com.cmvr.test.flow.runtime.event.FlowExecutionModuleCodes;
import com.cmvr.test.model.vo.TeTaskExecuteNormalVO;
import com.cmvr.test.model.vo.TeTaskOrchestraVO;
import com.cmvr.test.service.ITeTaskOrchestrationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
@ -24,6 +27,9 @@ import com.cmvr.aima.enums.AimaTaskStatusEnum;
import com.cmvr.aima.service.IAimaTaskService;
import com.cmvr.aima.domain.AimaTask;
import org.springframework.transaction.annotation.Transactional;
import java.util.stream.Collectors;
import java.util.HashMap;
import java.util.Map;
/**
* 爱玛任务执行实例Service业务层处理
@ -119,7 +125,7 @@ public class AimaTaskInstanceServiceImpl extends ServiceImpl<AimaTaskInstanceMap
* 开始执行任务实例
*/
@Override
public int startInstance(String id) {
public int startInstance(String id, AimaTaskStartVO startVO) {
AimaTaskInstance instance = aimaTaskInstanceMapper.selectAimaTaskInstanceById(id);
if (instance == null) {
throw new ServiceException("任务实例不存在");
@ -137,17 +143,44 @@ public class AimaTaskInstanceServiceImpl extends ServiceImpl<AimaTaskInstanceMap
AimaTask aimaTask = aimaTaskService.selectAimaTaskById(instance.getTaskId());
JSONObject runParams = new JSONObject();
// 从test模块的任务编排中获取所有的检测项id
if (aimaTask != null && aimaTask.getTaskConfigId() != null) {
teTaskOrchestrationService.queryByTaskId(aimaTask.getTaskConfigId())
.forEach(item -> runParams.put(item.getItemId(), new JSONObject()));
if (aimaTask == null || StrUtil.isBlank(aimaTask.getTaskConfigId())) {
throw new ServiceException("智能测试任务没有关联流程编排");
}
List<String> orderedItemIds = aimaTaskService.getTestCaseIds(instance.getTaskId()).stream()
.map(com.cmvr.aima.domain.AimaTestCase::getDetectItemId)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toList());
if (orderedItemIds.isEmpty()) {
throw new ServiceException("智能测试任务没有绑定可执行的测试用例");
}
teTaskOrchestrationService.insertTeTaskOrchestration(TeTaskOrchestraVO.builder()
.taskId(aimaTask.getTaskConfigId())
.itemIds(orderedItemIds)
.build());
JSONObject runParams = new JSONObject();
JSONObject requestedRunParams = startVO == null ? null : startVO.getRunParams();
Map<String, Integer> itemOccurrences = new HashMap<>();
// 从test模块的任务编排中获取所有的检测项id
teTaskOrchestrationService.queryByTaskId(aimaTask.getTaskConfigId()).forEach(item -> {
int occurrence = itemOccurrences.merge(item.getItemId(), 1, Integer::sum);
JSONObject submitted = requestedRunParams == null ? null
: requestedRunParams.getJSONObject(item.getItemId() + ":" + occurrence);
if (submitted == null && requestedRunParams != null) {
submitted = requestedRunParams.getJSONObject(item.getItemId());
}
JSONObject itemParams = submitted == null ? new JSONObject() : new JSONObject(submitted);
itemParams.put("phoneId", instance.getPhoneId());
itemParams.put("vehicleId", instance.getVehicleId());
itemParams.put("taskInstanceId", instance.getId());
runParams.put(item.getItemId() + ":" + occurrence, itemParams);
});
// 调用流程引擎执行
String insId = flowTaskRuntimeService.executeTask(
TeTaskExecuteNormalVO.builder()
.taskId(aimaTask.getTaskConfigId())
.moduleCode(FlowExecutionModuleCodes.AIMA)
.robotId(startVO == null ? null : startVO.getRobotId())
.runtimeAssignments(startVO == null ? null : startVO.getRuntimeAssignments())
.runParams(runParams)
.build()
);
@ -155,6 +188,36 @@ public class AimaTaskInstanceServiceImpl extends ServiceImpl<AimaTaskInstanceMap
return aimaTaskInstanceMapper.updateAimaTaskInstance(update);
}
@Override
public String rerunInstance(String id) {
AimaTaskInstance source = aimaTaskInstanceMapper.selectAimaTaskInstanceById(id);
if (source == null) {
throw new ServiceException("任务实例不存在");
}
if (AimaTaskStatusEnum.SUCCESS.getCode() != source.getStatus()
&& AimaTaskStatusEnum.FAILED.getCode() != source.getStatus()) {
throw new ServiceException("只有已完成或失败的任务才能再次执行");
}
if (StrUtil.isBlank(source.getTaskInsId())) {
throw new ServiceException("历史任务没有执行参数快照,无法再次执行");
}
TeTaskExecuteNormalVO snapshot = flowTaskRuntimeService.loadExecutionRequest(source.getTaskInsId());
AimaTaskInstance next = new AimaTaskInstance();
next.setTaskId(source.getTaskId());
next.setPhoneId(source.getPhoneId());
next.setVehicleId(source.getVehicleId());
next.setExecutionNumber(source.getExecutionNumber() == null ? 1 : source.getExecutionNumber() + 1);
next.setRemark(source.getRemark());
insertAimaTaskInstance(next);
AimaTaskStartVO startVO = new AimaTaskStartVO();
startVO.setRobotId(snapshot.getRobotId());
startVO.setRuntimeAssignments(snapshot.getRuntimeAssignments());
startVO.setRunParams(snapshot.getRunParams());
startInstance(next.getId(), startVO);
return next.getId();
}
/**
* 暂停任务实例
*/
@ -185,8 +248,12 @@ public class AimaTaskInstanceServiceImpl extends ServiceImpl<AimaTaskInstanceMap
if (instance == null) {
throw new ServiceException("任务实例不存在");
}
if (AimaTaskStatusEnum.NOT_STARTED.getCode() != instance.getStatus()
&& AimaTaskStatusEnum.RUNNING.getCode() != instance.getStatus()) {
if (AimaTaskStatusEnum.STOPPED.getCode() == instance.getStatus()) {
return 1;
}
if (AimaTaskStatusEnum.NOT_STARTED.getCode() != instance.getStatus()
&& AimaTaskStatusEnum.RUNNING.getCode() != instance.getStatus()
&& AimaTaskStatusEnum.PAUSED.getCode() != instance.getStatus()) {
throw new ServiceException("已完成或已取消的任务实例不能终止");
}
AimaTaskInstance update = new AimaTaskInstance();
@ -195,7 +262,9 @@ public class AimaTaskInstanceServiceImpl extends ServiceImpl<AimaTaskInstanceMap
update.setEndTime(DateUtils.getNowDate());
update.setUpdateTime(DateUtils.getNowDate());
update.setUpdateBy(SecurityUtils.getUsername());
flowControlService.stop(instance.getTaskInsId());
if (instance.getTaskInsId() != null && !instance.getTaskInsId().isBlank()) {
flowControlService.stop(instance.getTaskInsId());
}
return aimaTaskInstanceMapper.updateAimaTaskInstance(update);
}

View File

@ -6,8 +6,7 @@ import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.spring.service.impl.ServiceImpl;
import com.cmvr.aima.domain.AimaTaskTestCase;
import com.cmvr.aima.domain.AimaTestCase;
import com.cmvr.aima.domain.vo.AimaTaskVo;
@ -19,6 +18,7 @@ import com.cmvr.test.service.ITeTaskOrchestrationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.cmvr.common.utils.DateUtils;
import com.cmvr.common.utils.EntityCopyUtils;
import com.cmvr.common.utils.SecurityUtils;
import com.cmvr.aima.mapper.AimaTaskMapper;
import com.cmvr.aima.mapper.AimaTaskTestCaseMapper;
@ -76,6 +76,39 @@ public class AimaTaskServiceImpl extends ServiceImpl<AimaTaskMapper, AimaTask> i
return aimaTaskMapper.insertAimaTask(aimaTask);
}
@Override
@Transactional(rollbackFor = Exception.class)
public String copyAimaTask(String id) {
AimaTask source = aimaTaskMapper.selectAimaTaskById(id);
if (source == null) {
throw new IllegalArgumentException("爱玛任务不存在: " + id);
}
if (source.getTaskConfigId() == null || source.getTaskConfigId().isEmpty()) {
throw new IllegalStateException("爱玛任务未关联任务配置,无法完整复制: " + id);
}
String copiedConfigId = teTaskConfigInfoService.copyTeTaskConfigInfo(source.getTaskConfigId());
AimaTask copy = new AimaTask();
copy.setId(UUID.randomUUID().toString().replace("-", ""));
copy.setTaskCode(EntityCopyUtils.copyCode(source.getTaskCode(), 50));
copy.setTaskName(EntityCopyUtils.copyName(source.getTaskName(), 100));
copy.setTaskType(source.getTaskType());
copy.setPhoneId(source.getPhoneId());
copy.setVehicleId(source.getVehicleId());
copy.setTestCaseCount(0);
copy.setTaskConfigId(copiedConfigId);
copy.setRemark(source.getRemark());
copy.setCreateTime(DateUtils.getNowDate());
copy.setCreateBy(SecurityUtils.getUsername());
aimaTaskMapper.insertAimaTask(copy);
List<String> testCaseIds = aimaTaskTestCaseMapper.selectTestCasesByTaskId(id).stream()
.map(AimaTestCase::getId)
.collect(Collectors.toList());
bindTestCases(copy.getId(), testCaseIds);
return copy.getId();
}
@Override
public int updateAimaTask(AimaTask aimaTask) {
aimaTask.setUpdateTime(DateUtils.getNowDate());
@ -111,6 +144,21 @@ public class AimaTaskServiceImpl extends ServiceImpl<AimaTaskMapper, AimaTask> i
@Override
public void bindTestCases(String taskId, List<String> testCaseIds) {
aimaTaskTestCaseMapper.deleteTaskTestCaseByTaskId(taskId);
AimaTask existingTask = aimaTaskMapper.selectAimaTaskById(taskId);
if (existingTask == null) {
throw new IllegalArgumentException("任务不存在: " + taskId);
}
if (testCaseIds == null || testCaseIds.isEmpty()) {
AimaTask emptyTask = new AimaTask();
emptyTask.setId(taskId);
emptyTask.setTestCaseCount(0);
aimaTaskMapper.updateAimaTask(emptyTask);
if (existingTask.getTaskConfigId() != null) {
teTaskOrchestrationService.deleteByTaskId(existingTask.getTaskConfigId());
}
return;
}
// 先删除旧的关联
aimaTaskTestCaseMapper.deleteTaskTestCaseByTaskId(taskId);
@ -121,7 +169,7 @@ public class AimaTaskServiceImpl extends ServiceImpl<AimaTaskMapper, AimaTask> i
AimaTaskTestCase taskTestCase = new AimaTaskTestCase();
taskTestCase.setTaskId(taskId);
taskTestCase.setTestCaseId(testCaseIds.get(i));
taskTestCase.setSortOrder(i);
taskTestCase.setSortOrder(i + 1);
list.add(taskTestCase);
}
aimaTaskTestCaseMapper.batchInsertTaskTestCase(list);
@ -133,27 +181,34 @@ public class AimaTaskServiceImpl extends ServiceImpl<AimaTaskMapper, AimaTask> i
aimaTaskMapper.updateAimaTask(task);
// 查询测试用例,获取detectItemId列表
List<AimaTestCase> testCases = aimaTestCaseMapper.selectBatchIds(testCaseIds);
List<AimaTestCase> testCases = getTestCaseIds(taskId);
List<String> detectItemIds = testCases.stream()
.map(AimaTestCase::getDetectItemId)
.filter(itemId -> itemId != null && !itemId.isEmpty())
.collect(Collectors.toList());
// 同步任务编排到test模块
if (!detectItemIds.isEmpty()) {
AimaTask aimaTask = aimaTaskMapper.selectAimaTaskById(taskId);
if (!detectItemIds.isEmpty() && existingTask.getTaskConfigId() != null) {
TeTaskOrchestraVO teTaskOrchestraVO = TeTaskOrchestraVO.builder()
.taskId(aimaTask.getTaskConfigId())
.taskId(existingTask.getTaskConfigId())
.itemIds(detectItemIds)
.build();
teTaskOrchestrationService.insertTeTaskOrchestration(teTaskOrchestraVO);
} else if (existingTask.getTaskConfigId() != null) {
teTaskOrchestrationService.deleteByTaskId(existingTask.getTaskConfigId());
}
}
}
@Override
public List<AimaTestCase> getTestCaseIds(String taskId) {
List<String> ids = aimaTaskTestCaseMapper.selectTestCaseIdsByTaskId(taskId);
return aimaTestCaseMapper.selectList(new QueryWrapper<AimaTestCase>().in("id", ids));
return aimaTaskTestCaseMapper.selectTestCasesByTaskId(taskId);
}
@Override
public String getCaseName(String testCaseId) {
AimaTestCase aimaTestCase = aimaTestCaseMapper.selectAimaTestCaseById(testCaseId);
return aimaTestCase != null ? aimaTestCase.getCaseName() : null;
}
}

Some files were not shown because too many files have changed in this diff Show More