编辑 | blame | 历史 | 原始文档

MES Test Project 多设备联合测试扩展方案

📋 项目概述

基于现有的MES Test Project(mes-web + mes-plcSend),扩展支持多设备联合测试功能,实现"上大车设备 → 大理片设备 → 玻璃存储设备"的完整生产流程自动化测试。

🎯 核心需求

业务场景

  1. 上大车前请求:检测车辆容量(6000mm可配置)、玻璃规格匹配、节拍控制
  2. 大理片交互:与MES大理片信息比对验证、批量处理逻辑
  3. 多设备协调:设备间数据传递、状态同步、依赖管理

技术需求

  • 支持多PLC设备地址映射
  • 设备组配置和管理
  • 串行/并行执行模式
  • 设备间数据共享
  • 实时状态监控

📁 扩展架构设计

1. 后端扩展结构

1.1 新增目录结构

mes-plcSend/src/main/java/com/mes/
├── device/                          # 设备管理层
│   ├── entity/
│   │   ├── DeviceConfig.java        # 设备配置实体
│   │   ├── DeviceGroup.java         # 设备组实体
│   │   └── DeviceStatus.java        # 设备状态实体
│   ├── service/
│   │   ├── DeviceService.java       # 设备管理服务
│   │   ├── DeviceGroupService.java  # 设备组服务
│   │   └── DeviceCoordinationService.java # 设备协调服务
│   └── controller/
│       ├── DeviceController.java    # 设备管理API
│       └── DeviceGroupController.java # 设备组管理API
├── interaction/                     # 交互逻辑模块
│   ├── base/
│   │   ├── BaseInteraction.java     # 基础交互抽象
│   │   ├── InteractionContext.java  # 交互上下文
│   │   └── InteractionResult.java   # 交互结果
│   ├── 上大车/
│   │   ├── 上大车Interaction.java   # 上大车交互逻辑
│   │   └── 上大车Config.java        # 上大车配置
│   ├── 大理片/
│   │   ├── 大理片Interaction.java   # 大理片交互逻辑
│   │   └── 大理片Config.java        # 大理片配置
│   └── 玻璃存储/
│       ├── 玻璃存储Interaction.java # 玻璃存储交互逻辑
│       └── 玻璃存储Config.java      # 玻璃存储配置
└── task/                           # 任务管理层
    ├── entity/
    │   ├── MultiDeviceTask.java     # 多设备任务实体
    │   └── TaskStep.java            # 任务步骤实体
    ├── service/
    │   ├── MultiDeviceTaskService.java # 多设备任务服务
    │   └── TaskExecutionEngine.java    # 任务执行引擎
    └── controller/
        └── MultiDeviceTaskController.java # 多设备任务API

1.2 数据库表扩展

