Compare commits

...

55 Commits
dev2 ... dev

Author SHA1 Message Date
521e3e4e24 feat(inspection): 新增巡检仪表识别和人工判断功能
- 添加巡检仪表读数识别动作枚举和处理器
- 实现人工巡检判断任务创建功能
- 扩展巡检报警监听服务支持工作流上下文
- 新增统一巡检结果表和媒体证据表
- 更新巡检告警实体和数据传输对象
- 扩展工作流执行事件传递更多节点信息
- 优化巡检报警入库服务支持订阅模式
2026-07-24 13:43:03 +08:00
dfdf575932 feat(workflow): 添加巡检报警监听和机械臂控制功能
- 在ActionEnum中新增巡检报警监听相关枚举值
- 集成InspectionAlertListenService和EdgeArmService依赖
- 实现巡检报警监听开始和停止功能
- 添加机械臂移动到指定坐标点功能
- 在工作流执行器中增加报警事件类型解析逻辑
- 实现基于终端ID的边缘端IP解析和报警过滤机制
- 新增巡检报警监听工作流组件服务类
- 创建巡检报警监听状态管理服务
2026-07-23 13:52:37 +08:00
922d30632b feat(inspection): 添加边缘端gRPC服务IP字段并优化报警去重逻辑
- 在AlertEnvelope中添加grpcIp字段用于前端展示和问题定位
- 将数据库表inspection_detection_alert的幂等键改为仅记录模式,使用event_id进行重复过滤
- 移除检测框明细表及相关处理逻辑,简化数据结构
- 更新麦克风和扬声器控制器的参数传递方式,统一使用RequestBody
- 新增EdgeSpeakerPlayAudioVO类用于扬声器播放音频接口参数传递
2026-07-22 14:46:32 +08:00
ecda2b0ebc feat(audio): 实现实时语音会话功能支持设备分离和音频缓冲优化
- 配置Tomcat支持20MB大JSON请求处理PPE报警中的Base64图片
- 扩展AudioService支持独立的扬声器和麦克风设备ID参数处理
- 实现浏览器音频推送器,提供音频帧缓冲和定时推送机制
- 添加音频数据protobuf定义,支持多种音频格式和编解码参数
- 重构音频会话管理,实现音频队列、统计监控和性能优化
- 增强心跳检测机制,改进机器人离线状态处理逻辑
2026-07-22 13:49:05 +08:00
152cae0323 feat(arm): 添加机械臂故障清除功能
- 在 ArmService 中新增 clearFault RPC 方法
- 生成对应的 gRPC 客户端和服务端方法实现
- 在 EdgeArmController 中添加 /clearFault 接口
- 实现 EdgeArmService 的 clearFault 业务逻辑
- 更新方法 ID 映射和协议缓冲区描述符
- 添加同步和异步调用支持
2026-07-21 10:19:19 +08:00
9f462e41fc feat(flow): 集成商道智能体平台功能
- 新增 LLM 智能体查询接口,支持文本输入调用 AI 服务
- 实现智能体任务异步执行和取消机制
- 集成 TTS 语音播放功能,支持播放和停止控制
- 添加智能体 API 密钥管理和参数配置
- 实现线程池管理和任务生命周期控制
- 增加异常处理和错误返回机制
2026-07-21 10:01:20 +08:00
6aaed4dacc refactor(agv): 重构 AGV 控制器接口设计与数据传输对象
- 统一使用 VO 对象替代零散参数,提升接口一致性
- 新增多个 AGV 相关 VO 类,包括地图、导航、速度控制等场景
- 为所有 POST 接口添加 @RequestBody 注解明确请求体映射
- 优化 protobuf 对象到 Map 的转换逻辑,添加辅助方法
- 在所有查询接口中启用参数验证注解 @Validated
- 为紧急停止和故障清除等操作改为 POST 方法增强安全性
- 更新 README.md 添加项目架构说明与开发约定
- 调整 ARM 控制器参数传递方式与验证机制
2026-07-17 15:01:05 +08:00
885e94883b feat(audio): 添加机器人实时语音对讲功能
- 在GrpcServiceManager中注册机器人实时语音双向流服务异步Stub
- 引入webrtc-java依赖支持WebRTC功能
- 新增AudioService实现WebSocket与gRPC音频流转换业务逻辑
- 实现WebSocket信令处理和音频数据传输功能
- 添加机器人音频心跳检测和会话管理机制
- 新增RobotAudioGrpcAdapter实现gRPC双向流音频传输
- 定义RobotAudioService gRPC服务协议和消息格式
- 实现音频帧队列管理和流量控制机制
2026-07-17 10:24:53 +08:00
710f33d882 refactor(flow): 重构流程执行事件和任务状态管理
- 更新 FlowExecutionEvent 类的注释和字段描述,明确流程执行事件的用途和各字段含义
- 重命名 TaskInstHolder.markSuccess 为 markNodeSuccess 方法,并拆分任务完成逻辑
- 添加 markTaskCompleted 私有方法专门处理任务完成状态更新和事件发布
- 保留旧方法名作为兼容性支持,标记为 @Deprecated
- 修复 FlowLoggingInterceptor 中的方法调用以使用新的 markNodeSuccess 方法
- 优化 FlowTaskRuntimeEntry 中的终端ID检查逻辑,移除冗余空值判断
- 改进 TaskThreadRegistry.interruptAll 方法,精确匹配实例ID并添加线程中断状态检查
- 在任务中断后释放所有 latch 避免死锁,提升系统稳定性
2026-07-14 10:12:04 +08:00
659073db03 feat(flow): 添加流程执行模块编码支持实现事件精确分发
- 在AimaAlarm实体类中添加详细的字段注释和类文档说明
- 为AimaPhone实体类的各个属性添加中文注释说明
- 在AimaFlowExecutionListener中实现getModuleCode方法返回AIMA模块编码
- 在InspectionFlowExecutionListener中实现getModuleCode方法返回INSPECTION模块编码
- 修改AimaTaskInstanceServiceImpl和InspectionTaskInstanceServiceImpl传入模块编码参数
- 在FlowExecutionEvent中新增moduleCode字段用于模块识别
- 在FlowBeforeInterceptor和FlowAfterInterceptor中传递模块编码到事件对象
- 在FlowExecutionEventPublisher中实现基于模块编码的事件分发过滤机制
- 创建FlowExecutionModuleCodes常量类统一管理模块编码值
- 在FlowTaskRuntimeEntry中注册任务上下文时包含模块编码信息
- 在TaskContext和TaskInstHolder中增加对模块编码的处理和传递逻辑
2026-07-13 10:41:13 +08:00
736c510270 feat(agv): 添加AGV协议定义和重构命令接口
- 新增agv.proto文件定义AGV相关消息结构包括位姿、速度、电池状态等
- 新增统一2D/3D地图格式定义支持栅格、点云、体素等多种地图类型
- 重构agv_command.proto将原有命令接口替换为新的运行状态查询接口
- 添加导航控制、速度设置、地图管理等标准化命令接口
- 整合站点管理、路径规划、任务状态等AGV核心功能协议
- 引入运动约束选项和适配器参数扩展支持不同厂商设备特性
2026-07-09 16:01:36 +08:00
46fb4fecde refactor(controller): 移除未使用的导入以优化代码结构
- 移除了 EdgeAgvController 中未使用的 ArrayList 和 List 导入
- 移除了 EdgeArmController 中未使用的 Common 导入
- 移除了 EdgeArmOperateService 中未使用的 ArmCommand 导入
2026-07-01 16:41:52 +08:00
fb830f77ea feat(agv): 添加AGV控制系统相关模型和API接口
- 新增EdgeAgvBatteryStatusVO电池状态数据模型
- 新增EdgeAgvNavigationResultVO导航任务结果数据模型
- 新增EdgeAgvRobotLocationVO机器人位置数据模型
- 新增EdgeAgvStationVO站点信息数据模型
- 新增EdgeAgvTaskStatusVO任务状态数据模型
- 实现EdgeAgvController提供完整的AGV控制API接口
- 添加AGV状态信息、机器人位置、地图状态查询功能
- 实现地图下载上传、电池状态获取等核心功能
- 提供导航到指定站点、自由导航到坐标点等功能
- 实现导航任务的暂停、继续、取消操作接口
- 添加站点列表查询、地图切换等管理功能
2026-07-01 16:40:42 +08:00
9ae4334421 refactor(edge): 优化边缘控制器代码结构
- 移除EdgeArmController中未使用的返回值变量
- 将EdgeArmOperateService中的switch语句替换为if-else判断
- 添加Objects.requireNonNull进行空值检查
- 简化EdgeTouchOperateService中的条件判断逻辑
- 统一异常处理方式,移除多余的break语句
- 优化代码可读性和执行效率
2026-07-01 11:39:48 +08:00
ac35fd229b refactor(arm): 重构机械臂服务实现
- 将 HumanoidRobotService 重命名为 ArmService
- 更新 EdgeTouchOperateService 中的服务依赖注入
- 修改 FlowiseActionService 中的机械臂服务引用
- 更新 GrpcServiceManager 中的客户端工厂注册
- 在 ActionEnum 中添加 ARM_MOVE_TO_POINT 动作类型
- 替换 EdgeMoveJVO 为 EdgeArmMoveJVO 模型类
- 更新触控操作中的机械臂移动方法调用参数
2026-07-01 11:32:54 +08:00
2d338d259b remove(humanoid_robot): 删除人形机器人相关proto和服务代码
- 移除 humanoid_robot_command.proto 文件及其所有消息定义
- 移除 humanoid_robot_service.proto 文件及其服务定义
- 删除 HumanoidRobotServiceGrpc.java 服务桩代码文件
- 移除 HumanoidRobotServiceOuterClass.java 外部类定义文件
- 清理相关的关节控制、运动控制及状态获取功能接口
2026-07-01 09:41:49 +08:00
707baaefb0 refactor(service): 移除未使用的LLMAiTtsService依赖
- 从FlowActionExecutorService中移除llmAiTtsService字段
- 减少不必要的服务依赖注入
- 优化代码结构和内存使用
2026-06-30 15:00:58 +08:00
0f36d8b4d1 feat(agv): 添加AGV移动到指定点功能
- 在ActionEnum中新增AGV_MOVE_TO_POINT枚举值
- 在FlowActionExecutorService中添加AGV移动逻辑处理
- 新增EdgeAgvOperateService实现AGV边缘操作服务
- 集成EdgeAgvService进行机器人导航控制
- 实现坐标参数解析和验证功能
- 添加AGV移动任务下发和结果返回机制
2026-06-30 15:00:25 +08:00
db17d50ef6 feat(agv): 添加自由导航功能支持
- 新增 robotGoTargetWithFreeGo 方法支持坐标导航
- 实现自由导航到指定坐标位置的功能
- 添加机器人实时位置查询接口
- 提供基于坐标的导航控制能力
- 支持双轮差速底盘的自由导航操作
2026-06-29 15:50:32 +08:00
6d39c4296e refactor(robot): 重构机器人地图上传功能
- 移除原有的 uploadMapToRobot 方法,简化服务接口
- 添加新的 uploadMapFromSource 方法,支持从源机器人下载并上传到目标机器人
- 更新控制器中的路径变量注解,移除冗余参数定义
- 实现跨机器人地图传输功能,包括从源机器人下载和向目标机器人上传
- 添加完整的异常处理和验证逻辑,确保机器人和地图存在性检查
- 统一错误消息处理,提高代码可维护性
2026-06-29 09:05:18 +08:00
87d691f927 feat(agv): 扩展AGV控制协议定义,新增18个功能模块
- 添加详细的Protobuf消息定义和注释文档
- 新增基本信息查询、电池状态、位置查询等功能模块
- 增加地图上传下载、控制权管理、运动控制等核心功能
- 实现导航任务、站点管理、任务状态查询等高级特性
- 完善AGV服务接口定义,整合所有AGV相关命令
- 提供详细的API参数说明和使用注意事项
2026-06-25 17:25:34 +08:00
fe68120f71 fix(database): 修复测试日志查询排序问题
- 移除了日志时间降序排列的默认排序规则
- 确保日志按时间升序正确显示
- 解决了日志顺序混乱的显示问题
2026-06-24 13:51:14 +08:00
5ea88ddb99 feat(aima): 更新任务实例和测试日志的数据结构
- 将AimaTestLog实体中的logMessage字段改为logContent,并调整数据库表结构
- 在AimaTaskInstance表中新增create_by、update_time、update_by和remark字段
- 添加AimaTaskInstanceQuery查询DTO类支持多条件查询
- 修改控制器和业务层方法参数类型从AimaTaskInstance到AimaTaskInstanceQuery
- 更新MyBatis映射文件中的参数类型和字段引用
- 在监听器中更新日志记录的字段名和状态设置
- 移除测试日志服务中自动设置创建者和更新者的逻辑
2026-06-24 09:55:51 +08:00
08099e2eda feat(aima): 添加爱玛电动车测试模块功能
- 在pom.xml中添加爱玛电动车测试模块依赖
- 创建爱玛电动车测试模块数据库表结构
- 实现爱玛告警管理相关实体类、控制器、服务类和映射器
- 集成爱玛测试工作流执行监听器
- 完成爱玛告警管理的CRUD操作接口
- 重构TeTaskConfigInfoServiceImpl解决循环依赖问题
2026-06-24 08:47:52 +08:00
4d3375fc5b fix(workflow): 修复工作流取消功能
- 添加了工作流取消的日志记录
- 清空任务实例ID列表后添加了取消工作流的信息日志
- 确保工作流取消操作的可追踪性
2026-06-16 16:14:10 +08:00
b7c1edf68f feat(config): 添加生产环境配置文件并优化巡检任务日志查询
- 新增 application-prodbak.yml 生产环境配置文件
- 配置数据库连接池参数及Druid监控
- 设置 Redis 缓存连接参数
- 添加 MinIO 对象存储配置
- 配置 gRPC 客户端连接信息
- 移除巡检任务日志列表查询中的分页功能
2026-06-16 15:55:16 +08:00
2a47794a13 config(prod): 更新生产环境配置文件中的数据库、Redis、MinIO和Flowise连接信息
- 将主库数据源URL从mysql57容器地址改为192.168.28.10 IP地址
- 更新数据库用户名从cmvr-iot改为root,密码从cmvr-iot123456改为cmvr2468
- 将Redis主机地址从redis容器名改为192.168.28.10 IP地址
- 将MinIO服务地址从minio容器名改为192.168.28.10 IP地址
- 更新Flowise API的start和abort端点的流程ID参数
- 修改测试控制器中的终端ID配置值
2026-06-16 08:35:20 +08:00
d34bafba98 refactor(TeFlowiseController): 优化flowise启动参数构建逻辑
- 移除未使用的taskExecuteNormalVO对象初始化
- 重构runParams参数构建方式,使用更清晰的链式调用
- 确保TeTaskExecuteNormalVO通过builder模式正确构建并调用build()方法
- 保持原有的任务ID和终端ID设置逻辑不变
- 维持现有的任务实例停止功能
2026-06-15 17:20:54 +08:00
b17ade54a1 config(env): 更新开发环境配置文件中的服务器地址和认证信息
- 将数据库主机地址从 192.168.0.100 更新为 192.168.28.10
- 将 Redis 服务器地址从 192.168.0.100 更新为 192.168.28.10
- 将 MinIO 服务器地址从 192.168.0.10 更新为 192.168.28.10
- 将数据库用户名从 cmvr-iot 更新为 root
- 将数据库密码从 cmvr-iot123456 更新为 cmvr2468
- 创建新的备份配置文件 application-devbak.yml 保留原始配置信息
2026-06-15 16:58:35 +08:00
fdd6019a77 feat(robot): 添加机器人地图名称字段并支持任务类型查询
- 在 InspectionRobotMapper.xml 中添加 robotMapName 字段映射
- 修改数据库查询语句以包含 map_source_name 作为 robot_map_name
- 在 InspectionTaskInstanceMapper.xml 中添加任务类型查询条件
- 为 InspectionTaskInstanceQuery DTO 添加 taskType 属性
- 在 TeFlowiseController 中添加流程控制相关依赖注入
- 实现任务实例执行和终止功能,包括运行参数配置和流程控制
- 添加任务实例 ID 列表管理以支持任务启停操作
2026-06-15 16:46:04 +08:00
33507dc475 refactor(inspection): 优化巡检机器人控制器参数配置
- 移除不必要的MediaType导入
- 简化downloadMapFromRobot方法中的参数注解配置
- 保持机器人ID和地图名称参数的API文档注解
- 优化代码结构以提高可读性
2026-06-12 17:29:47 +08:00
9125dbc2b9 feat(inspection): 增加AGV机器人地图管理和状态同步功能
- 在GrpcServiceManager中注册AGV服务stub
- 为巡检地图服务增加按机器人ID查询地图列表功能
- 为巡检机器人服务增加地图列表获取、绑定、上传下载及状态同步功能
- 在数据库表中增加机器人终端ID字段并更新相关映射
- 生成AGV相关的gRPC协议文件和服务类
- 实现机器人与终端配置的自动关联管理
- 添加机器人的实时状态同步包括电量、位置等信息
2026-06-12 17:28:25 +08:00
be8b306eef refactor(inspection): 优化巡检任务日志数据结构
- 将数据库表字段 waypoint_id 和 waypoint_name 替换为 item_id
- 将进度字段从字符串类型改为 DECIMAL(5,2) 类型并保留2位小数
- 在代码中实现进度数值四舍五入保留2位小数的计算逻辑
- 更新实体类 InspectionTaskLog 的字段映射和注释
- 修改 MyBatis 映射文件中的字段引用和 SQL 查询语句
- 调整批量插入操作的数据字段映射关系
2026-06-05 17:09:07 +08:00
43e491c0b6 feat(inspection): 添加巡检任务执行日志功能
- 创建巡检任务执行日志表结构,包含任务实例ID、点位信息、执行状态等字段
- 实现巡检任务执行日志的增删改查接口和业务逻辑
- 在巡检流程执行监听器中集成日志记录功能
- 添加巡检日志类型枚举类用于区分不同类型的日志
- 实现巡检任务执行过程中的实时日志记录和推送
- 提供批量插入日志的功能以提高性能
- 完成日志相关的控制器和数据访问层代码实现
2026-06-05 15:35:14 +08:00
f57c9b9dda refactor(websocket): 重构频道订阅处理器基类实现
- 提取公共基类 BaseChannelSubscribeHandler 处理订阅逻辑
- 将原处理器拆分为带 GRPC 调用和简单订阅两种实现
- 使用依赖注入替代 Autowired 注解
- 统一订阅和取消订阅的处理流程
- 优化日志记录和异常处理机制
2026-06-05 14:57:57 +08:00
05b90f8dc3 refactor(websocket): 移除未使用的导入
- 移除了 WSMessageTypeEnum 的未使用导入
- 优化了 WSMessage 类的代码结构
2026-06-04 17:38:18 +08:00
c1fe2c582d feat(flow): 重构流程执行拦截器并添加节点名称支持
- 移除 AbstractFlowMsgPreInterceptor 中的 beforeIntercept 和 afterIntercept 方法实现
- 调整 FlowExceptionInterceptor、FlowLoggingInterceptor、FlowOutputStoreInterceptor 的执行顺序
- 在 FlowExecutionEvent 中新增 nodeName 字段用于存储节点名称
- 修改 FlowItemExecutor 以传递节点名称到执行消息中
- 更新 FlowMsgPreInterceptor 接口移除 beforeIntercept 和 afterIntercept 方法定义
- 新增 FlowAfterInterceptor 处理节点执行后的事件发布
- 新增 FlowBeforeInterceptor 处理节点执行前的事件发布
- 在巡检任务表中添加 map_id 字段并更新相关实体类和映射文件
- 实现巡检流程执行监听器中的节点执行状态推送功能
2026-06-04 17:37:56 +08:00
4229b07f63 refactor(flow): 重构流程引擎拦截器并优化巡检任务执行
- 在 BaseEntity 中添加 Lombok 注解支持
- 重构 FlowMsgPreInterceptor 接口,增加默认拦截方法和扩展钩子
- 创建 AbstractFlowMsgPreInterceptor 抽象类统一拦截器基础功能
- 实现流程执行事件发布机制,支持节点和任务级别事件
- 集成事件监听器模式用于外部模块监听流程状态变化
- 优化巡检任务实例查询 DTO,支持多状态查询条件
- 完善巡检任务执行状态管理,修复终端锁定检查逻辑
- 实现巡检流程执行监听器,同步任务状态变更
- 优化任务实例创建和执行流程,完善参数传递机制
- 添加流程恢复状态同步功能,确保状态一致性
2026-06-03 17:08:21 +08:00
68b2698614 feat(flow): 集成流程引擎控制巡检任务执行
- 在 GlobalException 中添加对原始异常的支持,实现错误链路追踪
- 为巡检任务和任务实例实体增加任务配置ID和检测任务ID字段
- 更新数据映射文件以同步数据库结构变更
- 在任务实例服务中注入流程控制服务并实现执行、暂停、停止和恢复功能
- 优化任务上下文管理器中的终端ID空值处理逻辑
- 移除终端ID非空校验并调整相关业务逻辑
- 重构任务执行VO类以支持流程引擎调用参数传递
2026-06-02 16:53:07 +08:00
8e4c122f9a feat(inspection): 添加巡检模块功能并更新项目配置
- 添加巡检告警、地图、机器人、任务、点位等实体类和服务接口
- 创建巡检模块数据库表结构,包含机器人、地图、任务等核心表
- 实现巡检告警控制器,提供增删改查和导出功能
- 配置巡检告警数据映射和查询视图
- 更新全局应用配置中的MinIO服务地址
- 在启动类中添加MyBatis扫描配置
- 重构设备配置实体类,移除冗余字段并优化注解配置
2026-06-02 15:01:23 +08:00
065c21b038 config(env): 更新测试环境配置
- 将活动配置文件从 dev 更改为 test
- 更新数据库连接地址从 10.148.28.10 到 192.168.28.10
- 更新 Redis 连接地址从 10.148.28.10 到 192.168.28.10
- 更新 Minio 服务地址从 10.148.28.10 到 192.168.10
2026-05-28 10:16:20 +08:00
218be8e008 config(database): 更新测试环境数据库连接配置 2026-05-27 14:24:32 +08:00
39ea145367 config(database): 更新测试环境数据库连接配置
- 将数据库用户名从 cmvr-iot 更改为 root
- 将数据库密码从 cmvr-iot123456 更改为 cmvr2468
- 保持数据库URL和其他连接参数不变
2026-05-16 16:42:05 +08:00
527b099f2c config(test): 更新测试环境配置文件
- 修改数据库连接地址从 10.148.20.35 到 10.148.121.100
- 添加 Redis 配置,包括主机地址、端口和密码
- 添加热部署功能开关配置
- 更新 Minio 存储服务地址到新 IP
- 添加 API 相关配置,包括应用ID、密钥和工作流URL
- 添加智能体应用配置,包含意图识别和触控坐标等服务
- 添加 Flowise TTS 和 API 配置项
- 统一 YAML 文件的缩进格式
- 保持 gRPC 客户端配置不变
2026-05-16 16:38:26 +08:00
6be70e78c8 feat(build): 添加 Dockerfile 配置支持容器化部署
- 配置 Maven 多模块项目构建环境
- 设置多阶段构建优化镜像大小
- 添加 JAR 包复制和清理逻辑
- 配置 Java 运行时参数和 GC 选项
- 设置开发环境默认配置文件
- 实现可执行 JAR 文件重命名机制
2026-05-16 15:52:56 +08:00
4e5c17d59f Merge remote-tracking branch 'origin/dev' into dev 2026-05-16 13:55:27 +08:00
79da694baf feat(flow): 添加任务执行入口和实例管理功能
- 在 FlowTaskRuntimeEntry 中添加事务注解支持
- 为 FlowTaskRuntimeService 接口添加方法间隔
- 在 TeFlowController 中引入任务实例ID列表管理
- 新增 demo 接口用于任务执行和实例清理
- 实现任务实例的自动停止和清理机制
- 集成 flowControlService 的 stop 功能用于流程控制
2026-05-16 13:55:20 +08:00
3000531ca9 ```
fix(config):修复生产环境配置中的拼写错误修正了 minio服务地址配置中 "mini" 到 "minio" 的拼写错误,
以及 API 密钥末尾多余的空白字符。

这两个配置项都位于 application-prod.yml 文件中,确保了生产环境
的正确连接和安全性。
```
2026-05-12 16:07:07 +08:00
7d7c471b39 ```
feat(agents): 更新生产环境意图识别服务的API密钥

- 将INTENT_RECOGNITION服务的app-id从d3c98sp0gdon0fcf6l00更新为d5dn5ibp9adhq1b34lig- 将INTENT_RECOGNITION服务的app-key从d3c98vp0gdon0fcf6n20更新为d6j5bcellh49on5tasvg
```
2026-05-12 15:49:59 +08:00
3868194932 refactor(flow): 优化流程控制异步处理和安全配置
- 将任务停止操作改为异步执行以提高性能
- 替换系统输出为日志记录以改进调试追踪
- 为安全工具类添加异常处理和匿名用户支持
- 更新安全配置允许流程执行接口匿名访问
- 为流程控制器添加匿名注解支持外部调用
2026-05-11 11:47:46 +08:00
0dbed4006c refactor(llm): 重构AI代理平台服务接口和实现
- 修改LLMAiAgentPlatformService.query方法签名,添加TTS相关参数
- 更新FlowActionExecutorService中的AI代理平台调用逻辑
- 移除FlowHttpNodeHandler中的formData清理逻辑
- 简化FlowNodeParamPreparer中参数准备逻辑
- 更新LLMAiAgentPlatformOperateService参数解析结构
- 修改LlmChatService.chat方法添加TTS配置参数
- 实现TTS播放控制功能,支持配置化语音合成参数
- 更新意图识别和工作流调用服务兼容性适配
2026-04-17 09:10:58 +08:00
37d800be69 feat(flow): 添加AI代理平台支持并优化流式处理
- 在FlowActionExecutorService中集成LLMAiAgentPlatformService和LLMAiTtsService
- 实现AI_AGENT_PLATFORM类型的执行逻辑,支持代理平台查询功能
- 重构FlowHttpNodeHandler参数解析,支持JSON和表单数据格式
- 在FlowItemExecutor中清空起始节点输入定义
- 重写FlowNodeParamPreparer参数准备逻辑,支持嵌套参数处理
- 优化LlmChatService中的SSE流处理,实现句子级别异步处理
- 添加TTS语音播放功能,在流式响应中实时处理句子
- 修改评估服务调用方式,在FlowEndNodeHandler中注释掉原有逻辑
2026-04-15 11:50:01 +08:00
722e8a6ba7 Merge branch 'dev2' into dev
# Conflicts:
#	cmvr-iot-test/src/main/java/com/cmvr/test/flow/control/FlowControlService.java
2026-04-09 17:33:11 +08:00
d8684a7736 refactor(flow): 优化暂停任务逻辑以异步停止边缘端
- 将边缘端停止操作改为异步执行避免阻塞主线程
- 添加日志记录开始暂停操作
- 保持原有的暂停状态设置和时间戳更新逻辑
- 继续使用统一的日志记录和状态同步机制
2026-04-09 14:42:14 +08:00
stream
8065faebcb fix: 触控交互 2026-03-24 10:59:50 +08:00
309 changed files with 107813 additions and 16835 deletions

50
Dockerfile Normal file
View File

@ -0,0 +1,50 @@
# ====================== 构建阶段 ======================
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"]

249
README.md Normal file
View File

@ -0,0 +1,249 @@
# cmvr-iot
cmvr-iot 是一个基于 Java 8 和 Spring Boot 2.5 的多模块后端工程提供设备管理、边缘机器人控制、测试编排、巡检、TTS、AIMA、VI/TI 等业务能力。工程入口在 `cmvr-iot-admin`,其余模块按业务和基础能力拆分。
## 技术栈
- 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
## 模块说明
| 模块 | 说明 |
| --- | --- |
| `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 |
## 目录结构
```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
- Redis
- 可访问的 gRPC 机器人/边缘终端服务
- 可选MinIO、外部大模型/TTS/评测服务
注意:`application-*.yml` 中包含环境相关地址、账号和密钥配置。实际部署或本地开发时,应使用本机配置、环境变量或配置中心覆盖,不要直接复用生产/测试环境敏感配置。
## 配置文件
主配置文件位于:
```text
cmvr-iot-admin/src/main/resources/application.yml
```
环境配置文件位于:
```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
```
只编译启动模块及其依赖:
```bash
mvn -pl cmvr-iot-admin -am compile -DskipTests
```
只重新生成并编译 gRPC 代码:
```bash
mvn -pl cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib -am compile -DskipTests
```
## 启动
启动类:
```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
```

View File

@ -77,6 +77,24 @@
<groupId>com.cmvr</groupId> <groupId>com.cmvr</groupId>
<artifactId>cmvr-iot-evaluation</artifactId> <artifactId>cmvr-iot-evaluation</artifactId>
</dependency> </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>
</dependencies> </dependencies>
<properties> <properties>
<env>dev</env> <env>dev</env>

View File

@ -1,5 +1,6 @@
package com.cmvr; package com.cmvr;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@ -11,6 +12,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
* @author cmvr-iot * @author cmvr-iot
*/ */
@EnableScheduling @EnableScheduling
@MapperScan("com.cmvr.**.mapper")
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class }) @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
public class CmvrIotApplication public class CmvrIotApplication
{ {

View File

@ -0,0 +1,75 @@
package com.cmvr.web.controller.aima;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.cmvr.common.annotation.Log;
import com.cmvr.common.core.controller.BaseController;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.common.enums.BusinessType;
import com.cmvr.aima.domain.AimaAlarm;
import com.cmvr.aima.service.IAimaAlarmService;
import com.cmvr.common.utils.poi.ExcelUtil;
import com.cmvr.common.core.page.TableDataInfo;
@RestController
@RequestMapping("/aima/alarm")
@Api(tags = "爱玛电动车--告警管理")
public class AimaAlarmController extends BaseController {
@Autowired
private IAimaAlarmService aimaAlarmService;
@ApiOperation("查询告警列表")
@PreAuthorize("@ss.hasPermi('aima:alarm:list')")
@GetMapping("/list")
public TableDataInfo list(AimaAlarm aimaAlarm) {
startPage();
List<AimaAlarm> list = aimaAlarmService.selectAimaAlarmList(aimaAlarm);
return getDataTable(list);
}
@ApiOperation("导出告警列表")
@PreAuthorize("@ss.hasPermi('aima:alarm:export')")
@Log(title = "告警管理", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, AimaAlarm aimaAlarm) {
List<AimaAlarm> list = aimaAlarmService.selectAimaAlarmList(aimaAlarm);
ExcelUtil<AimaAlarm> util = new ExcelUtil<>(AimaAlarm.class);
util.exportExcel(response, list, "告警数据");
}
@ApiOperation("获取告警详细信息")
@PreAuthorize("@ss.hasPermi('aima:alarm:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") String id) {
return success(aimaAlarmService.selectAimaAlarmById(id));
}
@ApiOperation("新增告警")
@PreAuthorize("@ss.hasPermi('aima:alarm:add')")
@Log(title = "告警管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody AimaAlarm aimaAlarm) {
return toAjax(aimaAlarmService.insertAimaAlarm(aimaAlarm));
}
@ApiOperation("修改告警")
@PreAuthorize("@ss.hasPermi('aima:alarm:edit')")
@Log(title = "告警管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody AimaAlarm aimaAlarm) {
return toAjax(aimaAlarmService.updateAimaAlarm(aimaAlarm));
}
@ApiOperation("删除告警")
@PreAuthorize("@ss.hasPermi('aima:alarm:remove')")
@Log(title = "告警管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable String[] ids) {
return toAjax(aimaAlarmService.deleteAimaAlarmByIds(ids));
}
}

View File

@ -0,0 +1,113 @@
package com.cmvr.web.controller.aima;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.annotation.Log;
import com.cmvr.common.core.controller.BaseController;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.common.enums.BusinessType;
import com.cmvr.aima.domain.AimaPhone;
import com.cmvr.aima.service.IAimaPhoneService;
import com.cmvr.common.utils.poi.ExcelUtil;
import com.cmvr.common.core.page.TableDataInfo;
/**
* 爱玛手机管理Controller
*
* @author cmvr-iot
* @since 2026-06-22
*/
@RestController
@RequestMapping("/aima/phone")
@Api(tags = "爱玛电动车--手机管理")
public class AimaPhoneController extends BaseController
{
@Autowired
private IAimaPhoneService aimaPhoneService;
/**
* 查询爱玛手机管理列表
*/
@ApiOperation("查询爱玛手机管理列表")
@PreAuthorize("@ss.hasPermi('aima:phone:list')")
@GetMapping("/list")
public TableDataInfo list(AimaPhone aimaPhone)
{
startPage();
List<AimaPhone> list = aimaPhoneService.selectAimaPhoneList(aimaPhone);
return getDataTable(list);
}
/**
* 导出爱玛手机管理列表
*/
@ApiOperation("导出爱玛手机管理列表")
@PreAuthorize("@ss.hasPermi('aima:phone:export')")
@Log(title = "爱玛手机管理", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, AimaPhone aimaPhone)
{
List<AimaPhone> list = aimaPhoneService.selectAimaPhoneList(aimaPhone);
ExcelUtil<AimaPhone> util = new ExcelUtil<>(AimaPhone.class);
util.exportExcel(response, list, "爱玛手机管理数据");
}
/**
* 获取爱玛手机管理详细信息
*/
@ApiOperation("获取爱玛手机管理详细信息")
@PreAuthorize("@ss.hasPermi('aima:phone:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") String id)
{
return success(aimaPhoneService.selectAimaPhoneById(id));
}
/**
* 新增爱玛手机管理
*/
@ApiOperation("新增爱玛手机管理")
@PreAuthorize("@ss.hasPermi('aima:phone:add')")
@Log(title = "爱玛手机管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody AimaPhone aimaPhone)
{
return toAjax(aimaPhoneService.insertAimaPhone(aimaPhone));
}
/**
* 修改爱玛手机管理
*/
@ApiOperation("修改爱玛手机管理")
@PreAuthorize("@ss.hasPermi('aima:phone:edit')")
@Log(title = "爱玛手机管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody AimaPhone aimaPhone)
{
return toAjax(aimaPhoneService.updateAimaPhone(aimaPhone));
}
/**
* 删除爱玛手机管理
*/
@ApiOperation("删除爱玛手机管理")
@PreAuthorize("@ss.hasPermi('aima:phone:remove')")
@Log(title = "爱玛手机管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable String[] ids)
{
return toAjax(aimaPhoneService.deleteAimaPhoneByIds(ids));
}
}

View File

@ -0,0 +1,98 @@
package com.cmvr.web.controller.aima;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.cmvr.common.annotation.Log;
import com.cmvr.common.core.controller.BaseController;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.common.enums.BusinessType;
import com.cmvr.aima.domain.AimaTask;
import com.cmvr.aima.domain.vo.AimaTaskVo;
import com.cmvr.aima.service.IAimaTaskService;
import com.cmvr.common.utils.poi.ExcelUtil;
import com.cmvr.common.core.page.TableDataInfo;
@RestController
@RequestMapping("/aima/task")
@Api(tags = "爱玛电动车--任务管理")
public class AimaTaskController extends BaseController {
@Autowired
private IAimaTaskService aimaTaskService;
@ApiOperation("查询任务列表")
@PreAuthorize("@ss.hasPermi('aima:task:list')")
@GetMapping("/list")
public TableDataInfo list(AimaTask aimaTask) {
startPage();
List<AimaTaskVo> list = aimaTaskService.selectAimaTaskVoList(aimaTask);
return getDataTable(list);
}
@ApiOperation("导出任务列表")
@PreAuthorize("@ss.hasPermi('aima:task:export')")
@Log(title = "任务管理", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, AimaTask aimaTask) {
List<AimaTaskVo> list = aimaTaskService.selectAimaTaskVoList(aimaTask);
ExcelUtil<AimaTaskVo> util = new ExcelUtil<>(AimaTaskVo.class);
util.exportExcel(response, list, "任务数据");
}
@ApiOperation("获取任务详细信息")
@PreAuthorize("@ss.hasPermi('aima:task:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") String id) {
return success(aimaTaskService.selectAimaTaskById(id));
}
@ApiOperation("新增任务")
@PreAuthorize("@ss.hasPermi('aima:task:add')")
@Log(title = "任务管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody AimaTask aimaTask) {
return toAjax(aimaTaskService.insertAimaTask(aimaTask));
}
@ApiOperation("修改任务")
@PreAuthorize("@ss.hasPermi('aima:task:edit')")
@Log(title = "任务管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody AimaTask aimaTask) {
return toAjax(aimaTaskService.updateAimaTask(aimaTask));
}
@ApiOperation("删除任务")
@PreAuthorize("@ss.hasPermi('aima:task:remove')")
@Log(title = "任务管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable String[] ids) {
return toAjax(aimaTaskService.deleteAimaTaskByIds(ids));
}
/**
* 绑定测试用例到任务
*/
@ApiOperation("绑定测试用例")
@PreAuthorize("@ss.hasPermi('aima:task:edit')")
@Log(title = "任务管理", businessType = BusinessType.UPDATE)
@PostMapping("/bindTestCases/{taskId}")
public AjaxResult bindTestCases(@PathVariable("taskId") String taskId, @RequestBody List<String> testCaseIds) {
aimaTaskService.bindTestCases(taskId, testCaseIds);
return success();
}
/**
* 获取任务的测试用例列表
*/
@ApiOperation("获取任务测试用例")
@PreAuthorize("@ss.hasPermi('aima:task:query')")
@GetMapping("/testCases/{taskId}")
public AjaxResult getTestCases(@PathVariable String taskId) {
return success(aimaTaskService.getTestCaseIds(taskId));
}
}

View File

@ -0,0 +1,121 @@
package com.cmvr.web.controller.aima;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.cmvr.common.annotation.Log;
import com.cmvr.common.core.controller.BaseController;
import com.cmvr.common.core.domain.AjaxResult;
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.service.IAimaTaskInstanceService;
import com.cmvr.common.utils.poi.ExcelUtil;
import com.cmvr.common.core.page.TableDataInfo;
@RestController
@RequestMapping("/aima/taskinstance")
@Api(tags = "爱玛电动车--任务执行实例")
public class AimaTaskInstanceController extends BaseController {
@Autowired
private IAimaTaskInstanceService aimaTaskInstanceService;
@ApiOperation("查询任务实例列表")
@PreAuthorize("@ss.hasPermi('aima:taskinstance:list')")
@GetMapping("/list")
public TableDataInfo list(AimaTaskInstanceQuery query) {
startPage();
List<AimaTaskInstanceVo> list = aimaTaskInstanceService.selectAimaTaskInstanceVoList(query);
return getDataTable(list);
}
@ApiOperation("导出任务实例列表")
@PreAuthorize("@ss.hasPermi('aima:taskinstance:export')")
@Log(title = "任务执行实例", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, AimaTaskInstanceQuery query) {
List<AimaTaskInstanceVo> list = aimaTaskInstanceService.selectAimaTaskInstanceVoList(query);
ExcelUtil<AimaTaskInstanceVo> util = new ExcelUtil<>(AimaTaskInstanceVo.class);
util.exportExcel(response, list, "任务实例数据");
}
@ApiOperation("获取任务实例详细信息")
@PreAuthorize("@ss.hasPermi('aima:taskinstance:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") String id) {
return success(aimaTaskInstanceService.selectAimaTaskInstanceById(id));
}
@ApiOperation("新增任务实例")
@PreAuthorize("@ss.hasPermi('aima:taskinstance:add')")
@Log(title = "任务执行实例", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody AimaTaskInstance aimaTaskInstance) {
return toAjax(aimaTaskInstanceService.insertAimaTaskInstance(aimaTaskInstance));
}
@ApiOperation("修改任务实例")
@PreAuthorize("@ss.hasPermi('aima:taskinstance:edit')")
@Log(title = "任务执行实例", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody AimaTaskInstance aimaTaskInstance) {
return toAjax(aimaTaskInstanceService.updateAimaTaskInstance(aimaTaskInstance));
}
@ApiOperation("删除任务实例")
@PreAuthorize("@ss.hasPermi('aima:taskinstance:remove')")
@Log(title = "任务执行实例", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable String[] ids) {
return toAjax(aimaTaskInstanceService.deleteAimaTaskInstanceByIds(ids));
}
/**
* 开始执行任务实例
*/
@ApiOperation("开始执行任务实例")
@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));
}
/**
* 暂停任务实例
*/
@ApiOperation("暂停任务实例")
@PreAuthorize("@ss.hasPermi('aima:taskinstance:edit')")
@Log(title = "任务执行实例", businessType = BusinessType.UPDATE)
@PutMapping("/pause/{id}")
public AjaxResult pause(@PathVariable("id") String id) {
return toAjax(aimaTaskInstanceService.pauseInstance(id));
}
/**
* 终止任务实例
*/
@ApiOperation("终止任务实例")
@PreAuthorize("@ss.hasPermi('aima:taskinstance:edit')")
@Log(title = "任务执行实例", businessType = BusinessType.UPDATE)
@PutMapping("/stop/{id}")
public AjaxResult stop(@PathVariable("id") String id) {
return toAjax(aimaTaskInstanceService.stopInstance(id));
}
/**
* 恢复任务实例
*/
@ApiOperation("恢复任务实例")
@PreAuthorize("@ss.hasPermi('aima:taskinstance:edit')")
@Log(title = "任务执行实例", businessType = BusinessType.UPDATE)
@PutMapping("/resume/{id}")
public AjaxResult resume(@PathVariable("id") String id) {
return toAjax(aimaTaskInstanceService.resumeInstance(id));
}
}

View File

@ -0,0 +1,76 @@
package com.cmvr.web.controller.aima;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.cmvr.common.annotation.Log;
import com.cmvr.common.core.controller.BaseController;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.common.enums.BusinessType;
import com.cmvr.aima.domain.AimaTestCase;
import com.cmvr.aima.domain.vo.AimaTestCaseVo;
import com.cmvr.aima.service.IAimaTestCaseService;
import com.cmvr.common.utils.poi.ExcelUtil;
import com.cmvr.common.core.page.TableDataInfo;
@RestController
@RequestMapping("/aima/testcase")
@Api(tags = "爱玛电动车--测试用例管理")
public class AimaTestCaseController extends BaseController {
@Autowired
private IAimaTestCaseService aimaTestCaseService;
@ApiOperation("查询测试用例列表")
@PreAuthorize("@ss.hasPermi('aima:testcase:list')")
@GetMapping("/list")
public TableDataInfo list(AimaTestCase aimaTestCase) {
startPage();
List<AimaTestCaseVo> list = aimaTestCaseService.selectAimaTestCaseVoList(aimaTestCase);
return getDataTable(list);
}
@ApiOperation("导出测试用例列表")
@PreAuthorize("@ss.hasPermi('aima:testcase:export')")
@Log(title = "测试用例管理", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, AimaTestCase aimaTestCase) {
List<AimaTestCaseVo> list = aimaTestCaseService.selectAimaTestCaseVoList(aimaTestCase);
ExcelUtil<AimaTestCaseVo> util = new ExcelUtil<>(AimaTestCaseVo.class);
util.exportExcel(response, list, "测试用例数据");
}
@ApiOperation("获取测试用例详细信息")
@PreAuthorize("@ss.hasPermi('aima:testcase:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") String id) {
return success(aimaTestCaseService.selectAimaTestCaseById(id));
}
@ApiOperation("新增测试用例")
@PreAuthorize("@ss.hasPermi('aima:testcase:add')")
@Log(title = "测试用例管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody AimaTestCase aimaTestCase) {
return toAjax(aimaTestCaseService.insertAimaTestCase(aimaTestCase));
}
@ApiOperation("修改测试用例")
@PreAuthorize("@ss.hasPermi('aima:testcase:edit')")
@Log(title = "测试用例管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody AimaTestCase aimaTestCase) {
return toAjax(aimaTestCaseService.updateAimaTestCase(aimaTestCase));
}
@ApiOperation("删除测试用例")
@PreAuthorize("@ss.hasPermi('aima:testcase:remove')")
@Log(title = "测试用例管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable String[] ids) {
return toAjax(aimaTestCaseService.deleteAimaTestCaseByIds(ids));
}
}

View File

@ -0,0 +1,75 @@
package com.cmvr.web.controller.aima;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.cmvr.common.annotation.Log;
import com.cmvr.common.core.controller.BaseController;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.common.enums.BusinessType;
import com.cmvr.aima.domain.AimaTestLog;
import com.cmvr.aima.service.IAimaTestLogService;
import com.cmvr.common.utils.poi.ExcelUtil;
import com.cmvr.common.core.page.TableDataInfo;
@RestController
@RequestMapping("/aima/testlog")
@Api(tags = "爱玛电动车--测试日志管理")
public class AimaTestLogController extends BaseController {
@Autowired
private IAimaTestLogService aimaTestLogService;
@ApiOperation("查询测试日志列表")
@PreAuthorize("@ss.hasPermi('aima:testlog:list')")
@GetMapping("/list")
public TableDataInfo list(AimaTestLog aimaTestLog) {
startPage();
List<AimaTestLog> list = aimaTestLogService.selectAimaTestLogList(aimaTestLog);
return getDataTable(list);
}
@ApiOperation("导出测试日志列表")
@PreAuthorize("@ss.hasPermi('aima:testlog:export')")
@Log(title = "测试日志管理", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, AimaTestLog aimaTestLog) {
List<AimaTestLog> list = aimaTestLogService.selectAimaTestLogList(aimaTestLog);
ExcelUtil<AimaTestLog> util = new ExcelUtil<>(AimaTestLog.class);
util.exportExcel(response, list, "测试日志数据");
}
@ApiOperation("获取测试日志详细信息")
@PreAuthorize("@ss.hasPermi('aima:testlog:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") String id) {
return success(aimaTestLogService.selectAimaTestLogById(id));
}
@ApiOperation("新增测试日志")
@PreAuthorize("@ss.hasPermi('aima:testlog:add')")
@Log(title = "测试日志管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody AimaTestLog aimaTestLog) {
return toAjax(aimaTestLogService.insertAimaTestLog(aimaTestLog));
}
@ApiOperation("修改测试日志")
@PreAuthorize("@ss.hasPermi('aima:testlog:edit')")
@Log(title = "测试日志管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody AimaTestLog aimaTestLog) {
return toAjax(aimaTestLogService.updateAimaTestLog(aimaTestLog));
}
@ApiOperation("删除测试日志")
@PreAuthorize("@ss.hasPermi('aima:testlog:remove')")
@Log(title = "测试日志管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable String[] ids) {
return toAjax(aimaTestLogService.deleteAimaTestLogByIds(ids));
}
}

View File

@ -0,0 +1,75 @@
package com.cmvr.web.controller.aima;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.cmvr.common.annotation.Log;
import com.cmvr.common.core.controller.BaseController;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.common.enums.BusinessType;
import com.cmvr.aima.domain.AimaVehicle;
import com.cmvr.aima.service.IAimaVehicleService;
import com.cmvr.common.utils.poi.ExcelUtil;
import com.cmvr.common.core.page.TableDataInfo;
@RestController
@RequestMapping("/aima/vehicle")
@Api(tags = "爱玛电动车--车辆管理")
public class AimaVehicleController extends BaseController {
@Autowired
private IAimaVehicleService aimaVehicleService;
@ApiOperation("查询车辆列表")
@PreAuthorize("@ss.hasPermi('aima:vehicle:list')")
@GetMapping("/list")
public TableDataInfo list(AimaVehicle aimaVehicle) {
startPage();
List<AimaVehicle> list = aimaVehicleService.selectAimaVehicleList(aimaVehicle);
return getDataTable(list);
}
@ApiOperation("导出车辆列表")
@PreAuthorize("@ss.hasPermi('aima:vehicle:export')")
@Log(title = "车辆管理", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, AimaVehicle aimaVehicle) {
List<AimaVehicle> list = aimaVehicleService.selectAimaVehicleList(aimaVehicle);
ExcelUtil<AimaVehicle> util = new ExcelUtil<>(AimaVehicle.class);
util.exportExcel(response, list, "车辆数据");
}
@ApiOperation("获取车辆详细信息")
@PreAuthorize("@ss.hasPermi('aima:vehicle:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") String id) {
return success(aimaVehicleService.selectAimaVehicleById(id));
}
@ApiOperation("新增车辆")
@PreAuthorize("@ss.hasPermi('aima:vehicle:add')")
@Log(title = "车辆管理", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody AimaVehicle aimaVehicle) {
return toAjax(aimaVehicleService.insertAimaVehicle(aimaVehicle));
}
@ApiOperation("修改车辆")
@PreAuthorize("@ss.hasPermi('aima:vehicle:edit')")
@Log(title = "车辆管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody AimaVehicle aimaVehicle) {
return toAjax(aimaVehicleService.updateAimaVehicle(aimaVehicle));
}
@ApiOperation("删除车辆")
@PreAuthorize("@ss.hasPermi('aima:vehicle:remove')")
@Log(title = "车辆管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable String[] ids) {
return toAjax(aimaVehicleService.deleteAimaVehicleByIds(ids));
}
}

View File

@ -0,0 +1,315 @@
package com.cmvr.web.controller.api;
import cmvr.msgs.Agv;
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.EdgeAgvStartMappingVO;
import com.cmvr.edge.client.model.agv.EdgeAgvUploadMapVO;
import com.cmvr.edge.client.model.agv.EdgeAgvVelocityVO;
import com.cmvr.edge.client.service.EdgeAgvService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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.RestController;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 边缘系统 AGV 控制器
*
* <p>接口入参约定
* GET 查询类接口使用 query 参数绑定 VOPOST 控制类接口只接收一个 JSON 请求体 VO
* 避免一个接口同时出现通用 VO 和零散业务参数保证前端调用Swagger 文档和后端绑定规则一致</p>
*
* @author cmvr-iot
* @since 2026-07-02
*/
@Api(tags = "边缘--AGV")
@RestController
@RequestMapping("/api/agv")
@RequiredArgsConstructor
public class EdgeAgvController {
private final EdgeAgvService edgeAgvService;
/**
* 获取 AGV 运行时状态
*/
@ApiOperation("获取AGV运行时状态")
@GetMapping("/getRuntimeState")
public AjaxResult getRuntimeState(@Validated EdgeCommonVO vo) {
Agv.AgvRuntimeState state = edgeAgvService.getRuntimeState(vo);
return AjaxResult.ok(toRuntimeStateMap(state));
}
/**
* 获取 AGV 当前导航状态
*/
@ApiOperation("获取导航状态")
@GetMapping("/getNavigationStatus")
public AjaxResult getNavigationStatus(@Validated EdgeCommonVO vo) {
Agv.AgvNavigationStatus status = edgeAgvService.getNavigationStatus(vo);
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("state", status.getState());
resultMap.put("type", status.getType());
resultMap.put("progress", status.getProgress());
resultMap.put("message", status.getMessage());
return AjaxResult.ok(resultMap);
}
/**
* AGV 紧急停止
*/
@ApiOperation("紧急停止")
@PostMapping("/emergencyStop")
public AjaxResult emergencyStop(@RequestBody @Validated EdgeCommonVO vo) {
edgeAgvService.emergencyStop(vo);
return AjaxResult.ok();
}
/**
* 清除 AGV 故障
*/
@ApiOperation("清除故障")
@PostMapping("/clearFault")
public AjaxResult clearFault(@RequestBody @Validated EdgeCommonVO vo) {
edgeAgvService.clearFault(vo);
return AjaxResult.ok();
}
/**
* 导航到指定坐标
*/
@ApiOperation("导航到指定位置")
@PostMapping("/navigateToPose")
public AjaxResult navigateToPose(@RequestBody @Validated EdgeAgvNavigateToPoseVO vo) {
Agv.AgvPose2d pose = Agv.AgvPose2d.newBuilder()
.setX(vo.getX())
.setY(vo.getY())
.setTheta(vo.getTheta())
.build();
edgeAgvService.navigateToPose(vo, pose);
return AjaxResult.ok();
}
/**
* 导航到指定站点
*/
@ApiOperation("导航到站点")
@PostMapping("/navigateToStation")
public AjaxResult navigateToStation(@RequestBody @Validated EdgeAgvNavigateToStationVO vo) {
edgeAgvService.navigateToStation(vo, vo.getStationId());
return AjaxResult.ok();
}
/**
* 暂停当前导航任务
*/
@ApiOperation("暂停导航")
@PostMapping("/pauseNavigation")
public AjaxResult pauseNavigation(@RequestBody @Validated EdgeCommonVO vo) {
edgeAgvService.pauseNavigation(vo);
return AjaxResult.ok();
}
/**
* 恢复当前导航任务
*/
@ApiOperation("恢复导航")
@PostMapping("/resumeNavigation")
public AjaxResult resumeNavigation(@RequestBody @Validated EdgeCommonVO vo) {
edgeAgvService.resumeNavigation(vo);
return AjaxResult.ok();
}
/**
* 取消当前导航任务
*/
@ApiOperation("取消导航")
@PostMapping("/cancelNavigation")
public AjaxResult cancelNavigation(@RequestBody @Validated EdgeCommonVO vo) {
edgeAgvService.cancelNavigation(vo);
return AjaxResult.ok();
}
/**
* 设置 AGV 速度
*/
@ApiOperation("设置速度")
@PostMapping("/setVelocity")
public AjaxResult setVelocity(@RequestBody @Validated EdgeAgvVelocityVO vo) {
Agv.AgvVelocity velocity = Agv.AgvVelocity.newBuilder()
.setVx(vo.getVx())
.setVy(vo.getVy())
.setWz(vo.getWz())
.build();
edgeAgvService.setVelocity(vo, velocity);
return AjaxResult.ok();
}
/**
* 停止速度控制
*/
@ApiOperation("停止速度控制")
@PostMapping("/stopVelocityControl")
public AjaxResult stopVelocityControl(@RequestBody @Validated EdgeCommonVO vo) {
edgeAgvService.stopVelocityControl(vo);
return AjaxResult.ok();
}
/**
* 查询机器人本地所有地图
*/
@ApiOperation("列出所有地图")
@GetMapping("/listMaps")
public AjaxResult listMaps(@Validated EdgeCommonVO vo) {
List<String> maps = edgeAgvService.listMaps(vo);
return AjaxResult.ok(maps);
}
/**
* 查询机器人本地所有站点
*/
@ApiOperation("列出所有站点")
@GetMapping("/listStations")
public AjaxResult listStations(@Validated EdgeCommonVO vo) {
List<Agv.AgvStation> stations = edgeAgvService.listStations(vo);
List<Map<String, Object>> stationList = new ArrayList<>();
for (Agv.AgvStation station : stations) {
stationList.add(toStationMap(station));
}
return AjaxResult.ok(stationList);
}
/**
* 切换机器人当前地图
*/
@ApiOperation("切换地图")
@PostMapping("/switchMap")
public AjaxResult switchMap(@RequestBody @Validated EdgeAgvMapNameVO vo) {
edgeAgvService.switchMap(vo, vo.getMapName());
return AjaxResult.ok();
}
/**
* 上传地图内容到机器人
*/
@ApiOperation("上传地图")
@PostMapping("/uploadMap")
public AjaxResult uploadMap(@RequestBody @Validated EdgeAgvUploadMapVO vo) {
edgeAgvService.uploadMap(vo, vo.getMapName(), vo.getContent());
return AjaxResult.ok();
}
/**
* 下载指定地图内容
*/
@ApiOperation("下载地图")
@GetMapping("/downloadMap")
public AjaxResult downloadMap(@Validated EdgeAgvMapNameVO vo) {
String content = edgeAgvService.downloadMap(vo, vo.getMapName());
return AjaxResult.ok(content);
}
/**
* 开始建图
*/
@ApiOperation("开始建图")
@PostMapping("/startMapping")
public AjaxResult startMapping(@RequestBody @Validated EdgeAgvStartMappingVO vo) {
Agv.AgvMapDimension mapDimension = Agv.AgvMapDimension.forNumber(vo.getDimension());
if (mapDimension == null) {
return AjaxResult.error("建图维度不合法允许值0=未指定1=2D2=3D3=2D+3D");
}
String sessionId = edgeAgvService.startMapping(vo, mapDimension, vo.getMapName(), vo.isRealTime());
Map<String, Object> result = new HashMap<>();
result.put("sessionId", sessionId);
return AjaxResult.ok(result);
}
/**
* 停止建图
*/
@ApiOperation("停止建图")
@PostMapping("/stopMapping")
public AjaxResult stopMapping(@RequestBody @Validated EdgeCommonVO vo) {
edgeAgvService.stopMapping(vo);
return AjaxResult.ok();
}
/**
* protobuf 运行时状态转换为前端友好的 Map避免直接暴露 protobuf 对象结构
*/
private Map<String, Object> toRuntimeStateMap(Agv.AgvRuntimeState state) {
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("timestamp", state.getTimestamp());
resultMap.put("mode", state.getMode());
resultMap.put("connected", state.getConnected());
resultMap.put("localized", state.getLocalized());
resultMap.put("moving", state.getMoving());
resultMap.put("fault", state.getFault());
resultMap.put("emergencyStopped", state.getEmergencyStopped());
if (state.hasPose()) {
resultMap.put("pose", toPoseMap(state.getPose()));
}
if (state.hasVelocity()) {
Map<String, Object> velocityMap = new HashMap<>();
velocityMap.put("vx", state.getVelocity().getVx());
velocityMap.put("vy", state.getVelocity().getVy());
velocityMap.put("wz", state.getVelocity().getWz());
resultMap.put("velocity", velocityMap);
}
if (state.hasBattery()) {
Map<String, Object> batteryMap = new HashMap<>();
batteryMap.put("percentage", state.getBattery().getPercentage());
batteryMap.put("voltage", state.getBattery().getVoltage());
batteryMap.put("current", state.getBattery().getCurrent());
batteryMap.put("temperature", state.getBattery().getTemperature());
batteryMap.put("charging", state.getBattery().getCharging());
resultMap.put("battery", batteryMap);
}
resultMap.put("currentMap", state.getCurrentMap());
resultMap.put("currentStation", state.getCurrentStation());
resultMap.put("lastError", state.getLastError());
return resultMap;
}
/**
* protobuf 站点对象转换为普通 Map
*/
private Map<String, Object> toStationMap(Agv.AgvStation station) {
Map<String, Object> stationMap = new HashMap<>();
stationMap.put("id", station.getId());
stationMap.put("type", station.getType());
stationMap.put("description", station.getDescription());
if (station.hasPose()) {
stationMap.put("pose", toPoseMap(station.getPose()));
}
return stationMap;
}
/**
* protobuf 二维位姿转换为普通 Map
*/
private Map<String, Object> toPoseMap(Agv.AgvPose2d pose) {
Map<String, Object> poseMap = new HashMap<>();
poseMap.put("x", pose.getX());
poseMap.put("y", pose.getY());
poseMap.put("theta", pose.getTheta());
return poseMap;
}
}

View File

@ -0,0 +1,232 @@
package com.cmvr.web.controller.api;
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.service.EdgeArmService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
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.RestController;
/**
* 边缘系统机械臂控制器
*
* @author cmvr-iot
* @since 2026-07-01
*/
@Api(tags = "边缘--机械臂")
@RestController
@RequestMapping("/api/arm")
@RequiredArgsConstructor
public class EdgeArmController {
private final EdgeArmService edgeArmService;
@ApiOperation("关闭力矩")
@GetMapping("/torqueOff")
public AjaxResult torqueOff(EdgeCommonVO vo) {
edgeArmService.torqueOff(vo);
return AjaxResult.ok();
}
@ApiOperation("开启力矩")
@GetMapping("/torqueOn")
public AjaxResult torqueOn(EdgeCommonVO vo) {
edgeArmService.torqueOn(vo);
return AjaxResult.ok();
}
/**
* 清除机械臂故障状态
*/
@ApiOperation("清除故障")
@GetMapping("/clearFault")
public AjaxResult clearFault(EdgeCommonVO vo) {
edgeArmService.clearFault(vo);
return AjaxResult.ok();
}
@ApiOperation("关节空间运动")
@PostMapping("/moveJ")
public AjaxResult moveJ(@RequestBody EdgeArmMoveJVO moveJVO) {
edgeArmService.moveJ(
moveJVO,
moveJVO.getTarget(),
moveJVO.getVelocity(),
moveJVO.getAcceleration(),
moveJVO.getBlendRadius(),
moveJVO.getJointVelocityLimits(),
moveJVO.getAsynchronous()
);
return AjaxResult.ok();
}
@ApiOperation("笛卡尔空间直线运动")
@PostMapping("/moveL")
public AjaxResult moveL(@RequestBody EdgeArmMoveLVO moveLVO) {
edgeArmService.moveL(
moveLVO,
moveLVO.getX(),
moveLVO.getY(),
moveLVO.getZ(),
moveLVO.getRx(),
moveLVO.getRy(),
moveLVO.getRz(),
moveLVO.getFrame(),
moveLVO.getVelocity(),
moveLVO.getAcceleration(),
moveLVO.getBlendRadius()
);
return AjaxResult.ok();
}
@ApiOperation("关节速度控制")
@PostMapping("/speedJ")
public AjaxResult speedJ(@RequestBody EdgeArmSpeedJVO speedJVO) {
edgeArmService.speedJ(
speedJVO,
speedJVO.getVelocities(),
speedJVO.getAcceleration(),
speedJVO.getDuration()
);
return AjaxResult.ok();
}
@ApiOperation("笛卡尔速度控制")
@PostMapping("/speedL")
public AjaxResult speedL(@RequestBody EdgeArmSpeedLVO speedLVO) {
edgeArmService.speedL(
speedLVO,
speedLVO.getVx(),
speedLVO.getVy(),
speedLVO.getVz(),
speedLVO.getWx(),
speedLVO.getWy(),
speedLVO.getWz(),
speedLVO.getFrame(),
speedLVO.getAcceleration(),
speedLVO.getDuration()
);
return AjaxResult.ok();
}
@ApiOperation("伺服关节控制")
@PostMapping("/servoJ")
public AjaxResult servoJ(@RequestBody EdgeArmServoJVO servoJVO) {
edgeArmService.servoJ(servoJVO, servoJVO.getTarget());
return AjaxResult.ok();
}
@ApiOperation("停止运动")
@GetMapping("/stopMotion")
public AjaxResult stopMotion(EdgeCommonVO vo) {
edgeArmService.stopMotion(vo);
return AjaxResult.ok();
}
@ApiOperation("获取关节状态")
@GetMapping("/getJointState")
public AjaxResult getJointState(EdgeCommonVO vo) {
ArmCommand.JointResponse result = edgeArmService.getJointState(vo);
ArmCommand.JointState state = result.getState();
// 转换为VO
EdgeArmJointStateVO jointStateVO = new EdgeArmJointStateVO();
jointStateVO.setName(state.getNameList());
jointStateVO.setPosition(state.getPositionList());
jointStateVO.setVelocity(state.getVelocityList());
jointStateVO.setEffort(state.getEffortList());
jointStateVO.setTimestamp(state.getTimestamp());
return AjaxResult.ok(jointStateVO);
}
@ApiOperation("获取末端位姿")
@GetMapping("/getPose")
public AjaxResult getPose(@Validated EdgeArmPoseQueryVO vo) {
ArmCommand.GetPose.Response result = edgeArmService.getPose(vo, vo.getBaseLink(), vo.getEeLink());
ArmCommand.CartesianPose pose = result.getPose();
// 转换为VO
EdgeArmCartesianPoseVO poseVO = new EdgeArmCartesianPoseVO();
poseVO.setX(pose.getX());
poseVO.setY(pose.getY());
poseVO.setZ(pose.getZ());
poseVO.setRx(pose.getRx());
poseVO.setRy(pose.getRy());
poseVO.setRz(pose.getRz());
return AjaxResult.ok(poseVO);
}
@ApiOperation("标定零点")
@GetMapping("/calibrateZeroQ")
public AjaxResult calibrateZeroQ(@Validated EdgeArmCalibrateZeroQVO vo) {
edgeArmService.calibrateZeroQ(vo, vo.getJointName());
return AjaxResult.ok();
}
@ApiOperation("获取位姿矩阵")
@GetMapping("/getPoseMatrix")
public AjaxResult getPoseMatrix(@Validated EdgeArmPoseQueryVO vo) {
ArmCommand.GetPoseMatrix.Response result = edgeArmService.getPoseMatrix(vo, vo.getBaseLink(), vo.getEeLink());
// 将TransformMatrix4x4转换为Map
ArmCommand.TransformMatrix4x4 matrix = result.getMatrix();
java.util.Map<String, Object> matrixMap = new java.util.HashMap<>();
matrixMap.put("m00", matrix.getM00());
matrixMap.put("m01", matrix.getM01());
matrixMap.put("m02", matrix.getM02());
matrixMap.put("m03", matrix.getM03());
matrixMap.put("m10", matrix.getM10());
matrixMap.put("m11", matrix.getM11());
matrixMap.put("m12", matrix.getM12());
matrixMap.put("m13", matrix.getM13());
matrixMap.put("m20", matrix.getM20());
matrixMap.put("m21", matrix.getM21());
matrixMap.put("m22", matrix.getM22());
matrixMap.put("m23", matrix.getM23());
matrixMap.put("m30", matrix.getM30());
matrixMap.put("m31", matrix.getM31());
matrixMap.put("m32", matrix.getM32());
matrixMap.put("m33", matrix.getM33());
return AjaxResult.ok(matrixMap);
}
@ApiOperation("计算正向运动学")
@PostMapping("/computeForwardKinematics")
public AjaxResult computeForwardKinematics(@RequestBody EdgeArmForwardKinematicsVO fkVO) {
ArmCommand.ComputeForwardKinematics.Response result = edgeArmService.computeForwardKinematics(
fkVO,
fkVO.getJoints(),
fkVO.getBaseLink(),
fkVO.getEeLink()
);
// 将TransformMatrix4x4转换为Map
ArmCommand.TransformMatrix4x4 matrix = result.getMatrix();
java.util.Map<String, Object> matrixMap = new java.util.HashMap<>();
matrixMap.put("m00", matrix.getM00());
matrixMap.put("m01", matrix.getM01());
matrixMap.put("m02", matrix.getM02());
matrixMap.put("m03", matrix.getM03());
matrixMap.put("m10", matrix.getM10());
matrixMap.put("m11", matrix.getM11());
matrixMap.put("m12", matrix.getM12());
matrixMap.put("m13", matrix.getM13());
matrixMap.put("m20", matrix.getM20());
matrixMap.put("m21", matrix.getM21());
matrixMap.put("m22", matrix.getM22());
matrixMap.put("m23", matrix.getM23());
matrixMap.put("m30", matrix.getM30());
matrixMap.put("m31", matrix.getM31());
matrixMap.put("m32", matrix.getM32());
matrixMap.put("m33", matrix.getM33());
return AjaxResult.ok(matrixMap);
}
}

View File

@ -1,55 +0,0 @@
package com.cmvr.web.controller.api;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.edge.client.model.EdgeCommonVO;
import com.cmvr.edge.client.model.humanoid.EdgeMoveJVO;
import com.cmvr.edge.client.model.humanoid.EdgeSpeedJVO;
import com.cmvr.edge.client.service.EdgeHumanoidRobotService;
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.RestController;
@Api(tags = "边缘--机械臂")
@RestController
@RequestMapping("/api/humanoid")
@RequiredArgsConstructor
public class EdgeHumanoidRobotController {
private final EdgeHumanoidRobotService edgeHumanoidRobotService;
@ApiOperation("moveJ")
@PostMapping("/moveJ")
public AjaxResult moveJ(@RequestBody EdgeMoveJVO moveJVO) {
return AjaxResult.ok(edgeHumanoidRobotService.moveJ(moveJVO));
}
@ApiOperation("speedJ")
@PostMapping("/speedJ")
public AjaxResult speedJ(@RequestBody EdgeSpeedJVO speedJVO) {
return AjaxResult.ok(edgeHumanoidRobotService.speedJ(speedJVO));
}
@ApiOperation("获取关节状态")
@GetMapping("/getJointState")
public AjaxResult status(EdgeCommonVO vo) {
return AjaxResult.ok(edgeHumanoidRobotService.getJointState(vo));
}
@ApiOperation("使能开")
@GetMapping("/torqueOn")
public AjaxResult torqueOn(EdgeCommonVO vo) {
return AjaxResult.ok(edgeHumanoidRobotService.torqueOn(vo));
}
@ApiOperation("使能关")
@GetMapping("/torqueOff")
public AjaxResult torqueOff(EdgeCommonVO vo) {
return AjaxResult.ok(edgeHumanoidRobotService.torqueOff(vo));
}
}

View File

@ -6,12 +6,10 @@ import com.cmvr.edge.client.model.microphone.EdgeMicrophoneVolumeVO;
import com.cmvr.edge.client.service.EdgeMicrophoneService; import com.cmvr.edge.client.service.EdgeMicrophoneService;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/** /**
* 麦克风管理 * 麦克风管理
@ -27,14 +25,17 @@ public class EdgeMicrophoneController {
@ApiOperation("获取麦克风状态") @ApiOperation("获取麦克风状态")
@PreAuthorize("@ss.hasPermi('system:microphone:query')") @PreAuthorize("@ss.hasPermi('system:microphone:query')")
@GetMapping("/status") @GetMapping("/status")
public AjaxResult getStatus(EdgeCommonVO edgeCommonVO) { public AjaxResult getStatus(@ApiParam(value = "终端设备ID", required = true) @RequestParam String terminalId, @ApiParam (value = "设备ID", required = true) @RequestParam String deviceId) {
EdgeCommonVO edgeCommonVO = new EdgeCommonVO();
edgeCommonVO.setTerminalId(terminalId);
edgeCommonVO.setDeviceId(deviceId);
return AjaxResult.success(microphoneService.getStatus(edgeCommonVO)); return AjaxResult.success(microphoneService.getStatus(edgeCommonVO));
} }
@ApiOperation("开始录音") @ApiOperation("开始录音")
@PreAuthorize("@ss.hasPermi('system:microphone:record')") @PreAuthorize("@ss.hasPermi('system:microphone:record')")
@PostMapping("/start") @PostMapping("/start")
public AjaxResult startRecord(EdgeCommonVO edgeCommonVO) { public AjaxResult startRecord(@RequestBody EdgeCommonVO edgeCommonVO) {
String filePath = microphoneService.startRecord(edgeCommonVO); String filePath = microphoneService.startRecord(edgeCommonVO);
return AjaxResult.success(filePath); return AjaxResult.success(filePath);
} }
@ -42,7 +43,7 @@ public class EdgeMicrophoneController {
@ApiOperation("停止录音") @ApiOperation("停止录音")
@PreAuthorize("@ss.hasPermi('system:microphone:stop')") @PreAuthorize("@ss.hasPermi('system:microphone:stop')")
@PostMapping("/stop") @PostMapping("/stop")
public AjaxResult stopRecord(EdgeCommonVO edgeCommonVO) { public AjaxResult stopRecord(@RequestBody EdgeCommonVO edgeCommonVO) {
microphoneService.stopRecord(edgeCommonVO); microphoneService.stopRecord(edgeCommonVO);
return AjaxResult.success(); return AjaxResult.success();
} }
@ -50,7 +51,7 @@ public class EdgeMicrophoneController {
@ApiOperation("暂停录音") @ApiOperation("暂停录音")
@PreAuthorize("@ss.hasPermi('system:microphone:pause')") @PreAuthorize("@ss.hasPermi('system:microphone:pause')")
@PostMapping("/pause") @PostMapping("/pause")
public AjaxResult pauseRecord(EdgeCommonVO edgeCommonVO) { public AjaxResult pauseRecord(@RequestBody EdgeCommonVO edgeCommonVO) {
microphoneService.pauseRecord(edgeCommonVO); microphoneService.pauseRecord(edgeCommonVO);
return AjaxResult.success(); return AjaxResult.success();
} }
@ -58,7 +59,7 @@ public class EdgeMicrophoneController {
@ApiOperation("恢复录音") @ApiOperation("恢复录音")
@PreAuthorize("@ss.hasPermi('system:microphone:resume')") @PreAuthorize("@ss.hasPermi('system:microphone:resume')")
@PostMapping("/resume") @PostMapping("/resume")
public AjaxResult resumeRecord(EdgeCommonVO edgeCommonVO) { public AjaxResult resumeRecord(@RequestBody EdgeCommonVO edgeCommonVO) {
microphoneService.resumeRecord(edgeCommonVO); microphoneService.resumeRecord(edgeCommonVO);
return AjaxResult.success(); return AjaxResult.success();
} }
@ -66,7 +67,7 @@ public class EdgeMicrophoneController {
@ApiOperation("设置音量") @ApiOperation("设置音量")
@PreAuthorize("@ss.hasPermi('system:microphone:volume')") @PreAuthorize("@ss.hasPermi('system:microphone:volume')")
@PostMapping("/volume") @PostMapping("/volume")
public AjaxResult setVolume(EdgeMicrophoneVolumeVO edgeMicrophoneVolumeVO) { public AjaxResult setVolume(@RequestBody EdgeMicrophoneVolumeVO edgeMicrophoneVolumeVO) {
microphoneService.setVolume(edgeMicrophoneVolumeVO); microphoneService.setVolume(edgeMicrophoneVolumeVO);
return AjaxResult.success(); return AjaxResult.success();
} }
@ -74,7 +75,10 @@ public class EdgeMicrophoneController {
@ApiOperation("获取音量") @ApiOperation("获取音量")
@PreAuthorize("@ss.hasPermi('system:microphone:volume')") @PreAuthorize("@ss.hasPermi('system:microphone:volume')")
@GetMapping("/volume") @GetMapping("/volume")
public AjaxResult getVolume(EdgeCommonVO edgeCommonVO) { public AjaxResult getVolume(@ApiParam(value = "终端设备ID", required = true) @RequestParam String terminalId, @ApiParam (value = "设备ID", required = true) @RequestParam String deviceId) {
EdgeCommonVO edgeCommonVO = new EdgeCommonVO();
edgeCommonVO.setTerminalId(terminalId);
edgeCommonVO.setDeviceId(deviceId);
return AjaxResult.success(microphoneService.getVolume(edgeCommonVO)); return AjaxResult.success(microphoneService.getVolume(edgeCommonVO));
} }
} }

View File

@ -1,6 +1,9 @@
package com.cmvr.web.controller.api; package com.cmvr.web.controller.api;
import cmvr.api.SpeakerCommand; import cmvr.api.SpeakerCommand;
import com.cmvr.edge.client.model.EdgeCommonVO;
import com.cmvr.edge.client.model.microphone.EdgeMicrophoneVolumeVO;
import com.cmvr.edge.client.model.speaker.EdgeSpeakerPlayAudioVO;
import com.cmvr.edge.client.service.EdgeSpeakerService; import com.cmvr.edge.client.service.EdgeSpeakerService;
import com.cmvr.common.core.domain.AjaxResult; import com.cmvr.common.core.domain.AjaxResult;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
@ -69,14 +72,8 @@ public class EdgeSpeakerController {
@ApiOperation("播放音频") @ApiOperation("播放音频")
@PreAuthorize("@ss.hasPermi('system:speaker:play')") @PreAuthorize("@ss.hasPermi('system:speaker:play')")
@PostMapping("/play") @PostMapping("/play")
public AjaxResult playAudio( public AjaxResult playAudio(@RequestBody EdgeSpeakerPlayAudioVO edgeSpeakerPlayAudioVO) {
@ApiParam(value = "终端设备ID", required = true) speakerService.playAudio(edgeSpeakerPlayAudioVO.getTerminalId(), edgeSpeakerPlayAudioVO.getDeviceId(), edgeSpeakerPlayAudioVO.getAudioPath());
@RequestParam String terminalId,
@ApiParam(value = "设备ID", required = true)
@RequestParam String deviceId,
@ApiParam(value = "音频文件路径", required = true)
@RequestParam String audioPath) {
speakerService.playAudio(terminalId, deviceId, audioPath);
return AjaxResult.success(); return AjaxResult.success();
} }
@ -86,12 +83,8 @@ public class EdgeSpeakerController {
@ApiOperation("停止播放") @ApiOperation("停止播放")
@PreAuthorize("@ss.hasPermi('system:speaker:stop')") @PreAuthorize("@ss.hasPermi('system:speaker:stop')")
@PostMapping("/stop") @PostMapping("/stop")
public AjaxResult stopPlayback( public AjaxResult stopPlayback(@RequestBody EdgeCommonVO edgeCommonVO) {
@ApiParam(value = "终端设备ID", required = true) speakerService.stopPlayback(edgeCommonVO.getTerminalId(), edgeCommonVO.getDeviceId());
@RequestParam String terminalId,
@ApiParam(value = "设备ID", required = true)
@RequestParam String deviceId) {
speakerService.stopPlayback(terminalId, deviceId);
return AjaxResult.success(); return AjaxResult.success();
} }
@ -101,12 +94,8 @@ public class EdgeSpeakerController {
@ApiOperation("暂停播放") @ApiOperation("暂停播放")
@PreAuthorize("@ss.hasPermi('system:speaker:pause')") @PreAuthorize("@ss.hasPermi('system:speaker:pause')")
@PostMapping("/pause") @PostMapping("/pause")
public AjaxResult pausePlayback( public AjaxResult pausePlayback(@RequestBody EdgeCommonVO edgeCommonVO) {
@ApiParam(value = "终端设备ID", required = true) speakerService.pausePlayback(edgeCommonVO.getTerminalId(), edgeCommonVO.getDeviceId());
@RequestParam String terminalId,
@ApiParam(value = "设备ID", required = true)
@RequestParam String deviceId) {
speakerService.pausePlayback(terminalId, deviceId);
return AjaxResult.success(); return AjaxResult.success();
} }
@ -116,12 +105,8 @@ public class EdgeSpeakerController {
@ApiOperation("恢复播放") @ApiOperation("恢复播放")
@PreAuthorize("@ss.hasPermi('system:speaker:resume')") @PreAuthorize("@ss.hasPermi('system:speaker:resume')")
@PostMapping("/resume") @PostMapping("/resume")
public AjaxResult resumePlayback( public AjaxResult resumePlayback(@RequestBody EdgeCommonVO edgeCommonVO) {
@ApiParam(value = "终端设备ID", required = true) speakerService.resumePlayback(edgeCommonVO.getTerminalId(), edgeCommonVO.getDeviceId());
@RequestParam String terminalId,
@ApiParam(value = "设备ID", required = true)
@RequestParam String deviceId) {
speakerService.resumePlayback(terminalId, deviceId);
return AjaxResult.success(); return AjaxResult.success();
} }
@ -131,14 +116,8 @@ public class EdgeSpeakerController {
@ApiOperation("设置音量") @ApiOperation("设置音量")
@PreAuthorize("@ss.hasPermi('system:speaker:volume')") @PreAuthorize("@ss.hasPermi('system:speaker:volume')")
@PostMapping("/volume") @PostMapping("/volume")
public AjaxResult setVolume( public AjaxResult setVolume(@RequestBody EdgeMicrophoneVolumeVO edgeMicrophoneVolumeVO) {
@ApiParam(value = "终端设备ID", required = true) speakerService.setVolume(edgeMicrophoneVolumeVO.getTerminalId(), edgeMicrophoneVolumeVO.getDeviceId(), edgeMicrophoneVolumeVO.getVolume());
@RequestParam String terminalId,
@ApiParam(value = "设备ID", required = true)
@RequestParam String deviceId,
@ApiParam(value = "音量大小0-100", required = true)
@RequestParam int volume) {
speakerService.setVolume(terminalId, deviceId, volume);
return AjaxResult.success(); return AjaxResult.success();
} }

View File

@ -0,0 +1,123 @@
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 lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
/**
* WebRTC 信令 WebSocket 入口
*
* 路径/ws/signaling
*
* 约束
* 1. WebSocket 只传 JSON 信令joinofferanswercandidateheartbeatstop
* 2. WebSocket 禁止传输 PCM 音频
* 3. PCM 音频由 WebRTC 媒体通道进入 Java 后端再由 AudioService gRPC 双向流
*/
@Configuration
@RequiredArgsConstructor
public class WebRtcSignalingController implements WebSocketConfigurer {
private final AudioService audioService;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new SignalingWebSocketHandler(audioService), "/ws/signaling")
.setAllowedOrigins("*");
registry.addHandler(new SignalingWebSocketHandler(audioService), "/ws/audio")
.setAllowedOrigins("*");
}
/**
* 信令处理器这里只处理文本 JSON不处理二进制音频
*/
private static class SignalingWebSocketHandler extends AbstractWebSocketHandler {
private static final Logger log = LoggerFactory.getLogger(SignalingWebSocketHandler.class);
private final AudioService audioService;
private final ObjectMapper objectMapper = new ObjectMapper();
private SignalingWebSocketHandler(AudioService audioService) {
this.audioService = audioService;
}
@Override
public void afterConnectionEstablished(@NotNull WebSocketSession session) {
log.info("WebRTC信令连接建立wsSessionId={}", session.getId());
sendConnected(session);
}
@Override
protected void handleTextMessage(@NotNull WebSocketSession session, @NotNull TextMessage message) {
try {
audioService.handleSignalingMessage(session, message.getPayload());
} catch (Exception e) {
log.error("处理WebRTC信令异常wsSessionId={}", session.getId(), e);
sendError(session, "处理WebRTC信令异常" + e.getMessage());
}
}
@Override
protected void handleBinaryMessage(@NotNull WebSocketSession session, @NotNull BinaryMessage message) {
try {
audioService.handleBrowserAudioMessage(session, message.getPayload());
} catch (Exception e) {
log.error("处理WebSocket音频二进制帧失败wsSessionId={}", session.getId(), e);
sendError(session, "audio binary frame failed: " + e.getMessage());
}
}
@Override
public void handleTransportError(@NotNull WebSocketSession session, @NotNull Throwable exception) {
log.error("WebRTC信令连接传输异常wsSessionId={}", session.getId(), exception);
audioService.handleWebSocketClosed(session);
}
@Override
public void afterConnectionClosed(@NotNull WebSocketSession session, @NotNull CloseStatus status) {
log.info("WebRTC信令连接关闭wsSessionId={}status={}", session.getId(), status);
audioService.handleWebSocketClosed(session);
}
private void sendConnected(WebSocketSession session) {
ObjectNode node = objectMapper.createObjectNode();
node.put("type", "connected");
node.put("timestamp", System.currentTimeMillis());
sendJson(session, node);
}
private void sendError(WebSocketSession session, String message) {
ObjectNode node = objectMapper.createObjectNode();
node.put("type", "error");
node.put("message", message);
node.put("timestamp", System.currentTimeMillis());
sendJson(session, node);
}
private void sendJson(WebSocketSession session, ObjectNode node) {
if (session == null || !session.isOpen()) {
return;
}
try {
synchronized (session) {
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(node)));
}
} catch (Exception e) {
log.error("发送WebRTC信令响应失败wsSessionId={}", session.getId(), e);
}
}
}
}

View File

@ -0,0 +1,114 @@
package com.cmvr.web.controller.inspection;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.annotation.Log;
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.InspectionAlarm;
import com.cmvr.inspection.domain.vo.InspectionAlarmVo;
import com.cmvr.inspection.service.IInspectionAlarmService;
import com.cmvr.common.utils.poi.ExcelUtil;
import com.cmvr.common.core.page.TableDataInfo;
/**
* 巡检告警Controller
*
* @author cmvr-iot
* @since 2026-05-29
*/
@RestController
@RequestMapping("/inspection/alarm")
@Api(tags = "智能巡检--告警管理")
public class InspectionAlarmController extends BaseController
{
@Autowired
private IInspectionAlarmService inspectionAlarmService;
/**
* 查询巡检告警列表
*/
@ApiOperation("查询巡检告警列表")
@PreAuthorize("@ss.hasPermi('inspection:alarm:list')")
@GetMapping("/list")
public TableDataInfo list(InspectionAlarm inspectionAlarm)
{
startPage();
List<InspectionAlarmVo> list = inspectionAlarmService.selectInspectionAlarmList(inspectionAlarm);
return getDataTable(list);
}
/**
* 导出巡检告警列表
*/
@ApiOperation("导出巡检告警列表")
@PreAuthorize("@ss.hasPermi('inspection:alarm:export')")
@Log(title = "巡检告警", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, InspectionAlarm inspectionAlarm)
{
List<InspectionAlarmVo> list = inspectionAlarmService.selectInspectionAlarmList(inspectionAlarm);
ExcelUtil<InspectionAlarmVo> util = new ExcelUtil<>(InspectionAlarmVo.class);
util.exportExcel(response, list, "巡检告警数据");
}
/**
* 获取巡检告警详细信息
*/
@ApiOperation("获取巡检告警详细信息")
@PreAuthorize("@ss.hasPermi('inspection:alarm:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") String id)
{
return success(inspectionAlarmService.selectInspectionAlarmById(id));
}
/**
* 新增巡检告警
*/
@ApiOperation("新增巡检告警")
@PreAuthorize("@ss.hasPermi('inspection:alarm:add')")
@Log(title = "巡检告警", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody InspectionAlarm inspectionAlarm)
{
return toAjax(inspectionAlarmService.insertInspectionAlarm(inspectionAlarm));
}
/**
* 修改巡检告警
*/
@ApiOperation("修改巡检告警")
@PreAuthorize("@ss.hasPermi('inspection:alarm:edit')")
@Log(title = "巡检告警", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody InspectionAlarm inspectionAlarm)
{
return toAjax(inspectionAlarmService.updateInspectionAlarm(inspectionAlarm));
}
/**
* 删除巡检告警
*/
@ApiOperation("删除巡检告警")
@PreAuthorize("@ss.hasPermi('inspection:alarm:remove')")
@Log(title = "巡检告警", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable String[] ids)
{
return toAjax(inspectionAlarmService.deleteInspectionAlarmByIds(ids));
}
}

View File

@ -0,0 +1,123 @@
package com.cmvr.web.controller.inspection;
import com.cmvr.common.annotation.Anonymous;
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 io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
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 java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* cmvr_edge_ai 检测报警推送入口
*
* 该接口由边缘服务主动调用使用HTTP状态码表达处理结果不使用项目通用AjaxResult包装
*/
@Slf4j
@Anonymous
@RestController
@RequiredArgsConstructor
@Api(tags = "智能巡检--PPE报警接收")
public class InspectionDetectionAlertController
{
/** 包含Base64图片的JSON请求最大允许20MB防止异常请求耗尽服务端内存。 */
private static final int MAX_REQUEST_BYTES = 20 * 1024 * 1024;
private final IInspectionDetectionAlertService detectionAlertService;
private final ObjectMapper objectMapper;
/**
* 接收PPE违规报警重复事件视为已处理仍返回204避免边缘端持续重试
*/
@ApiOperation("接收边缘AI的PPE违规报警")
@PostMapping(value = "/v1/detection-alerts", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> receive(
@RequestHeader(value = "Idempotency-Key", required = false) String idempotencyKey,
HttpServletRequest request)
{
try
{
String requestBody = readRequestBody(request);
AlertEnvelope envelope = objectMapper.readValue(requestBody, AlertEnvelope.class);
detectionAlertService.receive(envelope, idempotencyKey);
return ResponseEntity.noContent().build();
}
catch (JsonProcessingException | DetectionAlertBadRequestException ex)
{
log.warn("拒绝不合法的PPE报警请求idempotencyKey={},原因={}", idempotencyKey, ex.getMessage());
return ResponseEntity.badRequest().build();
}
catch (DuplicateKeyException ex)
{
// 预查与插入之间仍可能发生并发由数据库唯一索引完成最终幂等
log.info("忽略并发重复的PPE报警idempotencyKey={}", idempotencyKey);
return ResponseEntity.noContent().build();
}
catch (IOException ex)
{
log.warn("读取PPE报警请求失败idempotencyKey={},原因={}", idempotencyKey, ex.getMessage());
return ResponseEntity.badRequest().build();
}
catch (Exception ex)
{
log.error("PPE报警处理暂时失败idempotencyKey={}", idempotencyKey, ex);
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build();
}
}
private String readRequestBody(HttpServletRequest request) throws IOException
{
long contentLength = request.getContentLengthLong();
if (contentLength > MAX_REQUEST_BYTES)
{
throw new DetectionAlertBadRequestException("请求体不能超过20MB");
}
try (ServletInputStream inputStream = request.getInputStream();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(initialCapacity(contentLength)))
{
byte[] buffer = new byte[8192];
int total = 0;
int length;
while ((length = inputStream.read(buffer)) != -1)
{
total += length;
if (total > MAX_REQUEST_BYTES)
{
throw new DetectionAlertBadRequestException("请求体不能超过20MB");
}
outputStream.write(buffer, 0, length);
}
if (total == 0)
{
throw new DetectionAlertBadRequestException("请求体不能为空");
}
return new String(outputStream.toByteArray(), StandardCharsets.UTF_8);
}
}
private int initialCapacity(long contentLength)
{
if (contentLength <= 0 || contentLength > MAX_REQUEST_BYTES)
{
return 8192;
}
return (int) contentLength;
}
}

View File

@ -0,0 +1,114 @@
package com.cmvr.web.controller.inspection;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.annotation.Log;
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.InspectionMap;
import com.cmvr.inspection.domain.vo.InspectionMapVo;
import com.cmvr.inspection.service.IInspectionMapService;
import com.cmvr.common.utils.poi.ExcelUtil;
import com.cmvr.common.core.page.TableDataInfo;
/**
* 巡检地图Controller
*
* @author cmvr-iot
* @since 2026-05-29
*/
@RestController
@RequestMapping("/inspection/map")
@Api(tags = "智能巡检--地图管理")
public class InspectionMapController extends BaseController
{
@Autowired
private IInspectionMapService inspectionMapService;
/**
* 查询巡检地图列表
*/
@ApiOperation("查询巡检地图列表")
@PreAuthorize("@ss.hasPermi('inspection:map:list')")
@GetMapping("/list")
public TableDataInfo list(InspectionMap inspectionMap)
{
startPage();
List<InspectionMapVo> list = inspectionMapService.selectInspectionMapList(inspectionMap);
return getDataTable(list);
}
/**
* 导出巡检地图列表
*/
@ApiOperation("导出巡检地图列表")
@PreAuthorize("@ss.hasPermi('inspection:map:export')")
@Log(title = "巡检地图", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, InspectionMap inspectionMap)
{
List<InspectionMapVo> list = inspectionMapService.selectInspectionMapList(inspectionMap);
ExcelUtil<InspectionMapVo> util = new ExcelUtil<>(InspectionMapVo.class);
util.exportExcel(response, list, "巡检地图数据");
}
/**
* 获取巡检地图详细信息
*/
@ApiOperation("获取巡检地图详细信息")
@PreAuthorize("@ss.hasPermi('inspection:map:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") String id)
{
return success(inspectionMapService.selectInspectionMapById(id));
}
/**
* 新增巡检地图
*/
@ApiOperation("新增巡检地图")
@PreAuthorize("@ss.hasPermi('inspection:map:add')")
@Log(title = "巡检地图", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody InspectionMap inspectionMap)
{
return toAjax(inspectionMapService.insertInspectionMap(inspectionMap));
}
/**
* 修改巡检地图
*/
@ApiOperation("修改巡检地图")
@PreAuthorize("@ss.hasPermi('inspection:map:edit')")
@Log(title = "巡检地图", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody InspectionMap inspectionMap)
{
return toAjax(inspectionMapService.updateInspectionMap(inspectionMap));
}
/**
* 删除巡检地图
*/
@ApiOperation("删除巡检地图")
@PreAuthorize("@ss.hasPermi('inspection:map:remove')")
@Log(title = "巡检地图", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable String[] ids)
{
return toAjax(inspectionMapService.deleteInspectionMapByIds(ids));
}
}

View File

@ -0,0 +1,126 @@
package com.cmvr.web.controller.inspection;
import com.cmvr.common.annotation.Log;
import com.cmvr.common.core.controller.BaseController;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.common.core.page.TableDataInfo;
import com.cmvr.common.enums.BusinessType;
import com.cmvr.common.utils.poi.ExcelUtil;
import com.cmvr.inspection.domain.dto.InspectionResultQuery;
import com.cmvr.inspection.domain.dto.InspectionResultReviewRequest;
import com.cmvr.inspection.domain.vo.InspectionResultVo;
import com.cmvr.inspection.service.IInspectionResultService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.RestController;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
* 巡检结果人工复核和任务报表接口
*/
@RestController
@RequestMapping("/inspection/result")
@Api(tags = "智能巡检--巡检结果", description = "巡检结果查询、人工复核、三级告警统计和报表导出")
@ApiResponses({
@ApiResponse(code = 200, message = "请求处理成功"),
@ApiResponse(code = 400, message = "请求参数或状态不合法"),
@ApiResponse(code = 401, message = "未登录或登录已失效"),
@ApiResponse(code = 403, message = "无接口权限"),
@ApiResponse(code = 500, message = "系统内部异常")
})
@RequiredArgsConstructor
public class InspectionResultController extends BaseController
{
private final IInspectionResultService inspectionResultService;
@ApiOperation(value = "查询巡检结果列表",
notes = "分页查询PPE、仪表读数和人工判断结果查询条件均为可选结果按检测时间倒序。"
+ "返回rows元素为InspectionResultVo包含taskName任务名称和evidenceType主要证据类型"
+ "完整mediaList仅在详情接口返回")
@ApiImplicitParams({
@ApiImplicitParam(name = "pageNum", value = "页码从1开始", dataType = "int", paramType = "query", example = "1"),
@ApiImplicitParam(name = "pageSize", value = "每页数量", dataType = "int", paramType = "query", example = "10"),
@ApiImplicitParam(name = "taskInstanceId", value = "巡检任务实例数据库ID", dataType = "string", paramType = "query"),
@ApiImplicitParam(name = "taskId", value = "巡检任务ID", dataType = "string", paramType = "query"),
@ApiImplicitParam(name = "itemId", value = "检测项ID", dataType = "string", paramType = "query"),
@ApiImplicitParam(name = "resultType", value = "结果类型PPE、METER、MANUAL", dataType = "string", paramType = "query"),
@ApiImplicitParam(name = "resultStatus", value = "状态PENDING、NORMAL、ABNORMAL、RECOGNIZE_FAILED", dataType = "string", paramType = "query"),
@ApiImplicitParam(name = "alarmLevel", value = "告警级别1提示、2警告、3严重", dataType = "int", paramType = "query"),
@ApiImplicitParam(name = "resultName", value = "检查名称,支持模糊查询", dataType = "string", paramType = "query")
})
@PreAuthorize("@ss.hasPermi('inspection:result:list')")
@GetMapping("/list")
public TableDataInfo list(@ApiParam("巡检结果查询条件") InspectionResultQuery query)
{
startPage();
return getDataTable(inspectionResultService.selectResultList(query));
}
@ApiOperation(value = "查询巡检结果详情",
notes = "返回结构化巡检结果及按顺序排列的全部图片、视频证据",
response = InspectionResultVo.class)
@PreAuthorize("@ss.hasPermi('inspection:result:query')")
@GetMapping("/{id}")
public AjaxResult getInfo(@ApiParam(value = "巡检结果ID", required = true)
@PathVariable String id)
{
return success(inspectionResultService.selectResultById(id));
}
@ApiOperation(value = "提交人工复核结果",
notes = "仅PENDING或RECOGNIZE_FAILED状态可复核。ABNORMAL必须传alarmLevelreviewVersion用于并发控制")
@PreAuthorize("@ss.hasPermi('inspection:result:review')")
@Log(title = "巡检结果人工复核", businessType = BusinessType.UPDATE)
@PutMapping("/{id}/review")
public AjaxResult review(@ApiParam(value = "巡检结果ID", required = true)
@PathVariable String id,
@ApiParam(value = "人工复核内容", required = true)
@Validated @RequestBody InspectionResultReviewRequest request)
{
return success(inspectionResultService.review(id, request));
}
@ApiOperation(value = "查询巡检任务报表汇总",
notes = "返回summary统计和results明细。reviewCompleted=false表示仍有待复核或识别失败结果")
@PreAuthorize("@ss.hasPermi('inspection:result:report')")
@GetMapping("/report/{taskInstanceId}")
public AjaxResult report(@ApiParam(value = "巡检任务实例数据库ID", required = true)
@PathVariable String taskInstanceId)
{
AjaxResult result = AjaxResult.success();
result.put("summary", inspectionResultService.buildReportSummary(taskInstanceId));
InspectionResultQuery query = new InspectionResultQuery();
query.setTaskInstanceId(taskInstanceId);
result.put("results", inspectionResultService.selectResultList(query));
return result;
}
@ApiOperation(value = "导出巡检结果报表",
notes = "按查询条件导出Excel不传条件时导出全部巡检结果")
@PreAuthorize("@ss.hasPermi('inspection:result:export')")
@Log(title = "巡检结果", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response,
@ApiParam("导出筛选条件") InspectionResultQuery query)
{
List<InspectionResultVo> list = inspectionResultService.selectResultList(query);
ExcelUtil<InspectionResultVo> util = new ExcelUtil<>(InspectionResultVo.class);
util.exportExcel(response, list, "巡检结果数据");
}
}

View File

@ -0,0 +1,241 @@
package com.cmvr.web.controller.inspection;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import cmvr.msgs.Agv;
import com.cmvr.inspection.domain.vo.InspectionRobotVo;
import io.swagger.annotations.*;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.cmvr.common.annotation.Log;
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.InspectionRobot;
import com.cmvr.inspection.service.IInspectionRobotService;
import com.cmvr.common.utils.poi.ExcelUtil;
import com.cmvr.common.core.page.TableDataInfo;
/**
* 巡检机器人Controller
*
* @author cmvr-iot
* @since 2026-05-29
*/
@RestController
@RequestMapping("/inspection/robot")
@Api(tags = "智能巡检--机器人管理")
public class InspectionRobotController extends BaseController
{
@Autowired
private IInspectionRobotService inspectionRobotService;
/**
* 查询巡检机器人列表
*/
@ApiOperation("查询巡检机器人列表")
@PreAuthorize("@ss.hasPermi('inspection:robot:list')")
@GetMapping("/list")
public TableDataInfo list(InspectionRobot inspectionRobot)
{
startPage();
List<InspectionRobotVo> list = inspectionRobotService.selectInspectionRobotList(inspectionRobot);
return getDataTable(list);
}
/**
* 导出巡检机器人列表
*/
@ApiOperation("导出巡检机器人列表")
@PreAuthorize("@ss.hasPermi('inspection:robot:export')")
@Log(title = "巡检机器人", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, InspectionRobot inspectionRobot)
{
List<InspectionRobotVo> list = inspectionRobotService.selectInspectionRobotList(inspectionRobot);
ExcelUtil<InspectionRobotVo> util = new ExcelUtil<>(InspectionRobotVo.class);
util.exportExcel(response, list, "巡检机器人数据");
}
/**
* 获取巡检机器人详细信息
*/
@ApiOperation("获取巡检机器人详细信息")
@PreAuthorize("@ss.hasPermi('inspection:robot:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") String id)
{
return success(inspectionRobotService.selectInspectionRobotById(id));
}
/**
* 新增巡检机器人
*/
@ApiOperation("新增巡检机器人")
@PreAuthorize("@ss.hasPermi('inspection:robot:add')")
@Log(title = "巡检机器人", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody InspectionRobot inspectionRobot)
{
return toAjax(inspectionRobotService.insertInspectionRobot(inspectionRobot));
}
/**
* 修改巡检机器人
*/
@ApiOperation("修改巡检机器人")
@PreAuthorize("@ss.hasPermi('inspection:robot:edit')")
@Log(title = "巡检机器人", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody InspectionRobot inspectionRobot)
{
return toAjax(inspectionRobotService.updateInspectionRobot(inspectionRobot));
}
/**
* 删除巡检机器人
*/
@ApiOperation("删除巡检机器人")
@PreAuthorize("@ss.hasPermi('inspection:robot:remove')")
@Log(title = "巡检机器人", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable String[] ids)
{
return toAjax(inspectionRobotService.deleteInspectionRobotByIds(ids));
}
/**
* 获取机器人地图列表从AGV获取
*/
@ApiOperation("获取机器人地图列表")
@PreAuthorize("@ss.hasPermi('inspection:robot:query')")
@GetMapping("/{robotId}/maps")
public AjaxResult getRobotMapList(@PathVariable String robotId)
{
List<String> robotMapList = inspectionRobotService.getRobotMapList(robotId);
return success(robotMapList);
}
/**
* 绑定机器人地图
*/
@ApiOperation("绑定机器人地图")
@PreAuthorize("@ss.hasPermi('inspection:robot:edit')")
@Log(title = "绑定机器人地图", businessType = BusinessType.UPDATE)
@PostMapping("/{robotId}/bind-map/{mapId}")
public AjaxResult bindRobotMap(@PathVariable String robotId,
@PathVariable String mapId)
{
return toAjax(inspectionRobotService.bindRobotMap(robotId, mapId));
}
/**
* 从机器人下载地图
*/
@ApiOperation("从机器人下载地图")
@PreAuthorize("@ss.hasPermi('inspection:robot:add')")
@Log(title = "从机器人下载地图", businessType = BusinessType.INSERT)
@GetMapping("/{robotId}/download-map")
public AjaxResult downloadMapFromRobot(
@PathVariable @ApiParam("机器人ID") String robotId,
@RequestParam(value = "mapName") @ApiParam("地图名称") String mapName
)
{
Object mapId = inspectionRobotService.downloadMapFromRobot(robotId, mapName);
return success(mapId);
}
/**
* 上传地图从源机器人下载到目标机器人
*/
@ApiOperation("上传地图到机器人")
@PreAuthorize("@ss.hasPermi('inspection:robot:edit')")
@Log(title = "上传地图到机器人", businessType = BusinessType.OTHER)
@PostMapping("/{robotId}/upload-map-from-source")
public AjaxResult uploadMapFromSource(@PathVariable String robotId)
{
String result = inspectionRobotService.uploadMapFromSource(robotId);
return success(result);
}
/**
* 同步机器人状态
*/
@ApiOperation("同步机器人状态")
@PreAuthorize("@ss.hasPermi('inspection:robot:edit')")
@Log(title = "同步机器人状态", businessType = BusinessType.UPDATE)
@PostMapping("/{robotId}/sync-status")
public AjaxResult syncRobotStatus(@PathVariable String robotId)
{
return success(inspectionRobotService.syncRobotStatus(robotId));
}
/**
* 获取机器人实时位置
*/
@ApiOperation("获取机器人实时位置")
@PreAuthorize("@ss.hasPermi('inspection:robot:query')")
@GetMapping("/{robotId}/location")
public AjaxResult getRobotRealtimeLocation(@PathVariable String robotId)
{
Agv.AgvPose2d location = inspectionRobotService.getRobotRealtimeLocation(robotId);
return success(location);
}
/**
* 导航到指定位置坐标
*/
@ApiOperation("导航到指定位置")
@PreAuthorize("@ss.hasPermi('inspection:robot:edit')")
@Log(title = "导航到指定位置", businessType = BusinessType.OTHER)
@PostMapping("/{robotId}/navigate-to-position")
public AjaxResult navigateToPosition(
@PathVariable @ApiParam("机器人ID") String robotId,
@RequestParam @ApiParam("X坐标") Double x,
@RequestParam @ApiParam("Y坐标") Double y,
@RequestParam(required = false) @ApiParam("朝向角(弧度)") Double theta
)
{
inspectionRobotService.navigateToPosition(
robotId, x, y, theta
);
return success();
}
/**
* 开始建图
*/
@ApiOperation("开始建图")
@PreAuthorize("@ss.hasPermi('inspection:robot:edit')")
@Log(title = "开始建图", businessType = BusinessType.OTHER)
@PostMapping("/{robotId}/start-mapping")
public AjaxResult startMapping(
@PathVariable @ApiParam("机器人ID") String robotId,
@RequestParam @ApiParam("建图维度0:未指定 1:2D 2:3D 3:2D+3D") int dimension,
@RequestParam(required = false) @ApiParam("地图名称(可选)") String mapName,
@RequestParam(defaultValue = "true") @ApiParam("是否实时建图") boolean realTime
)
{
String sessionId = inspectionRobotService.startMapping(robotId, dimension, mapName, realTime);
java.util.Map<String, Object> result = new java.util.HashMap<>();
result.put("sessionId", sessionId);
return success(result);
}
/**
* 停止建图
*/
@ApiOperation("停止建图")
@PreAuthorize("@ss.hasPermi('inspection:robot:edit')")
@Log(title = "停止建图", businessType = BusinessType.OTHER)
@PostMapping("/{robotId}/stop-mapping")
public AjaxResult stopMapping(@PathVariable @ApiParam("机器人ID") String robotId)
{
inspectionRobotService.stopMapping(robotId);
return success();
}
}

View File

@ -0,0 +1,151 @@
package com.cmvr.web.controller.inspection;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.annotation.Log;
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.InspectionTask;
import com.cmvr.inspection.domain.InspectionWaypoint;
import com.cmvr.inspection.domain.vo.InspectionTaskVo;
import com.cmvr.inspection.service.IInspectionTaskService;
import com.cmvr.common.utils.poi.ExcelUtil;
import com.cmvr.common.core.page.TableDataInfo;
/**
* 巡检任务Controller
*
* @author cmvr-iot
* @since 2026-05-29
*/
@RestController
@RequestMapping("/inspection/task")
@Api(tags = "智能巡检--任务管理")
public class InspectionTaskController extends BaseController
{
@Autowired
private IInspectionTaskService inspectionTaskService;
/**
* 查询巡检任务列表
*/
@ApiOperation("查询巡检任务列表")
@PreAuthorize("@ss.hasPermi('inspection:task:list')")
@GetMapping("/list")
public TableDataInfo list(InspectionTask inspectionTask)
{
startPage();
List<InspectionTaskVo> list = inspectionTaskService.selectInspectionTaskList(inspectionTask);
return getDataTable(list);
}
/**
* 导出巡检任务列表
*/
@ApiOperation("导出巡检任务列表")
@PreAuthorize("@ss.hasPermi('inspection:task:export')")
@Log(title = "巡检任务", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, InspectionTask inspectionTask)
{
List<InspectionTaskVo> list = inspectionTaskService.selectInspectionTaskList(inspectionTask);
ExcelUtil<InspectionTaskVo> util = new ExcelUtil<>(InspectionTaskVo.class);
util.exportExcel(response, list, "巡检任务数据");
}
/**
* 获取巡检任务详细信息
*/
@ApiOperation("获取巡检任务详细信息")
@PreAuthorize("@ss.hasPermi('inspection:task:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") String id)
{
return success(inspectionTaskService.selectInspectionTaskById(id));
}
/**
* 新增巡检任务
*/
@ApiOperation("新增巡检任务")
@PreAuthorize("@ss.hasPermi('inspection:task:add')")
@Log(title = "巡检任务", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody InspectionTask inspectionTask)
{
return toAjax(inspectionTaskService.insertInspectionTask(inspectionTask));
}
/**
* 修改巡检任务
*/
@ApiOperation("修改巡检任务")
@PreAuthorize("@ss.hasPermi('inspection:task:edit')")
@Log(title = "巡检任务", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody InspectionTask inspectionTask)
{
return toAjax(inspectionTaskService.updateInspectionTask(inspectionTask));
}
/**
* 删除巡检任务
*/
@ApiOperation("删除巡检任务")
@PreAuthorize("@ss.hasPermi('inspection:task:remove')")
@Log(title = "巡检任务", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable String[] ids)
{
return toAjax(inspectionTaskService.deleteInspectionTaskByIds(ids));
}
/**
* 查询任务关联的点位列表
*/
@ApiOperation("查询任务关联的点位列表")
@PreAuthorize("@ss.hasPermi('inspection:task:query')")
@GetMapping("/{taskId}/waypoints")
public AjaxResult getWaypoints(@PathVariable("taskId") String taskId)
{
List<InspectionWaypoint> list = inspectionTaskService.selectWaypointsByTaskId(taskId);
return success(list);
}
/**
* 绑定点位到任务
*/
@ApiOperation("绑定点位到任务")
@PreAuthorize("@ss.hasPermi('inspection:task:edit')")
@Log(title = "巡检任务", businessType = BusinessType.UPDATE)
@PostMapping("/{taskId}/waypoints")
public AjaxResult bindWaypoints(@PathVariable("taskId") String taskId, @RequestBody String[] waypointIds)
{
return toAjax(inspectionTaskService.bindWaypoints(taskId, waypointIds));
}
/**
* 解绑任务的点位
*/
@ApiOperation("解绑任务的点位")
@PreAuthorize("@ss.hasPermi('inspection:task:edit')")
@Log(title = "巡检任务", businessType = BusinessType.UPDATE)
@DeleteMapping("/{taskId}/waypoints")
public AjaxResult unbindWaypoints(@PathVariable("taskId") String taskId)
{
return toAjax(inspectionTaskService.unbindWaypoints(taskId));
}
}

View File

@ -0,0 +1,164 @@
package com.cmvr.web.controller.inspection;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import com.cmvr.inspection.domain.dto.InspectionTaskInstanceQuery;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.annotation.Log;
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.InspectionTaskInstanceVo;
import com.cmvr.inspection.service.IInspectionTaskInstanceService;
import com.cmvr.common.utils.poi.ExcelUtil;
import com.cmvr.common.core.page.TableDataInfo;
/**
* 巡检任务执行实例Controller
*
* @author cmvr-iot
* @since 2026-06-01
*/
@RestController
@RequestMapping("/inspection/taskInstance")
@Api(tags = "智能巡检--任务执行实例管理")
public class InspectionTaskInstanceController extends BaseController
{
@Autowired
private IInspectionTaskInstanceService inspectionTaskInstanceService;
/**
* 查询巡检任务执行实例列表
*/
@ApiOperation("查询巡检任务执行实例列表")
@PreAuthorize("@ss.hasPermi('inspection:taskInstance:list')")
@GetMapping("/list")
public TableDataInfo list(InspectionTaskInstanceQuery inspectionTaskInstanceQuery)
{
startPage();
List<InspectionTaskInstanceVo> list = inspectionTaskInstanceService.selectInspectionTaskInstanceList(inspectionTaskInstanceQuery);
return getDataTable(list);
}
/**
* 导出巡检任务执行实例列表
*/
@ApiOperation("导出巡检任务执行实例列表")
@PreAuthorize("@ss.hasPermi('inspection:taskInstance:export')")
@Log(title = "巡检任务执行实例", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, InspectionTaskInstanceQuery inspectionTaskInstanceQuery)
{
List<InspectionTaskInstanceVo> list = inspectionTaskInstanceService.selectInspectionTaskInstanceList(inspectionTaskInstanceQuery);
ExcelUtil<InspectionTaskInstanceVo> util = new ExcelUtil<>(InspectionTaskInstanceVo.class);
util.exportExcel(response, list, "巡检任务执行实例数据");
}
/**
* 获取巡检任务执行实例详细信息
*/
@ApiOperation("获取巡检任务执行实例详细信息")
@PreAuthorize("@ss.hasPermi('inspection:taskInstance:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") String id)
{
return success(inspectionTaskInstanceService.selectInspectionTaskInstanceVoById(id));
}
/**
* 新增巡检任务执行实例
*/
@ApiOperation("新增巡检任务执行实例")
@PreAuthorize("@ss.hasPermi('inspection:taskInstance:add')")
@Log(title = "巡检任务执行实例", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody InspectionTaskInstance inspectionTaskInstance)
{
return toAjax(inspectionTaskInstanceService.insertInspectionTaskInstance(inspectionTaskInstance));
}
/**
* 修改巡检任务执行实例
*/
@ApiOperation("修改巡检任务执行实例")
@PreAuthorize("@ss.hasPermi('inspection:taskInstance:edit')")
@Log(title = "巡检任务执行实例", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody InspectionTaskInstance inspectionTaskInstance)
{
return toAjax(inspectionTaskInstanceService.updateInspectionTaskInstance(inspectionTaskInstance));
}
/**
* 删除巡检任务执行实例
*/
@ApiOperation("删除巡检任务执行实例")
@PreAuthorize("@ss.hasPermi('inspection:taskInstance:remove')")
@Log(title = "巡检任务执行实例", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable String[] ids)
{
return toAjax(inspectionTaskInstanceService.deleteInspectionTaskInstanceByIds(ids));
}
/**
* 开始执行任务实例
*/
@ApiOperation("开始执行任务实例")
@PreAuthorize("@ss.hasPermi('inspection:taskInstance:edit')")
@Log(title = "巡检任务执行实例", businessType = BusinessType.UPDATE)
@PutMapping("/start/{id}")
public AjaxResult start(@PathVariable("id") String id)
{
return toAjax(inspectionTaskInstanceService.startInstance(id));
}
/**
* 暂停任务实例
*/
@ApiOperation("暂停任务实例")
@PreAuthorize("@ss.hasPermi('inspection:taskInstance:edit')")
@Log(title = "巡检任务执行实例", businessType = BusinessType.UPDATE)
@PutMapping("/pause/{id}")
public AjaxResult pause(@PathVariable("id") String id)
{
return toAjax(inspectionTaskInstanceService.pauseInstance(id));
}
/**
* 终止任务实例
*/
@ApiOperation("终止任务实例")
@PreAuthorize("@ss.hasPermi('inspection:taskInstance:edit')")
@Log(title = "巡检任务执行实例", businessType = BusinessType.UPDATE)
@PutMapping("/stop/{id}")
public AjaxResult stop(@PathVariable("id") String id)
{
return toAjax(inspectionTaskInstanceService.stopInstance(id));
}
/**
* 恢复任务实例
*/
@ApiOperation("恢复任务实例")
@PreAuthorize("@ss.hasPermi('inspection:taskInstance:edit')")
@Log(title = "巡检任务执行实例", businessType = BusinessType.UPDATE)
@PutMapping("/resume/{id}")
public AjaxResult resume(@PathVariable("id") String id)
{
return toAjax(inspectionTaskInstanceService.resumeInstance(id));
}
}

View File

@ -0,0 +1,84 @@
package com.cmvr.web.controller.inspection;
import com.cmvr.common.core.controller.BaseController;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.common.core.page.TableDataInfo;
import com.cmvr.inspection.domain.InspectionTaskLog;
import com.cmvr.inspection.service.IInspectionTaskLogService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 巡检任务执行日志Controller
*
* @author cmvr-iot
* @since 2026-06-05
*/
@Api(tags = "智能巡检-巡检任务执行日志管理")
@RestController
@RequestMapping("/inspection/taskLog")
public class InspectionTaskLogController extends BaseController
{
@Autowired
private IInspectionTaskLogService inspectionTaskLogService;
/**
* 查询巡检任务执行日志列表
*/
@ApiOperation("查询巡检任务执行日志列表")
@GetMapping("/list")
public TableDataInfo list(@ApiParam("任务实例ID") @RequestParam String taskInstanceId) {
List<InspectionTaskLog> list = inspectionTaskLogService.selectInspectionTaskLogList(taskInstanceId);
return getDataTable(list);
}
/**
* 获取巡检任务执行日志详细信息
*/
@ApiOperation("获取巡检任务执行日志详细信息")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@ApiParam("日志ID") @PathVariable("id") String id) {
return success(inspectionTaskLogService.getById(id));
}
/**
* 新增巡检任务执行日志
*/
@ApiOperation("新增巡检任务执行日志")
@PostMapping
public AjaxResult add(@RequestBody InspectionTaskLog inspectionTaskLog) {
return toAjax(inspectionTaskLogService.insertInspectionTaskLog(inspectionTaskLog));
}
/**
* 批量新增巡检任务执行日志
*/
@ApiOperation("批量新增巡检任务执行日志")
@PostMapping("/batch")
public AjaxResult batchAdd(@RequestBody List<InspectionTaskLog> logList) {
return toAjax(inspectionTaskLogService.batchInsertInspectionTaskLog(logList));
}
/**
* 修改巡检任务执行日志
*/
@ApiOperation("修改巡检任务执行日志")
@PutMapping
public AjaxResult edit(@RequestBody InspectionTaskLog inspectionTaskLog) {
return toAjax(inspectionTaskLogService.updateById(inspectionTaskLog));
}
/**
* 删除巡检任务执行日志
*/
@ApiOperation("删除巡检任务执行日志")
@DeleteMapping("/{ids}")
public AjaxResult remove(@ApiParam("日志ID数组") @PathVariable String[] ids) {
return toAjax(inspectionTaskLogService.removeByIds(java.util.Arrays.asList(ids)));
}
}

View File

@ -0,0 +1,115 @@
package com.cmvr.web.controller.inspection;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
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.annotation.Log;
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.InspectionWaypoint;
import com.cmvr.inspection.domain.vo.InspectionWaypointVo;
import com.cmvr.inspection.service.IInspectionWaypointService;
import com.cmvr.common.utils.poi.ExcelUtil;
import com.cmvr.common.core.page.TableDataInfo;
/**
* 巡检点位Controller
*
* @author cmvr-iot
* @since 2026-05-29
*/
@RestController
@RequestMapping("/inspection/waypoint")
@Api(tags = "智能巡检--点位管理")
public class InspectionWaypointController extends BaseController
{
@Autowired
private IInspectionWaypointService inspectionWaypointService;
/**
* 查询巡检点位列表
*/
@ApiOperation("查询巡检点位列表")
@PreAuthorize("@ss.hasPermi('inspection:waypoint:list')")
@GetMapping("/list")
public TableDataInfo list(InspectionWaypoint inspectionWaypoint)
{
startPage();
List<InspectionWaypointVo> list = inspectionWaypointService.selectInspectionWaypointList(inspectionWaypoint);
return getDataTable(list);
}
/**
* 导出巡检点位列表
*/
@ApiOperation("导出巡检点位列表")
@PreAuthorize("@ss.hasPermi('inspection:waypoint:export')")
@Log(title = "巡检点位", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, InspectionWaypoint inspectionWaypoint)
{
List<InspectionWaypointVo> list = inspectionWaypointService.selectInspectionWaypointList(inspectionWaypoint);
ExcelUtil<InspectionWaypointVo> util = new ExcelUtil<>(InspectionWaypointVo.class);
util.exportExcel(response, list, "巡检点位数据");
}
/**
* 获取巡检点位详细信息
*/
@ApiOperation("获取巡检点位详细信息")
@PreAuthorize("@ss.hasPermi('inspection:waypoint:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") String id)
{
return success(inspectionWaypointService.selectInspectionWaypointById(id));
}
/**
* 新增巡检点位
*/
@ApiOperation("新增巡检点位")
@PreAuthorize("@ss.hasPermi('inspection:waypoint:add')")
@Log(title = "巡检点位", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody InspectionWaypoint inspectionWaypoint)
{
return toAjax(inspectionWaypointService.insertInspectionWaypoint(inspectionWaypoint));
}
/**
* 修改巡检点位
*/
@ApiOperation("修改巡检点位")
@PreAuthorize("@ss.hasPermi('inspection:waypoint:edit')")
@Log(title = "巡检点位", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody InspectionWaypoint inspectionWaypoint)
{
return toAjax(inspectionWaypointService.updateInspectionWaypoint(inspectionWaypoint));
}
/**
* 删除巡检点位
*/
@ApiOperation("删除巡检点位")
@PreAuthorize("@ss.hasPermi('inspection:waypoint:remove')")
@Log(title = "巡检点位", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable String[] ids)
{
return toAjax(inspectionWaypointService.deleteInspectionWaypointByIds(ids));
}
}

View File

@ -1,7 +1,12 @@
package com.cmvr.web.controller.test; package com.cmvr.web.controller.test;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.alibaba.fastjson2.JSONObject;
import com.cmvr.common.annotation.Anonymous;
import com.cmvr.common.core.controller.BaseController; import com.cmvr.common.core.controller.BaseController;
import com.cmvr.common.core.domain.AjaxResult; import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.llm.service.LLMAiAgentPlatformService;
import com.cmvr.test.flow.context.TaskContextManager; import com.cmvr.test.flow.context.TaskContextManager;
import com.cmvr.test.flow.control.FlowControlService; import com.cmvr.test.flow.control.FlowControlService;
import com.cmvr.test.flow.runtime.engine.FlowTaskRuntimeService; import com.cmvr.test.flow.runtime.engine.FlowTaskRuntimeService;
@ -25,7 +30,15 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PreDestroy;
import javax.validation.Valid; import javax.validation.Valid;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@Api(tags = "测试--流程服务") @Api(tags = "测试--流程服务")
@RestController @RestController
@ -33,6 +46,21 @@ import javax.validation.Valid;
@RequiredArgsConstructor @RequiredArgsConstructor
public class TeFlowController extends BaseController { public class TeFlowController extends BaseController {
/**
* TTS 播放接口当前按需求固定写死在 Controller
*/
private static final String TTS_PLAY_URL = "http://localhost:8080/tts/play";
/**
* TTS 停止接口停止智能体输出时同步调用
*/
private static final String TTS_STOP_URL = "http://localhost:8080/tts/stop";
/**
* 商道智能体 AppKey当前按需求固定写死不放入配置文件
*/
private static final String AI_AGENT_API_KEY = "d9etum54shheenol3apg";
private final ITeDetectionItemService teDetectionItemService; private final ITeDetectionItemService teDetectionItemService;
private final ITeNodeInstService nodeInstService; private final ITeNodeInstService nodeInstService;
private final FlowTaskRuntimeService flowTaskRuntimeService; private final FlowTaskRuntimeService flowTaskRuntimeService;
@ -40,6 +68,22 @@ public class TeFlowController extends BaseController {
private final FlowControlService flowControlService; private final FlowControlService flowControlService;
private final FlowActionExecutorService flowActionExecutorService; private final FlowActionExecutorService flowActionExecutorService;
private final TiTouchOperateService tiTouchOperateService; private final TiTouchOperateService tiTouchOperateService;
private final LLMAiAgentPlatformService llmAiAgentPlatformService;
private final List<String> taskInstIdList = new ArrayList<>();
/**
* 智能体调用是阻塞式 query这里用单线程保存当前任务句柄便于 stop 接口中断当前请求
*/
private final ExecutorService aiAgentExecutor = Executors.newSingleThreadExecutor(runnable -> {
Thread thread = new Thread(runnable, "flow-ai-agent-query");
thread.setDaemon(true);
return thread;
});
/**
* 当前正在执行的智能体任务当前接口按同一时间只保留一个前端智能体调用处理
*/
private volatile Future<JSONObject> currentAiAgentFuture;
@ApiOperation("流程发布") @ApiOperation("流程发布")
@PostMapping("/publish") @PostMapping("/publish")
@ -47,8 +91,26 @@ public class TeFlowController extends BaseController {
return toAjax(teDetectionItemService.publish(detectionItem.getId())); return toAjax(teDetectionItemService.publish(detectionItem.getId()));
} }
@ApiOperation("演示任务执行入口")
@PostMapping("/demo")
@Anonymous
public AjaxResult demo(@Valid @RequestBody TeTaskExecuteNormalVO taskExecuteNormalVO) {
if (!taskInstIdList.isEmpty()) {
try {
taskInstIdList.forEach(flowControlService::stop);
} catch (Exception e) {
// 演示入口清理旧任务失败时继续启动新任务避免影响手动调试
}
taskInstIdList.clear();
}
String insId = flowTaskRuntimeService.executeTask(taskExecuteNormalVO);
taskInstIdList.add(insId);
return AjaxResult.success();
}
@ApiOperation("任务执行入口") @ApiOperation("任务执行入口")
@PostMapping("/execute") @PostMapping("/execute")
@Anonymous
public AjaxResult execute(@Valid @RequestBody TeTaskExecuteNormalVO taskExecuteNormalVO) { public AjaxResult execute(@Valid @RequestBody TeTaskExecuteNormalVO taskExecuteNormalVO) {
return AjaxResult.success(flowTaskRuntimeService.executeTask(taskExecuteNormalVO)); return AjaxResult.success(flowTaskRuntimeService.executeTask(taskExecuteNormalVO));
} }
@ -94,15 +156,120 @@ public class TeFlowController extends BaseController {
} }
@PostMapping("/action") @PostMapping("/action")
@ApiOperation("单个节点执行") @ApiOperation(value = "单个节点执行",
public AjaxResult actionExecute(@RequestBody FlowActionRequestVO flowActionRequestVO) { notes = "巡检仪表节点action=INSPECTION_METER_RECOGNIZEpayload使用InspectionMeterRecognizeConfigVO"
+ "人工判断节点action=INSPECTION_MANUAL_REVIEW_CREATEpayload使用InspectionManualReviewConfigVO。"
+ "该接口用于试调节点,不会生成正式巡检任务结果")
public AjaxResult actionExecute(
@io.swagger.annotations.ApiParam(value = "单节点动作和参数", required = true)
@RequestBody FlowActionRequestVO flowActionRequestVO) {
return AjaxResult.ok(flowActionExecutorService.actionExecute(flowActionRequestVO)); return AjaxResult.ok(flowActionExecutorService.actionExecute(flowActionRequestVO));
} }
/**
* 调用商道智能体平台
*
* <p>前端只需要传入 text后端固定 actionapiKey TTS 参数
* 调用会放入可取消任务中前端可以通过 /flow/llm/stop 停止当前调用并停止 TTS</p>
*/
@GetMapping("/llm/query")
@ApiOperation("调用商道智能体")
public AjaxResult queryAiAgent(String text) {
if (text == null || text.trim().isEmpty()) {
return AjaxResult.error("text不能为空");
}
stopCurrentAiAgentFuture(true);
JSONObject params = new JSONObject();
params.put("url", TTS_PLAY_URL);
params.put("voice", "x4_yezi");
params.put("speed", 45);
params.put("volume", 100);
Future<JSONObject> future = aiAgentExecutor.submit(() -> llmAiAgentPlatformService.query(
null,
text.trim(),
AI_AGENT_API_KEY,
true,
params
));
currentAiAgentFuture = future;
try {
return AjaxResult.ok(future.get());
} catch (CancellationException e) {
return AjaxResult.error("智能体调用已停止");
} catch (InterruptedException e) {
future.cancel(true);
Thread.currentThread().interrupt();
return AjaxResult.error("智能体调用被中断");
} catch (ExecutionException e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
return AjaxResult.error("智能体调用失败:" + cause.getMessage());
} finally {
if (currentAiAgentFuture == future) {
currentAiAgentFuture = null;
}
}
}
/**
* 停止当前智能体调用并通知 TTS 服务停止播放
*
* <p>停止分两步先取消当前 query 任务触发底层 SSE 等待线程中断
* 再调用本地 TTS stop 接口停止已经进入播放队列的音频</p>
*/
@PostMapping("/llm/stop")
@ApiOperation("停止商道智能体和TTS")
public AjaxResult stopAiAgent() {
stopCurrentAiAgentFuture(false);
// String ttsStopResult = stopTts();
return AjaxResult.ok("ok");
}
@GetMapping("/testrun") @GetMapping("/testrun")
@ApiOperation("测试接口") @ApiOperation("测试接口")
public AjaxResult testrun(@RequestParam("imageUrl") String imageUrl, public AjaxResult testrun(@RequestParam("imageUrl") String imageUrl,
@RequestParam("iconUrl") String iconUrl) { @RequestParam("iconUrl") String iconUrl) {
return AjaxResult.ok(tiTouchOperateService.test(imageUrl, iconUrl)); return AjaxResult.ok(tiTouchOperateService.test(imageUrl, iconUrl));
} }
/**
* 取消当前智能体任务
*
* @param stopTts 是否同时停止 TTS 播放
*/
private void stopCurrentAiAgentFuture(boolean stopTts) {
Future<JSONObject> future = currentAiAgentFuture;
if (future != null && !future.isDone()) {
future.cancel(true);
}
currentAiAgentFuture = null;
if (stopTts) {
stopTts();
}
}
/**
* 调用本地 TTS 停止接口
*/
private String stopTts() {
try (HttpResponse response = HttpRequest.post(TTS_STOP_URL)
.timeout(3000)
.execute()) {
return response.body();
} catch (Exception e) {
return "调用TTS停止接口失败" + e.getMessage();
}
}
/**
* Spring 容器关闭时释放智能体执行线程
*/
@PreDestroy
public void destroy() {
stopCurrentAiAgentFuture(true);
aiAgentExecutor.shutdownNow();
}
} }

View File

@ -1,14 +1,22 @@
package com.cmvr.web.controller.test; package com.cmvr.web.controller.test;
import com.alibaba.fastjson2.JSONObject;
import com.cmvr.common.core.controller.BaseController; import com.cmvr.common.core.controller.BaseController;
import com.cmvr.common.core.domain.AjaxResult; import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.test.flow.control.FlowControlService;
import com.cmvr.test.flow.runtime.engine.FlowTaskRuntimeService;
import com.cmvr.test.flow.runtime.operator.edge.ti.TiTouchOperateService;
import com.cmvr.test.model.vo.FlowiseActionRequestVO; import com.cmvr.test.model.vo.FlowiseActionRequestVO;
import com.cmvr.test.model.vo.FlowiseChatRequestVO; import com.cmvr.test.model.vo.FlowiseChatRequestVO;
import com.cmvr.test.model.vo.FlowiseStartRequestVO; import com.cmvr.test.model.vo.FlowiseStartRequestVO;
import com.cmvr.test.model.vo.TeTaskExecuteNormalVO;
import com.cmvr.test.service.FlowActionExecutorService;
import com.cmvr.test.service.FlowiseActionService; import com.cmvr.test.service.FlowiseActionService;
import com.google.gson.JsonObject;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@ -16,6 +24,9 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@Api(tags = "测试--flowise服务") @Api(tags = "测试--flowise服务")
@RestController @RestController
@RequestMapping("/flowise") @RequestMapping("/flowise")
@ -23,11 +34,37 @@ import org.springframework.web.bind.annotation.RestController;
public class TeFlowiseController extends BaseController { public class TeFlowiseController extends BaseController {
private final FlowiseActionService flowiseActionService; private final FlowiseActionService flowiseActionService;
private final FlowTaskRuntimeService flowTaskRuntimeService;
private final FlowControlService flowControlService;
private final FlowActionExecutorService flowActionExecutorService;
private final TiTouchOperateService tiTouchOperateService;
private final List<String> taskInstIdList = new ArrayList<>();
@ApiOperation("调用flowise") @ApiOperation("调用flowise")
@PostMapping("/start") @PostMapping("/start")
public AjaxResult start(@RequestBody FlowiseStartRequestVO request) { public AjaxResult start(@RequestBody FlowiseStartRequestVO request) {
return AjaxResult.ok(flowiseActionService.start(request)); JSONObject runParams = new JSONObject();
JSONObject runParams1 = new JSONObject();
runParams1
.put("test", request.getQuestion());
runParams.put("5bb26d52e1861c6936f3c3149eaf17e8", runParams1);
TeTaskExecuteNormalVO taskExecuteNormalVO = TeTaskExecuteNormalVO.builder()
.taskId("b6ab5ffff783d9b25d0dc34f13be222d")
.terminalId("4ed1246c465b97975f96c9ef8371a3bd")
.runParams(JSONObject.from(runParams)).build();
if (!taskInstIdList.isEmpty()) {
try {
taskInstIdList.forEach(flowControlService::stop);
} catch (Exception e) {
// 忽略
}
taskInstIdList.clear();
}
String insId = flowTaskRuntimeService.executeTask(taskExecuteNormalVO);
taskInstIdList.add(insId);
return AjaxResult.success();
// return AjaxResult.ok(flowiseActionService.start(request));
} }
@ApiOperation("根据chatId查询执行结果") @ApiOperation("根据chatId查询执行结果")
@ -39,7 +76,17 @@ public class TeFlowiseController extends BaseController {
@ApiOperation("终止flowise") @ApiOperation("终止flowise")
@GetMapping("/abort") @GetMapping("/abort")
public AjaxResult abort() { public AjaxResult abort() {
return AjaxResult.ok(flowiseActionService.abort()); if (!taskInstIdList.isEmpty()) {
try {
taskInstIdList.forEach(flowControlService::stop);
} catch (Exception e) {
// 忽略
}
taskInstIdList.clear();
}
logger.info("取消工作流");
return AjaxResult.success();
// return AjaxResult.ok(flowiseActionService.abort());
} }
@ApiOperation("执行指令") @ApiOperation("执行指令")

View File

@ -0,0 +1,214 @@
package com.cmvr.web.controller.tts;
import com.cmvr.common.annotation.Log;
import com.cmvr.common.core.controller.BaseController;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.common.core.page.TableDataInfo;
import com.cmvr.common.enums.BusinessType;
import com.cmvr.tts.domain.CorpusCategory;
import com.cmvr.tts.domain.CorpusInfo;
import com.cmvr.tts.service.ICorpusInfoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
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 java.util.Map;
/**
* 车载语料数据Controller
*
* @author cmvr
*/
@RestController
@RequestMapping("/tts/corpus")
@Api(tags = "智能座舱-车载语料管理")
public class CorpusInfoController extends BaseController {
@Autowired
private ICorpusInfoService corpusInfoService;
/**
* 查询语料列表
*/
@ApiOperation("查询语料列表")
@PreAuthorize("@ss.hasPermi('tts:corpus:list')")
@GetMapping("/list")
public TableDataInfo list(CorpusInfo corpus) {
startPage();
return corpusInfoService.selectCorpusList(corpus);
}
/**
* 获取语料详细信息
*/
@ApiOperation("获取语料详细信息")
@PreAuthorize("@ss.hasPermi('tts:corpus:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") String id) {
return success(corpusInfoService.selectCorpusById(id));
}
/**
* 新增语料
*/
@ApiOperation("新增语料")
@PreAuthorize("@ss.hasPermi('tts:corpus:add')")
@Log(title = "车载语料", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody CorpusInfo corpus) {
return toAjax(corpusInfoService.insertCorpus(corpus));
}
/**
* 修改语料
*/
@ApiOperation("修改语料")
@PreAuthorize("@ss.hasPermi('tts:corpus:edit')")
@Log(title = "车载语料", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody CorpusInfo corpus) {
return toAjax(corpusInfoService.updateCorpus(corpus));
}
/**
* 删除语料
*/
@ApiOperation("删除语料")
@PreAuthorize("@ss.hasPermi('tts:corpus:remove')")
@Log(title = "车载语料", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable String[] ids) {
return toAjax(corpusInfoService.deleteCorpusByIds(ids));
}
/**
* 获取音频流
*/
@ApiOperation("获取音频流")
@PreAuthorize("@ss.hasPermi('tts:corpus:query')")
@GetMapping("/audio/{id}")
public void getAudio(@PathVariable String id, HttpServletResponse response) {
corpusInfoService.getAudioStream(id, response);
}
/**
* 上传语料音频
*/
@ApiOperation("上传语料音频")
@PreAuthorize("@ss.hasPermi('tts:corpus:edit')")
@Log(title = "车载语料", businessType = BusinessType.UPDATE)
@PostMapping("/audio/upload/{id}")
public AjaxResult uploadAudio(@PathVariable String id, @RequestParam("file") MultipartFile file) {
String path = corpusInfoService.uploadCorpusAudio(id, file);
return success(path);
}
/**
* 批量导入语料
*/
@ApiOperation("批量导入语料")
@PreAuthorize("@ss.hasPermi('tts:corpus:import')")
@Log(title = "车载语料", businessType = BusinessType.IMPORT)
@PostMapping("/import")
public AjaxResult importData(@RequestParam("file") MultipartFile file) {
Map<String, Object> result = corpusInfoService.importCorpus(file);
return success(result);
}
/**
* 查询看板统计数据
*/
@ApiOperation("查询看板统计数据")
@PreAuthorize("@ss.hasPermi('tts:corpus:query')")
@GetMapping("/dashboard/stats")
public AjaxResult getDashboardStats() {
return success(corpusInfoService.getDashboardStats());
}
/**
* 按语种统计
*/
@ApiOperation("按语种统计")
@PreAuthorize("@ss.hasPermi('tts:corpus:query')")
@GetMapping("/stats/language")
public AjaxResult getStatsByLanguage() {
return success(corpusInfoService.getStatsByLanguage());
}
/**
* 按方言统计
*/
@ApiOperation("按方言统计")
@PreAuthorize("@ss.hasPermi('tts:corpus:query')")
@GetMapping("/stats/dialect")
public AjaxResult getStatsByDialect() {
return success(corpusInfoService.getStatsByDialect());
}
/**
* 按情绪统计
*/
@ApiOperation("按情绪统计")
@PreAuthorize("@ss.hasPermi('tts:corpus:query')")
@GetMapping("/stats/emotion")
public AjaxResult getStatsByEmotion() {
return success(corpusInfoService.getStatsByEmotion());
}
/**
* 按分类统计
*/
@ApiOperation("按分类统计")
@PreAuthorize("@ss.hasPermi('tts:corpus:query')")
@GetMapping("/stats/category")
public AjaxResult getStatsByCategory() {
return success(corpusInfoService.getStatsByCategory());
}
/**
* 查询分类树
*/
@ApiOperation("查询分类树")
@PreAuthorize("@ss.hasPermi('tts:corpus:query')")
@GetMapping("/category/tree")
public AjaxResult getCategoryTree() {
return success(corpusInfoService.getCategoryTree());
}
/**
* 新增分类
*/
@ApiOperation("新增分类")
@PreAuthorize("@ss.hasPermi('tts:corpus:add')")
@Log(title = "语料分类", businessType = BusinessType.INSERT)
@PostMapping("/category")
public AjaxResult addCategory(@RequestBody CorpusCategory category) {
return toAjax(corpusInfoService.insertCategory(category));
}
/**
* 修改分类
*/
@ApiOperation("修改分类")
@PreAuthorize("@ss.hasPermi('tts:corpus:edit')")
@Log(title = "语料分类", businessType = BusinessType.UPDATE)
@PutMapping("/category")
public AjaxResult editCategory(@RequestBody CorpusCategory category) {
return toAjax(corpusInfoService.updateCategory(category));
}
/**
* 删除分类
*/
@ApiOperation("删除分类")
@PreAuthorize("@ss.hasPermi('tts:corpus:remove')")
@Log(title = "语料分类", businessType = BusinessType.DELETE)
@DeleteMapping("/category/{id}")
public AjaxResult removeCategory(@PathVariable String id) {
return toAjax(corpusInfoService.deleteCategoryById(id));
}
}

View File

@ -0,0 +1,253 @@
package com.cmvr.web.controller.tts;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.cmvr.common.annotation.Log;
import com.cmvr.common.core.controller.BaseController;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.common.core.page.TableDataInfo;
import com.cmvr.common.enums.BusinessType;
import com.cmvr.llm.service.LLMAiAgentPlatformService;
import com.cmvr.tts.domain.TtsSynthesizeTask;
import com.cmvr.tts.domain.vo.LlmQueryVo;
import com.cmvr.tts.service.ITtsSynthesizeTaskService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
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 java.util.List;
/**
* TTS合成任务Controller
*
* @author cmvr
*/
@RestController
@RequestMapping("/tts/task")
@Api(tags = "智能座舱-TTS合成任务管理")
@RequiredArgsConstructor
public class TtsSynthesizeTaskController extends BaseController {
@Autowired
private ITtsSynthesizeTaskService ttsTaskService;
private final LLMAiAgentPlatformService llmAiAgentPlatformService;
/**
* 查询TTS合成任务列表
*/
@ApiOperation("查询TTS合成任务列表")
@PreAuthorize("@ss.hasPermi('tts:task:list')")
@GetMapping("/list")
public TableDataInfo list(TtsSynthesizeTask task) {
startPage();
return ttsTaskService.selectTtsTaskList(task);
}
/**
* 获取TTS合成任务详细信息
*/
@ApiOperation("获取TTS合成任务详细信息")
@PreAuthorize("@ss.hasPermi('tts:task:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") String id) {
return success(ttsTaskService.selectTtsTaskById(id));
}
/**
* 新增TTS合成任务
*/
@ApiOperation("新增TTS合成任务")
@PreAuthorize("@ss.hasPermi('tts:task:add')")
@Log(title = "TTS合成任务", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody TtsSynthesizeTask task) {
return success(ttsTaskService.insertTtsTask(task));
}
/**
* 批量新增TTS合成任务
*/
@ApiOperation("批量新增TTS合成任务")
@PreAuthorize("@ss.hasPermi('tts:task:add')")
@Log(title = "TTS合成任务", businessType = BusinessType.INSERT)
@PostMapping("/batch")
public AjaxResult batchAdd(@RequestBody List<TtsSynthesizeTask> tasks) {
return toAjax(ttsTaskService.batchInsertTtsTask(tasks));
}
/**
* 修改TTS合成任务
*/
@ApiOperation("修改TTS合成任务")
@PreAuthorize("@ss.hasPermi('tts:task:edit')")
@Log(title = "TTS合成任务", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody TtsSynthesizeTask task) {
return toAjax(ttsTaskService.updateTtsTask(task));
}
/**
* 删除TTS合成任务
*/
@ApiOperation("删除TTS合成任务")
@PreAuthorize("@ss.hasPermi('tts:task:remove')")
@Log(title = "TTS合成任务", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable String[] ids) {
return toAjax(ttsTaskService.deleteTtsTaskByIds(ids));
}
/**
* 重生成TTS任务
*/
@ApiOperation("重生成TTS任务")
@PreAuthorize("@ss.hasPermi('tts:task:edit')")
@Log(title = "TTS合成任务", businessType = BusinessType.UPDATE)
@PostMapping("/regenerate/{id}")
public AjaxResult regenerate(@PathVariable String id) {
return toAjax(ttsTaskService.regenerateTask(id));
}
/**
* 获取音频文件流
*/
@ApiOperation("获取音频文件流")
@PreAuthorize("@ss.hasPermi('tts:task:query')")
@GetMapping("/audio/{id}")
public void getAudio(@PathVariable String id, HttpServletResponse response) {
ttsTaskService.getAudioStream(id, response);
}
/**
* 查询看板统计数据
*/
@ApiOperation("查询看板统计数据")
@PreAuthorize("@ss.hasPermi('tts:task:query')")
@GetMapping("/dashboard/stats")
public AjaxResult getDashboardStats() {
return success(ttsTaskService.getDashboardStats());
}
/**
* 按语种统计
*/
@ApiOperation("按语种统计")
@PreAuthorize("@ss.hasPermi('tts:task:query')")
@GetMapping("/stats/language")
public AjaxResult getStatsByLanguage() {
return success(ttsTaskService.getStatsByLanguage());
}
/**
* 按音色统计
*/
@ApiOperation("按音色统计")
@PreAuthorize("@ss.hasPermi('tts:task:query')")
@GetMapping("/stats/voice")
public AjaxResult getStatsByVoice() {
return success(ttsTaskService.getStatsByVoice());
}
/**
* 按情绪统计
*/
@ApiOperation("按情绪统计")
@PreAuthorize("@ss.hasPermi('tts:task:query')")
@GetMapping("/stats/emotion")
public AjaxResult getStatsByEmotion() {
return success(ttsTaskService.getStatsByEmotion());
}
/**
* 近7日趋势
*/
@ApiOperation("近7日趋势")
@PreAuthorize("@ss.hasPermi('tts:task:query')")
@GetMapping("/stats/trend")
public AjaxResult getLast7DaysTrend() {
return success(ttsTaskService.getLast7DaysTrend());
}
/**
* 文本泛化
*/
@ApiOperation("文本泛化")
@PreAuthorize("@ss.hasPermi('tts:task:query')")
@PostMapping("/llm/query")
public AjaxResult llmQuery(@RequestBody LlmQueryVo request) {
// 闲聊key d9672ql4shh4136opsfg
// 泛化key d966pq54shh4nveh8d20
JSONObject result = llmAiAgentPlatformService.query(
null,
request.getLanguage() + "," + request.getCount() + "," + request.getText(),
"d96ammd4shh4nvehb6mg",
false,
null
);
// 解析LLM返回的结果提取JSON数组
String resultStr = result.getString("result");
if (resultStr != null && !resultStr.isEmpty()) {
// 尝试多种格式解析
JSONArray jsonArray = parseJsonArray(resultStr);
return success(jsonArray);
}
return success(new JSONArray());
}
/**
* 解析JSON数组兼容多种格式
*/
private JSONArray parseJsonArray(String text) {
if (text == null || text.isEmpty()) {
return new JSONArray();
}
// 1. 直接尝试解析为JSON数组
try {
return JSONArray.parseArray(text);
} catch (Exception e) {
// 继续尝试其他格式
}
// 2. 移除markdown代码块标记
String cleaned = text.replace("```json", "").replace("```", "").trim();
try {
return JSONArray.parseArray(cleaned);
} catch (Exception e) {
// 继续尝试其他格式
}
// 3. 尝试从文本中提取JSON数组匹配 [...] 格式
int startIdx = text.indexOf("[");
int endIdx = text.lastIndexOf("]");
if (startIdx != -1 && endIdx != -1 && endIdx > startIdx) {
String jsonStr = text.substring(startIdx, endIdx + 1);
try {
return JSONArray.parseArray(jsonStr);
} catch (Exception e) {
// 继续尝试其他格式
}
}
// 4. 如果都失败返回包含原始文本的数组
return new JSONArray().fluentAdd(text);
}
/**
* 保存任务到语料库
*/
@ApiOperation("保存任务到语料库")
@PreAuthorize("@ss.hasPermi('tts:task:edit')")
@Log(title = "TTS合成任务", businessType = BusinessType.INSERT)
@PostMapping("/Nsave-to-corpus/{taskId}/{categoryId}")
public AjaxResult saveToCorpus(@PathVariable String taskId, @PathVariable String categoryId) {
return toAjax(ttsTaskService.saveToCorpus(taskId, categoryId));
}
}

View File

@ -7,6 +7,11 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import com.cmvr.common.config.CmvrIotConfig; 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.annotations.ApiOperation;
import io.swagger.models.auth.In; import io.swagger.models.auth.In;
import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.ApiInfoBuilder;
@ -34,6 +39,10 @@ public class SwaggerConfig
@Autowired @Autowired
private CmvrIotConfig cmvrIotConfig; private CmvrIotConfig cmvrIotConfig;
/** 用于将动态工作流payload对应的参数模型显式加入Swagger文档。 */
@Autowired
private TypeResolver typeResolver;
/** 是否开启swagger */ /** 是否开启swagger */
@Value("${swagger.enabled}") @Value("${swagger.enabled}")
private boolean enabled; private boolean enabled;
@ -62,6 +71,12 @@ public class SwaggerConfig
// 扫描所有 .apis(RequestHandlerSelectors.any()) // 扫描所有 .apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any()) .paths(PathSelectors.any())
.build() .build()
// 工作流单节点接口使用JSONObject作为payload需显式注册巡检节点参数模型
.additionalModels(
typeResolver.resolve(InspectionMeterRecognizeConfigVO.class),
typeResolver.resolve(InspectionAlarmRuleVO.class),
typeResolver.resolve(InspectionManualReviewConfigVO.class),
typeResolver.resolve(InspectionMediaVO.class))
/* 设置安全模式swagger可以设置访问token */ /* 设置安全模式swagger可以设置访问token */
.securitySchemes(securitySchemes()) .securitySchemes(securitySchemes())
.securityContexts(securityContexts()) .securityContexts(securityContexts())

View File

@ -9,9 +9,9 @@ spring:
druid: druid:
# 主库数据源 # 主库数据源
master: master:
url: jdbc:mysql://192.168.0.100:3306/cmvr-iot-dev?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 url: jdbc:mysql://192.168.28.10:3306/cmvr-iot-dev?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: cmvr-iot username: root
password: cmvr-iot123456 password: cmvr2468
# 从库数据源 # 从库数据源
slave: slave:
# 从数据源开关/默认关闭 # 从数据源开关/默认关闭
@ -70,7 +70,7 @@ spring:
# redis 配置 # redis 配置
redis: redis:
# 地址 # 地址
host: 192.168.0.100 host: 192.168.28.10
# 端口默认为6379 # 端口默认为6379
port: 6379 port: 6379
# 数据库索引 # 数据库索引
@ -92,7 +92,7 @@ spring:
# Minio配置 # Minio配置
minio: minio:
url: http://192.168.0.100:9000 url: http://192.168.28.10:9000
accessKey: AKICMVR accessKey: AKICMVR
secretKey: wJalrXUtnFEMI secretKey: wJalrXUtnFEMI
bucketName: cmvr-iot bucketName: cmvr-iot
@ -132,3 +132,8 @@ flowise:
abort: http://192.168.0.108:3000/api/v1/chatmessage/abort/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/ query: http://192.168.0.108:3000/api/v1/executions/
api-key: pg61JW6W_GXyqmqSoURJ4mlSRrCMRzGLBJli_w3BFkg api-key: pg61JW6W_GXyqmqSoURJ4mlSRrCMRzGLBJli_w3BFkg
# TTS外部接口配置
tts:
external-api:
url: http://192.168.0.102:9003/generate_advanced_audio

View File

@ -0,0 +1,134 @@
server:
# 服务器的HTTP端口默认为8080
port: 13080
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
# 主库数据源
master:
url: jdbc:mysql://192.168.0.100:3306/cmvr-iot-dev?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: cmvr-iot
password: cmvr-iot123456
# 从库数据源
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: cmvr-iot
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
# 服务模块
devtools:
restart:
# 热部署开关
enabled: true
# redis 配置
redis:
# 地址
host: 192.168.0.100
# 端口默认为6379
port: 6379
# 数据库索引
database: 0
# 密码
password: cmvr2468
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池:中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# Minio配置
minio:
url: http://192.168.0.100:9000
accessKey: AKICMVR
secretKey: wJalrXUtnFEMI
bucketName: cmvr-iot
# gRPC 客户端设置
grpc:
client:
grpc-server: # 自定义服务名
address: 'static://10.148.108.100:50051' # 调用 gRPC 的地址
# address: 'static://10.148.108.100:50051' # 调用 gRPC 的地址
enableKeepAlive: true
keepAliveWithoutCalls: true
negotiationType: plaintext # 明文传输
api:
app-id: d0epfibvo3em6c4iul40
app-key: d0eqt1kqek3vg7g4ofi0
work-flow-url: https://aiagentplatform.cmft.com/api/proxy/api/v1/run_app_workflow
query-result-url: https://aiagentplatform.cmft.com/api/proxy/api/v1/query_run_app_process
bge-vl: http://192.168.1.8:5000/search_similar
generate_advanced_audio: http://192.168.0.102:9003/generate_advanced_audio
# 智能体应用id和秘钥
# ActionEnum 的枚举作为 key
agents:
INTENT_RECOGNITION: # 意图识别
app-id: d5dn5ibp9adhq1b34lig
app-key: d6j5bcellh49on5tasvg
TI_TOUCH_COORDINATES: # 获取触控二维坐标
app-id: d1ebtabnjkflk4gmhikg
app-key: d5thge2cktmipk78h82g
evaluation: http://192.168.0.8:8000/analyze
current-page: http://192.168.0.222:5000/search_similar
flowise:
tts: 192.168.0.222:8080/tts/
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

View File

@ -9,9 +9,9 @@ spring:
druid: druid:
# 主库数据源 # 主库数据源
master: master:
url: jdbc:mysql://mysql57:3306/cmvr-iot-dev?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 url: jdbc:mysql://192.168.28.10:3306/cmvr-iot-dev?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: cmvr-iot username: root
password: cmvr-iot123456 password: cmvr2468
# 从库数据源 # 从库数据源
slave: slave:
# 从数据源开关/默认关闭 # 从数据源开关/默认关闭
@ -70,7 +70,7 @@ spring:
# redis 配置 # redis 配置
redis: redis:
# 地址 # 地址
host: redis host: 192.168.28.10
# 端口默认为6379 # 端口默认为6379
port: 6379 port: 6379
# 数据库索引 # 数据库索引
@ -92,7 +92,7 @@ spring:
# Minio配置 # Minio配置
minio: minio:
url: http://mini:9000 url: http://192.168.28.10:9000
accessKey: AKICMVR accessKey: AKICMVR
secretKey: wJalrXUtnFEMI secretKey: wJalrXUtnFEMI
bucketName: cmvr-iot bucketName: cmvr-iot
@ -118,8 +118,8 @@ api:
# ActionEnum 的枚举作为 key # ActionEnum 的枚举作为 key
agents: agents:
INTENT_RECOGNITION: # 意图识别 INTENT_RECOGNITION: # 意图识别
app-id: d3c98sp0gdon0fcf6l00 app-id: d5dn5ibp9adhq1b34lig
app-key: d3c98vp0gdon0fcf6n20 app-key: d6j5bcellh49on5tasvg
TI_TOUCH_COORDINATES: # 获取触控二维坐标 TI_TOUCH_COORDINATES: # 获取触控二维坐标
app-id: d1ebtabnjkflk4gmhikg app-id: d1ebtabnjkflk4gmhikg
app-key: d5thge2cktmipk78h82g app-key: d5thge2cktmipk78h82g
@ -128,7 +128,7 @@ api:
flowise: flowise:
tts: 192.168.0.222:8080/tts/ tts: 192.168.0.222:8080/tts/
start: http://192.168.0.108:3000/api/v1/prediction/86f01c9d-aaf4-495e-9530-d02e3172c807 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/86f01c9d-aaf4-495e-9530-d02e3172c807/ 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/ 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,134 @@
server:
# 服务器的HTTP端口默认为8080
port: 13080
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
# 主库数据源
master:
url: jdbc:mysql://mysql57:3306/cmvr-iot-dev?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: cmvr-iot
password: cmvr-iot123456
# 从库数据源
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: cmvr-iot
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
# 服务模块
devtools:
restart:
# 热部署开关
enabled: true
# redis 配置
redis:
# 地址
host: redis
# 端口默认为6379
port: 6379
# 数据库索引
database: 0
# 密码
password: cmvr2468
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池:中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# Minio配置
minio:
url: http://minio:9000
accessKey: AKICMVR
secretKey: wJalrXUtnFEMI
bucketName: cmvr-iot
# gRPC 客户端设置
grpc:
client:
grpc-server: # 自定义服务名
address: 'static://10.148.108.100:50051' # 调用 gRPC 的地址
# address: 'static://10.148.108.100:50051' # 调用 gRPC 的地址
enableKeepAlive: true
keepAliveWithoutCalls: true
negotiationType: plaintext # 明文传输
api:
app-id: d0epfibvo3em6c4iul40
app-key: d0eqt1kqek3vg7g4ofi0
work-flow-url: https://aiagentplatform.cmft.com/api/proxy/api/v1/run_app_workflow
query-result-url: https://aiagentplatform.cmft.com/api/proxy/api/v1/query_run_app_process
bge-vl: http://192.168.1.8:5000/search_similar
generate_advanced_audio: http://192.168.0.102:9003/generate_advanced_audio
# 智能体应用id和秘钥
# ActionEnum 的枚举作为 key
agents:
INTENT_RECOGNITION: # 意图识别
app-id: d5dn5ibp9adhq1b34lig
app-key: d6j5bcellh49on5tasvg
TI_TOUCH_COORDINATES: # 获取触控二维坐标
app-id: d1ebtabnjkflk4gmhikg
app-key: d5thge2cktmipk78h82g
evaluation: http://192.168.0.8:8000/analyze
current-page: http://192.168.0.222:5000/search_similar
flowise:
tts: 192.168.0.222:8080/tts/
start: http://192.168.0.108:3000/api/v1/prediction/86f01c9d-aaf4-495e-9530-d02e3172c807
abort: http://192.168.0.108:3000/api/v1/chatmessage/abort/86f01c9d-aaf4-495e-9530-d02e3172c807/
query: http://192.168.0.108:3000/api/v1/executions/
api-key: pg61JW6W_GXyqmqSoURJ4mlSRrCMRzGLBJli_w3BFkg

View File

@ -9,9 +9,9 @@ spring:
druid: druid:
# 主库数据源 # 主库数据源
master: master:
url: jdbc:mysql://10.148.20.35:3306/cmvr-iot-dev?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 url: jdbc:mysql://192.168.28.10:3306/cmvr-iot-dev?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: cmvr-iot username: root
password: cmvr-iot123456 password: cmvr2468
# 从库数据源 # 从库数据源
slave: slave:
# 从数据源开关/默认关闭 # 从数据源开关/默认关闭
@ -62,11 +62,38 @@ spring:
wall: wall:
config: config:
multi-statement-allow: true multi-statement-allow: true
# 服务模块
devtools:
restart:
# 热部署开关
enabled: true
# redis 配置
redis:
# 地址
host: 192.168.28.10
# 端口默认为6379
port: 6379
# 数据库索引
database: 0
# 密码
password: cmvr2468
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池:中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# Minio配置 # Minio配置
minio: minio:
url: http://10.148.20.35:9000 url: http://192.168.28.10:9000
accessKey: AKIBSP accessKey: AKICMVR
secretKey: wJalrXUtnFEMI secretKey: wJalrXUtnFEMI
bucketName: cmvr-iot bucketName: cmvr-iot
@ -79,3 +106,29 @@ grpc:
enableKeepAlive: true enableKeepAlive: true
keepAliveWithoutCalls: true keepAliveWithoutCalls: true
negotiationType: plaintext # 明文传输 negotiationType: plaintext # 明文传输
api:
app-id: d0epfibvo3em6c4iul40
app-key: d0eqt1kqek3vg7g4ofi0
work-flow-url: https://aiagentplatform.cmft.com/api/proxy/api/v1/run_app_workflow
query-result-url: https://aiagentplatform.cmft.com/api/proxy/api/v1/query_run_app_process
bge-vl: http://192.168.1.8:5000/search_similar
generate_advanced_audio: http://192.168.0.102:9003/generate_advanced_audio
# 智能体应用id和秘钥
# ActionEnum 的枚举作为 key
agents:
INTENT_RECOGNITION: # 意图识别
app-id: d5dn5ibp9adhq1b34lig
app-key: d6j5bcellh49on5tasvg
TI_TOUCH_COORDINATES: # 获取触控二维坐标
app-id: d1ebtabnjkflk4gmhikg
app-key: d5thge2cktmipk78h82g
evaluation: http://192.168.0.8:8000/analyze
current-page: http://192.168.0.222:5000/search_similar
flowise:
tts: 192.168.0.222:8080/tts/
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

View File

@ -21,6 +21,9 @@ server:
tomcat: tomcat:
# tomcat的URI编码 # tomcat的URI编码
uri-encoding: UTF-8 uri-encoding: UTF-8
# PPE报警包含Base64图片允许读取较大的JSON请求同时限制异常请求的吞入大小
max-http-form-post-size: 20MB
max-swallow-size: 20MB
# 连接数满后的排队数默认为100 # 连接数满后的排队数默认为100
accept-count: 1000 accept-count: 1000
threads: threads:
@ -50,7 +53,7 @@ spring:
# 国际化资源文件路径 # 国际化资源文件路径
basename: i18n/messages basename: i18n/messages
profiles: profiles:
active: dev active: test
# 文件上传 # 文件上传
servlet: servlet:
multipart: multipart:

33
cmvr-iot-aima/pom.xml Normal file
View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.cmvr</groupId>
<artifactId>cmvr-iot</artifactId>
<version>3.8.9</version>
</parent>
<artifactId>cmvr-iot-aima</artifactId>
<description>
爱玛电动车测试模块
</description>
<dependencies>
<!-- 通用工具-->
<dependency>
<groupId>com.cmvr</groupId>
<artifactId>cmvr-iot-common</artifactId>
</dependency>
<!-- 测试模块-->
<dependency>
<groupId>com.cmvr</groupId>
<artifactId>cmvr-iot-test</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,144 @@
package com.cmvr.aima.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import com.cmvr.common.annotation.Excel;
import com.cmvr.common.core.domain.BaseEntity;
import java.util.Date;
/**
* 爱玛告警实体
* <p>
* 用于描述任务执行过程中产生的告警信息包含任务设备车辆用例
* 告警详情以及处理结果等数据
*
* @author cmvr-iot
* @since 2026-06-22
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@ApiModel("爱玛告警管理")
public class AimaAlarm extends BaseEntity
{
/** 序列化版本号。 */
private static final long serialVersionUID = 1L;
/** 主键ID。 */
@ApiModelProperty("主键ID")
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
/** 告警编码。 */
@Excel(name = "告警编码")
@ApiModelProperty("告警编码")
private String alarmCode;
/** 任务执行实例ID。 */
@Excel(name = "任务执行实例ID")
@ApiModelProperty("任务执行实例ID")
private String taskInstanceId;
/** 任务ID。 */
@Excel(name = "任务ID")
@ApiModelProperty("任务ID")
private String taskId;
/** 任务名称。 */
@Excel(name = "任务名称")
@ApiModelProperty("任务名称")
private String taskName;
/** 手机ID。 */
@Excel(name = "手机ID")
@ApiModelProperty("手机ID")
private String phoneId;
/** 手机名称。 */
@Excel(name = "手机名称")
@ApiModelProperty("手机名称")
private String phoneName;
/** 车辆ID。 */
@Excel(name = "车辆ID")
@ApiModelProperty("车辆ID")
private String vehicleId;
/** 车辆车架号。 */
@Excel(name = "车架号")
@ApiModelProperty("车架号")
private String vin;
/** 测试用例ID。 */
@Excel(name = "测试用例ID")
@ApiModelProperty("测试用例ID")
private String testCaseId;
/** 测试用例名称。 */
@Excel(name = "用例名称")
@ApiModelProperty("用例名称")
private String caseName;
/** 告警级别1提示2警告3严重。 */
@Excel(name = "告警级别")
@ApiModelProperty("告警级别(1提示 2警告 3严重)")
private String alarmLevel;
/** 告警类型1设备异常2测试异常3通信异常4性能异常。 */
@Excel(name = "告警类型")
@ApiModelProperty("告警类型(1设备异常 2测试异常 3通信异常 4性能异常)")
private String alarmType;
/** 告警标题。 */
@Excel(name = "告警标题")
@ApiModelProperty("告警标题")
private String alarmTitle;
/** 告警内容详情。 */
@Excel(name = "告警内容")
@ApiModelProperty("告警内容")
private String alarmContent;
/** 告警发生位置或执行步骤。 */
@Excel(name = "告警位置")
@ApiModelProperty("告警位置/步骤")
private String alarmLocation;
/** 告警发生时间。 */
@Excel(name = "告警时间")
@ApiModelProperty("告警时间")
private Date alarmTime;
/** 处理状态0未处理1处理中2已处理3已忽略。 */
@Excel(name = "处理状态")
@ApiModelProperty("处理状态(0未处理 1处理中 2已处理 3已忽略)")
private String handleStatus;
/** 告警处理人。 */
@Excel(name = "处理人")
@ApiModelProperty("处理人")
private String handler;
/** 告警处理时间。 */
@Excel(name = "处理时间")
@ApiModelProperty("处理时间")
private Date handleTime;
/** 处理说明或备注。 */
@Excel(name = "处理说明")
@ApiModelProperty("处理说明")
private String handleRemark;
/** 告警相关图片证据。 */
@ApiModelProperty("图片证据")
private String evidenceImage;
}

View File

@ -0,0 +1,85 @@
package com.cmvr.aima.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import com.cmvr.common.annotation.Excel;
import com.cmvr.common.core.domain.BaseEntity;
/**
* 爱玛手机管理对象 aima_phone
*
* @author cmvr-iot
* @since 2026-06-22
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@ApiModel("爱玛手机管理")
public class AimaPhone extends BaseEntity
{
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@ApiModelProperty("主键ID")
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
/**
* 手机名称
*/
@Excel(name = "手机名称")
@ApiModelProperty("手机名称")
private String phoneName;
/**
* 手机型号
*/
@Excel(name = "手机型号")
@ApiModelProperty("手机型号")
private String phoneModel;
/**
* 操作系统(Android/iOS)
*/
@Excel(name = "操作系统")
@ApiModelProperty("操作系统(Android/iOS)")
private String osSystem;
/**
* 系统版本
*/
@Excel(name = "系统版本")
@ApiModelProperty("系统版本")
private String osVersion;
/**
* 规格参数
*/
@Excel(name = "规格参数")
@ApiModelProperty("规格参数")
private String specifications;
/**
* 厂商
*/
@Excel(name = "厂商")
@ApiModelProperty("厂商")
private String manufacturer;
/**
* 状态(0正常 1停用)
*/
@Excel(name = "状态")
@ApiModelProperty("状态(0正常 1停用)")
private String status;
}

View File

@ -0,0 +1,59 @@
package com.cmvr.aima.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import com.cmvr.common.annotation.Excel;
import com.cmvr.common.core.domain.BaseEntity;
/**
* 爱玛任务管理对象 aima_task
*
* @author cmvr-iot
* @since 2026-06-22
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@ApiModel("爱玛任务管理")
public class AimaTask extends BaseEntity
{
private static final long serialVersionUID = 1L;
@ApiModelProperty("主键ID")
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
@Excel(name = "任务编码")
@ApiModelProperty("任务编码")
private String taskCode;
@Excel(name = "任务名称")
@ApiModelProperty("任务名称")
private String taskName;
@Excel(name = "任务类型")
@ApiModelProperty("任务类型(1常规测试 2回归测试 3专项测试)")
private String taskType;
@ApiModelProperty("手机ID")
private String phoneId;
@ApiModelProperty("车辆ID")
private String vehicleId;
@Excel(name = "测试用例数量")
@ApiModelProperty("测试用例数量")
private Integer testCaseCount;
/** 任务配置id关联test模块 */
@ApiModelProperty("任务配置id")
private String taskConfigId;
}

View File

@ -0,0 +1,87 @@
package com.cmvr.aima.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import com.cmvr.common.annotation.Excel;
import com.cmvr.common.core.domain.BaseEntity;
import java.util.Date;
/**
* 爱玛任务执行实例对象 aima_task_instance
*
* @author cmvr-iot
* @since 2026-06-22
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@ApiModel("爱玛任务执行实例")
public class AimaTaskInstance extends BaseEntity
{
private static final long serialVersionUID = 1L;
@ApiModelProperty("主键ID")
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
@Excel(name = "任务ID")
@ApiModelProperty("任务ID")
private String taskId;
@Excel(name = "手机ID")
@ApiModelProperty("手机ID")
private String phoneId;
@Excel(name = "车辆ID")
@ApiModelProperty("车辆ID")
private String vehicleId;
@Excel(name = "第几次执行")
@ApiModelProperty("第几次执行")
private Integer executionNumber;
@Excel(name = "执行状态")
@ApiModelProperty("执行状态(0待执行 1执行中 2已完成 3已取消 4执行失败 5已暂停)")
private Integer status;
/** 流程实例id关联test模块 */
@ApiModelProperty("流程实例id")
private String taskInsId;
@Excel(name = "总用例数")
@ApiModelProperty("总用例数")
private Integer totalCases;
@Excel(name = "已完成用例数")
@ApiModelProperty("已完成用例数")
private Integer completedCases;
@Excel(name = "通过用例数")
@ApiModelProperty("通过用例数")
private Integer passedCases;
@Excel(name = "失败用例数")
@ApiModelProperty("失败用例数")
private Integer failedCases;
@Excel(name = "执行进度")
@ApiModelProperty("执行进度(百分比保留2位小数)")
private Double progress;
@Excel(name = "开始时间")
@ApiModelProperty("开始时间")
private Date startTime;
@Excel(name = "结束时间")
@ApiModelProperty("结束时间")
private Date endTime;
}

View File

@ -0,0 +1,26 @@
package com.cmvr.aima.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 爱玛任务与测试用例关联对象 aima_task_test_case
*
* @author cmvr-iot
* @since 2026-06-22
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AimaTaskTestCase
{
/** 任务ID */
private String taskId;
/** 测试用例ID */
private String testCaseId;
/** 排序号 */
private Integer sortOrder;
}

View File

@ -0,0 +1,79 @@
package com.cmvr.aima.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;
import com.cmvr.common.annotation.Excel;
import com.cmvr.common.core.domain.BaseEntity;
/**
* 爱玛测试用例对象 aima_test_case
*
* @author cmvr-iot
* @since 2026-06-22
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@Builder
@ApiModel("爱玛测试用例")
public class AimaTestCase extends BaseEntity
{
private static final long serialVersionUID = 1L;
@ApiModelProperty("主键ID")
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
@Excel(name = "用例编码")
@ApiModelProperty("用例编码")
private String caseCode;
@Excel(name = "用例名称")
@ApiModelProperty("用例名称")
private String caseName;
@Excel(name = "用例分类")
@ApiModelProperty("用例分类(功能测试/性能测试/安全测试等)")
private String caseCategory;
@Excel(name = "测试描述")
@ApiModelProperty("测试描述")
private String testDescription;
@Excel(name = "前置条件")
@ApiModelProperty("前置条件")
private String precondition;
@Excel(name = "测试步骤")
@ApiModelProperty("测试步骤")
private String testSteps;
@Excel(name = "预期结果")
@ApiModelProperty("预期结果")
private String expectedResult;
@Excel(name = "优先级")
@ApiModelProperty("优先级(1高 2中 3低)")
private String priority;
@Excel(name = "是否启用")
@ApiModelProperty("是否启用(0启用 1禁用)")
private String enabled;
@Excel(name = "排序号")
@ApiModelProperty("排序号")
private Integer sortOrder;
@Excel(name = "预计执行时长")
@ApiModelProperty("预计执行时长(秒)")
private Integer estimatedDuration;
/** 检测项配置id关联test模块 */
@ApiModelProperty("检测项配置id")
private String detectItemId;
}

View File

@ -0,0 +1,102 @@
package com.cmvr.aima.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;
import com.cmvr.common.annotation.Excel;
import com.cmvr.common.core.domain.BaseEntity;
import java.util.Date;
/**
* 爱玛测试日志对象 aima_test_log
*
* @author cmvr-iot
* @since 2026-06-22
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@ApiModel("爱玛测试日志")
@Builder
public class AimaTestLog extends BaseEntity
{
private static final long serialVersionUID = 1L;
@ApiModelProperty("主键ID")
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
@Excel(name = "任务执行实例ID")
@ApiModelProperty("任务执行实例ID")
private String taskInstanceId;
@Excel(name = "任务ID")
@ApiModelProperty("任务ID")
private String taskId;
@Excel(name = "任务名称")
@ApiModelProperty("任务名称")
private String taskName;
@Excel(name = "测试用例ID")
@ApiModelProperty("测试用例ID")
private String testCaseId;
@Excel(name = "用例名称")
@ApiModelProperty("用例名称")
private String caseName;
@Excel(name = "日志级别")
@ApiModelProperty("日志级别(DEBUG/INFO/WARN/ERROR)")
private String logLevel;
@Excel(name = "日志类型")
@ApiModelProperty("日志类型(执行日志/系统日志/异常日志)")
private String logType;
@Excel(name = "日志内容")
@ApiModelProperty("日志内容")
private String logContent;
@Excel(name = "日志时间")
@ApiModelProperty("日志时间")
private Date logTime;
@Excel(name = "执行步骤")
@ApiModelProperty("执行步骤")
private String executionStep;
@Excel(name = "实际结果")
@ApiModelProperty("实际结果")
private String actualResult;
@Excel(name = "测试结果")
@ApiModelProperty("测试结果(0未执行 1通过 2失败 3跳过)")
private String testStatus;
@ApiModelProperty("截图URL")
private String screenshotUrl;
@ApiModelProperty("视频URL")
private String videoUrl;
@ApiModelProperty("错误堆栈")
private String errorStack;
@Excel(name = "执行耗时")
@ApiModelProperty("执行耗时(毫秒)")
private Long durationMs;
@Excel(name = "进度")
@ApiModelProperty("执行进度(百分比)")
private Double progress;
@Excel(name = "状态")
@ApiModelProperty("执行状态(0待执行 1执行中 2已完成 3已取消 4执行失败 5已暂停)")
private Integer status;
}

View File

@ -0,0 +1,59 @@
package com.cmvr.aima.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import com.cmvr.common.annotation.Excel;
import com.cmvr.common.core.domain.BaseEntity;
import java.util.Date;
/**
* 爱玛车辆管理对象 aima_vehicle
*
* @author cmvr-iot
* @since 2026-06-22
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@ApiModel("爱玛车辆管理")
public class AimaVehicle extends BaseEntity
{
private static final long serialVersionUID = 1L;
@ApiModelProperty("主键ID")
@TableId(value = "id", type = IdType.ASSIGN_UUID)
private String id;
@Excel(name = "车架号")
@ApiModelProperty("车架号(VIN)")
private String vin;
@Excel(name = "车型")
@ApiModelProperty("车型")
private String vehicleModel;
@Excel(name = "车辆名称")
@ApiModelProperty("车辆名称")
private String vehicleName;
@Excel(name = "颜色")
@ApiModelProperty("颜色")
private String color;
@Excel(name = "生产日期")
@ApiModelProperty("生产日期")
private Date productionDate;
@Excel(name = "状态")
@ApiModelProperty("状态(0正常 1停用)")
private String status;
}

View File

@ -0,0 +1,34 @@
package com.cmvr.aima.domain.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
/**
* 爱玛任务实例查询入参DTO
*
* @author cmvr-iot
* @since 2026-06-22
*/
@Data
@ApiModel("爱玛任务实例查询条件")
public class AimaTaskInstanceQuery {
@ApiModelProperty("任务ID")
private String taskId;
@ApiModelProperty("手机ID")
private String phoneId;
@ApiModelProperty("车辆ID")
private String vehicleId;
@ApiModelProperty("单个状态(兼容原有精确查询)")
private Integer status;
@ApiModelProperty("多状态集合in查询用0待执行 1执行中 2已完成 3已取消 4执行失败 5已暂停")
private List<Integer> statusList;
}

View File

@ -0,0 +1,136 @@
package com.cmvr.aima.domain.vo;
import com.cmvr.common.annotation.Excel;
import com.cmvr.common.core.domain.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* 爱玛任务执行实例视图对象包含关联信息
*
* @author cmvr-iot
* @since 2026-06-22
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@ApiModel("爱玛任务执行实例视图对象")
public class
AimaTaskInstanceVo extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** id */
@ApiModelProperty("id")
private String id;
/** 任务ID */
@Excel(name = "任务ID")
@ApiModelProperty("任务ID")
private String taskId;
/** 任务名称 */
@Excel(name = "任务名称")
@ApiModelProperty("任务名称")
private String taskName;
/** 任务编码 */
@Excel(name = "任务编码")
@ApiModelProperty("任务编码")
private String taskCode;
/** 手机ID */
@ApiModelProperty("手机ID")
private String phoneId;
/** 手机名称 */
@Excel(name = "手机名称")
@ApiModelProperty("手机名称")
private String phoneName;
/** 车辆ID */
@ApiModelProperty("车辆ID")
private String vehicleId;
/** 车辆名称 */
@Excel(name = "车辆名称")
@ApiModelProperty("车辆名称")
private String vehicleName;
/** 车架号 */
@Excel(name = "车架号")
@ApiModelProperty("车架号")
private String vin;
/** 第几次执行 */
@Excel(name = "执行次数")
@ApiModelProperty("第几次执行")
private Integer executionNumber;
/** 执行状态 */
@Excel(name = "执行状态")
@ApiModelProperty("执行状态(0待执行 1执行中 2已完成 3已取消 4执行失败 5已暂停)")
private Integer status;
/** 流程实例id */
@ApiModelProperty("流程实例id")
private String taskInsId;
/** 总用例数 */
@Excel(name = "总用例数")
@ApiModelProperty("总用例数")
private Integer totalCases;
/** 已完成用例数 */
@Excel(name = "已完成")
@ApiModelProperty("已完成用例数")
private Integer completedCases;
/** 通过用例数 */
@Excel(name = "通过数")
@ApiModelProperty("通过用例数")
private Integer passedCases;
/** 失败用例数 */
@Excel(name = "失败数")
@ApiModelProperty("失败用例数")
private Integer failedCases;
/** 执行进度 */
@Excel(name = "执行进度")
@ApiModelProperty("执行进度(百分比保留2位小数)")
private Double progress;
/** 开始时间 */
@Excel(name = "开始时间", dateFormat = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("开始时间")
private Date startTime;
/** 结束时间 */
@Excel(name = "结束时间", dateFormat = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty("结束时间")
private Date endTime;
/** 创建人昵称 */
@Excel(name = "创建人昵称")
@ApiModelProperty("创建人昵称")
private String createByName;
/** 修改人昵称 */
@Excel(name = "修改人昵称")
@ApiModelProperty("修改人昵称")
private String updateByName;
}

View File

@ -0,0 +1,77 @@
package com.cmvr.aima.domain.vo;
import com.cmvr.common.annotation.Excel;
import com.cmvr.common.core.domain.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 爱玛任务视图对象包含关联信息
*
* @author cmvr-iot
* @since 2026-06-22
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@ApiModel("爱玛任务视图对象")
public class AimaTaskVo extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** id */
@ApiModelProperty("id")
private String id;
/** 任务编码 */
@Excel(name = "任务编码")
@ApiModelProperty("任务编码")
private String taskCode;
/** 任务名称 */
@Excel(name = "任务名称")
@ApiModelProperty("任务名称")
private String taskName;
/** 任务类型 */
@Excel(name = "任务类型")
@ApiModelProperty("任务类型(1常规测试 2回归测试 3专项测试)")
private String taskType;
/** 手机ID */
@ApiModelProperty("手机ID")
private String phoneId;
/** 手机名称 */
@Excel(name = "手机名称")
@ApiModelProperty("手机名称")
private String phoneName;
/** 车辆ID */
@ApiModelProperty("车辆ID")
private String vehicleId;
/** 测试用例数量 */
@Excel(name = "测试用例数量")
@ApiModelProperty("测试用例数量")
private Integer testCaseCount;
/** 任务配置id */
@ApiModelProperty("任务配置id")
private String taskConfigId;
/** 创建人昵称 */
@Excel(name = "创建人昵称")
@ApiModelProperty("创建人昵称")
private String createByName;
/** 修改人昵称 */
@Excel(name = "修改人昵称")
@ApiModelProperty("修改人昵称")
private String updateByName;
}

View File

@ -0,0 +1,100 @@
package com.cmvr.aima.domain.vo;
import com.cmvr.common.annotation.Excel;
import com.cmvr.common.core.domain.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 爱玛测试用例视图对象包含关联信息
*
* @author cmvr-iot
* @since 2026-06-22
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@ApiModel("爱玛测试用例视图对象")
public class AimaTestCaseVo extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** id */
@ApiModelProperty("id")
private String id;
/** 用例编码 */
@Excel(name = "用例编码")
@ApiModelProperty("用例编码")
private String caseCode;
/** 用例名称 */
@Excel(name = "用例名称")
@ApiModelProperty("用例名称")
private String caseName;
/** 用例分类 */
@Excel(name = "用例分类")
@ApiModelProperty("用例分类(功能测试/性能测试/安全测试等)")
private String caseCategory;
/** 测试描述 */
@ApiModelProperty("测试描述")
private String testDescription;
/** 前置条件 */
@ApiModelProperty("前置条件")
private String precondition;
/** 测试步骤 */
@ApiModelProperty("测试步骤")
private String testSteps;
/** 预期结果 */
@ApiModelProperty("预期结果")
private String expectedResult;
/** 优先级 */
@Excel(name = "优先级")
@ApiModelProperty("优先级(1高 2中 3低)")
private String priority;
/** 是否启用 */
@Excel(name = "是否启用")
@ApiModelProperty("是否启用(0启用 1禁用)")
private String enabled;
/** 排序号 */
@Excel(name = "排序号")
@ApiModelProperty("排序号")
private Integer sortOrder;
/** 预计执行时长 */
@Excel(name = "预计执行时长")
@ApiModelProperty("预计执行时长(秒)")
private Integer estimatedDuration;
/** 检测项配置id */
@ApiModelProperty("检测项配置id")
private String detectItemId;
/** 配置状态 */
@Excel(name = "配置状态")
@ApiModelProperty("配置状态(0未部署 1已部署)")
private String configStatus;
/** 创建人昵称 */
@Excel(name = "创建人昵称")
@ApiModelProperty("创建人昵称")
private String createByName;
/** 修改人昵称 */
@Excel(name = "修改人昵称")
@ApiModelProperty("修改人昵称")
private String updateByName;
}

View File

@ -0,0 +1,31 @@
package com.cmvr.aima.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 爱玛测试日志类型枚举
*
* @author cmvr-iot
* @since 2026-06-22
*/
@Getter
@AllArgsConstructor
public enum AimaLogTypeEnum {
TEXT(1, "文字"),
IMAGE(2, "图片"),
VIDEO(3, "视频"),
AUDIO(4, "音频");
private final int code;
private final String desc;
public static AimaLogTypeEnum fromCode(int code) {
for (AimaLogTypeEnum type : values()) {
if (type.code == code) {
return type;
}
}
return null;
}
}

View File

@ -0,0 +1,34 @@
package com.cmvr.aima.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 爱玛任务状态枚举
*
* @author cmvr-iot
* @since 2026-06-22
*/
@Getter
@AllArgsConstructor
public enum AimaTaskStatusEnum {
// 未执行
NOT_STARTED(5, "未执行"),
SUCCESS(0, "已完成"),
RUNNING(1, "执行中"),
FAILED(2, "失败"),
PAUSED(3, "已暂停"),
STOPPED(4, "已终止");
private final int code;
private final String desc;
public static AimaTaskStatusEnum fromCode(int code) {
for (AimaTaskStatusEnum status : values()) {
if (status.code == code) {
return status;
}
}
return null;
}
}

View File

@ -0,0 +1,206 @@
package com.cmvr.aima.listener;
import cn.hutool.core.util.StrUtil;
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.IAimaTestLogService;
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 lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.*;
/**
* 爱玛测试工作流执行监听器
* 监听流程引擎事件自动记录测试日志和更新任务状态
*
* @author cmvr-iot
* @since 2026-06-22
*/
@Component
@Slf4j
public class AimaFlowExecutionListener implements FlowExecutionListener {
private final IAimaTaskInstanceService aimaTaskInstanceService;
private final IAimaTestLogService aimaTestLogService;
private final MessagePushService messagePushService;
/** key:itemId, value:已完成节点集合 */
private final Map<String, Set<String>> itemNodeExecutMap = new HashMap<>();
/** key:taskInsId(流程实例id), value:已执行item集合 */
private final Map<String, Set<String>> taskItemExecutMap = new HashMap<>();
public AimaFlowExecutionListener(IAimaTaskInstanceService aimaTaskInstanceService,
IAimaTestLogService aimaTestLogService,
MessagePushService messagePushService) {
this.aimaTaskInstanceService = aimaTaskInstanceService;
this.aimaTestLogService = aimaTestLogService;
this.messagePushService = messagePushService;
}
@Override
public String getModuleCode() {
return FlowExecutionModuleCodes.AIMA;
}
@Override
public void onEvent(FlowExecutionEvent event) {
String instId = event.getInstId();
String itemId = event.getItemId();
String nodeId = event.getNodeId();
if (StrUtil.isBlank(instId)) {
return;
}
// 1.节点完成才记录当前item下node
if (StrUtil.isNotBlank(itemId) && StrUtil.isNotBlank(nodeId)
&& FlowExecutionEvent.EventType.NODE_COMPLETED == event.getEventType()) {
itemNodeExecutMap.computeIfAbsent(itemId, k -> new HashSet<>()).add(nodeId);
}
// 任意事件当前任务绑定itemitem一启动就入Map
if (StrUtil.isNotBlank(instId) && StrUtil.isNotBlank(itemId)) {
taskItemExecutMap.computeIfAbsent(instId, k -> new HashSet<>()).add(itemId);
}
// 查询任务实例
AimaTaskInstance taskInstance = aimaTaskInstanceService.lambdaQuery()
.eq(AimaTaskInstance::getTaskInsId, instId)
.one();
if (taskInstance == null) {
log.warn("未查询到爱玛测试实例,taskInsId:{}", instId);
return;
}
int targetStatus = AimaTaskStatusEnum.RUNNING.getCode();
boolean needUpdateDb = false;
Date now = DateUtils.getNowDate();
switch (event.getEventType()) {
case TASK_COMPLETED:
targetStatus = AimaTaskStatusEnum.SUCCESS.getCode();
needUpdateDb = true;
clearCache(instId);
pushCompleteMsg(taskInstance.getId(), now, targetStatus);
break;
case TASK_FAILED:
targetStatus = AimaTaskStatusEnum.FAILED.getCode();
needUpdateDb = true;
clearCache(instId);
break;
case NODE_COMPLETED:
default:
break;
}
if (needUpdateDb) {
taskInstance.setStatus(targetStatus);
taskInstance.setEndTime(now);
aimaTaskInstanceService.updateById(taskInstance);
}
// 节点推送 - 记录日志
if (StrUtil.isNotBlank(nodeId)) {
int totalCases = taskInstance.getTotalCases() == null ? 0 : taskInstance.getTotalCases();
double progress = 0D;
if (totalCases > 0) {
// 当前任务已执行item数量
Set<String> itemSet = taskItemExecutMap.getOrDefault(instId, new HashSet<>());
int itemTotal = itemSet.size();
int finishedItemNum = itemTotal - 1;
// 当前item已完成节点数
Set<String> finishedNodeSet = itemNodeExecutMap.getOrDefault(itemId, new HashSet<>());
int finishedNode = finishedNodeSet.size();
int totalNode = event.getNodeCount() == 0 ? 1 : event.getNodeCount();
// 已完整跑完用例占比
double itemTotalRatio = finishedItemNum / (double) totalCases;
// 当前运行item内部节点占比
double innerRatio = finishedNode / (double) totalNode / totalCases;
// 防止循环节点进度超限
double singleItemBase = 1d / totalCases;
innerRatio = Math.max(singleItemBase - 0.01, innerRatio);
progress = (itemTotalRatio + innerRatio) * 100;
}
String info = event.getEventType() == FlowExecutionEvent.EventType.NODE_STARTED ? "开始执行" : "执行完成";
String logId = UUID.randomUUID().toString().replace("-", "");
// 四舍五入保留2位小数
progress = Math.round(progress * 100) / 100.0;
// 保存日志到数据库
AimaTestLog testLog = AimaTestLog.builder()
.id(logId)
.taskInstanceId(taskInstance.getId())
.taskId(taskInstance.getTaskId())
.testCaseId(itemId)
.logLevel("INFO")
.logType("执行日志")
.logContent("节点【" + event.getNodeName() + "" + info)
.logTime(now)
.status(targetStatus)
.executionStep(event.getNodeName())
.testStatus("0") // 0未执行
.durationMs(0L)
.progress( progress)
.build();
try {
aimaTestLogService.insertAimaTestLog(testLog);
log.info("爱玛测试日志保存成功,logId:{},taskInstanceId:{},nodeId:{}", logId, taskInstance.getId(), nodeId);
} catch (Exception e) {
log.error("爱玛测试日志保存失败,taskInstanceId:{},nodeId:{}", taskInstance.getId(), nodeId, e);
}
try {
messagePushService.pushToChannel("AimaTaskInstance", testLog);
} catch (Exception e) {
log.info("消息推送异常,无订阅忽略,itemId:{}", itemId, e);
}
}
}
/** 任务结束统一清理缓存 */
private void clearCache(String taskInsId) {
Set<String> itemList = taskItemExecutMap.getOrDefault(taskInsId, new HashSet<>());
itemList.forEach(itemNodeExecutMap::remove);
taskItemExecutMap.remove(taskInsId);
}
/**
* 推送完成任务信息
*/
public void pushCompleteMsg(String dbInsId, Date now, int targetStatus) {
String logId = UUID.randomUUID().toString().replace("-", "");
// 保存完成日志
AimaTestLog testLog = AimaTestLog.builder()
.id(logId)
.taskInstanceId(dbInsId)
.logLevel("INFO")
.logType("系统日志")
.logContent("任务执行完成")
.logTime(now)
.status(targetStatus)
.build();
try {
aimaTestLogService.insertAimaTestLog(testLog);
log.info("任务完成日志保存成功,logId:{},taskInstanceId:{}", logId, dbInsId);
} catch (Exception e) {
log.error("任务完成日志保存失败,taskInstanceId:{}", dbInsId, e);
}
try {
messagePushService.pushToChannel("AimaTaskInstance", testLog);
} catch (Exception e) {
log.info("消息推送异常,无订阅忽略,insId:{}", dbInsId, e);
}
}
}

View File

@ -0,0 +1,61 @@
package com.cmvr.aima.mapper;
import java.util.List;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cmvr.aima.domain.AimaAlarm;
/**
* 爱玛告警管理Mapper接口
*
* @author cmvr-iot
* @since 2026-06-22
*/
public interface AimaAlarmMapper extends BaseMapper<AimaAlarm> {
/**
* 查询爱玛告警管理
*
* @param id 爱玛告警管理主键
* @return 爱玛告警管理
*/
AimaAlarm selectAimaAlarmById(String id);
/**
* 查询爱玛告警管理列表
*
* @param aimaAlarm 爱玛告警管理
* @return 爱玛告警管理集合
*/
List<AimaAlarm> selectAimaAlarmList(AimaAlarm aimaAlarm);
/**
* 新增爱玛告警管理
*
* @param aimaAlarm 爱玛告警管理
* @return 结果
*/
int insertAimaAlarm(AimaAlarm aimaAlarm);
/**
* 修改爱玛告警管理
*
* @param aimaAlarm 爱玛告警管理
* @return 结果
*/
int updateAimaAlarm(AimaAlarm aimaAlarm);
/**
* 删除爱玛告警管理
*
* @param id 爱玛告警管理主键
* @return 结果
*/
int deleteAimaAlarmById(String id);
/**
* 批量删除爱玛告警管理
*
* @param ids 需要删除的数据主键集合
* @return 结果
*/
int deleteAimaAlarmByIds(String[] ids);
}

View File

@ -0,0 +1,61 @@
package com.cmvr.aima.mapper;
import java.util.List;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cmvr.aima.domain.AimaPhone;
/**
* 爱玛手机管理Mapper接口
*
* @author cmvr-iot
* @since 2026-06-22
*/
public interface AimaPhoneMapper extends BaseMapper<AimaPhone> {
/**
* 查询爱玛手机管理
*
* @param id 爱玛手机管理主键
* @return 爱玛手机管理
*/
AimaPhone selectAimaPhoneById(String id);
/**
* 查询爱玛手机管理列表
*
* @param aimaPhone 爱玛手机管理
* @return 爱玛手机管理集合
*/
List<AimaPhone> selectAimaPhoneList(AimaPhone aimaPhone);
/**
* 新增爱玛手机管理
*
* @param aimaPhone 爱玛手机管理
* @return 结果
*/
int insertAimaPhone(AimaPhone aimaPhone);
/**
* 修改爱玛手机管理
*
* @param aimaPhone 爱玛手机管理
* @return 结果
*/
int updateAimaPhone(AimaPhone aimaPhone);
/**
* 删除爱玛手机管理
*
* @param id 爱玛手机管理主键
* @return 结果
*/
int deleteAimaPhoneById(String id);
/**
* 批量删除爱玛手机管理
*
* @param ids 需要删除的数据主键集合
* @return 结果
*/
int deleteAimaPhoneByIds(String[] ids);
}

View File

@ -0,0 +1,71 @@
package com.cmvr.aima.mapper;
import java.util.List;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cmvr.aima.domain.AimaTaskInstance;
import com.cmvr.aima.domain.dto.AimaTaskInstanceQuery;
import com.cmvr.aima.domain.vo.AimaTaskInstanceVo;
/**
* 爱玛任务执行实例Mapper接口
*
* @author cmvr-iot
* @since 2026-06-22
*/
public interface AimaTaskInstanceMapper extends BaseMapper<AimaTaskInstance> {
/**
* 查询爱玛任务执行实例
*
* @param id 爱玛任务执行实例主键
* @return 爱玛任务执行实例
*/
AimaTaskInstance selectAimaTaskInstanceById(String id);
/**
* 查询爱玛任务执行实例列表
*
* @param aimaTaskInstance 爱玛任务执行实例
* @return 爱玛任务执行实例集合
*/
List<AimaTaskInstance> selectAimaTaskInstanceList(AimaTaskInstance aimaTaskInstance);
/**
* 查询爱玛任务执行实例视图列表包含关联信息
*
* @param query 查询条件
* @return 爱玛任务执行实例视图集合
*/
List<AimaTaskInstanceVo> selectAimaTaskInstanceVoList(AimaTaskInstanceQuery query);
/**
* 新增爱玛任务执行实例
*
* @param aimaTaskInstance 爱玛任务执行实例
* @return 结果
*/
int insertAimaTaskInstance(AimaTaskInstance aimaTaskInstance);
/**
* 修改爱玛任务执行实例
*
* @param aimaTaskInstance 爱玛任务执行实例
* @return 结果
*/
int updateAimaTaskInstance(AimaTaskInstance aimaTaskInstance);
/**
* 删除爱玛任务执行实例
*
* @param id 爱玛任务执行实例主键
* @return 结果
*/
int deleteAimaTaskInstanceById(String id);
/**
* 批量删除爱玛任务执行实例
*
* @param ids 需要删除的数据主键集合
* @return 结果
*/
int deleteAimaTaskInstanceByIds(String[] ids);
}

View File

@ -0,0 +1,70 @@
package com.cmvr.aima.mapper;
import java.util.List;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cmvr.aima.domain.AimaTask;
import com.cmvr.aima.domain.vo.AimaTaskVo;
/**
* 爱玛任务管理Mapper接口
*
* @author cmvr-iot
* @since 2026-06-22
*/
public interface AimaTaskMapper extends BaseMapper<AimaTask> {
/**
* 查询爱玛任务管理
*
* @param id 爱玛任务管理主键
* @return 爱玛任务管理
*/
AimaTask selectAimaTaskById(String id);
/**
* 查询爱玛任务管理列表
*
* @param aimaTask 爱玛任务管理
* @return 爱玛任务管理集合
*/
List<AimaTask> selectAimaTaskList(AimaTask aimaTask);
/**
* 查询爱玛任务视图列表包含关联信息
*
* @param aimaTask 爱玛任务管理
* @return 爱玛任务视图集合
*/
List<AimaTaskVo> selectAimaTaskVoList(AimaTask aimaTask);
/**
* 新增爱玛任务管理
*
* @param aimaTask 爱玛任务管理
* @return 结果
*/
int insertAimaTask(AimaTask aimaTask);
/**
* 修改爱玛任务管理
*
* @param aimaTask 爱玛任务管理
* @return 结果
*/
int updateAimaTask(AimaTask aimaTask);
/**
* 删除爱玛任务管理
*
* @param id 爱玛任务管理主键
* @return 结果
*/
int deleteAimaTaskById(String id);
/**
* 批量删除爱玛任务管理
*
* @param ids 需要删除的数据主键集合
* @return 结果
*/
int deleteAimaTaskByIds(String[] ids);
}

View File

@ -0,0 +1,48 @@
package com.cmvr.aima.mapper;
import java.util.List;
import com.github.yulichang.base.MPJBaseMapper;
import org.apache.ibatis.annotations.Param;
import com.cmvr.aima.domain.AimaTaskTestCase;
/**
* 爱玛任务与测试用例关联Mapper接口
*
* @author cmvr-iot
* @since 2026-06-22
*/
public interface AimaTaskTestCaseMapper extends MPJBaseMapper<AimaTaskTestCase>
{
/**
* 批量插入任务-测试用例关联
*
* @param list 任务-测试用例关联列表
* @return 结果
*/
void batchInsertTaskTestCase(@Param("list") List<AimaTaskTestCase> list);
/**
* 根据任务ID删除关联
*
* @param taskId 任务ID
* @return 结果
*/
void deleteTaskTestCaseByTaskId(String taskId);
/**
* 根据任务ID查询测试用例ID列表
*
* @param taskId 任务ID
* @return 测试用例ID列表
*/
List<String> selectTestCaseIdsByTaskId(String taskId);
/**
* 批量删除任务-测试用例关联
*
* @param taskIds 需要删除的任务ID数组
* @return 结果
*/
void deleteTaskTestCaseByTaskIds(String[] taskIds);
}

View File

@ -0,0 +1,70 @@
package com.cmvr.aima.mapper;
import java.util.List;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cmvr.aima.domain.AimaTestCase;
import com.cmvr.aima.domain.vo.AimaTestCaseVo;
/**
* 爱玛测试用例Mapper接口
*
* @author cmvr-iot
* @since 2026-06-22
*/
public interface AimaTestCaseMapper extends BaseMapper<AimaTestCase> {
/**
* 查询爱玛测试用例
*
* @param id 爱玛测试用例主键
* @return 爱玛测试用例
*/
AimaTestCase selectAimaTestCaseById(String id);
/**
* 查询爱玛测试用例列表
*
* @param aimaTestCase 爱玛测试用例
* @return 爱玛测试用例集合
*/
List<AimaTestCase> selectAimaTestCaseList(AimaTestCase aimaTestCase);
/**
* 查询爱玛测试用例视图列表包含关联信息
*
* @param aimaTestCase 爱玛测试用例
* @return 爱玛测试用例视图集合
*/
List<AimaTestCaseVo> selectAimaTestCaseVoList(AimaTestCase aimaTestCase);
/**
* 新增爱玛测试用例
*
* @param aimaTestCase 爱玛测试用例
* @return 结果
*/
int insertAimaTestCase(AimaTestCase aimaTestCase);
/**
* 修改爱玛测试用例
*
* @param aimaTestCase 爱玛测试用例
* @return 结果
*/
int updateAimaTestCase(AimaTestCase aimaTestCase);
/**
* 删除爱玛测试用例
*
* @param id 爱玛测试用例主键
* @return 结果
*/
int deleteAimaTestCaseById(String id);
/**
* 批量删除爱玛测试用例
*
* @param ids 需要删除的数据主键集合
* @return 结果
*/
int deleteAimaTestCaseByIds(String[] ids);
}

View File

@ -0,0 +1,61 @@
package com.cmvr.aima.mapper;
import java.util.List;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cmvr.aima.domain.AimaTestLog;
/**
* 爱玛测试日志Mapper接口
*
* @author cmvr-iot
* @since 2026-06-22
*/
public interface AimaTestLogMapper extends BaseMapper<AimaTestLog> {
/**
* 查询爱玛测试日志
*
* @param id 爱玛测试日志主键
* @return 爱玛测试日志
*/
AimaTestLog selectAimaTestLogById(String id);
/**
* 查询爱玛测试日志列表
*
* @param aimaTestLog 爱玛测试日志
* @return 爱玛测试日志集合
*/
List<AimaTestLog> selectAimaTestLogList(AimaTestLog aimaTestLog);
/**
* 新增爱玛测试日志
*
* @param aimaTestLog 爱玛测试日志
* @return 结果
*/
int insertAimaTestLog(AimaTestLog aimaTestLog);
/**
* 修改爱玛测试日志
*
* @param aimaTestLog 爱玛测试日志
* @return 结果
*/
int updateAimaTestLog(AimaTestLog aimaTestLog);
/**
* 删除爱玛测试日志
*
* @param id 爱玛测试日志主键
* @return 结果
*/
int deleteAimaTestLogById(String id);
/**
* 批量删除爱玛测试日志
*
* @param ids 需要删除的数据主键集合
* @return 结果
*/
int deleteAimaTestLogByIds(String[] ids);
}

View File

@ -0,0 +1,61 @@
package com.cmvr.aima.mapper;
import java.util.List;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cmvr.aima.domain.AimaVehicle;
/**
* 爱玛车辆管理Mapper接口
*
* @author cmvr-iot
* @since 2026-06-22
*/
public interface AimaVehicleMapper extends BaseMapper<AimaVehicle> {
/**
* 查询爱玛车辆管理
*
* @param id 爱玛车辆管理主键
* @return 爱玛车辆管理
*/
AimaVehicle selectAimaVehicleById(String id);
/**
* 查询爱玛车辆管理列表
*
* @param aimaVehicle 爱玛车辆管理
* @return 爱玛车辆管理集合
*/
List<AimaVehicle> selectAimaVehicleList(AimaVehicle aimaVehicle);
/**
* 新增爱玛车辆管理
*
* @param aimaVehicle 爱玛车辆管理
* @return 结果
*/
int insertAimaVehicle(AimaVehicle aimaVehicle);
/**
* 修改爱玛车辆管理
*
* @param aimaVehicle 爱玛车辆管理
* @return 结果
*/
int updateAimaVehicle(AimaVehicle aimaVehicle);
/**
* 删除爱玛车辆管理
*
* @param id 爱玛车辆管理主键
* @return 结果
*/
int deleteAimaVehicleById(String id);
/**
* 批量删除爱玛车辆管理
*
* @param ids 需要删除的数据主键集合
* @return 结果
*/
int deleteAimaVehicleByIds(String[] ids);
}

View File

@ -0,0 +1,19 @@
package com.cmvr.aima.service;
import java.util.List;
import com.cmvr.aima.domain.AimaAlarm;
/**
* 爱玛告警管理Service接口
*
* @author cmvr-iot
* @since 2026-06-22
*/
public interface IAimaAlarmService {
AimaAlarm selectAimaAlarmById(String id);
List<AimaAlarm> selectAimaAlarmList(AimaAlarm aimaAlarm);
int insertAimaAlarm(AimaAlarm aimaAlarm);
int updateAimaAlarm(AimaAlarm aimaAlarm);
int deleteAimaAlarmByIds(String[] ids);
int deleteAimaAlarmById(String id);
}

View File

@ -0,0 +1,60 @@
package com.cmvr.aima.service;
import java.util.List;
import com.cmvr.aima.domain.AimaPhone;
/**
* 爱玛手机管理Service接口
*
* @author cmvr-iot
* @since 2026-06-22
*/
public interface IAimaPhoneService {
/**
* 查询爱玛手机管理
*
* @param id 爱玛手机管理主键
* @return 爱玛手机管理
*/
AimaPhone selectAimaPhoneById(String id);
/**
* 查询爱玛手机管理列表
*
* @param aimaPhone 爱玛手机管理
* @return 爱玛手机管理集合
*/
List<AimaPhone> selectAimaPhoneList(AimaPhone aimaPhone);
/**
* 新增爱玛手机管理
*
* @param aimaPhone 爱玛手机管理
* @return 结果
*/
int insertAimaPhone(AimaPhone aimaPhone);
/**
* 修改爱玛手机管理
*
* @param aimaPhone 爱玛手机管理
* @return 结果
*/
int updateAimaPhone(AimaPhone aimaPhone);
/**
* 批量删除爱玛手机管理
*
* @param ids 需要删除的爱玛手机管理主键集合
* @return 结果
*/
int deleteAimaPhoneByIds(String[] ids);
/**
* 删除爱玛手机管理信息
*
* @param id 爱玛手机管理主键
* @return 结果
*/
int deleteAimaPhoneById(String id);
}

View File

@ -0,0 +1,64 @@
package com.cmvr.aima.service;
import java.util.List;
import com.baomidou.mybatisplus.extension.service.IService;
import com.cmvr.aima.domain.AimaTaskInstance;
import com.cmvr.aima.domain.dto.AimaTaskInstanceQuery;
import com.cmvr.aima.domain.vo.AimaTaskInstanceVo;
/**
* 爱玛任务执行实例Service接口
*
* @author cmvr-iot
* @since 2026-06-22
*/
public interface IAimaTaskInstanceService extends IService<AimaTaskInstance> {
AimaTaskInstance selectAimaTaskInstanceById(String id);
List<AimaTaskInstance> selectAimaTaskInstanceList(AimaTaskInstance aimaTaskInstance);
List<AimaTaskInstanceVo> selectAimaTaskInstanceVoList(AimaTaskInstanceQuery query);
int insertAimaTaskInstance(AimaTaskInstance aimaTaskInstance);
int updateAimaTaskInstance(AimaTaskInstance aimaTaskInstance);
int deleteAimaTaskInstanceByIds(String[] ids);
int deleteAimaTaskInstanceById(String id);
/**
* 开始执行任务实例
*
* @param id 任务实例ID
* @return 结果
*/
int startInstance(String id);
/**
* 暂停任务实例
*
* @param id 任务实例ID
* @return 结果
*/
int pauseInstance(String id);
/**
* 终止任务实例
*
* @param id 任务实例ID
* @return 结果
*/
int stopInstance(String id);
/**
* 恢复任务实例从暂停恢复为执行中
*
* @param id 任务实例ID
* @return 结果
*/
int resumeInstance(String id);
/**
* 更新任务实例进度
*
* @param instanceId 任务实例ID
* @param progress 执行进度(百分比保留2位小数)
*/
void updateProgress(String instanceId, Double progress);
}

View File

@ -0,0 +1,38 @@
package com.cmvr.aima.service;
import java.util.List;
import com.cmvr.aima.domain.AimaTask;
import com.cmvr.aima.domain.AimaTestCase;
import com.cmvr.aima.domain.vo.AimaTaskVo;
/**
* 爱玛任务管理Service接口
*
* @author cmvr-iot
* @since 2026-06-22
*/
public interface IAimaTaskService {
AimaTask selectAimaTaskById(String id);
List<AimaTask> selectAimaTaskList(AimaTask aimaTask);
List<AimaTaskVo> selectAimaTaskVoList(AimaTask aimaTask);
int insertAimaTask(AimaTask aimaTask);
int updateAimaTask(AimaTask aimaTask);
int deleteAimaTaskByIds(String[] ids);
int deleteAimaTaskById(String id);
/**
* 为任务绑定测试用例
*
* @param taskId 任务ID
* @param testCaseIds 测试用例ID列表
*/
void bindTestCases(String taskId, List<String> testCaseIds);
/**
* 获取任务的测试用例列表
*
* @param taskId 任务ID
* @return 测试用例ID列表
*/
List<AimaTestCase> getTestCaseIds(String taskId);
}

View File

@ -0,0 +1,69 @@
package com.cmvr.aima.service;
import java.util.List;
import com.cmvr.aima.domain.AimaTestCase;
import com.cmvr.aima.domain.vo.AimaTestCaseVo;
/**
* 爱玛测试用例Service接口
*
* @author cmvr-iot
* @since 2026-06-22
*/
public interface IAimaTestCaseService {
/**
* 查询爱玛测试用例
*
* @param id 爱玛测试用例主键
* @return 爱玛测试用例
*/
AimaTestCase selectAimaTestCaseById(String id);
/**
* 查询爱玛测试用例列表
*
* @param aimaTestCase 爱玛测试用例
* @return 爱玛测试用例集合
*/
List<AimaTestCase> selectAimaTestCaseList(AimaTestCase aimaTestCase);
/**
* 查询爱玛测试用例视图列表包含关联信息
*
* @param aimaTestCase 爱玛测试用例
* @return 爱玛测试用例视图集合
*/
List<AimaTestCaseVo> selectAimaTestCaseVoList(AimaTestCase aimaTestCase);
/**
* 新增爱玛测试用例
*
* @param aimaTestCase 爱玛测试用例
* @return 结果
*/
int insertAimaTestCase(AimaTestCase aimaTestCase);
/**
* 修改爱玛测试用例
*
* @param aimaTestCase 爱玛测试用例
* @return 结果
*/
int updateAimaTestCase(AimaTestCase aimaTestCase);
/**
* 批量删除爱玛测试用例
*
* @param ids 需要删除的爱玛测试用例主键集合
* @return 结果
*/
int deleteAimaTestCaseByIds(String[] ids);
/**
* 删除爱玛测试用例信息
*
* @param id 爱玛测试用例主键
* @return 结果
*/
int deleteAimaTestCaseById(String id);
}

View File

@ -0,0 +1,19 @@
package com.cmvr.aima.service;
import java.util.List;
import com.cmvr.aima.domain.AimaTestLog;
/**
* 爱玛测试日志Service接口
*
* @author cmvr-iot
* @since 2026-06-22
*/
public interface IAimaTestLogService {
AimaTestLog selectAimaTestLogById(String id);
List<AimaTestLog> selectAimaTestLogList(AimaTestLog aimaTestLog);
int insertAimaTestLog(AimaTestLog aimaTestLog);
int updateAimaTestLog(AimaTestLog aimaTestLog);
int deleteAimaTestLogByIds(String[] ids);
int deleteAimaTestLogById(String id);
}

View File

@ -0,0 +1,60 @@
package com.cmvr.aima.service;
import java.util.List;
import com.cmvr.aima.domain.AimaVehicle;
/**
* 爱玛车辆管理Service接口
*
* @author cmvr-iot
* @since 2026-06-22
*/
public interface IAimaVehicleService {
/**
* 查询爱玛车辆管理
*
* @param id 爱玛车辆管理主键
* @return 爱玛车辆管理
*/
AimaVehicle selectAimaVehicleById(String id);
/**
* 查询爱玛车辆管理列表
*
* @param aimaVehicle 爱玛车辆管理
* @return 爱玛车辆管理集合
*/
List<AimaVehicle> selectAimaVehicleList(AimaVehicle aimaVehicle);
/**
* 新增爱玛车辆管理
*
* @param aimaVehicle 爱玛车辆管理
* @return 结果
*/
int insertAimaVehicle(AimaVehicle aimaVehicle);
/**
* 修改爱玛车辆管理
*
* @param aimaVehicle 爱玛车辆管理
* @return 结果
*/
int updateAimaVehicle(AimaVehicle aimaVehicle);
/**
* 批量删除爱玛车辆管理
*
* @param ids 需要删除的爱玛车辆管理主键集合
* @return 结果
*/
int deleteAimaVehicleByIds(String[] ids);
/**
* 删除爱玛车辆管理信息
*
* @param id 爱玛车辆管理主键
* @return 结果
*/
int deleteAimaVehicleById(String id);
}

View File

@ -0,0 +1,63 @@
package com.cmvr.aima.service.impl;
import java.util.List;
import java.util.UUID;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.cmvr.common.utils.DateUtils;
import com.cmvr.common.utils.SecurityUtils;
import com.cmvr.aima.mapper.AimaAlarmMapper;
import com.cmvr.aima.domain.AimaAlarm;
import com.cmvr.aima.service.IAimaAlarmService;
import org.springframework.transaction.annotation.Transactional;
/**
* 爱玛告警管理Service业务层处理
*
* @author cmvr-iot
* @since 2026-06-22
*/
@Service
@Transactional
public class AimaAlarmServiceImpl extends ServiceImpl<AimaAlarmMapper, AimaAlarm> implements IAimaAlarmService {
@Autowired
private AimaAlarmMapper aimaAlarmMapper;
@Override
public AimaAlarm selectAimaAlarmById(String id) {
return aimaAlarmMapper.selectAimaAlarmById(id);
}
@Override
public List<AimaAlarm> selectAimaAlarmList(AimaAlarm aimaAlarm) {
return aimaAlarmMapper.selectAimaAlarmList(aimaAlarm);
}
@Override
public int insertAimaAlarm(AimaAlarm aimaAlarm) {
if (aimaAlarm.getId() == null || aimaAlarm.getId().isEmpty()) {
aimaAlarm.setId(UUID.randomUUID().toString().replace("-", ""));
}
aimaAlarm.setCreateTime(DateUtils.getNowDate());
aimaAlarm.setCreateBy(SecurityUtils.getUsername());
return aimaAlarmMapper.insertAimaAlarm(aimaAlarm);
}
@Override
public int updateAimaAlarm(AimaAlarm aimaAlarm) {
aimaAlarm.setUpdateTime(DateUtils.getNowDate());
aimaAlarm.setUpdateBy(SecurityUtils.getUsername());
return aimaAlarmMapper.updateAimaAlarm(aimaAlarm);
}
@Override
public int deleteAimaAlarmByIds(String[] ids) {
return aimaAlarmMapper.deleteAimaAlarmByIds(ids);
}
@Override
public int deleteAimaAlarmById(String id) {
return aimaAlarmMapper.deleteAimaAlarmById(id);
}
}

View File

@ -0,0 +1,100 @@
package com.cmvr.aima.service.impl;
import java.util.List;
import java.util.UUID;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.cmvr.common.utils.DateUtils;
import com.cmvr.common.utils.SecurityUtils;
import com.cmvr.aima.mapper.AimaPhoneMapper;
import com.cmvr.aima.domain.AimaPhone;
import com.cmvr.aima.service.IAimaPhoneService;
import org.springframework.transaction.annotation.Transactional;
/**
* 爱玛手机管理Service业务层处理
*
* @author cmvr-iot
* @since 2026-06-22
*/
@Service
@Transactional
public class AimaPhoneServiceImpl extends ServiceImpl<AimaPhoneMapper, AimaPhone> implements IAimaPhoneService {
@Autowired
private AimaPhoneMapper aimaPhoneMapper;
/**
* 查询爱玛手机管理
*
* @param id 爱玛手机管理主键
* @return 爱玛手机管理
*/
@Override
public AimaPhone selectAimaPhoneById(String id) {
return aimaPhoneMapper.selectAimaPhoneById(id);
}
/**
* 查询爱玛手机管理列表
*
* @param aimaPhone 爱玛手机管理
* @return 爱玛手机管理
*/
@Override
public List<AimaPhone> selectAimaPhoneList(AimaPhone aimaPhone) {
return aimaPhoneMapper.selectAimaPhoneList(aimaPhone);
}
/**
* 新增爱玛手机管理
*
* @param aimaPhone 爱玛手机管理
* @return 结果
*/
@Override
public int insertAimaPhone(AimaPhone aimaPhone) {
// 生成UUID作为主键
if (aimaPhone.getId() == null || aimaPhone.getId().isEmpty()) {
aimaPhone.setId(UUID.randomUUID().toString().replace("-", ""));
}
aimaPhone.setCreateTime(DateUtils.getNowDate());
aimaPhone.setCreateBy(SecurityUtils.getUsername());
return aimaPhoneMapper.insertAimaPhone(aimaPhone);
}
/**
* 修改爱玛手机管理
*
* @param aimaPhone 爱玛手机管理
* @return 结果
*/
@Override
public int updateAimaPhone(AimaPhone aimaPhone) {
aimaPhone.setUpdateTime(DateUtils.getNowDate());
aimaPhone.setUpdateBy(SecurityUtils.getUsername());
return aimaPhoneMapper.updateAimaPhone(aimaPhone);
}
/**
* 批量删除爱玛手机管理
*
* @param ids 需要删除的爱玛手机管理主键
* @return 结果
*/
@Override
public int deleteAimaPhoneByIds(String[] ids) {
return aimaPhoneMapper.deleteAimaPhoneByIds(ids);
}
/**
* 删除爱玛手机管理信息
*
* @param id 爱玛手机管理主键
* @return 结果
*/
@Override
public int deleteAimaPhoneById(String id) {
return aimaPhoneMapper.deleteAimaPhoneById(id);
}
}

View File

@ -0,0 +1,234 @@
package com.cmvr.aima.service.impl;
import java.util.List;
import java.util.UUID;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cmvr.aima.domain.dto.AimaTaskInstanceQuery;
import com.cmvr.aima.domain.vo.AimaTaskInstanceVo;
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.service.ITeTaskOrchestrationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import com.cmvr.common.utils.DateUtils;
import com.cmvr.aima.mapper.AimaTaskInstanceMapper;
import com.cmvr.aima.domain.AimaTaskInstance;
import com.cmvr.aima.service.IAimaTaskInstanceService;
import com.cmvr.aima.enums.AimaTaskStatusEnum;
import com.cmvr.aima.service.IAimaTaskService;
import com.cmvr.aima.domain.AimaTask;
import org.springframework.transaction.annotation.Transactional;
/**
* 爱玛任务执行实例Service业务层处理
*
* @author cmvr-iot
* @since 2026-06-22
*/
@Service
@Transactional
public class AimaTaskInstanceServiceImpl extends ServiceImpl<AimaTaskInstanceMapper, AimaTaskInstance> implements IAimaTaskInstanceService {
@Autowired
private AimaTaskInstanceMapper aimaTaskInstanceMapper;
@Autowired
private IAimaTaskService aimaTaskService;
@Autowired
private ITeTaskOrchestrationService teTaskOrchestrationService;
@Autowired
@Lazy
private FlowTaskRuntimeService flowTaskRuntimeService;
@Autowired
@Lazy
private FlowControlService flowControlService;
@Override
public AimaTaskInstance selectAimaTaskInstanceById(String id) {
return aimaTaskInstanceMapper.selectAimaTaskInstanceById(id);
}
@Override
public List<AimaTaskInstance> selectAimaTaskInstanceList(AimaTaskInstance aimaTaskInstance) {
return aimaTaskInstanceMapper.selectAimaTaskInstanceList(aimaTaskInstance);
}
@Override
public List<AimaTaskInstanceVo> selectAimaTaskInstanceVoList(AimaTaskInstanceQuery query) {
return aimaTaskInstanceMapper.selectAimaTaskInstanceVoList(query);
}
@Override
public int insertAimaTaskInstance(AimaTaskInstance aimaTaskInstance) {
if (aimaTaskInstance.getId() == null || aimaTaskInstance.getId().isEmpty()) {
aimaTaskInstance.setId(UUID.randomUUID().toString().replace("-", ""));
}
aimaTaskInstance.setStatus(AimaTaskStatusEnum.NOT_STARTED.getCode());
aimaTaskInstance.setCreateTime(DateUtils.getNowDate());
aimaTaskInstance.setCreateBy(SecurityUtils.getUsername());
// 获取测试用例数量
AimaTask aimaTask = aimaTaskService.selectAimaTaskById(aimaTaskInstance.getTaskId());
if (aimaTask != null && aimaTask.getTestCaseCount() != null) {
aimaTaskInstance.setTotalCases(aimaTask.getTestCaseCount());
}
return aimaTaskInstanceMapper.insertAimaTaskInstance(aimaTaskInstance);
}
@Override
public int updateAimaTaskInstance(AimaTaskInstance aimaTaskInstance) {
// 校验状态只有未执行可以修改
if (AimaTaskStatusEnum.NOT_STARTED.getCode() != aimaTaskInstance.getStatus()) {
throw new ServiceException("只有未执行状态的任务实例才能修改");
}
aimaTaskInstance.setUpdateTime(DateUtils.getNowDate());
aimaTaskInstance.setUpdateBy(SecurityUtils.getUsername());
// 获取测试用例数量
AimaTask aimaTask = aimaTaskService.selectAimaTaskById(aimaTaskInstance.getTaskId());
if (aimaTask != null && aimaTask.getTestCaseCount() != null) {
aimaTaskInstance.setTotalCases(aimaTask.getTestCaseCount());
}
return aimaTaskInstanceMapper.updateAimaTaskInstance(aimaTaskInstance);
}
@Override
public int deleteAimaTaskInstanceByIds(String[] ids) {
// 校验状态只有未执行可以删除
for (String id : ids) {
AimaTaskInstance instance = aimaTaskInstanceMapper.selectAimaTaskInstanceById(id);
if (instance != null && AimaTaskStatusEnum.NOT_STARTED.getCode() != instance.getStatus()) {
throw new ServiceException("只有未执行状态的任务实例才能删除");
}
}
return aimaTaskInstanceMapper.deleteAimaTaskInstanceByIds(ids);
}
@Override
public int deleteAimaTaskInstanceById(String id) {
return aimaTaskInstanceMapper.deleteAimaTaskInstanceById(id);
}
/**
* 开始执行任务实例
*/
@Override
public int startInstance(String id) {
AimaTaskInstance instance = aimaTaskInstanceMapper.selectAimaTaskInstanceById(id);
if (instance == null) {
throw new ServiceException("任务实例不存在");
}
if (AimaTaskStatusEnum.NOT_STARTED.getCode() != instance.getStatus()) {
throw new ServiceException("只有待执行状态的任务实例才能开始执行");
}
AimaTaskInstance update = new AimaTaskInstance();
update.setId(id);
update.setStatus(AimaTaskStatusEnum.RUNNING.getCode());
update.setStartTime(DateUtils.getNowDate());
update.setUpdateTime(DateUtils.getNowDate());
update.setUpdateBy(SecurityUtils.getUsername());
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()));
}
// 调用流程引擎执行
String insId = flowTaskRuntimeService.executeTask(
TeTaskExecuteNormalVO.builder()
.taskId(aimaTask.getTaskConfigId())
.moduleCode(FlowExecutionModuleCodes.AIMA)
.runParams(runParams)
.build()
);
update.setTaskInsId(insId);
return aimaTaskInstanceMapper.updateAimaTaskInstance(update);
}
/**
* 暂停任务实例
*/
@Override
public int pauseInstance(String id) {
AimaTaskInstance instance = aimaTaskInstanceMapper.selectAimaTaskInstanceById(id);
if (instance == null) {
throw new ServiceException("任务实例不存在");
}
if (AimaTaskStatusEnum.RUNNING.getCode() != instance.getStatus()) {
throw new ServiceException("只有执行中状态的任务实例才能暂停");
}
AimaTaskInstance update = new AimaTaskInstance();
update.setId(id);
update.setStatus(AimaTaskStatusEnum.PAUSED.getCode());
update.setUpdateTime(DateUtils.getNowDate());
update.setUpdateBy(SecurityUtils.getUsername());
flowControlService.pause(instance.getTaskInsId());
return aimaTaskInstanceMapper.updateAimaTaskInstance(update);
}
/**
* 终止任务实例
*/
@Override
public int stopInstance(String id) {
AimaTaskInstance instance = aimaTaskInstanceMapper.selectAimaTaskInstanceById(id);
if (instance == null) {
throw new ServiceException("任务实例不存在");
}
if (AimaTaskStatusEnum.NOT_STARTED.getCode() != instance.getStatus()
&& AimaTaskStatusEnum.RUNNING.getCode() != instance.getStatus()) {
throw new ServiceException("已完成或已取消的任务实例不能终止");
}
AimaTaskInstance update = new AimaTaskInstance();
update.setId(id);
update.setStatus(AimaTaskStatusEnum.STOPPED.getCode());
update.setEndTime(DateUtils.getNowDate());
update.setUpdateTime(DateUtils.getNowDate());
update.setUpdateBy(SecurityUtils.getUsername());
flowControlService.stop(instance.getTaskInsId());
return aimaTaskInstanceMapper.updateAimaTaskInstance(update);
}
/**
* 恢复任务实例从暂停恢复为执行中
*/
@Override
public int resumeInstance(String id) {
AimaTaskInstance instance = aimaTaskInstanceMapper.selectAimaTaskInstanceById(id);
if (instance == null) {
throw new ServiceException("任务实例不存在");
}
if (AimaTaskStatusEnum.PAUSED.getCode() != instance.getStatus()) {
throw new ServiceException("只有已暂停状态的任务实例才能恢复");
}
AimaTaskInstance update = new AimaTaskInstance();
update.setId(id);
update.setStatus(AimaTaskStatusEnum.RUNNING.getCode());
update.setUpdateTime(DateUtils.getNowDate());
update.setUpdateBy(SecurityUtils.getUsername());
flowControlService.resume(instance.getTaskInsId());
return aimaTaskInstanceMapper.updateAimaTaskInstance(update);
}
/**
* 更新任务实例进度
*/
@Override
public void updateProgress(String instanceId, Double progress) {
AimaTaskInstance update = new AimaTaskInstance();
update.setId(instanceId);
update.setProgress(progress);
update.setUpdateTime(DateUtils.getNowDate());
aimaTaskInstanceMapper.updateAimaTaskInstance(update);
}
}

View File

@ -0,0 +1,159 @@
package com.cmvr.aima.service.impl;
import java.util.ArrayList;
import java.util.Arrays;
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.cmvr.aima.domain.AimaTaskTestCase;
import com.cmvr.aima.domain.AimaTestCase;
import com.cmvr.aima.domain.vo.AimaTaskVo;
import com.cmvr.aima.mapper.AimaTestCaseMapper;
import com.cmvr.test.model.domain.TeTaskConfigInfo;
import com.cmvr.test.model.vo.TeTaskOrchestraVO;
import com.cmvr.test.service.ITeTaskConfigInfoService;
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.SecurityUtils;
import com.cmvr.aima.mapper.AimaTaskMapper;
import com.cmvr.aima.mapper.AimaTaskTestCaseMapper;
import com.cmvr.aima.domain.AimaTask;
import com.cmvr.aima.service.IAimaTaskService;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class AimaTaskServiceImpl extends ServiceImpl<AimaTaskMapper, AimaTask> implements IAimaTaskService {
@Autowired
private AimaTaskMapper aimaTaskMapper;
@Autowired
private AimaTaskTestCaseMapper aimaTaskTestCaseMapper;
@Autowired
private AimaTestCaseMapper aimaTestCaseMapper;
@Autowired
private ITeTaskOrchestrationService teTaskOrchestrationService;
@Autowired
private ITeTaskConfigInfoService teTaskConfigInfoService;
@Override
public AimaTask selectAimaTaskById(String id) {
return aimaTaskMapper.selectAimaTaskById(id);
}
@Override
public List<AimaTask> selectAimaTaskList(AimaTask aimaTask) {
return aimaTaskMapper.selectAimaTaskList(aimaTask);
}
@Override
public List<AimaTaskVo> selectAimaTaskVoList(AimaTask aimaTask) {
return aimaTaskMapper.selectAimaTaskVoList(aimaTask);
}
@Override
public int insertAimaTask(AimaTask aimaTask) {
// 生成UUID作为主键
if (aimaTask.getId() == null || aimaTask.getId().isEmpty()) {
aimaTask.setId(UUID.randomUUID().toString().replace("-", ""));
}
aimaTask.setCreateTime(DateUtils.getNowDate());
aimaTask.setCreateBy(SecurityUtils.getUsername());
// 同步新增任务配置到test模块
TeTaskConfigInfo teTaskConfigInfo = TeTaskConfigInfo.builder()
.taskName(aimaTask.getTaskName())
.build();
teTaskConfigInfoService.insertTeTaskConfigInfo(teTaskConfigInfo);
aimaTask.setTaskConfigId(teTaskConfigInfo.getId());
return aimaTaskMapper.insertAimaTask(aimaTask);
}
@Override
public int updateAimaTask(AimaTask aimaTask) {
aimaTask.setUpdateTime(DateUtils.getNowDate());
aimaTask.setUpdateBy(SecurityUtils.getUsername());
// 同步更新任务配置名称
if (aimaTask.getTaskConfigId() != null) {
TeTaskConfigInfo teTaskConfigInfo = teTaskConfigInfoService.selectTeTaskConfigInfoById(aimaTask.getTaskConfigId());
if (teTaskConfigInfo != null) {
teTaskConfigInfo.setTaskName(aimaTask.getTaskName());
teTaskConfigInfoService.updateTeTaskConfigInfo(teTaskConfigInfo);
}
}
return aimaTaskMapper.updateAimaTask(aimaTask);
}
@Override
public int deleteAimaTaskByIds(String[] ids) {
// 先删除关联表数据
aimaTaskTestCaseMapper.deleteTaskTestCaseByTaskIds(ids);
// 批量查询任务
List<AimaTask> aimaTasks = this.baseMapper.selectBatchIds(Arrays.asList(ids));
// 批量删除任务配置
teTaskConfigInfoService.deleteTeTaskConfigInfoByIds(
aimaTasks.stream().map(AimaTask::getTaskConfigId).toArray(String[]::new)
);
return aimaTaskMapper.deleteAimaTaskByIds(ids);
}
@Override
public int deleteAimaTaskById(String id) {
return aimaTaskMapper.deleteAimaTaskById(id);
}
@Override
public void bindTestCases(String taskId, List<String> testCaseIds) {
// 先删除旧的关联
aimaTaskTestCaseMapper.deleteTaskTestCaseByTaskId(taskId);
// 批量插入新的关联
if (testCaseIds != null && !testCaseIds.isEmpty()) {
List<AimaTaskTestCase> list = new ArrayList<>();
for (int i = 0; i < testCaseIds.size(); i++) {
AimaTaskTestCase taskTestCase = new AimaTaskTestCase();
taskTestCase.setTaskId(taskId);
taskTestCase.setTestCaseId(testCaseIds.get(i));
taskTestCase.setSortOrder(i);
list.add(taskTestCase);
}
aimaTaskTestCaseMapper.batchInsertTaskTestCase(list);
// 更新任务的测试用例数量
AimaTask task = new AimaTask();
task.setId(taskId);
task.setTestCaseCount(testCaseIds.size());
aimaTaskMapper.updateAimaTask(task);
// 查询测试用例获取detectItemId列表
List<AimaTestCase> testCases = aimaTestCaseMapper.selectBatchIds(testCaseIds);
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);
TeTaskOrchestraVO teTaskOrchestraVO = TeTaskOrchestraVO.builder()
.taskId(aimaTask.getTaskConfigId())
.itemIds(detectItemIds)
.build();
teTaskOrchestrationService.insertTeTaskOrchestration(teTaskOrchestraVO);
}
}
}
@Override
public List<AimaTestCase> getTestCaseIds(String taskId) {
List<String> ids = aimaTaskTestCaseMapper.selectTestCaseIdsByTaskId(taskId);
return aimaTestCaseMapper.selectList(new QueryWrapper<AimaTestCase>().in("id", ids));
}
}

View File

@ -0,0 +1,139 @@
package com.cmvr.aima.service.impl;
import java.util.List;
import java.util.UUID;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.cmvr.aima.domain.vo.AimaTestCaseVo;
import com.cmvr.test.model.domain.TeDetectionItem;
import com.cmvr.test.service.ITeDetectionItemService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.cmvr.common.utils.DateUtils;
import com.cmvr.common.utils.SecurityUtils;
import com.cmvr.aima.mapper.AimaTestCaseMapper;
import com.cmvr.aima.domain.AimaTestCase;
import com.cmvr.aima.service.IAimaTestCaseService;
import org.springframework.transaction.annotation.Transactional;
/**
* 爱玛测试用例Service业务层处理
*
* @author cmvr-iot
* @since 2026-06-22
*/
@Service
@Transactional
public class AimaTestCaseServiceImpl extends ServiceImpl<AimaTestCaseMapper, AimaTestCase> implements IAimaTestCaseService {
@Autowired
private AimaTestCaseMapper aimaTestCaseMapper;
@Autowired
private ITeDetectionItemService teDetectionItemService;
/**
* 查询爱玛测试用例
*
* @param id 爱玛测试用例主键
* @return 爱玛测试用例
*/
@Override
public AimaTestCase selectAimaTestCaseById(String id) {
return aimaTestCaseMapper.selectAimaTestCaseById(id);
}
/**
* 查询爱玛测试用例列表
*
* @param aimaTestCase 爱玛测试用例
* @return 爱玛测试用例
*/
@Override
public List<AimaTestCase> selectAimaTestCaseList(AimaTestCase aimaTestCase) {
return aimaTestCaseMapper.selectAimaTestCaseList(aimaTestCase);
}
/**
* 查询爱玛测试用例视图列表包含关联信息
*
* @param aimaTestCase 爱玛测试用例
* @return 爱玛测试用例视图集合
*/
@Override
public List<AimaTestCaseVo> selectAimaTestCaseVoList(AimaTestCase aimaTestCase) {
return aimaTestCaseMapper.selectAimaTestCaseVoList(aimaTestCase);
}
/**
* 新增爱玛测试用例
*
* @param aimaTestCase 爱玛测试用例
* @return 结果
*/
@Override
public int insertAimaTestCase(AimaTestCase aimaTestCase) {
// 生成UUID作为主键
if (aimaTestCase.getId() == null || aimaTestCase.getId().isEmpty()) {
aimaTestCase.setId(UUID.randomUUID().toString().replace("-", ""));
}
aimaTestCase.setCreateTime(DateUtils.getNowDate());
aimaTestCase.setCreateBy(SecurityUtils.getUsername());
// 同步新增检测项配置到test模块
TeDetectionItem teDetectionItem = TeDetectionItem.builder()
.detectName(aimaTestCase.getCaseName())
.groupId(7L) // 可以根据实际情况调整分组ID
.build();
teDetectionItemService.insertTeDetectionItem(teDetectionItem);
aimaTestCase.setDetectItemId(teDetectionItem.getId());
return aimaTestCaseMapper.insertAimaTestCase(aimaTestCase);
}
/**
* 修改爱玛测试用例
*
* @param aimaTestCase 爱玛测试用例
* @return 结果
*/
@Override
public int updateAimaTestCase(AimaTestCase aimaTestCase) {
aimaTestCase.setUpdateTime(DateUtils.getNowDate());
aimaTestCase.setUpdateBy(SecurityUtils.getUsername());
// 同步更新检测项名称
if (aimaTestCase.getDetectItemId() != null) {
TeDetectionItem teDetectionItem = teDetectionItemService.selectTeDetectionItemById(aimaTestCase.getDetectItemId());
if (teDetectionItem != null) {
teDetectionItem.setDetectName(aimaTestCase.getCaseName());
teDetectionItemService.updateTeDetectionItem(teDetectionItem);
}
}
return aimaTestCaseMapper.updateAimaTestCase(aimaTestCase);
}
/**
* 批量删除爱玛测试用例
*
* @param ids 需要删除的爱玛测试用例主键
* @return 结果
*/
@Override
public int deleteAimaTestCaseByIds(String[] ids) {
// 先查询数据删除绑定的检测项
for (String id : ids) {
AimaTestCase aimaTestCase = aimaTestCaseMapper.selectAimaTestCaseById(id);
if (aimaTestCase != null && aimaTestCase.getDetectItemId() != null) {
teDetectionItemService.deleteTeDetectionItemById(aimaTestCase.getDetectItemId());
}
}
return aimaTestCaseMapper.deleteAimaTestCaseByIds(ids);
}
/**
* 删除爱玛测试用例信息
*
* @param id 爱玛测试用例主键
* @return 结果
*/
@Override
public int deleteAimaTestCaseById(String id) {
return aimaTestCaseMapper.deleteAimaTestCaseById(id);
}
}

View File

@ -0,0 +1,61 @@
package com.cmvr.aima.service.impl;
import java.util.List;
import java.util.UUID;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.cmvr.common.utils.DateUtils;
import com.cmvr.common.utils.SecurityUtils;
import com.cmvr.aima.mapper.AimaTestLogMapper;
import com.cmvr.aima.domain.AimaTestLog;
import com.cmvr.aima.service.IAimaTestLogService;
import org.springframework.transaction.annotation.Transactional;
/**
* 爱玛测试日志Service业务层处理
*
* @author cmvr-iot
* @since 2026-06-22
*/
@Service
@Transactional
public class AimaTestLogServiceImpl extends ServiceImpl<AimaTestLogMapper, AimaTestLog> implements IAimaTestLogService {
@Autowired
private AimaTestLogMapper aimaTestLogMapper;
@Override
public AimaTestLog selectAimaTestLogById(String id) {
return aimaTestLogMapper.selectAimaTestLogById(id);
}
@Override
public List<AimaTestLog> selectAimaTestLogList(AimaTestLog aimaTestLog) {
return aimaTestLogMapper.selectAimaTestLogList(aimaTestLog);
}
@Override
public int insertAimaTestLog(AimaTestLog aimaTestLog) {
if (aimaTestLog.getId() == null || aimaTestLog.getId().isEmpty()) {
aimaTestLog.setId(UUID.randomUUID().toString().replace("-", ""));
}
aimaTestLog.setCreateTime(DateUtils.getNowDate());
return aimaTestLogMapper.insertAimaTestLog(aimaTestLog);
}
@Override
public int updateAimaTestLog(AimaTestLog aimaTestLog) {
aimaTestLog.setUpdateTime(DateUtils.getNowDate());
return aimaTestLogMapper.updateAimaTestLog(aimaTestLog);
}
@Override
public int deleteAimaTestLogByIds(String[] ids) {
return aimaTestLogMapper.deleteAimaTestLogByIds(ids);
}
@Override
public int deleteAimaTestLogById(String id) {
return aimaTestLogMapper.deleteAimaTestLogById(id);
}
}

View File

@ -0,0 +1,63 @@
package com.cmvr.aima.service.impl;
import java.util.List;
import java.util.UUID;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.cmvr.common.utils.DateUtils;
import com.cmvr.common.utils.SecurityUtils;
import com.cmvr.aima.mapper.AimaVehicleMapper;
import com.cmvr.aima.domain.AimaVehicle;
import com.cmvr.aima.service.IAimaVehicleService;
import org.springframework.transaction.annotation.Transactional;
/**
* 爱玛车辆管理Service业务层处理
*
* @author cmvr-iot
* @since 2026-06-22
*/
@Service
@Transactional
public class AimaVehicleServiceImpl extends ServiceImpl<AimaVehicleMapper, AimaVehicle> implements IAimaVehicleService {
@Autowired
private AimaVehicleMapper aimaVehicleMapper;
@Override
public AimaVehicle selectAimaVehicleById(String id) {
return aimaVehicleMapper.selectAimaVehicleById(id);
}
@Override
public List<AimaVehicle> selectAimaVehicleList(AimaVehicle aimaVehicle) {
return aimaVehicleMapper.selectAimaVehicleList(aimaVehicle);
}
@Override
public int insertAimaVehicle(AimaVehicle aimaVehicle) {
if (aimaVehicle.getId() == null || aimaVehicle.getId().isEmpty()) {
aimaVehicle.setId(UUID.randomUUID().toString().replace("-", ""));
}
aimaVehicle.setCreateTime(DateUtils.getNowDate());
aimaVehicle.setCreateBy(SecurityUtils.getUsername());
return aimaVehicleMapper.insertAimaVehicle(aimaVehicle);
}
@Override
public int updateAimaVehicle(AimaVehicle aimaVehicle) {
aimaVehicle.setUpdateTime(DateUtils.getNowDate());
aimaVehicle.setUpdateBy(SecurityUtils.getUsername());
return aimaVehicleMapper.updateAimaVehicle(aimaVehicle);
}
@Override
public int deleteAimaVehicleByIds(String[] ids) {
return aimaVehicleMapper.deleteAimaVehicleByIds(ids);
}
@Override
public int deleteAimaVehicleById(String id) {
return aimaVehicleMapper.deleteAimaVehicleById(id);
}
}

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cmvr.aima.mapper.AimaAlarmMapper">
<resultMap type="AimaAlarm" id="AimaAlarmResult">
<result property="id" column="id"/><result property="createBy" column="create_by"/><result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/><result property="updateTime" column="update_time"/><result property="remark" column="remark"/>
<result property="alarmCode" column="alarm_code"/><result property="taskInstanceId" column="task_instance_id"/><result property="taskId" column="task_id"/>
<result property="taskName" column="task_name"/><result property="phoneId" column="phone_id"/><result property="phoneName" column="phone_name"/>
<result property="vehicleId" column="vehicle_id"/><result property="vin" column="vin"/><result property="testCaseId" column="test_case_id"/>
<result property="caseName" column="case_name"/><result property="alarmLevel" column="alarm_level"/><result property="alarmType" column="alarm_type"/>
<result property="alarmTitle" column="alarm_title"/><result property="alarmContent" column="alarm_content"/><result property="alarmLocation" column="alarm_location"/>
<result property="alarmTime" column="alarm_time"/><result property="handleStatus" column="handle_status"/><result property="handler" column="handler"/>
<result property="handleTime" column="handle_time"/><result property="handleRemark" column="handle_remark"/><result property="evidenceImage" column="evidence_image"/>
</resultMap>
<sql id="selectAimaAlarmVo">
select id, create_by, create_time, update_by, update_time, remark, alarm_code, task_instance_id, task_id, task_name, phone_id, phone_name, vehicle_id, vin, test_case_id, case_name, alarm_level, alarm_type, alarm_title, alarm_content, alarm_location, alarm_time, handle_status, handler, handle_time, handle_remark, evidence_image from aima_alarm
</sql>
<select id="selectAimaAlarmList" parameterType="AimaAlarm" resultMap="AimaAlarmResult">
<include refid="selectAimaAlarmVo"/>
<where>
<if test="alarmCode != null and alarmCode != ''"> and alarm_code = #{alarmCode}</if>
<if test="taskInstanceId != null and taskInstanceId != ''"> and task_instance_id = #{taskInstanceId}</if>
<if test="taskId != null and taskId != ''"> and task_id = #{taskId}</if>
<if test="alarmLevel != null and alarmLevel != ''"> and alarm_level = #{alarmLevel}</if>
<if test="alarmType != null and alarmType != ''"> and alarm_type = #{alarmType}</if>
<if test="handleStatus != null and handleStatus != ''"> and handle_status = #{handleStatus}</if>
</where>
order by alarm_time desc
</select>
<select id="selectAimaAlarmById" parameterType="String" resultMap="AimaAlarmResult">
<include refid="selectAimaAlarmVo"/> where id = #{id}
</select>
<insert id="insertAimaAlarm" parameterType="AimaAlarm">
insert into aima_alarm <trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if><if test="createBy != null">create_by,</if><if test="createTime != null">create_time,</if>
<if test="updateBy != null">update_by,</if><if test="updateTime != null">update_time,</if><if test="remark != null">remark,</if>
<if test="alarmCode != null">alarm_code,</if><if test="taskInstanceId != null">task_instance_id,</if><if test="taskId != null">task_id,</if>
<if test="taskName != null">task_name,</if><if test="phoneId != null">phone_id,</if><if test="phoneName != null">phone_name,</if>
<if test="vehicleId != null">vehicle_id,</if><if test="vin != null">vin,</if><if test="testCaseId != null">test_case_id,</if>
<if test="caseName != null">case_name,</if><if test="alarmLevel != null">alarm_level,</if><if test="alarmType != null">alarm_type,</if>
<if test="alarmTitle != null">alarm_title,</if><if test="alarmContent != null">alarm_content,</if><if test="alarmLocation != null">alarm_location,</if>
<if test="alarmTime != null">alarm_time,</if><if test="handleStatus != null">handle_status,</if><if test="handler != null">handler,</if>
<if test="handleTime != null">handle_time,</if><if test="handleRemark != null">handle_remark,</if><if test="evidenceImage != null">evidence_image,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">#{id},</if><if test="createBy != null">#{createBy},</if><if test="createTime != null">#{createTime},</if>
<if test="updateBy != null">#{updateBy},</if><if test="updateTime != null">#{updateTime},</if><if test="remark != null">#{remark},</if>
<if test="alarmCode != null">#{alarmCode},</if><if test="taskInstanceId != null">#{taskInstanceId},</if><if test="taskId != null">#{taskId},</if>
<if test="taskName != null">#{taskName},</if><if test="phoneId != null">#{phoneId},</if><if test="phoneName != null">#{phoneName},</if>
<if test="vehicleId != null">#{vehicleId},</if><if test="vin != null">#{vin},</if><if test="testCaseId != null">#{testCaseId},</if>
<if test="caseName != null">#{caseName},</if><if test="alarmLevel != null">#{alarmLevel},</if><if test="alarmType != null">#{alarmType},</if>
<if test="alarmTitle != null">#{alarmTitle},</if><if test="alarmContent != null">#{alarmContent},</if><if test="alarmLocation != null">#{alarmLocation},</if>
<if test="alarmTime != null">#{alarmTime},</if><if test="handleStatus != null">#{handleStatus},</if><if test="handler != null">#{handler},</if>
<if test="handleTime != null">#{handleTime},</if><if test="handleRemark != null">#{handleRemark},</if><if test="evidenceImage != null">#{evidenceImage},</if>
</trim>
</insert>
<update id="updateAimaAlarm" parameterType="AimaAlarm">
update aima_alarm <trim prefix="SET" suffixOverrides=",">
<if test="createBy != null">create_by = #{createBy},</if><if test="createTime != null">create_time = #{createTime},</if>
<if test="updateBy != null">update_by = #{updateBy},</if><if test="updateTime != null">update_time = #{updateTime},</if>
<if test="remark != null">remark = #{remark},</if><if test="alarmCode != null">alarm_code = #{alarmCode},</if>
<if test="taskInstanceId != null">task_instance_id = #{taskInstanceId},</if><if test="taskId != null">task_id = #{taskId},</if>
<if test="taskName != null">task_name = #{taskName},</if><if test="phoneId != null">phone_id = #{phoneId},</if>
<if test="phoneName != null">phone_name = #{phoneName},</if><if test="vehicleId != null">vehicle_id = #{vehicleId},</if>
<if test="vin != null">vin = #{vin},</if><if test="testCaseId != null">test_case_id = #{testCaseId},</if>
<if test="caseName != null">case_name = #{caseName},</if><if test="alarmLevel != null">alarm_level = #{alarmLevel},</if>
<if test="alarmType != null">alarm_type = #{alarmType},</if><if test="alarmTitle != null">alarm_title = #{alarmTitle},</if>
<if test="alarmContent != null">alarm_content = #{alarmContent},</if><if test="alarmLocation != null">alarm_location = #{alarmLocation},</if>
<if test="alarmTime != null">alarm_time = #{alarmTime},</if><if test="handleStatus != null">handle_status = #{handleStatus},</if>
<if test="handler != null">handler = #{handler},</if><if test="handleTime != null">handle_time = #{handleTime},</if>
<if test="handleRemark != null">handle_remark = #{handleRemark},</if><if test="evidenceImage != null">evidence_image = #{evidenceImage},</if>
</trim> where id = #{id}
</update>
<delete id="deleteAimaAlarmById" parameterType="String">delete from aima_alarm where id = #{id}</delete>
<delete id="deleteAimaAlarmByIds" parameterType="String">
delete from aima_alarm where id in <foreach item="id" collection="array" open="(" separator="," close=")">#{id}</foreach>
</delete>
</mapper>

View File

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cmvr.aima.mapper.AimaPhoneMapper">
<resultMap type="AimaPhone" id="AimaPhoneResult">
<result property="id" column="id" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
<result property="phoneName" column="phone_name" />
<result property="phoneModel" column="phone_model" />
<result property="osSystem" column="os_system" />
<result property="osVersion" column="os_version" />
<result property="specifications" column="specifications" />
<result property="manufacturer" column="manufacturer" />
<result property="status" column="status" />
</resultMap>
<sql id="selectAimaPhoneVo">
select id, create_by, create_time, update_by, update_time, remark, phone_name, phone_model, os_system, os_version, specifications, manufacturer, status from aima_phone
</sql>
<select id="selectAimaPhoneList" parameterType="AimaPhone" resultMap="AimaPhoneResult">
<include refid="selectAimaPhoneVo"/>
<where>
<if test="phoneName != null and phoneName != ''"> and phone_name like concat('%', #{phoneName}, '%')</if>
<if test="phoneModel != null and phoneModel != ''"> and phone_model like concat('%', #{phoneModel}, '%')</if>
<if test="osSystem != null and osSystem != ''"> and os_system = #{osSystem}</if>
<if test="status != null and status != ''"> and status = #{status}</if>
</where>
order by create_time desc
</select>
<select id="selectAimaPhoneById" parameterType="String" resultMap="AimaPhoneResult">
<include refid="selectAimaPhoneVo"/>
where id = #{id}
</select>
<insert id="insertAimaPhone" parameterType="AimaPhone">
insert into aima_phone
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if>
<if test="createBy != null">create_by,</if>
<if test="createTime != null">create_time,</if>
<if test="updateBy != null">update_by,</if>
<if test="updateTime != null">update_time,</if>
<if test="remark != null">remark,</if>
<if test="phoneName != null">phone_name,</if>
<if test="phoneModel != null">phone_model,</if>
<if test="osSystem != null">os_system,</if>
<if test="osVersion != null">os_version,</if>
<if test="specifications != null">specifications,</if>
<if test="manufacturer != null">manufacturer,</if>
<if test="status != null">status,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">#{id},</if>
<if test="createBy != null">#{createBy},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateBy != null">#{updateBy},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="remark != null">#{remark},</if>
<if test="phoneName != null">#{phoneName},</if>
<if test="phoneModel != null">#{phoneModel},</if>
<if test="osSystem != null">#{osSystem},</if>
<if test="osVersion != null">#{osVersion},</if>
<if test="specifications != null">#{specifications},</if>
<if test="manufacturer != null">#{manufacturer},</if>
<if test="status != null">#{status},</if>
</trim>
</insert>
<update id="updateAimaPhone" parameterType="AimaPhone">
update aima_phone
<trim prefix="SET" suffixOverrides=",">
<if test="createBy != null">create_by = #{createBy},</if>
<if test="createTime != null">create_time = #{createTime},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="phoneName != null">phone_name = #{phoneName},</if>
<if test="phoneModel != null">phone_model = #{phoneModel},</if>
<if test="osSystem != null">os_system = #{osSystem},</if>
<if test="osVersion != null">os_version = #{osVersion},</if>
<if test="specifications != null">specifications = #{specifications},</if>
<if test="manufacturer != null">manufacturer = #{manufacturer},</if>
<if test="status != null">status = #{status},</if>
</trim>
where id = #{id}
</update>
<delete id="deleteAimaPhoneById" parameterType="String">
delete from aima_phone where id = #{id}
</delete>
<delete id="deleteAimaPhoneByIds" parameterType="String">
delete from aima_phone where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>

View File

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cmvr.aima.mapper.AimaTaskInstanceMapper">
<resultMap type="AimaTaskInstance" id="AimaTaskInstanceResult">
<result property="id" column="id"/><result property="createBy" column="create_by"/><result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/><result property="updateTime" column="update_time"/><result property="remark" column="remark"/>
<result property="taskId" column="task_id"/><result property="phoneId" column="phone_id"/>
<result property="vehicleId" column="vehicle_id"/>
<result property="executionNumber" column="execution_number"/><result property="status" column="status"/><result property="taskInsId" column="task_ins_id"/>
<result property="totalCases" column="total_cases"/>
<result property="completedCases" column="completed_cases"/><result property="passedCases" column="passed_cases"/><result property="failedCases" column="failed_cases"/>
<result property="progress" column="progress"/>
<result property="startTime" column="start_time"/><result property="endTime" column="end_time"/>
</resultMap>
<resultMap type="com.cmvr.aima.domain.vo.AimaTaskInstanceVo" id="AimaTaskInstanceVoResult">
<result property="id" column="id"/><result property="createBy" column="create_by"/><result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/><result property="updateTime" column="update_time"/><result property="remark" column="remark"/>
<result property="taskId" column="task_id"/><result property="taskCode" column="task_code"/><result property="taskName" column="task_name"/>
<result property="phoneId" column="phone_id"/><result property="phoneName" column="phone_name"/>
<result property="vehicleId" column="vehicle_id"/><result property="vehicleName" column="vehicle_name"/><result property="vin" column="vin"/>
<result property="executionNumber" column="execution_number"/><result property="status" column="status"/><result property="taskInsId" column="task_ins_id"/>
<result property="totalCases" column="total_cases"/>
<result property="completedCases" column="completed_cases"/><result property="passedCases" column="passed_cases"/><result property="failedCases" column="failed_cases"/>
<result property="progress" column="progress"/>
<result property="startTime" column="start_time"/><result property="endTime" column="end_time"/>
<result property="createByName" column="create_by_name"/><result property="updateByName" column="update_by_name"/>
</resultMap>
<sql id="selectAimaTaskInstanceVo">
select id, create_by, create_time, update_by, update_time, remark, task_id, phone_id, vehicle_id, execution_number, status, task_ins_id, total_cases, completed_cases, passed_cases, failed_cases, progress, start_time, end_time from aima_task_instance
</sql>
<select id="selectAimaTaskInstanceList" parameterType="AimaTaskInstance" resultMap="AimaTaskInstanceResult">
<include refid="selectAimaTaskInstanceVo"/>
<where>
<if test="taskId != null and taskId != ''"> and task_id = #{taskId}</if>
<if test="phoneId != null and phoneId != ''"> and phone_id = #{phoneId}</if>
<if test="vehicleId != null and vehicleId != ''"> and vehicle_id = #{vehicleId}</if>
<if test="status != null"> and status = #{status}</if>
</where>
order by create_time desc
</select>
<select id="selectAimaTaskInstanceById" parameterType="String" resultMap="AimaTaskInstanceResult">
<include refid="selectAimaTaskInstanceVo"/> where id = #{id}
</select>
<select id="selectAimaTaskInstanceVoList" parameterType="com.cmvr.aima.domain.dto.AimaTaskInstanceQuery" resultMap="AimaTaskInstanceVoResult">
select ti.id, ti.create_by, ti.create_time, ti.update_by, ti.update_time, ti.remark,
ti.task_id, t.task_code, t.task_name,
ti.phone_id, p.phone_name, ti.vehicle_id, v.vehicle_name, v.vin,
ti.execution_number, ti.status, ti.task_ins_id,
ti.total_cases, ti.completed_cases, ti.passed_cases, ti.failed_cases, ti.progress,
ti.start_time, ti.end_time,
u1.nick_name as create_by_name, u2.nick_name as update_by_name
from aima_task_instance ti
left join aima_task t on ti.task_id = t.id
left join aima_phone p on ti.phone_id = p.id
left join aima_vehicle v on ti.vehicle_id = v.id
left join sys_user u1 on ti.create_by = u1.user_name
left join sys_user u2 on ti.update_by = u2.user_name
<where>
<if test="taskId != null and taskId != ''"> and ti.task_id = #{taskId}</if>
<if test="phoneId != null and phoneId != ''"> and ti.phone_id = #{phoneId}</if>
<if test="vehicleId != null and vehicleId != ''"> and ti.vehicle_id = #{vehicleId}</if>
<if test="status != null"> and ti.status = #{status}</if>
<if test="statusList != null and statusList.size() > 0">
and ti.status in
<foreach item="statusItem" collection="statusList" open="(" separator="," close=")">
#{statusItem}
</foreach>
</if>
</where>
order by ti.create_time desc
</select>
<insert id="insertAimaTaskInstance" parameterType="AimaTaskInstance">
insert into aima_task_instance <trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if><if test="createBy != null">create_by,</if><if test="createTime != null">create_time,</if>
<if test="updateBy != null">update_by,</if><if test="updateTime != null">update_time,</if><if test="remark != null">remark,</if>
<if test="taskId != null">task_id,</if><if test="phoneId != null">phone_id,</if>
<if test="vehicleId != null">vehicle_id,</if>
<if test="executionNumber != null">execution_number,</if><if test="status != null">status,</if><if test="totalCases != null">total_cases,</if>
<if test="completedCases != null">completed_cases,</if><if test="passedCases != null">passed_cases,</if><if test="failedCases != null">failed_cases,</if>
<if test="progress != null">progress,</if>
<if test="startTime != null">start_time,</if><if test="endTime != null">end_time,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">#{id},</if><if test="createBy != null">#{createBy},</if><if test="createTime != null">#{createTime},</if>
<if test="updateBy != null">#{updateBy},</if><if test="updateTime != null">#{updateTime},</if><if test="remark != null">#{remark},</if>
<if test="taskId != null">#{taskId},</if><if test="phoneId != null">#{phoneId},</if>
<if test="vehicleId != null">#{vehicleId},</if>
<if test="executionNumber != null">#{executionNumber},</if><if test="status != null">#{status},</if><if test="totalCases != null">#{totalCases},</if>
<if test="completedCases != null">#{completedCases},</if><if test="passedCases != null">#{passedCases},</if><if test="failedCases != null">#{failedCases},</if>
<if test="progress != null">#{progress},</if>
<if test="startTime != null">#{startTime},</if><if test="endTime != null">#{endTime},</if>
</trim>
</insert>
<update id="updateAimaTaskInstance" parameterType="AimaTaskInstance">
update aima_task_instance <trim prefix="SET" suffixOverrides=",">
<if test="createBy != null">create_by = #{createBy},</if><if test="createTime != null">create_time = #{createTime},</if>
<if test="updateBy != null">update_by = #{updateBy},</if><if test="updateTime != null">update_time = #{updateTime},</if>
<if test="remark != null">remark = #{remark},</if><if test="taskId != null">task_id = #{taskId},</if>
<if test="phoneId != null">phone_id = #{phoneId},</if>
<if test="vehicleId != null">vehicle_id = #{vehicleId},</if>
<if test="executionNumber != null">execution_number = #{executionNumber},</if>
<if test="status != null">status = #{status},</if><if test="totalCases != null">total_cases = #{totalCases},</if>
<if test="completedCases != null">completed_cases = #{completedCases},</if><if test="passedCases != null">passed_cases = #{passedCases},</if>
<if test="failedCases != null">failed_cases = #{failedCases},</if><if test="progress != null">progress = #{progress},</if>
<if test="startTime != null">start_time = #{startTime},</if><if test="endTime != null">end_time = #{endTime},</if>
<if test="taskInsId != null">task_ins_id = #{taskInsId},</if>
</trim> where id = #{id}
</update>
<delete id="deleteAimaTaskInstanceById" parameterType="String">delete from aima_task_instance where id = #{id}</delete>
<delete id="deleteAimaTaskInstanceByIds" parameterType="String">
delete from aima_task_instance where id in <foreach item="id" collection="array" open="(" separator="," close=")">#{id}</foreach>
</delete>
</mapper>

View File

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cmvr.aima.mapper.AimaTaskMapper">
<resultMap type="AimaTask" id="AimaTaskResult">
<result property="id" column="id"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="remark" column="remark"/>
<result property="taskCode" column="task_code"/>
<result property="taskName" column="task_name"/>
<result property="taskType" column="task_type"/>
<result property="phoneId" column="phone_id"/>
<result property="vehicleId" column="vehicle_id"/>
<result property="testCaseCount" column="test_case_count"/>
<result property="taskConfigId" column="task_config_id"/>
</resultMap>
<resultMap type="com.cmvr.aima.domain.vo.AimaTaskVo" id="AimaTaskVoResult">
<result property="id" column="id"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="remark" column="remark"/>
<result property="taskCode" column="task_code"/>
<result property="taskName" column="task_name"/>
<result property="taskType" column="task_type"/>
<result property="phoneId" column="phone_id"/>
<result property="vehicleId" column="vehicle_id"/>
<result property="testCaseCount" column="test_case_count"/>
<result property="taskConfigId" column="task_config_id"/>
<result property="phoneName" column="phone_name"/>
<result property="vin" column="vin"/>
<result property="createByName" column="create_by_name"/>
<result property="updateByName" column="update_by_name"/>
</resultMap>
<sql id="selectAimaTaskVo">
select id, create_by, create_time, update_by, update_time, remark, task_code, task_name, task_type, phone_id, vehicle_id, test_case_count, task_config_id from aima_task
</sql>
<select id="selectAimaTaskList" parameterType="AimaTask" resultMap="AimaTaskResult">
<include refid="selectAimaTaskVo"/>
<where>
<if test="taskCode != null and taskCode != ''"> and task_code = #{taskCode}</if>
<if test="taskName != null and taskName != ''"> and task_name like concat('%', #{taskName}, '%')</if>
<if test="taskType != null and taskType != ''"> and task_type = #{taskType}</if>
<if test="phoneId != null and phoneId != ''"> and phone_id = #{phoneId}</if>
<if test="vehicleId != null and vehicleId != ''"> and vehicle_id = #{vehicleId}</if>
</where>
order by create_time desc
</select>
<select id="selectAimaTaskById" parameterType="String" resultMap="AimaTaskResult">
<include refid="selectAimaTaskVo"/> where id = #{id}
</select>
<select id="selectAimaTaskVoList" parameterType="AimaTask" resultMap="AimaTaskVoResult">
select t.id, t.create_by, t.create_time, t.update_by, t.update_time, t.remark,
t.task_code, t.task_name, t.task_type, t.phone_id, t.vehicle_id,
t.test_case_count, t.task_config_id,
p.phone_name, v.vin,
u1.nick_name as create_by_name, u2.nick_name as update_by_name
from aima_task t
left join aima_phone p on t.phone_id = p.id
left join aima_vehicle v on t.vehicle_id = v.id
left join sys_user u1 on t.create_by = u1.user_name
left join sys_user u2 on t.update_by = u2.user_name
<where>
<if test="taskCode != null and taskCode != ''"> and t.task_code = #{taskCode}</if>
<if test="taskName != null and taskName != ''"> and t.task_name like concat('%', #{taskName}, '%')</if>
<if test="taskType != null and taskType != ''"> and t.task_type = #{taskType}</if>
<if test="phoneId != null and phoneId != ''"> and t.phone_id = #{phoneId}</if>
<if test="vehicleId != null and vehicleId != ''"> and t.vehicle_id = #{vehicleId}</if>
</where>
order by t.create_time desc
</select>
<insert id="insertAimaTask" parameterType="AimaTask">
insert into aima_task <trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if><if test="createBy != null">create_by,</if><if test="createTime != null">create_time,</if>
<if test="updateBy != null">update_by,</if><if test="updateTime != null">update_time,</if><if test="remark != null">remark,</if>
<if test="taskCode != null">task_code,</if><if test="taskName != null">task_name,</if><if test="taskType != null">task_type,</if>
<if test="phoneId != null">phone_id,</if><if test="vehicleId != null">vehicle_id,</if>
<if test="testCaseCount != null">test_case_count,</if><if test="taskConfigId != null">task_config_id,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">#{id},</if><if test="createBy != null">#{createBy},</if><if test="createTime != null">#{createTime},</if>
<if test="updateBy != null">#{updateBy},</if><if test="updateTime != null">#{updateTime},</if><if test="remark != null">#{remark},</if>
<if test="taskCode != null">#{taskCode},</if><if test="taskName != null">#{taskName},</if><if test="taskType != null">#{taskType},</if>
<if test="phoneId != null">#{phoneId},</if><if test="vehicleId != null">#{vehicleId},</if>
<if test="testCaseCount != null">#{testCaseCount},</if><if test="taskConfigId != null">#{taskConfigId},</if>
</trim>
</insert>
<update id="updateAimaTask" parameterType="AimaTask">
update aima_task <trim prefix="SET" suffixOverrides=",">
<if test="createBy != null">create_by = #{createBy},</if><if test="createTime != null">create_time = #{createTime},</if>
<if test="updateBy != null">update_by = #{updateBy},</if><if test="updateTime != null">update_time = #{updateTime},</if>
<if test="remark != null">remark = #{remark},</if><if test="taskCode != null">task_code = #{taskCode},</if>
<if test="taskName != null">task_name = #{taskName},</if><if test="taskType != null">task_type = #{taskType},</if>
<if test="phoneId != null">phone_id = #{phoneId},</if>
<if test="vehicleId != null">vehicle_id = #{vehicleId},</if>
<if test="testCaseCount != null">test_case_count = #{testCaseCount},</if><if test="taskConfigId != null">task_config_id = #{taskConfigId},</if>
</trim> where id = #{id}
</update>
<delete id="deleteAimaTaskById" parameterType="String">delete from aima_task where id = #{id}</delete>
<delete id="deleteAimaTaskByIds" parameterType="String">
delete from aima_task where id in <foreach item="id" collection="array" open="(" separator="," close=")">#{id}</foreach>
</delete>
</mapper>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cmvr.aima.mapper.AimaTaskTestCaseMapper">
<insert id="batchInsertTaskTestCase">
INSERT INTO aima_task_test_case (task_id, test_case_id, sort_order) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.taskId}, #{item.testCaseId}, #{item.sortOrder})
</foreach>
</insert>
<delete id="deleteTaskTestCaseByTaskId" parameterType="String">
DELETE FROM aima_task_test_case WHERE task_id = #{taskId}
</delete>
<select id="selectTestCaseIdsByTaskId" parameterType="String" resultType="String">
SELECT test_case_id FROM aima_task_test_case
WHERE task_id = #{taskId}
ORDER BY sort_order
</select>
<delete id="deleteTaskTestCaseByTaskIds" parameterType="String">
DELETE FROM aima_task_test_case WHERE task_id IN
<foreach item="taskId" collection="array" open="(" separator="," close=")">
#{taskId}
</foreach>
</delete>
</mapper>

View File

@ -0,0 +1,167 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cmvr.aima.mapper.AimaTestCaseMapper">
<resultMap type="AimaTestCase" id="AimaTestCaseResult">
<result property="id" column="id"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="remark" column="remark"/>
<result property="caseCode" column="case_code"/>
<result property="caseName" column="case_name"/>
<result property="caseCategory" column="case_category"/>
<result property="testDescription" column="test_description"/>
<result property="precondition" column="precondition"/>
<result property="testSteps" column="test_steps"/>
<result property="expectedResult" column="expected_result"/>
<result property="priority" column="priority"/>
<result property="enabled" column="enabled"/>
<result property="sortOrder" column="sort_order"/>
<result property="estimatedDuration" column="estimated_duration"/>
<result property="detectItemId" column="detect_item_id"/>
</resultMap>
<resultMap type="com.cmvr.aima.domain.vo.AimaTestCaseVo" id="AimaTestCaseVoResult">
<result property="id" column="id"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="remark" column="remark"/>
<result property="caseCode" column="case_code"/>
<result property="caseName" column="case_name"/>
<result property="caseCategory" column="case_category"/>
<result property="testDescription" column="test_description"/>
<result property="precondition" column="precondition"/>
<result property="testSteps" column="test_steps"/>
<result property="expectedResult" column="expected_result"/>
<result property="priority" column="priority"/>
<result property="enabled" column="enabled"/>
<result property="sortOrder" column="sort_order"/>
<result property="estimatedDuration" column="estimated_duration"/>
<result property="detectItemId" column="detect_item_id"/>
<result property="configStatus" column="config_status"/>
<result property="createByName" column="create_by_name"/>
<result property="updateByName" column="update_by_name"/>
</resultMap>
<sql id="selectAimaTestCaseVo">
select id, create_by, create_time, update_by, update_time, remark, case_code, case_name, case_category, test_description, precondition, test_steps, expected_result, priority, enabled, sort_order, estimated_duration, detect_item_id from aima_test_case
</sql>
<select id="selectAimaTestCaseList" parameterType="AimaTestCase" resultMap="AimaTestCaseResult">
<include refid="selectAimaTestCaseVo"/>
<where>
<if test="caseCode != null and caseCode != ''"> and case_code = #{caseCode}</if>
<if test="caseName != null and caseName != ''"> and case_name like concat('%', #{caseName}, '%')</if>
<if test="caseCategory != null and caseCategory != ''"> and case_category = #{caseCategory}</if>
<if test="enabled != null and enabled != ''"> and enabled = #{enabled}</if>
</where>
order by sort_order asc, create_time desc
</select>
<select id="selectAimaTestCaseById" parameterType="String" resultMap="AimaTestCaseResult">
<include refid="selectAimaTestCaseVo"/>
where id = #{id}
</select>
<select id="selectAimaTestCaseVoList" parameterType="AimaTestCase" resultMap="AimaTestCaseVoResult">
select tc.id, tc.create_by, tc.create_time, tc.update_by, tc.update_time, tc.remark,
tc.case_code, tc.case_name, tc.case_category, tc.test_description, tc.precondition,
tc.test_steps, tc.expected_result, tc.priority, tc.enabled, tc.sort_order,
tc.estimated_duration, tc.detect_item_id,
d.is_deploy as config_status,
u1.nick_name as create_by_name, u2.nick_name as update_by_name
from aima_test_case tc
left join te_detection_item d on tc.detect_item_id = d.id
left join sys_user u1 on tc.create_by = u1.user_name
left join sys_user u2 on tc.update_by = u2.user_name
<where>
<if test="caseCode != null and caseCode != ''"> and tc.case_code = #{caseCode}</if>
<if test="caseName != null and caseName != ''"> and tc.case_name like concat('%', #{caseName}, '%')</if>
<if test="caseCategory != null and caseCategory != ''"> and tc.case_category = #{caseCategory}</if>
<if test="enabled != null and enabled != ''"> and tc.enabled = #{enabled}</if>
</where>
order by tc.sort_order, tc.create_time desc
</select>
<insert id="insertAimaTestCase" parameterType="AimaTestCase">
insert into aima_test_case
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if>
<if test="createBy != null">create_by,</if>
<if test="createTime != null">create_time,</if>
<if test="updateBy != null">update_by,</if>
<if test="updateTime != null">update_time,</if>
<if test="remark != null">remark,</if>
<if test="caseCode != null">case_code,</if>
<if test="caseName != null">case_name,</if>
<if test="caseCategory != null">case_category,</if>
<if test="testDescription != null">test_description,</if>
<if test="precondition != null">precondition,</if>
<if test="testSteps != null">test_steps,</if>
<if test="expectedResult != null">expected_result,</if>
<if test="priority != null">priority,</if>
<if test="enabled != null">enabled,</if>
<if test="sortOrder != null">sort_order,</if>
<if test="estimatedDuration != null">estimated_duration,</if>
<if test="detectItemId != null">detect_item_id,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">#{id},</if>
<if test="createBy != null">#{createBy},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateBy != null">#{updateBy},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="remark != null">#{remark},</if>
<if test="caseCode != null">#{caseCode},</if>
<if test="caseName != null">#{caseName},</if>
<if test="caseCategory != null">#{caseCategory},</if>
<if test="testDescription != null">#{testDescription},</if>
<if test="precondition != null">#{precondition},</if>
<if test="testSteps != null">#{testSteps},</if>
<if test="expectedResult != null">#{expectedResult},</if>
<if test="priority != null">#{priority},</if>
<if test="enabled != null">#{enabled},</if>
<if test="sortOrder != null">#{sortOrder},</if>
<if test="estimatedDuration != null">#{estimatedDuration},</if>
<if test="detectItemId != null">#{detectItemId},</if>
</trim>
</insert>
<update id="updateAimaTestCase" parameterType="AimaTestCase">
update aima_test_case
<trim prefix="SET" suffixOverrides=",">
<if test="createBy != null">create_by = #{createBy},</if>
<if test="createTime != null">create_time = #{createTime},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="caseCode != null">case_code = #{caseCode},</if>
<if test="caseName != null">case_name = #{caseName},</if>
<if test="caseCategory != null">case_category = #{caseCategory},</if>
<if test="testDescription != null">test_description = #{testDescription},</if>
<if test="precondition != null">precondition = #{precondition},</if>
<if test="testSteps != null">test_steps = #{testSteps},</if>
<if test="expectedResult != null">expected_result = #{expectedResult},</if>
<if test="priority != null">priority = #{priority},</if>
<if test="enabled != null">enabled = #{enabled},</if>
<if test="sortOrder != null">sort_order = #{sortOrder},</if>
<if test="estimatedDuration != null">estimated_duration = #{estimatedDuration},</if>
<if test="detectItemId != null">detect_item_id = #{detectItemId},</if>
</trim>
where id = #{id}
</update>
<delete id="deleteAimaTestCaseById" parameterType="String">
delete from aima_test_case where id = #{id}
</delete>
<delete id="deleteAimaTestCaseByIds" parameterType="String">
delete from aima_test_case where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cmvr.aima.mapper.AimaTestLogMapper">
<resultMap type="AimaTestLog" id="AimaTestLogResult">
<result property="id" column="id"/><result property="createBy" column="create_by"/><result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/><result property="updateTime" column="update_time"/><result property="remark" column="remark"/>
<result property="taskInstanceId" column="task_instance_id"/><result property="taskId" column="task_id"/><result property="taskName" column="task_name"/>
<result property="testCaseId" column="test_case_id"/><result property="caseName" column="case_name"/><result property="logLevel" column="log_level"/>
<result property="logType" column="log_type"/><result property="logContent" column="log_content"/><result property="logTime" column="log_time"/>
<result property="executionStep" column="execution_step"/><result property="actualResult" column="actual_result"/><result property="testStatus" column="test_status"/>
<result property="screenshotUrl" column="screenshot_url"/><result property="videoUrl" column="video_url"/><result property="errorStack" column="error_stack"/>
<result property="durationMs" column="duration_ms"/>
<result property="progress" column="progress"/>
<result property="status" column="status"/>
</resultMap>
<sql id="selectAimaTestLogVo">
select id, create_by, create_time, update_by, update_time, remark, task_instance_id, task_id, task_name, test_case_id, case_name, log_level, log_type, log_content, log_time, execution_step, actual_result, test_status, screenshot_url, video_url, error_stack, duration_ms,progress,status from aima_test_log
</sql>
<select id="selectAimaTestLogList" parameterType="AimaTestLog" resultMap="AimaTestLogResult">
<include refid="selectAimaTestLogVo"/>
<where>
<if test="taskInstanceId != null and taskInstanceId != ''"> and task_instance_id = #{taskInstanceId}</if>
<if test="taskId != null and taskId != ''"> and task_id = #{taskId}</if>
<if test="testCaseId != null and testCaseId != ''"> and test_case_id = #{testCaseId}</if>
<if test="logLevel != null and logLevel != ''"> and log_level = #{logLevel}</if>
<if test="testStatus != null and testStatus != ''"> and test_status = #{testStatus}</if>
</where>
order by log_time
</select>
<select id="selectAimaTestLogById" parameterType="String" resultMap="AimaTestLogResult">
<include refid="selectAimaTestLogVo"/> where id = #{id}
</select>
<insert id="insertAimaTestLog" parameterType="AimaTestLog">
insert into aima_test_log <trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if><if test="createBy != null">create_by,</if><if test="createTime != null">create_time,</if>
<if test="updateBy != null">update_by,</if><if test="updateTime != null">update_time,</if><if test="remark != null">remark,</if>
<if test="taskInstanceId != null">task_instance_id,</if><if test="taskId != null">task_id,</if><if test="taskName != null">task_name,</if>
<if test="testCaseId != null">test_case_id,</if><if test="caseName != null">case_name,</if><if test="logLevel != null">log_level,</if>
<if test="logType != null">log_type,</if><if test="logContent != null">log_content,</if><if test="logTime != null">log_time,</if>
<if test="executionStep != null">execution_step,</if><if test="actualResult != null">actual_result,</if><if test="testStatus != null">test_status,</if>
<if test="screenshotUrl != null">screenshot_url,</if><if test="videoUrl != null">video_url,</if><if test="errorStack != null">error_stack,</if>
<if test="durationMs != null">duration_ms,</if>
<if test="progress != null">progress,</if>
<if test="status != null">status,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">#{id},</if><if test="createBy != null">#{createBy},</if><if test="createTime != null">#{createTime},</if>
<if test="updateBy != null">#{updateBy},</if><if test="updateTime != null">#{updateTime},</if><if test="remark != null">#{remark},</if>
<if test="taskInstanceId != null">#{taskInstanceId},</if><if test="taskId != null">#{taskId},</if><if test="taskName != null">#{taskName},</if>
<if test="testCaseId != null">#{testCaseId},</if><if test="caseName != null">#{caseName},</if><if test="logLevel != null">#{logLevel},</if>
<if test="logType != null">#{logType},</if><if test="logContent != null">#{logContent},</if><if test="logTime != null">#{logTime},</if>
<if test="executionStep != null">#{executionStep},</if><if test="actualResult != null">#{actualResult},</if><if test="testStatus != null">#{testStatus},</if>
<if test="screenshotUrl != null">#{screenshotUrl},</if><if test="videoUrl != null">#{videoUrl},</if><if test="errorStack != null">#{errorStack},</if>
<if test="durationMs != null">#{durationMs},</if>
<if test="progress != null">#{progress},</if>
<if test="status != null">#{status},</if>
</trim>
</insert>
<update id="updateAimaTestLog" parameterType="AimaTestLog">
update aima_test_log <trim prefix="SET" suffixOverrides=",">
<if test="createBy != null">create_by = #{createBy},</if><if test="createTime != null">create_time = #{createTime},</if>
<if test="updateBy != null">update_by = #{updateBy},</if><if test="updateTime != null">update_time = #{updateTime},</if>
<if test="remark != null">remark = #{remark},</if><if test="taskInstanceId != null">task_instance_id = #{taskInstanceId},</if>
<if test="taskId != null">task_id = #{taskId},</if><if test="taskName != null">task_name = #{taskName},</if>
<if test="testCaseId != null">test_case_id = #{testCaseId},</if><if test="caseName != null">case_name = #{caseName},</if>
<if test="logLevel != null">log_level = #{logLevel},</if><if test="logType != null">log_type = #{logType},</if>
<if test="logContent != null"> log_content = #{logContent},</if><if test="logTime != null">log_time = #{logTime},</if>
<if test="executionStep != null">execution_step = #{executionStep},</if><if test="actualResult != null">actual_result = #{actualResult},</if>
<if test="testStatus != null">test_status = #{testStatus},</if><if test="screenshotUrl != null">screenshot_url = #{screenshotUrl},</if>
<if test="videoUrl != null">video_url = #{videoUrl},</if><if test="errorStack != null">error_stack = #{errorStack},</if>
<if test="durationMs != null">duration_ms = #{durationMs},</if>
<if test="progress != null">progress = #{progress},</if>
<if test="status != null">status = #{status},</if>
</trim> where id = #{id}
</update>
<delete id="deleteAimaTestLogById" parameterType="String">delete from aima_test_log where id = #{id}</delete>
<delete id="deleteAimaTestLogByIds" parameterType="String">
delete from aima_test_log where id in <foreach item="id" collection="array" open="(" separator="," close=")">#{id}</foreach>
</delete>
</mapper>

View File

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cmvr.aima.mapper.AimaVehicleMapper">
<resultMap type="AimaVehicle" id="AimaVehicleResult">
<result property="id" column="id" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
<result property="vin" column="vin" />
<result property="vehicleModel" column="vehicle_model" />
<result property="vehicleName" column="vehicle_name" />
<result property="color" column="color" />
<result property="productionDate" column="production_date" />
<result property="status" column="status" />
</resultMap>
<sql id="selectAimaVehicleVo">
select id, create_by, create_time, update_by, update_time, remark, vin, vehicle_model, vehicle_name, color, production_date, status from aima_vehicle
</sql>
<select id="selectAimaVehicleList" parameterType="AimaVehicle" resultMap="AimaVehicleResult">
<include refid="selectAimaVehicleVo"/>
<where>
<if test="vin != null and vin != ''"> and vin = #{vin}</if>
<if test="vehicleModel != null and vehicleModel != ''"> and vehicle_model like concat('%', #{vehicleModel}, '%')</if>
<if test="vehicleName != null and vehicleName != ''"> and vehicle_name like concat('%', #{vehicleName}, '%')</if>
<if test="status != null and status != ''"> and status = #{status}</if>
</where>
order by create_time desc
</select>
<select id="selectAimaVehicleById" parameterType="String" resultMap="AimaVehicleResult">
<include refid="selectAimaVehicleVo"/>
where id = #{id}
</select>
<insert id="insertAimaVehicle" parameterType="AimaVehicle">
insert into aima_vehicle
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if>
<if test="createBy != null">create_by,</if>
<if test="createTime != null">create_time,</if>
<if test="updateBy != null">update_by,</if>
<if test="updateTime != null">update_time,</if>
<if test="remark != null">remark,</if>
<if test="vin != null">vin,</if>
<if test="vehicleModel != null">vehicle_model,</if>
<if test="vehicleName != null">vehicle_name,</if>
<if test="color != null">color,</if>
<if test="productionDate != null">production_date,</if>
<if test="status != null">status,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">#{id},</if>
<if test="createBy != null">#{createBy},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateBy != null">#{updateBy},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="remark != null">#{remark},</if>
<if test="vin != null">#{vin},</if>
<if test="vehicleModel != null">#{vehicleModel},</if>
<if test="vehicleName != null">#{vehicleName},</if>
<if test="color != null">#{color},</if>
<if test="productionDate != null">#{productionDate},</if>
<if test="status != null">#{status},</if>
</trim>
</insert>
<update id="updateAimaVehicle" parameterType="AimaVehicle">
update aima_vehicle
<trim prefix="SET" suffixOverrides=",">
<if test="createBy != null">create_by = #{createBy},</if>
<if test="createTime != null">create_time = #{createTime},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="vin != null">vin = #{vin},</if>
<if test="vehicleModel != null">vehicle_model = #{vehicleModel},</if>
<if test="vehicleName != null">vehicle_name = #{vehicleName},</if>
<if test="color != null">color = #{color},</if>
<if test="productionDate != null">production_date = #{productionDate},</if>
<if test="status != null">status = #{status},</if>
</trim>
where id = #{id}
</update>
<delete id="deleteAimaVehicleById" parameterType="String">
delete from aima_vehicle where id = #{id}
</delete>
<delete id="deleteAimaVehicleByIds" parameterType="String">
delete from aima_vehicle where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>

View File

@ -55,6 +55,12 @@
<version>1.5.9</version> <version>1.5.9</version>
</dependency> </dependency>
<dependency>
<groupId>dev.onvoid.webrtc</groupId>
<artifactId>webrtc-java</artifactId>
<version>0.14.0</version>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -0,0 +1,887 @@
package com.cmvr.edge.client.adapter;
import cmvr.api.Common;
import cmvr.api.MicPhoneServiceGrpc;
import cmvr.api.MicrophoneCommand;
import cmvr.api.SpeakerCommand;
import cmvr.api.SpeakerServiceGrpc;
import com.cmvr.edge.client.manage.GrpcServiceManager;
import com.cmvr.edge.client.utils.EdgeCommonUtil;
import com.google.protobuf.ByteString;
import io.grpc.Status;
import io.grpc.stub.ClientCallStreamObserver;
import io.grpc.stub.ClientResponseObserver;
import io.grpc.stub.StreamObserver;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PreDestroy;
import java.util.Arrays;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
/**
* 实时语音对讲 gRPC 适配层
*
* <p>当前机器人侧不再使用旧的 RobotAudio 双向流 proto而是拆成两个现有服务
* 浏览器 -> 机器人SpeakerService.StreamAudio 客户端流
* 机器人 -> 浏览器MicPhoneService.StreamAudio 服务端流</p>
*
* <p>音频格式固定为 PCM_S16LE / 48000Hz / 单声道 / 10ms / 960 bytes
* 所有发送到 gRPC StreamObserver 的动作都绑定到会话内单线程避免并发 onNext 引发底层崩溃</p>
*/
@Component
@RequiredArgsConstructor
public class RobotAudioGrpcAdapter {
private static final Logger log = LoggerFactory.getLogger(RobotAudioGrpcAdapter.class);
private static final int SAMPLE_RATE = 48000;
private static final int CHANNELS = 1;
private static final int BITS_PER_SAMPLE = 16;
private static final int FRAME_MS = 10;
private static final int FRAME_SAMPLES = SAMPLE_RATE * FRAME_MS / 1000;
private static final int FRAME_BYTES = FRAME_SAMPLES * CHANNELS * (BITS_PER_SAMPLE / 8);
private static final String CODEC = "PCM_S16LE";
private static final int MAX_QUEUED_BROWSER_FRAMES = 10;
private static final int TARGET_QUEUED_BROWSER_FRAMES = 2;
private static final long HEARTBEAT_INTERVAL_MS = 1000L;
private static final long HEARTBEAT_DEADLINE_MS = 2000L;
private final GrpcServiceManager grpcServiceManager;
private final ConcurrentMap<String, RobotAudioStream> streams = new ConcurrentHashMap<>();
public RobotAudioStream openStream(String terminalId,
String deviceId,
String sessionId,
String operatorId,
RobotAudioStreamListener listener) {
return openStream(terminalId, deviceId, deviceId, sessionId, operatorId, listener);
}
public RobotAudioStream openStream(String terminalId,
String speakerDeviceId,
String micDeviceId,
String sessionId,
String operatorId,
RobotAudioStreamListener listener) {
SpeakerServiceGrpc.SpeakerServiceStub speakerStub =
grpcServiceManager.getGrpcClient(terminalId, SpeakerServiceGrpc.SpeakerServiceStub.class);
MicPhoneServiceGrpc.MicPhoneServiceStub micStub =
grpcServiceManager.getGrpcClient(terminalId, MicPhoneServiceGrpc.MicPhoneServiceStub.class);
SpeakerServiceGrpc.SpeakerServiceBlockingStub speakerBlockingStub =
grpcServiceManager.getGrpcClient(terminalId, SpeakerServiceGrpc.SpeakerServiceBlockingStub.class);
MicPhoneServiceGrpc.MicPhoneServiceBlockingStub micBlockingStub =
grpcServiceManager.getGrpcClient(terminalId, MicPhoneServiceGrpc.MicPhoneServiceBlockingStub.class);
preflightDeviceStatus(speakerBlockingStub, micBlockingStub, speakerDeviceId, micDeviceId, sessionId);
RobotAudioStream stream = new RobotAudioStream(
terminalId, speakerDeviceId, micDeviceId, sessionId, operatorId, speakerStub, micStub, listener);
RobotAudioStream exists = streams.putIfAbsent(sessionId, stream);
if (exists != null) {
throw new IllegalStateException("实时语音会话已存在:" + sessionId);
}
try {
stream.start();
log.info("实时对讲 gRPC 流已创建sessionId={}terminalId={}speakerDeviceId={}micDeviceId={}operatorId={}sampleRate={}channels={}bits={}frameMs={}frameBytes={}",
sessionId, terminalId, speakerDeviceId, micDeviceId, operatorId, SAMPLE_RATE, CHANNELS, BITS_PER_SAMPLE, FRAME_MS, FRAME_BYTES);
return stream;
} catch (RuntimeException e) {
streams.remove(sessionId, stream);
stream.closeLocalResources();
throw e;
}
}
@PreDestroy
public void destroy() {
for (RobotAudioStream stream : streams.values()) {
try {
stream.cancel("application shutdown");
} catch (Exception e) {
log.debug("关闭实时语音流异常sessionId={}", stream.getSessionId(), e);
}
}
streams.clear();
}
private void preflightDeviceStatus(SpeakerServiceGrpc.SpeakerServiceBlockingStub speakerStub,
MicPhoneServiceGrpc.MicPhoneServiceBlockingStub micStub,
String speakerDeviceId,
String micDeviceId,
String sessionId) {
try {
SpeakerCommand.GetSpeakerStateCommand.Request speakerRequest = SpeakerCommand.GetSpeakerStateCommand.Request.newBuilder()
.setHeader(EdgeCommonUtil.buildRequest(speakerDeviceId))
.build();
SpeakerCommand.GetSpeakerStateCommand.Feedback speakerFeedback =
speakerStub.withDeadlineAfter(HEARTBEAT_DEADLINE_MS, TimeUnit.MILLISECONDS).getStatus(speakerRequest);
if (speakerFeedback != null && speakerFeedback.hasHeader() && !speakerFeedback.getHeader().getSuccess()) {
throw new IllegalStateException("speaker device unavailable: " + speakerFeedback.getHeader().getErrorMessage());
}
MicrophoneCommand.GetMicStateCommand.Request micRequest = MicrophoneCommand.GetMicStateCommand.Request.newBuilder()
.setHeader(EdgeCommonUtil.buildRequest(micDeviceId))
.build();
MicrophoneCommand.GetMicStateCommand.Feedback micFeedback =
micStub.withDeadlineAfter(HEARTBEAT_DEADLINE_MS, TimeUnit.MILLISECONDS).getStatus(micRequest);
if (micFeedback != null && micFeedback.hasHeader() && !micFeedback.getHeader().getSuccess()) {
throw new IllegalStateException("microphone device unavailable: " + micFeedback.getHeader().getErrorMessage());
}
log.info("实时对讲设备预检通过sessionId={}speakerDeviceId={}micDeviceId={}", sessionId, speakerDeviceId, micDeviceId);
} catch (RuntimeException e) {
log.error("实时对讲设备预检失败sessionId={}speakerDeviceId={}micDeviceId={}", sessionId, speakerDeviceId, micDeviceId, e);
throw e;
}
}
public interface RobotAudioStreamListener {
void onStarted(String sessionId, boolean accepted, String message);
void onRobotAudio(String sessionId, long sequence, long timestampMs, byte[] pcm);
void onHeartbeat(String sessionId, long timestampMs);
void onRobotStop(String sessionId, String reason);
void onError(String sessionId, Throwable throwable);
void onCompleted(String sessionId);
}
public class RobotAudioStream {
private final String terminalId;
private final String speakerDeviceId;
private final String micDeviceId;
private final String sessionId;
private final String operatorId;
private final SpeakerServiceGrpc.SpeakerServiceStub speakerStub;
private final MicPhoneServiceGrpc.MicPhoneServiceStub micStub;
private final RobotAudioStreamListener listener;
private final ScheduledExecutorService sendExecutor;
private final ArrayBlockingQueue<byte[]> browserFrameQueue = new ArrayBlockingQueue<>(MAX_QUEUED_BROWSER_FRAMES);
private final Object browserBufferLock = new Object();
private final AtomicBoolean closed = new AtomicBoolean(false);
private final AtomicBoolean terminalNotified = new AtomicBoolean(false);
private final AtomicBoolean heartbeatInFlight = new AtomicBoolean(false);
private final AtomicLong outboundSequence = new AtomicLong(0);
private final AtomicLong inboundSequence = new AtomicLong(0);
private final AtomicLong enqueued = new AtomicLong(0);
private final AtomicLong sent = new AtomicLong(0);
private final AtomicLong dropped = new AtomicLong(0);
private volatile StreamObserver<SpeakerCommand.StreamSpeakerAudioCommand.Request> speakerRequestObserver;
private volatile ClientCallStreamObserver<MicrophoneCommand.StreamMicAudioCommand.Request> micRequestObserver;
private volatile ScheduledFuture<?> sendFuture;
private volatile ScheduledFuture<?> heartbeatFuture;
private volatile long lastStatsLogAt;
private volatile byte[] pendingBrowserBytes = new byte[0];
private RobotAudioStream(String terminalId,
String speakerDeviceId,
String micDeviceId,
String sessionId,
String operatorId,
SpeakerServiceGrpc.SpeakerServiceStub speakerStub,
MicPhoneServiceGrpc.MicPhoneServiceStub micStub,
RobotAudioStreamListener listener) {
this.terminalId = terminalId;
this.speakerDeviceId = speakerDeviceId;
this.micDeviceId = micDeviceId;
this.sessionId = sessionId;
this.operatorId = operatorId;
this.speakerStub = speakerStub;
this.micStub = micStub;
this.listener = listener;
this.sendExecutor = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("robot-audio-send-" + sessionId));
}
private void start() {
openSpeakerStream();
openMicStream();
this.sendFuture = sendExecutor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
sendOneBrowserFrame();
}
}, 0L, FRAME_MS, TimeUnit.MILLISECONDS);
this.heartbeatFuture = sendExecutor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
probeHeartbeat();
}
}, HEARTBEAT_INTERVAL_MS, HEARTBEAT_INTERVAL_MS, TimeUnit.MILLISECONDS);
listener.onStarted(sessionId, true, "audio stream started");
}
private void openSpeakerStream() {
StreamObserver<SpeakerCommand.StreamSpeakerAudioCommand.Feedback> responseObserver =
new StreamObserver<SpeakerCommand.StreamSpeakerAudioCommand.Feedback>() {
@Override
public void onNext(SpeakerCommand.StreamSpeakerAudioCommand.Feedback feedback) {
if (closed.get()) {
return;
}
if (feedback != null && feedback.hasHeader() && !feedback.getHeader().getSuccess()) {
fail("speaker feedback", new IllegalStateException("speaker stream feedback failed: " + feedback.getHeader().getErrorMessage()));
} else {
listener.onHeartbeat(sessionId, System.currentTimeMillis());
}
}
@Override
public void onError(Throwable t) {
if (closed.get()) {
return;
}
fail("speaker stream", t);
}
@Override
public void onCompleted() {
if (closed.get()) {
return;
}
completeFromRemote();
}
};
this.speakerRequestObserver = speakerStub.streamAudio(responseObserver);
}
private void openMicStream() {
MicrophoneCommand.StreamMicAudioCommand.Request request = MicrophoneCommand.StreamMicAudioCommand.Request.newBuilder()
.setHeader(EdgeCommonUtil.buildRequest(micDeviceId))
.build();
micStub.streamAudio(request,
new ClientResponseObserver<MicrophoneCommand.StreamMicAudioCommand.Request, MicrophoneCommand.StreamMicAudioCommand.Feedback>() {
@Override
public void beforeStart(ClientCallStreamObserver<MicrophoneCommand.StreamMicAudioCommand.Request> requestStream) {
micRequestObserver = requestStream;
requestStream.disableAutoInboundFlowControl();
log.info("麦克风音频流已启用手动入站流控sessionId={}micDeviceId={}", sessionId, micDeviceId);
}
@Override
public void onNext(MicrophoneCommand.StreamMicAudioCommand.Feedback feedback) {
handleMicFeedback(feedback);
}
@Override
public void onError(Throwable t) {
if (closed.get()) {
return;
}
fail("mic stream", t);
}
@Override
public void onCompleted() {
if (closed.get()) {
return;
}
completeFromRemote();
}
});
requestNextMicFrame(1L);
}
/**
* 接收浏览器 WebSocket 二进制 PCM允许前端一次发多帧或半帧这里按 10ms 固定帧重组
*/
public void sendAudio(byte[] pcm) {
if (closed.get() || pcm == null || pcm.length == 0) {
return;
}
synchronized (browserBufferLock) {
byte[] merged = new byte[pendingBrowserBytes.length + pcm.length];
System.arraycopy(pendingBrowserBytes, 0, merged, 0, pendingBrowserBytes.length);
System.arraycopy(pcm, 0, merged, pendingBrowserBytes.length, pcm.length);
int offset = 0;
while (merged.length - offset >= FRAME_BYTES) {
byte[] frame = Arrays.copyOfRange(merged, offset, offset + FRAME_BYTES);
enqueueBrowserFrame(frame);
offset += FRAME_BYTES;
}
pendingBrowserBytes = Arrays.copyOfRange(merged, offset, merged.length);
}
}
public void sendHeartbeat() {
if (!closed.get()) {
probeHeartbeat();
}
}
public void sendStop(String reason) {
if (!closed.compareAndSet(false, true)) {
return;
}
log.info("关闭实时对讲 gRPC 流sessionId={}reason={}", sessionId, reason);
sendExecutor.execute(new Runnable() {
@Override
public void run() {
completeSpeakerStream();
cancelMicStream("browser stop");
stopTerminalAudio();
closeLocalResources();
streams.remove(sessionId, RobotAudioStream.this);
}
});
}
public void cancel(String reason) {
if (!closed.compareAndSet(false, true)) {
return;
}
log.warn("取消实时对讲 gRPC 流sessionId={}reason={}", sessionId, reason);
sendExecutor.execute(new Runnable() {
@Override
public void run() {
errorSpeakerStream(reason);
cancelMicStream(reason);
stopTerminalAudio();
closeLocalResources();
streams.remove(sessionId, RobotAudioStream.this);
}
});
}
public String getSessionId() {
return sessionId;
}
public String getTerminalId() {
return terminalId;
}
public String getDeviceId() {
return speakerDeviceId;
}
private void enqueueBrowserFrame(byte[] frame) {
if (browserFrameQueue.remainingCapacity() == 0) {
int targetDrops = Math.max(1, browserFrameQueue.size() - TARGET_QUEUED_BROWSER_FRAMES + 1);
for (int i = 0; i < targetDrops; i++) {
if (browserFrameQueue.poll() != null) {
dropped.incrementAndGet();
}
}
}
if (browserFrameQueue.offer(frame)) {
enqueued.incrementAndGet();
} else {
dropped.incrementAndGet();
}
logBrowserAudioStatsIfNeeded("browserAudioEnqueue");
}
private void sendOneBrowserFrame() {
if (closed.get()) {
return;
}
byte[] frame = browserFrameQueue.poll();
if (frame == null) {
return;
}
StreamObserver<SpeakerCommand.StreamSpeakerAudioCommand.Request> observer = speakerRequestObserver;
if (observer == null) {
dropped.incrementAndGet();
return;
}
long sequence = outboundSequence.incrementAndGet();
long timestampMs = System.currentTimeMillis();
try {
observer.onNext(buildSpeakerAudioRequest(frame, timestampMs));
sent.incrementAndGet();
logGrpcOutboundFormatIfNeeded(sequence, timestampMs, frame);
logPcmStatsIfNeeded("grpc-outbound-browser", sequence, timestampMs, frame);
logBrowserAudioStatsIfNeeded("browserAudioGrpcSend");
} catch (Throwable t) {
fail("speaker send", t);
}
}
private SpeakerCommand.StreamSpeakerAudioCommand.Request buildSpeakerAudioRequest(byte[] frame, long timestampMs) {
Common.AudioData audio = Common.AudioData.newBuilder()
.setData(ByteString.copyFrom(frame))
.setSampleRate(SAMPLE_RATE)
.setChannels(CHANNELS)
.setFormat(Common.AudioData.AudioFormat.PCM)
.setCodec(CODEC)
.setPts(timestampMs)
.setNbSamples(FRAME_SAMPLES)
.build();
return SpeakerCommand.StreamSpeakerAudioCommand.Request.newBuilder()
.setHeader(EdgeCommonUtil.buildRequest(speakerDeviceId))
.setAudio(audio)
.build();
}
private void handleMicFeedback(MicrophoneCommand.StreamMicAudioCommand.Feedback feedback) {
if (closed.get()) {
return;
}
if (feedback == null) {
listener.onHeartbeat(sessionId, System.currentTimeMillis());
requestNextMicFrame(FRAME_MS);
return;
}
if (feedback.hasHeader() && !feedback.getHeader().getSuccess()) {
fail("mic feedback", new IllegalStateException("microphone stream feedback failed: " + feedback.getHeader().getErrorMessage()));
return;
}
listener.onHeartbeat(sessionId, System.currentTimeMillis());
if (!feedback.hasAudio()) {
requestNextMicFrame(FRAME_MS);
return;
}
Common.AudioData audio = feedback.getAudio();
byte[] pcm = audio.getData().toByteArray();
if (!validateInboundAudio(audio, pcm)) {
requestNextMicFrame(FRAME_MS);
return;
}
long audioDurationMs = calculateAudioDurationMs(audio, pcm);
long sequence = inboundSequence.incrementAndGet();
long timestampMs = audio.getPts() > 0 ? audio.getPts() : System.currentTimeMillis();
logGrpcInboundFormatIfNeeded(sequence, timestampMs, audio, pcm);
logPcmStatsIfNeeded("grpc-inbound-robot", sequence, timestampMs, pcm);
byte[] normalizedPcm = normalizeInboundAudio(audio, pcm, sequence, timestampMs);
logPcmStatsIfNeeded("webrtc-outbound-robot-normalized", sequence, timestampMs, normalizedPcm);
try {
listener.onRobotAudio(sessionId, sequence, timestampMs, normalizedPcm);
} catch (Throwable t) {
log.error("投递机器人音频到业务层失败sessionId={}sequence={}", sessionId, sequence, t);
fail("business callback", t);
return;
}
requestNextMicFrame(audioDurationMs);
}
private boolean validateInboundAudio(Common.AudioData audio, byte[] pcm) {
if (pcm == null || pcm.length == 0) {
return false;
}
if ((pcm.length & 1) != 0) {
log.warn("机器人上行 PCM 字节数不是偶数sessionId={}bytes={}", sessionId, pcm.length);
return false;
}
if (audio.getFormat() != Common.AudioData.AudioFormat.PCM) {
log.warn("机器人上行音频格式不是 PCMsessionId={}format={}", sessionId, audio.getFormat());
return false;
}
String codec = audio.getCodec();
if (codec != null && codec.length() > 0 && !CODEC.equalsIgnoreCase(codec)) {
log.warn("机器人上行编码不匹配sessionId={}codec={}expected={}", sessionId, codec, CODEC);
}
return true;
}
private long calculateAudioDurationMs(Common.AudioData audio, byte[] pcm) {
int sampleRate = audio.getSampleRate() <= 0 ? SAMPLE_RATE : audio.getSampleRate();
int channels = audio.getChannels() <= 0 ? CHANNELS : audio.getChannels();
int samples = audio.getNbSamples();
if (samples <= 0 && pcm != null && pcm.length > 0) {
samples = pcm.length / 2 / Math.max(1, channels);
}
if (sampleRate <= 0 || samples <= 0) {
return FRAME_MS;
}
return Math.max(1L, Math.round(samples * 1000.0D / sampleRate));
}
private void requestNextMicFrame(long delayMs) {
if (closed.get()) {
return;
}
final long safeDelayMs = Math.max(1L, delayMs);
sendExecutor.schedule(new Runnable() {
@Override
public void run() {
if (closed.get()) {
return;
}
ClientCallStreamObserver<MicrophoneCommand.StreamMicAudioCommand.Request> observer = micRequestObserver;
if (observer == null) {
return;
}
try {
observer.request(1);
} catch (Throwable t) {
fail("mic flow-control request", t);
}
}
}, safeDelayMs, TimeUnit.MILLISECONDS);
}
private byte[] normalizeInboundAudio(Common.AudioData audio, byte[] pcm, long sequence, long timestampMs) {
int sourceSampleRate = audio.getSampleRate() <= 0 ? SAMPLE_RATE : audio.getSampleRate();
int sourceChannels = audio.getChannels() <= 0 ? CHANNELS : audio.getChannels();
if (sourceSampleRate == SAMPLE_RATE && sourceChannels == CHANNELS) {
return pcm;
}
short[] monoSamples = decodeToMonoSamples(pcm, sourceChannels);
short[] normalizedSamples = sourceSampleRate == SAMPLE_RATE
? monoSamples
: resampleLinear(monoSamples, sourceSampleRate, SAMPLE_RATE);
byte[] normalizedPcm = encodeMonoSamples(normalizedSamples);
if (sequence == 1 || sequence % 500 == 0) {
log.info("机器人上行音频已标准化sessionId={}sourceSampleRate={}sourceChannels={}targetSampleRate={}targetChannels={}sourceBytes={}targetBytes={}sequence={}timestampMs={}",
sessionId, sourceSampleRate, sourceChannels, SAMPLE_RATE, CHANNELS, pcm.length, normalizedPcm.length, sequence, timestampMs);
}
return normalizedPcm;
}
private short[] decodeToMonoSamples(byte[] pcm, int sourceChannels) {
int channels = Math.max(1, sourceChannels);
int frameCount = pcm.length / 2 / channels;
short[] mono = new short[frameCount];
for (int frame = 0; frame < frameCount; frame++) {
int sum = 0;
for (int channel = 0; channel < channels; channel++) {
int sampleIndex = (frame * channels + channel) * 2;
int lo = pcm[sampleIndex] & 0xff;
int hi = pcm[sampleIndex + 1];
sum += (short) ((hi << 8) | lo);
}
mono[frame] = (short) (sum / channels);
}
return mono;
}
private short[] resampleLinear(short[] input, int sourceSampleRate, int targetSampleRate) {
if (input.length == 0 || sourceSampleRate <= 0 || targetSampleRate <= 0) {
return input;
}
int outputLength = Math.max(1, (int) Math.round(input.length * (targetSampleRate / (double) sourceSampleRate)));
short[] output = new short[outputLength];
double sourceStep = sourceSampleRate / (double) targetSampleRate;
for (int i = 0; i < outputLength; i++) {
double sourceIndex = i * sourceStep;
int left = (int) sourceIndex;
int right = Math.min(left + 1, input.length - 1);
double fraction = sourceIndex - left;
double value = input[left] + (input[right] - input[left]) * fraction;
output[i] = clampToShort((int) Math.round(value));
}
return output;
}
private byte[] encodeMonoSamples(short[] samples) {
byte[] pcm = new byte[samples.length * 2];
for (int i = 0; i < samples.length; i++) {
pcm[i * 2] = (byte) (samples[i] & 0xff);
pcm[i * 2 + 1] = (byte) ((samples[i] >> 8) & 0xff);
}
return pcm;
}
private short clampToShort(int value) {
if (value > Short.MAX_VALUE) {
return Short.MAX_VALUE;
}
if (value < Short.MIN_VALUE) {
return Short.MIN_VALUE;
}
return (short) value;
}
private void probeHeartbeat() {
if (closed.get() || !heartbeatInFlight.compareAndSet(false, true)) {
return;
}
MicrophoneCommand.GetMicStateCommand.Request request = MicrophoneCommand.GetMicStateCommand.Request.newBuilder()
.setHeader(EdgeCommonUtil.buildRequest(micDeviceId))
.build();
micStub.withDeadlineAfter(HEARTBEAT_DEADLINE_MS, TimeUnit.MILLISECONDS).getStatus(request,
new StreamObserver<MicrophoneCommand.GetMicStateCommand.Feedback>() {
@Override
public void onNext(MicrophoneCommand.GetMicStateCommand.Feedback feedback) {
if (feedback != null && feedback.hasHeader() && !feedback.getHeader().getSuccess()) {
log.warn("实时对讲麦克风心跳返回失败sessionId={}message={}", sessionId, feedback.getHeader().getErrorMessage());
return;
}
listener.onHeartbeat(sessionId, System.currentTimeMillis());
}
@Override
public void onError(Throwable t) {
log.debug("实时对讲麦克风心跳探测失败sessionId={}", sessionId, t);
heartbeatInFlight.set(false);
}
@Override
public void onCompleted() {
heartbeatInFlight.set(false);
}
});
}
private void fail(String source, Throwable throwable) {
if (!closed.compareAndSet(false, true)) {
return;
}
Throwable actual = throwable == null ? new RuntimeException("audio stream failed") : throwable;
log.error("实时对讲 gRPC 流异常source={}sessionId={}terminalId={}speakerDeviceId={}micDeviceId={}",
source, sessionId, terminalId, speakerDeviceId, micDeviceId, actual);
sendExecutor.execute(new Runnable() {
@Override
public void run() {
errorSpeakerStream(actual.getMessage());
cancelMicStream(actual.getMessage());
stopTerminalAudio();
closeLocalResources();
streams.remove(sessionId, RobotAudioStream.this);
if (terminalNotified.compareAndSet(false, true)) {
listener.onError(sessionId, actual);
}
}
});
}
private void completeFromRemote() {
if (!closed.compareAndSet(false, true)) {
return;
}
log.info("实时对讲 gRPC 流远端结束sessionId={}terminalId={}speakerDeviceId={}micDeviceId={}", sessionId, terminalId, speakerDeviceId, micDeviceId);
sendExecutor.execute(new Runnable() {
@Override
public void run() {
completeSpeakerStream();
cancelMicStream("remote completed");
closeLocalResources();
streams.remove(sessionId, RobotAudioStream.this);
if (terminalNotified.compareAndSet(false, true)) {
listener.onCompleted(sessionId);
}
}
});
}
private void completeSpeakerStream() {
StreamObserver<SpeakerCommand.StreamSpeakerAudioCommand.Request> observer = speakerRequestObserver;
speakerRequestObserver = null;
if (observer != null) {
try {
observer.onCompleted();
} catch (Throwable t) {
log.debug("完成扬声器音频流异常sessionId={}", sessionId, t);
}
}
}
private void errorSpeakerStream(String reason) {
StreamObserver<SpeakerCommand.StreamSpeakerAudioCommand.Request> observer = speakerRequestObserver;
speakerRequestObserver = null;
if (observer != null) {
try {
observer.onError(Status.CANCELLED.withDescription(reason == null ? "audio cancelled" : reason).asRuntimeException());
} catch (Throwable t) {
log.debug("取消扬声器音频流异常sessionId={}", sessionId, t);
}
}
}
private void cancelMicStream(String reason) {
ClientCallStreamObserver<MicrophoneCommand.StreamMicAudioCommand.Request> observer = micRequestObserver;
micRequestObserver = null;
if (observer != null) {
try {
observer.cancel(reason == null ? "audio cancelled" : reason, null);
} catch (Throwable t) {
log.debug("取消麦克风音频流异常sessionId={}", sessionId, t);
}
}
}
/**
* 会话关闭时尽量停止终端侧播放和采集失败不阻断本地资源释放
*/
private void stopTerminalAudio() {
try {
SpeakerCommand.StopSpeakerCommand.Request speakerStop = SpeakerCommand.StopSpeakerCommand.Request.newBuilder()
.setHeader(EdgeCommonUtil.buildRequest(speakerDeviceId))
.build();
speakerStub.withDeadlineAfter(HEARTBEAT_DEADLINE_MS, TimeUnit.MILLISECONDS).stopPlayback(speakerStop,
new EmptyStreamObserver<SpeakerCommand.StopSpeakerCommand.Feedback>("stop speaker", sessionId));
} catch (Throwable t) {
log.debug("下发停止扬声器失败sessionId={}", sessionId, t);
}
try {
MicrophoneCommand.StopMicRecordingCommand.Request micStop = MicrophoneCommand.StopMicRecordingCommand.Request.newBuilder()
.setHeader(EdgeCommonUtil.buildRequest(micDeviceId))
.build();
micStub.withDeadlineAfter(HEARTBEAT_DEADLINE_MS, TimeUnit.MILLISECONDS).stopRecord(micStop,
new EmptyStreamObserver<MicrophoneCommand.StopMicRecordingCommand.Feedback>("stop mic", sessionId));
} catch (Throwable t) {
log.debug("下发停止麦克风失败sessionId={}", sessionId, t);
}
}
private void closeLocalResources() {
ScheduledFuture<?> sf = sendFuture;
if (sf != null) {
sf.cancel(false);
}
ScheduledFuture<?> hf = heartbeatFuture;
if (hf != null) {
hf.cancel(false);
}
browserFrameQueue.clear();
pendingBrowserBytes = new byte[0];
if (!sendExecutor.isShutdown()) {
sendExecutor.shutdown();
}
}
private void logGrpcInboundFormatIfNeeded(long sequence, long timestampMs, Common.AudioData audio, byte[] pcm) {
if (sequence == 1 || sequence % 500 == 0) {
log.info("gRPC机器人上行音频格式sessionId={}sampleRate={}channels={}bitsPerSample={}encoding={}frameSamples={}frameMs={}bytes={}sequence={}timestampMs={}",
sessionId,
audio.getSampleRate() == 0 ? SAMPLE_RATE : audio.getSampleRate(),
audio.getChannels() == 0 ? CHANNELS : audio.getChannels(),
BITS_PER_SAMPLE,
audio.getCodec() == null || audio.getCodec().length() == 0 ? CODEC : audio.getCodec(),
audio.getNbSamples() == 0 ? pcm.length / 2 : audio.getNbSamples(),
FRAME_MS,
pcm.length,
sequence,
timestampMs);
}
}
private void logGrpcOutboundFormatIfNeeded(long sequence, long timestampMs, byte[] frame) {
if (sequence == 1 || sequence % 500 == 0) {
log.info("gRPC浏览器下行音频格式sessionId={}sampleRate={}channels={}bitsPerSample={}encoding={}frameSamples={}frameMs={}bytes={}sequence={}timestampMs={}",
sessionId, SAMPLE_RATE, CHANNELS, BITS_PER_SAMPLE, CODEC, FRAME_SAMPLES, FRAME_MS, frame.length, sequence, timestampMs);
}
}
private void logBrowserAudioStatsIfNeeded(String source) {
long now = System.currentTimeMillis();
if (now - lastStatsLogAt < 5000L) {
return;
}
lastStatsLogAt = now;
log.info("gRPC浏览器音频统计source={}sessionId={}enqueued={}sent={}dropped={}queued={}",
source, sessionId, enqueued.get(), sent.get(), dropped.get(), browserFrameQueue.size());
}
private void logPcmStatsIfNeeded(String direction, long sequence, long timestampMs, byte[] pcm) {
if (sequence != 1 && sequence % 500 != 0) {
return;
}
PcmStats stats = PcmStats.from(pcm);
log.info("PCM-STATS direction={} sessionId={} sequence={} timestampMs={} bytes={} samples={} rms={} peak={} zeroCrossRate={} firstSamples={}",
direction, sessionId, sequence, timestampMs, pcm.length, stats.samples, stats.rms, stats.peak, stats.zeroCrossRate, stats.firstSamples);
}
}
private static class EmptyStreamObserver<T> implements StreamObserver<T> {
private final String action;
private final String sessionId;
private EmptyStreamObserver(String action, String sessionId) {
this.action = action;
this.sessionId = sessionId;
}
@Override
public void onNext(T value) {
}
@Override
public void onError(Throwable t) {
log.debug("{} failed, sessionId={}", action, sessionId, t);
}
@Override
public void onCompleted() {
}
}
private static class NamedThreadFactory implements ThreadFactory {
private final String name;
private NamedThreadFactory(String name) {
this.name = name;
}
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, name);
thread.setDaemon(true);
return thread;
}
}
private static class PcmStats {
private final int samples;
private final long rms;
private final int peak;
private final String zeroCrossRate;
private final String firstSamples;
private PcmStats(int samples, long rms, int peak, String zeroCrossRate, String firstSamples) {
this.samples = samples;
this.rms = rms;
this.peak = peak;
this.zeroCrossRate = zeroCrossRate;
this.firstSamples = firstSamples;
}
private static PcmStats from(byte[] pcm) {
int samples = pcm == null ? 0 : pcm.length / 2;
if (samples == 0) {
return new PcmStats(0, 0, 0, "0.0000", "[]");
}
long squareSum = 0L;
int peak = 0;
int zeroCross = 0;
int previous = 0;
StringBuilder first = new StringBuilder("[");
for (int i = 0; i < samples; i++) {
int lo = pcm[i * 2] & 0xff;
int hi = pcm[i * 2 + 1];
int value = (short) ((hi << 8) | lo);
int abs = Math.abs(value);
if (abs > peak) {
peak = abs;
}
squareSum += (long) value * value;
if (i > 0 && ((previous < 0 && value >= 0) || (previous >= 0 && value < 0))) {
zeroCross++;
}
previous = value;
if (i < 8) {
if (i > 0) {
first.append(',');
}
first.append(value);
}
}
first.append(']');
long rms = (long) Math.sqrt(squareSum / (double) samples);
String zcr = String.format("%.4f", zeroCross / (double) samples);
return new PcmStats(samples, rms, peak, zcr, first.toString());
}
}
}

View File

@ -47,16 +47,21 @@ public class GrpcServiceManager {
clientFactories.put(SystemServiceGrpc.SystemServiceBlockingStub.class, new GrpcClientFactory<>(SystemServiceGrpc::newBlockingStub)); clientFactories.put(SystemServiceGrpc.SystemServiceBlockingStub.class, new GrpcClientFactory<>(SystemServiceGrpc::newBlockingStub));
// 注册扬声器服务的 Stub // 注册扬声器服务的 Stub
clientFactories.put(SpeakerServiceGrpc.SpeakerServiceBlockingStub.class, new GrpcClientFactory<>(SpeakerServiceGrpc::newBlockingStub)); clientFactories.put(SpeakerServiceGrpc.SpeakerServiceBlockingStub.class, new GrpcClientFactory<>(SpeakerServiceGrpc::newBlockingStub));
clientFactories.put(SpeakerServiceGrpc.SpeakerServiceStub.class, new GrpcClientFactory<>(SpeakerServiceGrpc::newStub));
// 注册麦克风服务的 Stub // 注册麦克风服务的 Stub
clientFactories.put(MicPhoneServiceGrpc.MicPhoneServiceBlockingStub.class, new GrpcClientFactory<>(MicPhoneServiceGrpc::newBlockingStub)); clientFactories.put(MicPhoneServiceGrpc.MicPhoneServiceBlockingStub.class, new GrpcClientFactory<>(MicPhoneServiceGrpc::newBlockingStub));
clientFactories.put(MicPhoneServiceGrpc.MicPhoneServiceStub.class, new GrpcClientFactory<>(MicPhoneServiceGrpc::newStub));
// 注册机器人头部服务的 Stub // 注册机器人头部服务的 Stub
clientFactories.put(BioHeadServiceGrpc.BioHeadServiceBlockingStub.class, new GrpcClientFactory<>(BioHeadServiceGrpc::newBlockingStub)); clientFactories.put(BioHeadServiceGrpc.BioHeadServiceBlockingStub.class, new GrpcClientFactory<>(BioHeadServiceGrpc::newBlockingStub));
// 注册机器人灵巧手服务的 Stub // 注册机器人灵巧手服务的 Stub
clientFactories.put(DexHandServiceGrpc.DexHandServiceBlockingStub.class, new GrpcClientFactory<>(DexHandServiceGrpc::newBlockingStub)); clientFactories.put(DexHandServiceGrpc.DexHandServiceBlockingStub.class, new GrpcClientFactory<>(DexHandServiceGrpc::newBlockingStub));
clientFactories.put(DexHandServiceGrpc.DexHandServiceStub.class, new GrpcClientFactory<>(DexHandServiceGrpc::newStub)); clientFactories.put(DexHandServiceGrpc.DexHandServiceStub.class, new GrpcClientFactory<>(DexHandServiceGrpc::newStub));
// 注册机械臂服务的stub // 注册机械臂服务的stub
clientFactories.put(HumanoidRobotServiceGrpc.HumanoidRobotServiceBlockingStub.class, new GrpcClientFactory<>(HumanoidRobotServiceGrpc::newBlockingStub)); clientFactories.put(ArmServiceGrpc.ArmServiceBlockingStub.class, new GrpcClientFactory<>(ArmServiceGrpc::newBlockingStub));
clientFactories.put(HlcServiceGrpc.HlcServiceBlockingStub.class, new GrpcClientFactory<>(HlcServiceGrpc::newBlockingStub)); clientFactories.put(HlcServiceGrpc.HlcServiceBlockingStub.class, new GrpcClientFactory<>(HlcServiceGrpc::newBlockingStub));
// 注册agv服务的stub
clientFactories.put(AgvServiceGrpc.AgvServiceBlockingStub.class, new GrpcClientFactory<>(AgvServiceGrpc::newBlockingStub));
// 注册机器人实时语音双向流服务的异步Stub
} }

View File

@ -0,0 +1,28 @@
package com.cmvr.edge.client.model.agv;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* AGV电池状态VO
*
* @author cmvr-iot
* @since 2026-07-01
*/
@Data
@ApiModel("AGV电池状态")
public class EdgeAgvBatteryStatusVO {
@ApiModelProperty("电池电量百分比0-100")
private double batteryPercent;
@ApiModelProperty("电池电压(单位:伏特)")
private double voltage;
@ApiModelProperty("电池电流(单位:安培)")
private double current;
@ApiModelProperty("充电状态true=充电中false=未充电)")
private boolean charging;
}

View File

@ -0,0 +1,22 @@
package com.cmvr.edge.client.model.agv;
import com.cmvr.edge.client.model.EdgeCommonVO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotEmpty;
/**
* AGV 地图名称请求参数
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("AGV地图名称参数")
public class EdgeAgvMapNameVO extends EdgeCommonVO {
@NotEmpty(message = "地图名称不能为空")
@ApiModelProperty(value = "地图名称", required = true)
private String mapName;
}

View File

@ -0,0 +1,25 @@
package com.cmvr.edge.client.model.agv;
import com.cmvr.edge.client.model.EdgeCommonVO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* AGV 导航到指定坐标的请求参数
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("AGV导航到指定坐标参数")
public class EdgeAgvNavigateToPoseVO extends EdgeCommonVO {
@ApiModelProperty(value = "目标 X 坐标,单位:米", required = true)
private double x;
@ApiModelProperty(value = "目标 Y 坐标,单位:米", required = true)
private double y;
@ApiModelProperty(value = "目标朝向角,单位:弧度", required = true)
private double theta;
}

View File

@ -0,0 +1,22 @@
package com.cmvr.edge.client.model.agv;
import com.cmvr.edge.client.model.EdgeCommonVO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotEmpty;
/**
* AGV 导航到站点的请求参数
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("AGV导航到站点参数")
public class EdgeAgvNavigateToStationVO extends EdgeCommonVO {
@NotEmpty(message = "站点ID不能为空")
@ApiModelProperty(value = "站点ID", required = true)
private String stationId;
}

View File

@ -0,0 +1,25 @@
package com.cmvr.edge.client.model.agv;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* AGV导航任务结果VO
*
* @author cmvr-iot
* @since 2026-07-01
*/
@Data
@ApiModel("AGV导航任务结果")
public class EdgeAgvNavigationResultVO {
@ApiModelProperty("返回码0=成功)")
private int retCode;
@ApiModelProperty("错误信息")
private String errMsg;
@ApiModelProperty("任务ID")
private String taskId;
}

View File

@ -0,0 +1,28 @@
package com.cmvr.edge.client.model.agv;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* AGV机器人位置VO
*
* @author cmvr-iot
* @since 2026-07-01
*/
@Data
@ApiModel("AGV机器人位置")
public class EdgeAgvRobotLocationVO {
@ApiModelProperty("X坐标单位")
private double x;
@ApiModelProperty("Y坐标单位")
private double y;
@ApiModelProperty("角度(单位:弧度)")
private double theta;
@ApiModelProperty("地图名称")
private String mapName;
}

View File

@ -0,0 +1,25 @@
package com.cmvr.edge.client.model.agv;
import com.cmvr.edge.client.model.EdgeCommonVO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* AGV 开始建图请求参数
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("AGV开始建图参数")
public class EdgeAgvStartMappingVO extends EdgeCommonVO {
@ApiModelProperty(value = "建图维度0=未指定1=2D2=3D3=2D+3D", required = true)
private int dimension;
@ApiModelProperty("地图名称,可为空")
private String mapName;
@ApiModelProperty(value = "是否实时建图", required = true)
private boolean realTime;
}

View File

@ -0,0 +1,37 @@
package com.cmvr.edge.client.model.agv;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* AGV站点信息VO
*
* @author cmvr-iot
* @since 2026-07-01
*/
@Data
@ApiModel("AGV站点信息")
public class EdgeAgvStationVO {
@ApiModelProperty("站点ID")
private String stationId;
@ApiModelProperty("站点名称")
private String stationName;
@ApiModelProperty("X坐标单位")
private double x;
@ApiModelProperty("Y坐标单位")
private double y;
@ApiModelProperty("角度(单位:弧度)")
private double theta;
@ApiModelProperty("站点类型")
private String stationType;
@ApiModelProperty("站点描述")
private String description;
}

View File

@ -0,0 +1,31 @@
package com.cmvr.edge.client.model.agv;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* AGV任务状态VO
*
* @author cmvr-iot
* @since 2026-07-01
*/
@Data
@ApiModel("AGV任务状态")
public class EdgeAgvTaskStatusVO {
@ApiModelProperty("任务状态")
private int taskStatus;
@ApiModelProperty("当前站点ID")
private String currentStationId;
@ApiModelProperty("目标站点ID")
private String targetStationId;
@ApiModelProperty("已经过的站点数量")
private int passedStationCount;
@ApiModelProperty("剩余站点数量")
private int remainingStationCount;
}

View File

@ -0,0 +1,21 @@
package com.cmvr.edge.client.model.agv;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotEmpty;
/**
* AGV 上传地图请求参数
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("AGV上传地图参数")
public class EdgeAgvUploadMapVO extends EdgeAgvMapNameVO {
@NotEmpty(message = "地图内容不能为空")
@ApiModelProperty(value = "地图内容", required = true)
private String content;
}

View File

@ -0,0 +1,25 @@
package com.cmvr.edge.client.model.agv;
import com.cmvr.edge.client.model.EdgeCommonVO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* AGV 速度控制请求参数
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("AGV速度控制参数")
public class EdgeAgvVelocityVO extends EdgeCommonVO {
@ApiModelProperty(value = "X 方向线速度单位m/s", required = true)
private double vx;
@ApiModelProperty(value = "Y 方向线速度单位m/s", required = true)
private double vy;
@ApiModelProperty(value = "角速度单位rad/s", required = true)
private double wz;
}

View File

@ -0,0 +1,22 @@
package com.cmvr.edge.client.model.arm;
import com.cmvr.edge.client.model.EdgeCommonVO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotEmpty;
/**
* 机械臂标定零点参数
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("机械臂标定零点参数")
public class EdgeArmCalibrateZeroQVO extends EdgeCommonVO {
@NotEmpty(message = "关节名称不能为空")
@ApiModelProperty(value = "关节名称", required = true)
private String jointName;
}

View File

@ -0,0 +1,34 @@
package com.cmvr.edge.client.model.arm;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 机械臂笛卡尔位姿VO
*
* @author cmvr-iot
* @since 2026-07-01
*/
@Data
@ApiModel("机械臂笛卡尔位姿")
public class EdgeArmCartesianPoseVO {
@ApiModelProperty("X轴坐标单位")
private double x;
@ApiModelProperty("Y轴坐标单位")
private double y;
@ApiModelProperty("Z轴坐标单位")
private double z;
@ApiModelProperty("RX旋转角度单位弧度")
private double rx;
@ApiModelProperty("RY旋转角度单位弧度")
private double ry;
@ApiModelProperty("RZ旋转角度单位弧度")
private double rz;
}

View File

@ -0,0 +1,28 @@
package com.cmvr.edge.client.model.arm;
import com.cmvr.edge.client.model.EdgeCommonVO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 机械臂正向运动学计算VO
*
* @author cmvr-iot
* @since 2026-07-01
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("机械臂正向运动学计算参数")
public class EdgeArmForwardKinematicsVO extends EdgeCommonVO {
@ApiModelProperty(value = "关节位置数组6个关节角度单位弧度", required = true)
private double[] joints;
@ApiModelProperty("基座连杆名称")
private String baseLink;
@ApiModelProperty("末端执行器连杆名称")
private String eeLink;
}

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