huang
2025-11-25 19f59c243e8df97c8b9fd9dba4e758be8235d68b
添加卧转立扫码、卧转立、大车、大理片笼基础任务流转逻辑
19个文件已修改
27个文件已添加
3个文件已删除
9206 ■■■■ 已修改文件
mes-processes/mes-plcSend/ARCHITECTURE.md 536 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/README.md 645 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/config/TaskExecutorConfig.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DeviceConfig.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/DevicePlcOperationService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceConfigServiceImpl.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceCoordinationServiceImpl.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DevicePlcOperationServiceImpl.java 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/vo/DevicePlcVO.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceInteractionRegistry.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceLogicHandler.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/GlassStorageInteraction.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/LargeGlassInteraction.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/LoadVehicleInteraction.java 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/impl/LargeGlassLogicHandler.java 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/impl/LoadVehicleLogicHandler.java 451 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/largeglass/config/LargeGlassConfig.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/largeglass/handler/LargeGlassLogicHandler.java 379 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/largeglass/model/GridRange.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/coordination/VehicleCoordinationService.java 189 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/coordination/VehicleStatusManager.java 199 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/flow/LoadVehicleInteraction.java 225 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/handler/LoadVehicleLogicHandler.java 1719 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehiclePath.java 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehiclePosition.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehicleState.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehicleStatus.java 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehicleTask.java 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/base/WorkstationBaseHandler.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/config/WorkstationLogicConfig.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/scanner/handler/HorizontalScannerLogicHandler.java 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/transfer/handler/HorizontalTransferLogicHandler.java 481 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/s7/provider/S7SerializerProvider.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/controller/TaskStatusNotificationController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/MultiDeviceTaskServiceImpl.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/utils/constants.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/DeviceConfigList.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/DeviceEditDialog.vue 385 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/components/DeviceLogicConfig/LargeGlassConfig.vue 177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/components/DeviceLogicConfig/LoadVehicleConfig.vue 324 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/components/DeviceLogicConfig/WorkstationScannerConfig.vue 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/components/DeviceLogicConfig/WorkstationTransferConfig.vue 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/components/DeviceLogicConfig/index.js 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/MultiDeviceWorkbench.vue 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/components/DeviceGroup/GroupTopology.vue 478 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue 482 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/components/MultiDeviceTest/ResultAnalysis.vue 638 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue 199 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/ARCHITECTURE.md
New file
@@ -0,0 +1,536 @@
# 项目架构说明文档
## 📋 项目主体架构
### 核心主体:TaskExecutionEngine(任务执行引擎)
**位置**:`com.mes.task.service.TaskExecutionEngine`
**职责**:多设备任务执行的核心引擎,负责:
- 串行/并行执行模式控制
- 设备步骤执行调度
- 设备协调检查
- 错误处理和重试
- 实时状态通知
## 🔄 调用流程
### 1. 任务启动流程
```
用户请求 (Controller)
    ↓
MultiDeviceTaskServiceImpl.startTask()
    ├─ 验证设备组
    ├─ 获取设备列表
    ├─ 创建任务记录 (PENDING)
    └─ 异步执行 executeTaskAsync()
        ↓
    TaskExecutionEngine.execute()
        ├─ 设备协调检查 (DeviceCoordinationService)
        ├─ 确定执行模式 (串行/并行)
        └─ 执行设备步骤
```
### 2. 设备步骤执行流程
```
TaskExecutionEngine.executeStep()
    ↓
检查是否有 DeviceInteraction
    ├─ 有 → executeInteractionStepWithRetry()
    │       ↓
    │   DeviceInteraction.execute(InteractionContext)
    │       ↓
    │   具体设备交互实现(如 LoadVehicleInteraction)
    │       ↓
    │   调用 DeviceInteractionService
    │       ↓
    │   最终调用 DeviceLogicHandler
    │
    └─ 无 → 直接调用 DeviceLogicHandler
            ↓
        DeviceLogicHandlerFactory.getHandler(deviceType)
            ↓
        BaseDeviceLogicHandler.execute()
            ↓
        子类实现 doExecute()
```
### 3. 两种执行路径
#### 路径A:通过 DeviceInteraction(推荐用于复杂设备)
```
TaskExecutionEngine
    ↓
DeviceInteraction.execute(InteractionContext)
    ↓
具体实现类(如 LoadVehicleInteraction)
    ├─ 状态检查
    ├─ 设备选择(多实例协调)
    ├─ 数据准备
    └─ 调用 DeviceInteractionService
        ↓
    DeviceLogicHandler.execute()
```
**适用场景**:
- 需要多实例协调的设备(如大车设备)
- 需要复杂前置检查的设备
- 需要数据转换和准备的设备
#### 路径B:直接调用 DeviceLogicHandler(简单设备)
```
TaskExecutionEngine
    ↓
DeviceLogicHandlerFactory.getHandler(deviceType)
    ↓
BaseDeviceLogicHandler.execute()
    ├─ 参数解析
    ├─ 逻辑参数提取 (extraParams.deviceLogic)
    └─ 子类实现 doExecute()
```
**适用场景**:
- 简单设备,逻辑单一
- 不需要复杂协调的设备
## 🏗️ 分层架构
### 第一层:Controller 层(API入口)
**位置**:`com.mes.task.controller.*`
**职责**:
- 接收HTTP请求
- 参数验证
- 调用Service层
**主要类**:
- `MultiDeviceTaskController` - 任务管理API
- `TaskStatusNotificationController` - SSE实时通知API
### 第二层:Service 层(业务逻辑)
**位置**:`com.mes.task.service.*`
**职责**:
- 业务逻辑处理
- 事务管理
- 调用执行引擎
**主要类**:
- `MultiDeviceTaskServiceImpl` - 任务服务实现
  - 创建任务记录
  - 异步执行任务
  - 任务状态管理
- `TaskExecutionEngine` - **核心执行引擎**
  - 设备协调检查
  - 执行模式判断(串行/并行)
  - 步骤执行调度
  - 重试机制
### 第三层:Interaction 层(设备交互)
**位置**:`com.mes.interaction.*`
**职责**:
- 设备交互逻辑封装
- 多设备协调
- 数据传递
**主要组件**:
#### 3.1 DeviceInteraction(设备交互接口)
**接口**:`com.mes.interaction.DeviceInteraction`
**实现类示例**:
- `LoadVehicleInteraction` - 大车设备交互
- `LargeGlassInteraction` - 大理片笼交互
- `GlassStorageInteraction` - 玻璃存储交互
**注册机制**:
- `DeviceInteractionRegistry` - 自动注册所有 `@Component` 的 `DeviceInteraction` 实现
#### 3.2 DeviceLogicHandler(设备逻辑处理器)
**接口**:`com.mes.interaction.DeviceLogicHandler`
**基类**:`BaseDeviceLogicHandler`
- 提供通用功能:
  - 参数解析
  - 逻辑参数提取(从 `extraParams.deviceLogic`)
  - 错误处理
**实现类示例**:
- `LoadVehicleLogicHandler` - 大车设备逻辑
- `HorizontalScannerLogicHandler` - 卧转立扫码逻辑
- `HorizontalTransferLogicHandler` - 卧转立逻辑
- `LargeGlassLogicHandler` - 大理片笼逻辑
**注册机制**:
- `DeviceLogicHandlerFactory` - 自动注册所有 `DeviceLogicHandler` 实现
- 通过 `@PostConstruct` 初始化映射表
### 第四层:Coordination 层(设备协调)
**位置**:`com.mes.device.service.*` 和 `com.mes.interaction.*.coordination.*`
**职责**:
- 设备间数据传递
- 设备状态同步
- 设备依赖管理
- 多实例协调
**主要类**:
- `DeviceCoordinationService` - 通用设备协调服务
- `VehicleCoordinationService` - 大车设备协调服务
- `VehicleStatusManager` - 大车状态管理器
### 第五层:PLC 操作层(硬件通信)
**位置**:`com.mes.device.service.DevicePlcOperationService`
**职责**:
- PLC读写操作
- 地址映射
- 通信管理
## 📦 如何添加新设备
### 步骤1:定义设备类型常量
在 `DeviceConfig.DeviceType` 中添加:
```java
public static final class DeviceType {
    public static final String NEW_DEVICE = "新设备类型";
}
```
### 步骤2:创建设备逻辑处理器(必须)
**位置**:`com.mes.interaction.*.handler.NewDeviceLogicHandler`
```java
@Component
public class NewDeviceLogicHandler extends BaseDeviceLogicHandler {
    @Override
    public String getDeviceType() {
        return DeviceConfig.DeviceType.NEW_DEVICE;
    }
    @Override
    protected DevicePlcVO.OperationResult doExecute(
            DeviceConfig deviceConfig,
            String operation,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        // 1. 从 logicParams 中获取配置参数
        Integer timeout = getLogicParam(logicParams, "timeout", 5000);
        String mode = getLogicParam(logicParams, "mode", "default");
        // 2. 从 params 中获取运行时参数
        String glassId = (String) params.get("glassId");
        // 3. 执行设备逻辑
        // ... 具体实现
        // 4. 调用PLC操作
        Map<String, Object> plcParams = new HashMap<>();
        plcParams.put("field1", value1);
        DevicePlcVO.OperationResult result =
            devicePlcOperationService.writePlcData(deviceConfig, plcParams);
        return result;
    }
    @Override
    protected Map<String, Object> getDefaultLogicParams() {
        Map<String, Object> defaults = new HashMap<>();
        defaults.put("timeout", 5000);
        defaults.put("mode", "default");
        return defaults;
    }
}
```
**自动注册**:
- 实现 `DeviceLogicHandler` 接口
- 添加 `@Component` 注解
- `DeviceLogicHandlerFactory` 会自动发现并注册
### 步骤3:创建设备交互类(可选,复杂设备推荐)
**位置**:`com.mes.interaction.*.flow.NewDeviceInteraction`
```java
@Component
public class NewDeviceInteraction implements DeviceInteraction {
    @Override
    public String getDeviceType() {
        return DeviceConfig.DeviceType.NEW_DEVICE;
    }
    @Override
    public InteractionResult execute(InteractionContext context) {
        DeviceConfig device = context.getCurrentDevice();
        // 1. 前置条件检查
        if (device == null) {
            return InteractionResult.fail("设备配置不存在");
        }
        // 2. 数据准备
        List<String> glassIds = context.getParameters().getGlassIds();
        // 3. 调用设备逻辑处理器
        DeviceInteractionService service = ...;
        Map<String, Object> params = new HashMap<>();
        params.put("glassIds", glassIds);
        DevicePlcVO.OperationResult result =
            service.executeDeviceOperation(device, "operationName", params);
        if (result.isSuccess()) {
            // 4. 数据传递到下一个设备
            context.getSharedData().put("processedGlasses", glassIds);
            return InteractionResult.success("执行成功", result.getData());
        } else {
            return InteractionResult.fail(result.getMessage());
        }
    }
}
```
**自动注册**:
- 实现 `DeviceInteraction` 接口
- 添加 `@Component` 注解
- `DeviceInteractionRegistry` 会自动发现并注册
### 步骤4:配置设备逻辑参数(前端)
在设备配置的 `extraParams.deviceLogic` 中配置:
```json
{
  "deviceLogic": {
    "timeout": 5000,
    "mode": "default",
    "customParam1": "value1"
  }
}
```
### 步骤5:设备组织方式选择
#### 简单设备(推荐放在通用包)
```
interaction/
└── impl/
    └── NewDeviceLogicHandler.java
```
#### 复杂设备(需要协调、状态管理等)
```
interaction/
└── newdevice/
    ├── handler/
    │   └── NewDeviceLogicHandler.java
    ├── flow/
    │   └── NewDeviceInteraction.java
    ├── coordination/  (可选)
    │   └── NewDeviceCoordinationService.java
    └── model/  (可选)
        └── NewDeviceStatus.java
```
## 🔍 执行流程详解
### 串行执行模式
```
TaskExecutionEngine.execute()
    ↓
for (每个设备) {
    1. 创建步骤记录 (TaskStepDetail)
    2. executeStep()
        ↓
    3. 检查是否有 DeviceInteraction
        ├─ 有 → DeviceInteraction.execute()
        │       ↓
        │   调用 DeviceLogicHandler
        │
        └─ 无 → 直接调用 DeviceLogicHandler
                ↓
            BaseDeviceLogicHandler.execute()
                ↓
            子类 doExecute()
    4. 更新步骤状态
    5. 传递数据到下一个设备
}
```
### 并行执行模式
```
TaskExecutionEngine.execute()
    ↓
创建线程池任务列表
    ↓
for (每个设备) {
    提交到线程池执行
        ↓
    executeStep() (同上)
}
    ↓
等待所有任务完成
    ↓
汇总结果
```
## 📝 关键接口说明
### DeviceLogicHandler
**接口方法**:
```java
DevicePlcVO.OperationResult execute(
    DeviceConfig deviceConfig,
    String operation,
    Map<String, Object> params
);
```
**调用时机**:
- 任务执行引擎直接调用
- 或通过 DeviceInteraction 间接调用
### DeviceInteraction
**接口方法**:
```java
InteractionResult execute(InteractionContext context);
```
**调用时机**:
- 任务执行引擎优先检查是否有 DeviceInteraction
- 如果有,优先使用 DeviceInteraction
- 如果没有,直接调用 DeviceLogicHandler
### InteractionContext
**包含内容**:
- `currentDevice` - 当前设备配置
- `taskContext` - 任务上下文
- `parameters` - 任务参数
- `sharedData` - 设备间共享数据
## 🎯 最佳实践
### 1. 何时使用 DeviceInteraction?
**使用场景**:
- ✅ 需要多实例协调(如大车设备)
- ✅ 需要复杂前置检查
- ✅ 需要数据转换和准备
- ✅ 需要状态管理
**不使用场景**:
- ❌ 简单设备,逻辑单一
- ❌ 不需要协调的设备
### 2. 参数设计
**logicParams(逻辑参数)**:
- 从 `extraParams.deviceLogic` 中读取
- 设备配置时设置,运行时不变
- 如:超时时间、模式、容量等
**params(运行时参数)**:
- 任务执行时动态传入
- 如:玻璃ID、位置、数量等
### 3. 错误处理
**在 BaseDeviceLogicHandler 中**:
- 自动捕获异常
- 返回统一的错误格式
**在子类中**:
- 可以抛出业务异常
- 会被基类捕获并转换
### 4. 数据传递
**通过 InteractionContext.sharedData**:
```java
// 在设备A中设置
context.getSharedData().put("glassIds", glassIds);
// 在设备B中获取
List<String> glassIds = (List<String>) context.getSharedData().get("glassIds");
```
## 📚 相关文件位置
### 核心文件
- `TaskExecutionEngine.java` - 任务执行引擎
- `MultiDeviceTaskServiceImpl.java` - 任务服务
- `DeviceLogicHandlerFactory.java` - 处理器工厂
- `DeviceInteractionRegistry.java` - 交互注册中心
### 基类
- `BaseDeviceLogicHandler.java` - 逻辑处理器基类
- `DeviceInteraction.java` - 交互接口
### 示例实现
- `LoadVehicleLogicHandler.java` - 大车设备逻辑处理器
- `LoadVehicleInteraction.java` - 大车设备交互
- `HorizontalScannerLogicHandler.java` - 卧转立扫码处理器
## 🔧 调试技巧
### 1. 查看注册的设备处理器
```java
@Autowired
private DeviceLogicHandlerFactory factory;
// 查看所有已注册的设备类型
Set<String> types = factory.getSupportedDeviceTypes();
```
### 2. 查看注册的设备交互
```java
@Autowired
private DeviceInteractionRegistry registry;
// 查看所有已注册的交互
Map<String, DeviceInteraction> interactions = registry.getInteractions();
```
### 3. 日志输出
- 任务执行:查看 `TaskExecutionEngine` 日志
- 设备执行:查看具体 Handler 日志
- 协调服务:查看 `DeviceCoordinationService` 日志
## ✅ 总结
1. **项目主体**:`TaskExecutionEngine` 是核心执行引擎
2. **调用流程**:Controller → Service → Engine → Interaction/Handler → PLC
3. **分层架构**:Controller → Service → Interaction → Coordination → PLC
4. **添加设备**:实现 `DeviceLogicHandler`(必须)+ `DeviceInteraction`(可选)
5. **自动注册**:通过 Spring 的 `@Component` 和工厂类自动发现和注册
mes-processes/mes-plcSend/README.md
New file
@@ -0,0 +1,645 @@
# MES PLC Send 项目文档
## 📋 项目概述
MES PLC Send 是一个基于 Spring Boot 的多设备联合测试系统,支持 PLC 设备管理、设备组配置、多设备任务编排和执行。系统实现了"模板 + 实例"的设计模式,支持一个设备类型模板对应多个设备实例,实现了设备间的协调和数据传递。
**技术栈**:
- Spring Boot 2.x
- MyBatis-Plus
- S7NetPlus(PLC通信)
- MySQL
- Server-Sent Events (SSE) 实时推送
**服务端口**:10018
## 🎯 核心功能
### 1. 设备管理
- 设备配置管理(PLC IP、设备类型、模块名称等)
- 设备状态监控(在线/离线/忙碌/错误/维护中)
- 支持5种设备类型:
  - **大车设备** (`LOAD_VEHICLE`):支持多实例协调,自动状态管理,MES任务处理
  - **大理片笼** (`LARGE_GLASS`):格子范围配置,逻辑判断
  - **卧转立扫码** (`WORKSTATION_SCANNER`):定时扫描,MES数据读取
  - **卧转立** (`WORKSTATION_TRANSFER`):30s缓冲判定,批量处理
  - **卧式缓存** (`GLASS_STORAGE`):玻璃存储管理(已实现,但当前不使用)
### 2. 设备组管理
- 设备组配置(支持最大并发设备数控制)
- 设备组拓扑可视化
- 设备依赖关系管理(优先级、角色、连接顺序)
- 设备组任务编排
### 3. 多设备任务执行
- **串行执行**:按设备连接顺序执行
- **并行执行**:支持多设备并行执行,通过 `max_concurrent_devices` 控制并发数
- **实时监控**:基于 SSE(Server-Sent Events)的实时状态推送
- **错误处理**:自动重试机制,支持指数退避
- **数据传递**:设备间数据共享和状态同步
### 4. 设备协调服务
- 设备间数据传递机制
- 设备状态同步
- 设备依赖管理
- 多实例协调(如多车协调)
## 🏗️ 架构设计
### 目录结构
```
mes-plcSend/
├── device/                          # 设备管理层
│   ├── entity/                      # 实体类
│   │   ├── DeviceConfig.java        # 设备配置
│   │   ├── DeviceGroupConfig.java   # 设备组配置
│   │   ├── DeviceGroupRelation.java # 设备组关系
│   │   ├── DeviceStatus.java        # 设备状态
│   │   └── GlassInfo.java           # 玻璃信息
│   ├── service/                     # 服务层
│   │   ├── DeviceConfigService.java
│   │   ├── DeviceGroupConfigService.java
│   │   ├── DeviceCoordinationService.java  # 设备协调服务
│   │   └── GlassInfoService.java
│   └── controller/                  # 控制器
│       ├── DeviceConfigController.java
│       ├── DeviceGroupController.java
│       └── DevicePlcController.java
├── interaction/                     # 交互逻辑层
│   ├── base/                        # 基础接口
│   │   ├── BaseDeviceLogicHandler.java
│   │   ├── DeviceInteraction.java
│   │   └── InteractionContext.java
│   │
│   ├── vehicle/                     # 大车设备专用包
│   │   ├── handler/
│   │   │   └── LoadVehicleLogicHandler.java  # 共享逻辑处理器
│   │   ├── flow/
│   │   │   └── LoadVehicleInteraction.java
│   │   ├── coordination/
│   │   │   ├── VehicleStatusManager.java      # 状态管理器
│   │   │   └── VehicleCoordinationService.java # 协调服务
│   │   └── model/
│   │       ├── VehicleStatus.java
│   │       ├── VehiclePosition.java
│   │       ├── VehicleState.java
│   │       ├── VehiclePath.java
│   │       └── VehicleTask.java
│   │
│   ├── workstation/                 # 卧转立设备包
│   │   ├── base/
│   │   │   └── WorkstationBaseHandler.java
│   │   ├── config/
│   │   │   └── WorkstationLogicConfig.java
│   │   ├── scanner/
│   │   │   └── handler/
│   │   │       └── HorizontalScannerLogicHandler.java
│   │   └── transfer/
│   │       └── handler/
│   │           └── HorizontalTransferLogicHandler.java
│   │
│   ├── largeglass/                  # 大理片笼设备包
│   │   ├── handler/
│   │   │   └── LargeGlassLogicHandler.java
│   │   ├── config/
│   │   │   └── LargeGlassConfig.java
│   │   └── model/
│   │       └── GridRange.java
│   │
│   ├── flow/                        # 交互流程(通用)
│   │   ├── GlassStorageInteraction.java
│   │   └── LargeGlassInteraction.java
│   │
│   └── impl/                        # 简单设备实现
│       └── GlassStorageLogicHandler.java
├── task/                            # 任务管理层
│   ├── entity/
│   │   ├── MultiDeviceTask.java     # 多设备任务
│   │   └── TaskStepDetail.java      # 任务步骤详情
│   ├── service/
│   │   ├── MultiDeviceTaskService.java
│   │   ├── TaskExecutionEngine.java  # 任务执行引擎
│   │   └── TaskStatusNotificationService.java  # SSE推送服务
│   ├── controller/
│   │   ├── MultiDeviceTaskController.java
│   │   └── TaskStatusNotificationController.java
│   └── model/
│       ├── RetryPolicy.java         # 重试策略
│       ├── TaskExecutionContext.java
│       └── TaskExecutionResult.java
├── service/                         # PLC服务层
│   ├── PlcDynamicDataService.java   # PLC动态数据服务
│   └── PlcTestWriteService.java
└── s7/                              # S7通信
    └── provider/
        └── S7SerializerProvider.java
```
### 核心设计模式
#### 1. 模板 + 实例模式
**设计理念**:一个设备类型模板(共享逻辑)+ 多个设备实例(独立状态)
```
LoadVehicleLogicHandler (共享逻辑处理器)
    ↓
┌─────────┐  ┌─────────┐  ┌─────────┐
│ 大车实例1│  │ 大车实例2│  │ 大车实例3│
│ 状态独立 │  │ 状态独立 │  │ 状态独立 │
└─────────┘  └─────────┘  └─────────┘
```
**优势**:
- 共享逻辑:所有实例使用同一个逻辑处理器
- 独立状态:每个实例有独立的运行时状态
- 灵活配置:设备组可以包含任意数量的实例
- 易于扩展:新增实例只需在数据库添加记录
#### 2. 混合分层架构
- **复杂设备**(如大车):独立包,包含协调服务、状态管理等
- **简单设备**(如玻璃存储):放在通用包中
## 🔧 核心组件说明
### 1. 大车设备(LoadVehicle)
#### 功能特性
- **空闲状态监控**:没有任务时,`plcRequest` 保持为 1
- **MES任务读取**:当 `mesSend=1` 时,读取 MES 参数(玻璃ID、起始位置、目标位置等)
- **位置映射**:将 MES 位置编号(如 900、901)映射为实际网格位置(如 100、500)
- **时间计算**:根据车辆速度(grids/second)、当前位置、目标位置计算 `gotime` 和 `cartime`
- **状态管理**:`state1~6` 状态流转(0→1→2),自动触发 MES 汇报
- **自动协调**:当 `state=1`(上车完成)时,自动将"卧转立"设备的 `plcRequest` 设置为 0
- **出片逻辑**:支持进片和出片任务,根据 `startSlot` 和 `outboundSlotRanges` 自动判断任务类型
#### 配置参数(extraParams.deviceLogic)
```json
{
  "vehicleCapacity": 6,
  "vehicleSpeed": 1.0,
  "minRange": 1,
  "maxRange": 100,
  "homePosition": 50,
  "idleMonitorIntervalMs": 1000,
  "taskMonitorIntervalMs": 1000,
  "mesConfirmTimeoutMs": 30000,
  "positionMapping": {
    "900": 100,
    "901": 500
  },
  "outboundSlotRanges": [
    {"start": 1000, "end": 2000}
  ],
  "gridPositionMapping": {
    "1000": 80
  }
}
```
#### 状态流转
```
IDLE (空闲) → EXECUTING (执行中) → IDLE (空闲)
```
### 2. 卧转立扫码(HorizontalScanner)
#### 功能特性
- **定时扫描**:可配置扫描间隔(默认 10s)
- **MES数据读取**:当 `mesSend=1` 时,读取玻璃信息(`mesGlassId`、`mesWidth`、`mesHeight`、`workLine`)
- **数据落库**:将玻璃信息保存到 `glass_info` 表
- **自动确认**:读取后自动将 `mesSend` 写回 0
#### 配置参数(extraParams.deviceLogic)
```json
{
  "scanIntervalMs": 10000,
  "workLine": "LINE_001",
  "autoConfirm": true
}
```
### 3. 卧转立(HorizontalTransfer)
#### 功能特性
- **30s缓冲判定**:等待 30s,如果没有下一片玻璃扫码,则认为是最后一片
- **容量判断**:判断能否放下第二片玻璃
- **批量处理**:将多片玻璃组装成批次
- **PLC写入**:写入 `plcGlassId1~6`、`plcGlassCount`、`inPosition`、`plcRequest`
#### 配置参数(extraParams.deviceLogic)
```json
{
  "scanIntervalMs": 10000,
  "bufferTimeoutMs": 30000,
  "vehicleCapacity": 2,
  "monitorIntervalMs": 1000,
  "workLine": "LINE_001",
  "positionValue": 100
}
```
### 4. 大理片笼(LargeGlass)
#### 功能特性
- **格子范围配置**:支持多行格子配置(如第一行 1~52 格,第二行 53~101 格)
- **格子尺寸配置**:每格的长、宽、厚可配置
- **逻辑判断**:用于位置验证和格子管理,不涉及 PLC 写入
#### 配置参数(extraParams.deviceLogic)
```json
{
  "gridRanges": [
    {"row": 1, "start": 1, "end": 52},
    {"row": 2, "start": 53, "end": 101}
  ],
  "gridLength": 2000,
  "gridWidth": 1500,
  "gridThickness": 5
}
```
## 🚀 使用指南
### 1. 设备配置
#### 创建设备
```java
DeviceConfig device = new DeviceConfig();
device.setDeviceId("DEVICE_001");
device.setDeviceCode("DEV_001");
device.setDeviceName("大车设备1");
device.setDeviceType(DeviceConfig.DeviceType.LOAD_VEHICLE);
device.setPlcIp("192.168.1.101");
device.setPlcPort(102);
device.setPlcType(DeviceConfig.PlcType.S7_1200);
device.setModuleName("DB1");
device.setProjectId(1L);
device.setEnabled(true);
// 设置逻辑参数
device.setExtraParams(extraParams);
deviceConfigService.createDevice(device);
```
#### 配置设备逻辑参数
在 `extraParams.deviceLogic` 中配置设备特定的逻辑参数,如车辆速度、位置映射等。
### 2. 设备组配置
#### 创建设备组
```java
DeviceGroupConfig group = new DeviceGroupConfig();
group.setGroupCode("GROUP_001");
group.setGroupName("生产线A");
group.setGroupType(1);  // 1-生产线,2-测试线,3-辅助设备组
group.setProjectId(1L);
group.setStatus(1);  // 0-停用,1-启用,3-维护中
group.setMaxConcurrentDevices(3);  // 最大并发设备数
group.setHeartbeatInterval(30);
group.setCommunicationTimeout(5000);
deviceGroupService.createGroup(group);
```
#### 添加设备到设备组
```java
// 添加设备,设置优先级、角色、连接顺序
deviceGroupService.addDevicesToGroup(groupId, deviceIds, priorities, roles, connectionOrders);
```
### 3. 任务执行
#### 启动多设备任务
```java
MultiDeviceTaskRequest request = new MultiDeviceTaskRequest();
request.setGroupId(groupId);
request.setTaskName("测试任务");
request.setProjectId("PROJECT_001");
request.setParameters(taskParameters);
MultiDeviceTask task = multiDeviceTaskService.startTask(request);
```
#### API 端点
- `POST /device/task/start` - 启动任务
- `POST /device/task/list` - 查询任务列表
- `GET /device/task/{taskId}` - 查询任务详情
- `GET /device/task/{taskId}/steps` - 查询任务步骤详情
- `POST /device/task/{taskId}/cancel` - 取消任务
#### 实时监控(SSE)
前端通过 SSE 连接实时接收任务状态更新:
```javascript
// 监听指定任务
const eventSource = new EventSource('/task/notification/sse?taskId=xxx');
eventSource.addEventListener('taskStatus', (event) => {
  const data = JSON.parse(event.data);
  // 处理任务状态更新
});
eventSource.addEventListener('stepUpdate', (event) => {
  const data = JSON.parse(event.data);
  // 处理步骤更新
});
// 监听所有任务
const eventSourceAll = new EventSource('/task/notification/sse/all');
```
**SSE 端点**:
- `GET /task/notification/sse?taskId=xxx` - 监听指定任务
- `GET /task/notification/sse/all` - 监听所有任务
- `POST /task/notification/close/{taskId}` - 关闭指定任务的SSE连接
- `POST /task/notification/close/all` - 关闭所有SSE连接
## 📊 数据库设计
### 核心表结构
#### device_config(设备配置表)
- `id`:主键(BIGINT)
- `device_id`:设备唯一标识(VARCHAR(50),唯一)
- `device_code`:设备编码(VARCHAR(50),唯一)
- `device_name`:设备名称(VARCHAR(100))
- `device_type`:设备类型(VARCHAR(50))
- `project_id`:所属项目ID(BIGINT)
- `plc_ip`:PLC IP地址(VARCHAR(15))
- `plc_port`:PLC端口(INT)
- `plc_type`:PLC类型(VARCHAR(20))
- `module_name`:模块名称(VARCHAR(50))
- `status`:设备状态(VARCHAR(20))
- `is_primary`:是否主控设备(BOOLEAN)
- `enabled`:是否启用(BOOLEAN)
- `config_json`:设备特定配置(TEXT,JSON格式)
- `extra_params`:扩展参数(JSON)
- `description`:设备描述(VARCHAR(200))
- `is_deleted`:是否删除(INT,0-否,1-是)
- `created_time`、`updated_time`:创建/更新时间
- `created_by`、`updated_by`:创建/更新人
#### device_group_config(设备组配置表)
- `id`:主键(BIGINT)
- `group_code`:设备组编码(VARCHAR(50),唯一)
- `group_name`:设备组名称(VARCHAR(100))
- `group_type`:设备组类型(INT,1-生产线,2-测试线,3-辅助设备组)
- `project_id`:所属项目ID(BIGINT)
- `status`:设备组状态(INT,0-停用,1-启用,3-维护中)
- `max_concurrent_devices`:最大并发设备数(INT)
- `heartbeat_interval`:心跳检测间隔(INT,秒)
- `communication_timeout`:通信超时时间(INT,毫秒)
- `description`:设备组描述(VARCHAR(200))
- `extra_config`:扩展配置(JSON)
- `is_deleted`:是否删除(INT)
- `created_time`、`updated_time`:创建/更新时间
- `created_by`、`updated_by`:创建/更新人
#### device_group_relation(设备组关系表)
- `id`:主键(BIGINT)
- `group_id`:设备组ID(BIGINT)
- `device_id`:设备ID(BIGINT)
- `priority`:设备在组内的优先级(INT,1-最高,10-最低)
- `role`:设备在组内的角色(INT,1-主控,2-协作,3-监控)
- `status`:设备在该组中的状态(INT,0-未配置,1-正常,2-故障,3-维护)
- `connection_order`:连接顺序(INT,数值越小越先连接)
- `relation_desc`:关联描述(VARCHAR(200))
- `extra_params`:扩展参数(JSON)
- `is_deleted`:是否删除(INT)
- `created_time`、`updated_time`:创建/更新时间
- `created_by`、`updated_by`:创建/更新人
#### multi_device_task(多设备任务表)
- `id`:主键(BIGINT)
- `task_id`:任务唯一标识(VARCHAR(50),唯一)
- `group_id`:设备组ID(VARCHAR(50))
- `project_id`:项目ID(VARCHAR(50))
- `status`:任务状态(ENUM:PENDING, RUNNING, COMPLETED, FAILED, CANCELLED)
- `current_step`:当前执行步骤(INT)
- `total_steps`:总步骤数(INT)
- `start_time`:开始时间(DATETIME)
- `end_time`:结束时间(DATETIME)
- `error_message`:错误信息(TEXT)
- `result_data`:结果数据(JSON)
- `created_time`、`updated_time`:创建/更新时间
#### task_step_detail(任务步骤详情表)
- `id`:主键(BIGINT)
- `task_id`:任务ID(VARCHAR(50))
- `step_order`:步骤顺序(INT)
- `device_id`:设备ID(VARCHAR(50))
- `step_name`:步骤名称(VARCHAR(100))
- `status`:步骤状态(ENUM:PENDING, RUNNING, COMPLETED, FAILED, SKIPPED)
- `start_time`:步骤开始时间(DATETIME)
- `end_time`:步骤结束时间(DATETIME)
- `duration_ms`:执行耗时(BIGINT,毫秒)
- `input_data`:输入数据(JSON)
- `output_data`:输出数据(JSON)
- `error_message`:错误信息(TEXT)
- `retry_count`:重试次数(INT)
- `created_time`:创建时间(DATETIME)
#### glass_info(玻璃信息表)
- `id`:主键(BIGINT)
- `glass_id`:玻璃ID(VARCHAR(50))
- `width`:宽度(DECIMAL)
- `height`:高度(DECIMAL)
- `work_line`:产线编号(VARCHAR(50))
- `scan_time`:扫码时间(DATETIME)
- `status`:状态(VARCHAR(20))
- `created_time`、`updated_time`:创建/更新时间
#### device_status(设备状态监控表)
- `id`:主键(BIGINT)
- `device_id`:设备ID(VARCHAR(50))
- `task_id`:关联任务ID(VARCHAR(50),可选)
- `status`:设备状态(ENUM:ONLINE, OFFLINE, BUSY, ERROR, MAINTENANCE)
- `last_heartbeat`:最后心跳时间(DATETIME)
- `cpu_usage`:CPU使用率(DECIMAL(5,2))
- `memory_usage`:内存使用率(DECIMAL(5,2))
- `plc_connection_status`:PLC连接状态(ENUM:CONNECTED, DISCONNECTED, ERROR)
- `current_operation`:当前操作(VARCHAR(100))
- `operation_progress`:操作进度(DECIMAL(5,2),0-100)
- `alert_message`:告警信息(TEXT)
- `created_time`:记录时间(DATETIME)
## 🔄 任务执行流程
### 串行执行流程
```
1. 创建任务记录(status = PENDING)
2. 获取设备组中的设备列表(按 connection_order 排序)
3. 依次执行每个设备:
   a. 检查前置条件
   b. 更新步骤状态为 RUNNING
   c. 执行设备交互逻辑
   d. 传递数据到下一个设备
   e. 更新步骤状态为 COMPLETED
4. 所有设备执行完成后,更新任务状态为 COMPLETED
```
### 并行执行流程
```
1. 创建任务记录(status = PENDING)
2. 获取设备组中的设备列表
3. 使用线程池并行执行设备:
   a. 使用 max_concurrent_devices 控制并发数
   b. 每个设备独立执行
   c. 等待所有设备完成
4. 所有设备执行完成后,更新任务状态为 COMPLETED
```
## ⚙️ 配置说明
### 应用配置(application.yml)
```yaml
server:
  port: 10018
spring:
  profiles:
    active: dev
  application:
    name: plcSend
  liquibase:
    enabled: true
    change-log: classpath:changelog/changelogBase.xml
# PLC配置
s7:
  load:
    dbArea: DB1
    beginIndex: 0
  raw:
    dbArea: DB2
    beginIndex: 0
# PLC模拟配置
plc:
  simulate:
    enabled: false
    interval: 5000
    failure-rate: 0
    task-count: 10
    task-type: normal
# MES配置
mes:
  width: 2800
  height: 5000
```
### 设备逻辑参数配置
每个设备类型的逻辑参数在 `extraParams.deviceLogic` 中配置,具体参数见各设备类型的说明。
## 🛠️ 扩展开发
### 添加新设备类型
1. **在 DeviceConfig.DeviceType 中添加常量**
```java
public static final class DeviceType {
    public static final String NEW_DEVICE = "新设备类型";
}
```
2. **创建设备处理器**
```java
@Component
public class NewDeviceLogicHandler extends BaseDeviceLogicHandler {
    @Override
    public String getDeviceType() {
        return DeviceConfig.DeviceType.NEW_DEVICE;
    }
    @Override
    protected DevicePlcVO.OperationResult doExecute(
            DeviceConfig deviceConfig,
            String operation,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        // 实现设备逻辑
    }
}
```
3. **创建交互流程**(可选)
```java
@Component
public class NewDeviceInteraction implements DeviceInteraction {
    @Override
    public InteractionResult execute(InteractionContext context) {
        // 实现交互流程
    }
}
```
### 添加设备协调逻辑
对于需要多实例协调的复杂设备,可以创建专用包:
```
interaction/
└── newdevice/
    ├── handler/
    ├── coordination/
    └── model/
```
## 📝 注意事项
### 1. 状态管理
- 大车设备的状态存储在内存中(`VehicleStatusManager`),服务重启后会丢失
- 如需持久化,可以扩展支持数据库存储
### 2. 并发安全
- 状态管理器使用 `ConcurrentHashMap`,支持并发访问
- 任务执行使用线程池和 `max_concurrent_devices` 控制并发
### 3. 错误处理
- 支持自动重试机制(默认最多3次)
- 支持指数退避策略
- 错误分类:网络错误、超时错误可重试,业务错误不可重试
### 4. 实时监控
- SSE 连接超时时间:30分钟
- 支持监听指定任务或所有任务
- 连接断开后需要重新连接
### 5. 设备类型说明
- **卧式缓存**(`GLASS_STORAGE`):代码中已实现,但当前业务场景不使用,保留用于未来扩展
## 🎯 已完成功能
### ✅ 核心功能
- [x] 设备管理(配置、状态监控)
- [x] 设备组管理(并发控制、优先级、角色)
- [x] 多设备任务执行引擎(串行/并行)
- [x] 设备协调服务(数据传递、状态同步)
- [x] 实时监控推送(SSE)
- [x] 错误处理和重试机制
### ✅ 设备类型支持
- [x] 大车设备(多实例协调、状态管理、MES任务处理、进片/出片)
- [x] 卧转立扫码(定时扫描、数据落库)
- [x] 卧转立(30s缓冲、批量处理)
- [x] 大理片笼(格子配置、逻辑判断)
- [x] 卧式缓存(已实现,当前不使用)
### ✅ API 端点
- [x] 设备管理 API(`/device/*`)
- [x] 设备组管理 API(`/device/group/*`)
- [x] 多设备任务 API(`/device/task/*`)
- [x] SSE 实时通知 API(`/task/notification/*`)
## 📚 相关文档
- 数据库迁移脚本:`src/main/resources/db/migration/`
- API 文档:通过 Swagger 访问 `/swagger-ui.html`
- 前端文档:见 `mes-web` 项目
## 📞 联系方式
如有问题或建议,请联系开发团队。
mes-processes/mes-plcSend/src/main/java/com/mes/config/TaskExecutorConfig.java
New file
@@ -0,0 +1,63 @@
package com.mes.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
 * 任务执行器配置
 * 用于异步执行多设备组任务
 *
 * @author mes
 * @since 2025-01-XX
 */