-- 设备配置表
CREATE TABLE device_config (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    device_id VARCHAR(50) UNIQUE NOT NULL COMMENT '设备ID',
    device_name VARCHAR(100) NOT NULL COMMENT '设备名称',
    device_type VARCHAR(50) NOT NULL COMMENT '设备类型(上大车/大理片/玻璃存储)',
    plc_ip VARCHAR(15) NOT NULL COMMENT 'PLC IP地址',
    plc_type VARCHAR(20) NOT NULL COMMENT 'PLC类型',
    module_name VARCHAR(50) NOT NULL COMMENT '模块名称',
    is_primary BOOLEAN DEFAULT FALSE COMMENT '是否主控设备',
    enabled BOOLEAN DEFAULT TRUE COMMENT '是否启用',
    config_json TEXT COMMENT '设备特定配置(JSON)',
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 设备组配置表
CREATE TABLE device_group (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    group_id VARCHAR(50) UNIQUE NOT NULL COMMENT '设备组ID',
    group_name VARCHAR(100) NOT NULL COMMENT '设备组名称',
    project_id VARCHAR(50) NOT NULL COMMENT '关联项目ID',
    execution_mode ENUM('SERIAL', 'PARALLEL') DEFAULT 'SERIAL' COMMENT '执行模式',
    execution_config JSON COMMENT '执行配置',
    dependencies JSON COMMENT '设备依赖关系',
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 设备组与设备关系表
CREATE TABLE device_group_mapping (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    group_id VARCHAR(50) NOT NULL,
    device_id VARCHAR(50) NOT NULL,
    execution_order INT NOT NULL COMMENT '执行顺序',
    FOREIGN KEY (group_id) REFERENCES device_group(group_id) ON DELETE CASCADE,
    FOREIGN KEY (device_id) REFERENCES device_config(device_id) ON DELETE CASCADE,
    UNIQUE KEY uk_group_device (group_id, device_id)
);

-- 多设备任务表
CREATE TABLE multi_device_task (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    task_id VARCHAR(50) UNIQUE NOT NULL COMMENT '任务ID',
    group_id VARCHAR(50) NOT NULL COMMENT '设备组ID',
    project_id VARCHAR(50) NOT NULL COMMENT '项目ID',
    status ENUM('PENDING', 'RUNNING', 'COMPLETED', 'FAILED', 'CANCELLED') DEFAULT 'PENDING',
    current_step INT DEFAULT 0 COMMENT '当前步骤',
    total_steps INT DEFAULT 0 COMMENT '总步骤数',
    start_time DATETIME COMMENT '开始时间',
    end_time DATETIME COMMENT '结束时间',
    error_message TEXT COMMENT '错误信息',
    result_data JSON COMMENT '结果数据',
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 任务步骤详情表
CREATE TABLE task_step_detail (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    task_id VARCHAR(50) NOT NULL COMMENT '任务ID',
    step_order INT NOT NULL COMMENT '步骤顺序',
    device_id VARCHAR(50) NOT NULL COMMENT '设备ID',
    step_name VARCHAR(100) NOT NULL COMMENT '步骤名称',
    status ENUM('PENDING', 'RUNNING', 'COMPLETED', 'FAILED', 'SKIPPED') DEFAULT 'PENDING',
    start_time DATETIME COMMENT '步骤开始时间',
    end_time DATETIME COMMENT '步骤结束时间',
    duration_ms BIGINT COMMENT '执行耗时(毫秒)',
    input_data JSON COMMENT '输入数据',
    output_data JSON COMMENT '输出数据',
    error_message TEXT COMMENT '错误信息',
    retry_count INT DEFAULT 0 COMMENT '重试次数',
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (task_id) REFERENCES multi_device_task(task_id) ON DELETE CASCADE
);

2. 前端扩展结构

2.1 新增前端目录

mes-web/src/views/plcTest/
├── components/
│   ├── DeviceManagement/          # 设备管理组件
│   │   ├── DeviceList.vue         # 设备列表
│   │   ├── DeviceConfig.vue       # 设备配置
│   │   └── DeviceStatus.vue       # 设备状态
│   ├── DeviceGroup/              # 设备组组件
│   │   ├── GroupList.vue          # 设备组列表
│   │   ├── GroupConfig.vue        # 设备组配置
│   │   └── GroupTopology.vue      # 设备组拓扑图
│   ├── MultiDeviceTest/          # 多设备测试组件
│   │   ├── TestOrchestration.vue  # 测试编排
│   │   ├── ExecutionMonitor.vue   # 执行监控
│   │   └── ResultAnalysis.vue     # 结果分析
│   └── InteractionLogic/         # 交互逻辑组件
│       ├── 上大车Config.vue       # 上大车配置
│       ├── 大理片Config.vue       # 大理片配置
│       └── 玻璃存储Config.vue     # 玻璃存储配置

🔧 核心实现设计

3.1 设备管理实现

设备配置实体

@Data
@Entity
@TableName("device_config")
public class DeviceConfig {
    @TableId(type = IdType.AUTO)
    private Long id;
    
    @TableField("device_id")
    private String deviceId;
    
    @TableField("device_name")
    private String deviceName;
    
    @TableField("device_type")
    private String deviceType; // "上大车" / "大理片" / "玻璃存储"
    
    @TableField("plc_ip")
    private String plcIp;
    
    @TableField("plc_type")
    private String plcType;
    
    @TableField("module_name")
    private String moduleName;
    
    @TableField("is_primary")
    private Boolean isPrimary;
    
    @TableField("config_json")
    private String configJson; // 设备特定配置
    
    // 配置解析方法
    public <T> T getConfig(Class<T> clazz) {
        if (StringUtils.isBlank(configJson)) {
            return null;
        }
        try {
            return JsonUtils.fromJson(configJson, clazz);
        } catch (Exception e) {
            log.error("解析设备配置失败: {}", deviceId, e);
            return null;
        }
    }
}

设备管理服务

@Service
public class DeviceService {
    
    @Resource
    private DeviceConfigMapper deviceConfigMapper;
    
    /**
     * 注册设备
     */
    @Transactional
    public void registerDevice(DeviceConfig device) {
        // 验证PLC连接
        validatePlcConnection(device.getPlcIp(), device.getPlcType());
        
        // 保存设备配置
        deviceConfigMapper.insert(device);
        
        // 更新地址映射
        updatePlcAddressMapping(device);
        
        log.info("设备注册成功: {}", device.getDeviceId());
    }
    
    /**
     * 获取设备的PLC地址映射
     */
    public Map<String, Integer> getDeviceAddressMapping(String deviceId) {
        DeviceConfig device = getDeviceById(deviceId);
        if (device == null) {
            throw new RuntimeException("设备不存在: " + deviceId);
        }
        
        // 根据设备类型获取对应的地址映射配置
        String mappingKey = device.getDeviceType() + "_" + device.getModuleName();
        return plcAddressService.getMappingByKey(mappingKey);
    }
}

3.2 设备组管理实现

设备组实体

@Data
@TableName("device_group")
public class DeviceGroup {
    @TableId(type = IdType.AUTO)
    private Long id;
    
    @TableField("group_id")
    private String groupId;
    
    @TableField("group_name")
    private String groupName;
    
    @TableField("project_id")
    private String projectId;
    
    @TableField("execution_mode")
    private ExecutionMode executionMode; // SERIAL / PARALLEL
    
    @TableField("execution_config")
    private String executionConfig;
    
    @TableField("dependencies")
    private String dependencies; // JSON格式的依赖关系
    
    // 获取设备组中的所有设备
    public List<DeviceConfig> getDevices() {
        return deviceGroupService.getDevicesByGroupId(groupId);
    }
    
    // 获取执行顺序
    public List<DeviceConfig> getExecutionSequence() {
        return deviceGroupService.getDevicesByOrder(groupId);
    }
}

3.3 交互逻辑实现

基础交互接口

/**
 * 设备交互逻辑接口
 */
public interface DeviceInteraction {
    
    /**
     * 执行交互逻辑
     */
    InteractionResult execute(InteractionContext context);
    
    /**
     * 验证前置条件
     */
    boolean validatePreCondition(InteractionContext context);
    
    /**
     * 获取设备类型
     */
    String getDeviceType();
    
    /**
     * 获取默认配置
     */
    DeviceInteractionConfig getDefaultConfig();
}

上大车交互实现

@Component("上大车Interaction")
public class 上大车Interaction implements DeviceInteraction {
    
    @Override
    public InteractionResult execute(InteractionContext context) {
        上大车Config config = context.getConfig(上大车Config.class);
        
        try {
            // 1. 验证前置条件
            if (!validatePreCondition(context)) {
                return InteractionResult.fail("前置条件验证失败");
            }
            
            // 2. 获取车辆规格(可配置)
            VehicleSpec vehicle = context.getVehicleSpec();
            double vehicleCapacity = config.getVehicleCapacity(); // 默认6000mm
            
            // 3. 检查车辆容量
            if (vehicle.getMaxCapacity() > vehicleCapacity) {
                return InteractionResult.fail("车辆容量超出限制: " + vehicle.getMaxCapacity());
            }
            
            // 4. 获取当前玻璃信息
            GlassSpec currentGlass = context.getCurrentGlass();
            
            // 5. 计算装载空间
            double usedCapacity = calculateUsedCapacity(context);
            double remainingCapacity = vehicleCapacity - usedCapacity;
            
            if (remainingCapacity >= currentGlass.getLength()) {
                // 6. 分配车辆空间
                allocateVehicleSpace(context, currentGlass);
                
                // 7. 节拍控制(可配置间隔时间)
                sleep(config.getGlassIntervalMs()); // 默认1000ms
                
                // 8. 触发PLC写入
                triggerPlcWrite(context, "车辆装载", currentGlass);
                
                return InteractionResult.success("上大车成功", 
                    Map.of("remainingCapacity", remainingCapacity, 
                           "allocatedGlass", currentGlass));
            }
            
            return InteractionResult.wait("等待下一辆车上大车", 
                Map.of("remainingCapacity", remainingCapacity));
                
        } catch (Exception e) {
            log.error("上大车交互执行失败", e);
            return InteractionResult.fail("执行异常: " + e.getMessage());
        }
    }
    
    @Override
    public boolean validatePreCondition(InteractionContext context) {
        // 验证车辆信息、玻璃信息、PLC连接等
        return context.getVehicleSpec() != null && 
               context.getCurrentGlass() != null &&
               validatePlcConnection(context.getDeviceId());
    }
    
    @Override
    public String getDeviceType() {
        return "上大车";
    }
}

大理片交互实现

@Component("大理片Interaction")
public class 大理片Interaction implements DeviceInteraction {
    
    @Override
    public InteractionResult execute(InteractionContext context) {
        大理片Config config = context.getConfig(大理片Config.class);
        
        try {
            // 1. 获取上大车阶段传递的数据
            List<GlassSpec> glassesFromVehicle = context.getSharedData("glassesFromVehicle", List.class);
            VehicleSpec vehicleInfo = context.getSharedData("vehicleInfo", VehicleSpec.class);
            
            if (glassesFromVehicle == null || glassesFromVehicle.isEmpty()) {
                return InteractionResult.wait("等待上大车数据");
            }
            
            // 2. 与MES大理片信息比对验证
            List<GlassSpec> mesGlasses = fetchMesGlassesInfo(vehicleInfo.getVehicleId());
            
            for (GlassSpec vehicleGlass : glassesFromVehicle) {
                boolean matched = false;
                for (GlassSpec mesGlass : mesGlasses) {
                    if (isGlassMatched(vehicleGlass, mesGlass, config)) {
                        matched = true;
                        break;
                    }
                }
                
                if (!matched && config.isGlassMatchingEnabled()) {
                    return InteractionResult.fail("玻璃信息不匹配: " + vehicleGlass.getGlassId());
                }
            }
            
            // 3. 批量处理(可配置)
            if (config.isBatchProcessing()) {
                return processBatch(glassesFromVehicle, context, config);
            } else {
                return processIndividual(glassesFromVehicle, context, config);
            }
            
        } catch (Exception e) {
            log.error("大理片交互执行失败", e);
            return InteractionResult.fail("执行异常: " + e.getMessage());
        }
    }
    
    private InteractionResult processBatch(List<GlassSpec> glasses, InteractionContext context, 大理片Config config) {
        // 批量处理逻辑
        for (GlassSpec glass : glasses) {
            // 每片玻璃处理间隔
            sleep(config.getProcessingInterval()); // 默认2000ms
            triggerPlcWrite(context, "大理片处理", glass);
        }
        
        // 传递处理结果到下一个设备
        context.setSharedData("processedGlasses", glasses);
        
        return InteractionResult.success("大理片批量处理完成", 
            Map.of("processedCount", glasses.size()));
    }
}

3.4 多设备任务执行引擎

任务执行引擎

@Service
public class TaskExecutionEngine {
    
    @Resource
    private DeviceGroupService deviceGroupService;
    
    /**
     * 执行多设备联合测试
     */
    @Transactional
    public MultiDeviceTaskResult executeMultiDeviceTask(String groupId, TaskParameters parameters) {
        DeviceGroup group = deviceGroupService.getDeviceGroupById(groupId);
        if (group == null) {
            throw new RuntimeException("设备组不存在: " + groupId);
        }
        
        // 1. 创建任务记录
        MultiDeviceTask task = createTaskRecord(group, parameters);
        
        try {
            task.setStatus("RUNNING");
            task.setStartTime(new Date());
            
            // 2. 构建交互上下文
            InteractionContext context = buildInteractionContext(group, parameters);
            
            // 3. 按执行模式执行
            MultiDeviceTaskResult result;
            if (group.getExecutionMode() == ExecutionMode.SERIAL) {
                result = executeSerialDevices(group, context, task);
            } else {
                result = executeParallelDevices(group, context, task);
            }
            
            // 4. 更新任务结果
            task.setStatus(result.isSuccess() ? "COMPLETED" : "FAILED");
            task.setEndTime(new Date());
            task.setResultData(JsonUtils.toJson(result));
            
            return result;
            
        } catch (Exception e) {
            log.error("多设备任务执行失败: {}", groupId, e);
            
            task.setStatus("FAILED");
            task.setEndTime(new Date());
            task.setErrorMessage(e.getMessage());
            
            return MultiDeviceTaskResult.fail("任务执行失败: " + e.getMessage());
        }
    }
    
    /**
     * 串行设备执行:上大车 → 大理片 → 玻璃存储
     */
    private MultiDeviceTaskResult executeSerialDevices(DeviceGroup group, InteractionContext context, MultiDeviceTask task) {
        MultiDeviceTaskResult finalResult = new MultiDeviceTaskResult();
        List<DeviceConfig> executionSequence = group.getExecutionSequence();
        
        for (int i = 0; i < executionSequence.size(); i++) {
            DeviceConfig device = executionSequence.get(i);
            TaskStep step = createTaskStep(task.getTaskId(), i + 1, device);
            
            try {
                step.setStatus("RUNNING");
                step.setStartTime(new Date());
                
                // 设置当前设备上下文
                context.setCurrentDevice(device);
                context.setDeviceId(device.getDeviceId());
                
                // 执行设备交互逻辑
                DeviceInteraction interaction = getInteraction(device.getDeviceType());
                InteractionResult stepResult = interaction.execute(context);
                
                step.setEndTime(new Date());
                step.setDurationMs(System.currentTimeMillis() - step.getStartTime().getTime());
                
                if (stepResult.isSuccess()) {
                    step.setStatus("COMPLETED");
                    step.setOutputData(JsonUtils.toJson(stepResult.getData()));
                    
                    // 传递数据到下一个设备
                    passDataToNextDevice(context, device, stepResult);
                    
                    finalResult.addStepResult(device.getDeviceName(), stepResult);
                    
                } else {
                    step.setStatus("FAILED");
                    step.setErrorMessage(stepResult.getMessage());
                    
                    finalResult.addStepResult(device.getDeviceName(), stepResult);
                    finalResult.fail("设备 " + device.getDeviceName() + " 执行失败: " + stepResult.getMessage());
                    break;
                }
                
            } catch (Exception e) {
                step.setStatus("FAILED");
                step.setErrorMessage(e.getMessage());
                step.setEndTime(new Date());
                
                log.error("设备执行异常: {}", device.getDeviceName(), e);
                finalResult.fail("设备 " + device.getDeviceName() + " 执行异常: " + e.getMessage());
                break;
            }
        }
        
        return finalResult;
    }
    
    /**
     * 数据传递到下一个设备
     */
    private void passDataToNextDevice(InteractionContext context, DeviceConfig currentDevice, InteractionResult result) {
        String deviceType = currentDevice.getDeviceType();
        
        if ("上大车".equals(deviceType)) {
            // 上大车 → 大理片:传递玻璃列表和车辆信息
            context.setSharedData("glassesFromVehicle", result.getData("glasses"));
            context.setSharedData("vehicleInfo", result.getData("vehicle"));
            
        } else if ("大理片".equals(deviceType)) {
            // 大理片 → 玻璃存储:传递处理完成的玻璃
            context.setSharedData("processedGlasses", result.getData("processedGlasses"));
        }
    }
}

🎨 前端界面设计

4.1 设备管理界面

设备配置页面

<template>
  <div class="device-management">
    <!-- 设备列表 -->
    <div class="device-list">
      <el-table :data="devices" style="width: 100%">
        <el-table-column prop="deviceName" label="设备名称" />
        <el-table-column prop="deviceType" label="设备类型" />
        <el-table-column prop="plcIp" label="PLC IP" />
        <el-table-column prop="status" label="状态">
          <template #default="{ row }">
            <el-tag :type="getStatusType(row.status)">{{ row.status }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作">
          <template #default="{ row }">
            <el-button @click="editDevice(row)">编辑</el-button>
            <el-button @click="testConnection(row)">测试连接</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    
    <!-- 设备配置表单 -->
    <el-dialog v-model="showConfigDialog" title="设备配置">
      <el-form :model="deviceForm" label-width="120px">
        <el-form-item label="设备ID">
          <el-input v-model="deviceForm.deviceId" />
        </el-form-item>
        <el-form-item label="设备名称">
          <el-input v-model="deviceForm.deviceName" />
        </el-form-item>
        <el-form-item label="设备类型">
          <el-select v-model="deviceForm.deviceType">
            <el-option label="上大车设备" value="上大车" />
            <el-option label="大理片设备" value="大理片" />
            <el-option label="玻璃存储设备" value="玻璃存储" />
          </el-select>
        </el-form-item>
        <el-form-item label="PLC配置">
          <div class="plc-config">
            <el-input v-model="deviceForm.plcIp" placeholder="PLC IP地址" />
            <el-select v-model="deviceForm.plcType" placeholder="PLC类型">
              <el-option label="S7-1200" value="S7-1200" />
              <el-option label="S7-1500" value="S7-1500" />
            </el-select>
          </div>
        </el-form-item>
        
        <!-- 设备特定配置 -->
        <div v-if="deviceForm.deviceType === '上大车'">
          <el-form-item label="车辆容量(mm)">
            <el-input-number v-model="deviceForm.config.vehicleCapacity" :min="1000" :max="12000" />
          </el-form-item>
          <el-form-item label="玻璃间隔(ms)">
            <el-input-number v-model="deviceForm.config.glassIntervalMs" :min="100" :max="10000" />
          </el-form-item>
        </div>
        
        <div v-if="deviceForm.deviceType === '大理片'">
          <el-form-item label="启用玻璃比对">
            <el-switch v-model="deviceForm.config.glassMatchingEnabled" />
          </el-form-item>
          <el-form-item label="批量处理">
            <el-switch v-model="deviceForm.config.batchProcessing" />
          </el-form-item>
        </div>
      </el-form>
      
      <template #footer>
        <el-button @click="showConfigDialog = false">取消</el-button>
        <el-button type="primary" @click="saveDevice">保存</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script>
export default {
  data() {
    return {
      devices: [],
      showConfigDialog: false,
      deviceForm: {
        deviceId: '',
        deviceName: '',
        deviceType: '',
        plcIp: '',
        plcType: '',
        config: {
          vehicleCapacity: 6000,      // 默认6米
          glassIntervalMs: 1000,     // 默认1秒
          glassMatchingEnabled: true,
          batchProcessing: true
        }
      }
    }
  },
  methods: {
    async loadDevices() {
      // 加载设备列表
      const response = await this.$api.device.getDeviceList();
      this.devices = response.data;
    },
    
    async saveDevice() {
      try {
        if (this.deviceForm.deviceId) {
          await this.$api.device.updateDevice(this.deviceForm);
        } else {
          await this.$api.device.createDevice(this.deviceForm);
        }
        this.$message.success('保存成功');
        this.showConfigDialog = false;
        this.loadDevices();
      } catch (error) {
        this.$message.error('保存失败: ' + error.message);
      }
    }
  }
}
</script>

4.2 设备组配置界面

<template>
  <div class="device-group-management">
    <!-- 设备组列表 -->
    <div class="group-list">
      <div class="header">
        <h3>设备组配置</h3>
        <el-button type="primary" @click="createGroup">新建设备组</el-button>
      </div>
      
      <div class="groups">
        <el-card v-for="group in deviceGroups" :key="group.groupId" class="group-card">
          <template #header>
            <div class="card-header">
              <span>{{ group.groupName }}</span>
              <el-tag :type="group.executionMode === 'SERIAL' ? 'primary' : 'success'">
                {{ group.executionMode === 'SERIAL' ? '串行执行' : '并行执行' }}
              </el-tag>
            </div>
          </template>
          
          <div class="group-content">
            <!-- 设备拓扑图 -->
            <div class="topology">
              <div v-for="(device, index) in group.devices" :key="device.deviceId" class="device-node">
                <div class="device-box" :class="device.deviceType">
                  <div class="device-name">{{ device.deviceName }}</div>
                  <div class="device-type">{{ device.deviceType }}</div>
                </div>
                <div v-if="index < group.devices.length - 1" class="arrow">→</div>
              </div>
            </div>
            
            <!-- 依赖关系 -->
            <div class="dependencies" v-if="group.dependencies">
              <h4>设备依赖关系:</h4>
              <div v-for="(deps, device) in group.dependencies" :key="device" class="dependency-item">
                {{ device }} 依赖于: {{ deps.join(', ') }}
              </div>
            </div>
          </div>
          
          <template #footer>
            <div class="card-footer">
              <el-button @click="editGroup(group)">编辑</el-button>
              <el-button @click="startTest(group)">开始测试</el-button>
              <el-button @click="deleteGroup(group)">删除</el-button>
            </div>
          </template>
        </el-card>
      </div>
    </div>
    
    <!-- 设备组配置对话框 -->
    <el-dialog v-model="showGroupDialog" title="设备组配置" width="800px">
      <el-form :model="groupForm" label-width="120px">
        <el-form-item label="设备组名称">
          <el-input v-model="groupForm.groupName" />
        </el-form-item>
        <el-form-item label="关联项目">
          <el-select v-model="groupForm.projectId">
            <el-option v-for="project in projects" :key="project.id" 
                      :label="project.projectName" :value="project.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="执行模式">
          <el-radio-group v-model="groupForm.executionMode">
            <el-radio label="SERIAL">串行执行</el-radio>
            <el-radio label="PARALLEL">并行执行</el-radio>
          </el-radio-group>
        </el-form-item>
        
        <!-- 设备选择 -->
        <el-form-item label="包含设备">
          <el-transfer 
            v-model="selectedDevices"
            :data="availableDevices"
            :titles="['可用设备', '已选设备']"
            @change="updateDeviceOrder" />
        </el-form-item>
        
        <!-- 执行顺序调整 -->
        <el-form-item label="执行顺序" v-if="groupForm.executionMode === 'SERIAL'">
          <div class="execution-order">
            <div v-for="(deviceId, index) in deviceOrder" :key="deviceId" class="order-item">
              <span>{{ index + 1 }}. {{ getDeviceName(deviceId) }}</span>
              <el-button-group>
                <el-button @click="moveUp(index)" :disabled="index === 0">↑</el-button>
                <el-button @click="moveDown(index)" :disabled="index === deviceOrder.length - 1">↓</el-button>
              </el-button-group>
            </div>
          </div>
        </el-form-item>
      </el-form>
    </el-dialog>
  </div>
</template>

<script>
export default {
  data() {
    return {
      deviceGroups: [],
      availableDevices: [],
      selectedDevices: [],
      deviceOrder: [],
      showGroupDialog: false,
      groupForm: {
        groupId: '',
        groupName: '',
        projectId: '',
        executionMode: 'SERIAL'
      }
    }
  },
  methods: {
    async loadDeviceGroups() {
      const response = await this.$api.deviceGroup.getGroupList();
      this.deviceGroups = response.data;
    },
    
    async startTest(group) {
      try {
        // 跳转到测试执行页面
        this.$router.push({
          name: 'MultiDeviceTest',
          query: { groupId: group.groupId }
        });
      } catch (error) {
        this.$message.error('启动测试失败: ' + error.message);
      }
    }
  }
}
</script>

4.3 多设备测试执行界面

<template>
  <div class="multi-device-test">
    <!-- 测试配置 -->
    <div class="test-config">
      <el-card>
        <template #header>
          <h3>测试配置 - {{ groupInfo.groupName }}</h3>
        </template>
        
        <div class="config-content">
          <div class="execution-mode">
            <el-tag :type="groupInfo.executionMode === 'SERIAL' ? 'primary' : 'success'">
              {{ groupInfo.executionMode === 'SERIAL' ? '串行执行' : '并行执行' }}
            </el-tag>
          </div>
          
          <div class="parameters">
            <h4>测试参数:</h4>
            <el-form label-width="150px">
              <el-form-item label="车辆规格">
                <el-input-number v-model="testParams.vehicleLength" :min="1000" :max="12000" /> mm
              </el-form-item>
              <el-form-item label="测试玻璃数量">
                <el-input-number v-model="testParams.glassCount" :min="1" :max="50" />
              </el-form-item>
              <el-form-item label="执行间隔">
                <el-input-number v-model="testParams.executionInterval" :min="500" :max="10000" /> ms
              </el-form-item>
            </el-form>
          </div>
          
          <div class="test-controls">
            <el-button type="primary" size="large" @click="startTest" :loading="isRunning">
              {{ isRunning ? '测试执行中...' : '开始测试' }}
            </el-button>
            <el-button @click="stopTest" :disabled="!isRunning">停止测试</el-button>
            <el-button @click="pauseTest" :disabled="!isRunning">暂停测试</el-button>
          </div>
        </div>
      </el-card>
    </div>
    
    <!-- 执行监控 -->
    <div class="execution-monitor" v-if="isRunning || currentTask">
      <el-card>
        <template #header>
          <h3>执行监控</h3>
        </template>
        
        <div class="monitor-content">
          <!-- 任务进度 -->
          <div class="task-progress">
            <el-progress 
              :percentage="getOverallProgress()" 
              :status="getTaskStatus()" />
            <div class="progress-info">
              步骤 {{ currentStep }} / {{ totalSteps }} - {{ getCurrentStepName() }}
            </div>
          </div>
          
          <!-- 设备状态 -->
          <div class="device-status-grid">
            <div v-for="device in groupInfo.devices" :key="device.deviceId" 
                 class="device-status-card">
              <div class="device-header">
                <span class="device-name">{{ device.deviceName }}</span>
                <el-tag :type="getDeviceStatusType(device.deviceId)">
                  {{ getDeviceStatus(device.deviceId) }}
                </el-tag>
              </div>
              
              <div class="device-details" v-if="getDeviceDetails(device.deviceId)">
                <div class="detail-item">
                  <span>当前玻璃:</span> {{ getDeviceDetails(device.deviceId).currentGlass || '-' }}
                </div>
                <div class="detail-item">
                  <span>处理数量:</span> {{ getDeviceDetails(device.deviceId).processedCount || 0 }}
                </div>
                <div class="detail-item">
                  <span>剩余容量:</span> {{ getDeviceDetails(device.deviceId).remainingCapacity || '-' }} mm
                </div>
              </div>
              
              <!-- 实时日志 -->
              <div class="device-logs">
                <div v-for="log in getDeviceLogs(device.deviceId)" :key="log.timestamp" 
                     class="log-item" :class="log.level">
                  <span class="log-time">{{ formatTime(log.timestamp) }}</span>
                  <span class="log-message">{{ log.message }}</span>
                </div>
              </div>
            </div>
          </div>
          
          <!-- 数据流可视化 -->
          <div class="data-flow">
            <h4>数据流状态:</h4>
            <div class="flow-diagram">
              <div v-for="(device, index) in groupInfo.devices" :key="device.deviceId" class="flow-node">
                <div class="node" :class="['device-' + device.deviceType.toLowerCase(), getNodeStatus(device.deviceId)]">
                  {{ device.deviceName }}
                </div>
                <div class="data-indicator" v-if="index < groupInfo.devices.length - 1">
                  <div class="data-flow-arrow" :class="getFlowStatus(device.deviceId, index + 1)">
                    {{ getFlowData(device.deviceId) }}
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </el-card>
    </div>
    
    <!-- 结果分析 -->
    <div class="test-results" v-if="testResults">
      <el-card>
        <template #header>
          <h3>测试结果</h3>
        </template>
        
        <div class="results-content">
          <!-- 总体结果 -->
          <div class="overall-result">
            <el-result 
              :icon="testResults.success ? 'success' : 'error'" 
              :title="testResults.success ? '测试成功' : '测试失败'"
              :sub-title="testResults.message">
              <template #extra>
                <div class="result-stats">
                  <div class="stat-item">
                    <span class="label">执行时间:</span>
                    <span class="value">{{ formatDuration(testResults.duration) }}</span>
                  </div>
                  <div class="stat-item">
                    <span class="label">处理玻璃:</span>
                    <span class="value">{{ testResults.processedGlassCount }} 片</span>
                  </div>
                  <div class="stat-item">
                    <span class="label">成功率:</span>
                    <span class="value">{{ testResults.successRate }}%</span>
                  </div>
                </div>
              </template>
            </el-result>
          </div>
          
          <!-- 详细结果 -->
          <div class="detailed-results">
            <h4>各设备执行详情:</h4>
            <el-collapse>
              <el-collapse-item v-for="(result, deviceName) in testResults.deviceResults" 
                               :key="deviceName" :title="deviceName">
                <div class="device-result-detail">
                  <div class="result-summary">
                    <el-tag :type="result.success ? 'success' : 'error'">
                      {{ result.success ? '执行成功' : '执行失败' }}
                    </el-tag>
                    <span class="duration">耗时: {{ formatDuration(result.duration) }}</span>
                  </div>
                  
                  <div class="result-data">
                    <h5>输出数据:</h5>
                    <pre>{{ JSON.stringify(result.outputData, null, 2) }}</pre>
                  </div>
                  
                  <div class="result-logs">
                    <h5>执行日志:</h5>
                    <div class="log-list">
                      <div v-for="log in result.logs" :key="log.timestamp" class="log-line">
                        <span class="log-time">{{ formatTime(log.timestamp) }}</span>
                        <span class="log-level">{{ log.level }}</span>
                        <span class="log-message">{{ log.message }}</span>
                      </div>
                    </div>
                  </div>
                </div>
              </el-collapse-item>
            </el-collapse>
          </div>
          
          <!-- 导出结果 -->
          <div class="export-results">
            <el-button @click="exportResults('json')">导出JSON</el-button>
            <el-button @click="exportResults('excel')">导出Excel</el-button>
            <el-button @click="exportResults('pdf')">导出PDF报告</el-button>
          </div>
        </div>
      </el-card>
    </div>
  </div>
</template>

🚀 实施计划

第一阶段:基础架构搭建(1-2周)

  1. 数据库表创建
  • 创建设备管理相关表
  • 建立表关系和索引
  • 迁移现有数据(如需要)
  1. 后端基础组件
  • 设备配置实体和管理服务
  • 设备组管理组件
  • 基础交互接口定义
  1. 前端基础界面
  • 设备管理页面
  • 设备组配置页面
  • 基础组件开发

第二阶段:核心交互逻辑(2-3周)

  1. 设备交互实现
  • 上大车交互逻辑
  • 大理片交互逻辑
  • 玻璃存储交互逻辑
  1. 任务执行引擎
  • 串行执行引擎
  • 多设备协调机制
  • 错误处理和重试逻辑
  1. PLC地址映射扩展
  • 多设备地址映射支持
  • 地址配置管理
  • PLC通信适配

第三阶段:前端完善(1-2周)

  1. 测试执行界面
  • 多设备测试编排
  • 实时监控面板
  • 结果分析展示
  1. 用户交互优化
  • 配置向导
  • 可视化设备拓扑
  • 实时数据流展示

第四阶段:集成测试(1周)

  1. 功能测试
  • 多设备联合测试流程
  • 异常情况处理
  • 性能测试
  1. 系统集成
  • 与现有系统集成
  • 数据迁移验证
  • 用户验收测试

📊 技术要点总结

核心优势

  1. 模块化设计:每个设备类型独立实现,便于扩展
  2. 配置驱动:所有参数可配置,支持不同业务场景
  3. 数据流管理:设备间数据传递和状态同步
  4. 可视化监控:实时显示设备状态和数据流
  5. 灵活扩展:支持新增设备类型和交互逻辑

技术难点

  1. 设备协调:确保多设备执行的正确顺序
  2. 数据同步:设备间数据的准确传递
  3. 异常处理:单个设备失败对整个流程的影响
  4. 性能优化:大批量数据的处理性能

风险控制

  1. 向后兼容:不破坏现有单设备功能
  2. 渐进式升级:分阶段实施,降低风险
  3. 充分测试:每个阶段的全面测试验证
  4. 回滚机制:出现问题时的快速回滚方案

📝 结论

通过这个扩展方案,MES Test Project将具备完整的多设备联合测试能力,支持复杂的生产流程自动化测试。方案基于现有架构设计,风险可控,实施难度适中,能够很好地满足业务需求。

建议优先实现第一阶段的基础架构,然后逐步完善交互逻辑和前端界面,确保每个阶段都能交付可用的功能。