@Slf4j
@Configuration
@EnableAsync
public class TaskExecutorConfig {
    /**
     * 设备组任务执行线程池
     * 每个设备组作为一个独立线程执行
     */
    @Bean(name = "deviceGroupTaskExecutor")
    public Executor deviceGroupTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数:支持同时执行的核心设备组数量
        executor.setCorePoolSize(5);
        // 最大线程数:最多同时执行的设备组数量
        executor.setMaxPoolSize(20);
        // 队列容量:等待执行的设备组任务数量
        executor.setQueueCapacity(100);
        // 线程名前缀
        executor.setThreadNamePrefix("DeviceGroupTask-");
        // 线程空闲时间(秒)
        executor.setKeepAliveSeconds(60);
        // 拒绝策略:当线程池和队列都满时,由调用线程执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 等待所有任务结束后再关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        // 等待时间(秒)
        executor.setAwaitTerminationSeconds(60);
        executor.initialize();
        log.info("设备组任务线程池初始化完成: corePoolSize=5, maxPoolSize=20, queueCapacity=100");
        return executor;
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DeviceConfig.java
@@ -27,7 +27,7 @@
    @TableField("device_id")
    private String deviceId;
    @ApiModelProperty(value = "设备名称", example = "上大车设备1")
    @ApiModelProperty(value = "设备名称", example = "大车设备1")
    @TableField("device_name")
    private String deviceName;
@@ -35,7 +35,7 @@
    @TableField("device_code")
    private String deviceCode;
    @ApiModelProperty(value = "设备类型", example = "上大车/大理片/玻璃存储")
    @ApiModelProperty(value = "设备类型", example = "大车设备/大理片笼/卧式缓存")
    @TableField("device_type")
    private String deviceType;
@@ -59,7 +59,7 @@
    @TableField("plc_type")
    private String plcType;
    @ApiModelProperty(value = "模块名称", example = "上大车模块")
    @ApiModelProperty(value = "模块名称", example = "大车设备模块")
    @TableField("module_name")
    private String moduleName;
@@ -75,7 +75,7 @@
    @TableField("config_json")
    private String configJson;
    @ApiModelProperty(value = "设备描述", example = "上大车设备1")
    @ApiModelProperty(value = "设备描述", example = "大车设备1")
    @TableField("description")
    private String description;
@@ -108,9 +108,11 @@
    // 设备类型常量
    public static final class DeviceType {
        public static final String LOAD_VEHICLE = "上大车";      // 上大车
        public static final String LARGE_GLASS = "大理片";      // 大理片
        public static final String GLASS_STORAGE = "玻璃存储";   // 玻璃存储
        public static final String LOAD_VEHICLE = "大车设备";      // 大车设备
        public static final String LARGE_GLASS = "大理片笼";      // 大理片笼
        public static final String GLASS_STORAGE = "卧式缓存";   // 卧式缓存
        public static final String WORKSTATION_SCANNER = "卧转立扫码"; // 卧转立扫码设备
        public static final String WORKSTATION_TRANSFER = "卧转立";    // 卧转立设备
    }
    // PLC类型常量
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/DevicePlcOperationService.java
@@ -1,6 +1,7 @@
package com.mes.device.service;
import com.mes.device.vo.DevicePlcVO;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@@ -11,6 +12,7 @@
 * @author mes
 * @since 2025-11-17
 */
@Service
public interface DevicePlcOperationService {
    DevicePlcVO.OperationResult triggerRequest(Long deviceId);
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceConfigServiceImpl.java
@@ -135,9 +135,9 @@
        
        // 设备类型过滤
        if (deviceType != null && !deviceType.trim().isEmpty()) {
            String convertedDeviceType = convertDeviceTypeFromString(deviceType);
            if (convertedDeviceType != null) {
                wrapper.eq(DeviceConfig::getDeviceType, convertedDeviceType);
            List<String> convertedDeviceTypes = convertDeviceTypeFromString(deviceType);
            if (convertedDeviceTypes != null && !convertedDeviceTypes.isEmpty()) {
                wrapper.in(DeviceConfig::getDeviceType, convertedDeviceTypes);
            }
        }
        
@@ -315,24 +315,42 @@
    /**
     * 字符串转换为设备类型
     */
    private String convertDeviceTypeFromString(String deviceType) {
        if (deviceType == null) return null;
    private List<String> convertDeviceTypeFromString(String deviceType) {
        if (deviceType == null) {
            return Collections.emptyList();
        }
        
        switch (deviceType.trim().toLowerCase()) {
        String normalized = deviceType.trim().toLowerCase();
        switch (normalized) {
            case "load_vehicle":
            case "上大车":
            case "上大车设备":
            case "大车设备":
            case "1":
                return DeviceConfig.DeviceType.LOAD_VEHICLE;
                return Arrays.asList(
                    DeviceConfig.DeviceType.LOAD_VEHICLE,
                    "大车设备"
                );
            case "large_glass":
            case "大理片":
            case "大理片笼":
            case "2":
                return DeviceConfig.DeviceType.LARGE_GLASS;
                return Arrays.asList(
                    DeviceConfig.DeviceType.LARGE_GLASS,
                    "大理片笼"
                );
            case "glass_storage":
            case "玻璃存储":
            case "卧式缓存":
            case "玻璃存储设备":
            case "3":
                return DeviceConfig.DeviceType.GLASS_STORAGE;
                return Arrays.asList(
                    DeviceConfig.DeviceType.GLASS_STORAGE,
                    "卧式缓存",
                    "玻璃存储设备"
                );
            default:
                return null;
                return Collections.emptyList();
        }
    }
@@ -624,12 +642,12 @@
            }
            
            // 设备类型过滤
            if (deviceType != null && !deviceType.trim().isEmpty()) {
                String convertedDeviceType = convertDeviceTypeFromString(deviceType);
                if (convertedDeviceType != null) {
                    wrapper.eq(DeviceConfig::getDeviceType, convertedDeviceType);
                }
        if (deviceType != null && !deviceType.trim().isEmpty()) {
            List<String> convertedDeviceTypes = convertDeviceTypeFromString(deviceType);
            if (convertedDeviceTypes != null && !convertedDeviceTypes.isEmpty()) {
                wrapper.in(DeviceConfig::getDeviceType, convertedDeviceTypes);
            }
        }
            
            // 设备状态过滤
            if (deviceStatus != null && !deviceStatus.trim().isEmpty()) {
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceCoordinationServiceImpl.java
@@ -80,13 +80,13 @@
            // 根据设备类型,提取关键数据并更新上下文
            if (DeviceConfig.DeviceType.LOAD_VEHICLE.equals(fromDevice.getDeviceType())) {
                // 上大车设备完成,传递玻璃ID列表
                // 大车设备完成,传递玻璃ID列表
                Object glassIds = data.get("glassIds");
                if (glassIds instanceof List) {
                    @SuppressWarnings("unchecked")
                    List<String> ids = (List<String>) glassIds;
                    context.setLoadedGlassIds(new ArrayList<>(ids));
                    log.info("上大车设备数据传递: fromDevice={}, toDevice={}, glassIds={}",
                    log.info("大车设备数据传递: fromDevice={}, toDevice={}, glassIds={}",
                        fromDevice.getDeviceCode(), toDevice.getDeviceCode(), ids);
                }
            } else if (DeviceConfig.DeviceType.LARGE_GLASS.equals(fromDevice.getDeviceType())) {
@@ -147,13 +147,13 @@
        // 检查设备类型特定的依赖
        String deviceType = device.getDeviceType();
        if (DeviceConfig.DeviceType.LARGE_GLASS.equals(deviceType)) {
            // 大理片设备需要上大车设备先完成
            // 大理片设备需要大车设备先完成
            List<String> loadedGlassIds = context.getSafeLoadedGlassIds();
            if (CollectionUtils.isEmpty(loadedGlassIds)) {
                missingDependencies.add("上大车设备未完成,缺少玻璃ID列表");
                missingDependencies.add("大车设备未完成,缺少玻璃ID列表");
            }
        } else if (DeviceConfig.DeviceType.GLASS_STORAGE.equals(deviceType)) {
            // 玻璃存储设备需要大理片设备先完成(优先),或上大车设备完成
            // 玻璃存储设备需要大理片设备先完成(优先),或大车设备完成
            List<String> processedGlassIds = context.getSafeProcessedGlassIds();
            List<String> loadedGlassIds = context.getSafeLoadedGlassIds();
            if (CollectionUtils.isEmpty(processedGlassIds) && CollectionUtils.isEmpty(loadedGlassIds)) {
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DevicePlcOperationServiceImpl.java
@@ -40,15 +40,27 @@
    private final PlcTestWriteService plcTestWriteService;
    private final ObjectMapper objectMapper;
    public enum PlcOperationType {
    public static enum PlcOperationType {
        /** PLC请求操作 */
        REQUEST("PLC请求", "PLC 请求发送成功", "PLC 请求发送失败"),
        /** PLC汇报操作 */
        REPORT("PLC汇报", "PLC 汇报模拟成功", "PLC 汇报模拟失败"),
        /** PLC重置操作 */
        RESET("PLC重置", "PLC 状态已重置", "PLC 状态重置失败");
        /** 操作显示名称 */
        private final String display;
        /** 操作成功提示信息 */
        private final String successMsg;
        /** 操作失败提示信息 */
        private final String failedMsg;
        /**
         * 构造方法
         * @param display 操作显示名称
         * @param successMsg 成功提示信息
         * @param failedMsg 失败提示信息
         */
        PlcOperationType(String display, String successMsg, String failedMsg) {
            this.display = display;
            this.successMsg = successMsg;
@@ -103,7 +115,7 @@
            return DevicePlcVO.StatusInfo.builder()
                    .deviceId(deviceId)
                    .deviceName("未知设备")
                    .data(Collections.emptyMap())
                    .fieldValues(Collections.emptyMap())
                    .timestamp(LocalDateTime.now())
                    .build();
        }
@@ -115,7 +127,7 @@
                    .deviceName(device.getDeviceName())
                    .deviceCode(device.getDeviceCode())
                    .projectId(String.valueOf(device.getProjectId()))
                    .data(data)
                    .fieldValues(data)
                    .timestamp(LocalDateTime.now())
                    .build();
        } catch (Exception e) {
@@ -125,7 +137,7 @@
                    .deviceName(device.getDeviceName())
                    .deviceCode(device.getDeviceCode())
                    .projectId(null)
                    .data(Collections.emptyMap())
                    .fieldValues(Collections.emptyMap())
                    .timestamp(LocalDateTime.now())
                    .build();
        }
mes-processes/mes-plcSend/src/main/java/com/mes/device/vo/DevicePlcVO.java
@@ -36,6 +36,7 @@
        private Boolean success;
        private String message;
        private LocalDateTime timestamp;
        private Map<String, Object> data;
    }
    /**
@@ -51,7 +52,7 @@
        private String deviceName;
        private String deviceCode;
        private String projectId;
        private Map<String, Object> data;
        private Map<String, Object> fieldValues;
        private LocalDateTime timestamp;
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceInteractionRegistry.java
@@ -10,6 +10,7 @@
/**
 * 交互注册中心
 * @author huang
 */
@Slf4j
@Component
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceLogicHandler.java
@@ -17,7 +17,7 @@
    /**
     * 获取设备类型(用于匹配处理器)
     * 
     * @return 设备类型,如:"上大车"、"大理片"、"玻璃存储"
     * @return 设备类型,如:"大车设备"、"大理片笼"、"卧式缓存"
     */
    String getDeviceType();
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/GlassStorageInteraction.java
@@ -30,7 +30,7 @@
                return InteractionResult.fail("设备配置不存在");
            }
            // 优先使用处理后的玻璃ID,如果没有则使用上大车的玻璃ID
            // 优先使用处理后的玻璃ID,如果没有则使用大车设备的玻璃ID
            List<String> processed = context.getProcessedGlassIds();
            if (CollectionUtils.isEmpty(processed)) {
                processed = context.getLoadedGlassIds();
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/LargeGlassInteraction.java
@@ -31,14 +31,14 @@
                return InteractionResult.fail("设备配置不存在");
            }
            // 检查上大车是否完成
            // 检查大车设备是否完成
            Object source = context.getSharedData().get("glassesFromVehicle");
            List<String> glassQueue = castList(source);
            if (CollectionUtils.isEmpty(glassQueue)) {
                // 也尝试从上下文获取
                glassQueue = context.getLoadedGlassIds();
                if (CollectionUtils.isEmpty(glassQueue)) {
                    return InteractionResult.waitResult("等待上大车输出", null);
                    return InteractionResult.waitResult("等待大车设备输出", null);
                }
            }
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/LoadVehicleInteraction.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/impl/LargeGlassLogicHandler.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/impl/LoadVehicleLogicHandler.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/largeglass/config/LargeGlassConfig.java
New file
@@ -0,0 +1,37 @@
package com.mes.interaction.largeglass.config;
import com.mes.interaction.largeglass.model.GridRange;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
 * 大理片笼配置
 * 对应 extraParams.deviceLogic 中的字段
 */
@Data
public class LargeGlassConfig {
    /**
     * 格子范围列表
     * 例如:第一行1~52格,第二行53~101格
     */
    private List<GridRange> gridRanges = new ArrayList<>();
    /**
     * 每格长度(mm)
     */
    private Integer gridLength = 2000;
    /**
     * 每格宽度(mm)
     */
    private Integer gridWidth = 1500;
    /**
     * 每格厚度(mm)
     */
    private Integer gridThickness = 5;
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/largeglass/handler/LargeGlassLogicHandler.java
New file
@@ -0,0 +1,379 @@
package com.mes.interaction.largeglass.handler;
import com.mes.device.entity.DeviceConfig;
import com.mes.device.vo.DevicePlcVO;
import com.mes.interaction.BaseDeviceLogicHandler;
import com.mes.device.service.DevicePlcOperationService;
import com.mes.interaction.largeglass.config.LargeGlassConfig;
import com.mes.interaction.largeglass.model.GridRange;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.*;
/**
 * 大理片笼设备逻辑处理器
 * 负责格子范围配置、每格尺寸配置、逻辑判断等
 * 不涉及PLC写入操作,只用于逻辑判断和配置管理
 */
@Slf4j
@Component
public class LargeGlassLogicHandler extends BaseDeviceLogicHandler {
    public LargeGlassLogicHandler(DevicePlcOperationService devicePlcOperationService) {
        super(devicePlcOperationService);
    }
    @Override
    public String getDeviceType() {
        return DeviceConfig.DeviceType.LARGE_GLASS;
    }
    @Override
    protected DevicePlcVO.OperationResult doExecute(DeviceConfig deviceConfig,
                                                    String operation,
                                                    Map<String, Object> params,
                                                    Map<String, Object> logicParams) {
        try {
            LargeGlassConfig config = parseLargeGlassConfig(logicParams);
            switch (operation) {
                case "checkGrid":
                case "validateGrid":
                    return handleCheckGrid(deviceConfig, params, config);
                case "getGridInfo":
                    return handleGetGridInfo(deviceConfig, params, config);
                case "findAvailableGrid":
                    return handleFindAvailableGrid(deviceConfig, params, config);
                case "getGridPosition":
                    return handleGetGridPosition(deviceConfig, params, config);
                default:
                    return buildResult(deviceConfig, operation, false,
                            "不支持的操作: " + operation);
            }
        } catch (Exception e) {
            log.error("大理片笼处理异常: deviceId={}, operation={}",
                    deviceConfig.getId(), operation, e);
            return buildResult(deviceConfig, operation, false,
                    "处理异常: " + e.getMessage());
        }
    }
    /**
     * 检查格子是否有效
     */
    private DevicePlcVO.OperationResult handleCheckGrid(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            LargeGlassConfig config) {
        Integer gridNumber = getIntegerParam(params, "gridNumber");
        if (gridNumber == null) {
            return buildResult(deviceConfig, "checkGrid", false,
                    "未提供格子编号");
        }
        GridRange gridRange = findGridRange(gridNumber, config);
        if (gridRange == null) {
            return buildResult(deviceConfig, "checkGrid", false,
                    String.format("格子编号 %d 不在配置范围内", gridNumber));
        }
        return buildResult(deviceConfig, "checkGrid", true,
                String.format("格子编号 %d 有效,位于第 %d 行,范围 %d~%d",
                        gridNumber, gridRange.getRow(), gridRange.getStart(), gridRange.getEnd()));
    }
    /**
     * 获取格子信息
     */
    private DevicePlcVO.OperationResult handleGetGridInfo(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            LargeGlassConfig config) {
        Integer gridNumber = getIntegerParam(params, "gridNumber");
        if (gridNumber == null) {
            return buildResult(deviceConfig, "getGridInfo", false,
                    "未提供格子编号");
        }
        GridRange gridRange = findGridRange(gridNumber, config);
        if (gridRange == null) {
            return buildResult(deviceConfig, "getGridInfo", false,
                    String.format("格子编号 %d 不在配置范围内", gridNumber));
        }
        Map<String, Object> gridInfo = new HashMap<>();
        gridInfo.put("gridNumber", gridNumber);
        gridInfo.put("row", gridRange.getRow());
        gridInfo.put("start", gridRange.getStart());
        gridInfo.put("end", gridRange.getEnd());
        gridInfo.put("length", config.getGridLength());
        gridInfo.put("width", config.getGridWidth());
        gridInfo.put("thickness", config.getGridThickness());
        return buildResult(deviceConfig, "getGridInfo", true,
                "格子信息获取成功", gridInfo);
    }
    /**
     * 查找可用格子
     */
    private DevicePlcVO.OperationResult handleFindAvailableGrid(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            LargeGlassConfig config) {
        // 这里可以根据实际需求实现查找逻辑
        // 例如:查找第一个可用格子、查找指定行可用格子等
        List<GridRange> gridRanges = config.getGridRanges();
        if (gridRanges == null || gridRanges.isEmpty()) {
            return buildResult(deviceConfig, "findAvailableGrid", false,
                    "未配置格子范围");
        }
        // 简单实现:返回第一个格子的起始位置
        GridRange firstRange = gridRanges.get(0);
        Integer availableGrid = firstRange.getStart();
        Map<String, Object> result = new HashMap<>();
        result.put("availableGrid", availableGrid);
        result.put("row", firstRange.getRow());
        return buildResult(deviceConfig, "findAvailableGrid", true,
                String.format("找到可用格子: %d", availableGrid), result);
    }
    /**
     * 根据目标位置获取格子编号
     */
    private DevicePlcVO.OperationResult handleGetGridPosition(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            LargeGlassConfig config) {
        Integer targetPosition = getIntegerParam(params, "targetPosition");
        if (targetPosition == null) {
            return buildResult(deviceConfig, "getGridPosition", false,
                    "未提供目标位置");
        }
        // 查找目标位置在哪个格子范围内
        GridRange gridRange = findGridRangeByPosition(targetPosition, config);
        if (gridRange == null) {
            return buildResult(deviceConfig, "getGridPosition", false,
                    String.format("目标位置 %d 不在任何格子范围内", targetPosition));
        }
        // 计算格子编号(假设位置就是格子编号,或根据实际规则计算)
        Integer gridNumber = targetPosition;
        Map<String, Object> result = new HashMap<>();
        result.put("gridNumber", gridNumber);
        result.put("row", gridRange.getRow());
        result.put("position", targetPosition);
        return buildResult(deviceConfig, "getGridPosition", true,
                String.format("目标位置 %d 对应格子编号 %d,第 %d 行",
                        targetPosition, gridNumber, gridRange.getRow()), result);
    }
    /**
     * 解析大理片笼配置
     */
    private LargeGlassConfig parseLargeGlassConfig(Map<String, Object> logicParams) {
        LargeGlassConfig config = new LargeGlassConfig();
        if (logicParams == null) {
            return config;
        }
        // 解析格子范围配置
        @SuppressWarnings("unchecked")
        List<Map<String, Object>> gridRangesConfig =
                (List<Map<String, Object>>) logicParams.get("gridRanges");
        if (gridRangesConfig != null) {
            List<GridRange> gridRanges = new ArrayList<>();
            for (Map<String, Object> rangeConfig : gridRangesConfig) {
                Integer row = getIntegerValue(rangeConfig.get("row"));
                Integer start = getIntegerValue(rangeConfig.get("start"));
                Integer end = getIntegerValue(rangeConfig.get("end"));
                if (row != null && start != null && end != null) {
                    gridRanges.add(new GridRange(row, start, end));
                }
            }
            config.setGridRanges(gridRanges);
        }
        // 解析每格尺寸
        config.setGridLength(getLogicParam(logicParams, "gridLength", 2000)); // 默认2000mm
        config.setGridWidth(getLogicParam(logicParams, "gridWidth", 1500));  // 默认1500mm
        config.setGridThickness(getLogicParam(logicParams, "gridThickness", 5)); // 默认5mm
        return config;
    }
    /**
     * 查找格子所在的格子范围
     */
    private GridRange findGridRange(Integer gridNumber, LargeGlassConfig config) {
        List<GridRange> gridRanges = config.getGridRanges();
        if (gridRanges == null) {
            return null;
        }
        for (GridRange range : gridRanges) {
            if (gridNumber >= range.getStart() && gridNumber <= range.getEnd()) {
                return range;
            }
        }
        return null;
    }
    /**
     * 根据位置查找格子范围
     */
    private GridRange findGridRangeByPosition(Integer position, LargeGlassConfig config) {
        List<GridRange> gridRanges = config.getGridRanges();
        if (gridRanges == null) {
            return null;
        }
        for (GridRange range : gridRanges) {
            if (position >= range.getStart() && position <= range.getEnd()) {
                return range;
            }
        }
        return null;
    }
    /**
     * 获取整数参数
     */
    private Integer getIntegerParam(Map<String, Object> params, String key) {
        if (params == null) {
            return null;
        }
        return getIntegerValue(params.get(key));
    }
    /**
     * 获取整数值
     */
    private Integer getIntegerValue(Object value) {
        if (value instanceof Number) {
            return ((Number) value).intValue();
        }
        if (value == null) {
            return null;
        }
        try {
            return Integer.parseInt(String.valueOf(value));
        } catch (NumberFormatException e) {
            return null;
        }
    }
    /**
     * 构建操作结果
     */
    private DevicePlcVO.OperationResult buildResult(DeviceConfig deviceConfig,
                                                    String operation,
                                                    boolean success,
                                                    String message) {
        return buildResult(deviceConfig, operation, success, message, null);
    }
    /**
     * 构建操作结果(带数据)
     */
    private DevicePlcVO.OperationResult buildResult(DeviceConfig deviceConfig,
                                                    String operation,
                                                    boolean success,
                                                    String message,
                                                    Map<String, Object> data) {
        DevicePlcVO.OperationResult.OperationResultBuilder builder = DevicePlcVO.OperationResult.builder()
                .deviceId(deviceConfig.getId())
                .deviceName(deviceConfig.getDeviceName())
                .deviceCode(deviceConfig.getDeviceCode())
                .projectId(deviceConfig.getProjectId() != null ?
                        String.valueOf(deviceConfig.getProjectId()) : null)
                .operation(operation)
                .success(success)
                .message(message)
                .timestamp(LocalDateTime.now());
        if (data != null) {
            builder.data(data);
        }
        return builder.build();
    }
    @Override
    public String validateLogicParams(DeviceConfig deviceConfig) {
        Map<String, Object> logicParams = parseLogicParams(deviceConfig);
        LargeGlassConfig config = parseLargeGlassConfig(logicParams);
        // 验证格子范围配置
        List<GridRange> gridRanges = config.getGridRanges();
        if (gridRanges == null || gridRanges.isEmpty()) {
            return "必须配置至少一个格子范围(gridRanges)";
        }
        // 验证每格尺寸
        if (config.getGridLength() == null || config.getGridLength() <= 0) {
            return "格子长度(gridLength)必须大于0";
        }
        if (config.getGridWidth() == null || config.getGridWidth() <= 0) {
            return "格子宽度(gridWidth)必须大于0";
        }
        if (config.getGridThickness() == null || config.getGridThickness() <= 0) {
            return "格子厚度(gridThickness)必须大于0";
        }
        return null; // 验证通过
    }
    @Override
    public String getDefaultLogicParams() {
        Map<String, Object> defaultParams = new HashMap<>();
        // 默认格子范围配置:第一行1~52格,第二行53~101格
        List<Map<String, Object>> gridRanges = new ArrayList<>();
        Map<String, Object> row1 = new HashMap<>();
        row1.put("row", 1);
        row1.put("start", 1);
        row1.put("end", 52);
        gridRanges.add(row1);
        Map<String, Object> row2 = new HashMap<>();
        row2.put("row", 2);
        row2.put("start", 53);
        row2.put("end", 101);
        gridRanges.add(row2);
        defaultParams.put("gridRanges", gridRanges);
        // 默认每格尺寸(mm)
        defaultParams.put("gridLength", 2000);   // 长度2000mm
        defaultParams.put("gridWidth", 1500);    // 宽度1500mm
        defaultParams.put("gridThickness", 5);   // 厚度5mm
        try {
            return objectMapper.writeValueAsString(defaultParams);
        } catch (Exception e) {
            log.error("生成默认逻辑参数失败", e);
            return "{}";
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/largeglass/model/GridRange.java
New file
@@ -0,0 +1,30 @@
package com.mes.interaction.largeglass.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
 * 格子范围
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GridRange {
    /**
     * 行号(第几行)
     */
    private Integer row;
    /**
     * 起始格子编号
     */
    private Integer start;
    /**
     * 结束格子编号
     */
    private Integer end;
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/coordination/VehicleCoordinationService.java
New file
@@ -0,0 +1,189 @@
package com.mes.interaction.vehicle.coordination;
import com.mes.device.entity.DeviceConfig;
import com.mes.device.service.DeviceConfigService;
import com.mes.device.service.DeviceGroupRelationService;
import com.mes.device.vo.DeviceGroupVO;
import com.mes.interaction.vehicle.model.VehiclePath;
import com.mes.interaction.vehicle.model.VehicleStatus;
import com.mes.interaction.vehicle.model.VehicleState;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
 * 车辆协调服务
 * 负责在多个大车实例中选择和分配任务
 *
 * @author mes
 * @since 2025-01-XX
 */
@Slf4j
@Service
public class VehicleCoordinationService {
    @Autowired
    private VehicleStatusManager statusManager;
    @Autowired
    private DeviceConfigService deviceConfigService;
    @Autowired
    private DeviceGroupRelationService deviceGroupRelationService;
    /**
     * 从设备组中选择一个可用的大车实例
     *
     * @param groupId 设备组ID
     * @return 可用的设备配置,如果没有可用车辆则返回null
     */
    public DeviceConfig selectAvailableVehicle(Long groupId) {
        if (groupId == null) {
            log.warn("设备组ID为空,无法选择车辆");
            return null;
        }
        // 1. 获取设备组中的所有设备
        List<DeviceGroupVO.DeviceInfo> groupDevices = deviceGroupRelationService.getGroupDevices(groupId);
        if (groupDevices == null || groupDevices.isEmpty()) {
            log.warn("设备组 {} 中没有设备", groupId);
            return null;
        }
        // 2. 过滤出大车类型的设备
        List<DeviceGroupVO.DeviceInfo> vehicles = groupDevices.stream()
            .filter(d -> DeviceConfig.DeviceType.LOAD_VEHICLE.equals(d.getDeviceType()))
            .collect(Collectors.toList());
        if (vehicles.isEmpty()) {
            log.warn("设备组 {} 中没有大车设备", groupId);
            return null;
        }
        // 3. 过滤出可用的车辆
        List<DeviceGroupVO.DeviceInfo> availableVehicles = vehicles.stream()
            .filter(v -> {
                String deviceId = v.getId() != null ? v.getId().toString() : null;
                if (deviceId == null) {
                    deviceId = v.getDeviceCode();
                }
                return statusManager.isVehicleAvailable(deviceId);
            })
            .collect(Collectors.toList());
        if (availableVehicles.isEmpty()) {
            log.warn("设备组 {} 中没有可用的大车设备", groupId);
            return null;
        }
        // 4. 选择策略(简单策略:选择第一个可用的,可以扩展为按优先级、负载等选择)
        DeviceGroupVO.DeviceInfo selected = availableVehicles.get(0);
        // 5. 获取完整的设备配置
        DeviceConfig deviceConfig = null;
        if (selected.getId() != null) {
            deviceConfig = deviceConfigService.getDeviceById(selected.getId());
        } else if (selected.getDeviceCode() != null) {
            deviceConfig = deviceConfigService.getDeviceByCode(selected.getDeviceCode());
        }
        if (deviceConfig != null) {
            log.info("选择可用车辆: deviceId={}, deviceName={}",
                deviceConfig.getDeviceId(), deviceConfig.getDeviceName());
        }
        return deviceConfig;
    }
    /**
     * 检查路径冲突
     *
     * @param deviceId 设备ID
     * @param plannedPath 计划路径
     * @return true表示有冲突,false表示无冲突
     */
    public boolean hasPathConflict(String deviceId, VehiclePath plannedPath) {
        if (deviceId == null || plannedPath == null) {
            return false;
        }
        // 获取所有执行中的车辆
        List<VehicleStatus> executingVehicles = statusManager.getExecutingVehicles();
        // 排除自己
        executingVehicles = executingVehicles.stream()
            .filter(v -> !v.getDeviceId().equals(deviceId))
            .collect(Collectors.toList());
        // 检查路径是否冲突
        for (VehicleStatus vehicle : executingVehicles) {
            if (vehicle.getCurrentTask() != null) {
                VehiclePath existingPath = vehicle.getCurrentTask().getPlannedPath();
                if (existingPath != null && plannedPath.conflictsWith(existingPath)) {
                    log.warn("检测到路径冲突: deviceId={}, 与 deviceId={} 的路径冲突",
                        deviceId, vehicle.getDeviceId());
                    return true;
                }
            }
        }
        return false;
    }
    /**
     * 获取设备组中所有大车设备
     *
     * @param groupId 设备组ID
     * @return 大车设备列表
     */
    public List<DeviceConfig> getVehiclesInGroup(Long groupId) {
        if (groupId == null) {
            return Collections.emptyList();
        }
        List<DeviceGroupVO.DeviceInfo> groupDevices = deviceGroupRelationService.getGroupDevices(groupId);
        if (groupDevices == null || groupDevices.isEmpty()) {
            return Collections.emptyList();
        }
        return groupDevices.stream()
            .filter(d -> DeviceConfig.DeviceType.LOAD_VEHICLE.equals(d.getDeviceType()))
            .map(d -> {
                if (d.getId() != null) {
                    return deviceConfigService.getDeviceById(d.getId());
                } else if (d.getDeviceCode() != null) {
                    return deviceConfigService.getDeviceByCode(d.getDeviceCode());
                }
                return null;
            })
            .filter(d -> d != null)
            .collect(Collectors.toList());
    }
    /**
     * 获取设备组中所有可用的大车设备
     *
     * @param groupId 设备组ID
     * @return 可用的大车设备列表
     */
    public List<DeviceConfig> getAvailableVehiclesInGroup(Long groupId) {
        return getVehiclesInGroup(groupId).stream()
            .filter(v -> statusManager.isVehicleAvailable(v.getDeviceId()))
            .collect(Collectors.toList());
    }
    /**
     * 检查设备组中是否有可用的大车
     *
     * @param groupId 设备组ID
     * @return true表示有可用车辆,false表示没有
     */
    public boolean hasAvailableVehicle(Long groupId) {
        return selectAvailableVehicle(groupId) != null;
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/coordination/VehicleStatusManager.java
New file
@@ -0,0 +1,199 @@
package com.mes.interaction.vehicle.coordination;
import com.mes.interaction.vehicle.model.VehicleState;
import com.mes.interaction.vehicle.model.VehicleStatus;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
 * 车辆状态管理器
 * 管理所有大车设备实例的运行时状态
 *
 * @author huang
 * @since 2025-11-21
 */
@Slf4j
@Service
public class VehicleStatusManager {
    /**
     * 存储所有车辆实例的状态:deviceId -> VehicleStatus
     */
    private final Map<String, VehicleStatus> vehicleStatusMap = new ConcurrentHashMap<>();
    /**
     * 获取车辆状态
     * 如果不存在则创建并返回空闲状态
     *
     * @param deviceId 设备ID
     * @return 车辆状态
     */
    public VehicleStatus getVehicleStatus(String deviceId) {
        if (deviceId == null || deviceId.isEmpty()) {
            return null;
        }
        return vehicleStatusMap.get(deviceId);
    }
    /**
     * 获取或创建车辆状态
     *
     * @param deviceId 设备ID
     * @param deviceName 设备名称
     * @return 车辆状态
     */
    public VehicleStatus getOrCreateVehicleStatus(String deviceId, String deviceName) {
        return vehicleStatusMap.computeIfAbsent(deviceId,
            k -> new VehicleStatus(deviceId, deviceName));
    }
    /**
     * 更新车辆状态
     *
     * @param deviceId 设备ID
     * @param state 新状态
     */
    public void updateVehicleStatus(String deviceId, VehicleState state) {
        if (deviceId == null || deviceId.isEmpty()) {
            log.warn("设备ID为空,无法更新状态");
            return;
        }
        VehicleStatus status = vehicleStatusMap.computeIfAbsent(
            deviceId,
            k -> new VehicleStatus(deviceId)
        );
        status.setState(state);
        log.debug("更新车辆状态: deviceId={}, state={}", deviceId, state);
    }
    /**
     * 更新车辆状态(带设备名称)
     *
     * @param deviceId 设备ID
     * @param deviceName 设备名称
     * @param state 新状态
     */
    public void updateVehicleStatus(String deviceId, String deviceName, VehicleState state) {
        VehicleStatus status = getOrCreateVehicleStatus(deviceId, deviceName);
        status.setState(state);
        log.debug("更新车辆状态: deviceId={}, deviceName={}, state={}", deviceId, deviceName, state);
    }
    /**
     * 获取所有空闲的车辆
     *
     * @return 空闲车辆状态列表
     */
    public List<VehicleStatus> getIdleVehicles() {
        return vehicleStatusMap.values().stream()
            .filter(v -> v.getState() == VehicleState.IDLE)
            .collect(Collectors.toList());
    }
    /**
     * 获取所有执行中的车辆
     *
     * @return 执行中车辆状态列表
     */
    public List<VehicleStatus> getExecutingVehicles() {
        return vehicleStatusMap.values().stream()
            .filter(v -> v.getState() == VehicleState.EXECUTING)
            .collect(Collectors.toList());
    }
    /**
     * 获取所有等待中的车辆
     *
     * @return 等待中车辆状态列表
     */
    public List<VehicleStatus> getWaitingVehicles() {
        return vehicleStatusMap.values().stream()
            .filter(v -> v.getState() == VehicleState.WAITING)
            .collect(Collectors.toList());
    }
    /**
     * 检查车辆是否可用
     *
     * @param deviceId 设备ID
     * @return true表示可用,false表示不可用
     */
    public boolean isVehicleAvailable(String deviceId) {
        if (deviceId == null || deviceId.isEmpty()) {
            return false;
        }
        VehicleStatus status = vehicleStatusMap.get(deviceId);
        return status == null || status.isAvailable();
    }
    /**
     * 设置车辆任务
     *
     * @param deviceId 设备ID
     * @param task 任务
     */
    public void setVehicleTask(String deviceId, com.mes.interaction.vehicle.model.VehicleTask task) {
        VehicleStatus status = vehicleStatusMap.get(deviceId);
        if (status != null) {
            status.setCurrentTask(task);
            if (task != null && task.getPlannedPath() != null) {
                status.setTargetPosition(task.getPlannedPath().getEndPosition());
            }
        }
    }
    /**
     * 清除车辆任务
     *
     * @param deviceId 设备ID
     */
    public void clearVehicleTask(String deviceId) {
        VehicleStatus status = vehicleStatusMap.get(deviceId);
        if (status != null) {
            status.setCurrentTask(null);
            status.setTargetPosition(null);
        }
    }
    /**
     * 移除车辆状态(当设备被删除时)
     *
     * @param deviceId 设备ID
     */
    public void removeVehicleStatus(String deviceId) {
        vehicleStatusMap.remove(deviceId);
        log.info("移除车辆状态: deviceId={}", deviceId);
    }
    /**
     * 获取所有车辆状态
     *
     * @return 所有车辆状态列表
     */
    public List<VehicleStatus> getAllVehicleStatuses() {
        return vehicleStatusMap.values().stream()
            .collect(Collectors.toList());
    }
    /**
     * 获取指定设备组中的车辆状态
     *
     * @param deviceIds 设备ID列表
     * @return 车辆状态列表
     */
    public List<VehicleStatus> getVehicleStatuses(List<String> deviceIds) {
        return deviceIds.stream()
            .map(this::getVehicleStatus)
            .filter(status -> status != null)
            .collect(Collectors.toList());
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/flow/LoadVehicleInteraction.java
New file
@@ -0,0 +1,225 @@
package com.mes.interaction.vehicle.flow;
import com.mes.device.entity.DeviceConfig;
import com.mes.device.service.DeviceInteractionService;
import com.mes.device.vo.DevicePlcVO;
import com.mes.interaction.DeviceInteraction;
import com.mes.interaction.base.InteractionContext;
import com.mes.interaction.base.InteractionResult;
import com.mes.interaction.vehicle.coordination.VehicleCoordinationService;
import com.mes.interaction.vehicle.coordination.VehicleStatusManager;
import com.mes.interaction.vehicle.model.VehicleState;
import com.mes.interaction.vehicle.model.VehiclePosition;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * 大车设备交互实现(增强版)
 * 集成多车协调和状态管理功能
 *
 * @author mes
 * @since 2025-01-XX
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class LoadVehicleInteraction implements DeviceInteraction {
    private final DeviceInteractionService deviceInteractionService;
    @Autowired
    private VehicleCoordinationService coordinationService;
    @Autowired
    private VehicleStatusManager statusManager;
    @Override
    public String getDeviceType() {
        return DeviceConfig.DeviceType.LOAD_VEHICLE;
    }
    @Override
    public InteractionResult execute(InteractionContext context) {
        try {
            // 前置条件验证
            if (context.getCurrentDevice() == null) {
                return InteractionResult.fail("设备配置不存在");
            }
            DeviceConfig currentDevice = context.getCurrentDevice();
            String deviceId = currentDevice.getDeviceId();
            // 1. 检查车辆状态(如果设备已指定)
            if (deviceId != null) {
                if (!statusManager.isVehicleAvailable(deviceId)) {
                    com.mes.interaction.vehicle.model.VehicleStatus status =
                        statusManager.getVehicleStatus(deviceId);
                    String stateMsg = status != null ? status.getState().name() : "未知";
                    return InteractionResult.fail(
                        String.format("车辆 %s (%s) 当前状态为 %s,无法执行操作",
                            currentDevice.getDeviceName(), deviceId, stateMsg));
                }
            }
            // 2. 如果没有指定设备,尝试从设备组中选择可用车辆
            DeviceConfig selectedDevice = currentDevice;
            Long groupId = extractGroupId(context);
            if (groupId != null && (deviceId == null || !statusManager.isVehicleAvailable(deviceId))) {
                DeviceConfig availableVehicle = coordinationService.selectAvailableVehicle(groupId);
                if (availableVehicle != null) {
                    selectedDevice = availableVehicle;
                    log.info("从设备组 {} 中选择可用车辆: {}", groupId, availableVehicle.getDeviceName());
                } else if (deviceId == null) {
                    // 没有可用车辆,返回等待结果
                    return InteractionResult.waitResult(
                        "设备组中没有可用的大车设备,等待车辆空闲", null);
                }
            }
            // 3. 验证玻璃ID
            List<String> glassIds = context.getParameters().getGlassIds();
            if (CollectionUtils.isEmpty(glassIds)) {
                return InteractionResult.waitResult("未提供玻璃ID,等待输入", null);
            }
            // 验证玻璃ID格式
            for (String glassId : glassIds) {
                if (glassId == null || glassId.trim().isEmpty()) {
                    return InteractionResult.fail("玻璃ID不能为空");
                }
            }
            // 4. 标记车辆为执行中
            String selectedDeviceId = selectedDevice.getDeviceId();
            statusManager.updateVehicleStatus(
                selectedDeviceId,
                selectedDevice.getDeviceName(),
                VehicleState.EXECUTING);
            try {
                // 5. 构建PLC写入参数
                Map<String, Object> params = new HashMap<>();
                params.put("glassIds", glassIds);
                params.put("positionCode", context.getParameters().getPositionCode());
                params.put("positionValue", context.getParameters().getPositionValue());
                params.put("triggerRequest", true);
                // 6. 执行实际的PLC写入操作
                DevicePlcVO.OperationResult plcResult = deviceInteractionService.executeOperation(
                        selectedDevice.getId(),
                        "feedGlass",
                        params
                );
                // 7. 检查PLC写入结果
                if (plcResult == null || !Boolean.TRUE.equals(plcResult.getSuccess())) {
                    String errorMsg = plcResult != null ? plcResult.getMessage() : "PLC写入操作返回空结果";
                    // 执行失败,恢复为空闲状态
                    statusManager.updateVehicleStatus(selectedDeviceId, VehicleState.IDLE);
                    return InteractionResult.fail("PLC写入失败: " + errorMsg);
                }
                // 8. 更新车辆位置信息(如果有)
                if (context.getParameters().getPositionCode() != null ||
                    context.getParameters().getPositionValue() != null) {
                    com.mes.interaction.vehicle.model.VehicleStatus vehicleStatus =
                        statusManager.getOrCreateVehicleStatus(
                            selectedDeviceId,
                            selectedDevice.getDeviceName());
                    VehiclePosition position = new VehiclePosition(
                        context.getParameters().getPositionCode(),
                        context.getParameters().getPositionValue());
                    vehicleStatus.setCurrentPosition(position);
                }
                // 9. 执行大车设备操作(数据流转)
                List<String> copied = new ArrayList<>(glassIds);
                context.setLoadedGlassIds(copied);
                context.getSharedData().put("glassesFromVehicle", copied);
                context.getSharedData().put("loadVehicleTime", System.currentTimeMillis());
                context.getSharedData().put("selectedVehicleId", selectedDeviceId);
                context.getSharedData().put("selectedVehicleName", selectedDevice.getDeviceName());
                // 10. 后置条件检查
                if (context.getLoadedGlassIds().isEmpty()) {
                    statusManager.updateVehicleStatus(selectedDeviceId, VehicleState.IDLE);
                    return InteractionResult.fail("大车设备操作失败:玻璃ID列表为空");
                }
                // 11. 构建返回数据
                Map<String, Object> data = new HashMap<>();
                data.put("loaded", copied);
                data.put("glassCount", copied.size());
                data.put("deviceId", selectedDevice.getId());
                data.put("deviceCode", selectedDevice.getDeviceCode());
                data.put("deviceName", selectedDevice.getDeviceName());
                data.put("deviceIdString", selectedDeviceId);
                data.put("plcResult", plcResult.getMessage());
                // 注意:这里不立即恢复为空闲状态,因为实际执行可能需要时间
                // 真正的状态恢复应该在任务完成后通过回调或状态查询来更新
                // 或者可以通过异步任务在后台监控PLC状态,确认完成后再恢复
                log.info("大车设备交互执行成功: deviceId={}, deviceName={}, glassCount={}",
                    selectedDeviceId, selectedDevice.getDeviceName(), copied.size());
                return InteractionResult.success(data);
            } catch (Exception e) {
                // 发生异常时,恢复为空闲状态
                statusManager.updateVehicleStatus(selectedDeviceId, VehicleState.ERROR);
                log.error("大车设备交互执行异常: deviceId={}", selectedDeviceId, e);
                throw e;
            }
        } catch (Exception e) {
            return InteractionResult.fail("大车设备交互执行异常: " + e.getMessage());
        }
    }
    /**
     * 从上下文中提取设备组ID
     */
    private Long extractGroupId(InteractionContext context) {
        // 尝试从共享数据中获取
        Map<String, Object> sharedData = context.getSharedData();
        if (sharedData != null) {
            Object groupIdObj = sharedData.get("groupId");
            if (groupIdObj instanceof Number) {
                return ((Number) groupIdObj).longValue();
            } else if (groupIdObj instanceof String) {
                try {
                    return Long.parseLong((String) groupIdObj);
                } catch (NumberFormatException ignored) {
                }
            }
        }
        // 尝试从任务参数中获取
        if (context.getParameters() != null && context.getParameters().getExtra() != null) {
            Object groupIdObj = context.getParameters().getExtra().get("groupId");
            if (groupIdObj instanceof Number) {
                return ((Number) groupIdObj).longValue();
            }
        }
        return null;
    }
    @Override
    public boolean supportsOperation(String operation) {
        // 支持所有操作,但主要关注 feedGlass
        return true;
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/handler/LoadVehicleLogicHandler.java
New file
@@ -0,0 +1,1719 @@
package com.mes.interaction.vehicle.handler;
import com.mes.device.entity.DeviceConfig;
import com.mes.device.service.DeviceConfigService;
import com.mes.device.service.DeviceGroupRelationService;
import com.mes.device.service.DevicePlcOperationService;
import com.mes.device.service.GlassInfoService;
import com.mes.device.vo.DeviceGroupVO;
import com.mes.device.vo.DevicePlcVO;
import com.mes.interaction.BaseDeviceLogicHandler;
import com.mes.interaction.vehicle.coordination.VehicleStatusManager;
import com.mes.interaction.vehicle.model.VehiclePosition;
import com.mes.interaction.vehicle.model.VehicleState;
import com.mes.interaction.vehicle.model.VehicleStatus;
import com.mes.interaction.vehicle.model.VehicleTask;
import com.mes.s7.enhanced.EnhancedS7Serializer;
import com.mes.s7.provider.S7SerializerProvider;
import com.mes.service.PlcDynamicDataService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import javax.annotation.PreDestroy;
import java.util.*;
import java.util.concurrent.*;
/**
 * 大车设备逻辑处理器
 * 所有大车设备实例共享这个处理器
 * 集成多实例状态管理和协调功能
 *
 * @author huang
 * @since 2025-11-21
 */
@Slf4j
@Component
public class LoadVehicleLogicHandler extends BaseDeviceLogicHandler {
    private final GlassInfoService glassInfoService;
    @Autowired
    private VehicleStatusManager statusManager;
    @Autowired(required = false)
    private DeviceConfigService deviceConfigService;
    @Autowired(required = false)
    private DeviceGroupRelationService deviceGroupRelationService;
    @Autowired(required = false)
    private PlcDynamicDataService plcDynamicDataService;
    @Autowired(required = false)
    private S7SerializerProvider s7SerializerProvider;
    // MES字段列表(进片和出片共用同一套协议)
    private static final List<String> MES_FIELDS = Arrays.asList(
            "mesSend", "mesGlassId", "mesWidth", "mesHeight",
            "startSlot", "targetSlot", "workLine"
    );
    // 监控线程池:用于定期检查大车状态并协调卧转立设备
    private final ScheduledExecutorService stateMonitorExecutor = Executors.newScheduledThreadPool(5, r -> {
        Thread t = new Thread(r, "VehicleStateMonitor");
        t.setDaemon(true);
        return t;
    });
    // 空闲监控线程池:用于保持plcRequest=1
    private final ScheduledExecutorService idleMonitorExecutor = Executors.newScheduledThreadPool(3, r -> {
        Thread t = new Thread(r, "VehicleIdleMonitor");
        t.setDaemon(true);
        return t;
    });
    // 任务监控线程池:用于监控任务执行和状态切换
    private final ScheduledExecutorService taskMonitorExecutor = Executors.newScheduledThreadPool(5, r -> {
        Thread t = new Thread(r, "VehicleTaskMonitor");
        t.setDaemon(true);
        return t;
    });
    // 记录正在监控的设备:deviceId -> 监控任务
    private final Map<String, ScheduledFuture<?>> monitoringTasks = new ConcurrentHashMap<>();
    // 记录空闲监控任务:deviceId -> 空闲监控任务
    private final Map<String, ScheduledFuture<?>> idleMonitoringTasks = new ConcurrentHashMap<>();
    // 记录任务监控任务:deviceId -> 任务监控任务
    private final Map<String, ScheduledFuture<?>> taskMonitoringTasks = new ConcurrentHashMap<>();
    // 记录已协调的设备:deviceId -> 已协调的state字段集合(避免重复协调)
    private final Map<String, List<String>> coordinatedStates = new ConcurrentHashMap<>();
    // 记录当前任务:deviceId -> 任务信息
    private final Map<String, MesTaskInfo> currentTasks = new ConcurrentHashMap<>();
    @Autowired
    public LoadVehicleLogicHandler(
            DevicePlcOperationService devicePlcOperationService,
            @Qualifier("deviceGlassInfoService") GlassInfoService glassInfoService) {
        super(devicePlcOperationService);
        this.glassInfoService = glassInfoService;
    }
    @Override
    public String getDeviceType() {
        return DeviceConfig.DeviceType.LOAD_VEHICLE;
    }
    @Override
    protected DevicePlcVO.OperationResult doExecute(
            DeviceConfig deviceConfig,
            String operation,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        String deviceId = deviceConfig.getDeviceId();
        log.info("执行大车设备操作: deviceId={}, deviceName={}, operation={}",
            deviceId, deviceConfig.getDeviceName(), operation);
        // 1. 检查这个设备实例的状态(对于需要状态检查的操作)
        if (needsStateCheck(operation)) {
            VehicleStatus status = statusManager.getVehicleStatus(deviceId);
            if (status != null && !status.isAvailable()) {
                return DevicePlcVO.OperationResult.builder()
                    .success(false)
                    .message(String.format("车辆 %s (%s) 当前状态为 %s,无法执行操作 %s",
                        deviceConfig.getDeviceName(),
                        deviceId,
                        status.getState(),
                        operation))
                    .build();
            }
        }
        // 2. 标记为执行中(对于需要状态管理的操作)
        if (needsStateManagement(operation)) {
            statusManager.updateVehicleStatus(deviceId, deviceConfig.getDeviceName(), VehicleState.EXECUTING);
            // 创建任务信息
            VehicleTask task = createVehicleTask(deviceConfig, operation, params, logicParams);
            statusManager.setVehicleTask(deviceId, task);
        }
        try {
            // 3. 执行原有逻辑
            DevicePlcVO.OperationResult result;
            switch (operation) {
                case "feedGlass":
                    result = handleFeedGlass(deviceConfig, params, logicParams);
                    break;
                case "triggerRequest":
                    result = handleTriggerRequest(deviceConfig, params, logicParams);
                    break;
                case "triggerReport":
                    result = handleTriggerReport(deviceConfig, params, logicParams);
                    break;
                case "reset":
                    result = handleReset(deviceConfig, params, logicParams);
                    break;
                case "clearGlass":
                case "clearPlc":
                case "clear":
                    result = handleClearGlass(deviceConfig, params, logicParams);
                    break;
                case "checkStateAndCoordinate":
                    result = handleCheckStateAndCoordinate(deviceConfig, params, logicParams);
                    break;
                case "startIdleMonitor":
                    result = handleStartIdleMonitor(deviceConfig, params, logicParams);
                    break;
                case "stopIdleMonitor":
                    result = handleStopIdleMonitor(deviceConfig);
                    break;
                case "checkMesTask":
                    result = handleCheckMesTask(deviceConfig, params, logicParams);
                    break;
                case "checkMesOutboundTask":
                    // 出片任务也使用同一套协议,通过handleCheckMesTask处理
                    result = handleCheckMesTask(deviceConfig, params, logicParams);
                    break;
                case "startTaskMonitor":
                    result = handleStartTaskMonitor(deviceConfig, params, logicParams);
                    break;
                case "stopTaskMonitor":
                    result = handleStopTaskMonitor(deviceConfig);
                    break;
                default:
                    log.warn("不支持的操作类型: {}", operation);
                    result = DevicePlcVO.OperationResult.builder()
                            .success(false)
                            .message("不支持的操作: " + operation)
                            .build();
            }
            return result;
        } catch (Exception e) {
            log.error("执行大车设备操作异常: deviceId={}, operation={}", deviceId, operation, e);
            // 发生异常时,将状态设置为错误
            if (needsStateManagement(operation)) {
                statusManager.updateVehicleStatus(deviceId, VehicleState.ERROR);
            }
            throw e;
        } finally {
            // 4. 执行完成后恢复为空闲状态(对于需要状态管理的操作)
            if (needsStateManagement(operation)) {
                // 注意:这里不立即设置为IDLE,因为实际执行可能需要时间
                // 真正的状态更新应该在任务完成后通过回调或状态查询来更新
                // 这里先保持EXECUTING状态,等待外部确认完成后再更新
                log.debug("操作执行完成,保持执行中状态,等待外部确认: deviceId={}", deviceId);
            }
        }
    }
    /**
     * 判断操作是否需要状态检查
     */
    private boolean needsStateCheck(String operation) {
        // 所有操作都需要检查状态,除了查询类操作
        return !"query".equals(operation) && !"status".equals(operation);
    }
    /**
     * 判断操作是否需要状态管理
     */
    private boolean needsStateManagement(String operation) {
        // feedGlass 需要状态管理,其他操作根据实际情况
        return "feedGlass".equals(operation);
    }
    /**
     * 创建车辆任务信息
     */
    private VehicleTask createVehicleTask(
            DeviceConfig deviceConfig,
            String operation,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        VehicleTask task = new VehicleTask();
        task.setTaskId(generateTaskId(deviceConfig.getDeviceId()));
        task.setTaskName("大车设备-" + operation);
        task.setOperation(operation);
        // 从参数中提取位置信息
        String positionCode = (String) params.get("positionCode");
        Integer positionValue = (Integer) params.get("positionValue");
        if (positionCode != null || positionValue != null) {
            VehiclePosition position = new VehiclePosition(positionCode, positionValue);
            task.getPlannedPath().setStartPosition(position);
            task.getPlannedPath().setEndPosition(position);
        }
        // 从配置中获取速度(如果有)
        Double speed = getLogicParam(logicParams, "vehicleSpeed", null);
        if (speed != null) {
            task.setSpeed(speed);
            task.calculateEstimatedEndTime();
        }
        task.setParameters(new HashMap<>(params));
        return task;
    }
    /**
     * 生成任务ID
     */
    private String generateTaskId(String deviceId) {
        return "TASK_" + deviceId + "_" + System.currentTimeMillis();
    }
    /**
     * 处理玻璃上料操作
     */
    private DevicePlcVO.OperationResult handleFeedGlass(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        // 从逻辑参数中获取配置(从 extraParams.deviceLogic 读取)
        Integer vehicleCapacity = getLogicParam(logicParams, "vehicleCapacity", 6000);
        Integer glassIntervalMs = getLogicParam(logicParams, "glassIntervalMs", 1000);
        Boolean autoFeed = getLogicParam(logicParams, "autoFeed", true);
        Integer maxRetryCount = getLogicParam(logicParams, "maxRetryCount", 5);
        // 从运行时参数中获取数据(从接口调用时传入)
        List<GlassInfo> glassInfos = extractGlassInfos(params);
        if (glassInfos.isEmpty()) {
            return DevicePlcVO.OperationResult.builder()
                    .success(false)
                    .message("未提供有效的玻璃信息")
                    .build();
        }
        String positionCode = (String) params.get("positionCode");
        Integer positionValue = (Integer) params.get("positionValue");
        Boolean triggerRequest = (Boolean) params.getOrDefault("triggerRequest", autoFeed);
        List<GlassInfo> plannedGlasses = planGlassLoading(glassInfos, vehicleCapacity,
                getLogicParam(logicParams, "defaultGlassLength", 2000));
        if (plannedGlasses.isEmpty()) {
            return DevicePlcVO.OperationResult.builder()
                    .success(false)
                    .message("当前玻璃尺寸超出车辆容量,无法装载")
                    .build();
        }
        // 构建写入数据
        Map<String, Object> payload = new HashMap<>();
        // 写入玻璃ID
        int plcSlots = Math.min(plannedGlasses.size(), 6);
        for (int i = 0; i < plcSlots; i++) {
            String fieldName = "plcGlassId" + (i + 1);
            payload.put(fieldName, plannedGlasses.get(i).getGlassId());
        }
        payload.put("plcGlassCount", plcSlots);
        // 写入位置信息
        if (positionValue != null) {
            payload.put("inPosition", positionValue);
        } else if (positionCode != null) {
            // 从位置映射中获取位置值
            @SuppressWarnings("unchecked")
            Map<String, Integer> positionMapping = getLogicParam(logicParams, "positionMapping", new HashMap<>());
            Integer mappedValue = positionMapping.get(positionCode);
            if (mappedValue != null) {
                payload.put("inPosition", mappedValue);
            }
        }
        // 自动触发请求字
        if (triggerRequest != null && triggerRequest) {
            payload.put("plcRequest", 1);
        }
        String operationName = "大车设备-玻璃上料";
        if (positionCode != null) {
            operationName += "(" + positionCode + ")";
        }
        log.info("大车设备玻璃上料: deviceId={}, glassCount={}, position={}, plannedGlassIds={}",
                deviceConfig.getId(), plcSlots, positionCode, plannedGlasses);
        if (glassIntervalMs != null && glassIntervalMs > 0) {
            try {
                Thread.sleep(glassIntervalMs);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields(
            deviceConfig.getId(), payload, operationName);
        // 如果执行成功,更新位置信息到状态,并启动状态监控
        if (Boolean.TRUE.equals(result.getSuccess())) {
            VehicleStatus status = statusManager.getOrCreateVehicleStatus(
                deviceConfig.getDeviceId(), deviceConfig.getDeviceName());
            if (positionCode != null || positionValue != null) {
                VehiclePosition position = new VehiclePosition(positionCode, positionValue);
                status.setCurrentPosition(position);
            }
            // 启动自动状态监控,当 state=1 时自动协调卧转立设备
            startStateMonitoring(deviceConfig, logicParams);
        }
        return result;
    }
    /**
     * 处理触发请求操作
     */
    private DevicePlcVO.OperationResult handleTriggerRequest(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        Map<String, Object> payload = new HashMap<>();
        payload.put("plcRequest", 1);
        log.info("大车设备触发请求: deviceId={}", deviceConfig.getId());
        return devicePlcOperationService.writeFields(
                deviceConfig.getId(),
                payload,
                "大车设备-触发请求"
        );
    }
    /**
     * 处理触发汇报操作
     */
    private DevicePlcVO.OperationResult handleTriggerReport(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        Map<String, Object> payload = new HashMap<>();
        payload.put("plcReport", 1);
        log.info("大车设备触发汇报: deviceId={}", deviceConfig.getId());
        return devicePlcOperationService.writeFields(
                deviceConfig.getId(),
                payload,
                "大车设备-触发汇报"
        );
    }
    /**
     * 处理重置操作
     */
    private DevicePlcVO.OperationResult handleReset(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        Map<String, Object> payload = new HashMap<>();
        payload.put("plcRequest", 0);
        payload.put("plcReport", 0);
        log.info("大车设备重置: deviceId={}", deviceConfig.getId());
        DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields(
                deviceConfig.getId(),
                payload,
                "大车设备-重置"
        );
        // 重置时,清除任务并恢复为空闲状态,停止监控
        if (Boolean.TRUE.equals(result.getSuccess())) {
            statusManager.clearVehicleTask(deviceConfig.getDeviceId());
            statusManager.updateVehicleStatus(deviceConfig.getDeviceId(), VehicleState.IDLE);
            stopStateMonitoring(deviceConfig.getDeviceId());
        }
        return result;
    }
    /**
     * 清空PLC中的玻璃数据
     */
    private DevicePlcVO.OperationResult handleClearGlass(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        Map<String, Object> payload = new HashMap<>();
        int slotCount = getLogicParam(logicParams, "glassSlotCount", 6);
        if (slotCount <= 0) {
            slotCount = 6;
        }
        List<String> slotFields = resolveGlassSlotFields(logicParams, slotCount);
        for (String field : slotFields) {
            payload.put(field, "");
        }
        payload.put("plcGlassCount", 0);
        payload.put("plcRequest", 0);
        payload.put("plcReport", 0);
        if (params != null && params.containsKey("positionValue")) {
            payload.put("inPosition", params.get("positionValue"));
        } else if (params != null && Boolean.TRUE.equals(params.get("clearPosition"))) {
            payload.put("inPosition", 0);
        }
        log.info("清空大车设备PLC玻璃数据: deviceId={}, clearedSlots={}", deviceConfig.getId(), slotFields.size());
        DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields(
                deviceConfig.getId(),
                payload,
                "大车设备-清空玻璃数据"
        );
        // 清空后,恢复为空闲状态,停止监控
        if (Boolean.TRUE.equals(result.getSuccess())) {
            statusManager.clearVehicleTask(deviceConfig.getDeviceId());
            statusManager.updateVehicleStatus(deviceConfig.getDeviceId(), VehicleState.IDLE);
            stopStateMonitoring(deviceConfig.getDeviceId());
        }
        return result;
    }
    private List<String> resolveGlassSlotFields(Map<String, Object> logicParams, int fallbackCount) {
        List<String> fields = new ArrayList<>();
        if (logicParams != null) {
            Object slotFieldConfig = logicParams.get("glassSlotFields");
            if (slotFieldConfig instanceof List) {
                List<?> configured = (List<?>) slotFieldConfig;
                for (Object item : configured) {
                    if (item != null) {
                        String fieldName = String.valueOf(item).trim();
                        if (!fieldName.isEmpty()) {
                            fields.add(fieldName);
                        }
                    }
                }
            }
        }
        if (fields.isEmpty()) {
            for (int i = 1; i <= fallbackCount; i++) {
                fields.add("plcGlassId" + i);
            }
        }
        return fields;
    }
    @Override
    public String validateLogicParams(DeviceConfig deviceConfig) {
        Map<String, Object> logicParams = parseLogicParams(deviceConfig);
        // 验证必填参数
        Integer vehicleCapacity = getLogicParam(logicParams, "vehicleCapacity", null);
        if (vehicleCapacity == null || vehicleCapacity <= 0) {
            return "车辆容量(vehicleCapacity)必须大于0";
        }
        Integer glassIntervalMs = getLogicParam(logicParams, "glassIntervalMs", null);
        if (glassIntervalMs != null && glassIntervalMs < 0) {
            return "玻璃间隔时间(glassIntervalMs)不能为负数";
        }
        return null; // 验证通过
    }
    @Override
    public String getDefaultLogicParams() {
        Map<String, Object> defaultParams = new HashMap<>();
        defaultParams.put("vehicleCapacity", 6000);
        defaultParams.put("glassIntervalMs", 1000);
        defaultParams.put("autoFeed", true);
        defaultParams.put("maxRetryCount", 5);
        defaultParams.put("defaultGlassLength", 2000);
        // MES任务相关配置
        defaultParams.put("vehicleSpeed", 1.0); // 车辆速度(格/秒,grid/s),默认1格/秒
        defaultParams.put("minRange", 1); // 最小运动距离(格子)
        defaultParams.put("maxRange", 100); // 最大运动距离(格子),例如100格
        defaultParams.put("homePosition", 0); // 初始位置(格子)
        defaultParams.put("idleMonitorIntervalMs", 2000); // 空闲监控间隔(毫秒)
        defaultParams.put("taskMonitorIntervalMs", 1000); // 任务监控间隔(毫秒)
        defaultParams.put("mesConfirmTimeoutMs", 30000); // MES确认超时(毫秒)
        // 出片任务相关配置
        // outboundSlotRanges: 出片任务的startSlot范围,例如[1, 101]表示格子1~101都是出片任务
        // 如果不配置,则通过判断startSlot是否在positionMapping中来区分进片/出片
        List<Integer> outboundSlotRanges = new ArrayList<>();
        outboundSlotRanges.add(1);   // 最小格子编号
        outboundSlotRanges.add(101); // 最大格子编号
        defaultParams.put("outboundSlotRanges", outboundSlotRanges);
        // gridPositionMapping: 格子编号到位置的映射表(可选)
        // 如果不配置,则格子编号直接作为位置值
        Map<String, Integer> gridPositionMapping = new HashMap<>();
        defaultParams.put("gridPositionMapping", gridPositionMapping);
        Map<String, Integer> positionMapping = new HashMap<>();
        positionMapping.put("POS1", 1);
        positionMapping.put("POS2", 2);
        defaultParams.put("positionMapping", positionMapping);
        try {
            return objectMapper.writeValueAsString(defaultParams);
        } catch (Exception e) {
            log.error("生成默认逻辑参数失败", e);
            return "{}";
        }
    }
    @SuppressWarnings("unchecked")
    private List<GlassInfo> extractGlassInfos(Map<String, Object> params) {
        List<GlassInfo> result = new ArrayList<>();
        Object rawGlassInfos = params.get("glassInfos");
        if (rawGlassInfos instanceof List) {
            List<?> list = (List<?>) rawGlassInfos;
            for (Object item : list) {
                GlassInfo info = convertToGlassInfo(item);
                if (info != null) {
                    result.add(info);
                }
            }
        }
        if (result.isEmpty()) {
            List<String> glassIds = (List<String>) params.get("glassIds");
            if (glassIds != null && !glassIds.isEmpty()) {
                // 从数据库查询玻璃尺寸
                Map<String, Integer> lengthMap = glassInfoService.getGlassLengthMap(glassIds);
                for (String glassId : glassIds) {
                    Integer length = lengthMap.get(glassId);
                    result.add(new GlassInfo(glassId, length));
                }
                log.debug("从数据库查询玻璃尺寸: glassIds={}, lengthMap={}", glassIds, lengthMap);
            }
        }
        return result;
    }
    private GlassInfo convertToGlassInfo(Object source) {
        if (source instanceof GlassInfo) {
            return (GlassInfo) source;
        }
        if (source instanceof Map) {
            Map<?, ?> map = (Map<?, ?>) source;
            Object id = map.get("glassId");
            if (id == null) {
                id = map.get("id");
            }
            if (id == null) {
                return null;
            }
            Integer length = parseLength(map.get("length"));
            if (length == null) {
                length = parseLength(map.get("size"));
            }
            return new GlassInfo(String.valueOf(id), length);
        }
        if (source instanceof String) {
            return new GlassInfo((String) source, null);
        }
        return null;
    }
    private Integer parseLength(Object value) {
        if (value instanceof Number) {
            return ((Number) value).intValue();
        }
        if (value instanceof String) {
            try {
                return Integer.parseInt((String) value);
            } catch (NumberFormatException ignored) {
            }
        }
        return null;
    }
    private List<GlassInfo> planGlassLoading(List<GlassInfo> source,
                                             int vehicleCapacity,
                                             Integer defaultGlassLength) {
        List<GlassInfo> planned = new ArrayList<>();
        int usedLength = 0;
        int capacity = Math.max(vehicleCapacity, 1);
        int fallbackLength = defaultGlassLength != null && defaultGlassLength > 0 ? defaultGlassLength : 2000;
        for (GlassInfo info : source) {
            int length = info.getLength() != null && info.getLength() > 0 ? info.getLength() : fallbackLength;
            if (planned.isEmpty()) {
                planned.add(info.withLength(length));
                usedLength = length;
                continue;
            }
            if (usedLength + length <= capacity) {
                planned.add(info.withLength(length));
                usedLength += length;
            } else {
                break;
            }
        }
        return planned;
    }
    /**
     * 启动状态监控
     * 定期检查大车的 state1~6,当检测到 state=1 时自动协调卧转立设备
     */
    private void startStateMonitoring(DeviceConfig deviceConfig, Map<String, Object> logicParams) {
        String deviceId = deviceConfig.getDeviceId();
        // 如果已经在监控,先停止旧的监控任务
        stopStateMonitoring(deviceId);
        // 获取监控配置
        Integer monitorIntervalMs = getLogicParam(logicParams, "stateMonitorIntervalMs", 1000);
        Integer monitorTimeoutMs = getLogicParam(logicParams, "stateMonitorTimeoutMs", 300000);
        if (monitorIntervalMs == null || monitorIntervalMs <= 0) {
            monitorIntervalMs = 1000;
        }
        if (monitorTimeoutMs == null || monitorTimeoutMs <= 0) {
            monitorTimeoutMs = 300000;
        }
        final int finalMonitorIntervalMs = monitorIntervalMs;
        final int finalMonitorTimeoutMs = monitorTimeoutMs;
        // 初始化已协调状态记录
        coordinatedStates.put(deviceId, new CopyOnWriteArrayList<>());
        // 记录监控开始时间
        final long startTime = System.currentTimeMillis();
        // 启动监控任务
        ScheduledFuture<?> future = stateMonitorExecutor.scheduleWithFixedDelay(() -> {
            try {
                // 检查超时
                if (System.currentTimeMillis() - startTime > finalMonitorTimeoutMs) {
                    log.info("大车状态监控超时,停止监控: deviceId={}, timeout={}ms", deviceId, finalMonitorTimeoutMs);
                    stopStateMonitoring(deviceId);
                    return;
                }
                // 检查车辆是否还在执行任务
                VehicleStatus status = statusManager.getVehicleStatus(deviceId);
                if (status == null || status.getState() != VehicleState.EXECUTING) {
                    log.debug("大车状态已改变,停止监控: deviceId={}, state={}",
                            deviceId, status != null ? status.getState() : "null");
                    stopStateMonitoring(deviceId);
                    return;
                }
                // 执行状态检查和协调
                checkAndCoordinateState(deviceConfig);
            } catch (Exception e) {
                log.error("大车状态监控异常: deviceId={}", deviceId, e);
            }
        }, finalMonitorIntervalMs, finalMonitorIntervalMs, TimeUnit.MILLISECONDS);
        monitoringTasks.put(deviceId, future);
        log.info("已启动大车状态监控: deviceId={}, interval={}ms, timeout={}ms",
                deviceId, finalMonitorIntervalMs, finalMonitorTimeoutMs);
    }
    /**
     * 停止状态监控
     */
    private void stopStateMonitoring(String deviceId) {
        ScheduledFuture<?> future = monitoringTasks.remove(deviceId);
        if (future != null && !future.isCancelled()) {
            future.cancel(false);
            log.debug("已停止大车状态监控: deviceId={}", deviceId);
        }
        coordinatedStates.remove(deviceId);
    }
    /**
     * 检查大车状态并协调卧转立设备(内部方法,由监控线程调用)
     */
    private void checkAndCoordinateState(DeviceConfig deviceConfig) {
        String deviceId = deviceConfig.getDeviceId();
        List<String> alreadyCoordinated = coordinatedStates.get(deviceId);
        if (alreadyCoordinated == null) {
            alreadyCoordinated = new CopyOnWriteArrayList<>();
            coordinatedStates.put(deviceId, alreadyCoordinated);
        }
        try {
            // 读取 state1~6 字段
            List<String> stateFields = Arrays.asList("state1", "state2", "state3", "state4", "state5", "state6");
            Map<String, Object> stateValues = new HashMap<>();
            // 从 PLC 读取状态字段
            DevicePlcVO.StatusInfo statusInfo = devicePlcOperationService.readStatus(deviceConfig.getId());
            if (statusInfo == null || statusInfo.getFieldValues() == null) {
                return;
            }
            for (String field : stateFields) {
                Object value = statusInfo.getFieldValues().get(field);
                if (value != null) {
                    stateValues.put(field, value);
                }
            }
            // 检查是否有任何一个 state 为 1,且尚未协调过
            List<String> newStateOneFields = new ArrayList<>();
            for (String field : stateFields) {
                Integer stateValue = parseInteger(stateValues.get(field));
                if (stateValue != null && stateValue == 1 && !alreadyCoordinated.contains(field)) {
                    newStateOneFields.add(field);
                }
            }
            if (newStateOneFields.isEmpty()) {
                return; // 没有新的 state=1,无需协调
            }
            log.info("检测到大车新的 state=1: deviceId={}, stateFields={}", deviceId, newStateOneFields);
            // 查找同组的卧转立设备
            List<DeviceConfig> transferDevices = findTransferDevicesInSameGroup(deviceConfig);
            if (transferDevices.isEmpty()) {
                log.warn("未找到同组的卧转立设备: deviceId={}", deviceId);
                // 即使找不到设备,也标记为已协调,避免重复检查
                alreadyCoordinated.addAll(newStateOneFields);
                return;
            }
            // 将每个卧转立设备的 plcRequest 置 0
            boolean allSuccess = true;
            for (DeviceConfig transferDevice : transferDevices) {
                Map<String, Object> payload = new HashMap<>();
                payload.put("plcRequest", 0);
                DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields(
                        transferDevice.getId(),
                        payload,
                        "大车自动协调-清空卧转立请求"
                );
                if (Boolean.TRUE.equals(result.getSuccess())) {
                    log.info("已自动清空卧转立设备 plcRequest: vehicleDeviceId={}, transferDeviceId={}, transferDeviceName={}, stateFields={}",
                            deviceId, transferDevice.getId(), transferDevice.getDeviceName(), newStateOneFields);
                } else {
                    log.warn("自动清空卧转立设备 plcRequest 失败: vehicleDeviceId={}, transferDeviceId={}, message={}",
                            deviceId, transferDevice.getId(), result.getMessage());
                    allSuccess = false;
                }
            }
            // 标记为已协调(无论成功与否,避免重复协调)
            if (allSuccess) {
                alreadyCoordinated.addAll(newStateOneFields);
                log.info("大车状态协调完成: deviceId={}, coordinatedStateFields={}", deviceId, newStateOneFields);
            }
        } catch (Exception e) {
            log.error("检查大车状态并协调卧转立设备异常: deviceId={}", deviceId, e);
        }
    }
    /**
     * 检查大车状态并协调卧转立设备(手动调用接口)
     * 当 state1~6 中任何一个变为 1(上车完成)时,将同组卧转立设备的 plcRequest 置 0
     */
    private DevicePlcVO.OperationResult handleCheckStateAndCoordinate(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        try {
            // 读取 state1~6 字段
            List<String> stateFields = Arrays.asList("state1", "state2", "state3", "state4", "state5", "state6");
            Map<String, Object> stateValues = new HashMap<>();
            // 从 PLC 读取状态字段
            DevicePlcVO.StatusInfo statusInfo = devicePlcOperationService.readStatus(deviceConfig.getId());
            if (statusInfo != null && statusInfo.getFieldValues() != null) {
                for (String field : stateFields) {
                    Object value = statusInfo.getFieldValues().get(field);
                    if (value != null) {
                        stateValues.put(field, value);
                    }
                }
            }
            // 检查是否有任何一个 state 为 1
            boolean hasStateOne = false;
            List<String> stateOneFields = new ArrayList<>();
            for (String field : stateFields) {
                Integer stateValue = parseInteger(stateValues.get(field));
                if (stateValue != null && stateValue == 1) {
                    hasStateOne = true;
                    stateOneFields.add(field);
                }
            }
            if (!hasStateOne) {
                return DevicePlcVO.OperationResult.builder()
                        .success(true)
                        .message("当前无 state=1 的状态,无需协调")
                        .build();
            }
            log.info("检测到大车 state=1: deviceId={}, stateFields={}",
                    deviceConfig.getId(), stateOneFields);
            // 查找同组的卧转立设备
            List<DeviceConfig> transferDevices = findTransferDevicesInSameGroup(deviceConfig);
            if (transferDevices.isEmpty()) {
                log.warn("未找到同组的卧转立设备: deviceId={}", deviceConfig.getId());
                return DevicePlcVO.OperationResult.builder()
                        .success(true)
                        .message("未找到同组的卧转立设备,跳过协调")
                        .build();
            }
            // 将每个卧转立设备的 plcRequest 置 0
            List<DevicePlcVO.OperationResult> results = new ArrayList<>();
            for (DeviceConfig transferDevice : transferDevices) {
                Map<String, Object> payload = new HashMap<>();
                payload.put("plcRequest", 0);
                DevicePlcVO.OperationResult result = devicePlcOperationService.writeFields(
                        transferDevice.getId(),
                        payload,
                        "大车协调-清空卧转立请求"
                );
                results.add(result);
                if (Boolean.TRUE.equals(result.getSuccess())) {
                    log.info("已清空卧转立设备 plcRequest: transferDeviceId={}, transferDeviceName={}",
                            transferDevice.getId(), transferDevice.getDeviceName());
                } else {
                    log.warn("清空卧转立设备 plcRequest 失败: transferDeviceId={}, message={}",
                            transferDevice.getId(), result.getMessage());
                }
            }
            boolean allSuccess = results.stream()
                    .allMatch(r -> Boolean.TRUE.equals(r.getSuccess()));
            return DevicePlcVO.OperationResult.builder()
                    .success(allSuccess)
                    .message(String.format("已协调 %d 个卧转立设备,成功: %d",
                            transferDevices.size(),
                            results.stream().mapToInt(r -> Boolean.TRUE.equals(r.getSuccess()) ? 1 : 0).sum()))
                    .build();
        } catch (Exception e) {
            log.error("检查大车状态并协调卧转立设备异常: deviceId={}", deviceConfig.getId(), e);
            return DevicePlcVO.OperationResult.builder()
                    .success(false)
                    .message("协调异常: " + e.getMessage())
                    .build();
        }
    }
    /**
     * 查找同组内的卧转立设备
     */
    private List<DeviceConfig> findTransferDevicesInSameGroup(DeviceConfig vehicleDevice) {
        List<DeviceConfig> transferDevices = new ArrayList<>();
        if (deviceGroupRelationService == null || deviceConfigService == null) {
            log.warn("DeviceGroupRelationService 或 DeviceConfigService 未注入,无法查找同组设备");
            return transferDevices;
        }
        try {
            // 获取大车所属的设备组
            List<DeviceGroupVO.GroupInfo> groups = deviceGroupRelationService.getDeviceGroups(vehicleDevice.getId());
            if (groups.isEmpty()) {
                log.debug("大车设备未加入任何设备组: deviceId={}", vehicleDevice.getId());
                return transferDevices;
            }
            // 遍历所有设备组,查找卧转立设备
            for (DeviceGroupVO.GroupInfo group : groups) {
                List<DeviceGroupVO.DeviceInfo> groupDevices = deviceGroupRelationService.getGroupDevices(group.getId());
                for (DeviceGroupVO.DeviceInfo deviceInfo : groupDevices) {
                    // 检查是否为卧转立设备
                    if (DeviceConfig.DeviceType.WORKSTATION_TRANSFER.equals(deviceInfo.getDeviceType())) {
                        DeviceConfig transferDevice = deviceConfigService.getDeviceById(deviceInfo.getId());
                        if (transferDevice != null) {
                            transferDevices.add(transferDevice);
                        }
                    }
                }
            }
            log.debug("找到同组卧转立设备: vehicleDeviceId={}, transferDeviceCount={}",
                    vehicleDevice.getId(), transferDevices.size());
        } catch (Exception e) {
            log.error("查找同组卧转立设备异常: vehicleDeviceId={}", vehicleDevice.getId(), e);
        }
        return transferDevices;
    }
    private Integer parseInteger(Object value) {
        if (value instanceof Number) {
            return ((Number) value).intValue();
        }
        if (value == null) {
            return null;
        }
        try {
            return Integer.parseInt(String.valueOf(value));
        } catch (NumberFormatException e) {
            return null;
        }
    }
    private String parseString(Object value) {
        return value == null ? null : String.valueOf(value).trim();
    }
    private static class GlassInfo {
        private final String glassId;
        private final Integer length;
        GlassInfo(String glassId, Integer length) {
            this.glassId = glassId;
            this.length = length;
        }
        public String getGlassId() {
            return glassId;
        }
        public Integer getLength() {
            return length;
        }
        public GlassInfo withLength(Integer newLength) {
            return new GlassInfo(this.glassId, newLength);
        }
        @Override
        public String toString() {
            return glassId;
        }
        @Override
        public int hashCode() {
            return Objects.hash(glassId, length);
        }
        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null || getClass() != obj.getClass()) return false;
            GlassInfo other = (GlassInfo) obj;
            return Objects.equals(glassId, other.glassId) && Objects.equals(length, other.length);
        }
    }
    /**
     * 启动空闲监控(没有任务时,plcRequest一直保持为1)
     */
    private DevicePlcVO.OperationResult handleStartIdleMonitor(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        String deviceId = deviceConfig.getDeviceId();
        // 停止旧的监控任务
        handleStopIdleMonitor(deviceConfig);
        // 获取监控间隔
        Integer monitorIntervalMs = getLogicParam(logicParams, "idleMonitorIntervalMs", 2000); // 默认2秒
        // 启动监控任务
        ScheduledFuture<?> future = idleMonitorExecutor.scheduleWithFixedDelay(() -> {
            try {
                // 检查是否有任务在执行
                VehicleStatus status = statusManager.getVehicleStatus(deviceId);
                if (status != null && status.getState() == VehicleState.EXECUTING) {
                    // 有任务在执行,不设置plcRequest
                    return;
                }
                // 检查是否有待处理的进片或出片任务
                if (plcDynamicDataService != null && s7SerializerProvider != null) {
                    EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig);
                    if (serializer != null) {
                        // 检查进片任务
                        Map<String, Object> mesData = plcDynamicDataService.readPlcData(
                                deviceConfig, MES_FIELDS, serializer);
                        Integer mesSend = parseInteger(mesData != null ? mesData.get("mesSend") : null);
                        // 进片和出片共用mesSend字段,只需检查一次
                        // 如果有待处理的任务,不设置plcRequest(等待任务处理)
                        if (mesSend != null && mesSend == 1) {
                            log.debug("大车空闲监控: deviceId={}, 检测到待处理任务(mesSend=1),不设置plcRequest", deviceId);
                            return;
                        }
                    }
                }
                // 没有任务,保持plcRequest=1
                Map<String, Object> payload = new HashMap<>();
                payload.put("plcRequest", 1);
                devicePlcOperationService.writeFields(deviceConfig.getId(), payload, "大车空闲监控-保持请求");
                log.debug("大车空闲监控: deviceId={}, 保持plcRequest=1", deviceId);
            } catch (Exception e) {
                log.error("大车空闲监控异常: deviceId={}", deviceId, e);
            }
        }, monitorIntervalMs, monitorIntervalMs, TimeUnit.MILLISECONDS);
        idleMonitoringTasks.put(deviceId, future);
        log.info("已启动大车空闲监控: deviceId={}, interval={}ms", deviceId, monitorIntervalMs);
        return DevicePlcVO.OperationResult.builder()
                .success(true)
                .message("空闲监控已启动")
                .build();
    }
    /**
     * 停止空闲监控
     */
    private DevicePlcVO.OperationResult handleStopIdleMonitor(DeviceConfig deviceConfig) {
        String deviceId = deviceConfig.getDeviceId();
        ScheduledFuture<?> future = idleMonitoringTasks.remove(deviceId);
        if (future != null && !future.isCancelled()) {
            future.cancel(false);
            log.info("已停止大车空闲监控: deviceId={}", deviceId);
        }
        return DevicePlcVO.OperationResult.builder()
                .success(true)
                .message("空闲监控已停止")
                .build();
    }
    /**
     * 检查MES任务(当mesSend=1时,读取MES参数并创建任务)
     * 进片和出片共用同一套协议字段,通过位置信息判断任务类型
     */
    private DevicePlcVO.OperationResult handleCheckMesTask(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        if (plcDynamicDataService == null || s7SerializerProvider == null) {
            return DevicePlcVO.OperationResult.builder()
                    .success(false)
                    .message("PlcDynamicDataService或S7SerializerProvider未注入")
                    .build();
        }
        String deviceId = deviceConfig.getDeviceId();
        EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig);
        if (serializer == null) {
            return DevicePlcVO.OperationResult.builder()
                    .success(false)
                    .message("获取PLC序列化器失败")
                    .build();
        }
        try {
            // 读取MES字段(进片和出片共用)
            Map<String, Object> mesData = plcDynamicDataService.readPlcData(
                    deviceConfig, MES_FIELDS, serializer);
            if (mesData == null || mesData.isEmpty()) {
                return DevicePlcVO.OperationResult.builder()
                        .success(false)
                        .message("读取MES字段失败")
                        .build();
            }
            Integer mesSend = parseInteger(mesData.get("mesSend"));
            if (mesSend == null || mesSend == 0) {
                return DevicePlcVO.OperationResult.builder()
                        .success(true)
                        .message("暂无MES任务(mesSend=0)")
                        .build();
            }
            // mesSend=1,读取任务参数
            String glassId = parseString(mesData.get("mesGlassId"));
            Integer startSlot = parseInteger(mesData.get("startSlot")); // 起始位置编号
            Integer targetSlot = parseInteger(mesData.get("targetSlot")); // 目标位置编号
            Integer workLine = parseInteger(mesData.get("workLine"));
            if (glassId == null || glassId.isEmpty()) {
                return DevicePlcVO.OperationResult.builder()
                        .success(false)
                        .message("MES未提供玻璃ID")
                        .build();
            }
            // 判断是进片还是出片任务
            // 方法:通过startSlot判断
            // - 如果startSlot是卧转立编号(如900/901),则是进片任务
            // - 如果startSlot是格子编号(在大理片笼范围内),则是出片任务
            boolean isOutbound = isOutboundTask(startSlot, logicParams);
            // 位置映射
            Integer startPosition;
            if (isOutbound) {
                // 出片任务:startSlot是格子编号,需要映射到实际位置
                startPosition = mapOutboundPosition(startSlot, logicParams);
            } else {
                // 进片任务:startSlot是卧转立编号,通过positionMapping映射
                startPosition = mapPosition(startSlot, logicParams);
            }
            // targetSlot统一通过positionMapping映射
            Integer targetPosition = mapPosition(targetSlot, logicParams);
            if (startPosition == null || targetPosition == null) {
                return DevicePlcVO.OperationResult.builder()
                        .success(false)
                        .message(String.format("位置映射失败: startSlot=%s, targetSlot=%s, isOutbound=%s",
                                startSlot, targetSlot, isOutbound))
                        .build();
            }
            // 读取当前位置
            Integer currentPosition = getCurrentPosition(deviceConfig, logicParams);
            // 计算时间
            TimeCalculation timeCalc = calculateTime(
                    currentPosition, startPosition, targetPosition, logicParams);
            // 创建任务信息
            MesTaskInfo taskInfo = new MesTaskInfo();
            taskInfo.glassId = glassId;
            taskInfo.startSlot = startSlot;
            taskInfo.targetSlot = targetSlot;
            taskInfo.startPosition = startPosition;
            taskInfo.targetPosition = targetPosition;
            taskInfo.currentPosition = currentPosition;
            taskInfo.gotime = timeCalc.gotime;
            taskInfo.cartime = timeCalc.cartime;
            taskInfo.workLine = workLine;
            taskInfo.createdTime = System.currentTimeMillis();
            taskInfo.isOutbound = isOutbound;
            currentTasks.put(deviceId, taskInfo);
            // 清空plcRequest(表示已接收任务)
            Map<String, Object> payload = new HashMap<>();
            payload.put("plcRequest", 0);
            plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
            // 更新车辆状态为执行中
            statusManager.updateVehicleStatus(deviceId, deviceConfig.getDeviceName(), VehicleState.EXECUTING);
            // 启动任务监控
            handleStartTaskMonitor(deviceConfig, params, logicParams);
            String taskType = isOutbound ? "出片" : "进片";
            log.info("MES{}任务已创建: deviceId={}, glassId={}, startSlot={}(位置{}格), targetSlot={}(位置{}格), 距离{}格->{}格, gotime={}ms({}秒), cartime={}ms({}秒)",
                    taskType, deviceId, glassId, startSlot, startPosition, targetSlot, targetPosition,
                    Math.abs(startPosition - currentPosition), Math.abs(targetPosition - startPosition),
                    timeCalc.gotime, timeCalc.gotime / 1000.0, timeCalc.cartime, timeCalc.cartime / 1000.0);
            return DevicePlcVO.OperationResult.builder()
                    .success(true)
                    .message(String.format("MES%s任务已创建: glassId=%s, start=%d, target=%d",
                            taskType, glassId, startPosition, targetPosition))
                    .build();
        } catch (Exception e) {
            log.error("检查MES任务异常: deviceId={}", deviceId, e);
            return DevicePlcVO.OperationResult.builder()
                    .success(false)
                    .message("处理异常: " + e.getMessage())
                    .build();
        }
    }
    /**
     * 判断是否为出片任务
     * 通过startSlot判断:
     * - 如果startSlot在positionMapping中,且不在大理片笼格子范围内,则是进片任务
     * - 如果startSlot不在positionMapping中,或在大理片笼格子范围内,则是出片任务
     */
    private boolean isOutboundTask(Integer startSlot, Map<String, Object> logicParams) {
        if (startSlot == null) {
            return false;
        }
        // 方法1:检查startSlot是否在positionMapping中(卧转立编号)
        @SuppressWarnings("unchecked")
        Map<String, Integer> positionMapping = getLogicParam(logicParams, "positionMapping", new HashMap<>());
        if (positionMapping.containsKey(String.valueOf(startSlot))) {
            // startSlot在positionMapping中,说明是卧转立编号,是进片任务
            return false;
        }
        // 方法2:检查startSlot是否在大理片笼格子范围内
        // 通过查找同组的大理片笼设备,检查格子范围
        // 这里简化处理:如果startSlot不在positionMapping中,且是数字,可能是格子编号
        // 可以通过配置指定格子编号范围,或者通过查找同组设备判断
        // 方法3:通过配置指定出片任务的startSlot范围
        @SuppressWarnings("unchecked")
        List<Integer> outboundSlotRanges = getLogicParam(logicParams, "outboundSlotRanges", null);
        if (outboundSlotRanges != null && !outboundSlotRanges.isEmpty()) {
            // 如果配置了出片slot范围,检查startSlot是否在范围内
            // 例如:[1, 101] 表示格子1~101都是出片任务
            if (outboundSlotRanges.size() >= 2) {
                int minSlot = outboundSlotRanges.get(0);
                int maxSlot = outboundSlotRanges.get(1);
                if (startSlot >= minSlot && startSlot <= maxSlot) {
                    return true;
                }
            }
        }
        // 默认:如果startSlot不在positionMapping中,且是较小的数字(可能是格子编号),判断为出片
        // 这里可以根据实际需求调整判断逻辑
        // 暂时:如果startSlot不在positionMapping中,判断为出片任务
        return true;
    }
    /**
     * 映射出片源位置(格子编号转换为实际位置)
     * 通过查找同组的大理片笼设备配置,将格子编号转换为位置
     */
    private Integer mapOutboundPosition(Integer gridNumber, Map<String, Object> logicParams) {
        if (gridNumber == null) {
            return null;
        }
        // 方法1:如果配置了格子到位置的映射表
        @SuppressWarnings("unchecked")
        Map<String, Integer> gridPositionMapping = getLogicParam(logicParams, "gridPositionMapping", new HashMap<>());
        Integer position = gridPositionMapping.get(String.valueOf(gridNumber));
        if (position != null) {
            return position;
        }
        // 方法2:格子编号直接作为位置(如果格子编号就是位置值)
        // 例如:格子1对应位置1格,格子52对应位置52格
        // 这里可以根据实际需求调整映射逻辑
        // 暂时直接使用格子编号作为位置
        log.debug("使用格子编号作为位置: gridNumber={}", gridNumber);
        return gridNumber;
    }
    /**
     * 位置映射:将MES给的编号(如900/901)转换为实际位置值(如100/500)
     */
    private Integer mapPosition(Integer slotNumber, Map<String, Object> logicParams) {
        if (slotNumber == null) {
            return null;
        }
        // 从配置中获取位置映射表
        @SuppressWarnings("unchecked")
        Map<String, Integer> positionMapping = getLogicParam(logicParams, "positionMapping", new HashMap<>());
        // 查找映射
        Integer position = positionMapping.get(String.valueOf(slotNumber));
        if (position != null) {
            return position;
        }
        // 如果没有配置映射,尝试直接使用编号
        log.warn("位置映射未找到: slotNumber={}, 使用编号作为位置值", slotNumber);
        return slotNumber;
    }
    /**
     * 获取当前位置
     */
    private Integer getCurrentPosition(DeviceConfig deviceConfig, Map<String, Object> logicParams) {
        // 从状态管理器获取
        VehicleStatus status = statusManager.getVehicleStatus(deviceConfig.getDeviceId());
        if (status != null && status.getCurrentPosition() != null) {
            return status.getCurrentPosition().getPositionValue();
        }
        // 从配置中获取默认位置
        return getLogicParam(logicParams, "homePosition", 0);
    }
    /**
     * 时间计算:根据速度、当前位置、目标位置计算gotime和cartime
     * 速度单位:格/秒(grid/s)
     * 位置和距离单位:格子(grid)
     */
    private TimeCalculation calculateTime(Integer currentPos, Integer startPos,
                                          Integer targetPos, Map<String, Object> logicParams) {
        // 获取速度(格/秒,grid/s)
        Double speed = getLogicParam(logicParams, "vehicleSpeed", 1.0);
        if (speed == null || speed <= 0) {
            speed = 1.0; // 默认1格/秒
        }
        // 获取运动距离范围(格子)
        Integer minRange = getLogicParam(logicParams, "minRange", 1);
        Integer maxRange = getLogicParam(logicParams, "maxRange", 100);
        // 计算gotime:从当前位置到起始位置的时间(毫秒)
        // 公式:时间(ms) = 距离(格) / 速度(格/秒) * 1000
        long gotime = 0;
        if (currentPos != null && startPos != null) {
            int distance = Math.abs(startPos - currentPos); // 距离(格子)
            // 限制在范围内
            distance = Math.max(minRange, Math.min(maxRange, distance));
            gotime = (long) (distance / speed * 1000); // 转换为毫秒
        }
        // 计算cartime:从起始位置到目标位置的时间(毫秒)
        // 公式:时间(ms) = 距离(格) / 速度(格/秒) * 1000
        long cartime = 0;
        if (startPos != null && targetPos != null) {
            int distance = Math.abs(targetPos - startPos); // 距离(格子)
            // 限制在范围内
            distance = Math.max(minRange, Math.min(maxRange, distance));
            cartime = (long) (distance / speed * 1000); // 转换为毫秒
        }
        return new TimeCalculation(gotime, cartime);
    }
    /**
     * 启动任务监控(监控state状态切换和任务完成)
     */
    private DevicePlcVO.OperationResult handleStartTaskMonitor(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        String deviceId = deviceConfig.getDeviceId();
        // 停止旧的监控任务
        handleStopTaskMonitor(deviceConfig);
        MesTaskInfo taskInfo = currentTasks.get(deviceId);
        if (taskInfo == null) {
            return DevicePlcVO.OperationResult.builder()
                    .success(false)
                    .message("没有正在执行的任务")
                    .build();
        }
        // 获取监控间隔
        Integer monitorIntervalMs = getLogicParam(logicParams, "taskMonitorIntervalMs", 1000); // 默认1秒
        // 启动监控任务
        ScheduledFuture<?> future = taskMonitorExecutor.scheduleWithFixedDelay(() -> {
            try {
                monitorTaskExecution(deviceConfig, taskInfo, logicParams);
            } catch (Exception e) {
                log.error("任务监控异常: deviceId={}", deviceId, e);
            }
        }, monitorIntervalMs, monitorIntervalMs, TimeUnit.MILLISECONDS);
        taskMonitoringTasks.put(deviceId, future);
        log.info("已启动任务监控: deviceId={}, interval={}ms", deviceId, monitorIntervalMs);
        return DevicePlcVO.OperationResult.builder()
                .success(true)
                .message("任务监控已启动")
                .build();
    }
    /**
     * 监控任务执行
     */
    private void monitorTaskExecution(DeviceConfig deviceConfig,
                                      MesTaskInfo taskInfo,
                                      Map<String, Object> logicParams) {
        if (plcDynamicDataService == null || s7SerializerProvider == null) {
            return;
        }
        String deviceId = deviceConfig.getDeviceId();
        EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig);
        if (serializer == null) {
            return;
        }
        try {
            // 读取state1~6
            List<String> stateFields = Arrays.asList("state1", "state2", "state3", "state4", "state5", "state6");
            Map<String, Object> stateValues = new HashMap<>();
            DevicePlcVO.StatusInfo statusInfo = devicePlcOperationService.readStatus(deviceConfig.getId());
            if (statusInfo != null && statusInfo.getFieldValues() != null) {
                for (String field : stateFields) {
                    Object value = statusInfo.getFieldValues().get(field);
                    if (value != null) {
                        stateValues.put(field, value);
                    }
                }
            }
            // 根据时间计算更新state状态
            long currentTime = System.currentTimeMillis();
            long elapsed = currentTime - taskInfo.createdTime;
            // 计算状态切换时间点
            long state1Time = taskInfo.gotime; // 到达起始位置,上车完成
            long state2Time = taskInfo.gotime + taskInfo.cartime; // 到达目标位置,运输完成
            // 更新state状态
            if (elapsed >= state1Time && elapsed < state2Time) {
                // state应该为1
                if (taskInfo.isOutbound) {
                    // 出片任务:到达源位置(大理片笼),取片完成
                    updateStateIfNeeded(deviceConfig, serializer, stateValues, 1, taskInfo);
                } else {
                    // 进片任务:到达起始位置(卧转立),上车完成
                    updateStateIfNeeded(deviceConfig, serializer, stateValues, 1, taskInfo);
                }
            } else if (elapsed >= state2Time) {
                // state应该为2(运输完成)
                updateStateIfNeeded(deviceConfig, serializer, stateValues, 2, taskInfo);
                // 检查是否所有state都>=2,如果是则给MES汇报
                if (allStatesCompleted(stateValues)) {
                    reportToMes(deviceConfig, serializer, taskInfo, logicParams);
                }
            }
        } catch (Exception e) {
            log.error("监控任务执行异常: deviceId={}", deviceId, e);
        }
    }
    /**
     * 更新state状态(如果需要)
     */
    private void updateStateIfNeeded(DeviceConfig deviceConfig,
                                     EnhancedS7Serializer serializer,
                                     Map<String, Object> currentStates,
                                     int targetState,
                                     MesTaskInfo taskInfo) {
        // 这里可以根据实际需求更新state字段
        // 暂时只记录日志,实际更新可能需要根据具体PLC字段配置
        log.debug("任务状态更新: deviceId={}, targetState={}",
                deviceConfig.getDeviceId(), targetState);
    }
    /**
     * 检查是否所有state都已完成(>=2)
     */
    private boolean allStatesCompleted(Map<String, Object> stateValues) {
        for (Object value : stateValues.values()) {
            Integer state = parseInteger(value);
            if (state == null || state < 2) {
                return false;
            }
        }
        return !stateValues.isEmpty();
    }
    /**
     * 给MES汇报
     */
    private void reportToMes(DeviceConfig deviceConfig,
                             EnhancedS7Serializer serializer,
                             MesTaskInfo taskInfo,
                             Map<String, Object> logicParams) {
        try {
            // 设置汇报字
            Map<String, Object> payload = new HashMap<>();
            payload.put("plcReport", 1);
            plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
            String taskType = taskInfo.isOutbound ? "出片" : "进片";
            log.info("已给MES汇报({}任务): deviceId={}, glassId={}",
                    taskType, deviceConfig.getDeviceId(), taskInfo.glassId);
            // 等待MES确认
            waitForMesConfirm(deviceConfig, serializer, taskInfo, logicParams);
        } catch (Exception e) {
            log.error("给MES汇报异常: deviceId={}", deviceConfig.getDeviceId(), e);
        }
    }
    /**
     * 等待MES确认
     */
    private void waitForMesConfirm(DeviceConfig deviceConfig,
                                  EnhancedS7Serializer serializer,
                                  MesTaskInfo taskInfo,
                                  Map<String, Object> logicParams) {
        try {
            // 读取确认字(假设字段名为mesConfirm)
            Integer maxWaitTime = getLogicParam(logicParams, "mesConfirmTimeoutMs", 30000); // 默认30秒
            long startTime = System.currentTimeMillis();
            while (System.currentTimeMillis() - startTime < maxWaitTime) {
                Object confirmValue = plcDynamicDataService.readPlcField(
                        deviceConfig, "mesConfirm", serializer);
                Integer confirm = parseInteger(confirmValue);
                if (confirm != null && confirm == 1) {
                    // MES已确认,清空state和汇报字
                    clearTaskStates(deviceConfig, serializer);
                    // 任务完成,恢复为空闲状态
                    statusManager.updateVehicleStatus(
                            deviceConfig.getDeviceId(), VehicleState.IDLE);
                    statusManager.clearVehicleTask(deviceConfig.getDeviceId());
                    // 移除任务
                    currentTasks.remove(deviceConfig.getDeviceId());
                    // 停止任务监控
                    handleStopTaskMonitor(deviceConfig);
                    // 恢复plcRequest=1(可以接收新任务)
                    Map<String, Object> payload = new HashMap<>();
                    payload.put("plcRequest", 1);
                    plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
                    log.info("MES任务已完成: deviceId={}, glassId={}",
                            deviceConfig.getDeviceId(), taskInfo.glassId);
                    return;
                }
                Thread.sleep(500); // 等待500ms后重试
            }
            log.warn("等待MES确认超时: deviceId={}, glassId={}",
                    deviceConfig.getDeviceId(), taskInfo.glassId);
        } catch (Exception e) {
            log.error("等待MES确认异常: deviceId={}", deviceConfig.getDeviceId(), e);
        }
    }
    /**
     * 清空任务状态
     */
    private void clearTaskStates(DeviceConfig deviceConfig, EnhancedS7Serializer serializer) {
        try {
            Map<String, Object> payload = new HashMap<>();
            // 清空state1~6
            for (int i = 1; i <= 6; i++) {
                payload.put("state" + i, 0);
            }
            // 清空汇报字
            payload.put("plcReport", 0);
            plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
        } catch (Exception e) {
            log.error("清空任务状态异常: deviceId={}", deviceConfig.getDeviceId(), e);
        }
    }
    /**
     * 停止任务监控
     */
    private DevicePlcVO.OperationResult handleStopTaskMonitor(DeviceConfig deviceConfig) {
        String deviceId = deviceConfig.getDeviceId();
        ScheduledFuture<?> future = taskMonitoringTasks.remove(deviceId);
        if (future != null && !future.isCancelled()) {
            future.cancel(false);
            log.info("已停止任务监控: deviceId={}", deviceId);
        }
        return DevicePlcVO.OperationResult.builder()
                .success(true)
                .message("任务监控已停止")
                .build();
    }
    /**
     * 时间计算结果
     */
    private static class TimeCalculation {
        final long gotime;  // 到起始位置的时间(毫秒)
        final long cartime; // 从起始到目标位置的时间(毫秒)
        TimeCalculation(long gotime, long cartime) {
            this.gotime = gotime;
            this.cartime = cartime;
        }
    }
    /**
     * MES任务信息
     */
    private static class MesTaskInfo {
        String glassId;
        Integer startSlot;
        Integer targetSlot;
        Integer startPosition;
        Integer targetPosition;
        Integer currentPosition;
        long gotime;
        long cartime;
        Integer workLine;
        long createdTime;
        boolean isOutbound = false; // 是否为出片任务(false=进片,true=出片)
    }
    /**
     * 应用关闭时清理资源
     */
    @PreDestroy
    public void destroy() {
        log.info("正在关闭大车监控线程池...");
        // 停止所有监控任务
        for (String deviceId : new ArrayList<>(monitoringTasks.keySet())) {
            stopStateMonitoring(deviceId);
        }
        for (String deviceId : new ArrayList<>(idleMonitoringTasks.keySet())) {
            ScheduledFuture<?> future = idleMonitoringTasks.remove(deviceId);
            if (future != null) future.cancel(false);
        }
        for (String deviceId : new ArrayList<>(taskMonitoringTasks.keySet())) {
            ScheduledFuture<?> future = taskMonitoringTasks.remove(deviceId);
            if (future != null) future.cancel(false);
        }
        // 关闭线程池
        shutdownExecutor(stateMonitorExecutor, "状态监控");
        shutdownExecutor(idleMonitorExecutor, "空闲监控");
        shutdownExecutor(taskMonitorExecutor, "任务监控");
        log.info("大车监控线程池已关闭");
    }
    private void shutdownExecutor(ScheduledExecutorService executor, String name) {
        executor.shutdown();
        try {
            if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
                executor.shutdownNow();
                if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
                    log.warn("{}线程池未能正常关闭", name);
                }
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehiclePath.java
New file
@@ -0,0 +1,91 @@
package com.mes.interaction.vehicle.model;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
 * 车辆路径信息
 *
 * @author mes
 * @since 2025-01-XX
 */
@Data
public class VehiclePath {
    /**
     * 起始位置
     */
    private VehiclePosition startPosition;
    /**
     * 目标位置
     */
    private VehiclePosition endPosition;
    /**
     * 路径点列表(如果路径不是直线)
     */
    private List<VehiclePosition> waypoints;
    /**
     * 路径宽度(用于冲突检测)
     */
    private Double pathWidth;
    public VehiclePath() {
        this.waypoints = new ArrayList<>();
    }
    public VehiclePath(VehiclePosition start, VehiclePosition end) {
        this.startPosition = start;
        this.endPosition = end;
        this.waypoints = new ArrayList<>();
    }
    /**
     * 检查路径是否与另一条路径冲突
     */
    public boolean conflictsWith(VehiclePath other) {
        if (other == null) {
            return false;
        }
        // 简单冲突检测:检查起点和终点是否重叠
        if (startPosition != null && other.startPosition != null) {
            if (positionsOverlap(startPosition, other.startPosition)) {
                return true;
            }
        }
        if (endPosition != null && other.endPosition != null) {
            if (positionsOverlap(endPosition, other.endPosition)) {
                return true;
            }
        }
        // TODO: 更复杂的路径交叉检测
        return false;
    }
    private boolean positionsOverlap(VehiclePosition pos1, VehiclePosition pos2) {
        if (pos1 == null || pos2 == null) {
            return false;
        }
        // 如果有坐标,使用坐标判断
        if (pos1.getX() != null && pos1.getY() != null &&
            pos2.getX() != null && pos2.getY() != null) {
            double distance = pos1.distanceTo(pos2);
            double threshold = (pathWidth != null ? pathWidth : 100.0) / 2.0;
            return distance < threshold;
        }
        // 如果有位置值,使用位置值判断
        if (pos1.getPositionValue() != null && pos2.getPositionValue() != null) {
            return pos1.getPositionValue().equals(pos2.getPositionValue());
        }
        return false;
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehiclePosition.java
New file
@@ -0,0 +1,63 @@
package com.mes.interaction.vehicle.model;
import lombok.Data;
/**
 * 车辆位置信息
 *
 * @author huang
 * @since 2025-11-21
 */
@Data
public class VehiclePosition {
    /**
     * X坐标
     */
    private Double x;
    /**
     * Y坐标
     */
    private Double y;
    /**
     * Z坐标(如果需要)
     */
    private Double z;
    /**
     * 位置编码(如:POS1, POS2)
     */
    private String positionCode;
    /**
     * 位置值(PLC中的位置值)
     */
    private Integer positionValue;
    public VehiclePosition() {
    }
    public VehiclePosition(Double x, Double y) {
        this.x = x;
        this.y = y;
    }
    public VehiclePosition(String positionCode, Integer positionValue) {
        this.positionCode = positionCode;
        this.positionValue = positionValue;
    }
    /**
     * 计算到目标位置的距离
     */
    public double distanceTo(VehiclePosition target) {
        if (x == null || y == null || target.x == null || target.y == null) {
            return 0.0;
        }
        double dx = target.x - x;
        double dy = target.y - y;
        return Math.sqrt(dx * dx + dy * dy);
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehicleState.java
New file
@@ -0,0 +1,30 @@
package com.mes.interaction.vehicle.model;
/**
 * 车辆状态枚举
 *
 * @author mes
 * @since 2025-01-XX
 */
public enum VehicleState {
    /**
     * 空闲 - 车辆可用,可以接受新任务
     */
    IDLE,
    /**
     * 执行中 - 车辆正在执行任务,不能操作
     */
    EXECUTING,
    /**
     * 等待 - 车辆在排队等待执行
     */
    WAITING,
    /**
     * 错误 - 车辆出现错误
     */
    ERROR
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehicleStatus.java
New file
@@ -0,0 +1,113 @@
package com.mes.interaction.vehicle.model;
import lombok.Data;
import java.time.LocalDateTime;
/**
 * 车辆运行时状态
 * 每个设备实例有独立的状态对象
 *
 * @author huang
 * @since 2025-11-21
 */
@Data
public class VehicleStatus {
    /**
     * 设备ID(对应DeviceConfig.deviceId)
     */
    private String deviceId;
    /**
     * 设备名称
     */
    private String deviceName;
    /**
     * 当前状态
     */
    private VehicleState state;
    /**
     * 当前位置
     */
    private VehiclePosition currentPosition;
    /**
     * 目标位置
     */
    private VehiclePosition targetPosition;
    /**
     * 当前速度
     */
    private Double speed;
    /**
     * 任务开始时间
     */
    private LocalDateTime taskStartTime;
    /**
     * 预计结束时间
     */
    private LocalDateTime estimatedEndTime;
    /**
     * 当前任务
     */
    private VehicleTask currentTask;
    /**
     * 最后更新时间
     */
    private LocalDateTime lastUpdateTime;
    public VehicleStatus() {
        this.state = VehicleState.IDLE;
        this.lastUpdateTime = LocalDateTime.now();
    }
    public VehicleStatus(String deviceId) {
        this();
        this.deviceId = deviceId;
    }
    public VehicleStatus(String deviceId, String deviceName) {
        this(deviceId);
        this.deviceName = deviceName;
    }
    /**
     * 更新状态
     */
    public void setState(VehicleState newState) {
        this.state = newState;
        this.lastUpdateTime = LocalDateTime.now();
        if (newState == VehicleState.EXECUTING && currentTask != null) {
            this.taskStartTime = LocalDateTime.now();
            currentTask.setStartTime(taskStartTime);
            currentTask.calculateEstimatedEndTime();
            this.estimatedEndTime = currentTask.getEstimatedEndTime();
        } else if (newState == VehicleState.IDLE) {
            this.taskStartTime = null;
            this.estimatedEndTime = null;
            this.currentTask = null;
        }
    }
    /**
     * 检查是否可用
     */
    public boolean isAvailable() {
        return state == VehicleState.IDLE;
    }
    /**
     * 检查是否正在执行
     */
    public boolean isExecuting() {
        return state == VehicleState.EXECUTING;
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/vehicle/model/VehicleTask.java
New file
@@ -0,0 +1,81 @@
package com.mes.interaction.vehicle.model;
import lombok.Data;
import java.time.LocalDateTime;
/**
 * 车辆任务信息
 *
 * @author mes
 * @since 2025-01-XX
 */
@Data
public class VehicleTask {
    /**
     * 任务ID
     */
    private String taskId;
    /**
     * 任务名称
     */
    private String taskName;
    /**
     * 操作类型(如:feedGlass, triggerRequest等)
     */
    private String operation;
    /**
     * 计划路径
     */
    private VehiclePath plannedPath;
    /**
     * 任务开始时间
     */
    private LocalDateTime startTime;
    /**
     * 预计结束时间
     */
    private LocalDateTime estimatedEndTime;
    /**
     * 车辆速度(用于计算预计结束时间)
     */
    private Double speed;
    /**
     * 任务参数
     */
    private java.util.Map<String, Object> parameters;
    public VehicleTask() {
        this.parameters = new java.util.HashMap<>();
    }
    /**
     * 计算预计结束时间
     */
    public void calculateEstimatedEndTime() {
        if (plannedPath == null || speed == null || speed <= 0) {
            return;
        }
        VehiclePosition start = plannedPath.getStartPosition();
        VehiclePosition end = plannedPath.getEndPosition();
        if (start != null && end != null) {
            double distance = start.distanceTo(end);
            if (distance > 0) {
                // 时间 = 距离 / 速度(秒)
                long seconds = (long) (distance / speed);
                estimatedEndTime = startTime != null ?
                    startTime.plusSeconds(seconds) :
                    LocalDateTime.now().plusSeconds(seconds);
            }
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/base/WorkstationBaseHandler.java
New file
@@ -0,0 +1,80 @@
package com.mes.interaction.workstation.base;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.mes.device.entity.DeviceConfig;
import com.mes.device.service.DevicePlcOperationService;
import com.mes.device.vo.DevicePlcVO;
import com.mes.interaction.BaseDeviceLogicHandler;
import com.mes.interaction.workstation.config.WorkstationLogicConfig;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
import java.util.Map;
/**
 * 卧转立系列设备的通用处理器基类
 * 负责解析 workstation 配置、提供占位的执行模板,后续具体设备在子类中实现。
 *
 * @author mes
 * @since 2025-11-24
 */
@Slf4j
public abstract class WorkstationBaseHandler extends BaseDeviceLogicHandler {
    protected WorkstationBaseHandler(DevicePlcOperationService devicePlcOperationService) {
        super(devicePlcOperationService);
    }
    /**
     * 解析 workstation 相关配置
     */
    protected WorkstationLogicConfig parseWorkstationConfig(Map<String, Object> logicParams) {
        WorkstationLogicConfig config = new WorkstationLogicConfig();
        if (logicParams == null) {
            return config;
        }
        config.setScanIntervalMs(getLogicParam(logicParams, "scanIntervalMs", 10_000));
        config.setTransferDelayMs(getLogicParam(logicParams, "transferDelayMs", 30_000));
        config.setVehicleCapacity(getLogicParam(logicParams, "vehicleCapacity", 6000));
        config.setAutoAck(getLogicParam(logicParams, "autoAck", Boolean.TRUE));
        return config;
    }
    /**
     * 默认实现:提示尚未实现具体逻辑
     */
    @Override
    protected DevicePlcVO.OperationResult doExecute(com.mes.device.entity.DeviceConfig deviceConfig,
                                                   String operation,
                                                   java.util.Map<String, Object> params,
                                                   java.util.Map<String, Object> logicParams) {
        log.warn("当前设备逻辑尚未实现: deviceType={}, operation={}", deviceConfig.getDeviceType(), operation);
        return DevicePlcVO.OperationResult.builder()
                .success(false)
                .message("设备逻辑待实现: " + deviceConfig.getDeviceType())
                .build();
    }
    @Override
    public String validateLogicParams(DeviceConfig deviceConfig) {
        // 默认认为配置合法,具体校验交由子类实现
        return null;
    }
    @Override
    public String getDefaultLogicParams() {
        Map<String, Object> defaults = new HashMap<>();
        defaults.put("scanIntervalMs", 10_000);
        defaults.put("transferDelayMs", 30_000);
        defaults.put("vehicleCapacity", 6_000);
        defaults.put("autoAck", true);
        try {
            return objectMapper.writeValueAsString(defaults);
        } catch (JsonProcessingException e) {
            return "{\"scanIntervalMs\":10000,\"transferDelayMs\":30000,\"vehicleCapacity\":6000,\"autoAck\":true}";
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/config/WorkstationLogicConfig.java
New file
@@ -0,0 +1,32 @@
package com.mes.interaction.workstation.config;
import lombok.Data;
/**
 * 卧转立相关设备的逻辑配置
 * 对应 extraParams.deviceLogic 中的字段
 */
@Data
public class WorkstationLogicConfig {
    /**
     * 扫码设备发送请求的时间间隔(毫秒)
     */
    private Integer scanIntervalMs = 10_000;
    /**
     * 卧转立到大车的运输时间(毫秒)
     */
    private Integer transferDelayMs = 30_000;
    /**
     * 可装载的最大宽度(mm)
     */
    private Integer vehicleCapacity = 6_000;
    /**
     * 是否自动确认 MES 发送的玻璃信息
     */
    private Boolean autoAck = Boolean.TRUE;
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/scanner/handler/HorizontalScannerLogicHandler.java
New file
@@ -0,0 +1,161 @@
package com.mes.interaction.workstation.scanner.handler;
import com.mes.device.entity.DeviceConfig;
import com.mes.device.entity.GlassInfo;
import com.mes.device.service.DevicePlcOperationService;
import com.mes.device.service.GlassInfoService;
import com.mes.device.vo.DevicePlcVO;
import com.mes.interaction.workstation.base.WorkstationBaseHandler;
import com.mes.interaction.workstation.config.WorkstationLogicConfig;
import com.mes.s7.enhanced.EnhancedS7Serializer;
import com.mes.s7.provider.S7SerializerProvider;
import com.mes.service.PlcDynamicDataService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
 * 卧转立扫码设备逻辑处理器
 * 负责从MES写区读取玻璃尺寸,并落库 glass_info
 */
@Slf4j
@Component
public class HorizontalScannerLogicHandler extends WorkstationBaseHandler {
    private static final List<String> MES_FIELDS = Arrays.asList("mesSend", "mesGlassId", "mesWidth", "mesHeight", "workLine");
    private final PlcDynamicDataService plcDynamicDataService;
    private final GlassInfoService glassInfoService;
    private final S7SerializerProvider s7SerializerProvider;
    public HorizontalScannerLogicHandler(DevicePlcOperationService devicePlcOperationService,
                                         PlcDynamicDataService plcDynamicDataService,
                                         GlassInfoService glassInfoService,
                                         S7SerializerProvider s7SerializerProvider) {
        super(devicePlcOperationService);
        this.plcDynamicDataService = plcDynamicDataService;
        this.glassInfoService = glassInfoService;
        this.s7SerializerProvider = s7SerializerProvider;
    }
    @Override
    public String getDeviceType() {
        return DeviceConfig.DeviceType.WORKSTATION_SCANNER;
    }
    @Override
    protected DevicePlcVO.OperationResult doExecute(DeviceConfig deviceConfig,
                                                    String operation,
                                                    Map<String, Object> params,
                                                    Map<String, Object> logicParams) {
        WorkstationLogicConfig config = parseWorkstationConfig(logicParams);
        EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig);
        if (serializer == null) {
            return buildResult(deviceConfig, operation, false, "获取PLC序列化器失败");
        }
        try {
            log.debug("卧转立扫码读取MES写区: deviceId={}, scanInterval={}ms",
                    deviceConfig.getId(), config.getScanIntervalMs());
            Map<String, Object> mesData = plcDynamicDataService.readPlcData(deviceConfig, MES_FIELDS, serializer);
            if (mesData == null || mesData.isEmpty()) {
                return buildResult(deviceConfig, operation, false, "读取MES写区失败");
            }
            Integer mesSend = parseInteger(mesData.get("mesSend"));
            if (mesSend == null || mesSend == 0) {
                return buildResult(deviceConfig, operation, true, "暂无待处理的玻璃信息");
            }
            String glassId = parseString(mesData.get("mesGlassId"));
            if (!StringUtils.hasText(glassId)) {
                return buildResult(deviceConfig, operation, false, "MES写区未提供玻璃ID");
            }
            Integer longSide = convertDimension(parseInteger(mesData.get("mesWidth")));
            Integer shortSide = convertDimension(parseInteger(mesData.get("mesHeight")));
            Integer workLine = parseInteger(mesData.get("workLine"));
            GlassInfo glassInfo = buildGlassInfo(glassId, longSide, shortSide, workLine);
            boolean saved = glassInfoService.saveOrUpdateGlassInfo(glassInfo);
            if (!saved) {
                return buildResult(deviceConfig, operation, false, "保存玻璃信息失败: " + glassId);
            }
            // 读取到MES数据后,重置mesSend,避免重复消费
            plcDynamicDataService.writePlcField(deviceConfig, "mesSend", 0, serializer);
            String msg = String.format("玻璃[%s] 尺寸[%s x %s] 已接收并入库,workLine=%s",
                    glassId,
                    longSide != null ? longSide + "mm" : "-",
                    shortSide != null ? shortSide + "mm" : "-",
                    workLine != null ? workLine : "-");
            return buildResult(deviceConfig, operation, true, msg);
        } catch (Exception e) {
            log.error("卧转立扫码处理异常, deviceId={}", deviceConfig.getId(), e);
            return buildResult(deviceConfig, operation, false, "处理异常: " + e.getMessage());
        }
    }
    private GlassInfo buildGlassInfo(String glassId, Integer longSide, Integer shortSide, Integer workLine) {
        GlassInfo glassInfo = new GlassInfo();
        glassInfo.setGlassId(glassId.trim());
        if (longSide != null) {
            glassInfo.setGlassLength(longSide);
        }
        if (shortSide != null) {
            glassInfo.setGlassWidth(shortSide);
        }
        glassInfo.setStatus(GlassInfo.Status.ACTIVE);
        if (workLine != null) {
            glassInfo.setDescription("workLine=" + workLine);
        }
        return glassInfo;
    }
    private Integer parseInteger(Object value) {
        if (value instanceof Number) {
            return ((Number) value).intValue();
        }
        if (value == null) {
            return null;
        }
        try {
            return Integer.parseInt(String.valueOf(value));
        } catch (NumberFormatException e) {
            return null;
        }
    }
    private String parseString(Object value) {
        return value == null ? null : String.valueOf(value).trim();
    }
    private Integer convertDimension(Integer raw) {
        if (raw == null) {
            return null;
        }
        return raw / 10;
    }
    private DevicePlcVO.OperationResult buildResult(DeviceConfig deviceConfig,
                                                    String operation,
                                                    boolean success,
                                                    String message) {
        return DevicePlcVO.OperationResult.builder()
                .deviceId(deviceConfig.getId())
                .deviceName(deviceConfig.getDeviceName())
                .deviceCode(deviceConfig.getDeviceCode())
                .projectId(deviceConfig.getProjectId() != null ? String.valueOf(deviceConfig.getProjectId()) : null)
                .operation(operation)
                .success(success)
                .message(message)
                .timestamp(LocalDateTime.now())
                .build();
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/transfer/handler/HorizontalTransferLogicHandler.java
New file
@@ -0,0 +1,481 @@
package com.mes.interaction.workstation.transfer.handler;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mes.device.entity.DeviceConfig;
import com.mes.device.entity.GlassInfo;
import com.mes.device.mapper.DeviceGlassInfoMapper;
import com.mes.device.service.DevicePlcOperationService;
import com.mes.device.service.GlassInfoService;
import com.mes.device.vo.DevicePlcVO;
import com.mes.interaction.workstation.base.WorkstationBaseHandler;
import com.mes.interaction.workstation.config.WorkstationLogicConfig;
import com.mes.s7.enhanced.EnhancedS7Serializer;
import com.mes.s7.provider.S7SerializerProvider;
import com.mes.service.PlcDynamicDataService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PreDestroy;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
/**
 * 卧转立主体设备逻辑处理器
 * 负责玻璃缓冲、容量校验、批次组装、PLC写入等逻辑
 */
@Slf4j
@Component
public class HorizontalTransferLogicHandler extends WorkstationBaseHandler {
    private final PlcDynamicDataService plcDynamicDataService;
    private final GlassInfoService glassInfoService;
    private final S7SerializerProvider s7SerializerProvider;
    @Autowired(required = false)
    private DeviceGlassInfoMapper glassInfoMapper;
    // 玻璃缓冲队列:deviceId -> 玻璃信息列表
    private final Map<String, List<GlassBufferItem>> glassBuffer = new ConcurrentHashMap<>();
    // 最后扫码时间:deviceId -> 最后扫码时间戳
    private final Map<String, AtomicLong> lastScanTime = new ConcurrentHashMap<>();
    // 监控任务:deviceId -> 监控任务
    private final Map<String, ScheduledFuture<?>> monitorTasks = new ConcurrentHashMap<>();
    // 监控线程池
    private final ScheduledExecutorService monitorExecutor = Executors.newScheduledThreadPool(5, r -> {
        Thread t = new Thread(r, "HorizontalTransferMonitor");
        t.setDaemon(true);
        return t;
    });
    @Autowired
    public HorizontalTransferLogicHandler(DevicePlcOperationService devicePlcOperationService,
                                         PlcDynamicDataService plcDynamicDataService,
                                         @Qualifier("deviceGlassInfoService") GlassInfoService glassInfoService,
                                         S7SerializerProvider s7SerializerProvider) {
        super(devicePlcOperationService);
        this.plcDynamicDataService = plcDynamicDataService;
        this.glassInfoService = glassInfoService;
        this.s7SerializerProvider = s7SerializerProvider;
    }
    @Override
    public String getDeviceType() {
        return DeviceConfig.DeviceType.WORKSTATION_TRANSFER;
    }
    @Override
    protected DevicePlcVO.OperationResult doExecute(DeviceConfig deviceConfig,
                                                    String operation,
                                                    Map<String, Object> params,
                                                    Map<String, Object> logicParams) {
        WorkstationLogicConfig config = parseWorkstationConfig(logicParams);
        try {
            switch (operation) {
                case "checkAndProcess":
                case "process":
                    return handleCheckAndProcess(deviceConfig, config, logicParams);
                case "startMonitor":
                    return handleStartMonitor(deviceConfig, config, logicParams);
                case "stopMonitor":
                    return handleStopMonitor(deviceConfig);
                case "clearBuffer":
                    return handleClearBuffer(deviceConfig);
                default:
                    return buildResult(deviceConfig, operation, false,
                            "不支持的操作: " + operation);
            }
        } catch (Exception e) {
            log.error("卧转立主体处理异常: deviceId={}, operation={}",
                    deviceConfig.getId(), operation, e);
            return buildResult(deviceConfig, operation, false,
                    "处理异常: " + e.getMessage());
        }
    }
    /**
     * 检查并处理玻璃批次
     * 从数据库读取最近扫码的玻璃,进行容量判断,组装批次,写入PLC
     */
    private DevicePlcVO.OperationResult handleCheckAndProcess(
            DeviceConfig deviceConfig,
            WorkstationLogicConfig config,
            Map<String, Object> logicParams) {
        String deviceId = deviceConfig.getDeviceId();
        EnhancedS7Serializer serializer = s7SerializerProvider.getSerializer(deviceConfig);
        if (serializer == null) {
            return buildResult(deviceConfig, "checkAndProcess", false,
                    "获取PLC序列化器失败");
        }
        try {
            // 1. 从数据库查询最近扫码的玻璃信息(最近1分钟内的记录)
            List<GlassInfo> recentGlasses = queryRecentScannedGlasses(deviceConfig, logicParams);
            if (recentGlasses.isEmpty()) {
                return buildResult(deviceConfig, "checkAndProcess", true,
                        "暂无待处理的玻璃信息");
            }
            log.info("查询到最近扫码的玻璃: deviceId={}, count={}",
                    deviceId, recentGlasses.size());
            // 2. 更新缓冲队列和最后扫码时间
            updateBuffer(deviceId, recentGlasses);
            lastScanTime.put(deviceId, new AtomicLong(System.currentTimeMillis()));
            // 3. 检查是否需要立即处理(容量已满或30s内无新玻璃)
            List<GlassBufferItem> buffer = glassBuffer.get(deviceId);
            if (buffer == null || buffer.isEmpty()) {
                return buildResult(deviceConfig, "checkAndProcess", true,
                        "缓冲队列为空");
            }
            // 4. 判断是否满足处理条件
            boolean shouldProcess = shouldProcessBatch(deviceId, buffer, config);
            if (!shouldProcess) {
                return buildResult(deviceConfig, "checkAndProcess", true,
                        "等待更多玻璃或30s超时");
            }
            // 5. 容量判断和批次组装
            List<GlassInfo> batch = assembleBatch(buffer, config.getVehicleCapacity());
            if (batch.isEmpty()) {
                return buildResult(deviceConfig, "checkAndProcess", false,
                        "无法组装有效批次(容量不足)");
            }
            // 6. 写入PLC
            DevicePlcVO.OperationResult writeResult = writeBatchToPlc(
                    deviceConfig, batch, serializer, logicParams);
            if (!Boolean.TRUE.equals(writeResult.getSuccess())) {
                return writeResult;
            }
            // 7. 从缓冲队列中移除已处理的玻璃
            removeProcessedGlasses(deviceId, batch);
            String msg = String.format("批次已写入PLC: glassCount=%d, glassIds=%s",
                    batch.size(),
                    batch.stream().map(GlassInfo::getGlassId).collect(Collectors.joining(",")));
            return buildResult(deviceConfig, "checkAndProcess", true, msg);
        } catch (Exception e) {
            log.error("检查并处理玻璃批次异常: deviceId={}", deviceId, e);
            return buildResult(deviceConfig, "checkAndProcess", false,
                    "处理异常: " + e.getMessage());
        }
    }
    /**
     * 查询最近扫码的玻璃信息
     */
    private List<GlassInfo> queryRecentScannedGlasses(
            DeviceConfig deviceConfig,
            Map<String, Object> logicParams) {
        if (glassInfoMapper == null) {
            log.warn("GlassInfoMapper未注入,无法查询最近扫码的玻璃");
            return Collections.emptyList();
        }
        try {
            // 从配置中获取workLine,用于过滤
            String workLine = getLogicParam(logicParams, "workLine", null);
            // 查询最近2分钟内的玻璃记录(扩大时间窗口,确保不遗漏)
            Date twoMinutesAgo = new Date(System.currentTimeMillis() - 120000);
            LambdaQueryWrapper<GlassInfo> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(GlassInfo::getStatus, GlassInfo.Status.ACTIVE)
                   .ge(GlassInfo::getCreatedTime, twoMinutesAgo)
                   .orderByDesc(GlassInfo::getCreatedTime)
                   .last("LIMIT 20"); // 限制查询数量,避免过多
            // 如果配置了workLine,则过滤description
            if (workLine != null && !workLine.isEmpty()) {
                wrapper.like(GlassInfo::getDescription, "workLine=" + workLine);
            }
            List<GlassInfo> recentGlasses = glassInfoMapper.selectList(wrapper);
            log.debug("查询到最近扫码的玻璃: deviceId={}, workLine={}, count={}",
                    deviceConfig.getId(), workLine, recentGlasses.size());
            return recentGlasses;
        } catch (Exception e) {
            log.error("查询最近扫码的玻璃信息异常: deviceId={}",
                    deviceConfig.getId(), e);
            return Collections.emptyList();
        }
    }
    /**
     * 更新缓冲队列
     */
    private void updateBuffer(String deviceId, List<GlassInfo> newGlasses) {
        List<GlassBufferItem> buffer = glassBuffer.computeIfAbsent(
                deviceId, k -> new CopyOnWriteArrayList<>());
        Set<String> existingIds = buffer.stream()
                .map(item -> item.glassInfo.getGlassId())
                .collect(Collectors.toSet());
        for (GlassInfo glass : newGlasses) {
            if (!existingIds.contains(glass.getGlassId())) {
                buffer.add(new GlassBufferItem(glass, System.currentTimeMillis()));
                log.debug("添加玻璃到缓冲队列: deviceId={}, glassId={}",
                        deviceId, glass.getGlassId());
            }
        }
    }
    /**
     * 判断是否应该处理批次
     */
    private boolean shouldProcessBatch(String deviceId,
                                      List<GlassBufferItem> buffer,
                                      WorkstationLogicConfig config) {
        // 条件1:缓冲队列已满(达到容量限制)
        int totalLength = buffer.stream()
                .mapToInt(item -> item.glassInfo.getGlassLength() != null ?
                        item.glassInfo.getGlassLength() : 0)
                .sum();
        if (totalLength >= config.getVehicleCapacity()) {
            log.info("缓冲队列容量已满,触发批次处理: deviceId={}, totalLength={}, capacity={}",
                    deviceId, totalLength, config.getVehicleCapacity());
            return true;
        }
        // 条件2:30s内无新玻璃扫码
        AtomicLong lastTime = lastScanTime.get(deviceId);
        if (lastTime != null) {
            long elapsed = System.currentTimeMillis() - lastTime.get();
            if (elapsed >= config.getTransferDelayMs()) {
                log.info("30s内无新玻璃扫码,触发批次处理: deviceId={}, elapsed={}ms",
                        deviceId, elapsed);
                return true;
            }
        }
        return false;
    }
    /**
     * 组装批次(容量判断)
     */
    private List<GlassInfo> assembleBatch(List<GlassBufferItem> buffer,
                                          int vehicleCapacity) {
        List<GlassInfo> batch = new ArrayList<>();
        int usedLength = 0;
        for (GlassBufferItem item : buffer) {
            GlassInfo glass = item.glassInfo;
            int glassLength = glass.getGlassLength() != null ?
                    glass.getGlassLength() : 0;
            if (usedLength + glassLength <= vehicleCapacity && batch.size() < 6) {
                batch.add(glass);
                usedLength += glassLength;
            } else {
                break;
            }
        }
        return batch;
    }
    /**
     * 写入批次到PLC
     */
    private DevicePlcVO.OperationResult writeBatchToPlc(
            DeviceConfig deviceConfig,
            List<GlassInfo> batch,
            EnhancedS7Serializer serializer,
            Map<String, Object> logicParams) {
        Map<String, Object> payload = new HashMap<>();
        // 写入玻璃ID(最多6个)
        int count = Math.min(batch.size(), 6);
        for (int i = 0; i < count; i++) {
            String fieldName = "plcGlassId" + (i + 1);
            payload.put(fieldName, batch.get(i).getGlassId());
        }
        // 写入玻璃数量
        payload.put("plcGlassCount", count);
        // 写入位置信息(如果有配置)
        Integer inPosition = getLogicParam(logicParams, "inPosition", null);
        if (inPosition != null) {
            payload.put("inPosition", inPosition);
        }
        // 写入请求字(触发大车)
        payload.put("plcRequest", 1);
        try {
            plcDynamicDataService.writePlcData(deviceConfig, payload, serializer);
            log.info("批次已写入PLC: deviceId={}, glassCount={}",
                    deviceConfig.getId(), count);
            return buildResult(deviceConfig, "writeBatchToPlc", true,
                    "批次写入成功");
        } catch (Exception e) {
            log.error("写入批次到PLC失败: deviceId={}", deviceConfig.getId(), e);
            return buildResult(deviceConfig, "writeBatchToPlc", false,
                    "写入失败: " + e.getMessage());
        }
    }
    /**
     * 从缓冲队列移除已处理的玻璃
     */
    private void removeProcessedGlasses(String deviceId, List<GlassInfo> processed) {
        List<GlassBufferItem> buffer = glassBuffer.get(deviceId);
        if (buffer == null) {
            return;
        }
        Set<String> processedIds = processed.stream()
                .map(GlassInfo::getGlassId)
                .collect(Collectors.toSet());
        buffer.removeIf(item -> processedIds.contains(item.glassInfo.getGlassId()));
    }
    /**
     * 启动监控任务(定期检查并处理)
     */
    private DevicePlcVO.OperationResult handleStartMonitor(
            DeviceConfig deviceConfig,
            WorkstationLogicConfig config,
            Map<String, Object> logicParams) {
        String deviceId = deviceConfig.getDeviceId();
        // 停止旧的监控任务
        handleStopMonitor(deviceConfig);
        // 获取监控间隔
        Integer monitorIntervalMs = getLogicParam(logicParams, "monitorIntervalMs",
                config.getScanIntervalMs());
        // 启动监控任务
        ScheduledFuture<?> future = monitorExecutor.scheduleWithFixedDelay(() -> {
            try {
                handleCheckAndProcess(deviceConfig, config, logicParams);
            } catch (Exception e) {
                log.error("监控任务执行异常: deviceId={}", deviceId, e);
            }
        }, monitorIntervalMs, monitorIntervalMs, TimeUnit.MILLISECONDS);
        monitorTasks.put(deviceId, future);
        log.info("已启动卧转立监控任务: deviceId={}, interval={}ms",
                deviceId, monitorIntervalMs);
        return buildResult(deviceConfig, "startMonitor", true,
                "监控任务已启动");
    }
    /**
     * 停止监控任务
     */
    private DevicePlcVO.OperationResult handleStopMonitor(DeviceConfig deviceConfig) {
        String deviceId = deviceConfig.getDeviceId();
        ScheduledFuture<?> future = monitorTasks.remove(deviceId);
        if (future != null && !future.isCancelled()) {
            future.cancel(false);
            log.info("已停止卧转立监控任务: deviceId={}", deviceId);
        }
        return buildResult(deviceConfig, "stopMonitor", true, "监控任务已停止");
    }
    /**
     * 清空缓冲队列
     */
    private DevicePlcVO.OperationResult handleClearBuffer(DeviceConfig deviceConfig) {
        String deviceId = deviceConfig.getDeviceId();
        glassBuffer.remove(deviceId);
        lastScanTime.remove(deviceId);
        log.info("已清空缓冲队列: deviceId={}", deviceId);
        return buildResult(deviceConfig, "clearBuffer", true, "缓冲队列已清空");
    }
    /**
     * 构建操作结果
     */
    private DevicePlcVO.OperationResult buildResult(DeviceConfig deviceConfig,
                                                    String operation,
                                                    boolean success,
                                                    String message) {
        return DevicePlcVO.OperationResult.builder()
                .deviceId(deviceConfig.getId())
                .deviceName(deviceConfig.getDeviceName())
                .deviceCode(deviceConfig.getDeviceCode())
                .projectId(deviceConfig.getProjectId() != null ?
                        String.valueOf(deviceConfig.getProjectId()) : null)
                .operation(operation)
                .success(success)
                .message(message)
                .timestamp(LocalDateTime.now())
                .build();
    }
    /**
     * 应用关闭时清理资源
     */
    @PreDestroy
    public void destroy() {
        log.info("正在关闭卧转立监控线程池...");
        // 停止所有监控任务
        for (String deviceId : new ArrayList<>(monitorTasks.keySet())) {
            ScheduledFuture<?> future = monitorTasks.remove(deviceId);
            if (future != null && !future.isCancelled()) {
                future.cancel(false);
            }
        }
        // 关闭线程池
        monitorExecutor.shutdown();
        try {
            if (!monitorExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
                monitorExecutor.shutdownNow();
                if (!monitorExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
                    log.warn("卧转立监控线程池未能正常关闭");
                }
            }
        } catch (InterruptedException e) {
            monitorExecutor.shutdownNow();
            Thread.currentThread().interrupt();
        }
        log.info("卧转立监控线程池已关闭");
    }
    /**
     * 玻璃缓冲项
     */
    private static class GlassBufferItem {
        final GlassInfo glassInfo;
        final long timestamp;
        GlassBufferItem(GlassInfo glassInfo, long timestamp) {
            this.glassInfo = glassInfo;
            this.timestamp = timestamp;
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/s7/provider/S7SerializerProvider.java
New file
@@ -0,0 +1,90 @@
package com.mes.s7.provider;
import com.github.xingshuangs.iot.protocol.s7.enums.EPlcType;
import com.github.xingshuangs.iot.protocol.s7.service.S7PLC;
import com.mes.device.entity.DeviceConfig;
import com.mes.s7.enhanced.EnhancedS7Serializer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
 * 提供 S7 序列化器的公共工厂,避免各处重复创建/缓存
 * @author huang
 */
@Slf4j
@Component
public class S7SerializerProvider {
    private final ConcurrentMap<String, EnhancedS7Serializer> serializerCache = new ConcurrentHashMap<>();
    /**
     * 获取或创建与设备绑定的 S7 序列化器
     */
    public EnhancedS7Serializer getSerializer(DeviceConfig deviceConfig) {
        if (deviceConfig == null) {
            log.error("设备配置为空,无法获取S7序列化器");
            return null;
        }
        String cacheKey = buildCacheKey(deviceConfig);
        return serializerCache.computeIfAbsent(cacheKey, key -> createSerializer(deviceConfig));
    }
    /**
     * 清除指定设备对应的缓存
     */
    public void clear(DeviceConfig deviceConfig) {
        if (deviceConfig == null) {
            return;
        }
        serializerCache.remove(buildCacheKey(deviceConfig));
    }
    private String buildCacheKey(DeviceConfig deviceConfig) {
        if (deviceConfig.getId() != null) {
            return "device:" + deviceConfig.getId();
        }
        if (deviceConfig.getDeviceCode() != null) {
            return "device:" + deviceConfig.getDeviceCode();
        }
        return "device:" + Objects.hash(deviceConfig);
    }
    private EnhancedS7Serializer createSerializer(DeviceConfig deviceConfig) {
        try {
            EPlcType plcType = parsePlcType(deviceConfig.getPlcType());
            String plcIp = deviceConfig.getPlcIp();
            if (plcIp == null || plcIp.isEmpty()) {
                log.warn("设备未配置PLC IP,使用默认值 192.168.10.21, deviceId={}", deviceConfig.getId());
                plcIp = "192.168.10.21";
            }
            S7PLC s7Plc = new S7PLC(plcType, plcIp);
            EnhancedS7Serializer serializer = EnhancedS7Serializer.newInstance(s7Plc);
            if (serializer == null) {
                log.error("创建EnhancedS7Serializer失败: deviceId={}, plcIp={}, plcType={}",
                        deviceConfig.getId(), plcIp, plcType);
            }
            return serializer;
        } catch (Exception e) {
            log.error("创建S7序列化器异常: deviceId={}", deviceConfig.getId(), e);
            return null;
        }
    }
    private EPlcType parsePlcType(String plcTypeValue) {
        if (plcTypeValue == null || plcTypeValue.isEmpty()) {
            return EPlcType.S1200;
        }
        try {
            return EPlcType.valueOf(plcTypeValue);
        } catch (IllegalArgumentException e) {
            log.warn("未知的PLC类型: {},使用默认类型S1200", plcTypeValue);
            return EPlcType.S1200;
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/task/controller/TaskStatusNotificationController.java
@@ -16,15 +16,17 @@
 * @since 2025-01-XX
 */
@RestController
@RequestMapping("/api/plcSend/task/notification")
@RequestMapping("task/notification")
@Api(tags = "任务状态通知")
@RequiredArgsConstructor
@CrossOrigin(origins = "*", maxAge = 3600)
public class TaskStatusNotificationController {
    private final TaskStatusNotificationService notificationService;
    @GetMapping(value = "/sse", produces = "text/event-stream")
    @ApiOperation("创建SSE连接,监听任务状态变化")
    @CrossOrigin(origins = "*")
    public SseEmitter createConnection(@RequestParam(required = false) String taskId) {
        SseEmitter emitter = notificationService.createConnection(taskId);
        if (emitter == null) {
@@ -33,8 +35,15 @@
        return emitter;
    }
    @RequestMapping(value = "/sse", method = RequestMethod.OPTIONS)
    @CrossOrigin(origins = "*")
    public void options() {
        // 处理 OPTIONS 预检请求
    }
    @GetMapping(value = "/sse/all", produces = "text/event-stream")
    @ApiOperation("创建SSE连接,监听所有任务状态变化")
    @CrossOrigin(origins = "*")
    public SseEmitter createConnectionForAllTasks() {
        return createConnection(null);
    }
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java
@@ -769,7 +769,7 @@
        switch (device.getDeviceType()) {
            case DeviceConfig.DeviceType.LOAD_VEHICLE:
                context.setLoadedGlassIds(glassIds);
                // 数据传递:上大车 -> 下一个设备
                // 数据传递:大车设备 -> 下一个设备
                if (!CollectionUtils.isEmpty(glassIds)) {
                    Map<String, Object> transferData = new HashMap<>();
                    transferData.put("glassIds", glassIds);
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/MultiDeviceTaskServiceImpl.java
@@ -22,6 +22,7 @@
import com.mes.task.service.TaskStatusNotificationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
@@ -69,6 +70,7 @@
            throw new IllegalArgumentException("至少需要配置一条玻璃ID");
        }
        // 创建任务记录
        MultiDeviceTask task = new MultiDeviceTask();
        task.setTaskId(generateTaskId(groupConfig));
        task.setGroupId(String.valueOf(groupConfig.getId()));
@@ -79,11 +81,39 @@
        task.setStartTime(new Date());
        save(task);
        // 异步执行任务,立即返回任务ID
        executeTaskAsync(task, groupConfig, devices, parameters);
        log.info("设备组任务已启动(异步执行): taskId={}, groupId={}, groupName={}",
            task.getTaskId(), groupConfig.getId(), groupConfig.getGroupName());
        return task;
    }
    /**
     * 异步执行设备组任务
     * 每个设备组作为独立线程执行,互不阻塞
     */
    @Async("deviceGroupTaskExecutor")
    public void executeTaskAsync(MultiDeviceTask task,
                                  DeviceGroupConfig groupConfig,
                                  List<DeviceConfig> devices,
                                  TaskParameters parameters) {
        try {
            log.info("开始执行设备组任务: taskId={}, groupId={}, deviceCount={}",
                task.getTaskId(), groupConfig.getId(), devices.size());
            // 更新任务状态为运行中
            task.setStatus(MultiDeviceTask.Status.RUNNING.name());
            updateById(task);
            // 通知任务开始
            notificationService.notifyTaskStatus(task);
            
            // 执行任务
            TaskExecutionResult result = taskExecutionEngine.execute(task, groupConfig, devices, parameters);
            // 更新任务结果
            task.setStatus(result.isSuccess() ? MultiDeviceTask.Status.COMPLETED.name() : MultiDeviceTask.Status.FAILED.name());
            task.setErrorMessage(result.isSuccess() ? null : result.getMessage());
            task.setEndTime(new Date());
@@ -93,14 +123,20 @@
            // 通知任务完成
            notificationService.notifyTaskStatus(task);
            
            return task;
            log.info("设备组任务执行完成: taskId={}, success={}, message={}",
                task.getTaskId(), result.isSuccess(), result.getMessage());
        } catch (Exception ex) {
            log.error("多设备任务执行异常, taskId={}", task.getTaskId(), ex);
            log.error("设备组任务执行异常: taskId={}, groupId={}", task.getTaskId(), groupConfig.getId(), ex);
            // 更新任务状态为失败
            task.setStatus(MultiDeviceTask.Status.FAILED.name());
            task.setErrorMessage(ex.getMessage());
            task.setEndTime(new Date());
            updateById(task);
            throw new RuntimeException("多设备任务执行失败: " + ex.getMessage(), ex);
            // 通知任务失败
            notificationService.notifyTaskStatus(task);
        }
    }
mes-web/src/utils/constants.js
@@ -1,6 +1,6 @@
// export const WebSocketHost = "10.153.19.150";
// export const WebSocketHost = "172.17.2.7";
export const WebSocketHost = "10.153.19.49";//hxl
export const WebSocketHost = "10.153.19.225";//hxl
// export const WebSocketHost = "10.153.19.2";//zt
//export const WebSocketHost = "10.153.19.20";//wsx
// export const WebSocketHost = "127.0.0.1";
mes-web/src/views/device/DeviceConfigList.vue
@@ -190,8 +190,8 @@
      pageNum: pagination.page,
      pageSize: pagination.size,
      deviceType: searchForm.deviceType || undefined,
      status: searchForm.deviceStatus || undefined,
      deviceCode: searchForm.keyword || undefined
      deviceStatus: searchForm.deviceStatus || undefined,
      keyword: searchForm.keyword?.trim() || undefined
    }
    
    const response = await deviceConfigApi.getList(params)
mes-web/src/views/device/DeviceEditDialog.vue
@@ -41,9 +41,10 @@
            <el-form-item label="设备类型" prop="deviceType">
              <el-select v-model="deviceForm.deviceType" placeholder="选择设备类型" style="width: 100%;">
                <el-option label="上大车" value="上大车" />
                <el-option label="大理片" value="大理片" />
                <el-option label="玻璃存储" value="玻璃存储" />
                <el-option label="大车设备" value="大车设备" />
                <el-option label="大理片笼" value="大理片笼" />
                <el-option label="卧转立扫码" value="卧转立扫码" />
                <el-option label="卧转立" value="卧转立" />
              </el-select>
            </el-form-item>
@@ -51,10 +52,7 @@
              <el-select v-model="deviceForm.plcType" placeholder="选择PLC类型" style="width: 100%;" clearable>
                <el-option label="西门子 S7-1200" value="S1200" />
                <el-option label="西门子 S7-1500" value="S1500" />
                <el-option label="西门子 S7-400" value="S400" />
                <el-option label="西门子 S7-300" value="S300" />
                <el-option label="西门子 S7-200" value="S200" />
                <el-option label="西门子 S7-200 SMART" value="S200_SMART" />
                <el-option label="Modbus 控制器" value="MODBUS" />
              </el-select>
            </el-form-item>
@@ -254,204 +252,19 @@
          <span class="form-tip">根据设备类型配置特定的业务逻辑参数</span>
        </template>
        <!-- 上大车设备逻辑配置 -->
        <div v-if="deviceForm.deviceType === '上大车'">
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="车辆容量">
                <el-input-number
                  v-model="deviceLogicParams.vehicleCapacity"
                  :min="1"
                  :max="10000"
                  :step="100"
                  style="width: 100%;"
        <!-- 使用动态组件加载对应设备类型的配置组件 -->
        <component
          :is="deviceConfigComponent"
          v-if="deviceConfigComponent"
          v-model="deviceLogicParams"
                />
                <span class="form-tip">车辆最大容量</span>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="玻璃间隔(ms)">
                <el-input-number
                  v-model="deviceLogicParams.glassIntervalMs"
                  :min="100"
                  :max="10000"
                  :step="100"
                  style="width: 100%;"
                />
                <span class="form-tip">玻璃上料间隔时间(毫秒)</span>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="默认玻璃长度(mm)">
                <el-input-number
                  v-model="deviceLogicParams.defaultGlassLength"
                  :min="100"
                  :max="10000"
                  :step="100"
                  style="width: 100%;"
                />
                <span class="form-tip">当玻璃未提供长度时使用的默认值</span>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="自动上料">
                <el-switch v-model="deviceLogicParams.autoFeed" />
                <span class="form-tip">是否自动触发上料请求</span>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="最大重试次数">
                <el-input-number
                  v-model="deviceLogicParams.maxRetryCount"
                  :min="0"
                  :max="10"
                  :step="1"
                  style="width: 100%;"
                />
              </el-form-item>
            </el-col>
          </el-row>
          <el-form-item label="位置映射">
            <div class="position-mapping">
              <div
                v-for="(value, key, index) in deviceLogicParams.positionMapping"
                :key="index"
                class="mapping-item"
              >
                <el-input
                  v-model="mappingKeys[index]"
                  placeholder="位置代码"
                  size="small"
                  style="width: 150px; margin-right: 10px;"
                  @input="updatePositionMapping(index, $event, value)"
                />
                <el-input-number
                  v-model="deviceLogicParams.positionMapping[mappingKeys[index] || key]"
                  :min="0"
                  :max="100"
                  size="small"
                  style="width: 120px; margin-right: 10px;"
                />
                <el-button
                  type="danger"
                  size="small"
                  @click="removePositionMapping(key)"
                >
                  删除
                </el-button>
              </div>
              <el-button type="primary" size="small" @click="addPositionMapping">
                添加位置映射
              </el-button>
            </div>
          </el-form-item>
        </div>
        <!-- 大理片设备逻辑配置 -->
        <div v-if="deviceForm.deviceType === '大理片'">
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="玻璃尺寸">
                <el-input-number
                  v-model="deviceLogicParams.glassSize"
                  :min="100"
                  :max="5000"
                  :step="100"
                  style="width: 100%;"
                />
                <span class="form-tip">玻璃尺寸(mm)</span>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="处理时间(ms)">
                <el-input-number
                  v-model="deviceLogicParams.processingTime"
                  :min="1000"
                  :max="60000"
                  :step="1000"
                  style="width: 100%;"
                />
                <span class="form-tip">玻璃处理时间(毫秒)</span>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="自动处理">
                <el-switch v-model="deviceLogicParams.autoProcess" />
                <span class="form-tip">是否自动触发处理请求</span>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="最大重试次数">
                <el-input-number
                  v-model="deviceLogicParams.maxRetryCount"
                  :min="0"
                  :max="10"
                  :step="1"
                  style="width: 100%;"
                />
              </el-form-item>
            </el-col>
          </el-row>
        </div>
        <!-- 玻璃存储设备逻辑配置 -->
        <div v-if="deviceForm.deviceType === '玻璃存储'">
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="存储容量">
                <el-input-number
                  v-model="deviceLogicParams.storageCapacity"
                  :min="1"
                  :max="1000"
                  :step="1"
                  style="width: 100%;"
                />
                <span class="form-tip">最大存储数量</span>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="取货模式">
                <el-select v-model="deviceLogicParams.retrievalMode" style="width: 100%;">
                  <el-option label="先进先出 (FIFO)" value="FIFO" />
                  <el-option label="后进先出 (LIFO)" value="LIFO" />
                  <el-option label="随机 (RANDOM)" value="RANDOM" />
                </el-select>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="自动存储">
                <el-switch v-model="deviceLogicParams.autoStore" />
                <span class="form-tip">是否自动触发存储请求</span>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="自动取货">
                <el-switch v-model="deviceLogicParams.autoRetrieve" />
                <span class="form-tip">是否自动触发取货请求</span>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="最大重试次数">
                <el-input-number
                  v-model="deviceLogicParams.maxRetryCount"
                  :min="0"
                  :max="10"
                  :step="1"
                  style="width: 100%;"
                />
              </el-form-item>
            </el-col>
          </el-row>
        <div v-else class="no-config-tip">
          <el-alert
            :title="`设备类型「${deviceForm.deviceType}」暂无配置组件`"
            type="info"
            :closable="false"
            show-icon
          />
        </div>
      </el-card>
@@ -510,6 +323,7 @@
import { ref, reactive, watch, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { deviceConfigApi } from '@/api/device/deviceManagement'
import { getDeviceConfigComponent } from './components/DeviceLogicConfig'
// Props定义
const props = defineProps({
@@ -534,27 +348,18 @@
const testResult = ref(null)
// 设备逻辑参数(根据设备类型动态显示)
const deviceLogicParams = reactive({
  // 上大车参数
  vehicleCapacity: 6000,
  glassIntervalMs: 1000,
  defaultGlassLength: 2000,
  autoFeed: true,
  maxRetryCount: 5,
  positionMapping: {},
  // 大理片参数
  glassSize: 2000,
  processingTime: 5000,
  autoProcess: true,
  // 玻璃存储参数
  storageCapacity: 100,
  retrievalMode: 'FIFO',
  autoStore: true,
  autoRetrieve: true
})
const deviceLogicParams = reactive({})
// 位置映射的键数组(用于v-for)
const mappingKeys = ref([])
const S7_PLC_TYPES = ['S1200', 'S1500']
const MODBUS_PLC_TYPES = ['MODBUS']
// 计算属性:根据设备类型获取对应的配置组件
const deviceConfigComponent = computed(() => {
  if (!deviceForm.deviceType) {
    return null
  }
  return getDeviceConfigComponent(deviceForm.deviceType)
})
// 设备表单数据
const getDefaultForm = () => ({
@@ -660,22 +465,37 @@
// 监听PLC类型变化,自动设置通讯协议
watch(() => deviceForm.plcType, (newPlcType) => {
  // 如果选择的是S7系列PLC,自动设置通讯协议为S7 Communication
  if (newPlcType && (newPlcType.startsWith('S') || newPlcType.includes('S7'))) {
    if (!deviceForm.protocolType || deviceForm.protocolType === '其他') {
  if (!newPlcType) {
    return
  }
  if (S7_PLC_TYPES.includes(newPlcType)) {
    if (!deviceForm.protocolType || deviceForm.protocolType === '其他' || deviceForm.protocolType === 'Modbus TCP') {
      deviceForm.protocolType = 'S7 Communication'
    }
    return
  }
  if (MODBUS_PLC_TYPES.includes(newPlcType)) {
    if (!deviceForm.protocolType || deviceForm.protocolType === '其他' || deviceForm.protocolType === 'S7 Communication') {
      deviceForm.protocolType = 'Modbus TCP'
    }
  }
})
// 处理通讯协议变化
const handleProtocolTypeChange = (value) => {
  // 如果选择了非S7协议,但PLC类型是S7系列,给出提示
  if (value && value !== 'S7 Communication' && deviceForm.plcType) {
    const s7Types = ['S1200', 'S1500', 'S400', 'S300', 'S200', 'S200_SMART']
    if (s7Types.includes(deviceForm.plcType)) {
      ElMessage.warning('S7系列PLC通常使用S7 Communication协议,请确认协议选择是否正确')
    }
  if (!deviceForm.plcType || !value) {
    return
  }
  if (value !== 'S7 Communication' && S7_PLC_TYPES.includes(deviceForm.plcType)) {
    ElMessage.warning('S7系列PLC通常使用S7 Communication协议,请确认协议选择是否正确')
    return
  }
  if (value !== 'Modbus TCP' && MODBUS_PLC_TYPES.includes(deviceForm.plcType)) {
    ElMessage.warning('Modbus 类型PLC通常使用 Modbus TCP 协议,请确认协议选择是否正确')
  }
}
@@ -824,72 +644,26 @@
// 加载设备逻辑参数
const loadDeviceLogicParams = (deviceLogic, deviceType) => {
  if (deviceType === '上大车') {
    deviceLogicParams.vehicleCapacity = deviceLogic.vehicleCapacity ?? 6000
    deviceLogicParams.glassIntervalMs = deviceLogic.glassIntervalMs ?? 1000
    deviceLogicParams.defaultGlassLength = deviceLogic.defaultGlassLength ?? 2000
    deviceLogicParams.autoFeed = deviceLogic.autoFeed ?? true
    deviceLogicParams.maxRetryCount = deviceLogic.maxRetryCount ?? 5
    deviceLogicParams.positionMapping = deviceLogic.positionMapping || {}
    mappingKeys.value = Object.keys(deviceLogicParams.positionMapping)
  } else if (deviceType === '大理片') {
    deviceLogicParams.glassSize = deviceLogic.glassSize ?? 2000
    deviceLogicParams.processingTime = deviceLogic.processingTime ?? 5000
    deviceLogicParams.autoProcess = deviceLogic.autoProcess ?? true
    deviceLogicParams.maxRetryCount = deviceLogic.maxRetryCount ?? 3
  } else if (deviceType === '玻璃存储') {
    deviceLogicParams.storageCapacity = deviceLogic.storageCapacity ?? 100
    deviceLogicParams.retrievalMode = deviceLogic.retrievalMode || 'FIFO'
    deviceLogicParams.autoStore = deviceLogic.autoStore ?? true
    deviceLogicParams.autoRetrieve = deviceLogic.autoRetrieve ?? true
    deviceLogicParams.maxRetryCount = deviceLogic.maxRetryCount ?? 3
  // 清空现有参数
  Object.keys(deviceLogicParams).forEach(key => {
    delete deviceLogicParams[key]
  })
  // 根据设备类型加载对应的参数
  if (deviceLogic && Object.keys(deviceLogic).length > 0) {
    Object.assign(deviceLogicParams, deviceLogic)
  }
}
// 位置映射相关方法
const addPositionMapping = () => {
  const newKey = `POS${Object.keys(deviceLogicParams.positionMapping).length + 1}`
  deviceLogicParams.positionMapping[newKey] = 1
  mappingKeys.value.push(newKey)
}
const removePositionMapping = (key) => {
  delete deviceLogicParams.positionMapping[key]
  mappingKeys.value = mappingKeys.value.filter(k => k !== key)
}
const updatePositionMapping = (index, newKey, oldValue) => {
  const oldKey = mappingKeys.value[index]
  if (oldKey && oldKey !== newKey) {
    delete deviceLogicParams.positionMapping[oldKey]
  }
  mappingKeys.value[index] = newKey
  if (newKey) {
    deviceLogicParams.positionMapping[newKey] = oldValue || 1
  }
}
const resetForm = () => {
  Object.assign(deviceForm, getDefaultForm())
  deviceFormRef.value?.clearValidate()
  
  // 重置设备逻辑参数
  deviceLogicParams.vehicleCapacity = 6000
  deviceLogicParams.glassIntervalMs = 1000
  deviceLogicParams.defaultGlassLength = 2000
  deviceLogicParams.autoFeed = true
  deviceLogicParams.maxRetryCount = 5
  deviceLogicParams.positionMapping = {}
  mappingKeys.value = []
  deviceLogicParams.glassSize = 2000
  deviceLogicParams.processingTime = 5000
  deviceLogicParams.autoProcess = true
  deviceLogicParams.storageCapacity = 100
  deviceLogicParams.retrievalMode = 'FIFO'
  deviceLogicParams.autoStore = true
  deviceLogicParams.autoRetrieve = true
  Object.keys(deviceLogicParams).forEach(key => {
    delete deviceLogicParams[key]
  })
}
const addConfigParam = () => {
@@ -963,30 +737,9 @@
    plcType: deviceForm.plcType
  }
    // 保存设备逻辑参数
    const deviceLogic = {}
    if (deviceForm.deviceType === '上大车') {
      deviceLogic.vehicleCapacity = deviceLogicParams.vehicleCapacity
      deviceLogic.glassIntervalMs = deviceLogicParams.glassIntervalMs
      deviceLogic.defaultGlassLength = deviceLogicParams.defaultGlassLength
      deviceLogic.autoFeed = deviceLogicParams.autoFeed
      deviceLogic.maxRetryCount = deviceLogicParams.maxRetryCount
      deviceLogic.positionMapping = deviceLogicParams.positionMapping
    } else if (deviceForm.deviceType === '大理片') {
      deviceLogic.glassSize = deviceLogicParams.glassSize
      deviceLogic.processingTime = deviceLogicParams.processingTime
      deviceLogic.autoProcess = deviceLogicParams.autoProcess
      deviceLogic.maxRetryCount = deviceLogicParams.maxRetryCount
    } else if (deviceForm.deviceType === '玻璃存储') {
      deviceLogic.storageCapacity = deviceLogicParams.storageCapacity
      deviceLogic.retrievalMode = deviceLogicParams.retrievalMode
      deviceLogic.autoStore = deviceLogicParams.autoStore
      deviceLogic.autoRetrieve = deviceLogicParams.autoRetrieve
      deviceLogic.maxRetryCount = deviceLogicParams.maxRetryCount
    }
    if (Object.keys(deviceLogic).length > 0) {
      extraObj.deviceLogic = deviceLogic
    // 保存设备逻辑参数(直接使用deviceLogicParams,由各个配置组件管理)
    if (deviceLogicParams && Object.keys(deviceLogicParams).length > 0) {
      extraObj.deviceLogic = { ...deviceLogicParams }
    }
    // 构建 configJson:将 configParams 数组转换为 JSON 字符串
@@ -1142,4 +895,8 @@
  border-radius: 6px;
  background-color: #fafafa;
}
.no-config-tip {
  padding: 20px;
}
</style>
mes-web/src/views/device/components/DeviceLogicConfig/LargeGlassConfig.vue
New file
@@ -0,0 +1,177 @@
<template>
  <div class="large-glass-config">
    <el-form-item label="格子范围配置">
      <div class="grid-ranges">
        <div
          v-for="(range, index) in config.gridRanges"
          :key="index"
          class="grid-range-item"
        >
          <el-input-number
            v-model="range.row"
            :min="1"
            :max="100"
            :step="1"
            style="width: 100px; margin-right: 10px;"
            placeholder="行号"
          />
          <span>行:</span>
          <el-input-number
            v-model="range.start"
            :min="1"
            :max="1000"
            :step="1"
            style="width: 120px; margin: 0 10px;"
            placeholder="起始格子"
          />
          <span>~</span>
          <el-input-number
            v-model="range.end"
            :min="1"
            :max="1000"
            :step="1"
            style="width: 120px; margin-left: 10px;"
            placeholder="结束格子"
          />
          <el-button
            type="danger"
            size="small"
            style="margin-left: 10px;"
            @click="removeGridRange(index)"
          >
            删除
          </el-button>
        </div>
        <el-button type="primary" size="small" @click="addGridRange">
          添加格子范围
        </el-button>
      </div>
      <span class="form-tip">配置每行的格子范围,例如:第一行1~52格,第二行53~101格</span>
    </el-form-item>
    <el-row :gutter="20">
      <el-col :span="8">
        <el-form-item label="每格长度(mm)">
          <el-input-number
            v-model="config.gridLength"
            :min="100"
            :max="10000"
            :step="100"
            style="width: 100%;"
          />
          <span class="form-tip">每格长度(毫米)</span>
        </el-form-item>
      </el-col>
      <el-col :span="8">
        <el-form-item label="每格宽度(mm)">
          <el-input-number
            v-model="config.gridWidth"
            :min="100"
            :max="10000"
            :step="100"
            style="width: 100%;"
          />
          <span class="form-tip">每格宽度(毫米)</span>
        </el-form-item>
      </el-col>
      <el-col :span="8">
        <el-form-item label="每格厚度(mm)">
          <el-input-number
            v-model="config.gridThickness"
            :min="1"
            :max="100"
            :step="1"
            style="width: 100%;"
          />
          <span class="form-tip">每格厚度(毫米)</span>
        </el-form-item>
      </el-col>
    </el-row>
  </div>
</template>
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
  modelValue: {
    type: Object,
    default: () => ({})
  }
})
const emit = defineEmits(['update:modelValue'])
// 配置数据
const config = ref({
  gridRanges: [
    { row: 1, start: 1, end: 52 },
    { row: 2, start: 53, end: 101 }
  ],
  gridLength: 2000,
  gridWidth: 1500,
  gridThickness: 5
})
// 监听props变化
watch(() => props.modelValue, (newVal) => {
  if (newVal && Object.keys(newVal).length > 0) {
    config.value = {
      gridRanges: newVal.gridRanges || [
        { row: 1, start: 1, end: 52 },
        { row: 2, start: 53, end: 101 }
      ],
      gridLength: newVal.gridLength ?? 2000,
      gridWidth: newVal.gridWidth ?? 1500,
      gridThickness: newVal.gridThickness ?? 5
    }
  }
}, { immediate: true, deep: true })
// 监听config变化,同步到父组件
watch(config, (newVal) => {
  emit('update:modelValue', { ...newVal })
}, { deep: true })
// 格子范围相关方法
const addGridRange = () => {
  const maxRow = config.value.gridRanges.length > 0
    ? Math.max(...config.value.gridRanges.map(r => r.row))
    : 0
  const lastEnd = config.value.gridRanges.length > 0
    ? Math.max(...config.value.gridRanges.map(r => r.end))
    : 0
  config.value.gridRanges.push({
    row: maxRow + 1,
    start: lastEnd + 1,
    end: lastEnd + 50
  })
}
const removeGridRange = (index) => {
  config.value.gridRanges.splice(index, 1)
}
</script>
<style scoped>
.form-tip {
  margin-left: 10px;
  font-size: 12px;
  color: #909399;
}
.grid-ranges {
  width: 100%;
}
.grid-range-item {
  display: flex;
  align-items: center;
  margin-bottom: 12px;
  padding: 12px;
  border: 1px solid #ebeef5;
  border-radius: 6px;
  background-color: #fafafa;
}
</style>
mes-web/src/views/device/components/DeviceLogicConfig/LoadVehicleConfig.vue
New file
@@ -0,0 +1,324 @@
<template>
  <div class="load-vehicle-config">
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="车辆容量(mm)">
          <el-input-number
            v-model="config.vehicleCapacity"
            :min="1"
            :max="10000"
            :step="100"
            style="width: 100%;"
          />
          <span class="form-tip">车辆最大容量</span>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="车辆速度(格/秒)">
          <el-input-number
            v-model="config.vehicleSpeed"
            :min="0.1"
            :max="10"
            :step="0.1"
            :precision="1"
            style="width: 100%;"
          />
          <span class="form-tip">车辆运动速度,默认1格/秒</span>
        </el-form-item>
      </el-col>
    </el-row>
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="玻璃间隔(ms)">
          <el-input-number
            v-model="config.glassIntervalMs"
            :min="100"
            :max="10000"
            :step="100"
            style="width: 100%;"
          />
          <span class="form-tip">玻璃上料间隔时间(毫秒)</span>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="默认玻璃长度(mm)">
          <el-input-number
            v-model="config.defaultGlassLength"
            :min="100"
            :max="10000"
            :step="100"
            style="width: 100%;"
          />
          <span class="form-tip">当玻璃未提供长度时使用的默认值</span>
        </el-form-item>
      </el-col>
    </el-row>
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="初始位置(格)">
          <el-input-number
            v-model="config.homePosition"
            :min="0"
            :max="1000"
            :step="1"
            style="width: 100%;"
          />
          <span class="form-tip">车辆初始位置(格子)</span>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="运动距离范围">
          <el-input-number
            v-model="config.minRange"
            :min="1"
            :max="1000"
            :step="1"
            style="width: 48%;"
            placeholder="最小"
          />
          <span style="margin: 0 2%;">~</span>
          <el-input-number
            v-model="config.maxRange"
            :min="1"
            :max="1000"
            :step="1"
            style="width: 48%;"
            placeholder="最大"
          />
          <span class="form-tip">运动距离范围(格子)</span>
        </el-form-item>
      </el-col>
    </el-row>
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="空闲监控间隔(ms)">
          <el-input-number
            v-model="config.idleMonitorIntervalMs"
            :min="500"
            :max="10000"
            :step="100"
            style="width: 100%;"
          />
          <span class="form-tip">空闲状态监控间隔,默认2000ms</span>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="任务监控间隔(ms)">
          <el-input-number
            v-model="config.taskMonitorIntervalMs"
            :min="500"
            :max="10000"
            :step="100"
            style="width: 100%;"
          />
          <span class="form-tip">任务执行监控间隔,默认1000ms</span>
        </el-form-item>
      </el-col>
    </el-row>
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="MES确认超时(ms)">
          <el-input-number
            v-model="config.mesConfirmTimeoutMs"
            :min="5000"
            :max="300000"
            :step="1000"
            style="width: 100%;"
          />
          <span class="form-tip">等待MES确认的超时时间,默认30000ms</span>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="自动上料">
          <el-switch v-model="config.autoFeed" />
          <span class="form-tip">是否自动触发上料请求</span>
        </el-form-item>
      </el-col>
    </el-row>
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="最大重试次数">
          <el-input-number
            v-model="config.maxRetryCount"
            :min="0"
            :max="10"
            :step="1"
            style="width: 100%;"
          />
        </el-form-item>
      </el-col>
    </el-row>
    <el-form-item label="位置映射">
      <div class="position-mapping">
        <div
          v-for="(value, key, index) in config.positionMapping"
          :key="index"
          class="mapping-item"
        >
          <el-input
            v-model="mappingKeys[index]"
            placeholder="位置代码(如900/901)"
            size="small"
            style="width: 150px; margin-right: 10px;"
            @input="updatePositionMapping(index, $event, value)"
          />
          <el-input-number
            v-model="config.positionMapping[mappingKeys[index] || key]"
            :min="0"
            :max="1000"
            :step="1"
            size="small"
            style="width: 120px; margin-right: 10px;"
            placeholder="位置值(格)"
          />
          <el-button
            type="danger"
            size="small"
            @click="removePositionMapping(key)"
          >
            删除
          </el-button>
        </div>
        <el-button type="primary" size="small" @click="addPositionMapping">
          添加位置映射
        </el-button>
      </div>
      <span class="form-tip">将MES编号(如900/901)映射为实际位置值(格子)</span>
    </el-form-item>
    <el-form-item label="出片任务格子范围">
      <el-input-number
        v-model="config.outboundSlotRanges[0]"
        :min="1"
        :max="1000"
        :step="1"
        style="width: 48%;"
        placeholder="最小格子编号"
      />
      <span style="margin: 0 2%;">~</span>
      <el-input-number
        v-model="config.outboundSlotRanges[1]"
        :min="1"
        :max="1000"
        :step="1"
        style="width: 48%;"
        placeholder="最大格子编号"
      />
      <span class="form-tip">出片任务的startSlot范围,例如[1, 101]表示格子1~101都是出片任务</span>
    </el-form-item>
  </div>
</template>
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
  modelValue: {
    type: Object,
    default: () => ({})
  }
})
const emit = defineEmits(['update:modelValue'])
// 配置数据
const config = ref({
  vehicleCapacity: 6000,
  vehicleSpeed: 1.0,
  glassIntervalMs: 1000,
  defaultGlassLength: 2000,
  homePosition: 0,
  minRange: 1,
  maxRange: 100,
  idleMonitorIntervalMs: 2000,
  taskMonitorIntervalMs: 1000,
  mesConfirmTimeoutMs: 30000,
  autoFeed: true,
  maxRetryCount: 5,
  positionMapping: {},
  outboundSlotRanges: [1, 101]
})
// 位置映射的键数组
const mappingKeys = ref([])
// 监听props变化
watch(() => props.modelValue, (newVal) => {
  if (newVal && Object.keys(newVal).length > 0) {
    config.value = {
      vehicleCapacity: newVal.vehicleCapacity ?? 6000,
      vehicleSpeed: newVal.vehicleSpeed ?? 1.0,
      glassIntervalMs: newVal.glassIntervalMs ?? 1000,
      defaultGlassLength: newVal.defaultGlassLength ?? 2000,
      homePosition: newVal.homePosition ?? 0,
      minRange: newVal.minRange ?? 1,
      maxRange: newVal.maxRange ?? 100,
      idleMonitorIntervalMs: newVal.idleMonitorIntervalMs ?? 2000,
      taskMonitorIntervalMs: newVal.taskMonitorIntervalMs ?? 1000,
      mesConfirmTimeoutMs: newVal.mesConfirmTimeoutMs ?? 30000,
      autoFeed: newVal.autoFeed ?? true,
      maxRetryCount: newVal.maxRetryCount ?? 5,
      positionMapping: newVal.positionMapping || {},
      outboundSlotRanges: newVal.outboundSlotRanges || [1, 101]
    }
    mappingKeys.value = Object.keys(config.value.positionMapping)
  }
}, { immediate: true, deep: true })
// 监听config变化,同步到父组件
watch(config, (newVal) => {
  emit('update:modelValue', { ...newVal })
}, { deep: true })
// 位置映射相关方法
const addPositionMapping = () => {
  const newKey = `POS${Object.keys(config.value.positionMapping).length + 1}`
  config.value.positionMapping[newKey] = 1
  mappingKeys.value.push(newKey)
}
const removePositionMapping = (key) => {
  delete config.value.positionMapping[key]
  mappingKeys.value = mappingKeys.value.filter(k => k !== key)
}
const updatePositionMapping = (index, newKey, oldValue) => {
  const oldKey = mappingKeys.value[index]
  if (oldKey && oldKey !== newKey) {
    delete config.value.positionMapping[oldKey]
  }
  mappingKeys.value[index] = newKey
  if (newKey) {
    config.value.positionMapping[newKey] = oldValue || 1
  }
}
</script>
<style scoped>
.form-tip {
  margin-left: 10px;
  font-size: 12px;
  color: #909399;
}
.position-mapping {
  width: 100%;
}
.mapping-item {
  display: flex;
  align-items: center;
  margin-bottom: 12px;
  padding: 12px;
  border: 1px solid #ebeef5;
  border-radius: 6px;
  background-color: #fafafa;
}
</style>
mes-web/src/views/device/components/DeviceLogicConfig/WorkstationScannerConfig.vue
New file
@@ -0,0 +1,84 @@
<template>
  <div class="workstation-scanner-config">
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="扫码间隔(ms)">
          <el-input-number
            v-model="config.scanIntervalMs"
            :min="1000"
            :max="60000"
            :step="1000"
            style="width: 100%;"
          />
          <span class="form-tip">定时扫描MES写区的时间间隔,默认10000ms(10秒)</span>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="产线编号">
          <el-input-number
            v-model="config.workLine"
            :min="1"
            :max="100"
            :step="1"
            style="width: 100%;"
          />
          <span class="form-tip">产线编号,用于过滤玻璃信息</span>
        </el-form-item>
      </el-col>
    </el-row>
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="自动确认">
          <el-switch v-model="config.autoAck" />
          <span class="form-tip">是否自动确认MES发送的玻璃信息(回写mesSend=0)</span>
        </el-form-item>
      </el-col>
    </el-row>
  </div>
</template>
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
  modelValue: {
    type: Object,
    default: () => ({})
  }
})
const emit = defineEmits(['update:modelValue'])
// 配置数据
const config = ref({
  scanIntervalMs: 10000,
  workLine: null,
  autoAck: true
})
// 监听props变化
watch(() => props.modelValue, (newVal) => {
  if (newVal && Object.keys(newVal).length > 0) {
    config.value = {
      scanIntervalMs: newVal.scanIntervalMs ?? 10000,
      workLine: newVal.workLine ?? null,
      autoAck: newVal.autoAck ?? true
    }
  }
}, { immediate: true, deep: true })
// 监听config变化,同步到父组件
watch(config, (newVal) => {
  emit('update:modelValue', { ...newVal })
}, { deep: true })
</script>
<style scoped>
.form-tip {
  margin-left: 10px;
  font-size: 12px;
  color: #909399;
}
</style>
mes-web/src/views/device/components/DeviceLogicConfig/WorkstationTransferConfig.vue
New file
@@ -0,0 +1,146 @@
<template>
  <div class="workstation-transfer-config">
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="扫码间隔(ms)">
          <el-input-number
            v-model="config.scanIntervalMs"
            :min="1000"
            :max="60000"
            :step="1000"
            style="width: 100%;"
          />
          <span class="form-tip">定时查询最近扫码玻璃的时间间隔,默认10000ms(10秒)</span>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="缓冲判定时间(ms)">
          <el-input-number
            v-model="config.transferDelayMs"
            :min="5000"
            :max="120000"
            :step="1000"
            style="width: 100%;"
          />
          <span class="form-tip">30秒内无新玻璃扫码则判定为最后一片,默认30000ms(30秒)</span>
        </el-form-item>
      </el-col>
    </el-row>
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="车辆容量(mm)">
          <el-input-number
            v-model="config.vehicleCapacity"
            :min="1000"
            :max="20000"
            :step="100"
            style="width: 100%;"
          />
          <span class="form-tip">可装载的最大宽度(毫米),默认6000mm</span>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="监控间隔(ms)">
          <el-input-number
            v-model="config.monitorIntervalMs"
            :min="1000"
            :max="60000"
            :step="1000"
            style="width: 100%;"
          />
          <span class="form-tip">批次处理监控间隔,默认使用scanIntervalMs</span>
        </el-form-item>
      </el-col>
    </el-row>
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="产线编号">
          <el-input-number
            v-model="config.workLine"
            :min="1"
            :max="100"
            :step="1"
            style="width: 100%;"
          />
          <span class="form-tip">产线编号,用于过滤玻璃信息</span>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="位置值(格)">
          <el-input-number
            v-model="config.inPosition"
            :min="0"
            :max="1000"
            :step="1"
            style="width: 100%;"
          />
          <span class="form-tip">写入PLC的inPosition值(格子)</span>
        </el-form-item>
      </el-col>
    </el-row>
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="自动确认">
          <el-switch v-model="config.autoAck" />
          <span class="form-tip">是否自动确认MES发送的玻璃信息</span>
        </el-form-item>
      </el-col>
    </el-row>
  </div>
</template>
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
  modelValue: {
    type: Object,
    default: () => ({})
  }
})
const emit = defineEmits(['update:modelValue'])
// 配置数据
const config = ref({
  scanIntervalMs: 10000,
  transferDelayMs: 30000,
  vehicleCapacity: 6000,
  monitorIntervalMs: 10000,
  workLine: null,
  inPosition: null,
  autoAck: true
})
// 监听props变化
watch(() => props.modelValue, (newVal) => {
  if (newVal && Object.keys(newVal).length > 0) {
    config.value = {
      scanIntervalMs: newVal.scanIntervalMs ?? 10000,
      transferDelayMs: newVal.transferDelayMs ?? 30000,
      vehicleCapacity: newVal.vehicleCapacity ?? 6000,
      monitorIntervalMs: newVal.monitorIntervalMs ?? newVal.scanIntervalMs ?? 10000,
      workLine: newVal.workLine ?? null,
      inPosition: newVal.inPosition ?? null,
      autoAck: newVal.autoAck ?? true
    }
  }
}, { immediate: true, deep: true })
// 监听config变化,同步到父组件
watch(config, (newVal) => {
  emit('update:modelValue', { ...newVal })
}, { deep: true })
</script>
<style scoped>
.form-tip {
  margin-left: 10px;
  font-size: 12px;
  color: #909399;
}
</style>
mes-web/src/views/device/components/DeviceLogicConfig/index.js
New file
@@ -0,0 +1,34 @@
/**
 * 设备逻辑配置组件导出
 * 统一管理所有设备类型的配置组件
 */
import LoadVehicleConfig from './LoadVehicleConfig.vue'
import LargeGlassConfig from './LargeGlassConfig.vue'
import WorkstationScannerConfig from './WorkstationScannerConfig.vue'
import WorkstationTransferConfig from './WorkstationTransferConfig.vue'
// 设备类型到组件的映射
export const deviceTypeComponentMap = {
  '大车设备': LoadVehicleConfig,
  '大理片笼': LargeGlassConfig,
  '卧转立扫码': WorkstationScannerConfig,
  '卧转立': WorkstationTransferConfig,
  // 兼容旧名称
  '上大车': LoadVehicleConfig,
  '大理片': LargeGlassConfig
}
// 导出所有组件
export {
  LoadVehicleConfig,
  LargeGlassConfig,
  WorkstationScannerConfig,
  WorkstationTransferConfig
}
// 根据设备类型获取对应的配置组件
export function getDeviceConfigComponent(deviceType) {
  return deviceTypeComponentMap[deviceType] || null
}
mes-web/src/views/plcTest/MultiDeviceWorkbench.vue
@@ -3,23 +3,56 @@
    <div class="main-grid">
      <div class="left-panel">
        <GroupList @select="handleGroupSelect" />
        <GroupTopology
          v-if="selectedGroup"
          :group="selectedGroup"
          class="topology-panel"
        />
      </div>
      <div class="right-panel">
        <TaskOrchestration :group="selectedGroup" @task-started="refreshMonitor" />
        <ExecutionMonitor ref="monitorRef" :group-id="selectedGroupId" class="monitor-panel" />
        <el-tabs v-model="activeTab" type="card" class="workbench-tabs">
          <el-tab-pane label="任务编排" name="orchestration">
            <TaskOrchestration
              :group="selectedGroup"
              @task-started="handleTaskStarted"
            />
          </el-tab-pane>
          <el-tab-pane label="执行监控" name="monitor">
            <ExecutionMonitor
              ref="monitorRef"
              :group-id="selectedGroupId"
              :task-id="selectedTaskId"
              class="monitor-panel"
              @task-selected="handleTaskSelected"
            />
          </el-tab-pane>
          <el-tab-pane label="结果分析" name="analysis">
            <ResultAnalysis
              ref="analysisRef"
              :task="selectedTask"
              class="analysis-panel"
            />
          </el-tab-pane>
        </el-tabs>
      </div>
    </div>
  </div>
</template>
<script setup>
import { computed, ref } from 'vue'
import { computed, ref, watch } from 'vue'
import GroupList from './components/DeviceGroup/GroupList.vue'
import GroupTopology from './components/DeviceGroup/GroupTopology.vue'
import TaskOrchestration from './components/MultiDeviceTest/TaskOrchestration.vue'
import ExecutionMonitor from './components/MultiDeviceTest/ExecutionMonitor.vue'
import ResultAnalysis from './components/MultiDeviceTest/ResultAnalysis.vue'
const selectedGroup = ref(null)
const monitorRef = ref(null)
const analysisRef = ref(null)
const activeTab = ref('orchestration')
const selectedTaskId = ref(null)
const selectedTask = ref(null)
const selectedGroupId = computed(() => {
  if (!selectedGroup.value) return null
@@ -28,10 +61,39 @@
const handleGroupSelect = (group) => {
  selectedGroup.value = group
  selectedTask.value = null
  selectedTaskId.value = null
  // 切换到编排标签页
  activeTab.value = 'orchestration'
}
const refreshMonitor = () => {
  monitorRef.value?.fetchTasks?.()
const handleTaskStarted = (task) => {
  // 任务启动后,切换到监控标签页(如果当前不在监控页)
  if (activeTab.value !== 'monitor') {
    activeTab.value = 'monitor'
  }
  // 立即刷新监控列表,显示新启动的任务
  setTimeout(() => {
    monitorRef.value?.fetchTasks?.()
  }, 300)
  // 如果传入了任务信息,可以自动选中
  if (task && task.taskId) {
    selectedTaskId.value = task.taskId
  }
}
const handleTaskSelected = (task) => {
  selectedTask.value = task
  selectedTaskId.value = task?.taskId || null
  // 如果任务已完成或失败,切换到结果分析标签页
  if (task && (task.status === 'COMPLETED' || task.status === 'FAILED')) {
    activeTab.value = 'analysis'
    // 刷新分析数据
    setTimeout(() => {
      analysisRef.value?.fetchSteps?.()
    }, 100)
  }
}
</script>
@@ -48,14 +110,43 @@
  gap: 24px;
}
.right-panel {
.left-panel {
  display: flex;
  flex-direction: column;
  gap: 24px;
}
.monitor-panel {
.topology-panel {
  flex: 1;
  min-height: 300px;
}
.right-panel {
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.workbench-tabs {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.workbench-tabs :deep(.el-tabs__content) {
  flex: 1;
  overflow: auto;
}
.workbench-tabs :deep(.el-tab-pane) {
  height: 100%;
}
.monitor-panel,
.analysis-panel {
  flex: 1;
  min-height: 500px;
}
@media (max-width: 1200px) {
mes-web/src/views/plcTest/components/DeviceGroup/GroupTopology.vue
New file
@@ -0,0 +1,478 @@
<template>
  <div class="group-topology">
    <div class="panel-header">
      <div>
        <h3>设备组拓扑图</h3>
        <p v-if="group">{{ group.groupName }} - 设备执行流程可视化</p>
        <p v-else class="warning">请先选择一个设备组</p>
      </div>
      <div class="action-buttons">
        <el-button :loading="loading" @click="handleRefresh">
          <el-icon><Refresh /></el-icon>
          刷新
        </el-button>
        <el-button @click="toggleLayout">
          <el-icon><Grid /></el-icon>
          {{ layoutMode === 'horizontal' ? '垂直布局' : '水平布局' }}
        </el-button>
      </div>
    </div>
    <div v-if="!group" class="empty-state">
      <el-empty description="请选择设备组查看拓扑图" />
    </div>
    <div v-else class="topology-container" :class="`layout-${layoutMode}`">
      <template v-for="(device, index) in devices" :key="device.id || device.deviceId">
        <div
          class="topology-node-wrapper"
          :class="`layout-${layoutMode}`"
        >
          <div
            class="topology-node"
            :class="getDeviceTypeClass(device.deviceType)"
            @click="handleNodeClick(device)"
            :title="`点击查看设备详情 | 执行顺序: ${index + 1}`"
          >
            <div class="node-content">
              <div class="node-icon">
                <el-icon :size="24">
                  <component :is="getDeviceIcon(device.deviceType)" />
                </el-icon>
              </div>
              <div class="node-info">
                <div class="node-name">{{ device.deviceName || device.deviceCode }}</div>
                <div class="node-type">{{ getDeviceTypeLabel(device.deviceType) }}</div>
                <div class="node-status">
                  <el-tag :type="getStatusType(device.status)" size="small">
                    {{ getStatusLabel(device.status) }}
                  </el-tag>
                </div>
              </div>
            </div>
            <!-- 执行顺序标识:右上角的数字圆圈 -->
            <div class="node-order" :title="`执行顺序: 第 ${index + 1} 步`">
              {{ index + 1 }}
            </div>
          </div>
          <!-- 流程方向箭头:表示设备执行顺序和数据流向 -->
          <div
            v-if="index < devices.length - 1"
            class="node-arrow"
            :title="`数据流向: ${device.deviceName || device.deviceCode} → ${devices[index + 1]?.deviceName || devices[index + 1]?.deviceCode}`"
          >
            <el-icon :size="20">
              <ArrowRight v-if="layoutMode === 'horizontal'" />
              <ArrowDown v-else />
            </el-icon>
          </div>
        </div>
      </template>
    </div>
    <!-- 设备详情卡片 -->
    <el-card v-if="selectedDevice" class="device-detail-card" shadow="never">
      <template #header>
        <div class="card-header">
          <span>设备详情</span>
          <el-button link @click="selectedDevice = null">
            <el-icon><Close /></el-icon>
          </el-button>
        </div>
      </template>
      <el-descriptions :column="2" border>
        <el-descriptions-item label="设备名称">
          {{ selectedDevice.deviceName }}
        </el-descriptions-item>
        <el-descriptions-item label="设备编码">
          {{ selectedDevice.deviceCode }}
        </el-descriptions-item>
        <el-descriptions-item label="设备类型">
          {{ getDeviceTypeLabel(selectedDevice.deviceType) }}
        </el-descriptions-item>
        <el-descriptions-item label="状态">
          <el-tag :type="getStatusType(selectedDevice.status)">
            {{ getStatusLabel(selectedDevice.status) }}
          </el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="PLC IP" v-if="selectedDevice.plcIp">
          {{ selectedDevice.plcIp }}
        </el-descriptions-item>
        <el-descriptions-item label="PLC类型" v-if="selectedDevice.plcType">
          {{ selectedDevice.plcType }}
        </el-descriptions-item>
        <el-descriptions-item label="模块名称" v-if="selectedDevice.moduleName">
          {{ selectedDevice.moduleName }}
        </el-descriptions-item>
        <el-descriptions-item label="是否启用">
          <el-tag :type="selectedDevice.enabled ? 'success' : 'info'">
            {{ selectedDevice.enabled ? '启用' : '停用' }}
          </el-tag>
        </el-descriptions-item>
      </el-descriptions>
    </el-card>
  </div>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import {
  Refresh,
  Grid,
  ArrowRight,
  ArrowDown,
  Close,
  Files,
  Box,
  Folder
} from '@element-plus/icons-vue'
import { deviceGroupApi } from '@/api/device/deviceManagement'
const props = defineProps({
  group: {
    type: Object,
    default: null
  }
})
const loading = ref(false)
const devices = ref([])
const layoutMode = ref('horizontal') // 'horizontal' | 'vertical'
const selectedDevice = ref(null)
const fetchDevices = async () => {
  if (!props.group) {
    devices.value = []
    return
  }
  const groupId = props.group.id || props.group.groupId
  if (!groupId) {
    devices.value = []
    return
  }
  try {
    loading.value = true
    const response = await deviceGroupApi.getGroupDevices(groupId)
    const rawList = response?.data
    const deviceList = Array.isArray(rawList)
      ? rawList
      : Array.isArray(rawList?.records)
      ? rawList.records
      : Array.isArray(rawList?.data)
      ? rawList.data
      : []
    // 按执行顺序排序
    devices.value = deviceList.sort((a, b) => {
      const orderA = a.executionOrder || a.order || 0
      const orderB = b.executionOrder || b.order || 0
      return orderA - orderB
    })
  } catch (error) {
    ElMessage.error(error?.message || '加载设备列表失败')
    devices.value = []
  } finally {
    loading.value = false
  }
}
const handleRefresh = () => {
  fetchDevices()
}
const toggleLayout = () => {
  layoutMode.value = layoutMode.value === 'horizontal' ? 'vertical' : 'horizontal'
}
const getDeviceTypeClass = (deviceType) => {
  if (!deviceType) return 'type-unknown'
  const type = deviceType.toUpperCase()
  if (type.includes('VEHICLE') || type.includes('大车')) return 'type-vehicle'
  if (type.includes('GLASS') || type.includes('大理片')) return 'type-glass'
  if (type.includes('STORAGE') || type.includes('存储')) return 'type-storage'
  return 'type-unknown'
}
const getDeviceIcon = (deviceType) => {
  if (!deviceType) return Box
  const type = deviceType.toUpperCase()
  if (type.includes('VEHICLE') || type.includes('大车')) return Files
  if (type.includes('GLASS') || type.includes('大理片')) return Box
  if (type.includes('STORAGE') || type.includes('存储')) return Folder
  return Box
}
const getDeviceTypeLabel = (deviceType) => {
  if (!deviceType) return '未知设备'
  const type = deviceType.toUpperCase()
  if (type.includes('VEHICLE') || type.includes('大车')) return '上大车设备'
  if (type.includes('GLASS') || type.includes('大理片')) return '大理片设备'
  if (type.includes('STORAGE') || type.includes('存储')) return '玻璃存储设备'
  return deviceType
}
const getStatusType = (status) => {
  if (!status) return 'info'
  const s = String(status).toUpperCase()
  if (s === '1' || s === '启用' || s === 'ENABLED' || s === 'ONLINE') return 'success'
  if (s === '0' || s === '停用' || s === 'DISABLED' || s === 'OFFLINE') return 'danger'
  if (s === '2' || s === '维护' || s === 'MAINTENANCE') return 'warning'
  return 'info'
}
const getStatusLabel = (status) => {
  if (!status) return '未知'
  const s = String(status).toUpperCase()
  if (s === '1' || s === '启用' || s === 'ENABLED' || s === 'ONLINE') return '在线'
  if (s === '0' || s === '停用' || s === 'DISABLED' || s === 'OFFLINE') return '离线'
  if (s === '2' || s === '维护' || s === 'MAINTENANCE') return '维护中'
  return String(status)
}
watch(
  () => props.group,
  () => {
    fetchDevices()
    selectedDevice.value = null
  },
  { immediate: true }
)
// 点击节点选择设备
const handleNodeClick = (device) => {
  selectedDevice.value = device
}
defineExpose({
  fetchDevices
})
</script>
<style scoped>
.group-topology {
  background: #fff;
  border-radius: 12px;
  padding: 20px;
  box-shadow: 0 8px 32px rgba(15, 18, 63, 0.08);
}
.panel-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.panel-header h3 {
  margin: 0;
}
.panel-header p {
  margin: 4px 0 0;
  color: #909399;
  font-size: 13px;
}
.panel-header .warning {
  color: #f56c6c;
}
.action-buttons {
  display: flex;
  gap: 12px;
}
.empty-state {
  padding: 60px 0;
}
.topology-container {
  display: flex;
  align-items: center;
  padding: 20px 0;
  min-height: 200px;
  overflow-x: auto;
  /* 平滑滚动 */
  scroll-behavior: smooth;
}
.topology-container.layout-horizontal {
  flex-direction: row;
  justify-content: flex-start;
  align-items: center;
  /* 添加左右内边距,确保第一个和最后一个节点完全可见 */
  padding-left: 20px;
  padding-right: 20px;
}
.topology-container.layout-vertical {
  flex-direction: column;
  align-items: center;
}
/* 节点包装器:水平布局时横向排列,垂直布局时纵向排列 */
.topology-node-wrapper {
  display: flex;
  align-items: center;
}
.topology-node-wrapper.layout-horizontal {
  flex-direction: row;
  align-items: center;
}
.topology-node-wrapper.layout-vertical {
  flex-direction: column;
  align-items: center;
}
.topology-node {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  cursor: pointer;
  transition: transform 0.2s;
}
.topology-node:hover {
  transform: translateY(-4px);
}
.node-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px;
  background: #fff;
  border-radius: 12px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  min-width: 160px;
  transition: all 0.3s;
}
.topology-node:hover .node-content {
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.node-icon {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 12px;
  color: #fff;
}
.type-vehicle .node-icon {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.type-glass .node-icon {
  background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.type-storage .node-icon {
  background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.type-unknown .node-icon {
  background: linear-gradient(135deg, #909399 0%, #b1b3b8 100%);
}
.node-info {
  text-align: center;
  width: 100%;
}
.node-name {
  font-size: 16px;
  font-weight: 600;
  color: #303133;
  margin-bottom: 4px;
  word-break: break-all;
}
.node-type {
  font-size: 12px;
  color: #909399;
  margin-bottom: 8px;
}
.node-status {
  display: flex;
  justify-content: center;
}
.node-order {
  position: absolute;
  top: -8px;
  right: -8px;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: #409eff;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 600;
  box-shadow: 0 2px 8px rgba(64, 158, 255, 0.4);
}
.node-arrow {
  color: #c0c4cc;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* 水平布局:箭头在节点右侧 */
.topology-node-wrapper.layout-horizontal .node-arrow {
  margin-left: 20px;
  margin-right: 20px;
}
/* 垂直布局:箭头在节点下方 */
.topology-node-wrapper.layout-vertical .node-arrow {
  margin-top: 15px;
  margin-bottom: 15px;
}
.device-detail-card {
  margin-top: 20px;
}
.device-detail-card .card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
@media (max-width: 768px) {
  .panel-header {
    flex-direction: column;
    align-items: flex-start;
    gap: 12px;
  }
  .action-buttons {
    width: 100%;
    flex-wrap: wrap;
  }
  .topology-container.layout-horizontal {
    flex-direction: column;
    gap: 30px;
  }
  .node-arrow {
    transform: rotate(90deg);
  }
}
</style>
mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue
@@ -4,11 +4,34 @@
      <div>
        <h3>任务执行监控</h3>
        <p>实时查看最新的多设备任务</p>
        <p v-if="sseConnected" class="sse-status connected">
          <el-icon><Connection /></el-icon>
          实时监控已连接
        </p>
        <p v-else class="sse-status disconnected">
          <el-icon><Close /></el-icon>
          实时监控未连接
        </p>
      </div>
      <el-button :loading="loading" @click="fetchTasks">
        <el-icon><Refresh /></el-icon>
        刷新
      </el-button>
      <div class="action-buttons">
        <el-button :loading="loading" @click="fetchTasks">
          <el-icon><Refresh /></el-icon>
          刷新
        </el-button>
        <el-button
          v-if="!sseConnected"
          type="success"
          @click="connectSSE"
          :loading="sseConnecting"
        >
          <el-icon><VideoPlay /></el-icon>
          开启实时监控
        </el-button>
        <el-button v-else type="danger" @click="disconnectSSE">
          <el-icon><VideoPause /></el-icon>
          关闭实时监控
        </el-button>
      </div>
    </div>
    <el-table
@@ -17,17 +40,29 @@
      height="300"
      stripe
      @row-click="handleRowClick"
      row-key="taskId"
    >
      <el-table-column prop="taskId" label="任务编号" min-width="160" />
      <el-table-column prop="groupId" label="设备组ID" width="120" />
      <el-table-column prop="status" label="状态" width="120">
        <template #default="{ row }">
          <el-tag :type="statusType(row.status)">{{ row.status }}</el-tag>
          <el-tag :type="statusType(row.status)">
            {{ getStatusLabel(row.status) }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="currentStep" label="进度" width="120">
      <el-table-column prop="currentStep" label="进度" width="140">
        <template #default="{ row }">
          {{ row.currentStep || 0 }} / {{ row.totalSteps || 0 }}
          <div class="progress-cell">
            <span>{{ row.currentStep || 0 }} / {{ row.totalSteps || 0 }}</span>
            <el-progress
              :percentage="getProgressPercentage(row)"
              :status="getProgressStatus(row.status)"
              :stroke-width="6"
              :show-text="false"
              style="margin-top: 4px;"
            />
          </div>
        </template>
      </el-table-column>
      <el-table-column label="开始时间" min-width="160">
@@ -40,20 +75,61 @@
          {{ formatDateTime(row.endTime) }}
        </template>
      </el-table-column>
      <el-table-column label="操作" width="120" fixed="right">
        <template #default="{ row }">
          <el-button
            link
            type="primary"
            size="small"
            @click.stop="handleRowClick(row)"
          >
            查看详情
          </el-button>
          <el-button
            v-if="row.status === 'RUNNING'"
            link
            type="danger"
            size="small"
            @click.stop="handleCancelTask(row)"
          >
            取消
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    <el-drawer v-model="drawerVisible" size="40%" title="任务步骤详情">
      <el-timeline v-loading="stepsLoading" :reverse="false">
    <el-drawer v-model="drawerVisible" size="40%" :title="`任务步骤详情 - ${currentTaskId || ''}`">
      <div class="drawer-header" v-if="currentTask">
        <el-descriptions :column="2" border size="small">
          <el-descriptions-item label="任务状态">
            <el-tag :type="statusType(currentTask.status)">
              {{ getStatusLabel(currentTask.status) }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="进度">
            {{ currentTask.currentStep || 0 }} / {{ currentTask.totalSteps || 0 }}
          </el-descriptions-item>
        </el-descriptions>
      </div>
      <el-timeline v-loading="stepsLoading" :reverse="false" style="margin-top: 20px;">
        <el-timeline-item
          v-for="step in steps"
          :key="step.id"
          :timestamp="formatDateTime(step.startTime) || '-'"
          :type="step.status === 'COMPLETED' ? 'success' : step.status === 'FAILED' ? 'danger' : 'primary'"
          :type="getStepTimelineType(step.status)"
        >
          <div class="step-title">{{ step.stepName }}</div>
          <div class="step-desc">状态:{{ step.status }}</div>
          <div class="step-desc">
            <el-tag :type="getStepStatusType(step.status)" size="small">
              {{ getStepStatusLabel(step.status) }}
            </el-tag>
          </div>
          <div class="step-desc">耗时:{{ formatDuration(step.durationMs) }}</div>
          <div class="step-desc" v-if="step.errorMessage">
          <div class="step-desc" v-if="step.retryCount > 0">
            重试次数:{{ step.retryCount }}
          </div>
          <div class="step-desc error-message" v-if="step.errorMessage">
            <el-icon><Warning /></el-icon>
            错误:{{ step.errorMessage }}
          </div>
        </el-timeline-item>
@@ -63,14 +139,25 @@
</template>
<script setup>
import { onMounted, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
  Refresh,
  Connection,
  Close,
  VideoPlay,
  VideoPause,
  Warning
} from '@element-plus/icons-vue'
import { multiDeviceTaskApi } from '@/api/device/multiDeviceTask'
const props = defineProps({
  groupId: {
    type: [String, Number],
    default: null
  },
  taskId: {
    type: String,
    default: null
  }
})
@@ -81,6 +168,13 @@
const stepsLoading = ref(false)
const steps = ref([])
const currentTaskId = ref(null)
const currentTask = ref(null)
// SSE相关
const sseConnected = ref(false)
const sseConnecting = ref(false)
let eventSource = null
const baseURL = import.meta.env.VITE_API_BASE_URL || ''
const fetchTasks = async () => {
  try {
@@ -98,8 +192,223 @@
  }
}
// SSE连接
const connectSSE = () => {
  if (eventSource) {
    disconnectSSE()
  }
  sseConnecting.value = true
  try {
    // 构建SSE URL - 后端只支持 taskId 参数,不支持 groupId
    let url = `${baseURL}/api/plcSend/task/notification/sse`
    // 如果没有指定 taskId,则监听所有任务(不传参数)
    if (props.taskId) {
      url += `?taskId=${encodeURIComponent(props.taskId)}`
    }
    // 注意:后端不支持 groupId 参数,如果需要监听某个组的所有任务,
    // 需要在前端根据 groupId 获取任务列表,然后为每个任务创建连接
    // 或者使用不传参数的方式监听所有任务,然后在前端过滤
    eventSource = new EventSource(url)
    eventSource.onopen = () => {
      sseConnected.value = true
      sseConnecting.value = false
      ElMessage.success('实时监控已连接')
    }
    eventSource.onerror = (error) => {
      console.error('SSE连接错误:', error)
      sseConnected.value = false
      sseConnecting.value = false
      if (eventSource?.readyState === EventSource.CLOSED) {
        ElMessage.warning('实时监控连接已断开')
        // 尝试重连
        setTimeout(() => {
          if (!sseConnected.value) {
            connectSSE()
          }
        }, 3000)
      }
    }
    // 监听连接成功事件
    eventSource.addEventListener('connected', (event) => {
      try {
        const data = JSON.parse(event.data)
        console.log('SSE连接成功:', data)
      } catch (error) {
        console.error('解析连接消息失败:', error)
      }
    })
    // 监听任务状态更新
    eventSource.addEventListener('taskStatus', (event) => {
      try {
        const data = JSON.parse(event.data)
        // 如果指定了 groupId,只更新该组的任务
        if (!props.groupId || !data.groupId || String(data.groupId) === String(props.groupId)) {
          updateTaskFromSSE(data)
        }
      } catch (error) {
        console.error('解析任务状态失败:', error)
      }
    })
    // 监听步骤更新
    eventSource.addEventListener('stepUpdate', (event) => {
      try {
        const data = JSON.parse(event.data)
        // 如果数据中包含 taskId,检查是否匹配当前查看的任务
        if (data.taskId && data.taskId === currentTaskId.value) {
          updateStepFromSSE(data)
        } else if (!data.taskId) {
          // 如果没有 taskId,可能是步骤数据直接传递
          updateStepFromSSE(data)
        }
      } catch (error) {
        console.error('解析步骤更新失败:', error)
      }
    })
    // 监听步骤列表更新
    eventSource.addEventListener('stepsUpdate', (event) => {
      try {
        const data = JSON.parse(event.data)
        if (data.taskId === currentTaskId.value && Array.isArray(data.steps)) {
          steps.value = data.steps
        }
      } catch (error) {
        console.error('解析步骤列表失败:', error)
      }
    })
  } catch (error) {
    console.error('创建SSE连接失败:', error)
    ElMessage.error('连接实时监控失败: ' + error.message)
    sseConnecting.value = false
  }
}
const disconnectSSE = () => {
  if (eventSource) {
    eventSource.close()
    eventSource = null
  }
  sseConnected.value = false
  sseConnecting.value = false
}
// 从SSE更新任务状态
const updateTaskFromSSE = (data) => {
  if (!data || !data.taskId) return
  // 如果指定了 groupId,只处理该组的任务
  if (props.groupId && data.groupId && String(data.groupId) !== String(props.groupId)) {
    return
  }
  const taskIndex = tasks.value.findIndex(t => t.taskId === data.taskId)
  if (taskIndex >= 0) {
    // 更新任务 - 保留原有字段,只更新SSE传来的字段
    const existingTask = tasks.value[taskIndex]
    tasks.value[taskIndex] = {
      ...existingTask,
      status: data.status || existingTask.status,
      currentStep: data.currentStep !== undefined ? data.currentStep : existingTask.currentStep,
      totalSteps: data.totalSteps !== undefined ? data.totalSteps : existingTask.totalSteps,
      startTime: data.startTime ? new Date(data.startTime) : existingTask.startTime,
      endTime: data.endTime ? new Date(data.endTime) : existingTask.endTime,
      errorMessage: data.errorMessage || existingTask.errorMessage
    }
    // 如果当前查看的是这个任务,也更新
    if (currentTaskId.value === data.taskId) {
      currentTask.value = tasks.value[taskIndex]
    }
  } else {
    // 新任务,添加到列表(需要转换时间戳为Date对象)
    const newTask = {
      ...data,
      startTime: data.startTime ? new Date(data.startTime) : null,
      endTime: data.endTime ? new Date(data.endTime) : null
    }
    tasks.value.unshift(newTask)
  }
}
// 从SSE更新步骤
const updateStepFromSSE = (data) => {
  if (!data) return
  // 如果数据中包含 taskId,检查是否匹配当前查看的任务
  if (data.taskId && data.taskId !== currentTaskId.value) {
    return
  }
  // 如果当前没有打开任务详情,不更新步骤
  if (!currentTaskId.value) {
    return
  }
  // 使用 id 或 stepOrder 来查找步骤
  const stepIndex = data.id
    ? steps.value.findIndex(s => s.id === data.id)
    : data.stepOrder !== undefined
    ? steps.value.findIndex(s => s.stepOrder === data.stepOrder)
    : -1
  if (stepIndex >= 0) {
    // 更新步骤 - 保留原有字段,只更新SSE传来的字段
    const existingStep = steps.value[stepIndex]
    steps.value[stepIndex] = {
      ...existingStep,
      status: data.status || existingStep.status,
      startTime: data.startTime ? new Date(data.startTime) : existingStep.startTime,
      endTime: data.endTime ? new Date(data.endTime) : existingStep.endTime,
      durationMs: data.durationMs !== undefined ? data.durationMs : existingStep.durationMs,
      retryCount: data.retryCount !== undefined ? data.retryCount : existingStep.retryCount,
      errorMessage: data.errorMessage || existingStep.errorMessage
    }
  } else if (data.stepOrder !== undefined) {
    // 新步骤,添加到列表(需要转换时间戳为Date对象)
    const newStep = {
      ...data,
      startTime: data.startTime ? new Date(data.startTime) : null,
      endTime: data.endTime ? new Date(data.endTime) : null
    }
    steps.value.push(newStep)
    // 按 stepOrder 排序
    steps.value.sort((a, b) => (a.stepOrder || 0) - (b.stepOrder || 0))
  }
}
const handleCancelTask = async (row) => {
  try {
    await ElMessageBox.confirm(
      `确定要取消任务 ${row.taskId} 吗?`,
      '确认取消',
      {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }
    )
    await multiDeviceTaskApi.cancelTask(row.taskId)
    ElMessage.success('任务已取消')
    fetchTasks()
  } catch (error) {
    if (error !== 'cancel') {
      ElMessage.error(error?.message || '取消任务失败')
    }
  }
}
const emit = defineEmits(['task-selected'])
const handleRowClick = async (row) => {
  currentTaskId.value = row.taskId
  currentTask.value = row
  emit('task-selected', row)
  drawerVisible.value = true
  stepsLoading.value = true
  try {
@@ -120,9 +429,67 @@
      return 'danger'
    case 'RUNNING':
      return 'warning'
    case 'PENDING':
      return 'info'
    case 'CANCELLED':
      return 'info'
    default:
      return 'info'
  }
}
const getStatusLabel = (status) => {
  const s = (status || '').toUpperCase()
  const statusMap = {
    'COMPLETED': '已完成',
    'FAILED': '失败',
    'RUNNING': '执行中',
    'PENDING': '等待中',
    'CANCELLED': '已取消'
  }
  return statusMap[s] || s || '未知'
}
const getProgressPercentage = (row) => {
  if (!row.totalSteps || row.totalSteps === 0) return 0
  return Math.round(((row.currentStep || 0) / row.totalSteps) * 100)
}
const getProgressStatus = (status) => {
  const s = (status || '').toUpperCase()
  if (s === 'COMPLETED') return 'success'
  if (s === 'FAILED') return 'exception'
  if (s === 'RUNNING') return 'active'
  return null
}
const getStepTimelineType = (status) => {
  const s = (status || '').toUpperCase()
  if (s === 'COMPLETED') return 'success'
  if (s === 'FAILED') return 'danger'
  if (s === 'RUNNING') return 'primary'
  return 'info'
}
const getStepStatusType = (status) => {
  const s = (status || '').toUpperCase()
  if (s === 'COMPLETED') return 'success'
  if (s === 'FAILED') return 'danger'
  if (s === 'RUNNING') return 'warning'
  if (s === 'PENDING') return 'info'
  return 'default'
}
const getStepStatusLabel = (status) => {
  const s = (status || '').toUpperCase()
  const statusMap = {
    'COMPLETED': '已完成',
    'FAILED': '失败',
    'RUNNING': '执行中',
    'PENDING': '等待中',
    'SKIPPED': '已跳过'
  }
  return statusMap[s] || s || '未知'
}
const formatDuration = (ms) => {
@@ -157,14 +524,45 @@
  () => props.groupId,
  () => {
    fetchTasks()
    // 如果SSE已连接,重新连接(因为监听所有任务,前端会过滤)
    if (sseConnected.value) {
      disconnectSSE()
      // 延迟重连,避免频繁连接
      setTimeout(() => {
        connectSSE()
      }, 500)
    }
  },
  { immediate: true }
)
onMounted(fetchTasks)
watch(
  () => props.taskId,
  () => {
    // 如果指定了 taskId,重新连接以监听特定任务
    if (sseConnected.value) {
      disconnectSSE()
      setTimeout(() => {
        connectSSE()
      }, 500)
    }
  }
)
onMounted(() => {
  fetchTasks()
  // 自动连接SSE
  connectSSE()
})
onUnmounted(() => {
  disconnectSSE()
})
defineExpose({
  fetchTasks
  fetchTasks,
  connectSSE,
  disconnectSSE
})
</script>
@@ -193,6 +591,56 @@
  font-size: 13px;
}
.sse-status {
  margin-top: 4px;
  font-size: 12px;
  display: flex;
  align-items: center;
  gap: 4px;
}
.sse-status.connected {
  color: #67c23a;
}
.sse-status.disconnected {
  color: #f56c6c;
}
.action-buttons {
  display: flex;
  gap: 12px;
}
.progress-cell {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.drawer-header {
  margin-bottom: 20px;
}
.step-title {
  font-weight: 600;
  margin-bottom: 4px;
}
.step-desc {
  font-size: 13px;
  color: #606266;
  margin-top: 4px;
}
.step-desc.error-message {
  color: #f56c6c;
  display: flex;
  align-items: center;
  gap: 4px;
  margin-top: 8px;
}
.step-title {
  font-weight: 600;
  margin-bottom: 4px;
mes-web/src/views/plcTest/components/MultiDeviceTest/ResultAnalysis.vue
New file
@@ -0,0 +1,638 @@
<template>
  <div class="result-analysis">
    <div class="panel-header">
      <div>
        <h3>测试结果分析</h3>
        <p v-if="task">任务编号:{{ task.taskId }}</p>
        <p v-else class="warning">请选择一个任务查看分析结果</p>
      </div>
      <div class="action-buttons">
        <el-button :loading="loading" @click="handleRefresh">
          <el-icon><Refresh /></el-icon>
          刷新
        </el-button>
        <el-button type="primary" :disabled="!task" @click="handleExport('json')">
          <el-icon><Download /></el-icon>
          导出JSON
        </el-button>
        <el-button type="success" :disabled="!task" @click="handleExport('excel')">
          <el-icon><Document /></el-icon>
          导出Excel
        </el-button>
      </div>
    </div>
    <div v-if="!task" class="empty-state">
      <el-empty description="暂无任务数据" />
    </div>
    <div v-else class="analysis-content">
      <!-- 总体结果 -->
      <el-card class="overall-result-card" shadow="never">
        <template #header>
          <div class="card-header">
            <span>总体结果</span>
            <el-tag :type="getOverallStatusType()" size="large">
              {{ getOverallStatusLabel() }}
            </el-tag>
          </div>
        </template>
        <div class="result-stats">
          <div class="stat-item">
            <div class="stat-label">执行时间</div>
            <div class="stat-value">{{ formatDuration(taskDuration) }}</div>
          </div>
          <div class="stat-item">
            <div class="stat-label">总步骤数</div>
            <div class="stat-value">{{ task.totalSteps || 0 }}</div>
          </div>
          <div class="stat-item">
            <div class="stat-label">完成步骤</div>
            <div class="stat-value success">{{ completedSteps }}</div>
          </div>
          <div class="stat-item">
            <div class="stat-label">失败步骤</div>
            <div class="stat-value danger">{{ failedSteps }}</div>
          </div>
          <div class="stat-item">
            <div class="stat-label">成功率</div>
            <div class="stat-value" :class="successRateClass">
              {{ successRate }}%
            </div>
          </div>
        </div>
      </el-card>
      <!-- 进度条 -->
      <el-card class="progress-card" shadow="never">
        <template #header>
          <span>执行进度</span>
        </template>
        <el-progress
          :percentage="progressPercentage"
          :status="progressStatus"
          :stroke-width="20"
          :format="() => `${completedSteps}/${task.totalSteps || 0}`"
        />
      </el-card>
      <!-- 步骤详情 -->
      <el-card class="steps-card" shadow="never">
        <template #header>
          <span>步骤执行详情</span>
        </template>
        <el-table
          v-loading="stepsLoading"
          :data="steps"
          stripe
          style="width: 100%"
        >
          <el-table-column type="index" label="序号" width="60" />
          <el-table-column prop="stepName" label="步骤名称" min-width="150" />
          <el-table-column prop="deviceId" label="设备ID" width="120" />
          <el-table-column prop="status" label="状态" width="100">
            <template #default="{ row }">
              <el-tag :type="getStepStatusType(row.status)">
                {{ getStepStatusLabel(row.status) }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column label="耗时" width="100">
            <template #default="{ row }">
              {{ formatDuration(row.durationMs) }}
            </template>
          </el-table-column>
          <el-table-column label="重试次数" width="100">
            <template #default="{ row }">
              {{ row.retryCount || 0 }}
            </template>
          </el-table-column>
          <el-table-column label="开始时间" min-width="160">
            <template #default="{ row }">
              {{ formatDateTime(row.startTime) }}
            </template>
          </el-table-column>
          <el-table-column label="结束时间" min-width="160">
            <template #default="{ row }">
              {{ formatDateTime(row.endTime) }}
            </template>
          </el-table-column>
          <el-table-column prop="errorMessage" label="错误信息" min-width="200" show-overflow-tooltip />
          <el-table-column label="操作" width="120" fixed="right">
            <template #default="{ row }">
              <el-button
                link
                type="primary"
                size="small"
                @click="viewStepDetail(row)"
              >
                查看详情
              </el-button>
            </template>
          </el-table-column>
        </el-table>
      </el-card>
      <!-- 数据统计图表 -->
      <el-card class="chart-card" shadow="never" v-if="steps.length > 0">
        <template #header>
          <span>执行时间分布</span>
        </template>
        <div class="chart-container">
          <div class="chart-item" v-for="(step, index) in steps" :key="step.id">
            <div class="chart-bar">
              <div
                class="bar-fill"
                :class="getStepStatusClass(step.status)"
                :style="{ width: getBarWidth(step.durationMs) + '%' }"
              >
                <span class="bar-label">{{ formatDuration(step.durationMs) }}</span>
              </div>
            </div>
            <div class="chart-label">{{ step.stepName }}</div>
          </div>
        </div>
      </el-card>
    </div>
    <!-- 步骤详情对话框 -->
    <el-dialog
      v-model="detailDialogVisible"
      :title="`步骤详情 - ${selectedStep?.stepName || ''}`"
      width="60%"
    >
      <div v-if="selectedStep" class="step-detail-content">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="步骤名称">
            {{ selectedStep.stepName }}
          </el-descriptions-item>
          <el-descriptions-item label="设备ID">
            {{ selectedStep.deviceId }}
          </el-descriptions-item>
          <el-descriptions-item label="状态">
            <el-tag :type="getStepStatusType(selectedStep.status)">
              {{ getStepStatusLabel(selectedStep.status) }}
            </el-tag>
          </el-descriptions-item>
          <el-descriptions-item label="重试次数">
            {{ selectedStep.retryCount || 0 }}
          </el-descriptions-item>
          <el-descriptions-item label="开始时间">
            {{ formatDateTime(selectedStep.startTime) }}
          </el-descriptions-item>
          <el-descriptions-item label="结束时间">
            {{ formatDateTime(selectedStep.endTime) }}
          </el-descriptions-item>
          <el-descriptions-item label="耗时">
            {{ formatDuration(selectedStep.durationMs) }}
          </el-descriptions-item>
          <el-descriptions-item label="错误信息" v-if="selectedStep.errorMessage">
            {{ selectedStep.errorMessage }}
          </el-descriptions-item>
        </el-descriptions>
        <el-divider>输入数据</el-divider>
        <el-input
          v-model="selectedStepInputData"
          type="textarea"
          :rows="6"
          readonly
        />
        <el-divider>输出数据</el-divider>
        <el-input
          v-model="selectedStepOutputData"
          type="textarea"
          :rows="6"
          readonly
        />
      </div>
    </el-dialog>
  </div>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Refresh, Download, Document } from '@element-plus/icons-vue'
import { multiDeviceTaskApi } from '@/api/device/multiDeviceTask'
const props = defineProps({
  task: {
    type: Object,
    default: null
  }
})
const loading = ref(false)
const stepsLoading = ref(false)
const steps = ref([])
const detailDialogVisible = ref(false)
const selectedStep = ref(null)
const selectedStepInputData = computed(() => {
  if (!selectedStep.value?.inputData) return ''
  try {
    return JSON.stringify(JSON.parse(selectedStep.value.inputData), null, 2)
  } catch {
    return selectedStep.value.inputData
  }
})
const selectedStepOutputData = computed(() => {
  if (!selectedStep.value?.outputData) return ''
  try {
    return JSON.stringify(JSON.parse(selectedStep.value.outputData), null, 2)
  } catch {
    return selectedStep.value.outputData
  }
})
const taskDuration = computed(() => {
  if (!props.task?.startTime || !props.task?.endTime) return 0
  try {
    const start = new Date(props.task.startTime)
    const end = new Date(props.task.endTime)
    return end.getTime() - start.getTime()
  } catch {
    return 0
  }
})
const completedSteps = computed(() => {
  return steps.value.filter(s => s.status === 'COMPLETED').length
})
const failedSteps = computed(() => {
  return steps.value.filter(s => s.status === 'FAILED').length
})
const successRate = computed(() => {
  if (steps.value.length === 0) return 0
  return Math.round((completedSteps.value / steps.value.length) * 100)
})
const successRateClass = computed(() => {
  if (successRate.value >= 90) return 'success'
  if (successRate.value >= 70) return 'warning'
  return 'danger'
})
const progressPercentage = computed(() => {
  if (!props.task?.totalSteps || props.task.totalSteps === 0) return 0
  return Math.round((completedSteps.value / props.task.totalSteps) * 100)
})
const progressStatus = computed(() => {
  if (props.task?.status === 'COMPLETED') return 'success'
  if (props.task?.status === 'FAILED') return 'exception'
  return 'active'
})
const fetchSteps = async () => {
  if (!props.task?.taskId) {
    steps.value = []
    return
  }
  try {
    stepsLoading.value = true
    const { data } = await multiDeviceTaskApi.getTaskSteps(props.task.taskId)
    steps.value = Array.isArray(data) ? data : (data?.data || [])
  } catch (error) {
    ElMessage.error(error?.message || '加载步骤详情失败')
    steps.value = []
  } finally {
    stepsLoading.value = false
  }
}
const handleRefresh = () => {
  fetchSteps()
}
const viewStepDetail = (step) => {
  selectedStep.value = step
  detailDialogVisible.value = true
}
const handleExport = async (format) => {
  if (!props.task) {
    ElMessage.warning('请先选择任务')
    return
  }
  try {
    loading.value = true
    // 构建导出数据
    const exportData = {
      task: props.task,
      steps: steps.value,
      statistics: {
        totalSteps: props.task.totalSteps || 0,
        completedSteps: completedSteps.value,
        failedSteps: failedSteps.value,
        successRate: successRate.value,
        duration: taskDuration.value
      }
    }
    if (format === 'json') {
      const blob = new Blob([JSON.stringify(exportData, null, 2)], {
        type: 'application/json'
      })
      const url = URL.createObjectURL(blob)
      const a = document.createElement('a')
      a.href = url
      a.download = `task_${props.task.taskId}_${Date.now()}.json`
      a.click()
      URL.revokeObjectURL(url)
      ElMessage.success('导出成功')
    } else if (format === 'excel') {
      // TODO: 实现Excel导出
      ElMessage.info('Excel导出功能开发中')
    }
  } catch (error) {
    ElMessage.error('导出失败: ' + error.message)
  } finally {
    loading.value = false
  }
}
const getOverallStatusType = () => {
  const status = props.task?.status?.toUpperCase()
  if (status === 'COMPLETED') return 'success'
  if (status === 'FAILED') return 'danger'
  if (status === 'RUNNING') return 'warning'
  return 'info'
}
const getOverallStatusLabel = () => {
  const status = props.task?.status?.toUpperCase()
  const statusMap = {
    'COMPLETED': '已完成',
    'FAILED': '失败',
    'RUNNING': '执行中',
    'PENDING': '等待中',
    'CANCELLED': '已取消'
  }
  return statusMap[status] || status || '未知'
}
const getStepStatusType = (status) => {
  const s = (status || '').toUpperCase()
  if (s === 'COMPLETED') return 'success'
  if (s === 'FAILED') return 'danger'
  if (s === 'RUNNING') return 'warning'
  if (s === 'PENDING') return 'info'
  return 'default'
}
const getStepStatusLabel = (status) => {
  const s = (status || '').toUpperCase()
  const statusMap = {
    'COMPLETED': '已完成',
    'FAILED': '失败',
    'RUNNING': '执行中',
    'PENDING': '等待中',
    'SKIPPED': '已跳过'
  }
  return statusMap[s] || s || '未知'
}
const getStepStatusClass = (status) => {
  const s = (status || '').toUpperCase()
  if (s === 'COMPLETED') return 'status-success'
  if (s === 'FAILED') return 'status-danger'
  if (s === 'RUNNING') return 'status-warning'
  return 'status-info'
}
const getBarWidth = (durationMs) => {
  if (!durationMs || steps.value.length === 0) return 0
  const maxDuration = Math.max(...steps.value.map(s => s.durationMs || 0))
  if (maxDuration === 0) return 0
  return Math.min((durationMs / maxDuration) * 100, 100)
}
const formatDuration = (ms) => {
  if (!ms) return '-'
  if (ms < 1000) return `${ms} ms`
  if (ms < 60000) return `${(ms / 1000).toFixed(1)} s`
  const minutes = Math.floor(ms / 60000)
  const seconds = ((ms % 60000) / 1000).toFixed(0)
  return `${minutes}分${seconds}秒`
}
const formatDateTime = (dateTime) => {
  if (!dateTime) return '-'
  try {
    const date = new Date(dateTime)
    if (isNaN(date.getTime())) return dateTime
    const year = date.getFullYear()
    const month = String(date.getMonth() + 1).padStart(2, '0')
    const day = String(date.getDate()).padStart(2, '0')
    const hours = String(date.getHours()).padStart(2, '0')
    const minutes = String(date.getMinutes()).padStart(2, '0')
    const seconds = String(date.getSeconds()).padStart(2, '0')
    return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
  } catch {
    return dateTime
  }
}
watch(
  () => props.task,
  () => {
    fetchSteps()
  },
  { immediate: true }
)
defineExpose({
  fetchSteps
})
</script>
<style scoped>
.result-analysis {
  background: #fff;
  border-radius: 12px;
  padding: 20px;
  box-shadow: 0 8px 32px rgba(15, 18, 63, 0.08);
}
.panel-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}
.panel-header h3 {
  margin: 0;
}
.panel-header p {
  margin: 4px 0 0;
  color: #909399;
  font-size: 13px;
}
.panel-header .warning {
  color: #f56c6c;
}
.action-buttons {
  display: flex;
  gap: 12px;
}
.empty-state {
  padding: 60px 0;
}
.analysis-content {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.overall-result-card .card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.result-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 20px;
}
.stat-item {
  text-align: center;
}
.stat-label {
  font-size: 13px;
  color: #909399;
  margin-bottom: 8px;
}
.stat-value {
  font-size: 24px;
  font-weight: 600;
  color: #303133;
}
.stat-value.success {
  color: #67c23a;
}
.stat-value.danger {
  color: #f56c6c;
}
.stat-value.warning {
  color: #e6a23c;
}
.progress-card {
  margin-top: 0;
}
.steps-card {
  margin-top: 0;
}
.chart-card {
  margin-top: 0;
}
.chart-container {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 20px 0;
}
.chart-item {
  display: flex;
  align-items: center;
  gap: 12px;
}
.chart-bar {
  flex: 1;
  height: 32px;
  background: #f0f2f5;
  border-radius: 4px;
  position: relative;
  overflow: hidden;
}
.bar-fill {
  height: 100%;
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: 0 8px;
  transition: width 0.3s;
  min-width: 60px;
}
.bar-fill.status-success {
  background: linear-gradient(90deg, #67c23a 0%, #85ce61 100%);
}
.bar-fill.status-danger {
  background: linear-gradient(90deg, #f56c6c 0%, #f78989 100%);
}
.bar-fill.status-warning {
  background: linear-gradient(90deg, #e6a23c 0%, #ebb563 100%);
}
.bar-fill.status-info {
  background: linear-gradient(90deg, #909399 0%, #b1b3b8 100%);
}
.bar-label {
  color: #fff;
  font-size: 12px;
  font-weight: 500;
  white-space: nowrap;
}
.chart-label {
  width: 150px;
  font-size: 13px;
  color: #606266;
  text-align: right;
  flex-shrink: 0;
}
.step-detail-content {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
@media (max-width: 768px) {
  .panel-header {
    flex-direction: column;
    align-items: flex-start;
    gap: 12px;
  }
  .action-buttons {
    width: 100%;
    flex-wrap: wrap;
  }
  .result-stats {
    grid-template-columns: repeat(2, 1fr);
  }
}
</style>
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue
@@ -5,7 +5,7 @@
        <h3>多设备测试编排</h3>
        <p v-if="group">当前设备组:{{ group.groupName }}({{ group.deviceCount || '-' }} 台设备)</p>
        <p v-else class="warning">请先在左侧选择一个设备组</p>
        <p v-if="group && loadDeviceName" class="sub-info">上大车设备:{{ loadDeviceName }}</p>
        <p v-if="group && loadDeviceName" class="sub-info">当前设备:{{ loadDeviceName }}</p>
      </div>
      <div class="action-buttons">
        <el-button
@@ -25,23 +25,94 @@
      </div>
    </div>
    <el-form :model="form" label-width="120px">
      <el-form-item label="玻璃ID列表">
    <el-form :model="form" label-width="120px" :rules="rules" ref="formRef">
      <el-form-item label="玻璃ID列表" prop="glassIds" required>
        <el-input
          v-model="glassIdsInput"
          type="textarea"
          :rows="4"
          placeholder="请输入玻璃条码,支持多行或逗号分隔"
          placeholder="请输入玻璃条码,支持多行或逗号分隔,每行一个或逗号分隔"
          show-word-limit
          :maxlength="5000"
        />
        <div class="form-tip">
          已输入 {{ glassIds.length }} 个玻璃ID
        </div>
      </el-form-item>
      <el-divider content-position="left">设备特定配置</el-divider>
      <el-form-item label="位置编码">
        <el-input v-model="form.positionCode" placeholder="例如:POS1" />
        <el-input
          v-model="form.positionCode"
          placeholder="例如:POS1"
          clearable
        />
        <div class="form-tip">上大车设备的位置编码</div>
      </el-form-item>
      <el-form-item label="位置值">
        <el-input-number
          v-model="form.positionValue"
          :min="0"
          :max="9999"
          placeholder="位置数值"
        />
        <div class="form-tip">上大车设备的位置数值</div>
      </el-form-item>
      <el-form-item label="存储位置">
        <el-input-number v-model="form.storagePosition" :min="1" :max="200" />
        <el-input-number
          v-model="form.storagePosition"
          :min="1"
          :max="200"
          placeholder="存储位置编号"
        />
        <div class="form-tip">玻璃存储设备的存储位置</div>
      </el-form-item>
      <el-form-item label="处理类型">
        <el-select v-model="form.processType" placeholder="选择处理类型" clearable>
          <el-option label="标准处理" :value="1" />
          <el-option label="快速处理" :value="2" />
          <el-option label="慢速处理" :value="3" />
        </el-select>
        <div class="form-tip">大理片设备的处理类型</div>
      </el-form-item>
      <el-divider content-position="left">执行配置</el-divider>
      <el-form-item label="执行间隔 (ms)">
        <el-input-number v-model="form.executionInterval" :min="100" :max="10000" />
        <el-input-number
          v-model="form.executionInterval"
          :min="100"
          :max="10000"
          :step="100"
          placeholder="设备操作间隔时间"
        />
        <div class="form-tip">每个设备操作之间的间隔时间(毫秒)</div>
      </el-form-item>
      <el-form-item label="超时时间 (分钟)">
        <el-input-number
          v-model="form.timeoutMinutes"
          :min="1"
          :max="60"
          :step="1"
          placeholder="任务超时时间"
        />
        <div class="form-tip">任务执行的最大超时时间</div>
      </el-form-item>
      <el-form-item label="重试次数">
        <el-input-number
          v-model="form.retryCount"
          :min="0"
          :max="10"
          :step="1"
          placeholder="失败重试次数"
        />
        <div class="form-tip">设备操作失败时的最大重试次数</div>
      </el-form-item>
    </el-form>
  </div>
@@ -65,9 +136,41 @@
const form = reactive({
  positionCode: '',
  positionValue: null,
  storagePosition: null,
  executionInterval: 1000
  processType: null,
  executionInterval: 1000,
  timeoutMinutes: 30,
  retryCount: 3
})
const formRef = ref(null)
const rules = {
  glassIds: [
    {
      validator: (rule, value, callback) => {
        if (glassIds.value.length === 0) {
          callback(new Error('请至少输入一个玻璃ID'))
        } else if (glassIds.value.length > 100) {
          callback(new Error('玻璃ID数量不能超过100个'))
        } else {
          // 验证玻璃ID格式
          const invalidIds = glassIds.value.filter(id => {
            // 简单的格式验证:不能为空,长度在1-50之间
            return !id || id.length === 0 || id.length > 50
          })
          if (invalidIds.length > 0) {
            callback(new Error(`存在无效的玻璃ID格式,请检查`))
          } else {
            callback()
          }
        }
      },
      trigger: 'blur'
    }
  ]
}
const glassIdsInput = ref('')
const loading = ref(false)
@@ -133,23 +236,78 @@
    ElMessage.warning('请先选择设备组')
    return
  }
  // 表单验证
  if (!formRef.value) return
  try {
    await formRef.value.validate()
  } catch (error) {
    ElMessage.warning('请检查表单输入')
    return
  }
  if (glassIds.value.length === 0) {
    ElMessage.warning('请至少输入一个玻璃ID')
    return
  }
  try {
    loading.value = true
    await multiDeviceTaskApi.startTask({
    // 构建任务参数
    const parameters = {
      glassIds: glassIds.value,
      executionInterval: form.executionInterval || 1000
    }
    // 添加可选参数
    if (form.positionCode) {
      parameters.positionCode = form.positionCode
    }
    if (form.positionValue !== null) {
      parameters.positionValue = form.positionValue
    }
    if (form.storagePosition !== null) {
      parameters.storagePosition = form.storagePosition
    }
    if (form.processType !== null) {
      parameters.processType = form.processType
    }
    if (form.timeoutMinutes) {
      parameters.timeoutMinutes = form.timeoutMinutes
    }
    if (form.retryCount !== null) {
      parameters.retryCount = form.retryCount
    }
    // 异步启动任务,立即返回,不阻塞
    const response = await multiDeviceTaskApi.startTask({
      groupId: props.group.id || props.group.groupId,
      parameters: {
        glassIds: glassIds.value,
        positionCode: form.positionCode || null,
        storagePosition: form.storagePosition,
        executionInterval: form.executionInterval
      }
      parameters
    })
    ElMessage.success('任务已启动')
    emit('task-started')
    const task = response?.data
    if (task && task.taskId) {
      ElMessage.success(`任务已启动(异步执行): ${task.taskId}`)
      emit('task-started', task)
      // 立即刷新监控列表,显示新启动的任务
      setTimeout(() => {
        emit('task-started')
      }, 500)
      // 重置表单(保留部分配置),方便继续启动其他设备组
      glassIdsInput.value = ''
      form.positionCode = ''
      form.positionValue = null
      form.storagePosition = null
      form.processType = null
      // 提示用户可以继续启动其他设备组
      ElMessage.info('可以继续选择其他设备组启动测试,多个设备组将并行执行')
    } else {
      ElMessage.warning('任务启动响应异常')
    }
  } catch (error) {
    ElMessage.error(error?.message || '任务启动失败')
  } finally {
@@ -234,5 +392,12 @@
  gap: 12px;
  align-items: center;
}
.form-tip {
  font-size: 12px;
  color: #909399;
  margin-top: 4px;
  line-height: 1.4;
}
</style>