huang
2025-11-20 366ba040d2447bacd3455299425e3166f1f992bb
添加大车、大理片笼以及多设备串行/并行执行写入基础逻辑
28个文件已修改
19个文件已添加
25个文件已删除
12808 ■■■■■ 已修改文件
mes-processes/mes-plcSend/src/main/java/com/mes/config/MybatisMetaObjectHandler.java 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/controller/PlcAddressController.java 338 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/controller/PlcTestController.java 376 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/controller/PlcTestTaskController.java 162 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/controller/PlcTestWriteController.java 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/controller/PlcTestWriteLegacyController.java 352 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/controller/DeviceGroupController.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/controller/DeviceStatusController.java 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DEVICE_CONFIG_FIELDS.md 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DeviceStatus.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/GlassInfo.java 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/request/LoadVehicleRequest.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/request/VerticalCarData.java 426 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/mapper/DeviceGlassInfoMapper.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/mapper/DeviceGroupRelationMapper.java 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/mapper/DeviceStatusMapper.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/DeviceCoordinationService.java 166 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/DeviceGroupConfigService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/DeviceStatusService.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/GlassInfoService.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceConfigServiceImpl.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceCoordinationServiceImpl.java 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceGroupConfigServiceImpl.java 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DevicePlcOperationServiceImpl.java 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceStatusServiceImpl.java 201 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/GlassInfoServiceImpl.java 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/vo/DeviceGroupVO.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/多设备联合测试扩展方案.md 1153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/entity/PlcAddress.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/entity/PlcBaseData.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/entity/PlcTestTask.java 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/README.md 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/GlassStorageInteraction.java 58 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/LargeGlassInteraction.java 56 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/LoadVehicleInteraction.java 76 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/impl/LoadVehicleLogicHandler.java 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/job/PlcAutoTestTaskScheduler.java 295 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/job/config/PlcAddressYmlConfig.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/mapper/PlcAddressMapper.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/mapper/PlcTestTaskMapper.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/service/PlcAddressService.java 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/service/PlcAutoTestService.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/service/PlcDynamicDataService.java 63 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/service/PlcTestTaskService.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/service/PlcTestWriteService.java 578 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/service/impl/PlcAddressServiceImpl.java 270 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/service/impl/PlcAutoTestServiceImpl.java 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/service/impl/PlcDynamicDataServiceImpl.java 508 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/service/impl/PlcTestTaskServiceImpl.java 211 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/service/impl/PlcTestWriteServiceImpl.java 295 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/controller/TaskStatusNotificationController.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/model/RetryPolicy.java 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java 610 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskStatusNotificationService.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/MultiDeviceTaskServiceImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/TaskStatusNotificationServiceImpl.java 322 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/resources/application.yml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/resources/db/migration/V20241120__create_glass_info_table.sql 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/resources/mapper/DeviceGlassInfoMapper.xml 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/resources/mapper/device/DeviceGroupRelationMapper.xml 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/api/device/deviceManagement.js 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/router/index.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/utils/PlcTestUtil.js 418 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/utils/plcFieldMapping.js 633 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/DeviceConfigList.vue 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/DeviceEditDialog.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/DeviceGroupEditDialog.vue 98 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/DeviceGroupList.vue 399 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/Test.vue 1688 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/components/DeviceGroup/GroupList.vue 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/config/MybatisMetaObjectHandler.java
@@ -30,8 +30,16 @@
    }
    private String resolveOperator() {
        // TODO: ä¹‹åŽå¯æŽ¥å…¥ç™»å½•上下文,这里临时回退为 system
        return "system";
        // æ³¨æ„ï¼šè¿™é‡Œå¯ä»¥æŽ¥å…¥Spring Security或其他认证框架获取当前登录用户
        // ä¾‹å¦‚:SecurityContextHolder.getContext().getAuthentication().getName()
        // å½“前暂时使用system作为默认值
        try {
            // å¯ä»¥å°è¯•从请求上下文获取用户信息
            // è¿™é‡Œæš‚时返回system,后续可以扩展
            return "system";
        } catch (Exception e) {
            return "system";
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/controller/PlcAddressController.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/controller/PlcTestController.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/controller/PlcTestTaskController.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/controller/PlcTestWriteController.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/controller/PlcTestWriteLegacyController.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/device/controller/DeviceGroupController.java
@@ -1,6 +1,7 @@
package com.mes.device.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mes.device.entity.DeviceGroupConfig;
import com.mes.device.request.DeviceGroupRequest;
import com.mes.device.service.DeviceGroupConfigService;
@@ -37,6 +38,9 @@
    @Autowired
    private DeviceGroupRelationService deviceGroupRelationService;
    @Resource
    private ObjectMapper objectMapper;
    /**
     * åˆ›å»ºè®¾å¤‡ç»„
@@ -46,18 +50,21 @@
    public Result<DeviceGroupConfig> createGroup(
            @Valid @RequestBody DeviceGroupRequest request) {
        try {
            DeviceGroupConfig groupConfig = (DeviceGroupConfig) request.getGroupConfig();
            DeviceGroupConfig groupConfig = convertToDeviceGroupConfig(request.getGroupConfig());
            if (groupConfig == null) {
                return Result.error("设备组配置信息不能为空");
            }
            boolean success = deviceGroupConfigService.createDeviceGroup(groupConfig);
            if (success) {
                // åˆ›å»ºæˆåŠŸåŽï¼Œé‡æ–°èŽ·å–è®¾å¤‡ç»„å¯¹è±¡
                DeviceGroupConfig created = deviceGroupConfigService.getDeviceGroupByCode(groupConfig.getGroupCode());
                return Result.success(created);
            } else {
                return Result.error();
                return Result.error("创建设备组失败");
            }
        } catch (Exception e) {
            log.error("创建设备组失败", e);
            return Result.error();
            return Result.error("创建设备组失败: " + e.getMessage());
        }
    }
@@ -69,7 +76,13 @@
    public Result<DeviceGroupConfig> updateGroup(
            @Valid @RequestBody DeviceGroupRequest request) {
        try {
            DeviceGroupConfig groupConfig = (DeviceGroupConfig) request.getGroupConfig();
            if (request.getGroupId() == null) {
                return Result.error("设备组ID不能为空");
            }
            DeviceGroupConfig groupConfig = convertToDeviceGroupConfig(request.getGroupConfig());
            if (groupConfig == null) {
                return Result.error("设备组配置信息不能为空");
            }
            groupConfig.setId(request.getGroupId());
            boolean success = deviceGroupConfigService.updateDeviceGroup(groupConfig);
            if (success) {
@@ -77,11 +90,11 @@
                DeviceGroupConfig updated = deviceGroupConfigService.getDeviceGroupByCode(groupConfig.getGroupCode());
                return Result.success(updated);
            } else {
                return Result.error();
                return Result.error("更新设备组配置失败");
            }
        } catch (Exception e) {
            log.error("更新设备组配置失败", e);
            return Result.error();
            return Result.error("更新设备组配置失败: " + e.getMessage());
        }
    }
@@ -418,4 +431,39 @@
            return Result.error();
        }
    }
    /**
     * å°†Object转换为DeviceGroupConfig
     *
     * @param obj å¾…转换的对象
     * @return DeviceGroupConfig对象,如果转换失败返回null
     */
    private DeviceGroupConfig convertToDeviceGroupConfig(Object obj) {
        if (obj == null) {
            return null;
        }
        // å¦‚果已经是DeviceGroupConfig类型,直接返回
        if (obj instanceof DeviceGroupConfig) {
            return (DeviceGroupConfig) obj;
        }
        // å¦‚果是Map类型,使用ObjectMapper转换
        if (obj instanceof Map) {
            try {
                return objectMapper.convertValue(obj, DeviceGroupConfig.class);
            } catch (Exception e) {
                log.error("转换Map到DeviceGroupConfig失败", e);
                return null;
            }
        }
        // å…¶ä»–类型,尝试使用ObjectMapper转换
        try {
            return objectMapper.convertValue(obj, DeviceGroupConfig.class);
        } catch (Exception e) {
            log.error("转换Object到DeviceGroupConfig失败: obj={}", obj, e);
            return null;
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/controller/DeviceStatusController.java
New file
@@ -0,0 +1,141 @@
package com.mes.device.controller;
import com.mes.device.entity.DeviceStatus;
import com.mes.device.service.DeviceStatusService;
import com.mes.vo.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
/**
 * è®¾å¤‡çŠ¶æ€ç®¡ç†æŽ§åˆ¶å™¨
 */
@RestController
@RequestMapping("device/status")
@Api(tags = "设备状态管理")
@Validated
@RequiredArgsConstructor
public class DeviceStatusController {
    private final DeviceStatusService deviceStatusService;
    @PostMapping("/update")
    @ApiOperation("更新设备在线状态")
    public Result<Boolean> updateDeviceOnlineStatus(
            @Valid @RequestBody DeviceStatusUpdateRequest request) {
        try {
            boolean success = deviceStatusService.updateDeviceOnlineStatus(
                    request.getDeviceId(),
                    request.getStatus()
            );
            if (success) {
                return Result.success(true);
            } else {
                return Result.error("更新设备在线状态失败");
            }
        } catch (Exception e) {
            return Result.error("更新设备在线状态失败: " + e.getMessage());
        }
    }
    @PostMapping("/batch-update")
    @ApiOperation("批量更新设备在线状态")
    public Result<Boolean> batchUpdateDeviceOnlineStatus(
            @Valid @RequestBody DeviceStatusBatchUpdateRequest request) {
        try {
            boolean success = deviceStatusService.batchUpdateDeviceOnlineStatus(
                    request.getDeviceIds(),
                    request.getStatus()
            );
            if (success) {
                return Result.success(true);
            } else {
                return Result.error("批量更新设备在线状态失败");
            }
        } catch (Exception e) {
            return Result.error("批量更新设备在线状态失败: " + e.getMessage());
        }
    }
    @GetMapping("/latest/{deviceId}")
    @ApiOperation("获取设备最新状态")
    public Result<DeviceStatus> getLatestStatus(
            @ApiParam(value = "设备配置ID", required = true) @PathVariable Long deviceId) {
        try {
            DeviceStatus status = deviceStatusService.getLatestByDeviceConfigId(deviceId);
            return Result.success(status);
        } catch (Exception e) {
            return Result.error("获取设备状态失败: " + e.getMessage());
        }
    }
    @PostMapping("/heartbeat")
    @ApiOperation("记录设备心跳")
    public Result<Boolean> recordHeartbeat(
            @Valid @RequestBody DeviceHeartbeatRequest request) {
        try {
            boolean success = deviceStatusService.recordHeartbeat(
                    request.getDeviceId(),
                    request.getStatus()
            );
            if (success) {
                return Result.success(true);
            } else {
                return Result.error("记录设备心跳失败");
            }
        } catch (Exception e) {
            return Result.error("记录设备心跳失败: " + e.getMessage());
        }
    }
    /**
     * è®¾å¤‡çŠ¶æ€æ›´æ–°è¯·æ±‚
     */
    @Data
    public static class DeviceStatusUpdateRequest {
        @NotNull(message = "设备ID不能为空")
        @ApiParam(value = "设备配置ID", required = true)
        private Long deviceId;
        @NotEmpty(message = "设备状态不能为空")
        @ApiParam(value = "设备状态(ONLINE/OFFLINE/BUSY/ERROR/MAINTENANCE)", required = true)
        private String status;
    }
    /**
     * æ‰¹é‡è®¾å¤‡çŠ¶æ€æ›´æ–°è¯·æ±‚
     */
    @Data
    public static class DeviceStatusBatchUpdateRequest {
        @NotEmpty(message = "设备ID列表不能为空")
        @ApiParam(value = "设备配置ID列表", required = true)
        private List<Long> deviceIds;
        @NotEmpty(message = "设备状态不能为空")
        @ApiParam(value = "设备状态(ONLINE/OFFLINE/BUSY/ERROR/MAINTENANCE)", required = true)
        private String status;
    }
    /**
     * è®¾å¤‡å¿ƒè·³è¯·æ±‚
     */
    @Data
    public static class DeviceHeartbeatRequest {
        @NotEmpty(message = "设备ID不能为空")
        @ApiParam(value = "设备ID(device_config.device_id)", required = true)
        private String deviceId;
        @ApiParam(value = "设备状态(可选,默认为ONLINE)")
        private String status;
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DEVICE_CONFIG_FIELDS.md
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DeviceStatus.java
New file
@@ -0,0 +1,90 @@
package com.mes.device.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
 * è®¾å¤‡çŠ¶æ€å®žä½“ç±»
 * å¯¹åº”数据库表:device_status
 */
@Data
@TableName("device_status")
@ApiModel(value = "设备状态信息")
public class DeviceStatus {
    @ApiModelProperty(value = "记录ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty(value = "设备ID(device_config.device_id)", example = "DEVICE_001")
    @TableField("device_id")
    private String deviceId;
    @ApiModelProperty(value = "关联任务ID", example = "TASK_001")
    @TableField("task_id")
    private String taskId;
    @ApiModelProperty(value = "设备状态", example = "ONLINE/OFFLINE/BUSY/ERROR/MAINTENANCE")
    @TableField("status")
    private String status;
    @ApiModelProperty(value = "最后心跳时间")
    @TableField("last_heartbeat")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date lastHeartbeat;
    @ApiModelProperty(value = "CPU使用率(%)")
    @TableField("cpu_usage")
    private BigDecimal cpuUsage;
    @ApiModelProperty(value = "内存使用率(%)")
    @TableField("memory_usage")
    private BigDecimal memoryUsage;
    @ApiModelProperty(value = "PLC连接状态", example = "CONNECTED/DISCONNECTED/ERROR")
    @TableField("plc_connection_status")
    private String plcConnectionStatus;
    @ApiModelProperty(value = "当前操作")
    @TableField("current_operation")
    private String currentOperation;
    @ApiModelProperty(value = "操作进度(0-100)")
    @TableField("operation_progress")
    private BigDecimal operationProgress;
    @ApiModelProperty(value = "告警信息")
    @TableField("alert_message")
    private String alertMessage;
    @ApiModelProperty(value = "记录时间")
    @TableField("created_time")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createdTime;
    // è®¾å¤‡çŠ¶æ€å¸¸é‡
    public static final class Status {
        public static final String ONLINE = "ONLINE";           // åœ¨çº¿
        public static final String OFFLINE = "OFFLINE";         // ç¦»çº¿
        public static final String BUSY = "BUSY";               // å¿™ç¢Œ
        public static final String ERROR = "ERROR";             // é”™è¯¯
        public static final String MAINTENANCE = "MAINTENANCE"; // ç»´æŠ¤ä¸­
    }
    // PLC连接状态常量
    public static final class PlcConnectionStatus {
        public static final String CONNECTED = "CONNECTED";         // å·²è¿žæŽ¥
        public static final String DISCONNECTED = "DISCONNECTED";   // æœªè¿žæŽ¥
        public static final String ERROR = "ERROR";                 // è¿žæŽ¥é”™è¯¯
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/GlassInfo.java
New file
@@ -0,0 +1,93 @@
package com.mes.device.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.util.Date;
/**
 * çŽ»ç’ƒä¿¡æ¯å®žä½“ç±»
 * å¯¹åº”数据库表:glass_info
 */
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("glass_info")
@ApiModel(value = "GlassInfo", description = "玻璃信息")
public class GlassInfo {
    @ApiModelProperty(value = "主键ID", example = "1")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty(value = "玻璃ID(唯一标识)", example = "GLS-2024-001")
    @TableField("glass_id")
    private String glassId;
    @ApiModelProperty(value = "玻璃长度(mm)", example = "2000")
    @TableField("glass_length")
    private Integer glassLength;
    @ApiModelProperty(value = "玻璃宽度(mm)", example = "1500")
    @TableField("glass_width")
    private Integer glassWidth;
    @ApiModelProperty(value = "玻璃厚度(mm)", example = "5.0")
    @TableField("glass_thickness")
    private BigDecimal glassThickness;
    @ApiModelProperty(value = "玻璃类型", example = "普通玻璃")
    @TableField("glass_type")
    private String glassType;
    @ApiModelProperty(value = "生产厂商", example = "厂商A")
    @TableField("manufacturer")
    private String manufacturer;
    @ApiModelProperty(value = "生产日期")
    @TableField("production_date")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date productionDate;
    @ApiModelProperty(value = "状态:ACTIVE-活跃, ARCHIVED-已归档", example = "ACTIVE")
    @TableField("status")
    private String status;
    @ApiModelProperty(value = "描述信息")
    @TableField("description")
    private String description;
    @ApiModelProperty(value = "创建时间")
    @TableField(value = "created_time", fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createdTime;
    @ApiModelProperty(value = "更新时间")
    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updatedTime;
    @ApiModelProperty(value = "创建人", example = "system")
    @TableField(value = "created_by", fill = FieldFill.INSERT)
    private String createdBy;
    @ApiModelProperty(value = "更新人", example = "system")
    @TableField(value = "updated_by", fill = FieldFill.INSERT_UPDATE)
    private String updatedBy;
    @ApiModelProperty(value = "是否删除:0-否,1-是", example = "0")
    @TableField("is_deleted")
    @TableLogic
    private Integer isDeleted;
    // çŠ¶æ€å¸¸é‡
    public static final class Status {
        public static final String ACTIVE = "ACTIVE";      // æ´»è·ƒ
        public static final String ARCHIVED = "ARCHIVED";  // å·²å½’æ¡£
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/request/LoadVehicleRequest.java
New file
@@ -0,0 +1,72 @@
package com.mes.device.entity.request;
import com.github.xingshuangs.iot.common.enums.EDataType;
import com.github.xingshuangs.iot.protocol.s7.serializer.S7Variable;
import lombok.Data;
/**
 * ä¸Šå¤§è½¦è®¾å¤‡è¯·æ±‚实体
 * ç”¨äºŽå®šä¹‰ä¸Šå¤§è½¦è®¾å¤‡å‘PLC写入的字段结构
 * å­—段地址映射通过DeviceConfig.configJson中的addressMapping配置
 *
 * @author mes
 * @since 2025-11-19
 */
@Data
public class LoadVehicleRequest {
    /**
     * è¯·æ±‚å­— 0无请求 1有请求(上大车清0)
     */
    @S7Variable(address = "plcRequest", type = EDataType.UINT16)
    private Integer plcRequest;
    /**
     * è¿›ç‰‡ä½ç½®
     */
    @S7Variable(address = "inPosition", type = EDataType.UINT16)
    private Integer inPosition;
    /**
     * çŽ»ç’ƒID1
     */
    @S7Variable(address = "plcGlassId1", type = EDataType.STRING, count = 20)
    private String plcGlassId1;
    /**
     * çŽ»ç’ƒID2
     */
    @S7Variable(address = "plcGlassId2", type = EDataType.STRING, count = 20)
    private String plcGlassId2;
    /**
     * çŽ»ç’ƒID3
     */
    @S7Variable(address = "plcGlassId3", type = EDataType.STRING, count = 20)
    private String plcGlassId3;
    /**
     * çŽ»ç’ƒID4
     */
    @S7Variable(address = "plcGlassId4", type = EDataType.STRING, count = 20)
    private String plcGlassId4;
    /**
     * çŽ»ç’ƒID5
     */
    @S7Variable(address = "plcGlassId5", type = EDataType.STRING, count = 20)
    private String plcGlassId5;
    /**
     * çŽ»ç’ƒID6
     */
    @S7Variable(address = "plcGlassId6", type = EDataType.STRING, count = 20)
    private String plcGlassId6;
    /**
     * çŽ»ç’ƒæ•°é‡
     */
    @S7Variable(address = "plcGlassCount", type = EDataType.UINT16)
    private Integer plcGlassCount;
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/request/VerticalCarData.java
New file
@@ -0,0 +1,426 @@
package com.mes.device.entity.request;
import cn.hutool.core.collection.CollectionUtil;
import com.github.xingshuangs.iot.common.enums.EDataType;
import com.github.xingshuangs.iot.protocol.s7.serializer.S7Variable;
import com.mes.vertical.history.VerticalSheetCageHistoryTask;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
 * @Author : zhoush
 * @Date: 2025/6/15 15:15
 * @Description:
 */
@ApiModel(description = ":")
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class VerticalCarData {
    @ApiModelProperty(value = "联机状态", position = 1)
    @S7Variable(address = "verticalCar.onlineState", type = EDataType.BOOL)
    private Boolean onlineState;
    @ApiModelProperty(value = "请求字", position = 2)
    @S7Variable(address = "verticalCar.plcRequest", type = EDataType.UINT16)
    private Integer plcRequest;
    @ApiModelProperty(value = "汇报字", position = 3)
    @S7Variable(address = "verticalCar.reportWord", type = EDataType.UINT16)
    private Integer reportWord;
    @ApiModelProperty(value = "1状态", position = 4)
    @S7Variable(address = "verticalCar.state1", type = EDataType.UINT16)
    private Integer state1;
    @ApiModelProperty(value = "2状态", position = 5)
    @S7Variable(address = "verticalCar.state2", type = EDataType.UINT16)
    private Integer state2;
    @ApiModelProperty(value = "3状态", position = 6)
    @S7Variable(address = "verticalCar.state3", type = EDataType.UINT16)
    private Integer state3;
    @ApiModelProperty(value = "4状态", position = 7)
    @S7Variable(address = "verticalCar.state4", type = EDataType.UINT16)
    private Integer state4;
    @ApiModelProperty(value = "5状态", position = 8)
    @S7Variable(address = "verticalCar.state5", type = EDataType.UINT16)
    private Integer state5;
    @ApiModelProperty(value = "6状态", position = 9)
    @S7Variable(address = "verticalCar.state6", type = EDataType.UINT16)
    private Integer state6;
    @ApiModelProperty(value = "发送字", position = 10)
    @S7Variable(address = "verticalCar.mesSend", type = EDataType.UINT16)
    private Integer mesSend;
    @ApiModelProperty(value = "确认字", position = 11)
    @S7Variable(address = "verticalCar.confirmWord", type = EDataType.UINT16)
    private Integer confirmWord;
    @ApiModelProperty(value = "车次信息", position = 12)
    @S7Variable(address = "verticalCar.trainInfo", type = EDataType.STRING, count = 20)
    private String trainInfo;
    @ApiModelProperty(value = "玻璃id1", position = 13)
    @S7Variable(address = "verticalCar.mesGlassId1", type = EDataType.STRING, count = 20)
    private String mesGlassId1;
    @ApiModelProperty(value = "玻璃id2", position = 14)
    @S7Variable(address = "verticalCar.mesGlassId2", type = EDataType.STRING, count = 20)
    private String mesGlassId2;
    @ApiModelProperty(value = "玻璃id3", position = 15)
    @S7Variable(address = "verticalCar.mesGlassId3", type = EDataType.STRING, count = 20)
    private String mesGlassId3;
    @ApiModelProperty(value = "玻璃id4", position = 16)
    @S7Variable(address = "verticalCar.mesGlassId4", type = EDataType.STRING, count = 20)
    private String mesGlassId4;
    @ApiModelProperty(value = "玻璃id5", position = 17)
    @S7Variable(address = "verticalCar.mesGlassId5", type = EDataType.STRING, count = 20)
    private String mesGlassId5;
    @ApiModelProperty(value = "玻璃id6", position = 18)
    @S7Variable(address = "verticalCar.mesGlassId6", type = EDataType.STRING, count = 20)
    private String mesGlassId6;
    @ApiModelProperty(value = "起始格子1", position = 19)
    @S7Variable(address = "verticalCar.start1", type = EDataType.UINT16)
    private Integer start1;
    @ApiModelProperty(value = "起始格子2", position = 20)
    @S7Variable(address = "verticalCar.start2", type = EDataType.UINT16)
    private Integer start2;
    @ApiModelProperty(value = "起始格子3", position = 21)
    @S7Variable(address = "verticalCar.start3", type = EDataType.UINT16)
    private Integer start3;
    @ApiModelProperty(value = "起始格子4", position = 22)
    @S7Variable(address = "verticalCar.start4", type = EDataType.UINT16)
    private Integer start4;
    @ApiModelProperty(value = "起始格子5", position = 23)
    @S7Variable(address = "verticalCar.start5", type = EDataType.UINT16)
    private Integer start5;
    @ApiModelProperty(value = "起始格子6", position = 24)
    @S7Variable(address = "verticalCar.start6", type = EDataType.UINT16)
    private Integer start6;
    @ApiModelProperty(value = "目标格子1", position = 25)
    @S7Variable(address = "verticalCar.target1", type = EDataType.UINT16)
    private Integer target1;
    @ApiModelProperty(value = "目标格子2", position = 26)
    @S7Variable(address = "verticalCar.target2", type = EDataType.UINT16)
    private Integer target2;
    @ApiModelProperty(value = "目标格子3", position = 27)
    @S7Variable(address = "verticalCar.target3", type = EDataType.UINT16)
    private Integer target3;
    @ApiModelProperty(value = "目标格子4", position = 28)
    @S7Variable(address = "verticalCar.target4", type = EDataType.UINT16)
    private Integer target4;
    @ApiModelProperty(value = "目标格子5", position = 29)
    @S7Variable(address = "verticalCar.target5", type = EDataType.UINT16)
    private Integer target5;
    @ApiModelProperty(value = "目标格子6", position = 30)
    @S7Variable(address = "verticalCar.target6", type = EDataType.UINT16)
    private Integer target6;
    @ApiModelProperty(value = "长边1", position = 31)
    @S7Variable(address = "verticalCar.width1", type = EDataType.UINT16)
    private Integer width1;
    @ApiModelProperty(value = "长边2", position = 32)
    @S7Variable(address = "verticalCar.width2", type = EDataType.UINT16)
    private Integer width2;
    @ApiModelProperty(value = "长边3", position = 33)
    @S7Variable(address = "verticalCar.width3", type = EDataType.UINT16)
    private Integer width3;
    @ApiModelProperty(value = "长边4", position = 34)
    @S7Variable(address = "verticalCar.width4", type = EDataType.UINT16)
    private Integer width4;
    @ApiModelProperty(value = "长边5", position = 35)
    @S7Variable(address = "verticalCar.width5", type = EDataType.UINT16)
    private Integer width5;
    @ApiModelProperty(value = "长边6", position = 36)
    @S7Variable(address = "verticalCar.width6", type = EDataType.UINT16)
    private Integer width6;
    @ApiModelProperty(value = "短边1", position = 37)
    @S7Variable(address = "verticalCar.height1", type = EDataType.UINT16)
    private Integer height1;
    @ApiModelProperty(value = "短边2", position = 38)
    @S7Variable(address = "verticalCar.height2", type = EDataType.UINT16)
    private Integer height2;
    @ApiModelProperty(value = "短边3", position = 39)
    @S7Variable(address = "verticalCar.height3", type = EDataType.UINT16)
    private Integer height3;
    @ApiModelProperty(value = "短边4", position = 40)
    @S7Variable(address = "verticalCar.height4", type = EDataType.UINT16)
    private Integer height4;
    @ApiModelProperty(value = "短边5", position = 41)
    @S7Variable(address = "verticalCar.height5", type = EDataType.UINT16)
    private Integer height5;
    @ApiModelProperty(value = "短边6", position = 42)
    @S7Variable(address = "verticalCar.height6", type = EDataType.UINT16)
    private Integer height6;
    @ApiModelProperty(value = "厚1", position = 43)
    @S7Variable(address = "verticalCar.thickness1", type = EDataType.UINT16)
    private Integer thickness1;
    @ApiModelProperty(value = "厚2", position = 44)
    @S7Variable(address = "verticalCar.thickness2", type = EDataType.UINT16)
    private Integer thickness2;
    @ApiModelProperty(value = "厚3", position = 45)
    @S7Variable(address = "verticalCar.thickness3", type = EDataType.UINT16)
    private Integer thickness3;
    @ApiModelProperty(value = "厚4", position = 46)
    @S7Variable(address = "verticalCar.thickness4", type = EDataType.UINT16)
    private Integer thickness4;
    @ApiModelProperty(value = "厚5", position = 47)
    @S7Variable(address = "verticalCar.thickness5", type = EDataType.UINT16)
    private Integer thickness5;
    @ApiModelProperty(value = "厚6", position = 48)
    @S7Variable(address = "verticalCar.thickness6", type = EDataType.UINT16)
    private Integer thickness6;
    @ApiModelProperty(value = "靠边距1", position = 49)
    @S7Variable(address = "verticalCar.edgeDistance1", type = EDataType.UINT16)
    private Integer edgeDistance1;
    @ApiModelProperty(value = "靠边距2", position = 50)
    @S7Variable(address = "verticalCar.edgeDistance2", type = EDataType.UINT16)
    private Integer edgeDistance2;
    @ApiModelProperty(value = "靠边距3", position = 51)
    @S7Variable(address = "verticalCar.edgeDistance3", type = EDataType.UINT16)
    private Integer edgeDistance3;
    @ApiModelProperty(value = "靠边距4", position = 52)
    @S7Variable(address = "verticalCar.edgeDistance4", type = EDataType.UINT16)
    private Integer edgeDistance4;
    @ApiModelProperty(value = "靠边距5", position = 53)
    @S7Variable(address = "verticalCar.edgeDistance5", type = EDataType.UINT16)
    private Integer edgeDistance5;
    @ApiModelProperty(value = "靠边距6", position = 54)
    @S7Variable(address = "verticalCar.edgeDistance6", type = EDataType.UINT16)
    private Integer edgeDistance6;
    @ApiModelProperty(value = "目标靠边距1", position = 55)
    @S7Variable(address = "verticalCar.targetEdgeDistance1", type = EDataType.UINT16)
    private Integer targetEdgeDistance1;
    @ApiModelProperty(value = "目标靠边距2", position = 56)
    @S7Variable(address = "verticalCar.targetEdgeDistance2", type = EDataType.UINT16)
    private Integer targetEdgeDistance2;
    @ApiModelProperty(value = "目标靠边距3", position = 57)
    @S7Variable(address = "verticalCar.targetEdgeDistance3", type = EDataType.UINT16)
    private Integer targetEdgeDistance3;
    @ApiModelProperty(value = "目标靠边距4", position = 58)
    @S7Variable(address = "verticalCar.targetEdgeDistance4", type = EDataType.UINT16)
    private Integer targetEdgeDistance4;
    @ApiModelProperty(value = "目标靠边距5", position = 59)
    @S7Variable(address = "verticalCar.targetEdgeDistance5", type = EDataType.UINT16)
    private Integer targetEdgeDistance5;
    @ApiModelProperty(value = "目标靠边距6", position = 60)
    @S7Variable(address = "verticalCar.targetEdgeDistance6", type = EDataType.UINT16)
    private Integer targetEdgeDistance6;
    @ApiModelProperty(value = "报警信号", position = 61)
    @S7Variable(address = "verticalCar.alarmSignal", type = EDataType.UINT16)
    private Integer alarmSignal;
    public List<Integer> getStartSlots() {
        return Arrays.asList(start1, start2, start3, start4, start5, start6);
    }
    public List<Integer> getTargetSlots() {
        return Arrays.asList(target1, target2, target3, target4, target5, target6);
    }
    public List<Integer> getStates() {
        return Arrays.asList(state1, state2, state3, state4, state5, state6);
    }
    public List<String> getGlassIds() {
        return Arrays.asList(mesGlassId1, mesGlassId2, mesGlassId3, mesGlassId4, mesGlassId5, mesGlassId6)
                .stream()
                .filter(glassId -> glassId != null && !glassId.trim().isEmpty())
                .collect(Collectors.toList());
    }
    public List<VerticalSheetCageHistoryTask> getTaskList() {
        List<VerticalSheetCageHistoryTask> inTaskList = new ArrayList();
        List<String> glassIdList = this.getGlassIds();
        if (CollectionUtil.isEmpty(glassIdList)) {
            return inTaskList;
        }
        List<Integer> targetList = this.getTargetSlots();
        List<Integer> stateList = this.getStates();
        List<Integer> startList = this.getStartSlots();
        for (int i = 0; i < glassIdList.size(); i++) {
            VerticalSheetCageHistoryTask task = new VerticalSheetCageHistoryTask();
            task.setGlassId(glassIdList.get(i));
            task.setTargetSlot(targetList.get(i));
            task.setTaskState(stateList.get(i));
            task.setStartSlot(startList.get(i));
            inTaskList.add(task);
        }
        return inTaskList;
    }
    public VerticalCarData setGlassIdsAndPosition(List<String> glassIds, Integer inPosition) {
        VerticalCarData verticalCarData = new VerticalCarData();
        verticalCarData.setTrainInfo(inPosition.toString());
        int i = 1;
        for (String glassId : glassIds
        ) {
            switch (i) {
                case 1:
                    verticalCarData.setMesGlassId1(glassId);
                    verticalCarData.setStart1(inPosition);
                    break;
                case 2:
                    verticalCarData.setMesGlassId2(glassId);
                    verticalCarData.setStart2(inPosition);
                    break;
                case 3:
                    verticalCarData.setMesGlassId3(glassId);
                    verticalCarData.setStart3(inPosition);
                    break;
                case 4:
                    verticalCarData.setMesGlassId4(glassId);
                    verticalCarData.setStart4(inPosition);
                    break;
                case 5:
                    verticalCarData.setMesGlassId5(glassId);
                    verticalCarData.setStart5(inPosition);
                    break;
                case 6:
                    verticalCarData.setMesGlassId6(glassId);
                    verticalCarData.setStart6(inPosition);
                    break;
                default:
                    break;
            }
            i++;
        }
        return verticalCarData;
    }
    public VerticalCarData setMesGlassInfo(VerticalCarData verticalCarData, List<VerticalSheetCageHistoryTask> verticalSheetCageHistoryTasks) {
        int i = 1;
        for (VerticalSheetCageHistoryTask verticalSheetCageHistoryTask : verticalSheetCageHistoryTasks
        ) {
            switch (i) {
                case 1:
                    verticalCarData.setMesGlassId1(verticalSheetCageHistoryTask.getGlassId());
                    verticalCarData.setStart1(verticalSheetCageHistoryTask.getStartSlot());
                    verticalCarData.setTarget1(verticalSheetCageHistoryTask.getTargetSlot());
                    verticalCarData.setWidth1(Math.max((int) verticalSheetCageHistoryTask.getWidth().doubleValue(), (int) verticalSheetCageHistoryTask.getHeight().doubleValue()));
                    verticalCarData.setHeight1(Math.min((int) verticalSheetCageHistoryTask.getWidth().doubleValue(), (int) verticalSheetCageHistoryTask.getHeight().doubleValue()));
                    verticalCarData.setThickness1((int) verticalSheetCageHistoryTask.getThickness().doubleValue());
                    verticalCarData.setEdgeDistance1(verticalSheetCageHistoryTask.getEdgeDistance());
                    verticalCarData.setTargetEdgeDistance1(verticalSheetCageHistoryTask.getTargetEdgeDistance());
                    break;
                case 2:
                    verticalCarData.setMesGlassId2(verticalSheetCageHistoryTask.getGlassId());
                    verticalCarData.setStart2(verticalSheetCageHistoryTask.getStartSlot());
                    verticalCarData.setTarget2(verticalSheetCageHistoryTask.getTargetSlot());
                    verticalCarData.setWidth2(Math.max((int) verticalSheetCageHistoryTask.getWidth().doubleValue(), (int) verticalSheetCageHistoryTask.getHeight().doubleValue()));
                    verticalCarData.setHeight2(Math.min((int) verticalSheetCageHistoryTask.getWidth().doubleValue(), (int) verticalSheetCageHistoryTask.getHeight().doubleValue()));
                    verticalCarData.setThickness2((int) verticalSheetCageHistoryTask.getThickness().doubleValue());
                    verticalCarData.setEdgeDistance2(verticalSheetCageHistoryTask.getEdgeDistance());
                    verticalCarData.setTargetEdgeDistance2(verticalSheetCageHistoryTask.getTargetEdgeDistance());
                    break;
                case 3:
                    // ç¬¬ä¸‰ç»„玻璃数据赋值
                    verticalCarData.setMesGlassId3(verticalSheetCageHistoryTask.getGlassId());
                    verticalCarData.setStart3(verticalSheetCageHistoryTask.getStartSlot());
                    verticalCarData.setTarget3(verticalSheetCageHistoryTask.getTargetSlot());
                    verticalCarData.setWidth3(Math.max((int) verticalSheetCageHistoryTask.getWidth().doubleValue(), (int) verticalSheetCageHistoryTask.getHeight().doubleValue()));
                    verticalCarData.setHeight3(Math.min((int) verticalSheetCageHistoryTask.getWidth().doubleValue(), (int) verticalSheetCageHistoryTask.getHeight().doubleValue()));
                    verticalCarData.setThickness3(Math.min((int) verticalSheetCageHistoryTask.getWidth().doubleValue(), (int) verticalSheetCageHistoryTask.getHeight().doubleValue()));
                    verticalCarData.setEdgeDistance3(verticalSheetCageHistoryTask.getEdgeDistance());
                    verticalCarData.setTargetEdgeDistance3(verticalSheetCageHistoryTask.getTargetEdgeDistance());
                    break;
                case 4:
                    verticalCarData.setMesGlassId4(verticalSheetCageHistoryTask.getGlassId());
                    verticalCarData.setStart4(verticalSheetCageHistoryTask.getStartSlot());
                    verticalCarData.setTarget4(verticalSheetCageHistoryTask.getTargetSlot());
                    verticalCarData.setWidth4(Math.max((int) verticalSheetCageHistoryTask.getWidth().doubleValue(), (int) verticalSheetCageHistoryTask.getHeight().doubleValue()));
                    verticalCarData.setHeight4(Math.min((int) verticalSheetCageHistoryTask.getWidth().doubleValue(), (int) verticalSheetCageHistoryTask.getHeight().doubleValue()));
                    verticalCarData.setThickness4((int) verticalSheetCageHistoryTask.getThickness().doubleValue());
                    verticalCarData.setEdgeDistance4(verticalSheetCageHistoryTask.getEdgeDistance());
                    verticalCarData.setTargetEdgeDistance4(verticalSheetCageHistoryTask.getTargetEdgeDistance());
                    break;
                case 5:
                    verticalCarData.setMesGlassId5(verticalSheetCageHistoryTask.getGlassId());
                    verticalCarData.setStart5(verticalSheetCageHistoryTask.getStartSlot());
                    verticalCarData.setTarget5(verticalSheetCageHistoryTask.getTargetSlot());
                    verticalCarData.setWidth5(Math.max((int) verticalSheetCageHistoryTask.getWidth().doubleValue(), (int) verticalSheetCageHistoryTask.getHeight().doubleValue()));
                    verticalCarData.setHeight5(Math.min((int) verticalSheetCageHistoryTask.getWidth().doubleValue(), (int) verticalSheetCageHistoryTask.getHeight().doubleValue()));
                    verticalCarData.setThickness5((int) verticalSheetCageHistoryTask.getThickness().doubleValue());
                    verticalCarData.setEdgeDistance5(verticalSheetCageHistoryTask.getEdgeDistance());
                    verticalCarData.setTargetEdgeDistance5(verticalSheetCageHistoryTask.getTargetEdgeDistance());
                    break;
                case 6:
                    verticalCarData.setMesGlassId6(verticalSheetCageHistoryTask.getGlassId());
                    verticalCarData.setStart6(verticalSheetCageHistoryTask.getStartSlot());
                    verticalCarData.setTarget6(verticalSheetCageHistoryTask.getTargetSlot());
                    verticalCarData.setWidth6(Math.max((int) verticalSheetCageHistoryTask.getWidth().doubleValue(), (int) verticalSheetCageHistoryTask.getHeight().doubleValue()));
                    verticalCarData.setHeight6(Math.min((int) verticalSheetCageHistoryTask.getWidth().doubleValue(), (int) verticalSheetCageHistoryTask.getHeight().doubleValue()));
                    verticalCarData.setThickness6((int) verticalSheetCageHistoryTask.getThickness().doubleValue());
                    verticalCarData.setEdgeDistance6(verticalSheetCageHistoryTask.getEdgeDistance());
                    verticalCarData.setTargetEdgeDistance6(verticalSheetCageHistoryTask.getTargetEdgeDistance());
                    break;
                default:
                    break;
            }
            i++;
        }
        return verticalCarData;
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/mapper/DeviceGlassInfoMapper.java
New file
@@ -0,0 +1,46 @@
package com.mes.device.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mes.device.entity.GlassInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
 * è®¾å¤‡çŽ»ç’ƒä¿¡æ¯Mapper接口
 *
 * @author mes
 * @since 2024-11-20
 */
@Mapper
public interface DeviceGlassInfoMapper extends BaseMapper<GlassInfo> {
    /**
     * æ ¹æ®çŽ»ç’ƒID查询玻璃信息
     *
     * @param glassId çŽ»ç’ƒID
     * @return çŽ»ç’ƒä¿¡æ¯
     */
    @Select("SELECT * FROM glass_info WHERE glass_id = #{glassId} AND is_deleted = 0 LIMIT 1")
    GlassInfo selectByGlassId(@Param("glassId") String glassId);
    /**
     * æ ¹æ®çŽ»ç’ƒID列表批量查询玻璃信息
     *
     * @param glassIds çŽ»ç’ƒID列表
     * @return çŽ»ç’ƒä¿¡æ¯åˆ—è¡¨
     */
    List<GlassInfo> selectByGlassIds(@Param("glassIds") List<String> glassIds);
    /**
     * æ ¹æ®çŠ¶æ€æŸ¥è¯¢çŽ»ç’ƒä¿¡æ¯åˆ—è¡¨
     *
     * @param status çŠ¶æ€
     * @return çŽ»ç’ƒä¿¡æ¯åˆ—è¡¨
     */
    @Select("SELECT * FROM glass_info WHERE status = #{status} AND is_deleted = 0 ORDER BY created_time DESC")
    List<GlassInfo> selectByStatus(@Param("status") String status);
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/mapper/DeviceGroupRelationMapper.java
@@ -57,11 +57,15 @@
     * @return è®¾å¤‡ä¿¡æ¯åˆ—表
     */
    @Select("SELECT d.id, d.device_name as deviceName, d.device_code as deviceCode, " +
            "d.device_type as deviceType, dgr.role, d.status, " +
            "d.last_heartbeat as lastHeartbeat, d.is_online as isOnline " +
            "d.device_type as deviceType, d.plc_ip as plcIp, dgr.role, d.status, " +
            "ds.last_heartbeat as lastHeartbeat, " +
            "CASE WHEN ds.status = 'ONLINE' THEN TRUE ELSE FALSE END as isOnline " +
            "FROM device_config d " +
            "INNER JOIN device_group_relation dgr ON d.id = dgr.device_id " +
            "WHERE dgr.group_id = #{groupId} AND dgr.is_deleted = 0 AND d.is_deleted = 0")
            "LEFT JOIN device_status ds ON d.device_id = ds.device_id " +
            "  AND ds.id = (SELECT MAX(id) FROM device_status WHERE device_id = d.device_id) " +
            "WHERE dgr.group_id = #{groupId} AND dgr.is_deleted = 0 AND d.is_deleted = 0 " +
            "ORDER BY dgr.connection_order ASC")
    List<DeviceGroupVO.DeviceInfo> getGroupDevices(@Param("groupId") Long groupId);
    /**
@@ -93,4 +97,21 @@
            "  AND d.is_deleted = 0 " +
            "ORDER BY IFNULL(dgr.connection_order, 0) ASC, dgr.id ASC")
    List<DeviceConfig> getOrderedDeviceConfigs(@Param("groupId") Long groupId);
    /**
     * èŽ·å–è®¾å¤‡ç»„ä¸‹çš„åœ¨çº¿è®¾å¤‡æ•°é‡
     *
     * @param groupId è®¾å¤‡ç»„ID
     * @return åœ¨çº¿è®¾å¤‡æ•°é‡
     */
    @Select("SELECT COUNT(DISTINCT d.id) " +
            "FROM device_config d " +
            "INNER JOIN device_group_relation dgr ON d.id = dgr.device_id " +
            "LEFT JOIN device_status ds ON d.device_id = ds.device_id " +
            "  AND ds.id = (SELECT MAX(id) FROM device_status WHERE device_id = d.device_id) " +
            "WHERE dgr.group_id = #{groupId} " +
            "  AND dgr.is_deleted = 0 " +
            "  AND d.is_deleted = 0 " +
            "  AND ds.status = 'ONLINE'")
    Integer getOnlineDeviceCountByGroupId(@Param("groupId") Long groupId);
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/mapper/DeviceStatusMapper.java
New file
@@ -0,0 +1,56 @@
package com.mes.device.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mes.device.entity.DeviceStatus;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.Date;
import java.util.List;
/**
 * è®¾å¤‡çŠ¶æ€Mapper
 */
@Mapper
public interface DeviceStatusMapper extends BaseMapper<DeviceStatus> {
    /**
     * æ ¹æ®è®¾å¤‡ID获取最新的设备状态
     */
    @Select("SELECT * FROM device_status WHERE device_id = #{deviceId} " +
            "ORDER BY id DESC LIMIT 1")
    DeviceStatus getLatestByDeviceId(@Param("deviceId") String deviceId);
    /**
     * æ›´æ–°è®¾å¤‡çŠ¶æ€ï¼ˆæ›´æ–°æœ€æ–°è®°å½•ï¼‰
     */
    @Update("UPDATE device_status ds1 " +
            "INNER JOIN (" +
            "  SELECT MAX(id) as max_id FROM device_status WHERE device_id = #{deviceId}" +
            ") ds2 ON ds1.id = ds2.max_id " +
            "SET ds1.status = #{status}, ds1.last_heartbeat = #{lastHeartbeat} " +
            "WHERE ds1.device_id = #{deviceId}")
    int updateLatestStatus(@Param("deviceId") String deviceId,
                          @Param("status") String status,
                          @Param("lastHeartbeat") Date lastHeartbeat);
    /**
     * æ ¹æ®è®¾å¤‡ID列表获取最新的设备状态
     */
    @Select("<script>" +
            "SELECT ds1.* FROM device_status ds1 " +
            "INNER JOIN (" +
            "  SELECT device_id, MAX(id) as max_id " +
            "  FROM device_status " +
            "  WHERE device_id IN " +
            "  <foreach collection='deviceIds' item='deviceId' open='(' separator=',' close=')'>" +
            "    #{deviceId}" +
            "  </foreach>" +
            "  GROUP BY device_id" +
            ") ds2 ON ds1.device_id = ds2.device_id AND ds1.id = ds2.max_id" +
            "</script>")
    List<DeviceStatus> getLatestByDeviceIds(@Param("deviceIds") List<String> deviceIds);
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/DeviceCoordinationService.java
New file
@@ -0,0 +1,166 @@
package com.mes.device.service;
import com.mes.device.entity.DeviceConfig;
import com.mes.device.entity.DeviceGroupConfig;
import com.mes.task.model.TaskExecutionContext;
import java.util.List;
import java.util.Map;
/**
 * è®¾å¤‡åè°ƒæœåŠ¡
 * è´Ÿè´£è®¾å¤‡é—´æ•°æ®ä¼ é€’、状态同步、依赖管理等协调工作
 *
 * @author mes
 * @since 2025-01-XX
 */
public interface DeviceCoordinationService {
    /**
     * åè°ƒè®¾å¤‡ç»„执行
     * æ ¹æ®è®¾å¤‡ä¾èµ–关系和执行顺序,协调多个设备的执行
     *
     * @param groupConfig è®¾å¤‡ç»„配置
     * @param devices è®¾å¤‡åˆ—表(已按执行顺序排序)
     * @param context ä»»åŠ¡æ‰§è¡Œä¸Šä¸‹æ–‡
     * @return åè°ƒç»“果,包含是否可以执行、依赖关系等信息
     */
    CoordinationResult coordinateExecution(DeviceGroupConfig groupConfig,
                                          List<DeviceConfig> devices,
                                          TaskExecutionContext context);
    /**
     * ä¼ é€’数据到下一个设备
     * å°†å½“前设备的数据传递给下一个设备
     *
     * @param fromDevice æºè®¾å¤‡
     * @param toDevice ç›®æ ‡è®¾å¤‡
     * @param data è¦ä¼ é€’的数据
     * @param context ä»»åŠ¡æ‰§è¡Œä¸Šä¸‹æ–‡
     * @return æ˜¯å¦ä¼ é€’成功
     */
    boolean transferData(DeviceConfig fromDevice,
                        DeviceConfig toDevice,
                        Map<String, Object> data,
                        TaskExecutionContext context);
    /**
     * åŒæ­¥è®¾å¤‡çŠ¶æ€
     * å°†è®¾å¤‡çŠ¶æ€åŒæ­¥åˆ°å…±äº«ä¸Šä¸‹æ–‡
     *
     * @param device è®¾å¤‡é…ç½®
     * @param status è®¾å¤‡çŠ¶æ€
     * @param context ä»»åŠ¡æ‰§è¡Œä¸Šä¸‹æ–‡
     */
    void syncDeviceStatus(DeviceConfig device,
                         DeviceStatus status,
                         TaskExecutionContext context);
    /**
     * æ£€æŸ¥è®¾å¤‡ä¾èµ–关系
     * æ£€æŸ¥è®¾å¤‡æ˜¯å¦æ»¡è¶³æ‰§è¡Œçš„前置条件
     *
     * @param device è®¾å¤‡é…ç½®
     * @param context ä»»åŠ¡æ‰§è¡Œä¸Šä¸‹æ–‡
     * @return ä¾èµ–检查结果
     */
    DependencyCheckResult checkDependencies(DeviceConfig device,
                                            TaskExecutionContext context);
    /**
     * èŽ·å–è®¾å¤‡ä¾èµ–çš„è®¾å¤‡åˆ—è¡¨
     *
     * @param device è®¾å¤‡é…ç½®
     * @param groupConfig è®¾å¤‡ç»„配置
     * @return ä¾èµ–的设备列表
     */
    List<DeviceConfig> getDependentDevices(DeviceConfig device,
                                           DeviceGroupConfig groupConfig);
    /**
     * åè°ƒç»“æžœ
     */
    class CoordinationResult {
        private final boolean canExecute;
        private final String message;
        private final Map<String, Object> metadata;
        public CoordinationResult(boolean canExecute, String message, Map<String, Object> metadata) {
            this.canExecute = canExecute;
            this.message = message;
            this.metadata = metadata;
        }
        public static CoordinationResult success(String message) {
            return new CoordinationResult(true, message, null);
        }
        public static CoordinationResult success(String message, Map<String, Object> metadata) {
            return new CoordinationResult(true, message, metadata);
        }
        public static CoordinationResult failure(String message) {
            return new CoordinationResult(false, message, null);
        }
        public boolean canExecute() {
            return canExecute;
        }
        public String getMessage() {
            return message;
        }
        public Map<String, Object> getMetadata() {
            return metadata;
        }
    }
    /**
     * è®¾å¤‡çŠ¶æ€
     */
    enum DeviceStatus {
        IDLE,           // ç©ºé—²
        READY,          // å°±ç»ª
        RUNNING,        // è¿è¡Œä¸­
        COMPLETED,      // å·²å®Œæˆ
        FAILED,         // å¤±è´¥
        WAITING         // ç­‰å¾…中
    }
    /**
     * ä¾èµ–检查结果
     */
    class DependencyCheckResult {
        private final boolean satisfied;
        private final String message;
        private final List<String> missingDependencies;
        public DependencyCheckResult(boolean satisfied, String message, List<String> missingDependencies) {
            this.satisfied = satisfied;
            this.message = message;
            this.missingDependencies = missingDependencies;
        }
        public static DependencyCheckResult satisfied() {
            return new DependencyCheckResult(true, "依赖条件满足", null);
        }
        public static DependencyCheckResult unsatisfied(String message, List<String> missingDependencies) {
            return new DependencyCheckResult(false, message, missingDependencies);
        }
        public boolean isSatisfied() {
            return satisfied;
        }
        public String getMessage() {
            return message;
        }
        public List<String> getMissingDependencies() {
            return missingDependencies;
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/DeviceGroupConfigService.java
@@ -6,10 +6,12 @@
import com.mes.device.vo.DeviceGroupConfigVO;
import com.mes.device.vo.DeviceGroupVO;
import com.mes.device.vo.StatisticsVO;
import org.springframework.stereotype.Service;
/**
 * è®¾å¤‡ç»„配置服务接口
 */
@Service
public interface DeviceGroupConfigService extends IService<DeviceGroupConfig> {
    /**
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/DeviceStatusService.java
New file
@@ -0,0 +1,42 @@
package com.mes.device.service;
import com.mes.device.entity.DeviceStatus;
import java.util.List;
/**
 * è®¾å¤‡çŠ¶æ€æœåŠ¡æŽ¥å£
 */
public interface DeviceStatusService {
    /**
     * æ ¹æ®è®¾å¤‡ID获取最新的设备状态
     */
    DeviceStatus getLatestByDeviceId(String deviceId);
    /**
     * æ ¹æ®è®¾å¤‡ID列表获取最新的设备状态
     */
    List<DeviceStatus> getLatestByDeviceIds(List<String> deviceIds);
    /**
     * æ›´æ–°è®¾å¤‡åœ¨çº¿çŠ¶æ€ï¼ˆæ‰‹åŠ¨è®¾ç½®ï¼‰
     */
    boolean updateDeviceOnlineStatus(Long deviceId, String status);
    /**
     * æ‰¹é‡æ›´æ–°è®¾å¤‡åœ¨çº¿çŠ¶æ€
     */
    boolean batchUpdateDeviceOnlineStatus(List<Long> deviceIds, String status);
    /**
     * è®°å½•设备心跳(自动更新在线状态)
     */
    boolean recordHeartbeat(String deviceId, String status);
    /**
     * æ ¹æ®è®¾å¤‡é…ç½®ID获取设备状态
     */
    DeviceStatus getLatestByDeviceConfigId(Long deviceConfigId);
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/GlassInfoService.java
New file
@@ -0,0 +1,64 @@
package com.mes.device.service;
import com.mes.device.entity.GlassInfo;
import java.util.List;
import java.util.Map;
/**
 * çŽ»ç’ƒä¿¡æ¯æœåŠ¡æŽ¥å£
 *
 * @author mes
 * @since 2024-11-20
 */
public interface GlassInfoService {
    /**
     * æ ¹æ®çŽ»ç’ƒID查询玻璃信息
     *
     * @param glassId çŽ»ç’ƒID
     * @return çŽ»ç’ƒä¿¡æ¯ï¼Œå¦‚æžœä¸å­˜åœ¨è¿”å›žnull
     */
    GlassInfo getGlassInfo(String glassId);
    /**
     * æ ¹æ®çŽ»ç’ƒID获取玻璃长度
     *
     * @param glassId çŽ»ç’ƒID
     * @return çŽ»ç’ƒé•¿åº¦ï¼ˆmm),如果不存在返回null
     */
    Integer getGlassLength(String glassId);
    /**
     * æ ¹æ®çŽ»ç’ƒID列表批量查询玻璃信息
     *
     * @param glassIds çŽ»ç’ƒID列表
     * @return çŽ»ç’ƒä¿¡æ¯åˆ—è¡¨
     */
    List<GlassInfo> getGlassInfos(List<String> glassIds);
    /**
     * æ ¹æ®çŽ»ç’ƒID列表批量获取玻璃长度映射
     *
     * @param glassIds çŽ»ç’ƒID列表
     * @return çŽ»ç’ƒID到长度的映射Map
     */
    Map<String, Integer> getGlassLengthMap(List<String> glassIds);
    /**
     * åˆ›å»ºæˆ–更新玻璃信息
     *
     * @param glassInfo çŽ»ç’ƒä¿¡æ¯
     * @return æ˜¯å¦æˆåŠŸ
     */
    boolean saveOrUpdateGlassInfo(GlassInfo glassInfo);
    /**
     * æ‰¹é‡åˆ›å»ºæˆ–更新玻璃信息
     *
     * @param glassInfos çŽ»ç’ƒä¿¡æ¯åˆ—è¡¨
     * @return æ˜¯å¦æˆåŠŸ
     */
    boolean batchSaveOrUpdateGlassInfo(List<GlassInfo> glassInfos);
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceConfigServiceImpl.java
@@ -27,6 +27,7 @@
public class DeviceConfigServiceImpl extends ServiceImpl<DeviceConfigMapper, DeviceConfig> implements DeviceConfigService {
    private final ObjectMapper objectMapper = new ObjectMapper();
    private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<Map<String, Object>>() {};
    @Override
    public boolean createDevice(DeviceConfig deviceConfig) {
@@ -214,7 +215,7 @@
            vo.setStatus(getStatusName(device.getStatus()));
            vo.setDeviceStatus(convertStatusToCode(device.getStatus()));
            vo.setDescription(device.getDescription());
            vo.setLocation("默认位置"); // TODO: ä»Žæ‰©å±•参数或关联表中获取
            vo.setLocation(extractLocationFromDevice(device));
            vo.setCreatedTime(device.getCreatedTime());
            vo.setUpdatedTime(device.getUpdatedTime());
            vo.setProjectId(device.getProjectId());
@@ -771,4 +772,34 @@
            return new ArrayList<>();
        }
    }
    /**
     * ä»Žè®¾å¤‡æ‰©å±•参数中提取位置信息
     */
    private String extractLocationFromDevice(DeviceConfig device) {
        if (device == null) {
            return "默认位置";
        }
        try {
            // ä¼˜å…ˆä»ŽextraParams中获取
            if (device.getExtraParams() != null && !device.getExtraParams().trim().isEmpty()) {
                Map<String, Object> extraParams = objectMapper.readValue(device.getExtraParams(), MAP_TYPE);
                Object location = extraParams.get("location");
                if (location != null) {
                    return String.valueOf(location);
                }
            }
            // ä»ŽconfigJson中获取
            if (device.getConfigJson() != null && !device.getConfigJson().trim().isEmpty()) {
                Map<String, Object> configJson = objectMapper.readValue(device.getConfigJson(), MAP_TYPE);
                Object location = configJson.get("location");
                if (location != null) {
                    return String.valueOf(location);
                }
            }
        } catch (Exception e) {
            log.warn("解析设备位置信息失败, deviceId={}", device.getId(), e);
        }
        return "默认位置";
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceCoordinationServiceImpl.java
New file
@@ -0,0 +1,233 @@
package com.mes.device.service.impl;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mes.device.entity.DeviceConfig;
import com.mes.device.entity.DeviceGroupConfig;
import com.mes.device.service.DeviceCoordinationService;
import com.mes.task.model.TaskExecutionContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.*;
import java.util.stream.Collectors;
/**
 * è®¾å¤‡åè°ƒæœåŠ¡å®žçŽ°
 *
 * @author mes
 * @since 2025-01-XX
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class DeviceCoordinationServiceImpl implements DeviceCoordinationService {
    private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<Map<String, Object>>() {};
    private final ObjectMapper objectMapper;
    @Override
    public CoordinationResult coordinateExecution(DeviceGroupConfig groupConfig,
                                                  List<DeviceConfig> devices,
                                                  TaskExecutionContext context) {
        if (CollectionUtils.isEmpty(devices)) {
            return CoordinationResult.failure("设备列表为空");
        }
        // æ£€æŸ¥æ‰€æœ‰è®¾å¤‡çš„依赖关系
        List<String> unsatisfiedDevices = new ArrayList<>();
        for (DeviceConfig device : devices) {
            DependencyCheckResult checkResult = checkDependencies(device, context);
            if (!checkResult.isSatisfied()) {
                unsatisfiedDevices.add(device.getDeviceName() + "(" + device.getDeviceCode() + ")");
                log.warn("设备依赖检查失败: deviceId={}, message={}", device.getId(), checkResult.getMessage());
            }
        }
        if (!unsatisfiedDevices.isEmpty()) {
            String message = "以下设备的依赖条件不满足: " + String.join(", ", unsatisfiedDevices);
            return CoordinationResult.failure(message);
        }
        // æž„建协调元数据
        Map<String, Object> metadata = new HashMap<>();
        metadata.put("deviceCount", devices.size());
        metadata.put("executionOrder", devices.stream()
            .map(d -> d.getDeviceName() + "(" + d.getDeviceCode() + ")")
            .collect(Collectors.toList()));
        return CoordinationResult.success("设备协调成功,可以开始执行", metadata);
    }
    @Override
    public boolean transferData(DeviceConfig fromDevice,
                               DeviceConfig toDevice,
                               Map<String, Object> data,
                               TaskExecutionContext context) {
        if (fromDevice == null || toDevice == null || data == null || context == null) {
            log.warn("数据传递参数不完整");
            return false;
        }
        try {
            // å°†æ•°æ®å­˜å‚¨åˆ°å…±äº«ä¸Šä¸‹æ–‡ä¸­
            String dataKey = String.format("device_%s_to_%s", fromDevice.getId(), toDevice.getId());
            context.getSharedData().put(dataKey, data);
            // æ ¹æ®è®¾å¤‡ç±»åž‹ï¼Œæå–关键数据并更新上下文
            if (DeviceConfig.DeviceType.LOAD_VEHICLE.equals(fromDevice.getDeviceType())) {
                // ä¸Šå¤§è½¦è®¾å¤‡å®Œæˆï¼Œä¼ é€’玻璃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={}",
                        fromDevice.getDeviceCode(), toDevice.getDeviceCode(), ids);
                }
            } else if (DeviceConfig.DeviceType.LARGE_GLASS.equals(fromDevice.getDeviceType())) {
                // å¤§ç†ç‰‡è®¾å¤‡å®Œæˆï¼Œä¼ é€’处理后的玻璃ID列表
                Object glassIds = data.get("glassIds");
                if (glassIds instanceof List) {
                    @SuppressWarnings("unchecked")
                    List<String> ids = (List<String>) glassIds;
                    context.setProcessedGlassIds(new ArrayList<>(ids));
                    log.info("大理片设备数据传递: fromDevice={}, toDevice={}, glassIds={}",
                        fromDevice.getDeviceCode(), toDevice.getDeviceCode(), ids);
                }
            }
            // å­˜å‚¨é€šç”¨æ•°æ®
            context.getSharedData().put("lastTransferFrom", fromDevice.getId());
            context.getSharedData().put("lastTransferTo", toDevice.getId());
            context.getSharedData().put("lastTransferTime", System.currentTimeMillis());
            return true;
        } catch (Exception e) {
            log.error("数据传递失败: fromDevice={}, toDevice={}",
                fromDevice.getDeviceCode(), toDevice.getDeviceCode(), e);
            return false;
        }
    }
    @Override
    public void syncDeviceStatus(DeviceConfig device,
                                DeviceStatus status,
                                TaskExecutionContext context) {
        if (device == null || context == null) {
            return;
        }
        String statusKey = String.format("device_%s_status", device.getId());
        context.getSharedData().put(statusKey, status.name());
        context.getSharedData().put(statusKey + "_time", System.currentTimeMillis());
        // æ›´æ–°è®¾å¤‡çŠ¶æ€æ˜ å°„
        @SuppressWarnings("unchecked")
        Map<Long, String> deviceStatusMap = (Map<Long, String>) context.getSharedData()
            .computeIfAbsent("deviceStatusMap", k -> new HashMap<Long, String>());
        deviceStatusMap.put(device.getId(), status.name());
        log.debug("设备状态同步: deviceId={}, status={}", device.getId(), status);
    }
    @Override
    public DependencyCheckResult checkDependencies(DeviceConfig device,
                                                  TaskExecutionContext context) {
        if (device == null || context == null) {
            return DependencyCheckResult.unsatisfied("设备或上下文为空", Collections.emptyList());
        }
        List<String> missingDependencies = new ArrayList<>();
        // æ£€æŸ¥è®¾å¤‡ç±»åž‹ç‰¹å®šçš„依赖
        String deviceType = device.getDeviceType();
        if (DeviceConfig.DeviceType.LARGE_GLASS.equals(deviceType)) {
            // å¤§ç†ç‰‡è®¾å¤‡éœ€è¦ä¸Šå¤§è½¦è®¾å¤‡å…ˆå®Œæˆ
            List<String> loadedGlassIds = context.getSafeLoadedGlassIds();
            if (CollectionUtils.isEmpty(loadedGlassIds)) {
                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)) {
                missingDependencies.add("前置设备未完成,缺少玻璃ID列表");
            }
        }
        // æ£€æŸ¥è®¾å¤‡é…ç½®ä¸­çš„依赖关系(从extraParams中读取)
        Map<String, Object> deviceDependencies = getDeviceDependencies(device);
        if (!CollectionUtils.isEmpty(deviceDependencies)) {
            for (Map.Entry<String, Object> entry : deviceDependencies.entrySet()) {
                String depDeviceCode = entry.getKey();
                Object depStatus = entry.getValue();
                // æ£€æŸ¥ä¾èµ–设备的状态
                @SuppressWarnings("unchecked")
                Map<Long, String> deviceStatusMap = (Map<Long, String>) context.getSharedData()
                    .get("deviceStatusMap");
                if (deviceStatusMap != null) {
                    // è¿™é‡Œç®€åŒ–处理,实际应该根据deviceCode查找设备ID
                    // æš‚时跳过基于设备代码的依赖检查
                }
            }
        }
        if (missingDependencies.isEmpty()) {
            return DependencyCheckResult.satisfied();
        }
        return DependencyCheckResult.unsatisfied(
            "设备依赖条件不满足: " + String.join(", ", missingDependencies),
            missingDependencies
        );
    }
    @Override
    public List<DeviceConfig> getDependentDevices(DeviceConfig device,
                                                  DeviceGroupConfig groupConfig) {
        if (device == null || groupConfig == null) {
            return Collections.emptyList();
        }
        // ä»Žè®¾å¤‡é…ç½®ä¸­è¯»å–依赖关系
        Map<String, Object> deviceDependencies = getDeviceDependencies(device);
        if (CollectionUtils.isEmpty(deviceDependencies)) {
            return Collections.emptyList();
        }
        // è¿™é‡Œéœ€è¦æ ¹æ®deviceCode查找对应的DeviceConfig
        // ç®€åŒ–实现,返回空列表
        // å®žé™…应该查询设备组中的所有设备,然后根据deviceCode匹配
        log.debug("获取设备依赖: deviceId={}, dependencies={}", device.getId(), deviceDependencies);
        return Collections.emptyList();
    }
    /**
     * ä»Žè®¾å¤‡é…ç½®ä¸­èŽ·å–ä¾èµ–å…³ç³»
     */
    private Map<String, Object> getDeviceDependencies(DeviceConfig device) {
        String extraParams = device.getExtraParams();
        if (!StringUtils.hasText(extraParams)) {
            return Collections.emptyMap();
        }
        try {
            Map<String, Object> extraParamsMap = objectMapper.readValue(extraParams, MAP_TYPE);
            @SuppressWarnings("unchecked")
            Map<String, Object> dependencies = (Map<String, Object>) extraParamsMap.get("dependencies");
            return dependencies != null ? dependencies : Collections.emptyMap();
        } catch (Exception e) {
            log.warn("解析设备依赖关系失败, deviceId={}", device.getId(), e);
            return Collections.emptyMap();
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceGroupConfigServiceImpl.java
@@ -7,7 +7,9 @@
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mes.device.entity.DeviceGroupConfig;
import com.mes.device.entity.DeviceGroupRelation;
import com.mes.device.mapper.DeviceGroupConfigMapper;
import com.mes.device.mapper.DeviceGroupRelationMapper;
import com.mes.device.service.DeviceGroupConfigService;
import com.mes.device.vo.DeviceGroupConfigVO;
import com.mes.device.vo.DeviceGroupVO;
@@ -27,6 +29,12 @@
public class DeviceGroupConfigServiceImpl extends ServiceImpl<DeviceGroupConfigMapper, DeviceGroupConfig> implements DeviceGroupConfigService {
    private final ObjectMapper objectMapper = new ObjectMapper();
    private final DeviceGroupRelationMapper deviceGroupRelationMapper;
    private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<Map<String, Object>>() {};
    public DeviceGroupConfigServiceImpl(DeviceGroupRelationMapper deviceGroupRelationMapper) {
        this.deviceGroupRelationMapper = deviceGroupRelationMapper;
    }
    @Override
    public boolean createDeviceGroup(DeviceGroupConfig groupConfig) {
@@ -194,6 +202,7 @@
                vo.setGroupType(getGroupTypeName(group.getGroupType()));
                vo.setStatus(getStatusName(group.getStatus()));
                vo.setDeviceCount(getDeviceCountByGroupId(group.getId()));
                vo.setOnlineDeviceCount(getOnlineDeviceCountByGroupId(group.getId()));
                vo.setCreateTime(group.getCreatedTime());
                vo.setProjectId(group.getProjectId());
                return vo;
@@ -212,7 +221,6 @@
    @Override
    public List<DeviceGroupConfigVO.GroupInfo> getDeviceGroupVOList(Long projectId, Integer groupType, Integer status) {
        // TODO: è¿™é‡Œéœ€è¦å®žçްVO转换逻辑,包括设备数量统计
        List<DeviceGroupConfig> groupList = getDeviceGroupList(projectId, groupType, status);
        
        return groupList.stream().map(group -> {
@@ -225,8 +233,8 @@
            vo.setStatus(getStatusName(group.getStatus()));
            vo.setDeviceCount(getDeviceCountByGroupId(group.getId()));
            vo.setIsEnabled(group.getStatus() != null && group.getStatus() == DeviceGroupConfig.Status.ENABLED);
            vo.setLocation("默认位置"); // TODO: ä»Žæ‰©å±•配置或关联表中获取
            vo.setSupervisor("默认管理员"); // TODO: ä»Žæ‰©å±•配置或关联表中获取
            vo.setLocation(extractLocationFromExtraConfig(group));
            vo.setSupervisor(extractSupervisorFromExtraConfig(group));
            vo.setCreatedTime(new Date());
            vo.setUpdatedTime(new Date());
            vo.setProjectId(group.getProjectId());
@@ -425,9 +433,37 @@
    @Override
    public int getDeviceCountByGroupId(Long groupId) {
        // è¿™é‡Œéœ€è¦æŸ¥è¯¢device_group_relation表来获取设备数量
        // ç®€åŒ–实现,实际需要注入DeviceGroupRelationMapper
        return 0; // TODO: å®žçŽ°çœŸå®žé€»è¾‘
        if (groupId == null) {
            return 0;
        }
        try {
            LambdaQueryWrapper<DeviceGroupRelation> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(DeviceGroupRelation::getGroupId, groupId)
                   .eq(DeviceGroupRelation::getIsDeleted, 0);
            return (int) deviceGroupRelationMapper.selectCount(wrapper);
        } catch (Exception e) {
            log.error("获取设备组设备数量失败, groupId={}", groupId, e);
            return 0;
        }
    }
    /**
     * èŽ·å–è®¾å¤‡ç»„ä¸‹çš„åœ¨çº¿è®¾å¤‡æ•°é‡
     *
     * @param groupId è®¾å¤‡ç»„ID
     * @return åœ¨çº¿è®¾å¤‡æ•°é‡
     */
    private int getOnlineDeviceCountByGroupId(Long groupId) {
        if (groupId == null) {
            return 0;
        }
        try {
            Integer count = deviceGroupRelationMapper.getOnlineDeviceCountByGroupId(groupId);
            return count != null ? count : 0;
        } catch (Exception e) {
            log.error("获取设备组在线设备数量失败, groupId={}", groupId, e);
            return 0;
        }
    }
    /**
@@ -577,8 +613,8 @@
            // èŽ·å–è®¾å¤‡ç»„ä¸‹çš„è®¾å¤‡æ•°é‡
            int totalDevices = getDeviceCountByGroupId(groupId);
            
            // èŽ·å–åœ¨çº¿è®¾å¤‡æ•°é‡
            int activeDevices = 0; // TODO: éœ€è¦æ ¹æ®å®žé™…业务逻辑实现
            // èŽ·å–åœ¨çº¿è®¾å¤‡æ•°é‡ï¼ˆçŠ¶æ€ä¸ºæ­£å¸¸çš„è®¾å¤‡æ•°ï¼‰
            int activeDevices = getActiveDeviceCountByGroupId(groupId);
            
            // è®¡ç®—平均性能指标(模拟数据)
            double averageCpuUsage = 45.5; // CPU使用率百分比
@@ -628,7 +664,7 @@
            // èŽ·å–è®¾å¤‡ç»„ä¸‹çš„è®¾å¤‡ä¿¡æ¯
            int totalDevices = getDeviceCountByGroupId(groupId);
            int onlineDevices = 0; // TODO: éœ€è¦æ ¹æ®å®žé™…业务逻辑实现
            int onlineDevices = getActiveDeviceCountByGroupId(groupId);
            int offlineDevices = totalDevices - onlineDevices;
            
            // è®¡ç®—健康状态
@@ -792,6 +828,59 @@
        }
    }
    /**
     * ä»Žæ‰©å±•配置中提取位置信息
     */
    private String extractLocationFromExtraConfig(DeviceGroupConfig group) {
        if (group == null || group.getExtraConfig() == null) {
            return "默认位置";
        }
        try {
            Map<String, Object> extraConfig = objectMapper.readValue(group.getExtraConfig(), MAP_TYPE);
            Object location = extraConfig.get("location");
            return location != null ? String.valueOf(location) : "默认位置";
        } catch (Exception e) {
            log.warn("解析设备组位置信息失败, groupId={}", group.getId(), e);
            return "默认位置";
        }
    }
    /**
     * ä»Žæ‰©å±•配置中提取管理员信息
     */
    private String extractSupervisorFromExtraConfig(DeviceGroupConfig group) {
        if (group == null || group.getExtraConfig() == null) {
            return "默认管理员";
        }
        try {
            Map<String, Object> extraConfig = objectMapper.readValue(group.getExtraConfig(), MAP_TYPE);
            Object supervisor = extraConfig.get("supervisor");
            return supervisor != null ? String.valueOf(supervisor) : "默认管理员";
        } catch (Exception e) {
            log.warn("解析设备组管理员信息失败, groupId={}", group.getId(), e);
            return "默认管理员";
        }
    }
    /**
     * èŽ·å–è®¾å¤‡ç»„ä¸­æ´»è·ƒè®¾å¤‡æ•°é‡ï¼ˆçŠ¶æ€ä¸ºæ­£å¸¸çš„è®¾å¤‡ï¼‰
     */
    private int getActiveDeviceCountByGroupId(Long groupId) {
        if (groupId == null) {
            return 0;
        }
        try {
            LambdaQueryWrapper<DeviceGroupRelation> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(DeviceGroupRelation::getGroupId, groupId)
                   .eq(DeviceGroupRelation::getStatus, DeviceGroupRelation.Status.NORMAL)
                   .eq(DeviceGroupRelation::getIsDeleted, 0);
            return (int) deviceGroupRelationMapper.selectCount(wrapper);
        } catch (Exception e) {
            log.error("获取设备组活跃设备数量失败, groupId={}", groupId, e);
            return 0;
        }
    }
    @Override
    public java.util.List<String> getAllGroupStatuses() {
        try {
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DevicePlcOperationServiceImpl.java
@@ -16,7 +16,6 @@
import org.springframework.util.CollectionUtils;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -40,6 +39,22 @@
    private final DeviceGroupRelationService deviceGroupRelationService;
    private final PlcTestWriteService plcTestWriteService;
    private final ObjectMapper objectMapper;
    public enum PlcOperationType {
        REQUEST("PLC请求", "PLC è¯·æ±‚发送成功", "PLC è¯·æ±‚发送失败"),
        REPORT("PLC汇报", "PLC æ±‡æŠ¥æ¨¡æ‹ŸæˆåŠŸ", "PLC æ±‡æŠ¥æ¨¡æ‹Ÿå¤±è´¥"),
        RESET("PLC重置", "PLC çŠ¶æ€å·²é‡ç½®", "PLC çŠ¶æ€é‡ç½®å¤±è´¥");
        private final String display;
        private final String successMsg;
        private final String failedMsg;
        PlcOperationType(String display, String successMsg, String failedMsg) {
            this.display = display;
            this.successMsg = successMsg;
            this.failedMsg = failedMsg;
        }
    }
    @Override
    public DevicePlcVO.OperationResult triggerRequest(Long deviceId) {
@@ -280,20 +295,6 @@
        throw new IllegalStateException("无法解析设备的 PLC é¡¹ç›®æ ‡è¯†, deviceId=" + device.getId());
    }
    public enum PlcOperationType {
        REQUEST("PLC请求", "PLC è¯·æ±‚发送成功", "PLC è¯·æ±‚发送失败"),
        REPORT("PLC汇报", "PLC æ±‡æŠ¥æ¨¡æ‹ŸæˆåŠŸ", "PLC æ±‡æŠ¥æ¨¡æ‹Ÿå¤±è´¥"),
        RESET("PLC重置", "PLC çŠ¶æ€å·²é‡ç½®", "PLC çŠ¶æ€é‡ç½®å¤±è´¥");
        private final String display;
        private final String successMsg;
        private final String failedMsg;
        PlcOperationType(String display, String successMsg, String failedMsg) {
            this.display = display;
            this.successMsg = successMsg;
            this.failedMsg = failedMsg;
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceStatusServiceImpl.java
New file
@@ -0,0 +1,201 @@
package com.mes.device.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mes.device.entity.DeviceConfig;
import com.mes.device.entity.DeviceStatus;
import com.mes.device.mapper.DeviceConfigMapper;
import com.mes.device.mapper.DeviceStatusMapper;
import com.mes.device.service.DeviceConfigService;
import com.mes.device.service.DeviceStatusService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
 * è®¾å¤‡çŠ¶æ€æœåŠ¡å®žçŽ°ç±»
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class DeviceStatusServiceImpl extends ServiceImpl<DeviceStatusMapper, DeviceStatus>
        implements DeviceStatusService {
    private final DeviceConfigService deviceConfigService;
    private final DeviceConfigMapper deviceConfigMapper;
    @Override
    public DeviceStatus getLatestByDeviceId(String deviceId) {
        if (deviceId == null || deviceId.trim().isEmpty()) {
            return null;
        }
        try {
            return baseMapper.getLatestByDeviceId(deviceId.trim());
        } catch (Exception e) {
            log.error("获取设备状态失败, deviceId={}", deviceId, e);
            return null;
        }
    }
    @Override
    public List<DeviceStatus> getLatestByDeviceIds(List<String> deviceIds) {
        if (deviceIds == null || deviceIds.isEmpty()) {
            return new ArrayList<>();
        }
        try {
            List<String> validIds = deviceIds.stream()
                    .filter(id -> id != null && !id.trim().isEmpty())
                    .map(String::trim)
                    .distinct()
                    .collect(Collectors.toList());
            if (validIds.isEmpty()) {
                return new ArrayList<>();
            }
            return baseMapper.getLatestByDeviceIds(validIds);
        } catch (Exception e) {
            log.error("批量获取设备状态失败, deviceIds={}", deviceIds, e);
            return new ArrayList<>();
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean updateDeviceOnlineStatus(Long deviceId, String status) {
        if (deviceId == null) {
            log.warn("设备ID不能为空");
            return false;
        }
        if (status == null || status.trim().isEmpty()) {
            log.warn("设备状态不能为空");
            return false;
        }
        try {
            // èŽ·å–è®¾å¤‡é…ç½®
            DeviceConfig device = deviceConfigService.getDeviceById(deviceId);
            if (device == null) {
                log.warn("设备不存在: deviceId={}", deviceId);
                return false;
            }
            String deviceIdStr = device.getDeviceId();
            if (deviceIdStr == null || deviceIdStr.trim().isEmpty()) {
                log.warn("设备配置中device_id字段为空: id={}", deviceId);
                return false;
            }
            // æ£€æŸ¥æ˜¯å¦å·²æœ‰çŠ¶æ€è®°å½•
            DeviceStatus existing = getLatestByDeviceId(deviceIdStr);
            Date now = new Date();
            if (existing != null) {
                // æ›´æ–°çŽ°æœ‰è®°å½•
                existing.setStatus(status);
                existing.setLastHeartbeat(now);
                boolean result = updateById(existing);
                if (result) {
                    log.info("更新设备在线状态成功: deviceId={}, status={}", deviceId, status);
                }
                return result;
            } else {
                // åˆ›å»ºæ–°è®°å½•
                DeviceStatus newStatus = new DeviceStatus();
                newStatus.setDeviceId(deviceIdStr);
                newStatus.setStatus(status);
                newStatus.setLastHeartbeat(now);
                newStatus.setCreatedTime(now);
                boolean result = save(newStatus);
                if (result) {
                    log.info("创建设备状态记录成功: deviceId={}, status={}", deviceId, status);
                }
                return result;
            }
        } catch (Exception e) {
            log.error("更新设备在线状态失败: deviceId={}, status={}", deviceId, status, e);
            return false;
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean batchUpdateDeviceOnlineStatus(List<Long> deviceIds, String status) {
        if (deviceIds == null || deviceIds.isEmpty()) {
            log.warn("设备ID列表不能为空");
            return false;
        }
        if (status == null || status.trim().isEmpty()) {
            log.warn("设备状态不能为空");
            return false;
        }
        try {
            boolean allSuccess = true;
            for (Long deviceId : deviceIds) {
                if (!updateDeviceOnlineStatus(deviceId, status)) {
                    allSuccess = false;
                }
            }
            log.info("批量更新设备在线状态完成: deviceIds={}, status={}, success={}",
                    deviceIds.size(), status, allSuccess);
            return allSuccess;
        } catch (Exception e) {
            log.error("批量更新设备在线状态失败: deviceIds={}, status={}", deviceIds, status, e);
            return false;
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean recordHeartbeat(String deviceId, String status) {
        if (deviceId == null || deviceId.trim().isEmpty()) {
            return false;
        }
        if (status == null || status.trim().isEmpty()) {
            status = DeviceStatus.Status.ONLINE; // é»˜è®¤åœ¨çº¿
        }
        try {
            Date now = new Date();
            DeviceStatus existing = getLatestByDeviceId(deviceId);
            if (existing != null) {
                existing.setStatus(status);
                existing.setLastHeartbeat(now);
                return updateById(existing);
            } else {
                DeviceStatus newStatus = new DeviceStatus();
                newStatus.setDeviceId(deviceId);
                newStatus.setStatus(status);
                newStatus.setLastHeartbeat(now);
                newStatus.setCreatedTime(now);
                return save(newStatus);
            }
        } catch (Exception e) {
            log.error("记录设备心跳失败: deviceId={}, status={}", deviceId, status, e);
            return false;
        }
    }
    @Override
    public DeviceStatus getLatestByDeviceConfigId(Long deviceConfigId) {
        if (deviceConfigId == null) {
            return null;
        }
        try {
            DeviceConfig device = deviceConfigService.getDeviceById(deviceConfigId);
            if (device == null || device.getDeviceId() == null) {
                return null;
            }
            return getLatestByDeviceId(device.getDeviceId());
        } catch (Exception e) {
            log.error("根据设备配置ID获取设备状态失败: deviceConfigId={}", deviceConfigId, e);
            return null;
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/GlassInfoServiceImpl.java
New file
@@ -0,0 +1,128 @@
package com.mes.device.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mes.device.entity.GlassInfo;
import com.mes.device.mapper.DeviceGlassInfoMapper;
import com.mes.device.service.GlassInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
 * çŽ»ç’ƒä¿¡æ¯æœåŠ¡å®žçŽ°ç±»
 *
 * @author mes
 * @since 2024-11-20
 */
@Slf4j
@Service("deviceGlassInfoService")
public class GlassInfoServiceImpl extends ServiceImpl<DeviceGlassInfoMapper, GlassInfo> implements GlassInfoService {
    @Override
    public GlassInfo getGlassInfo(String glassId) {
        if (glassId == null || glassId.trim().isEmpty()) {
            return null;
        }
        try {
            return baseMapper.selectByGlassId(glassId.trim());
        } catch (Exception e) {
            log.error("查询玻璃信息失败, glassId={}", glassId, e);
            return null;
        }
    }
    @Override
    public Integer getGlassLength(String glassId) {
        GlassInfo glassInfo = getGlassInfo(glassId);
        return glassInfo != null ? glassInfo.getGlassLength() : null;
    }
    @Override
    public List<GlassInfo> getGlassInfos(List<String> glassIds) {
        if (glassIds == null || glassIds.isEmpty()) {
            return Collections.emptyList();
        }
        try {
            // è¿‡æ»¤ç©ºå€¼å¹¶åŽ»é‡
            List<String> validIds = glassIds.stream()
                    .filter(id -> id != null && !id.trim().isEmpty())
                    .map(String::trim)
                    .distinct()
                    .collect(Collectors.toList());
            if (validIds.isEmpty()) {
                return Collections.emptyList();
            }
            return baseMapper.selectByGlassIds(validIds);
        } catch (Exception e) {
            log.error("批量查询玻璃信息失败, glassIds={}", glassIds, e);
            return Collections.emptyList();
        }
    }
    @Override
    public Map<String, Integer> getGlassLengthMap(List<String> glassIds) {
        Map<String, Integer> lengthMap = new HashMap<>();
        if (glassIds == null || glassIds.isEmpty()) {
            return lengthMap;
        }
        try {
            List<GlassInfo> glassInfos = getGlassInfos(glassIds);
            for (GlassInfo glassInfo : glassInfos) {
                if (glassInfo.getGlassId() != null && glassInfo.getGlassLength() != null) {
                    lengthMap.put(glassInfo.getGlassId(), glassInfo.getGlassLength());
                }
            }
        } catch (Exception e) {
            log.error("获取玻璃长度映射失败, glassIds={}", glassIds, e);
        }
        return lengthMap;
    }
    @Override
    public boolean saveOrUpdateGlassInfo(GlassInfo glassInfo) {
        if (glassInfo == null || glassInfo.getGlassId() == null) {
            return false;
        }
        try {
            // æ£€æŸ¥æ˜¯å¦å·²å­˜åœ¨
            GlassInfo existing = baseMapper.selectByGlassId(glassInfo.getGlassId());
            if (existing != null) {
                glassInfo.setId(existing.getId());
                return updateById(glassInfo);
            } else {
                return save(glassInfo);
            }
        } catch (Exception e) {
            log.error("保存或更新玻璃信息失败, glassInfo={}", glassInfo, e);
            return false;
        }
    }
    @Override
    public boolean batchSaveOrUpdateGlassInfo(List<GlassInfo> glassInfos) {
        if (glassInfos == null || glassInfos.isEmpty()) {
            return true;
        }
        try {
            for (GlassInfo glassInfo : glassInfos) {
                saveOrUpdateGlassInfo(glassInfo);
            }
            return true;
        } catch (Exception e) {
            log.error("批量保存或更新玻璃信息失败", e);
            return false;
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/vo/DeviceGroupVO.java
@@ -26,6 +26,7 @@
        private String deviceName;
        private String deviceCode;
        private String deviceType;
        private String plcIp;
        private String deviceRole;
        private String status;
        private Date lastHeartbeat;
@@ -45,6 +46,7 @@
        private String groupType;
        private String status;
        private Integer deviceCount;
        private Integer onlineDeviceCount;
        private Date createTime;
        private Long projectId;
    }
mes-processes/mes-plcSend/src/main/java/com/mes/device/¶àÉ豸ÁªºÏ²âÊÔÀ©Õ¹·½°¸.md
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/entity/PlcAddress.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/entity/PlcBaseData.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/entity/PlcTestTask.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/README.md
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/GlassStorageInteraction.java
@@ -24,15 +24,57 @@
    @Override
    public InteractionResult execute(InteractionContext context) {
        List<String> processed = context.getProcessedGlassIds();
        if (CollectionUtils.isEmpty(processed)) {
            return InteractionResult.waitResult("没有可存储的玻璃", null);
        }
        try {
            // å‰ç½®æ¡ä»¶éªŒè¯
            if (context.getCurrentDevice() == null) {
                return InteractionResult.fail("设备配置不存在");
            }
        Map<String, Object> data = new HashMap<>();
        data.put("storedCount", processed.size());
        data.put("storedGlasses", processed);
        return InteractionResult.success(data);
            // ä¼˜å…ˆä½¿ç”¨å¤„理后的玻璃ID,如果没有则使用上大车的玻璃ID
            List<String> processed = context.getProcessedGlassIds();
            if (CollectionUtils.isEmpty(processed)) {
                processed = context.getLoadedGlassIds();
                if (CollectionUtils.isEmpty(processed)) {
                    // å°è¯•从共享数据获取
                    Object processedGlasses = context.getSharedData().get("processedGlasses");
                    if (processedGlasses instanceof List) {
                        @SuppressWarnings("unchecked")
                        List<String> list = (List<String>) processedGlasses;
                        processed = list;
                    }
                }
            }
            if (CollectionUtils.isEmpty(processed)) {
                return InteractionResult.waitResult("没有可存储的玻璃", null);
            }
            // éªŒè¯çŽ»ç’ƒID
            for (String glassId : processed) {
                if (glassId == null || glassId.trim().isEmpty()) {
                    return InteractionResult.fail("玻璃ID不能为空");
                }
            }
            // æ‰§è¡Œå­˜å‚¨æ“ä½œ
            context.getSharedData().put("storedGlasses", processed);
            context.getSharedData().put("storageTime", System.currentTimeMillis());
            // åŽç½®æ¡ä»¶æ£€æŸ¥
            Object stored = context.getSharedData().get("storedGlasses");
            if (stored == null) {
                return InteractionResult.fail("玻璃存储失败:存储数据为空");
            }
            Map<String, Object> data = new HashMap<>();
            data.put("storedCount", processed.size());
            data.put("storedGlasses", processed);
            data.put("deviceId", context.getCurrentDevice().getId());
            data.put("deviceCode", context.getCurrentDevice().getDeviceCode());
            return InteractionResult.success(data);
        } catch (Exception e) {
            return InteractionResult.fail("玻璃存储交互执行异常: " + e.getMessage());
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/LargeGlassInteraction.java
@@ -25,20 +25,50 @@
    @Override
    public InteractionResult execute(InteractionContext context) {
        Object source = context.getSharedData().get("glassesFromVehicle");
        List<String> glassQueue = castList(source);
        if (CollectionUtils.isEmpty(glassQueue)) {
            return InteractionResult.waitResult("等待上大车输出", null);
        try {
            // å‰ç½®æ¡ä»¶éªŒè¯
            if (context.getCurrentDevice() == null) {
                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);
                }
            }
            // éªŒè¯çŽ»ç’ƒID
            for (String glassId : glassQueue) {
                if (glassId == null || glassId.trim().isEmpty()) {
                    return InteractionResult.fail("玻璃ID不能为空");
                }
            }
            // æ‰§è¡Œå¤§ç†ç‰‡å¤„理
            List<String> processed = new ArrayList<>(glassQueue);
            context.setProcessedGlassIds(processed);
            context.getSharedData().put("processedGlasses", processed);
            context.getSharedData().put("largeGlassProcessTime", System.currentTimeMillis());
            // åŽç½®æ¡ä»¶æ£€æŸ¥
            if (context.getProcessedGlassIds().isEmpty()) {
                return InteractionResult.fail("大理片处理失败:处理后的玻璃ID列表为空");
            }
            Map<String, Object> data = new HashMap<>();
            data.put("processedCount", processed.size());
            data.put("processedGlasses", processed);
            data.put("deviceId", context.getCurrentDevice().getId());
            data.put("deviceCode", context.getCurrentDevice().getDeviceCode());
            return InteractionResult.success(data);
        } catch (Exception e) {
            return InteractionResult.fail("大理片交互执行异常: " + e.getMessage());
        }
        List<String> processed = new ArrayList<>(glassQueue);
        context.setProcessedGlassIds(processed);
        context.getSharedData().put("processedGlasses", processed);
        Map<String, Object> data = new HashMap<>();
        data.put("processedCount", processed.size());
        data.put("processedGlasses", processed);
        return InteractionResult.success(data);
    }
    @SuppressWarnings("unchecked")
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/LoadVehicleInteraction.java
@@ -1,9 +1,12 @@
package com.mes.interaction.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 lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
@@ -16,7 +19,10 @@
 * ä¸Šå¤§è½¦äº¤äº’实现
 */
@Component
@RequiredArgsConstructor
public class LoadVehicleInteraction implements DeviceInteraction {
    private final DeviceInteractionService deviceInteractionService;
    @Override
    public String getDeviceType() {
@@ -25,19 +31,65 @@
    @Override
    public InteractionResult execute(InteractionContext context) {
        List<String> glassIds = context.getParameters().getGlassIds();
        if (CollectionUtils.isEmpty(glassIds)) {
            return InteractionResult.waitResult("未提供玻璃ID,等待输入", null);
        try {
            // å‰ç½®æ¡ä»¶éªŒè¯
            if (context.getCurrentDevice() == null) {
                return InteractionResult.fail("设备配置不存在");
            }
            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不能为空");
                }
            }
            // æž„建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);
            // æ‰§è¡Œå®žé™…çš„PLC写入操作
            DevicePlcVO.OperationResult plcResult = deviceInteractionService.executeOperation(
                    context.getCurrentDevice().getId(),
                    "feedGlass",
                    params
            );
            // æ£€æŸ¥PLC写入结果
            if (plcResult == null || !Boolean.TRUE.equals(plcResult.getSuccess())) {
                String errorMsg = plcResult != null ? plcResult.getMessage() : "PLC写入操作返回空结果";
                return InteractionResult.fail("PLC写入失败: " + errorMsg);
            }
            // æ‰§è¡Œä¸Šå¤§è½¦æ“ä½œï¼ˆæ•°æ®æµè½¬ï¼‰
            List<String> copied = new ArrayList<>(glassIds);
            context.setLoadedGlassIds(copied);
            context.getSharedData().put("glassesFromVehicle", copied);
            context.getSharedData().put("loadVehicleTime", System.currentTimeMillis());
            // åŽç½®æ¡ä»¶æ£€æŸ¥
            if (context.getLoadedGlassIds().isEmpty()) {
                return InteractionResult.fail("上大车操作失败:玻璃ID列表为空");
            }
            Map<String, Object> data = new HashMap<>();
            data.put("loaded", copied);
            data.put("glassCount", copied.size());
            data.put("deviceId", context.getCurrentDevice().getId());
            data.put("deviceCode", context.getCurrentDevice().getDeviceCode());
            data.put("plcResult", plcResult.getMessage());
            return InteractionResult.success(data);
        } catch (Exception e) {
            return InteractionResult.fail("上大车交互执行异常: " + e.getMessage());
        }
        List<String> copied = new ArrayList<>(glassIds);
        context.setLoadedGlassIds(copied);
        context.getSharedData().put("glassesFromVehicle", copied);
        Map<String, Object> data = new HashMap<>();
        data.put("loaded", copied);
        data.put("glassCount", copied.size());
        return InteractionResult.success(data);
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/impl/LoadVehicleLogicHandler.java
@@ -3,8 +3,10 @@
import com.mes.device.entity.DeviceConfig;
import com.mes.interaction.BaseDeviceLogicHandler;
import com.mes.device.service.DevicePlcOperationService;
import com.mes.device.service.GlassInfoService;
import com.mes.device.vo.DevicePlcVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
@@ -23,8 +25,13 @@
@Component
public class LoadVehicleLogicHandler extends BaseDeviceLogicHandler {
    public LoadVehicleLogicHandler(DevicePlcOperationService devicePlcOperationService) {
    private final GlassInfoService glassInfoService;
    public LoadVehicleLogicHandler(
            DevicePlcOperationService devicePlcOperationService,
            @Qualifier("deviceGlassInfoService") GlassInfoService glassInfoService) {
        super(devicePlcOperationService);
        this.glassInfoService = glassInfoService;
    }
    @Override
@@ -50,6 +57,10 @@
                return handleTriggerReport(deviceConfig, params, logicParams);
            case "reset":
                return handleReset(deviceConfig, params, logicParams);
            case "clearGlass":
            case "clearPlc":
            case "clear":
                return handleClearGlass(deviceConfig, params, logicParams);
            default:
                log.warn("不支持的操作类型: {}", operation);
                return DevicePlcVO.OperationResult.builder()
@@ -201,6 +212,70 @@
        );
    }
    /**
     * æ¸…空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());
        return devicePlcOperationService.writeFields(
                deviceConfig.getId(),
                payload,
                "上大车-清空玻璃数据"
        );
    }
    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);
@@ -257,10 +332,14 @@
        if (result.isEmpty()) {
            List<String> glassIds = (List<String>) params.get("glassIds");
            if (glassIds != null) {
            if (glassIds != null && !glassIds.isEmpty()) {
                // ä»Žæ•°æ®åº“查询玻璃尺寸
                Map<String, Integer> lengthMap = glassInfoService.getGlassLengthMap(glassIds);
                for (String glassId : glassIds) {
                    result.add(new GlassInfo(glassId, null));
                    Integer length = lengthMap.get(glassId);
                    result.add(new GlassInfo(glassId, length));
                }
                log.debug("从数据库查询玻璃尺寸: glassIds={}, lengthMap={}", glassIds, lengthMap);
            }
        }
        return result;
mes-processes/mes-plcSend/src/main/java/com/mes/job/PlcAutoTestTaskScheduler.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/job/config/PlcAddressYmlConfig.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/mapper/PlcAddressMapper.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/mapper/PlcTestTaskMapper.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/service/PlcAddressService.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/service/PlcAutoTestService.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/service/PlcDynamicDataService.java
@@ -4,7 +4,6 @@
import com.github.xingshuangs.iot.common.enums.EDataType;
import com.github.xingshuangs.iot.protocol.s7.serializer.S7Parameter;
import com.mes.device.entity.DeviceConfig;
import com.mes.entity.PlcAddress;
import com.mes.s7.enhanced.EnhancedS7Serializer;
import java.util.ArrayList;
@@ -13,60 +12,12 @@
/**
 * PLC动态数据读写服务
 * æ ¹æ®PlcAddress配置动态构建参数,支持任意字段组合的PLC数据交互
 * æ ¹æ®DeviceConfig配置动态构建参数,支持任意字段组合的PLC数据交互
 * 
 * @author huang
 * @date 2025/11/05
 */
public interface PlcDynamicDataService {
    /**
     * æ ¹æ®PlcAddress配置和字段名称读取PLC数据
     *
     * @param config PLC地址映射配置
     * @param fieldNames è¦è¯»å–的字段名称列表
     * @param s7Serializer S7序列化器
     * @return å­—段名->值 çš„Map
     */
    Map<String, Object> readPlcData(PlcAddress config, List<String> fieldNames, EnhancedS7Serializer s7Serializer);
    /**
     * æ ¹æ®PlcAddress配置和数据Map写入PLC
     *
     * @param config PLC地址映射配置
     * @param dataMap å­—段名->值 çš„Map
     * @param s7Serializer S7序列化器
     */
    void writePlcData(PlcAddress config, Map<String, Object> dataMap, EnhancedS7Serializer s7Serializer);
    /**
     * è¯»å–PLC所有字段
     *
     * @param config PLC地址映射配置
     * @param s7Serializer S7序列化器
     * @return æ‰€æœ‰å­—段的值
     */
    Map<String, Object> readAllPlcData(PlcAddress config, EnhancedS7Serializer s7Serializer);
    /**
     * è¯»å–单个字段
     *
     * @param config PLC地址映射配置
     * @param fieldName å­—段名
     * @param s7Serializer S7序列化器
     * @return å­—段值
     */
    Object readPlcField(PlcAddress config, String fieldName, EnhancedS7Serializer s7Serializer);
    /**
     * å†™å…¥å•个字段
     *
     * @param config PLC地址映射配置
     * @param fieldName å­—段名
     * @param value å­—段值
     * @param s7Serializer S7序列化器
     */
    void writePlcField(PlcAddress config, String fieldName, Object value, EnhancedS7Serializer s7Serializer);
    
    /**
     * æ ¹æ®DeviceConfig配置和字段名称读取PLC数据
@@ -115,4 +66,16 @@
     * @param s7Serializer S7序列化器
     */
    void writePlcField(DeviceConfig device, String fieldName, Object value, EnhancedS7Serializer s7Serializer);
    /**
     * æ ¹æ®å®žä½“类和DeviceConfig配置写入PLC数据
     * å®žä½“类字段使用@S7Variable注解,address字段为字段名(对应configJson中的paramKey)
     * åç§»é‡ä»ŽconfigJson中的paramValue获取
     *
     * @param <T> å®žä½“类型
     * @param device è®¾å¤‡é…ç½®
     * @param entity å®žä½“对象
     * @param s7Serializer S7序列化器
     */
    <T> void writePlcDataByEntity(DeviceConfig device, T entity, EnhancedS7Serializer s7Serializer);
}
mes-processes/mes-plcSend/src/main/java/com/mes/service/PlcTestTaskService.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/service/PlcTestWriteService.java
@@ -7,8 +7,6 @@
import com.mes.device.entity.DeviceConfig;
import com.mes.device.service.DeviceConfigService;
import com.mes.device.util.ConfigJsonHelper;
import com.mes.entity.PlcBaseData;
import com.mes.entity.PlcAddress;
import com.mes.service.PlcDynamicDataService;
import com.mes.s7.enhanced.EnhancedS7Serializer;
import lombok.extern.slf4j.Slf4j;
@@ -16,13 +14,17 @@
import javax.annotation.Resource;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
 * PLC测试写入服务
 * æ¨¡æ‹ŸPLC行为,向PLC写入测试数据,用于测试MES程序
 *
 * åŸºäºŽDeviceConfig的新API,用于模拟PLC行为进行测试
 *
 * æŽ¨èä½¿ç”¨ï¼šDevicePlcOperationService(生产环境)
 * 
 * @author huang
 * @date 2025/10/29
@@ -31,9 +33,6 @@
@Service
public class PlcTestWriteService {
    @Resource
    private PlcAddressService plcAddressService;
    @Resource
    private DeviceConfigService deviceConfigService;
    
@@ -46,310 +45,10 @@
    private static final int ON = 1;
    private static final int OFF = 0;
    
    // å½“前使用的项目标识
    private String currentProjectId = "vertical";
    // ç¼“存不同项目的S7Serializer实例
    // ç¼“存不同设备的S7Serializer实例
    private final ConcurrentMap<String, EnhancedS7Serializer> serializerCache = new ConcurrentHashMap<>();
    /**
     * æ¨¡æ‹ŸPLC发送请求字(触发MES任务下发)
     */
    public boolean simulatePlcRequest() {
        return simulatePlcRequest(currentProjectId);
    }
    /**
     * æ¨¡æ‹ŸPLC发送请求字(触发MES任务下发)- æ”¯æŒæŒ‡å®šé¡¹ç›®
     */
    public boolean simulatePlcRequest(String projectId) {
        try {
            PlcAddress config = plcAddressService.getProjectConfigWithMapping(projectId);
            if (config == null) {
                log.error("项目配置不存在: projectId={}", projectId);
                return false;
            }
            EnhancedS7Serializer s7Serializer = getSerializerForProject(projectId, config);
            if (s7Serializer == null) {
                log.error("无法创建S7Serializer: projectId={}", projectId);
                return false;
            }
            return simulatePlcRequestInternal(projectId, config, s7Serializer);
        } catch (Exception e) {
            log.error("模拟PLC请求字失败", e);
            return false;
        }
    }
    private boolean simulatePlcRequestInternal(String projectId, PlcAddress config, EnhancedS7Serializer s7Serializer) throws Exception {
        PlcBaseData currentData = s7Serializer.read(PlcBaseData.class, config.getDbArea(), config.getBeginIndex());
        if (currentData == null) {
            log.error("读取PLC数据失败,返回null: projectId={}, dbArea={}, beginIndex={}",
                    projectId, config.getDbArea(), config.getBeginIndex());
            return false;
        }
        if (currentData.getOnlineState() == OFF) {
            log.info("当前PLC联机模式为0,停止联机");
            return false;
        } else if (currentData.getPlcReport() == ON) {
            log.info("当前上片PLC汇报字为1,重置为0");
            currentData.setPlcReport(OFF);
        }
        currentData.setPlcRequest(ON);
        s7Serializer.write(currentData, config.getDbArea(), config.getBeginIndex());
        log.info("模拟PLC发送请求字成功:plcRequest=1, projectId={}, dbArea={}, beginIndex={}",
                projectId, config.getDbArea(), config.getBeginIndex());
        return true;
    }
    /**
     * æ¨¡æ‹ŸPLC任务完成汇报
     */
    public boolean simulatePlcReport() {
        return simulatePlcReport(currentProjectId);
    }
    /**
     * æ¨¡æ‹ŸPLC任务完成汇报 - æ”¯æŒæŒ‡å®šé¡¹ç›®
     */
    public boolean simulatePlcReport(String projectId) {
        try {
            // èŽ·å–é¡¹ç›®é…ç½®ï¼ˆæ•°æ®åº“å®žä½“ï¼‰
            PlcAddress config = plcAddressService.getProjectConfigWithMapping(projectId);
            if (config == null) {
                log.error("项目配置不存在: projectId={}", projectId);
                return false;
            }
            // èŽ·å–å¯¹åº”çš„S7Serializer
            EnhancedS7Serializer s7Serializer = getSerializerForProject(projectId, config);
            if (s7Serializer == null) {
                log.error("无法创建S7Serializer: projectId={}", projectId);
                return false;
            }
            return simulatePlcReportInternal(projectId, config, s7Serializer);
        } catch (Exception e) {
            log.error("模拟PLC任务完成汇报失败", e);
            return false;
        }
    }
    private boolean simulatePlcReportInternal(String projectId, PlcAddress config, EnhancedS7Serializer s7Serializer) throws Exception {
        PlcBaseData currentData = s7Serializer.read(PlcBaseData.class, config.getDbArea(), config.getBeginIndex());
        if (currentData == null) {
            log.error("读取PLC数据失败,返回null: projectId={}, dbArea={}, beginIndex={}",
                    projectId, config.getDbArea(), config.getBeginIndex());
            return false;
        }
        currentData.setPlcReport(ON);
        currentData.setPlcRequest(OFF);
        currentData.setMesGlassCount(10);
        s7Serializer.write(currentData, config.getDbArea(), config.getBeginIndex());
        log.info("模拟PLC任务完成汇报:plcReport=1, mesGlassCount=10, projectId={}, dbArea={}, beginIndex={}",
                projectId, config.getDbArea(), config.getBeginIndex());
        return true;
    }
    /**
     * æ¨¡æ‹ŸPLC发送联机状态
     */
    public boolean simulateOnlineStatus(int onlineState) {
        return simulateOnlineStatus(onlineState, currentProjectId);
    }
    /**
     * æ¨¡æ‹ŸPLC发送联机状态 - æ”¯æŒæŒ‡å®šé¡¹ç›®
     */
    public boolean simulateOnlineStatus(int onlineState, String projectId) {
        try {
            // èŽ·å–é¡¹ç›®é…ç½®ï¼ˆæ•°æ®åº“å®žä½“ï¼‰
            PlcAddress config = plcAddressService.getProjectConfigWithMapping(projectId);
            if (config == null) {
                log.error("项目配置不存在: projectId={}", projectId);
                return false;
            }
            // èŽ·å–å¯¹åº”çš„S7Serializer
            EnhancedS7Serializer s7Serializer = getSerializerForProject(projectId, config);
            if (s7Serializer == null) {
                log.error("无法创建S7Serializer: projectId={}", projectId);
                return false;
            }
            return simulateOnlineStatusInternal(onlineState, projectId, config, s7Serializer);
        } catch (Exception e) {
            log.error("模拟PLC联机状态失败", e);
            return false;
        }
    }
    private boolean simulateOnlineStatusInternal(int onlineState, String projectId, PlcAddress config, EnhancedS7Serializer s7Serializer) throws Exception {
        PlcBaseData currentData = s7Serializer.read(PlcBaseData.class, config.getDbArea(), config.getBeginIndex());
        if (currentData == null) {
            log.error("读取PLC数据失败,返回null: projectId={}, dbArea={}, beginIndex={}",
                    projectId, config.getDbArea(), config.getBeginIndex());
            return false;
        }
        currentData.setOnlineState(onlineState);
        s7Serializer.write(currentData, config.getDbArea(), config.getBeginIndex());
        log.info("模拟PLC联机状态:onlineState={}, projectId={}, dbArea={}, beginIndex={}",
                onlineState, projectId, config.getDbArea(), config.getBeginIndex());
        return true;
    }
    /**
     * é‡ç½®PLC所有状态
     */
    public boolean resetPlc() {
        return resetPlc(currentProjectId);
    }
    /**
     * é‡ç½®PLC所有状态 - æ”¯æŒæŒ‡å®šé¡¹ç›®
     */
    public boolean resetPlc(String projectId) {
        try {
            // èŽ·å–é¡¹ç›®é…ç½®ï¼ˆæ•°æ®åº“å®žä½“ï¼‰
            PlcAddress config = plcAddressService.getProjectConfigWithMapping(projectId);
            if (config == null) {
                log.error("项目配置不存在: projectId={}", projectId);
                return false;
            }
            EnhancedS7Serializer s7Serializer = getSerializerForProject(projectId, config);
            if (s7Serializer == null) {
                log.error("无法创建S7Serializer: projectId={}", projectId);
                return false;
            }
            return resetPlcInternal(projectId, config, s7Serializer);
        } catch (Exception e) {
            log.error("重置PLC状态失败", e);
            return false;
        }
    }
    private boolean resetPlcInternal(String projectId, PlcAddress config, EnhancedS7Serializer s7Serializer) throws Exception {
        PlcBaseData resetData = new PlcBaseData();
        resetData.setPlcRequest(OFF);
        resetData.setPlcReport(OFF);
        resetData.setMesSend(OFF);
        resetData.setMesConfirm(OFF);
        resetData.setOnlineState(ON);
        resetData.setMesGlassCount(0);
        resetData.setAlarmInfo(OFF);
        s7Serializer.write(resetData, config.getDbArea(), config.getBeginIndex());
        log.info("PLC状态已重置, projectId={}, dbArea={}, beginIndex={}",
                projectId, config.getDbArea(), config.getBeginIndex());
        return true;
    }
    /**
     * è¯»å–PLC当前状态
     */
    public PlcBaseData readPlcStatus() {
        return readPlcStatus(currentProjectId);
    }
    /**
     * è¯»å–PLC当前状态 - æ”¯æŒæŒ‡å®šé¡¹ç›®
     */
    public PlcBaseData readPlcStatus(String projectId) {
        try {
            // èŽ·å–é¡¹ç›®é…ç½®ï¼ˆæ•°æ®åº“å®žä½“ï¼‰
            PlcAddress config = plcAddressService.getProjectConfigWithMapping(projectId);
            if (config == null) {
                log.error("项目配置不存在: projectId={}", projectId);
                return null;
            }
            EnhancedS7Serializer s7Serializer = getSerializerForProject(projectId, config);
            if (s7Serializer == null) {
                log.error("无法创建S7Serializer: projectId={}", projectId);
                return null;
            }
            return readPlcStatusInternal(projectId, config, s7Serializer);
        } catch (Exception e) {
            log.error("读取PLC状态失败", e);
            return null;
        }
    }
    private PlcBaseData readPlcStatusInternal(String projectId, PlcAddress config, EnhancedS7Serializer s7Serializer) throws Exception {
        PlcBaseData data = s7Serializer.read(PlcBaseData.class, config.getDbArea(), config.getBeginIndex());
        if (data == null) {
            log.error("读取PLC状态返回null: projectId={}, dbArea={}, beginIndex={}",
                    projectId, config.getDbArea(), config.getBeginIndex());
        }
        return data;
    }
    /**
     * è®¾ç½®å½“前项目标识
     */
    public void setCurrentProjectId(String projectId) {
        this.currentProjectId = projectId;
    }
    /**
     * èŽ·å–å½“å‰é¡¹ç›®æ ‡è¯†
     */
    public String getCurrentProjectId() {
        return this.currentProjectId;
    }
    /**
     * èŽ·å–é¡¹ç›®å¯¹åº”çš„S7Serializer实例
     * å¦‚果不存在,则创建一个新的实例并缓存
     *
     * @param projectId é¡¹ç›®æ ‡è¯†
     * @param config é¡¹ç›®é…ç½®
     * @return S7Serializer实例
     */
    private EnhancedS7Serializer getSerializerForProject(String projectId, PlcAddress config) {
        return serializerCache.computeIfAbsent(projectId, id -> {
            // è§£æžPLC类型
            EPlcType plcType = EPlcType.S1200; // é»˜è®¤å€¼
            if (config != null && config.getPlcType() != null) {
                try {
                    plcType = EPlcType.valueOf(config.getPlcType());
                } catch (IllegalArgumentException e) {
                    log.warn("未知的PLC类型: {}, ä½¿ç”¨é»˜è®¤ç±»åž‹ S1200", config.getPlcType());
                }
            }
            // åˆ›å»ºS7PLC实例
            String plcIp = (config != null && config.getPlcIp() != null) ? config.getPlcIp() : "192.168.10.21";
            S7PLC s7Plc = new S7PLC(plcType, plcIp);
            // åˆ›å»ºå¹¶è¿”回EnhancedS7Serializer实例
            return EnhancedS7Serializer.newInstance(s7Plc);
        });
    }
    /**
     * æ¸…除指定项目的S7Serializer缓存
     *
     * @param projectId é¡¹ç›®æ ‡è¯†
     */
    public void clearSerializerCache(String projectId) {
        serializerCache.remove(projectId);
        log.info("已清除项目 {} çš„S7Serializer缓存", projectId);
    }
    /**
     * æ¸…除所有S7Serializer缓存
     */
    public void clearAllSerializerCache() {
        serializerCache.clear();
        log.info("已清除所有S7Serializer缓存");
    }
    // ==================== åŸºäºŽDeviceConfig的新API(推荐使用) ====================
    
    /**
     * æ ¹æ®è®¾å¤‡ID模拟PLC发送请求字
@@ -364,10 +63,73 @@
            return false;
        }
        try {
            String projectId = resolveProjectId(device);
            PlcAddress config = buildPlcAddressFromDevice(device);
            EnhancedS7Serializer s7Serializer = getSerializerForDevice(device);
            return simulatePlcRequestInternal(projectId, config, s7Serializer);
            if (s7Serializer == null) {
                log.error("获取S7Serializer失败: deviceId={}", deviceId);
                return false;
            }
            // ä½¿ç”¨PlcDynamicDataService读取数据(支持addressMapping)
            Map<String, Object> currentData = plcDynamicDataService.readAllPlcData(device, s7Serializer);
            if (currentData == null || currentData.isEmpty()) {
                log.error("读取PLC数据失败,返回空: deviceId={}", deviceId);
                return false;
            }
            // æ£€æŸ¥è”机状态
            Object onlineStateObj = currentData.get("onlineState");
            Integer onlineState = null;
            if (onlineStateObj != null) {
                if (onlineStateObj instanceof Number) {
                    onlineState = ((Number) onlineStateObj).intValue();
                } else {
                    try {
                        String strValue = String.valueOf(onlineStateObj);
                        if (!strValue.isEmpty() && !"null".equalsIgnoreCase(strValue)) {
                            onlineState = Integer.parseInt(strValue);
                        }
                    } catch (NumberFormatException e) {
                        log.warn("解析onlineState失败: deviceId={}, value={}", deviceId, onlineStateObj, e);
                    }
                }
            }
            if (onlineState != null && onlineState == OFF) {
                log.info("当前PLC联机模式为0,停止联机: deviceId={}", deviceId);
                return false;
            }
            // æ£€æŸ¥æ±‡æŠ¥å­—,如果为1则重置为0
            Object plcReportObj = currentData.get("plcReport");
            Integer plcReport = null;
            if (plcReportObj != null) {
                if (plcReportObj instanceof Number) {
                    plcReport = ((Number) plcReportObj).intValue();
                } else {
                    try {
                        String strValue = String.valueOf(plcReportObj);
                        if (!strValue.isEmpty() && !"null".equalsIgnoreCase(strValue)) {
                            plcReport = Integer.parseInt(strValue);
                        }
                    } catch (NumberFormatException e) {
                        log.warn("解析plcReport失败: deviceId={}, value={}", deviceId, plcReportObj, e);
                    }
                }
            }
            if (plcReport != null && plcReport == ON) {
                log.info("当前上片PLC汇报字为1,重置为0: deviceId={}", deviceId);
                currentData.put("plcReport", OFF);
            }
            // è®¾ç½®è¯·æ±‚字为1
            currentData.put("plcRequest", ON);
            // ä½¿ç”¨PlcDynamicDataService写入数据
            plcDynamicDataService.writePlcData(device, currentData, s7Serializer);
            log.info("模拟PLC发送请求字成功:plcRequest=1, deviceId={}", deviceId);
            return true;
        } catch (Exception e) {
            log.error("根据设备模拟PLC请求字失败: deviceId={}", deviceId, e);
            return false;
@@ -387,10 +149,29 @@
            return false;
        }
        try {
            String projectId = resolveProjectId(device);
            PlcAddress config = buildPlcAddressFromDevice(device);
            EnhancedS7Serializer s7Serializer = getSerializerForDevice(device);
            return simulatePlcReportInternal(projectId, config, s7Serializer);
            if (s7Serializer == null) {
                log.error("获取S7Serializer失败: deviceId={}", deviceId);
                return false;
            }
            // ä½¿ç”¨PlcDynamicDataService读取数据
            Map<String, Object> currentData = plcDynamicDataService.readAllPlcData(device, s7Serializer);
            if (currentData == null || currentData.isEmpty()) {
                log.error("读取PLC数据失败,返回空: deviceId={}", deviceId);
                return false;
            }
            // è®¾ç½®æ±‡æŠ¥å­—为1,请求字清0
            currentData.put("plcReport", ON);
            currentData.put("plcRequest", OFF);
            currentData.put("mesGlassCount", 10);
            // ä½¿ç”¨PlcDynamicDataService写入数据
            plcDynamicDataService.writePlcData(device, currentData, s7Serializer);
            log.info("模拟PLC任务完成汇报:plcReport=1, mesGlassCount=10, deviceId={}", deviceId);
            return true;
        } catch (Exception e) {
            log.error("根据设备模拟PLC汇报失败: deviceId={}", deviceId, e);
            return false;
@@ -410,10 +191,27 @@
            return false;
        }
        try {
            String projectId = resolveProjectId(device);
            PlcAddress config = buildPlcAddressFromDevice(device);
            EnhancedS7Serializer s7Serializer = getSerializerForDevice(device);
            return resetPlcInternal(projectId, config, s7Serializer);
            if (s7Serializer == null) {
                log.error("获取S7Serializer失败: deviceId={}", deviceId);
                return false;
            }
            // æž„建重置数据
            Map<String, Object> resetData = new HashMap<>();
            resetData.put("plcRequest", OFF);
            resetData.put("plcReport", OFF);
            resetData.put("mesSend", OFF);
            resetData.put("mesConfirm", OFF);
            resetData.put("onlineState", ON);
            resetData.put("mesGlassCount", 0);
            resetData.put("alarmInfo", OFF);
            // ä½¿ç”¨PlcDynamicDataService写入数据
            plcDynamicDataService.writePlcData(device, resetData, s7Serializer);
            log.info("PLC状态已重置, deviceId={}", deviceId);
            return true;
        } catch (Exception e) {
            log.error("根据设备重置PLC状态失败: deviceId={}", deviceId, e);
            return false;
@@ -433,15 +231,14 @@
            return null;
        }
        try {
            String projectId = resolveProjectId(device);
            PlcAddress config = buildPlcAddressFromDevice(device);
            EnhancedS7Serializer s7Serializer = getSerializerForDevice(device);
            PlcBaseData data = readPlcStatusInternal(projectId, config, s7Serializer);
            if (data == null) {
            if (s7Serializer == null) {
                log.error("获取S7Serializer失败: deviceId={}", deviceId);
                return null;
            }
            String json = objectMapper.writeValueAsString(data);
            return objectMapper.readValue(json, MAP_TYPE);
            // ä½¿ç”¨PlcDynamicDataService读取所有数据(支持addressMapping)
            Map<String, Object> data = plcDynamicDataService.readAllPlcData(device, s7Serializer);
            return data;
        } catch (Exception e) {
            log.error("读取设备PLC状态失败: deviceId={}", deviceId, e);
            return null;
@@ -463,16 +260,17 @@
        }
        
        try {
            // ä»Žè®¾å¤‡é…ç½®ä¸­èŽ·å–é¡¹ç›®æ ‡è¯†
            String projectId = resolveProjectId(device);
            // èŽ·å–å¯¹åº”çš„S7Serializer(使用设备配置)
            EnhancedS7Serializer s7Serializer = getSerializerForDevice(device);
            if (s7Serializer == null) {
                log.error("获取S7Serializer失败: deviceId={}", deviceId);
                return false;
            }
            
            // ä½¿ç”¨åŠ¨æ€æ•°æ®æœåŠ¡å†™å…¥å­—æ®µï¼ˆåŸºäºŽDeviceConfig)
            plcDynamicDataService.writePlcData(device, fieldValues, s7Serializer);
            
            log.info("写入PLC字段成功: deviceId={}, projectId={}, fields={}", deviceId, projectId, fieldValues.keySet());
            log.info("写入PLC字段成功: deviceId={}, fields={}", deviceId, fieldValues.keySet());
            return true;
        } catch (Exception e) {
            log.error("写入PLC字段失败: deviceId={}", deviceId, e);
@@ -487,86 +285,64 @@
     * @return S7Serializer实例
     */
    private EnhancedS7Serializer getSerializerForDevice(DeviceConfig device) {
        String cacheKey = "device:" + (device.getId() != null ? device.getId() : resolveProjectId(device));
        return serializerCache.computeIfAbsent(cacheKey, id -> {
            // è§£æžPLC类型(仅取实体字段)
            EPlcType plcType = EPlcType.S1200;
            String plcTypeValue = device.getPlcType();
            if (plcTypeValue == null || plcTypeValue.isEmpty()) {
                log.warn("设备未配置PLC类型,使用默认类型S1200, deviceId={}", device.getId());
        if (device == null) {
            log.error("设备配置为空,无法创建S7Serializer");
            return null;
        }
        try {
            String cacheKey;
            if (device.getId() != null) {
                cacheKey = "device:" + device.getId();
            } else {
                try {
                    plcType = EPlcType.valueOf(plcTypeValue);
                } catch (IllegalArgumentException e) {
                    log.warn("未知的PLC类型: {}, ä½¿ç”¨é»˜è®¤ç±»åž‹ S1200", plcTypeValue);
                    cacheKey = "device:" + resolveProjectId(device);
                } catch (Exception e) {
                    cacheKey = "device:" + (device.getDeviceCode() != null ? device.getDeviceCode() : "unknown");
                }
            }
            
            // åˆ›å»ºS7PLC实例(仅取实体字段)
            String plcIp = device.getPlcIp();
            if (plcIp == null || plcIp.isEmpty()) {
                log.warn("设备未配置PLC IP,使用默认 192.168.10.21, deviceId={}", device.getId());
                plcIp = "192.168.10.21";
            }
            S7PLC s7Plc = new S7PLC(plcType, plcIp);
            // åˆ›å»ºå¹¶è¿”回EnhancedS7Serializer实例
            return EnhancedS7Serializer.newInstance(s7Plc);
        });
            return serializerCache.computeIfAbsent(cacheKey, id -> {
                try {
                    // è§£æžPLC类型(仅取实体字段)
                    EPlcType plcType = EPlcType.S1200;
                    String plcTypeValue = device.getPlcType();
                    if (plcTypeValue == null || plcTypeValue.isEmpty()) {
                        log.warn("设备未配置PLC类型,使用默认类型S1200, deviceId={}", device.getId());
                    } else {
                        try {
                            plcType = EPlcType.valueOf(plcTypeValue);
                        } catch (IllegalArgumentException e) {
                            log.warn("未知的PLC类型: {}, ä½¿ç”¨é»˜è®¤ç±»åž‹ S1200", plcTypeValue);
                        }
                    }
                    // åˆ›å»ºS7PLC实例(仅取实体字段)
                    String plcIp = device.getPlcIp();
                    if (plcIp == null || plcIp.isEmpty()) {
                        log.warn("设备未配置PLC IP,使用默认 192.168.10.21, deviceId={}", device.getId());
                        plcIp = "192.168.10.21";
                    }
                    S7PLC s7Plc = new S7PLC(plcType, plcIp);
                    // åˆ›å»ºå¹¶è¿”回EnhancedS7Serializer实例
                    EnhancedS7Serializer serializer = EnhancedS7Serializer.newInstance(s7Plc);
                    if (serializer == null) {
                        log.error("创建EnhancedS7Serializer失败: deviceId={}, plcIp={}, plcType={}",
                            device.getId(), plcIp, plcType);
                    }
                    return serializer;
                } catch (Exception e) {
                    log.error("创建S7Serializer异常: deviceId={}", device.getId(), e);
                    return null;
                }
            });
        } catch (Exception e) {
            log.error("获取S7Serializer失败: deviceId={}", device.getId(), e);
            return null;
        }
    }
    
    private PlcAddress buildPlcAddressFromDevice(DeviceConfig device) {
        Map<String, Object> plcConfig = getPlcConfigParams(device);
        String dbArea = plcConfig.get("dbArea") != null ? String.valueOf(plcConfig.get("dbArea")) : "DB12";
        int beginIndex = plcConfig.get("beginIndex") != null ? parseInteger(plcConfig.get("beginIndex")) : 0;
        String plcIp = device.getPlcIp();
        if (plcIp == null || plcIp.isEmpty()) {
            log.warn("设备未配置PLC IP,使用默认 192.168.10.21, deviceId={}", device.getId());
            plcIp = "192.168.10.21";
        }
        String plcType = device.getPlcType();
        if (plcType == null || plcType.isEmpty()) {
            log.warn("设备未配置PLC类型,使用默认S1200, deviceId={}", device.getId());
            plcType = EPlcType.S1200.name();
        }
        String addressMapping = resolveAddressMapping(device);
        PlcAddress config = new PlcAddress();
        config.setProjectId(resolveProjectId(device));
        config.setDbArea(dbArea);
        config.setBeginIndex(beginIndex);
        config.setPlcIp(plcIp);
        config.setPlcType(plcType);
        config.setAddressMapping(addressMapping);
        return config;
    }
    private String resolveAddressMapping(DeviceConfig device) {
        Map<String, Object> mapping = ConfigJsonHelper.parseToMap(device.getConfigJson(), objectMapper);
        if (!mapping.isEmpty()) {
            try {
                return objectMapper.writeValueAsString(mapping);
            } catch (Exception e) {
                log.warn("序列化configJson字段映射失败, deviceId={}", device.getId(), e);
            }
        }
        Map<String, Object> extraParams = parseExtraParams(device);
        Object addressMapping = extraParams.get("addressMapping");
        if (addressMapping instanceof String) {
            return (String) addressMapping;
        }
        if (addressMapping != null) {
            try {
                return objectMapper.writeValueAsString(addressMapping);
            } catch (Exception e) {
                log.warn("序列化extraParams.addressMapping失败, deviceId={}", device.getId(), e);
            }
        }
        throw new IllegalStateException("设备未配置PLC字段映射, deviceId=" + device.getId());
    }
    private Map<String, Object> parseExtraParams(DeviceConfig device) {
        if (device.getExtraParams() == null || device.getExtraParams().trim().isEmpty()) {
@@ -646,4 +422,4 @@
        
        throw new IllegalStateException("无法解析设备的PLC项目标识, deviceId=" + device.getId());
    }
}
}
mes-processes/mes-plcSend/src/main/java/com/mes/service/impl/PlcAddressServiceImpl.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/service/impl/PlcAutoTestServiceImpl.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/service/impl/PlcDynamicDataServiceImpl.java
@@ -7,12 +7,14 @@
import com.github.xingshuangs.iot.protocol.s7.serializer.S7Parameter;
import com.mes.device.entity.DeviceConfig;
import com.mes.device.util.ConfigJsonHelper;
import com.mes.entity.PlcAddress;
import com.mes.s7.enhanced.EnhancedS7Serializer;
import com.mes.service.PlcDynamicDataService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import com.github.xingshuangs.iot.protocol.s7.serializer.S7Variable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -21,7 +23,7 @@
/**
 * PLC动态数据读写服务实现
 * é€šè¿‡PlcAddress中的addressMapping配置动态读写任意字段组合
 * é€šè¿‡DeviceConfig中的configJson配置动态读写任意字段组合
 * 
 * @author huang
 * @date 2025/11/05
@@ -33,194 +35,6 @@
    private final ObjectMapper objectMapper = new ObjectMapper();
    private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<Map<String, Object>>() {};
    /**
     * æ ¹æ®PlcAddress配置和字段名称读取PLC数据
     *
     * @param config PLC地址映射配置
     * @param fieldNames è¦è¯»å–的字段名称列表
     * @param s7Serializer S7序列化器
     * @return å­—段名->值 çš„Map
     */
    @Override
    public Map<String, Object> readPlcData(PlcAddress config, List<String> fieldNames, EnhancedS7Serializer s7Serializer) {
        if (config == null || config.getAddressMapping() == null) {
            throw new IllegalArgumentException("PlcAddress配置或addressMapping不能为空");
        }
        try {
            // è§£æžaddressMapping JSON配置
            JSONObject addressMapping = JSONObject.parseObject(config.getAddressMapping());
            // æž„建S7Parameter列表
            List<S7Parameter> parameters = buildS7Parameters(config, addressMapping, fieldNames);
            // ä»ŽPLC读取数据
            List<S7Parameter> results = s7Serializer.read(parameters);
            // å°†ç»“果转换为Map
            Map<String, Object> resultMap = new HashMap<>();
            for (int i = 0; i < fieldNames.size() && i < results.size(); i++) {
                String fieldName = fieldNames.get(i);
                Object value = results.get(i).getValue();
                resultMap.put(fieldName, value);
            }
            return resultMap;
        } catch (Exception e) {
            log.error("读取PLC数据失败,请检查:1.PLC IP地址是否正确[{}] 2.PLC设备是否在线 3.网络连接是否正常,详细错误: {}",
                config.getPlcIp(), e.getMessage(), e);
            return new HashMap<>();
        }
    }
    /**
     * æ ¹æ®PlcAddress配置和数据Map写入PLC
     *
     * @param config PLC地址映射配置
     * @param dataMap å­—段名->值 çš„Map
     * @param s7Serializer S7序列化器
     */
    @Override
    public void writePlcData(PlcAddress config, Map<String, Object> dataMap, EnhancedS7Serializer s7Serializer) {
        if (config == null || config.getAddressMapping() == null) {
            throw new IllegalArgumentException("PlcAddress配置或addressMapping不能为空");
        }
        try {
            // è§£æžaddressMapping JSON配置
            JSONObject addressMapping = JSONObject.parseObject(config.getAddressMapping());
            // æž„建S7Parameter列表,并填充值
            List<S7Parameter> parameters = buildS7ParametersWithValues(config, addressMapping, dataMap);
            // å†™å…¥PLC
            s7Serializer.write(parameters);
        } catch (Exception e) {
            log.error("写入PLC数据失败,请检查:1.PLC IP地址是否正确[{}] 2.PLC设备是否在线 3.网络连接是否正常,详细错误: {}",
                config.getPlcIp(), e.getMessage(), e);
        }
    }
    /**
     * è¯»å–PLC所有字段
     *
     * @param config PLC地址映射配置
     * @param s7Serializer S7序列化器
     * @return æ‰€æœ‰å­—段的值
     */
    @Override
    public Map<String, Object> readAllPlcData(PlcAddress config, EnhancedS7Serializer s7Serializer) {
        if (config == null || config.getAddressMapping() == null) {
            throw new IllegalArgumentException("PlcAddress配置或addressMapping不能为空");
        }
        // èŽ·å–æ‰€æœ‰å­—æ®µå
        JSONObject addressMapping = JSONObject.parseObject(config.getAddressMapping());
        List<String> allFields = new ArrayList<>(addressMapping.keySet());
        // è¯»å–所有字段
        return readPlcData(config, allFields, s7Serializer);
    }
    /**
     * è¯»å–单个字段
     *
     * @param config PLC地址映射配置
     * @param fieldName å­—段名
     * @param s7Serializer S7序列化器
     * @return å­—段值
     */
    @Override
    public Object readPlcField(PlcAddress config, String fieldName, EnhancedS7Serializer s7Serializer) {
        List<String> fields = new ArrayList<>();
        fields.add(fieldName);
        Map<String, Object> result = readPlcData(config, fields, s7Serializer);
        return result.get(fieldName);
    }
    /**
     * å†™å…¥å•个字段
     *
     * @param config PLC地址映射配置
     * @param fieldName å­—段名
     * @param value å­—段值
     * @param s7Serializer S7序列化器
     */
    @Override
    public void writePlcField(PlcAddress config, String fieldName, Object value, EnhancedS7Serializer s7Serializer) {
        Map<String, Object> dataMap = new HashMap<>();
        dataMap.put(fieldName, value);
        writePlcData(config, dataMap, s7Serializer);
    }
    /**
     * æž„建S7Parameter列表(不包含值)
     *
     * @param config PLC地址配置
     * @param addressMapping åœ°å€æ˜ å°„
     * @param fieldNames å­—段名列表
     * @return S7Parameter列表
     */
    private List<S7Parameter> buildS7Parameters(PlcAddress config, JSONObject addressMapping, List<String> fieldNames) {
        List<S7Parameter> parameters = new ArrayList<>();
        for (String fieldName : fieldNames) {
            if (!addressMapping.containsKey(fieldName)) {
                log.warn("字段 {} åœ¨addressMapping中不存在,跳过", fieldName);
                continue;
            }
            // èŽ·å–å­—æ®µçš„åç§»åœ°å€
            int offset = addressMapping.getInteger(fieldName);
            // æž„建完整地址:dbArea + offset(如:DB12.2)
            String fullAddress = config.getDbArea() + "." + offset;
            // åˆ›å»ºS7Parameter,默认使用UINT16类型(16位无符号整数)
            S7Parameter parameter = new S7Parameter(fullAddress, EDataType.UINT16, 1);
            parameters.add(parameter);
        }
        return parameters;
    }
    /**
     * æž„建S7Parameter列表(包含值)
     *
     * @param config PLC地址配置
     * @param addressMapping åœ°å€æ˜ å°„
     * @param dataMap å­—段名->值 çš„Map
     * @return S7Parameter列表
     */
    private List<S7Parameter> buildS7ParametersWithValues(PlcAddress config, JSONObject addressMapping, Map<String, Object> dataMap) {
        List<S7Parameter> parameters = new ArrayList<>();
        for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
            String fieldName = entry.getKey();
            Object value = entry.getValue();
            if (!addressMapping.containsKey(fieldName)) {
                log.warn("字段 {} åœ¨addressMapping中不存在,跳过", fieldName);
                continue;
            }
            // èŽ·å–å­—æ®µçš„åç§»åœ°å€
            int offset = addressMapping.getInteger(fieldName);
            // æž„建完整地址
            String fullAddress = config.getDbArea() + "." + offset;
            // åˆ›å»ºS7Parameter,设置值
            S7Parameter parameter = new S7Parameter(fullAddress, EDataType.UINT16, 1);
            parameter.setValue(value);
            parameters.add(parameter);
        }
        return parameters;
    }
    /**
     * ä»ŽDeviceConfig中提取地址映射配置
     * 
@@ -391,12 +205,18 @@
            throw new IllegalArgumentException("设备配置不能为空");
        }
        
        String addressMapping = extractAddressMapping(device);
        if (addressMapping == null || addressMapping.isEmpty()) {
            throw new IllegalArgumentException("设备配置中addressMapping不能为空");
        if (s7Serializer == null) {
            log.error("S7Serializer为空,无法写入PLC数据: deviceId={}", device.getId());
            return;
        }
        
        try {
            String addressMapping = extractAddressMapping(device);
            if (addressMapping == null || addressMapping.isEmpty()) {
                log.error("设备配置中addressMapping为空: deviceId={}", device.getId());
                return;
            }
            // è§£æžaddressMapping JSON配置
            JSONObject addressMappingObj = JSONObject.parseObject(addressMapping);
            
@@ -418,17 +238,28 @@
            throw new IllegalArgumentException("设备配置不能为空");
        }
        
        String addressMapping = extractAddressMapping(device);
        if (addressMapping == null || addressMapping.isEmpty()) {
            throw new IllegalArgumentException("设备配置中addressMapping不能为空");
        if (s7Serializer == null) {
            log.error("S7Serializer为空,无法读取PLC数据: deviceId={}", device.getId());
            return new HashMap<>();
        }
        
        // èŽ·å–æ‰€æœ‰å­—æ®µå
        JSONObject addressMappingObj = JSONObject.parseObject(addressMapping);
        List<String> allFields = new ArrayList<>(addressMappingObj.keySet());
        // è¯»å–所有字段
        return readPlcData(device, allFields, s7Serializer);
        try {
            String addressMapping = extractAddressMapping(device);
            if (addressMapping == null || addressMapping.isEmpty()) {
                log.error("设备配置中addressMapping为空: deviceId={}", device.getId());
                return new HashMap<>();
            }
            // èŽ·å–æ‰€æœ‰å­—æ®µå
            JSONObject addressMappingObj = JSONObject.parseObject(addressMapping);
            List<String> allFields = new ArrayList<>(addressMappingObj.keySet());
            // è¯»å–所有字段
            return readPlcData(device, allFields, s7Serializer);
        } catch (Exception e) {
            log.error("读取所有PLC数据失败: deviceId={}", device.getId(), e);
            return new HashMap<>();
        }
    }
    
    @Override
@@ -448,11 +279,123 @@
        writePlcData(device, dataMap, s7Serializer);
    }
    
    @Override
    public <T> void writePlcDataByEntity(DeviceConfig device, T entity, EnhancedS7Serializer s7Serializer) {
        if (device == null || entity == null) {
            throw new IllegalArgumentException("设备配置和实体对象不能为空");
        }
        try {
            // 1. ä»ŽconfigJson中获取地址映射(字段名 -> åç§»é‡ï¼‰
            Map<String, Object> addressMapping = ConfigJsonHelper.parseToMap(device.getConfigJson(), objectMapper);
            if (addressMapping.isEmpty()) {
                throw new IllegalArgumentException("设备配置中未找到地址映射配置, deviceId=" + device.getId());
            }
            // 2. èŽ·å–dbArea和beginIndex
            String dbArea = extractDbArea(device);
            int beginIndex = extractBeginIndex(device);
            // 3. èŽ·å–å­—æ®µé…ç½®ï¼ˆç±»åž‹å’Œcount)
            Map<String, FieldConfig> fieldConfigMap = extractFieldConfigMap(device);
            // 4. è§£æžå®žä½“类,获取所有带@S7Variable注解的字段
            Class<?> entityClass = entity.getClass();
            List<S7Parameter> parameters = new ArrayList<>();
            for (Field field : entityClass.getDeclaredFields()) {
                S7Variable annotation = field.getAnnotation(S7Variable.class);
                if (annotation == null) {
                    continue;
                }
                // èŽ·å–å­—æ®µåï¼ˆä»Žæ³¨è§£çš„address获取,对应configJson中的paramKey)
                String fieldName = annotation.address();
                if (fieldName == null || fieldName.isEmpty()) {
                    continue;
                }
                // ä»ŽaddressMapping中获取偏移量
                Object offsetObj = addressMapping.get(fieldName);
                if (offsetObj == null) {
                    log.warn("字段 {} åœ¨configJson地址映射中不存在,跳过", fieldName);
                    continue;
                }
                int offset;
                if (offsetObj instanceof Number) {
                    offset = ((Number) offsetObj).intValue();
                } else {
                    offset = Integer.parseInt(String.valueOf(offsetObj));
                }
                // æž„建完整地址:dbArea + (beginIndex + offset)
                String fullAddress = dbArea + "." + (beginIndex + offset);
                // ç¡®å®šæ•°æ®ç±»åž‹å’Œcount
                // ä¼˜å…ˆçº§ï¼š1. æ³¨è§£ä¸­çš„type和count 2. configJson中的配置 3. æ ¹æ®å­—段名推断
                EDataType dataType = annotation.type();
                int count = annotation.count();
                // å¦‚果注解中没有指定count,尝试从configJson或字段名推断
                if (count <= 0) {
                    FieldConfig fieldConfig = fieldConfigMap.get(fieldName);
                    if (fieldConfig != null && fieldConfig.count > 0) {
                        count = fieldConfig.count;
                    } else {
                        count = determineFieldCountByName(fieldName);
                    }
                }
                // å¦‚果注解中的类型是UINT16但字段是String类型,尝试从configJson获取
                if (dataType == EDataType.UINT16 && field.getType() == String.class) {
                    FieldConfig fieldConfig = fieldConfigMap.get(fieldName);
                    if (fieldConfig != null && fieldConfig.dataType != null) {
                        dataType = fieldConfig.dataType;
                    } else {
                        dataType = determineFieldTypeByName(fieldName);
                    }
                }
                // èŽ·å–å­—æ®µå€¼
                field.setAccessible(true);
                Object value = field.get(entity);
                if (value == null) {
                    continue; // è·³è¿‡null值
                }
                // åˆ›å»ºS7Parameter并设置值
                S7Parameter parameter = new S7Parameter(fullAddress, dataType, count);
                parameter.setValue(value);
                parameters.add(parameter);
            }
            if (parameters.isEmpty()) {
                log.warn("实体类 {} ä¸­æ²¡æœ‰æ‰¾åˆ°æœ‰æ•ˆçš„字段,无法写入PLC", entityClass.getSimpleName());
                return;
            }
            // 5. å†™å…¥PLC
            s7Serializer.write(parameters);
            log.info("根据实体类写入PLC数据成功: deviceId={}, entityClass={}, fields={}",
                    device.getId(), entityClass.getSimpleName(), parameters.size());
        } catch (Exception e) {
            log.error("根据实体类写入PLC数据失败: deviceId={}, entityClass={}",
                    device.getId(), entity != null ? entity.getClass().getSimpleName() : "null", e);
            throw new RuntimeException("写入PLC数据失败: " + e.getMessage(), e);
        }
    }
    /**
     * æž„建S7Parameter列表(不包含值)- åŸºäºŽDeviceConfig
     */
    private List<S7Parameter> buildS7ParametersForDevice(DeviceConfig device, String dbArea, JSONObject addressMapping, List<String> fieldNames) {
        List<S7Parameter> parameters = new ArrayList<>();
        // èŽ·å–å­—æ®µé…ç½®ï¼ˆä»ŽconfigJson中解析类型和count)
        Map<String, FieldConfig> fieldConfigMap = extractFieldConfigMap(device);
        
        for (String fieldName : fieldNames) {
            if (!addressMapping.containsKey(fieldName)) {
@@ -460,14 +403,25 @@
                continue;
            }
            
            // èŽ·å–å­—æ®µçš„åç§»åœ°å€
            int offset = addressMapping.getInteger(fieldName);
            // èŽ·å–å­—æ®µçš„åç§»åœ°å€ï¼ˆaddressMapping中只存储数字偏移量)
            Object offsetObj = addressMapping.get(fieldName);
            int offset;
            if (offsetObj instanceof Number) {
                offset = ((Number) offsetObj).intValue();
            } else {
                offset = Integer.parseInt(String.valueOf(offsetObj));
            }
            
            // æž„建完整地址:dbArea + offset(如:DB12.2)
            String fullAddress = dbArea + "." + offset;
            
            // åˆ›å»ºS7Parameter,默认使用UINT16类型(16位无符号整数)
            S7Parameter parameter = new S7Parameter(fullAddress, EDataType.UINT16, 1);
            // èŽ·å–å­—æ®µç±»åž‹å’Œé•¿åº¦ï¼ˆä»ŽconfigJson或根据字段名推断)
            FieldConfig fieldConfig = fieldConfigMap.getOrDefault(fieldName, new FieldConfig());
            EDataType dataType = fieldConfig.dataType != null ? fieldConfig.dataType : determineFieldTypeByName(fieldName);
            int count = fieldConfig.count > 0 ? fieldConfig.count : determineFieldCountByName(fieldName);
            // åˆ›å»ºS7Parameter
            S7Parameter parameter = new S7Parameter(fullAddress, dataType, count);
            parameters.add(parameter);
        }
        
@@ -480,6 +434,9 @@
    private List<S7Parameter> buildS7ParametersWithValuesForDevice(DeviceConfig device, String dbArea, JSONObject addressMapping, Map<String, Object> dataMap) {
        List<S7Parameter> parameters = new ArrayList<>();
        
        // èŽ·å–å­—æ®µé…ç½®ï¼ˆä»ŽconfigJson中解析类型和count)
        Map<String, FieldConfig> fieldConfigMap = extractFieldConfigMap(device);
        for (Map.Entry<String, Object> entry : dataMap.entrySet()) {
            String fieldName = entry.getKey();
            Object value = entry.getValue();
@@ -489,18 +446,151 @@
                continue;
            }
            
            // èŽ·å–å­—æ®µçš„åç§»åœ°å€
            int offset = addressMapping.getInteger(fieldName);
            // èŽ·å–å­—æ®µçš„åç§»åœ°å€ï¼ˆaddressMapping中只存储数字偏移量)
            Object offsetObj = addressMapping.get(fieldName);
            int offset;
            if (offsetObj instanceof Number) {
                offset = ((Number) offsetObj).intValue();
            } else {
                offset = Integer.parseInt(String.valueOf(offsetObj));
            }
            
            // æž„建完整地址
            String fullAddress = dbArea + "." + offset;
            
            // èŽ·å–å­—æ®µç±»åž‹å’Œé•¿åº¦ï¼ˆä»ŽconfigJson或根据字段名推断)
            FieldConfig fieldConfig = fieldConfigMap.getOrDefault(fieldName, new FieldConfig());
            EDataType dataType = fieldConfig.dataType != null ? fieldConfig.dataType : determineFieldTypeByName(fieldName);
            int count = fieldConfig.count > 0 ? fieldConfig.count : determineFieldCountByName(fieldName);
            // åˆ›å»ºS7Parameter,设置值
            S7Parameter parameter = new S7Parameter(fullAddress, EDataType.UINT16, 1);
            S7Parameter parameter = new S7Parameter(fullAddress, dataType, count);
            parameter.setValue(value);
            parameters.add(parameter);
        }
        
        return parameters;
    }
    /**
     * å­—段配置信息
     */
    private static class FieldConfig {
        EDataType dataType;
        int count;
        FieldConfig() {
            this.dataType = null;
            this.count = 0;
        }
        FieldConfig(EDataType dataType, int count) {
            this.dataType = dataType;
            this.count = count;
        }
    }
    /**
     * ä»Žè®¾å¤‡é…ç½®ä¸­æå–字段配置映射(类型和count)
     * configJson格式: [{paramKey: "plcGlassId1", paramValue: "4", description: "玻璃id1", dataType: "STRING", count: 20}]
     */
    private Map<String, FieldConfig> extractFieldConfigMap(DeviceConfig device) {
        Map<String, FieldConfig> configMap = new HashMap<>();
        try {
            String configJson = device.getConfigJson();
            if (configJson == null || configJson.trim().isEmpty()) {
                return configMap;
            }
            String trimmed = configJson.trim();
            // å¦‚æžœconfigJson是数组格式,解析数组
            if (trimmed.startsWith("[")) {
                List<Map<String, Object>> paramList = objectMapper.readValue(trimmed,
                    new TypeReference<List<Map<String, Object>>>() {});
                for (Map<String, Object> param : paramList) {
                    Object paramKeyObj = param.get("paramKey");
                    if (paramKeyObj == null) {
                        continue;
                    }
                    String paramKey = String.valueOf(paramKeyObj);
                    EDataType dataType = null;
                    int count = 0;
                    // è§£æždataType
                    Object dataTypeObj = param.get("dataType");
                    if (dataTypeObj != null) {
                        String dataTypeStr = String.valueOf(dataTypeObj).toUpperCase();
                        try {
                            dataType = EDataType.valueOf(dataTypeStr);
                        } catch (IllegalArgumentException e) {
                            log.debug("无法解析数据类型: {}, å­—段: {}", dataTypeStr, paramKey);
                        }
                    }
                    // è§£æžcount
                    Object countObj = param.get("count");
                    if (countObj != null) {
                        if (countObj instanceof Number) {
                            count = ((Number) countObj).intValue();
                        } else {
                            try {
                                count = Integer.parseInt(String.valueOf(countObj));
                            } catch (NumberFormatException e) {
                                log.debug("无法解析count值: {}, å­—段: {}", countObj, paramKey);
                            }
                        }
                    }
                    if (dataType != null || count > 0) {
                        configMap.put(paramKey, new FieldConfig(dataType, count));
                    }
                }
            }
        } catch (Exception e) {
            log.debug("解析字段配置映射失败: {}", e.getMessage());
        }
        return configMap;
    }
    /**
     * æ ¹æ®å­—段名推断数据类型
     */
    private EDataType determineFieldTypeByName(String fieldName) {
        if (fieldName == null) {
            return EDataType.UINT16;
        }
        String lowerName = fieldName.toLowerCase();
        // çŽ»ç’ƒID字段通常是字符串
        if (lowerName.contains("glassid") || lowerName.contains("glass_id") ||
            lowerName.startsWith("plcglassid")) {
            return EDataType.STRING;
        }
        // é»˜è®¤è¿”回UINT16
        return EDataType.UINT16;
    }
    /**
     * æ ¹æ®å­—段名推断字段长度/数量
     */
    private int determineFieldCountByName(String fieldName) {
        if (fieldName == null) {
            return 1;
        }
        String lowerName = fieldName.toLowerCase();
        // çŽ»ç’ƒID通常是20个字符
        if (lowerName.contains("glassid") || lowerName.contains("glass_id") ||
            lowerName.startsWith("plcglassid")) {
            return 20; // é»˜è®¤20个字符
        }
        // é»˜è®¤è¿”回1
        return 1;
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/service/impl/PlcTestTaskServiceImpl.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/service/impl/PlcTestWriteServiceImpl.java
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/task/controller/TaskStatusNotificationController.java
New file
@@ -0,0 +1,56 @@
package com.mes.task.controller;
import com.mes.task.service.TaskStatusNotificationService;
import com.mes.vo.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/**
 * ä»»åŠ¡çŠ¶æ€é€šçŸ¥æŽ§åˆ¶å™¨
 * æä¾›SSE端点用于实时推送任务执行状态
 *
 * @author mes
 * @since 2025-01-XX
 */
@RestController
@RequestMapping("/api/plcSend/task/notification")
@Api(tags = "任务状态通知")
@RequiredArgsConstructor
public class TaskStatusNotificationController {
    private final TaskStatusNotificationService notificationService;
    @GetMapping(value = "/sse", produces = "text/event-stream")
    @ApiOperation("创建SSE连接,监听任务状态变化")
    public SseEmitter createConnection(@RequestParam(required = false) String taskId) {
        SseEmitter emitter = notificationService.createConnection(taskId);
        if (emitter == null) {
            throw new RuntimeException("创建SSE连接失败");
        }
        return emitter;
    }
    @GetMapping(value = "/sse/all", produces = "text/event-stream")
    @ApiOperation("创建SSE连接,监听所有任务状态变化")
    public SseEmitter createConnectionForAllTasks() {
        return createConnection(null);
    }
    @PostMapping("/close/{taskId}")
    @ApiOperation("关闭指定任务的SSE连接")
    public Result<Boolean> closeConnections(@PathVariable String taskId) {
        notificationService.closeConnections(taskId);
        return Result.success(true);
    }
    @PostMapping("/close/all")
    @ApiOperation("关闭所有SSE连接")
    public Result<Boolean> closeAllConnections() {
        notificationService.closeAllConnections();
        return Result.success(true);
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/task/model/RetryPolicy.java
New file
@@ -0,0 +1,148 @@
package com.mes.task.model;
import lombok.Builder;
import lombok.Data;
/**
 * é‡è¯•策略配置
 *
 * @author mes
 * @since 2025-01-XX
 */
@Data
@Builder
public class RetryPolicy {
    /**
     * æœ€å¤§é‡è¯•次数
     */
    @Builder.Default
    private int maxRetryCount = 3;
    /**
     * åˆå§‹é‡è¯•间隔(毫秒)
     */
    @Builder.Default
    private long initialRetryIntervalMs = 1000;
    /**
     * é‡è¯•间隔增长倍数(指数退避)
     */
    @Builder.Default
    private double backoffMultiplier = 2.0;
    /**
     * æœ€å¤§é‡è¯•间隔(毫秒)
     */
    @Builder.Default
    private long maxRetryIntervalMs = 30000;
    /**
     * æ˜¯å¦å¯ç”¨æŒ‡æ•°é€€é¿
     */
    @Builder.Default
    private boolean enableExponentialBackoff = true;
    /**
     * å¯é‡è¯•的异常类型(类名列表)
     */
    private java.util.List<String> retryableExceptions;
    /**
     * ä¸å¯é‡è¯•的异常类型(类名列表)
     */
    private java.util.List<String> nonRetryableExceptions;
    /**
     * é»˜è®¤é‡è¯•ç­–ç•¥
     */
    public static RetryPolicy defaultPolicy() {
        return RetryPolicy.builder()
            .maxRetryCount(3)
            .initialRetryIntervalMs(1000)
            .backoffMultiplier(2.0)
            .maxRetryIntervalMs(30000)
            .enableExponentialBackoff(true)
            .build();
    }
    /**
     * è®¡ç®—重试间隔
     *
     * @param retryAttempt å½“前重试次数(从1开始)
     * @return é‡è¯•间隔(毫秒)
     */
    public long calculateRetryInterval(int retryAttempt) {
        if (!enableExponentialBackoff) {
            return initialRetryIntervalMs;
        }
        long interval = (long) (initialRetryIntervalMs * Math.pow(backoffMultiplier, retryAttempt - 1));
        return Math.min(interval, maxRetryIntervalMs);
    }
    /**
     * åˆ¤æ–­å¼‚常是否可重试
     *
     * @param exception å¼‚常对象
     * @return æ˜¯å¦å¯é‡è¯•
     */
    public boolean isRetryable(Throwable exception) {
        if (exception == null) {
            return false;
        }
        String exceptionClassName = exception.getClass().getName();
        // æ£€æŸ¥ä¸å¯é‡è¯•列表
        if (nonRetryableExceptions != null) {
            for (String nonRetryable : nonRetryableExceptions) {
                if (exceptionClassName.contains(nonRetryable)) {
                    return false;
                }
            }
        }
        // æ£€æŸ¥å¯é‡è¯•列表
        if (retryableExceptions != null && !retryableExceptions.isEmpty()) {
            for (String retryable : retryableExceptions) {
                if (exceptionClassName.contains(retryable)) {
                    return true;
                }
            }
            return false; // å¦‚果指定了可重试列表,但当前异常不在列表中,则不可重试
        }
        // é»˜è®¤ç­–略:网络异常、超时异常可重试,其他不可重试
        return isDefaultRetryable(exception);
    }
    /**
     * é»˜è®¤é‡è¯•判断逻辑
     */
    private boolean isDefaultRetryable(Throwable exception) {
        String exceptionClassName = exception.getClass().getName().toLowerCase();
        String message = exception.getMessage() != null ? exception.getMessage().toLowerCase() : "";
        // ç½‘络相关异常
        if (exceptionClassName.contains("timeout") ||
            exceptionClassName.contains("connection") ||
            exceptionClassName.contains("network") ||
            message.contains("timeout") ||
            message.contains("connection") ||
            message.contains("网络")) {
            return true;
        }
        // ä¸šåŠ¡å¼‚å¸¸é€šå¸¸ä¸å¯é‡è¯•
        if (exceptionClassName.contains("illegalargument") ||
            exceptionClassName.contains("illegalstate") ||
            exceptionClassName.contains("validation")) {
            return false;
        }
        // å…¶ä»–异常默认不可重试
        return false;
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java
@@ -3,6 +3,7 @@
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mes.device.entity.DeviceConfig;
import com.mes.device.entity.DeviceGroupConfig;
@@ -12,14 +13,17 @@
import com.mes.interaction.DeviceLogicHandlerFactory;
import com.mes.interaction.base.InteractionContext;
import com.mes.interaction.base.InteractionResult;
import com.mes.device.service.DeviceCoordinationService;
import com.mes.device.service.DeviceInteractionService;
import com.mes.task.dto.TaskParameters;
import com.mes.task.entity.MultiDeviceTask;
import com.mes.task.entity.TaskStepDetail;
import com.mes.task.mapper.MultiDeviceTaskMapper;
import com.mes.task.mapper.TaskStepDetailMapper;
import com.mes.task.model.RetryPolicy;
import com.mes.task.model.TaskExecutionContext;
import com.mes.task.model.TaskExecutionResult;
import com.mes.task.service.TaskStatusNotificationService;
import com.mes.device.vo.DevicePlcVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -28,9 +32,11 @@
import org.springframework.util.StringUtils;
import java.util.*;
import java.util.concurrent.*;
/**
 * å¤šè®¾å¤‡ä»»åŠ¡æ‰§è¡Œå¼•æ“Ž
 * æ”¯æŒä¸²è¡Œå’Œå¹¶è¡Œä¸¤ç§æ‰§è¡Œæ¨¡å¼
 */
@Slf4j
@Component
@@ -38,6 +44,11 @@
public class TaskExecutionEngine {
    private static final Map<String, String> DEFAULT_OPERATIONS = new HashMap<>();
    private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<Map<String, Object>>() {};
    // æ‰§è¡Œæ¨¡å¼å¸¸é‡
    private static final String EXECUTION_MODE_SERIAL = "SERIAL";
    private static final String EXECUTION_MODE_PARALLEL = "PARALLEL";
    static {
        DEFAULT_OPERATIONS.put(DeviceConfig.DeviceType.LOAD_VEHICLE, "feedGlass");
@@ -50,7 +61,16 @@
    private final DeviceInteractionService deviceInteractionService;
    private final DeviceInteractionRegistry interactionRegistry;
    private final DeviceLogicHandlerFactory handlerFactory;
    private final DeviceCoordinationService deviceCoordinationService;
    private final TaskStatusNotificationService notificationService;
    private final ObjectMapper objectMapper;
    // çº¿ç¨‹æ± ç”¨äºŽå¹¶è¡Œæ‰§è¡Œ
    private final ExecutorService executorService = Executors.newCachedThreadPool(r -> {
        Thread t = new Thread(r, "TaskExecutionEngine-Parallel");
        t.setDaemon(true);
        return t;
    });
    public TaskExecutionResult execute(MultiDeviceTask task,
                                       DeviceGroupConfig groupConfig,
@@ -62,24 +82,55 @@
        }
        TaskExecutionContext context = new TaskExecutionContext(parameters);
        // è®¾å¤‡åè°ƒï¼šæ£€æŸ¥ä¾èµ–关系和执行条件
        DeviceCoordinationService.CoordinationResult coordinationResult =
            deviceCoordinationService.coordinateExecution(groupConfig, devices, context);
        if (!coordinationResult.canExecute()) {
            log.warn("设备协调失败: {}", coordinationResult.getMessage());
            return TaskExecutionResult.failure(coordinationResult.getMessage(), Collections.emptyMap());
        }
        log.info("设备协调成功: {}", coordinationResult.getMessage());
        task.setTotalSteps(devices.size());
        task.setStatus(MultiDeviceTask.Status.RUNNING.name());
        multiDeviceTaskMapper.updateById(task);
        // é€šçŸ¥ä»»åŠ¡å¼€å§‹æ‰§è¡Œ
        notificationService.notifyTaskStatus(task);
        // ç¡®å®šæ‰§è¡Œæ¨¡å¼
        String executionMode = determineExecutionMode(groupConfig);
        Integer maxConcurrent = getMaxConcurrentDevices(groupConfig);
        List<Map<String, Object>> stepSummaries = new ArrayList<>();
        boolean success = true;
        String failureMessage = null;
        log.info("任务执行模式: {}, æœ€å¤§å¹¶å‘æ•°: {}, è®¾å¤‡æ•°: {}", executionMode, maxConcurrent, devices.size());
        for (int i = 0; i < devices.size(); i++) {
            DeviceConfig device = devices.get(i);
            int order = i + 1;
            TaskStepDetail step = createStepRecord(task, device, order);
            StepResult stepResult = executeStep(task, step, device, context);
            stepSummaries.add(stepResult.toSummary());
            if (!stepResult.isSuccess()) {
                success = false;
                failureMessage = stepResult.getMessage();
                break;
        List<Map<String, Object>> stepSummaries;
        boolean success;
        String failureMessage;
        if (EXECUTION_MODE_PARALLEL.equals(executionMode)) {
            // å¹¶è¡Œæ‰§è¡Œæ¨¡å¼
            stepSummaries = new ArrayList<>(Collections.nCopies(devices.size(), null));
            Pair<Boolean, String> result = executeParallel(task, devices, context, stepSummaries, maxConcurrent);
            success = result.getFirst();
            failureMessage = result.getSecond();
        } else {
            // ä¸²è¡Œæ‰§è¡Œæ¨¡å¼ï¼ˆé»˜è®¤ï¼‰
            stepSummaries = new ArrayList<>();
            success = true;
            failureMessage = null;
            for (int i = 0; i < devices.size(); i++) {
                DeviceConfig device = devices.get(i);
                int order = i + 1;
                TaskStepDetail step = createStepRecord(task, device, order);
                StepResult stepResult = executeStep(task, step, device, context);
                stepSummaries.add(stepResult.toSummary());
                if (!stepResult.isSuccess()) {
                    success = false;
                    failureMessage = stepResult.getMessage();
                    break;
                }
            }
        }
@@ -87,11 +138,212 @@
        payload.put("steps", stepSummaries);
        payload.put("groupId", groupConfig.getId());
        payload.put("deviceCount", devices.size());
        payload.put("executionMode", executionMode);
        // æ›´æ–°ä»»åŠ¡æœ€ç»ˆçŠ¶æ€
        if (success) {
            task.setStatus(MultiDeviceTask.Status.COMPLETED.name());
        } else {
            task.setStatus(MultiDeviceTask.Status.FAILED.name());
            task.setErrorMessage(failureMessage);
        }
        task.setEndTime(new Date());
        multiDeviceTaskMapper.updateById(task);
        // é€šçŸ¥ä»»åŠ¡å®Œæˆ
        notificationService.notifyTaskStatus(task);
        if (success) {
            return TaskExecutionResult.success(payload);
        }
        return TaskExecutionResult.failure(failureMessage != null ? failureMessage : "任务执行失败", payload);
    }
    /**
     * å¹¶è¡Œæ‰§è¡Œå¤šä¸ªè®¾å¤‡æ“ä½œ
     */
    private Pair<Boolean, String> executeParallel(MultiDeviceTask task,
                                                   List<DeviceConfig> devices,
                                                   TaskExecutionContext context,
                                                   List<Map<String, Object>> stepSummaries,
                                                   Integer maxConcurrent) {
        int concurrency = maxConcurrent != null && maxConcurrent > 0
            ? Math.min(maxConcurrent, devices.size())
            : devices.size();
        // åˆ›å»ºæ‰€æœ‰æ­¥éª¤è®°å½•
        List<TaskStepDetail> steps = new ArrayList<>();
        for (int i = 0; i < devices.size(); i++) {
            DeviceConfig device = devices.get(i);
            int order = i + 1;
            TaskStepDetail step = createStepRecord(task, device, order);
            steps.add(step);
        }
        // ä½¿ç”¨ä¿¡å·é‡æŽ§åˆ¶å¹¶å‘æ•°
        Semaphore semaphore = new Semaphore(concurrency);
        List<CompletableFuture<StepResult>> futures = new ArrayList<>();
        for (int i = 0; i < devices.size(); i++) {
            final int index = i;
            final DeviceConfig device = devices.get(index);
            final TaskStepDetail step = steps.get(index);
            CompletableFuture<StepResult> future = CompletableFuture.supplyAsync(() -> {
                try {
                    semaphore.acquire();
                    try {
                        return executeStep(task, step, device, context);
                    } finally {
                        semaphore.release();
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    log.error("并行执行被中断, deviceId={}", device.getId(), e);
                    return StepResult.failure(device.getDeviceName(), "执行被中断");
                } catch (Exception e) {
                    log.error("并行执行异常, deviceId={}", device.getId(), e);
                    return StepResult.failure(device.getDeviceName(), e.getMessage());
                }
            }, executorService);
            final int finalIndex = index;
            future.whenComplete((result, throwable) -> {
                if (throwable != null) {
                    log.error("并行执行完成时异常, deviceId={}", device.getId(), throwable);
                    stepSummaries.set(finalIndex, StepResult.failure(device.getDeviceName(),
                        throwable.getMessage()).toSummary());
                } else if (result != null) {
                    stepSummaries.set(finalIndex, result.toSummary());
                }
            });
            futures.add(future);
        }
        // ç­‰å¾…所有任务完成
        CompletableFuture<Void> allFutures = CompletableFuture.allOf(
            futures.toArray(new CompletableFuture[0])
        );
        try {
            allFutures.get(30, TimeUnit.MINUTES); // æœ€å¤šç­‰å¾…30分钟
        } catch (TimeoutException e) {
            log.error("并行执行超时, taskId={}", task.getTaskId(), e);
            return Pair.of(false, "任务执行超时");
        } catch (Exception e) {
            log.error("等待并行执行完成时异常, taskId={}", task.getTaskId(), e);
            return Pair.of(false, "等待执行完成时发生异常: " + e.getMessage());
        }
        // æ£€æŸ¥æ‰€æœ‰æ­¥éª¤çš„æ‰§è¡Œç»“æžœ
        boolean allSuccess = true;
        String firstFailureMessage = null;
        for (int i = 0; i < futures.size(); i++) {
            try {
                StepResult result = futures.get(i).get();
                if (result != null && !result.isSuccess()) {
                    allSuccess = false;
                    if (firstFailureMessage == null) {
                        firstFailureMessage = result.getMessage();
                    }
                }
            } catch (Exception e) {
                log.error("获取步骤执行结果异常, stepIndex={}", i, e);
                allSuccess = false;
                if (firstFailureMessage == null) {
                    firstFailureMessage = "获取执行结果异常: " + e.getMessage();
                }
            }
        }
        return Pair.of(allSuccess, firstFailureMessage);
    }
    /**
     * ç¡®å®šæ‰§è¡Œæ¨¡å¼
     */
    private String determineExecutionMode(DeviceGroupConfig groupConfig) {
        if (groupConfig == null) {
            return EXECUTION_MODE_SERIAL; // é»˜è®¤ä¸²è¡Œ
        }
        // ä»ŽextraConfig中读取executionMode
        String extraConfig = groupConfig.getExtraConfig();
        if (StringUtils.hasText(extraConfig)) {
            try {
                Map<String, Object> config = objectMapper.readValue(extraConfig, MAP_TYPE);
                Object mode = config.get("executionMode");
                if (mode != null) {
                    String modeStr = String.valueOf(mode).toUpperCase();
                    if (EXECUTION_MODE_PARALLEL.equals(modeStr) || EXECUTION_MODE_SERIAL.equals(modeStr)) {
                        return modeStr;
                    }
                }
            } catch (Exception e) {
                log.warn("解析设备组执行模式失败, groupId={}", groupConfig.getId(), e);
            }
        }
        // å¦‚果有maxConcurrentDevices且大于1,默认使用并行模式
        if (groupConfig.getMaxConcurrentDevices() != null && groupConfig.getMaxConcurrentDevices() > 1) {
            return EXECUTION_MODE_PARALLEL;
        }
        return EXECUTION_MODE_SERIAL; // é»˜è®¤ä¸²è¡Œ
    }
    /**
     * èŽ·å–æœ€å¤§å¹¶å‘è®¾å¤‡æ•°
     */
    private Integer getMaxConcurrentDevices(DeviceGroupConfig groupConfig) {
        if (groupConfig == null) {
            return 1;
        }
        // ä»ŽextraConfig中读取maxConcurrent
        String extraConfig = groupConfig.getExtraConfig();
        if (StringUtils.hasText(extraConfig)) {
            try {
                Map<String, Object> config = objectMapper.readValue(extraConfig, MAP_TYPE);
                Object maxConcurrent = config.get("maxConcurrent");
                if (maxConcurrent instanceof Number) {
                    return ((Number) maxConcurrent).intValue();
                }
            } catch (Exception e) {
                log.warn("解析设备组最大并发数失败, groupId={}", groupConfig.getId(), e);
            }
        }
        // ä½¿ç”¨å®žä½“字段
        return groupConfig.getMaxConcurrentDevices() != null && groupConfig.getMaxConcurrentDevices() > 0
            ? groupConfig.getMaxConcurrentDevices()
            : 1;
    }
    /**
     * ç®€å•çš„Pair类用于返回两个值
     */
    private static class Pair<T, U> {
        private final T first;
        private final U second;
        private Pair(T first, U second) {
            this.first = first;
            this.second = second;
        }
        public static <T, U> Pair<T, U> of(T first, U second) {
            return new Pair<>(first, second);
        }
        public T getFirst() {
            return first;
        }
        public U getSecond() {
            return second;
        }
    }
    private TaskStepDetail createStepRecord(MultiDeviceTask task, DeviceConfig device, int order) {
@@ -110,13 +362,25 @@
                                   TaskStepDetail step,
                                   DeviceConfig device,
                                   TaskExecutionContext context) {
        return executeStepWithRetry(task, step, device, context, getRetryPolicy(device));
    }
    /**
     * å¸¦é‡è¯•的步骤执行
     */
    private StepResult executeStepWithRetry(MultiDeviceTask task,
                                            TaskStepDetail step,
                                            DeviceConfig device,
                                            TaskExecutionContext context,
                                            RetryPolicy retryPolicy) {
        Date startTime = new Date();
        step.setStartTime(startTime);
        step.setStatus(TaskStepDetail.Status.RUNNING.name());
        step.setRetryCount(0);
        DeviceInteraction deviceInteraction = interactionRegistry.getInteraction(device.getDeviceType());
        if (deviceInteraction != null) {
            return executeInteractionStep(task, step, device, context, deviceInteraction);
            return executeInteractionStepWithRetry(task, step, device, context, deviceInteraction, retryPolicy);
        }
        Map<String, Object> params = buildOperationParams(device, context);
@@ -125,65 +389,260 @@
        String operation = determineOperation(device, params);
        DeviceLogicHandler handler = handlerFactory.getHandler(device.getDeviceType());
        DevicePlcVO.OperationResult result;
        int retryAttempt = 0;
        Exception lastException = null;
        while (retryAttempt <= retryPolicy.getMaxRetryCount()) {
            try {
                if (retryAttempt > 0) {
                    // é‡è¯•前等待
                    long waitTime = retryPolicy.calculateRetryInterval(retryAttempt);
                    log.info("步骤执行重试: deviceId={}, operation={}, retryAttempt={}/{}, waitTime={}ms",
                        device.getId(), operation, retryAttempt, retryPolicy.getMaxRetryCount(), waitTime);
                    Thread.sleep(waitTime);
                    // æ›´æ–°æ­¥éª¤çŠ¶æ€
                    step.setRetryCount(retryAttempt);
                    step.setStatus(TaskStepDetail.Status.RUNNING.name());
                    step.setStartTime(new Date());
                    taskStepDetailMapper.updateById(step);
                }
        try {
            if (handler == null) {
                result = deviceInteractionService.executeOperation(device.getId(), operation, params);
            } else {
                result = handler.execute(device, operation, params);
                DevicePlcVO.OperationResult result;
                if (handler == null) {
                    result = deviceInteractionService.executeOperation(device.getId(), operation, params);
                } else {
                    result = handler.execute(device, operation, params);
                }
                boolean opSuccess = Boolean.TRUE.equals(result.getSuccess());
                updateStepAfterOperation(step, result, opSuccess);
                updateTaskProgress(task, step.getStepOrder(), opSuccess);
                // é€šçŸ¥æ­¥éª¤æ›´æ–°
                notificationService.notifyStepUpdate(task.getTaskId(), step);
                if (opSuccess) {
                    updateContextAfterSuccess(device, context, params);
                    // åŒæ­¥è®¾å¤‡çŠ¶æ€
                    deviceCoordinationService.syncDeviceStatus(device,
                        DeviceCoordinationService.DeviceStatus.COMPLETED, context);
                    return StepResult.success(device.getDeviceName(), result.getMessage());
                } else {
                    // ä¸šåŠ¡å¤±è´¥ï¼Œåˆ¤æ–­æ˜¯å¦å¯é‡è¯•
                    if (retryAttempt < retryPolicy.getMaxRetryCount() && isRetryableFailure(result)) {
                        retryAttempt++;
                        lastException = new RuntimeException(result.getMessage());
                        log.warn("步骤执行失败,准备重试: deviceId={}, operation={}, retryAttempt={}, message={}",
                            device.getId(), operation, retryAttempt, result.getMessage());
                        continue;
                    }
                    // åŒæ­¥å¤±è´¥çŠ¶æ€
                    deviceCoordinationService.syncDeviceStatus(device,
                        DeviceCoordinationService.DeviceStatus.FAILED, context);
                    return StepResult.failure(device.getDeviceName(), result.getMessage());
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                log.error("步骤执行被中断, deviceId={}, operation={}", device.getId(), operation, e);
                step.setStatus(TaskStepDetail.Status.FAILED.name());
                step.setErrorMessage("执行被中断: " + e.getMessage());
                step.setEndTime(new Date());
                step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime());
                step.setRetryCount(retryAttempt);
                taskStepDetailMapper.updateById(step);
                updateTaskProgress(task, step.getStepOrder(), false);
                return StepResult.failure(device.getDeviceName(), "执行被中断");
            } catch (Exception e) {
                lastException = e;
                log.error("设备操作异常, deviceId={}, operation={}, retryAttempt={}",
                    device.getId(), operation, retryAttempt, e);
                // åˆ¤æ–­æ˜¯å¦å¯é‡è¯•
                if (retryAttempt < retryPolicy.getMaxRetryCount() && retryPolicy.isRetryable(e)) {
                    retryAttempt++;
                    log.warn("步骤执行异常,准备重试: deviceId={}, operation={}, retryAttempt={}, exception={}",
                        device.getId(), operation, retryAttempt, e.getClass().getSimpleName());
                    continue;
                }
                // ä¸å¯é‡è¯•或达到最大重试次数
                step.setStatus(TaskStepDetail.Status.FAILED.name());
                step.setErrorMessage(e.getMessage());
                step.setEndTime(new Date());
                step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime());
                step.setRetryCount(retryAttempt);
                taskStepDetailMapper.updateById(step);
                updateTaskProgress(task, step.getStepOrder(), false);
                // é€šçŸ¥æ­¥éª¤æ›´æ–°
                notificationService.notifyStepUpdate(task.getTaskId(), step);
                // åŒæ­¥å¤±è´¥çŠ¶æ€
                deviceCoordinationService.syncDeviceStatus(device,
                    DeviceCoordinationService.DeviceStatus.FAILED, context);
                String errorMsg = retryAttempt > 0
                    ? String.format("执行失败(已重试%d次): %s", retryAttempt, e.getMessage())
                    : e.getMessage();
                return StepResult.failure(device.getDeviceName(), errorMsg);
            }
            boolean opSuccess = Boolean.TRUE.equals(result.getSuccess());
            updateStepAfterOperation(step, result, opSuccess);
            updateTaskProgress(task, step.getStepOrder(), opSuccess);
            if (opSuccess) {
                updateContextAfterSuccess(device, context, params);
                return StepResult.success(device.getDeviceName(), result.getMessage());
            }
            return StepResult.failure(device.getDeviceName(), result.getMessage());
        } catch (Exception e) {
            log.error("设备操作异常, deviceId={}, operation={}", device.getId(), operation, e);
            step.setStatus(TaskStepDetail.Status.FAILED.name());
            step.setErrorMessage(e.getMessage());
            step.setEndTime(new Date());
            step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime());
            taskStepDetailMapper.updateById(step);
            updateTaskProgress(task, step.getStepOrder(), false);
            return StepResult.failure(device.getDeviceName(), e.getMessage());
        }
        // è¾¾åˆ°æœ€å¤§é‡è¯•次数
        step.setStatus(TaskStepDetail.Status.FAILED.name());
        step.setErrorMessage(lastException != null ? lastException.getMessage() : "执行失败");
        step.setEndTime(new Date());
        step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime());
        step.setRetryCount(retryAttempt);
        taskStepDetailMapper.updateById(step);
        updateTaskProgress(task, step.getStepOrder(), false);
        // é€šçŸ¥æ­¥éª¤æ›´æ–°
        notificationService.notifyStepUpdate(task.getTaskId(), step);
        deviceCoordinationService.syncDeviceStatus(device,
            DeviceCoordinationService.DeviceStatus.FAILED, context);
        return StepResult.failure(device.getDeviceName(),
            String.format("执行失败(已重试%d次)", retryAttempt));
    }
    private StepResult executeInteractionStep(MultiDeviceTask task,
                                              TaskStepDetail step,
                                              DeviceConfig device,
                                              TaskExecutionContext context,
                                              DeviceInteraction deviceInteraction) {
        try {
            InteractionContext interactionContext = new InteractionContext(device, context);
            step.setInputData(toJson(context.getParameters()));
            InteractionResult interactionResult = deviceInteraction.execute(interactionContext);
            boolean success = interactionResult != null && interactionResult.isSuccess();
            updateStepAfterInteraction(step, interactionResult);
            updateTaskProgress(task, step.getStepOrder(), success);
    /**
     * å¸¦é‡è¯•的交互步骤执行
     */
    private StepResult executeInteractionStepWithRetry(MultiDeviceTask task,
                                                      TaskStepDetail step,
                                                      DeviceConfig device,
                                                      TaskExecutionContext context,
                                                      DeviceInteraction deviceInteraction,
                                                      RetryPolicy retryPolicy) {
        int retryAttempt = 0;
        Exception lastException = null;
        while (retryAttempt <= retryPolicy.getMaxRetryCount()) {
            try {
                if (retryAttempt > 0) {
                    long waitTime = retryPolicy.calculateRetryInterval(retryAttempt);
                    log.info("交互步骤执行重试: deviceId={}, retryAttempt={}/{}, waitTime={}ms",
                        device.getId(), retryAttempt, retryPolicy.getMaxRetryCount(), waitTime);
                    Thread.sleep(waitTime);
                    step.setRetryCount(retryAttempt);
                    step.setStatus(TaskStepDetail.Status.RUNNING.name());
                    step.setStartTime(new Date());
                    taskStepDetailMapper.updateById(step);
                }
            if (success) {
                return StepResult.success(device.getDeviceName(), interactionResult.getMessage());
                InteractionContext interactionContext = new InteractionContext(device, context);
                step.setInputData(toJson(context.getParameters()));
                InteractionResult interactionResult = deviceInteraction.execute(interactionContext);
                boolean success = interactionResult != null && interactionResult.isSuccess();
                updateStepAfterInteraction(step, interactionResult);
                updateTaskProgress(task, step.getStepOrder(), success);
                if (success) {
                    deviceCoordinationService.syncDeviceStatus(device,
                        DeviceCoordinationService.DeviceStatus.COMPLETED, context);
                    return StepResult.success(device.getDeviceName(), interactionResult.getMessage());
                } else {
                    if (retryAttempt < retryPolicy.getMaxRetryCount()) {
                        retryAttempt++;
                        continue;
                    }
                    deviceCoordinationService.syncDeviceStatus(device,
                        DeviceCoordinationService.DeviceStatus.FAILED, context);
                    String message = interactionResult != null ? interactionResult.getMessage() : "交互执行失败";
                    return StepResult.failure(device.getDeviceName(), message);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                log.error("交互步骤执行被中断, deviceId={}", device.getId(), e);
                step.setStatus(TaskStepDetail.Status.FAILED.name());
                step.setErrorMessage("执行被中断: " + e.getMessage());
                step.setEndTime(new Date());
                step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime());
                step.setRetryCount(retryAttempt);
                taskStepDetailMapper.updateById(step);
                updateTaskProgress(task, step.getStepOrder(), false);
                return StepResult.failure(device.getDeviceName(), "执行被中断");
            } catch (Exception e) {
                lastException = e;
                log.error("交互执行异常, deviceId={}, retryAttempt={}", device.getId(), retryAttempt, e);
                if (retryAttempt < retryPolicy.getMaxRetryCount() && retryPolicy.isRetryable(e)) {
                    retryAttempt++;
                    continue;
                }
                step.setStatus(TaskStepDetail.Status.FAILED.name());
                step.setErrorMessage(e.getMessage());
                step.setEndTime(new Date());
                step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime());
                step.setRetryCount(retryAttempt);
                taskStepDetailMapper.updateById(step);
                updateTaskProgress(task, step.getStepOrder(), false);
                // é€šçŸ¥æ­¥éª¤æ›´æ–°
                notificationService.notifyStepUpdate(task.getTaskId(), step);
                deviceCoordinationService.syncDeviceStatus(device,
                    DeviceCoordinationService.DeviceStatus.FAILED, context);
                String errorMsg = retryAttempt > 0
                    ? String.format("执行失败(已重试%d次): %s", retryAttempt, e.getMessage())
                    : e.getMessage();
                return StepResult.failure(device.getDeviceName(), errorMsg);
            }
            String message = interactionResult != null ? interactionResult.getMessage() : "交互执行失败";
            return StepResult.failure(device.getDeviceName(), message);
        } catch (Exception e) {
            log.error("交互执行异常, deviceId={}", device.getId(), e);
            step.setStatus(TaskStepDetail.Status.FAILED.name());
            step.setErrorMessage(e.getMessage());
            step.setEndTime(new Date());
            step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime());
            taskStepDetailMapper.updateById(step);
            updateTaskProgress(task, step.getStepOrder(), false);
            return StepResult.failure(device.getDeviceName(), e.getMessage());
        }
        step.setStatus(TaskStepDetail.Status.FAILED.name());
        step.setErrorMessage(lastException != null ? lastException.getMessage() : "交互执行失败");
        step.setEndTime(new Date());
        step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime());
        step.setRetryCount(retryAttempt);
        taskStepDetailMapper.updateById(step);
        updateTaskProgress(task, step.getStepOrder(), false);
        // é€šçŸ¥æ­¥éª¤æ›´æ–°
        notificationService.notifyStepUpdate(task.getTaskId(), step);
        deviceCoordinationService.syncDeviceStatus(device,
            DeviceCoordinationService.DeviceStatus.FAILED, context);
        return StepResult.failure(device.getDeviceName(),
            String.format("执行失败(已重试%d次)", retryAttempt));
    }
    /**
     * èŽ·å–é‡è¯•ç­–ç•¥
     */
    private RetryPolicy getRetryPolicy(DeviceConfig device) {
        // å¯ä»¥ä»Žè®¾å¤‡é…ç½®ä¸­è¯»å–重试策略
        // æš‚时使用默认策略
        return RetryPolicy.defaultPolicy();
    }
    /**
     * åˆ¤æ–­ä¸šåŠ¡å¤±è´¥æ˜¯å¦å¯é‡è¯•
     */
    private boolean isRetryableFailure(DevicePlcVO.OperationResult result) {
        if (result == null || result.getMessage() == null) {
            return false;
        }
        String message = result.getMessage().toLowerCase();
        // ç½‘络错误、超时错误可重试
        return message.contains("timeout") ||
               message.contains("connection") ||
               message.contains("网络") ||
               message.contains("超时");
    }
    private void updateStepAfterOperation(TaskStepDetail step,
                                          DevicePlcVO.OperationResult result,
@@ -223,6 +682,9 @@
            update.set(MultiDeviceTask::getStatus, MultiDeviceTask.Status.FAILED.name());
        }
        multiDeviceTaskMapper.update(null, update);
        // é€šçŸ¥ä»»åŠ¡çŠ¶æ€æ›´æ–°
        notificationService.notifyTaskStatus(task);
    }
    private String determineOperation(DeviceConfig device, Map<String, Object> params) {
@@ -302,12 +764,28 @@
    private void updateContextAfterSuccess(DeviceConfig device,
                                           TaskExecutionContext context,
                                           Map<String, Object> params) {
        List<String> glassIds = extractGlassIds(params);
        switch (device.getDeviceType()) {
            case DeviceConfig.DeviceType.LOAD_VEHICLE:
                context.setLoadedGlassIds(extractGlassIds(params));
                context.setLoadedGlassIds(glassIds);
                // æ•°æ®ä¼ é€’:上大车 -> ä¸‹ä¸€ä¸ªè®¾å¤‡
                if (!CollectionUtils.isEmpty(glassIds)) {
                    Map<String, Object> transferData = new HashMap<>();
                    transferData.put("glassIds", glassIds);
                    transferData.put("sourceDevice", device.getDeviceCode());
                    // è¿™é‡Œç®€åŒ–处理,实际应该找到下一个设备
                    // åœ¨ä¸²è¡Œæ¨¡å¼ä¸‹ï¼Œä¸‹ä¸€ä¸ªè®¾å¤‡ä¼šåœ¨å¾ªçŽ¯ä¸­è‡ªåŠ¨èŽ·å–
                }
                break;
            case DeviceConfig.DeviceType.LARGE_GLASS:
                context.setProcessedGlassIds(extractGlassIds(params));
                context.setProcessedGlassIds(glassIds);
                // æ•°æ®ä¼ é€’:大理片 -> ä¸‹ä¸€ä¸ªè®¾å¤‡
                if (!CollectionUtils.isEmpty(glassIds)) {
                    Map<String, Object> transferData = new HashMap<>();
                    transferData.put("glassIds", glassIds);
                    transferData.put("sourceDevice", device.getDeviceCode());
                }
                break;
            default:
                break;
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskStatusNotificationService.java
New file
@@ -0,0 +1,61 @@
package com.mes.task.service;
import com.mes.task.entity.MultiDeviceTask;
import com.mes.task.entity.TaskStepDetail;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.List;
/**
 * ä»»åŠ¡çŠ¶æ€é€šçŸ¥æœåŠ¡
 * ç”¨äºŽå®žæ—¶æŽ¨é€ä»»åŠ¡æ‰§è¡ŒçŠ¶æ€
 *
 * @author mes
 * @since 2025-01-XX
 */
public interface TaskStatusNotificationService {
    /**
     * åˆ›å»ºSSE连接
     *
     * @param taskId ä»»åŠ¡ID(可选,如果为null则监听所有任务)
     * @return SSE发射器
     */
    SseEmitter createConnection(String taskId);
    /**
     * å‘送任务状态更新
     *
     * @param task ä»»åŠ¡å®žä½“
     */
    void notifyTaskStatus(MultiDeviceTask task);
    /**
     * å‘送任务步骤更新
     *
     * @param taskId ä»»åŠ¡ID
     * @param step æ­¥éª¤è¯¦æƒ…
     */
    void notifyStepUpdate(String taskId, TaskStepDetail step);
    /**
     * å‘送任务步骤列表更新
     *
     * @param taskId ä»»åŠ¡ID
     * @param steps æ­¥éª¤åˆ—表
     */
    void notifyStepsUpdate(String taskId, List<TaskStepDetail> steps);
    /**
     * å…³é—­æŒ‡å®šä»»åŠ¡çš„è¿žæŽ¥
     *
     * @param taskId ä»»åŠ¡ID
     */
    void closeConnections(String taskId);
    /**
     * å…³é—­æ‰€æœ‰è¿žæŽ¥
     */
    void closeAllConnections();
}
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/MultiDeviceTaskServiceImpl.java
@@ -19,6 +19,7 @@
import com.mes.task.model.TaskExecutionResult;
import com.mes.task.service.MultiDeviceTaskService;
import com.mes.task.service.TaskExecutionEngine;
import com.mes.task.service.TaskStatusNotificationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -44,6 +45,7 @@
    private final DeviceGroupRelationMapper deviceGroupRelationMapper;
    private final TaskStepDetailMapper taskStepDetailMapper;
    private final TaskExecutionEngine taskExecutionEngine;
    private final TaskStatusNotificationService notificationService;
    private final ObjectMapper objectMapper;
    @Override
@@ -78,12 +80,19 @@
        save(task);
        try {
            // é€šçŸ¥ä»»åС开始
            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());
            task.setResultData(writeJson(result.getData()));
            updateById(task);
            // é€šçŸ¥ä»»åŠ¡å®Œæˆ
            notificationService.notifyTaskStatus(task);
            return task;
        } catch (Exception ex) {
            log.error("多设备任务执行异常, taskId={}", task.getTaskId(), ex);
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/TaskStatusNotificationServiceImpl.java
New file
@@ -0,0 +1,322 @@
package com.mes.task.service.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mes.task.entity.MultiDeviceTask;
import com.mes.task.entity.TaskStepDetail;
import com.mes.task.service.TaskStatusNotificationService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
 * ä»»åŠ¡çŠ¶æ€é€šçŸ¥æœåŠ¡å®žçŽ°
 *
 * @author mes
 * @since 2025-01-XX
 */
@Slf4j
@Service
public class TaskStatusNotificationServiceImpl implements TaskStatusNotificationService {
    private final ObjectMapper objectMapper = new ObjectMapper();
    // å­˜å‚¨æ‰€æœ‰SSE连接:taskId -> List<SseEmitter>
    private final Map<String, List<SseEmitter>> connections = new ConcurrentHashMap<>();
    // å­˜å‚¨æ‰€æœ‰ä»»åŠ¡çš„è¿žæŽ¥ï¼ˆtaskId为null时使用)
    private final List<SseEmitter> allTaskConnections = new CopyOnWriteArrayList<>();
    // è¿žæŽ¥è¶…时时间(毫秒)
    private static final long TIMEOUT = 30 * 60 * 1000L; // 30分钟
    @Override
    public SseEmitter createConnection(String taskId) {
        SseEmitter emitter = new SseEmitter(TIMEOUT);
        // è®¾ç½®å®Œæˆå’Œè¶…时回调
        emitter.onCompletion(() -> {
            log.info("SSE连接完成: taskId={}", taskId);
            removeConnection(taskId, emitter);
        });
        emitter.onTimeout(() -> {
            log.info("SSE连接超时: taskId={}", taskId);
            removeConnection(taskId, emitter);
        });
        emitter.onError((ex) -> {
            log.error("SSE连接错误: taskId={}", taskId, ex);
            removeConnection(taskId, emitter);
        });
        // æ·»åŠ åˆ°è¿žæŽ¥åˆ—è¡¨
        if (taskId != null && !taskId.isEmpty()) {
            connections.computeIfAbsent(taskId, k -> new CopyOnWriteArrayList<>()).add(emitter);
        } else {
            allTaskConnections.add(emitter);
        }
        try {
            // å‘送初始连接成功消息
            Map<String, Object> initData = new HashMap<>();
            if (taskId != null) {
                initData.put("taskId", taskId);
            }
            emitter.send(SseEmitter.event()
                .name("connected")
                .data(createMessage("连接成功", initData)));
        } catch (IOException e) {
            log.error("发送初始消息失败: taskId={}", taskId, e);
            removeConnection(taskId, emitter);
            return null;
        }
        log.info("创建SSE连接: taskId={}, å½“前连接数={}", taskId, getConnectionCount(taskId));
        return emitter;
    }
    @Override
    public void notifyTaskStatus(MultiDeviceTask task) {
        if (task == null || task.getTaskId() == null) {
            return;
        }
        String taskId = task.getTaskId();
        Map<String, Object> data = createTaskStatusData(task);
        // å‘送给指定任务的连接
        sendToConnections(taskId, "taskStatus", data);
        // å‘送给所有任务的连接
        sendToAllTaskConnections("taskStatus", data);
        log.debug("推送任务状态: taskId={}, status={}", taskId, task.getStatus());
    }
    @Override
    public void notifyStepUpdate(String taskId, TaskStepDetail step) {
        if (taskId == null || step == null) {
            return;
        }
        Map<String, Object> data = createStepData(step);
        // å‘送给指定任务的连接
        sendToConnections(taskId, "stepUpdate", data);
        // å‘送给所有任务的连接
        Map<String, Object> allTaskData = new HashMap<>();
        allTaskData.put("taskId", taskId);
        allTaskData.put("step", data);
        sendToAllTaskConnections("stepUpdate", allTaskData);
        log.debug("推送步骤更新: taskId={}, stepOrder={}, status={}",
            taskId, step.getStepOrder(), step.getStatus());
    }
    @Override
    public void notifyStepsUpdate(String taskId, List<TaskStepDetail> steps) {
        if (taskId == null || steps == null) {
            return;
        }
        Map<String, Object> data = new HashMap<>();
        data.put("taskId", taskId);
        data.put("steps", steps);
        data.put("stepCount", steps.size());
        // å‘送给指定任务的连接
        sendToConnections(taskId, "stepsUpdate", data);
        // å‘送给所有任务的连接
        sendToAllTaskConnections("stepsUpdate", data);
        log.debug("推送步骤列表更新: taskId={}, stepCount={}", taskId, steps.size());
    }
    @Override
    public void closeConnections(String taskId) {
        if (taskId == null) {
            return;
        }
        List<SseEmitter> emitters = connections.remove(taskId);
        if (emitters != null) {
            for (SseEmitter emitter : emitters) {
                try {
                    emitter.complete();
                } catch (Exception e) {
                    log.warn("关闭SSE连接失败: taskId={}", taskId, e);
                }
            }
            log.info("关闭任务连接: taskId={}, è¿žæŽ¥æ•°={}", taskId, emitters.size());
        }
    }
    @Override
    public void closeAllConnections() {
        // å…³é—­æ‰€æœ‰ä»»åŠ¡è¿žæŽ¥
        for (Map.Entry<String, List<SseEmitter>> entry : connections.entrySet()) {
            for (SseEmitter emitter : entry.getValue()) {
                try {
                    emitter.complete();
                } catch (Exception e) {
                    log.warn("关闭SSE连接失败: taskId={}", entry.getKey(), e);
                }
            }
        }
        connections.clear();
        // å…³é—­æ‰€æœ‰ä»»åŠ¡ç›‘å¬è¿žæŽ¥
        for (SseEmitter emitter : allTaskConnections) {
            try {
                emitter.complete();
            } catch (Exception e) {
                log.warn("关闭SSE连接失败", e);
            }
        }
        allTaskConnections.clear();
        log.info("关闭所有SSE连接");
    }
    /**
     * å‘送消息到指定任务的连接
     */
    private void sendToConnections(String taskId, String eventName, Map<String, Object> data) {
        List<SseEmitter> emitters = connections.get(taskId);
        if (emitters == null || emitters.isEmpty()) {
            return;
        }
        List<SseEmitter> toRemove = new CopyOnWriteArrayList<>();
        for (SseEmitter emitter : emitters) {
            try {
                emitter.send(SseEmitter.event()
                    .name(eventName)
                    .data(createMessage("", data)));
            } catch (IOException e) {
                log.warn("发送SSE消息失败: taskId={}, event={}", taskId, eventName, e);
                toRemove.add(emitter);
            }
        }
        // ç§»é™¤å¤±è´¥çš„连接
        emitters.removeAll(toRemove);
    }
    /**
     * å‘送消息到所有任务监听连接
     */
    private void sendToAllTaskConnections(String eventName, Map<String, Object> data) {
        if (allTaskConnections.isEmpty()) {
            return;
        }
        List<SseEmitter> toRemove = new CopyOnWriteArrayList<>();
        for (SseEmitter emitter : allTaskConnections) {
            try {
                emitter.send(SseEmitter.event()
                    .name(eventName)
                    .data(createMessage("", data)));
            } catch (IOException e) {
                log.warn("发送SSE消息失败: event={}", eventName, e);
                toRemove.add(emitter);
            }
        }
        // ç§»é™¤å¤±è´¥çš„连接
        allTaskConnections.removeAll(toRemove);
    }
    /**
     * ç§»é™¤è¿žæŽ¥
     */
    private void removeConnection(String taskId, SseEmitter emitter) {
        if (taskId != null && !taskId.isEmpty()) {
            List<SseEmitter> emitters = connections.get(taskId);
            if (emitters != null) {
                emitters.remove(emitter);
                if (emitters.isEmpty()) {
                    connections.remove(taskId);
                }
            }
        } else {
            allTaskConnections.remove(emitter);
        }
    }
    /**
     * èŽ·å–è¿žæŽ¥æ•°
     */
    private int getConnectionCount(String taskId) {
        if (taskId != null && !taskId.isEmpty()) {
            List<SseEmitter> emitters = connections.get(taskId);
            return emitters != null ? emitters.size() : 0;
        }
        return allTaskConnections.size();
    }
    /**
     * åˆ›å»ºä»»åŠ¡çŠ¶æ€æ•°æ®
     */
    private Map<String, Object> createTaskStatusData(MultiDeviceTask task) {
        Map<String, Object> data = new HashMap<>();
        data.put("taskId", task.getTaskId() != null ? task.getTaskId() : "");
        data.put("groupId", task.getGroupId() != null ? task.getGroupId() : "");
        data.put("status", task.getStatus() != null ? task.getStatus() : "");
        data.put("currentStep", task.getCurrentStep() != null ? task.getCurrentStep() : 0);
        data.put("totalSteps", task.getTotalSteps() != null ? task.getTotalSteps() : 0);
        data.put("startTime", task.getStartTime() != null ? task.getStartTime().getTime() : 0);
        data.put("endTime", task.getEndTime() != null ? task.getEndTime().getTime() : 0);
        data.put("errorMessage", task.getErrorMessage() != null ? task.getErrorMessage() : "");
        return data;
    }
    /**
     * åˆ›å»ºæ­¥éª¤æ•°æ®
     */
    private Map<String, Object> createStepData(TaskStepDetail step) {
        Map<String, Object> data = new HashMap<>();
        data.put("id", step.getId() != null ? step.getId() : 0);
        data.put("stepOrder", step.getStepOrder() != null ? step.getStepOrder() : 0);
        data.put("deviceId", step.getDeviceId() != null ? step.getDeviceId() : "");
        data.put("stepName", step.getStepName() != null ? step.getStepName() : "");
        data.put("status", step.getStatus() != null ? step.getStatus() : "");
        data.put("startTime", step.getStartTime() != null ? step.getStartTime().getTime() : 0);
        data.put("endTime", step.getEndTime() != null ? step.getEndTime().getTime() : 0);
        data.put("durationMs", step.getDurationMs() != null ? step.getDurationMs() : 0);
        data.put("retryCount", step.getRetryCount() != null ? step.getRetryCount() : 0);
        data.put("errorMessage", step.getErrorMessage() != null ? step.getErrorMessage() : "");
        return data;
    }
    /**
     * åˆ›å»ºæ¶ˆæ¯å¯¹è±¡
     */
    private String createMessage(String message, Map<String, Object> data) {
        try {
            Map<String, Object> result = new java.util.HashMap<>();
            result.put("timestamp", System.currentTimeMillis());
            if (message != null && !message.isEmpty()) {
                result.put("message", message);
            }
            if (data != null && !data.isEmpty()) {
                result.putAll(data);
            }
            return objectMapper.writeValueAsString(result);
        } catch (Exception e) {
            log.error("序列化消息失败", e);
            return "{}";
        }
    }
}
mes-processes/mes-plcSend/src/main/resources/application.yml
@@ -17,7 +17,7 @@
        connectTimeout: 5000
        readTimeout: 5000
mybatis-plus:
  mapper-locations: classpath*:mapper/*.xml
  mapper-locations: classpath*:mapper/**/*.xml
#  configuration:
#    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mes-processes/mes-plcSend/src/main/resources/db/migration/V20241120__create_glass_info_table.sql
New file
@@ -0,0 +1,44 @@
-- çŽ»ç’ƒä¿¡æ¯è¡¨
-- ç”¨äºŽå­˜å‚¨çŽ»ç’ƒID和尺寸等信息的映射关系
CREATE TABLE IF NOT EXISTS glass_info (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    glass_id VARCHAR(50) NOT NULL UNIQUE COMMENT '玻璃ID(唯一标识)',
    glass_length INT DEFAULT NULL COMMENT '玻璃长度(mm)',
    glass_width INT DEFAULT NULL COMMENT '玻璃宽度(mm)',
    glass_thickness DECIMAL(5,2) DEFAULT NULL COMMENT '玻璃厚度(mm)',
    glass_type VARCHAR(50) DEFAULT NULL COMMENT '玻璃类型',
    manufacturer VARCHAR(100) DEFAULT NULL COMMENT '生产厂商',
    production_date DATE DEFAULT NULL COMMENT '生产日期',
    status VARCHAR(20) DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE-活跃, ARCHIVED-已归档',
    description VARCHAR(500) DEFAULT NULL COMMENT '描述信息',
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    created_by VARCHAR(50) DEFAULT 'system' COMMENT '创建人',
    updated_by VARCHAR(50) DEFAULT 'system' COMMENT '更新人',
    is_deleted TINYINT DEFAULT 0 COMMENT '是否删除:0-否,1-是',
    INDEX idx_glass_id (glass_id),
    INDEX idx_status (status),
    INDEX idx_created_time (created_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='玻璃信息表';
-- æ’入测试数据
INSERT INTO glass_info (glass_id, glass_length, glass_width, glass_thickness, glass_type, manufacturer, status, description) VALUES
('GLS-2024-001', 2000, 1500, 5.0, '普通玻璃', '厂商A', 'ACTIVE', '测试玻璃1'),
('GLS-2024-002', 1800, 1200, 6.0, '钢化玻璃', '厂商B', 'ACTIVE', '测试玻璃2'),
('GLS-2024-003', 2200, 1600, 5.5, '普通玻璃', '厂商A', 'ACTIVE', '测试玻璃3'),
('GLS-2024-004', 1900, 1400, 6.5, '钢化玻璃', '厂商C', 'ACTIVE', '测试玻璃4'),
('GLS-2024-005', 2100, 1500, 5.0, '普通玻璃', '厂商B', 'ACTIVE', '测试玻璃5'),
('GLS-2024-006', 2000, 1600, 6.0, '钢化玻璃', '厂商A', 'ACTIVE', '测试玻璃6'),
('GLS-2024-007', 1850, 1300, 5.5, '普通玻璃', '厂商C', 'ACTIVE', '测试玻璃7'),
('GLS-2024-008', 1950, 1450, 6.0, '钢化玻璃', '厂商B', 'ACTIVE', '测试玻璃8'),
('GLS-2024-009', 2050, 1550, 5.0, '普通玻璃', '厂商A', 'ACTIVE', '测试玻璃9'),
('GLS-2024-010', 1750, 1250, 6.5, '钢化玻璃', '厂商C', 'ACTIVE', '测试玻璃10'),
('DEVICE_001-GLS-001', 2000, 1500, 5.0, '普通玻璃', '测试厂商', 'ACTIVE', '设备1测试玻璃1'),
('DEVICE_001-GLS-002', 1800, 1200, 6.0, '钢化玻璃', '测试厂商', 'ACTIVE', '设备1测试玻璃2'),
('DEVICE_001-GLS-003', 2200, 1600, 5.5, '普通玻璃', '测试厂商', 'ACTIVE', '设备1测试玻璃3'),
('DEVICE_002-GLS-001', 1900, 1400, 6.5, '钢化玻璃', '测试厂商', 'ACTIVE', '设备2测试玻璃1'),
('DEVICE_002-GLS-002', 2100, 1500, 5.0, '普通玻璃', '测试厂商', 'ACTIVE', '设备2测试玻璃2'),
('DEVICE_003-GLS-001', 2000, 1600, 6.0, '钢化玻璃', '测试厂商', 'ACTIVE', '设备3测试玻璃1');
mes-processes/mes-plcSend/src/main/resources/mapper/DeviceGlassInfoMapper.xml
New file
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mes.device.mapper.DeviceGlassInfoMapper">
    <!-- æ ¹æ®çŽ»ç’ƒID列表批量查询玻璃信息 -->
    <select id="selectByGlassIds" resultType="com.mes.device.entity.GlassInfo">
        SELECT * FROM glass_info
        WHERE glass_id IN
        <foreach collection="glassIds" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
        AND is_deleted = 0
    </select>
</mapper>
mes-processes/mes-plcSend/src/main/resources/mapper/device/DeviceGroupRelationMapper.xml
@@ -5,38 +5,46 @@
    <!-- æ‰¹é‡æ·»åŠ è®¾å¤‡åˆ°è®¾å¤‡ç»„ -->
    <insert id="batchAddDevicesToGroup" parameterType="map">
        INSERT INTO device_group_relation (group_id, device_id, role, status, priority, connection_order, 
                                          created_at, updated_at, created_by, updated_by)
                                          created_time, updated_time, created_by, updated_by)
        SELECT 
            #{groupId} as group_id,
            device_id,
            #{groupId},
            t.device_id,
            CASE 
                WHEN #{deviceRole} = 'CONTROLLER' THEN 1
                WHEN #{deviceRole} = 'COLLABORATOR' THEN 2
                WHEN #{deviceRole} = 'MONITOR' THEN 3
                ELSE 2
            END as role,
            1 as status,
            5 as priority,
            ROW_NUMBER() OVER (ORDER BY device_id) as connection_order,
            NOW() as created_at,
            NOW() as updated_at,
            'system' as created_by,
            'system' as updated_by
            END,
            1,
            5,
            (SELECT IFNULL(MAX(connection_order), 0) FROM device_group_relation WHERE group_id = #{groupId} AND is_deleted = 0) +
            t.row_num as connection_order,
            NOW(),
            NOW(),
            'system',
            'system'
        FROM (
            SELECT
                device_id,
                @row_num := @row_num + 1 as row_num
        FROM (
            <foreach collection="deviceIds" item="deviceId" separator=" UNION ALL ">
                SELECT #{deviceId} as device_id
            </foreach>
            ) d
            CROSS JOIN (SELECT @row_num := 0) r
            ORDER BY device_id
        ) t
        WHERE NOT EXISTS (
            SELECT 1 FROM device_group_relation
            WHERE group_id = #{groupId} AND device_id = device_id AND is_deleted = 0
            SELECT 1 FROM device_group_relation dgr
            WHERE dgr.group_id = #{groupId} AND dgr.device_id = t.device_id AND dgr.is_deleted = 0
        )
    </insert>
    <!-- æ‰¹é‡ä»Žè®¾å¤‡ç»„移除设备 -->
    <update id="batchRemoveDevicesFromGroup" parameterType="map">
        UPDATE device_group_relation 
        SET is_deleted = 1, updated_at = NOW(), updated_by = 'system'
        SET is_deleted = 1, updated_time = NOW(), updated_by = 'system'
        WHERE group_id = #{groupId} 
        AND device_id IN 
        <foreach collection="deviceIds" item="deviceId" open="(" separator="," close=")">
@@ -54,7 +62,7 @@
                WHEN #{deviceRole} = 'MONITOR' THEN 3
                ELSE 2
            END,
            updated_at = NOW(),
            updated_time = NOW(),
            updated_by = 'system'
        WHERE group_id = #{groupId} 
        AND device_id = #{deviceId} 
@@ -73,7 +81,7 @@
    <!-- åˆ é™¤è®¾å¤‡ç»„关联 -->
    <delete id="deleteDeviceFromGroup">
        UPDATE device_group_relation 
        SET is_deleted = 1, updated_at = NOW(), updated_by = 'system'
        SET is_deleted = 1, updated_time = NOW(), updated_by = 'system'
        WHERE group_id = #{groupId} 
        AND device_id = #{deviceId} 
        AND is_deleted = 0
mes-web/src/api/device/deviceManagement.js
@@ -557,6 +557,56 @@
  }
}
// è®¾å¤‡çŠ¶æ€ç®¡ç†API
export const deviceStatusApi = {
  /**
   * æ›´æ–°è®¾å¤‡åœ¨çº¿çŠ¶æ€
   * @param {Object} data - { deviceId, status }
   */
  updateDeviceOnlineStatus(data) {
    return request({
      url: '/api/plcSend/device/status/update',
      method: 'post',
      data
    })
  },
  /**
   * æ‰¹é‡æ›´æ–°è®¾å¤‡åœ¨çº¿çŠ¶æ€
   * @param {Object} data - { deviceIds, status }
   */
  batchUpdateDeviceOnlineStatus(data) {
    return request({
      url: '/api/plcSend/device/status/batch-update',
      method: 'post',
      data
    })
  },
  /**
   * èŽ·å–è®¾å¤‡æœ€æ–°çŠ¶æ€
   * @param {Number} deviceId - è®¾å¤‡é…ç½®ID
   */
  getLatestStatus(deviceId) {
    return request({
      url: `/api/plcSend/device/status/latest/${deviceId}`,
      method: 'get'
    })
  },
  /**
   * è®°å½•设备心跳
   * @param {Object} data - { deviceId, status }
   */
  recordHeartbeat(data) {
    return request({
      url: '/api/plcSend/device/status/heartbeat',
      method: 'post',
      data
    })
  }
}
// ç»Ÿè®¡API
export const getDeviceStatistics = (data) => {
  return request({
@@ -579,6 +629,7 @@
  deviceGroupApi,
  devicePlcApi,
  deviceInteractionApi,
  deviceStatusApi,
  getDeviceStatistics,
  getDeviceGroupStatistics
}
mes-web/src/router/index.js
@@ -30,11 +30,6 @@
          component: () => import('../views/plcTest/return.vue'),
          children: [
            {
              path: '/plcTest/Test',
              name: 'plcTest',
              component: () => import('../views/plcTest/Test.vue')
            },
            {
              path: '/plcTest/MultiDeviceWorkbench',
              name: 'MultiDeviceWorkbench',
              component: () => import('../views/plcTest/MultiDeviceWorkbench.vue')
mes-web/src/utils/PlcTestUtil.js
File was deleted
mes-web/src/utils/plcFieldMapping.js
File was deleted
mes-web/src/views/device/DeviceConfigList.vue
@@ -5,9 +5,9 @@
      <el-form :model="searchForm" :inline="true" class="search-form">
        <el-form-item label="设备类型">
          <el-select v-model="searchForm.deviceType" placeholder="选择设备类型" clearable>
            <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>
        <el-form-item label="设备状态">
@@ -438,9 +438,9 @@
// å·¥å…·å‡½æ•°
const getDeviceTypeTag = (type) => {
  const typeMap = {
    '上大车': 'primary',
    '大理片': 'success',
    '玻璃存储': 'warning'
    '大车设备': 'primary',
    '大理片笼': 'success',
    '卧式缓存': 'warning'
  }
  return typeMap[type] || 'info'
}
mes-web/src/views/device/DeviceEditDialog.vue
@@ -284,11 +284,25 @@
          </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
@@ -524,6 +538,7 @@
  // ä¸Šå¤§è½¦å‚æ•°
  vehicleCapacity: 6000,
  glassIntervalMs: 1000,
  defaultGlassLength: 2000,
  autoFeed: true,
  maxRetryCount: 5,
  positionMapping: {},
@@ -812,6 +827,7 @@
  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 || {}
@@ -860,6 +876,7 @@
  // é‡ç½®è®¾å¤‡é€»è¾‘参数
  deviceLogicParams.vehicleCapacity = 6000
  deviceLogicParams.glassIntervalMs = 1000
  deviceLogicParams.defaultGlassLength = 2000
  deviceLogicParams.autoFeed = true
  deviceLogicParams.maxRetryCount = 5
  deviceLogicParams.positionMapping = {}
@@ -951,6 +968,7 @@
    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
mes-web/src/views/device/DeviceGroupEditDialog.vue
@@ -1,11 +1,10 @@
<template>
  <el-dialog
    :visible="visible"
    v-model="dialogVisible"
    :title="title"
    width="70%"
    :close-on-click-modal="false"
    @close="handleClose"
    @update:visible="(val) => emit('update:visible', val)"
  >
    <el-form
      ref="formRef"
@@ -51,10 +50,9 @@
            
            <el-form-item label="组类型" prop="groupType">
              <el-select v-model="form.groupType" placeholder="选择组类型">
                <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>
            
@@ -93,7 +91,16 @@
              <el-select v-model="form.groupStatus" placeholder="选择组状态">
                <el-option label="启用" value="ENABLED" />
                <el-option label="禁用" value="DISABLED" />
                <el-option label="维护中" value="MAINTENANCE" />
              </el-select>
            </el-form-item>
            <el-form-item label="执行模式" prop="executionMode">
              <el-radio-group v-model="form.executionMode">
                <el-radio label="SERIAL">串行执行</el-radio>
                <el-radio label="PARALLEL">并行执行</el-radio>
              </el-radio-group>
              <div class="form-tip">串行:按顺序依次执行设备操作;并行:同时执行多个设备操作</div>
            </el-form-item>
            
            <el-form-item label="最大设备数" prop="maxDeviceCount">
@@ -348,16 +355,23 @@
const testing = ref(false)
const saving = ref(false)
const validationResult = ref(null)
// ä½¿ç”¨è®¡ç®—属性来同步 visible prop å’Œå†…部 dialogVisible
const dialogVisible = computed({
  get: () => props.visible,
  set: (val) => emit('update:visible', val)
})
const customParamsText = ref('')
// è¡¨å•数据
const form = reactive({
  groupName: '',
  groupCode: '',
  groupType: '设备组',
  groupType: '生产线',
  description: '',
  sortOrder: 0,
  groupStatus: 'ENABLED',
  executionMode: 'SERIAL', // æ‰§è¡Œæ¨¡å¼ï¼šSERIAL串行 / PARALLEL并行
  maxDeviceCount: 100,
  heartbeatInterval: 30,
  connectionTimeout: 10,
@@ -435,17 +449,18 @@
      }
    })
  }
})
}, { immediate: true })
// æ–¹æ³•定义
const resetForm = () => {
  Object.assign(form, {
    groupName: '',
    groupCode: '',
    groupType: '设备组',
    groupType: '生产线',
    description: '',
    sortOrder: 0,
    groupStatus: 'ENABLED',
    executionMode: 'SERIAL',
    maxDeviceCount: 100,
    heartbeatInterval: 30,
    connectionTimeout: 10,
@@ -471,13 +486,31 @@
const loadFormData = () => {
  if (!props.data) return
  
  // è½¬æ¢åŽç«¯çŠ¶æ€å­—æ®µåˆ°å‰ç«¯æ ¼å¼
  // åŽç«¯ status: 0=停用, 1=启用, 2=维护中
  // å‰ç«¯ groupStatus: "ENABLED"/"DISABLED"/"MAINTENANCE"
  let groupStatus = 'ENABLED'
  if (props.data.status !== undefined) {
    // ä¼˜å…ˆä½¿ç”¨åŽç«¯çš„ status å­—段
    if (props.data.status === 1) {
      groupStatus = 'ENABLED'
    } else if (props.data.status === 2) {
      groupStatus = 'MAINTENANCE'
    } else {
      groupStatus = 'DISABLED'
    }
  } else if (props.data.groupStatus) {
    // å…¼å®¹å‰ç«¯å·²æœ‰çš„ groupStatus å­—段
    groupStatus = props.data.groupStatus
  }
  Object.assign(form, {
    groupName: props.data.groupName || '',
    groupCode: props.data.groupCode || '',
    groupType: props.data.groupType || '设备组',
    groupType: props.data.groupType || '生产线',
    description: props.data.description || '',
    sortOrder: props.data.sortOrder || 0,
    groupStatus: props.data.groupStatus || 'ENABLED',
    groupStatus: groupStatus,
    maxDeviceCount: props.data.maxDeviceCount || 100,
    heartbeatInterval: props.data.heartbeatInterval || 30,
    connectionTimeout: props.data.connectionTimeout || 10,
@@ -493,8 +526,24 @@
    logLevel: props.data.logLevel || 'INFO',
    enableAutoBackup: props.data.enableAutoBackup || false,
    backupInterval: props.data.backupInterval || 24,
    customParams: props.data.customParams || {}
    customParams: props.data.customParams || props.data.extraConfig ? (typeof props.data.extraConfig === 'string' ? JSON.parse(props.data.extraConfig) : props.data.extraConfig) : {}
  })
  // ä»ŽcustomParams或extraConfig中读取executionMode
  let executionMode = 'SERIAL' // é»˜è®¤ä¸²è¡Œ
  if (form.customParams && form.customParams.executionMode) {
    executionMode = form.customParams.executionMode
  } else if (props.data.extraConfig) {
    try {
      const extraConfig = typeof props.data.extraConfig === 'string' ? JSON.parse(props.data.extraConfig) : props.data.extraConfig
      if (extraConfig.executionMode) {
        executionMode = extraConfig.executionMode
      }
    } catch (e) {
      console.warn('解析extraConfig失败:', e)
    }
  }
  form.executionMode = executionMode
  
  customParamsText.value = JSON.stringify(form.customParams, null, 2)
}
@@ -595,10 +644,33 @@
      return
    }
    
    // è½¬æ¢å‰ç«¯çŠ¶æ€å­—æ®µåˆ°åŽç«¯æ ¼å¼
    // å‰ç«¯ groupStatus: "ENABLED"/"DISABLED"/"MAINTENANCE"
    // åŽç«¯ status: 0=停用, 1=启用, 2=维护中
    let status = 1 // é»˜è®¤å¯ç”¨
    if (form.groupStatus === 'ENABLED') {
      status = 1
    } else if (form.groupStatus === 'MAINTENANCE') {
      status = 2
    } else {
      status = 0
    }
    // å°†executionMode保存到customParams中,以便后端从extraConfig中读取
    const customParams = {
      ...form.customParams,
      executionMode: form.executionMode
    }
    const config = {
      ...form,
      customParams: form.customParams
      status: status, // åŽç«¯éœ€è¦çš„ status å­—段
      customParams: customParams,
      extraConfig: JSON.stringify(customParams) // åŽç«¯ä½¿ç”¨extraConfig字段
    }
    // ç§»é™¤å‰ç«¯ä¸“用的 groupStatus å’Œ executionMode å­—段,避免后端混淆
    delete config.groupStatus
    delete config.executionMode
    
    const response = isEdit.value
      ? await deviceGroupApi.update(props.data.id, config)
mes-web/src/views/device/DeviceGroupList.vue
@@ -5,10 +5,9 @@
      <el-form :model="searchForm" :inline="true" class="search-form">
        <el-form-item label="组类型">
          <el-select v-model="searchForm.groupType" placeholder="选择组类型" clearable>
            <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>
        <el-form-item label="组状态">
@@ -67,6 +66,13 @@
          <template #default="scope">
            <el-tag :type="getGroupTypeTag(scope.row.groupType)">
              {{ scope.row.groupType }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="executionMode" label="执行模式" width="110">
          <template #default="scope">
            <el-tag :type="getExecutionModeTag(scope.row)">
              {{ getExecutionModeText(scope.row) }}
            </el-tag>
          </template>
        </el-table-column>
@@ -157,12 +163,26 @@
      <div class="device-management">
        <div class="dialog-header">
          <div class="device-stats">
            <el-statistic title="总设备数" :value="currentGroup?.deviceCount || 0" />
            <el-statistic title="在线设备" :value="currentGroup?.onlineDeviceCount || 0" />
            <el-statistic title="离线设备" :value="(currentGroup?.deviceCount || 0) - (currentGroup?.onlineDeviceCount || 0)" />
            <el-statistic title="总设备数" :value="groupDeviceList.length" />
            <el-statistic title="在线设备" :value="onlineDeviceCount" />
            <el-statistic title="离线设备" :value="offlineDeviceCount" />
          </div>
          <div class="dialog-buttons">
            <el-button type="primary" @click="addDevices">添加设备</el-button>
            <el-select
              v-model="selectedDeviceIds"
              multiple
              filterable
              placeholder="选择要添加的设备"
              style="width: 300px; margin-right: 12px;"
              @change="handleDeviceSelectChange"
            >
              <el-option
                v-for="device in availableDeviceList"
                :key="device.id || device.deviceId"
                :label="`${device.deviceName} (${device.deviceCode})`"
                :value="device.id || device.deviceId"
              />
            </el-select>
            <el-button type="danger" @click="removeDevices" :disabled="selectedDevicesInGroup.length === 0">
              ç§»é™¤è®¾å¤‡
            </el-button>
@@ -181,20 +201,43 @@
          <el-table-column prop="deviceCode" label="设备编码" />
          <el-table-column prop="deviceType" label="设备类型" />
          <el-table-column prop="plcIp" label="PLC IP" />
          <el-table-column prop="deviceStatus" label="设备状态">
          <el-table-column prop="isOnline" label="在线状态" width="120">
            <template #default="scope">
              <el-tag :type="getDeviceStatusTag(scope.row.deviceStatus)" size="small">
                {{ getDeviceStatusText(scope.row.deviceStatus) }}
              <el-tag :type="scope.row.isOnline ? 'success' : 'info'" size="small">
                {{ scope.row.isOnline ? '在线' : '离线' }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column label="操作" width="200" fixed="right">
            <template #default="scope">
              <el-button
                v-if="scope.row.isOnline"
                type="warning"
                size="small"
                @click="updateDeviceOnlineStatus(scope.row, 'OFFLINE')"
                :loading="scope.row.statusUpdating"
              >
                è®¾ä¸ºç¦»çº¿
              </el-button>
              <el-button
                v-else
                type="success"
                size="small"
                @click="updateDeviceOnlineStatus(scope.row, 'ONLINE')"
                :loading="scope.row.statusUpdating"
              >
                è®¾ä¸ºåœ¨çº¿
              </el-button>
            </template>
          </el-table-column>
        </el-table>
      </div>
      
      <template #footer>
        <el-button @click="deviceDialogVisible = false">关闭</el-button>
        <el-button @click="handleCloseDeviceDialog">关闭</el-button>
      </template>
    </el-dialog>
    <!-- ç»Ÿè®¡è¯¦æƒ…弹窗 -->
    <el-dialog
@@ -234,10 +277,10 @@
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, ArrowDown } from '@element-plus/icons-vue'
import { deviceGroupApi, devicePlcApi } from '@/api/device/deviceManagement'
import { deviceGroupApi, devicePlcApi, deviceConfigApi, deviceStatusApi } from '@/api/device/deviceManagement'
// å“åº”式数据
const groupTable = ref(null)
@@ -270,6 +313,22 @@
// ç»Ÿè®¡å¼¹çª—
const statisticsDialogVisible = ref(false)
// æ·»åŠ è®¾å¤‡ç›¸å…³
const availableDeviceList = ref([])
const selectedDeviceIds = ref([])
// è®¡ç®—属性:根据实际设备列表计算在线/离线设备数量
const onlineDeviceCount = computed(() => {
  return groupDeviceList.value.filter(device => {
    const status = device.deviceStatus || device.status
    return status === 'ONLINE' || status === '在线' || device.isOnline === true
  }).length
})
const offlineDeviceCount = computed(() => {
  return groupDeviceList.value.length - onlineDeviceCount.value
})
// äº‹ä»¶å®šä¹‰
const emit = defineEmits(['group-selected', 'refresh-statistics'])
@@ -288,7 +347,55 @@
    const response = await deviceGroupApi.getList(params)
    // MyBatis-Plus Page å¯¹è±¡ç»“构:{ records: [], total: 0 }
    if (response && response.data) {
      groupList.value = response.data.records || response.data.content || response.data.list || []
      const records = response.data.records || response.data.content || response.data.list || []
      // è½¬æ¢åŽç«¯çŠ¶æ€å­—æ®µåˆ°å‰ç«¯æ ¼å¼
      groupList.value = records.map(item => {
        // åŽç«¯å¯èƒ½è¿”回的 status æ ¼å¼ï¼š
        // 1. æ•°å­—类型:0=停用, 1=启用, 2=维护中
        // 2. å­—符串类型:"启用"、"停用"、"维护中"
        // 3. å­—符串类型:"ENABLED"、"DISABLED"、"MAINTENANCE"
        let statusNum = 0
        let statusStr = item.status
        if (typeof statusStr === 'number') {
          statusNum = statusStr
        } else if (typeof statusStr === 'string') {
          // å¤„理中文状态字符串
          if (statusStr === '启用' || statusStr === 'ENABLED') {
            statusNum = 1
            statusStr = 'ENABLED'
          } else if (statusStr === '停用' || statusStr === 'DISABLED') {
            statusNum = 0
            statusStr = 'DISABLED'
          } else if (statusStr === '维护中' || statusStr === 'MAINTENANCE') {
            statusNum = 2
            statusStr = 'MAINTENANCE'
          } else {
            // é»˜è®¤åœç”¨
            statusNum = 0
            statusStr = 'DISABLED'
          }
        } else if (item.groupStatus) {
          // å¦‚果有 groupStatus å­—段,使用它
          if (item.groupStatus === 'ENABLED') {
            statusNum = 1
            statusStr = 'ENABLED'
          } else if (item.groupStatus === 'MAINTENANCE') {
            statusNum = 2
            statusStr = 'MAINTENANCE'
          } else {
            statusNum = 0
            statusStr = 'DISABLED'
          }
        }
        return {
          ...item,
          status: statusNum,
          groupStatus: statusStr,
          enabled: statusNum === 1
        }
      })
      pagination.total = response.data.total || response.data.totalElements || 0
    } else {
      groupList.value = []
@@ -400,15 +507,24 @@
    if (row.enabled) {
      await deviceGroupApi.enable(groupId)
      ElMessage.success('设备组启用成功')
      // åŒæ­¥æ›´æ–° groupStatus
      row.groupStatus = 'ENABLED'
      row.status = 1
    } else {
      await deviceGroupApi.disable(groupId)
      ElMessage.success('设备组禁用成功')
      // åŒæ­¥æ›´æ–° groupStatus
      row.groupStatus = 'DISABLED'
      row.status = 0
    }
    emit('refresh-statistics')
    loadGroupList() // åˆ·æ–°åˆ—表
    // ä¸é‡æ–°åŠ è½½åˆ—è¡¨ï¼Œç›´æŽ¥æ›´æ–°å½“å‰è¡ŒçŠ¶æ€
  } catch (error) {
    console.error('更新设备组状态失败:', error)
    row.enabled = !row.enabled // æ¢å¤çŠ¶æ€
    // æ¢å¤çŠ¶æ€
    row.enabled = !row.enabled
    row.groupStatus = row.enabled ? 'ENABLED' : 'DISABLED'
    row.status = row.enabled ? 1 : 0
    ElMessage.error('更新设备组状态失败: ' + (error.response?.data?.message || error.message))
  }
}
@@ -477,7 +593,9 @@
const manageDevices = async (row) => {
  currentGroup.value = row
  deviceDialogVisible.value = true
  selectedDeviceIds.value = []
  await loadGroupDevices(row.id || row.groupId)
  await loadAvailableDevices()
}
const loadGroupDevices = async (groupId) => {
@@ -485,7 +603,41 @@
    deviceLoading.value = true
    const response = await deviceGroupApi.getGroupDevices(groupId)
    if (response && response.data) {
      groupDeviceList.value = response.data || []
      // è½¬æ¢åŽç«¯è¿”回的数据格式,将 status è½¬æ¢ä¸º deviceStatus,并转换状态值
      groupDeviceList.value = (response.data || []).map(device => {
        // ä¼˜å…ˆä½¿ç”¨ isOnline å­—段(来自 device_status è¡¨çš„æœ€æ–°çŠ¶æ€ï¼‰
        let deviceStatus = 'OFFLINE'
        if (device.isOnline !== undefined && device.isOnline !== null) {
          deviceStatus = device.isOnline ? 'ONLINE' : 'OFFLINE'
        } else if (device.status) {
          // å¦‚果没有 isOnline,则使用 status å­—段
          const statusMap = {
            '在线': 'ONLINE',
            '离线': 'OFFLINE',
            '维护中': 'MAINTENANCE',
            '故障': 'MAINTENANCE',
            '禁用': 'DISABLED',
            'ONLINE': 'ONLINE',
            'OFFLINE': 'OFFLINE',
            'MAINTENANCE': 'MAINTENANCE',
            'DISABLED': 'DISABLED'
          }
          // å¦‚果是数字,转换为字符串
          const statusStr = typeof device.status === 'number'
            ? (device.status === 1 ? 'ONLINE' : 'OFFLINE')
            : String(device.status)
          deviceStatus = statusMap[statusStr] || 'OFFLINE'
        }
        return {
          ...device,
          deviceStatus: deviceStatus,
          // ä¿ç•™åŽŸå§‹å­—æ®µä»¥ä¾¿å…¼å®¹
          status: device.status,
          isOnline: device.isOnline !== undefined ? device.isOnline : (deviceStatus === 'ONLINE'),
          statusUpdating: false // æ·»åŠ æ›´æ–°çŠ¶æ€æ ‡è®°
        }
      })
    } else {
      groupDeviceList.value = []
    }
@@ -507,9 +659,80 @@
  selectedDevicesInGroup.value = selection
}
const addDevices = () => {
  // æ·»åŠ è®¾å¤‡åˆ°ç»„é€»è¾‘
  ElMessage.info('添加设备功能开发中...')
const loadAvailableDevices = async () => {
  try {
    // èŽ·å–æ‰€æœ‰è®¾å¤‡åˆ—è¡¨
    const response = await deviceConfigApi.getList({
      page: 1,
      size: 1000 // èŽ·å–è¶³å¤Ÿå¤šçš„è®¾å¤‡
    })
    if (response && response.data) {
      const allDevices = response.data.records || response.data.content || response.data.list || []
      // è¿‡æ»¤æŽ‰å·²ç»åœ¨è®¾å¤‡ç»„中的设备
      const currentDeviceIds = new Set(groupDeviceList.value.map(d => d.id || d.deviceId))
      availableDeviceList.value = allDevices
        .filter(device => {
          const deviceId = device.id || device.deviceId
          return !currentDeviceIds.has(deviceId)
        })
        .map(device => {
          // è½¬æ¢è®¾å¤‡çŠ¶æ€
          let deviceStatus = 'OFFLINE'
          if (device.deviceStatus) {
            deviceStatus = device.deviceStatus
          } else if (device.status) {
            const statusMap = {
              '在线': 'ONLINE',
              '离线': 'OFFLINE',
              '维护中': 'MAINTENANCE',
              '故障': 'MAINTENANCE',
              '禁用': 'DISABLED'
            }
            deviceStatus = statusMap[device.status] || 'OFFLINE'
          }
          return {
            ...device,
            deviceStatus: deviceStatus
          }
        })
    } else {
      availableDeviceList.value = []
    }
  } catch (error) {
    console.error('加载可用设备失败:', error)
    ElMessage.error('加载可用设备失败: ' + (error.response?.data?.message || error.message))
    availableDeviceList.value = []
  }
}
const handleDeviceSelectChange = async (deviceIds) => {
  if (!deviceIds || deviceIds.length === 0) {
      return
    }
  try {
    const groupId = currentGroup.value.id || currentGroup.value.groupId
    await deviceGroupApi.batchAddDevicesToGroup({
      groupId: groupId,
      deviceIds: deviceIds
    })
    ElMessage.success(`成功添加 ${deviceIds.length} ä¸ªè®¾å¤‡åˆ°è®¾å¤‡ç»„`)
    // æ¸…空选择
    selectedDeviceIds.value = []
    // åˆ·æ–°è®¾å¤‡åˆ—表和可用设备列表
    await loadGroupDevices(groupId)
    await loadAvailableDevices()
    emit('refresh-statistics')
  } catch (error) {
    console.error('添加设备失败:', error)
    ElMessage.error('添加设备失败: ' + (error.response?.data?.message || error.message))
    // æ¸…空选择以便重试
    selectedDeviceIds.value = []
  }
}
const removeDevices = async () => {
@@ -519,11 +742,6 @@
      return
    }
    
    await ElMessageBox.confirm(
      `确定要从设备组中移除选中的 ${selectedDevicesInGroup.value.length} ä¸ªè®¾å¤‡å—?`,
      '移除设备确认'
    )
    const deviceIds = selectedDevicesInGroup.value.map(item => item.id || item.deviceId)
    // æ‰¹é‡ç§»é™¤è®¾å¤‡
    await deviceGroupApi.batchRemoveDevicesFromGroup({
@@ -531,13 +749,73 @@
      deviceIds
    })
    ElMessage.success(`成功移除 ${deviceIds.length} ä¸ªè®¾å¤‡`)
    loadGroupDevices(currentGroup.value.id || currentGroup.value.groupId)
    // æ¸…空选择
    selectedDevicesInGroup.value = []
    const groupId = currentGroup.value.id || currentGroup.value.groupId
    await loadGroupDevices(groupId)
    await loadAvailableDevices()
    emit('refresh-statistics')
  } catch (error) {
    if (error !== 'cancel') {
      console.error('移除设备失败:', error)
      ElMessage.error('移除设备失败')
    }
    ElMessage.error('移除设备失败: ' + (error.response?.data?.message || error.message))
  }
}
// æ›´æ–°è®¾å¤‡åœ¨çº¿çŠ¶æ€
const updateDeviceOnlineStatus = async (device, status) => {
  try {
    // è®¾ç½®æ›´æ–°ä¸­çŠ¶æ€
    device.statusUpdating = true
    const deviceId = device.id || device.deviceId
    if (!deviceId) {
      ElMessage.warning('设备ID不存在')
      return
    }
    await deviceStatusApi.updateDeviceOnlineStatus({
      deviceId: deviceId,
      status: status
    })
    // æ›´æ–°æœ¬åœ°çŠ¶æ€
    device.isOnline = status === 'ONLINE'
    // åŒæ—¶æ›´æ–° deviceStatus å­—段以保持一致性
    if (status === 'ONLINE') {
      device.deviceStatus = 'ONLINE'
    } else if (status === 'OFFLINE') {
      device.deviceStatus = 'OFFLINE'
    }
    ElMessage.success(`设备状态已更新为:${status === 'ONLINE' ? '在线' : '离线'}`)
    // åˆ·æ–°è®¾å¤‡åˆ—表以获取最新状态
    const groupId = currentGroup.value.id || currentGroup.value.groupId
    await loadGroupDevices(groupId)
    // åˆ·æ–°è®¾å¤‡ç»„列表以更新在线设备数量统计
    await loadGroupList()
    // æ›´æ–°å½“前设备组的在线设备数量(如果当前设备组在列表中)
    const currentGroupInList = groupList.value.find(g => (g.id || g.groupId) === groupId)
    if (currentGroupInList) {
      // é‡æ–°è®¡ç®—在线设备数量
      const onlineCount = groupDeviceList.value.filter(d => d.isOnline === true).length
      currentGroupInList.onlineDeviceCount = onlineCount
    }
  } catch (error) {
    console.error('更新设备在线状态失败:', error)
    ElMessage.error('更新设备在线状态失败: ' + (error.response?.data?.message || error.message))
  } finally {
    device.statusUpdating = false
  }
}
// å…³é—­è®¾å¤‡ç®¡ç†å¼¹çª—
const handleCloseDeviceDialog = async () => {
  deviceDialogVisible.value = false
  // å…³é—­æ—¶åˆ·æ–°è®¾å¤‡ç»„列表,确保在线设备数量是最新的
  await loadGroupList()
}
const handleCommand = async (command, row) => {
@@ -594,10 +872,9 @@
// å·¥å…·å‡½æ•°
const getGroupTypeTag = (type) => {
  const typeMap = {
    '设备组': 'primary',
    '管理组': 'success',
    '监控组': 'warning',
    '维护组': 'info'
    '生产线': 'primary',
    '测试线': 'success',
    '辅助设备组': 'warning'
  }
  return typeMap[type] || 'info'
}
@@ -605,7 +882,8 @@
const getGroupStatusTag = (status) => {
  const statusMap = {
    'ENABLED': 'success',
    'DISABLED': 'info'
    'DISABLED': 'info',
    'MAINTENANCE': 'warning'
  }
  return statusMap[status] || 'info'
}
@@ -613,9 +891,47 @@
const getGroupStatusText = (status) => {
  const statusMap = {
    'ENABLED': '启用',
    'DISABLED': '禁用'
    'DISABLED': '禁用',
    'MAINTENANCE': '维护中'
  }
  return statusMap[status] || status
}
// èŽ·å–æ‰§è¡Œæ¨¡å¼æ ‡ç­¾ç±»åž‹
const getExecutionModeTag = (row) => {
  const mode = getExecutionMode(row)
  return mode === 'PARALLEL' ? 'success' : 'primary'
}
// èŽ·å–æ‰§è¡Œæ¨¡å¼æ–‡æœ¬
const getExecutionModeText = (row) => {
  const mode = getExecutionMode(row)
  return mode === 'PARALLEL' ? '并行执行' : '串行执行'
}
// ä»ŽextraConfig或customParams中提取执行模式
const getExecutionMode = (row) => {
  // ä¼˜å…ˆä»ŽextraConfig中读取
  if (row.extraConfig) {
    try {
      const extraConfig = typeof row.extraConfig === 'string' ? JSON.parse(row.extraConfig) : row.extraConfig
      if (extraConfig.executionMode) {
        return extraConfig.executionMode.toUpperCase()
      }
    } catch (e) {
      console.warn('解析extraConfig失败:', e)
    }
  }
  // ä»ŽcustomParams中读取
  if (row.customParams && row.customParams.executionMode) {
    return String(row.customParams.executionMode).toUpperCase()
  }
  // å¦‚果有maxConcurrentDevices且大于1,默认并行
  if (row.maxConcurrentDevices && row.maxConcurrentDevices > 1) {
    return 'PARALLEL'
  }
  // é»˜è®¤ä¸²è¡Œ
  return 'SERIAL'
}
const getDeviceStatusTag = (status) => {
@@ -745,4 +1061,15 @@
  font-weight: bold;
  color: #409eff;
}
.add-device-dialog {
  padding: 10px 0;
}
.dialog-search {
  margin-bottom: 16px;
  padding: 16px;
  background-color: #f5f7fa;
  border-radius: 8px;
}
</style>
mes-web/src/views/plcTest/Test.vue
File was deleted
mes-web/src/views/plcTest/components/DeviceGroup/GroupList.vue
@@ -1,7 +1,7 @@
<template>
  <div class="group-list-panel">
    <div class="panel-header">
      <div>
      <div class="header-title">
        <h3>设备组列表</h3>
        <p>选择一个设备组进行编排测试</p>
      </div>
@@ -113,28 +113,58 @@
.panel-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  align-items: flex-start;
  gap: 16px;
  flex-wrap: wrap;
}
.header-title {
  flex: 0 0 auto;
  min-width: 0;
}
.panel-header h3 {
  margin: 0;
  font-size: 18px;
  white-space: nowrap;
}
.panel-header p {
  margin: 2px 0 0;
  color: #909399;
  font-size: 13px;
  white-space: nowrap;
  word-break: keep-all;
}
.actions {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-shrink: 0;
}
.search-input {
  width: 240px;
  min-width: 200px;
}
/* å°å±å¹•时,操作区域换行 */
@media (max-width: 768px) {
  .panel-header {
    flex-direction: column;
    align-items: stretch;
  }
  .actions {
    width: 100%;
    justify-content: flex-end;
  }
  .search-input {
    flex: 1;
    min-width: 0;
  }
}
.group-table {
mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue
@@ -30,8 +30,16 @@
          {{ row.currentStep || 0 }} / {{ row.totalSteps || 0 }}
        </template>
      </el-table-column>
      <el-table-column label="开始时间" min-width="160" prop="startTime" />
      <el-table-column label="结束时间" min-width="160" prop="endTime" />
      <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>
    <el-drawer v-model="drawerVisible" size="40%" title="任务步骤详情">
@@ -39,7 +47,7 @@
        <el-timeline-item
          v-for="step in steps"
          :key="step.id"
          :timestamp="step.startTime || '-'"
          :timestamp="formatDateTime(step.startTime) || '-'"
          :type="step.status === 'COMPLETED' ? 'success' : step.status === 'FAILED' ? 'danger' : 'primary'"
        >
          <div class="step-title">{{ step.stepName }}</div>
@@ -123,6 +131,28 @@
  return `${(ms / 1000).toFixed(1)} s`
}
// æ ¼å¼åŒ–日期时间
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 (error) {
    console.warn('格式化时间失败:', dateTime, error)
    return dateTime
  }
}
watch(
  () => props.groupId,
  () => {
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue
@@ -5,11 +5,24 @@
        <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>
      </div>
      <el-button type="primary" :disabled="!group" :loading="loading" @click="handleSubmit">
        <el-icon><Promotion /></el-icon>
        å¯åŠ¨æµ‹è¯•
      </el-button>
      <div class="action-buttons">
        <el-button
          type="danger"
          plain
          :disabled="!group || !loadDeviceId || loadDeviceLoading"
          :loading="clearLoading"
          @click="handleClearPlc"
        >
          <el-icon><Delete /></el-icon>
          æ¸…空PLC
        </el-button>
        <el-button type="primary" :disabled="!group" :loading="loading" @click="handleSubmit">
          <el-icon><Promotion /></el-icon>
          å¯åŠ¨æµ‹è¯•
        </el-button>
      </div>
    </div>
    <el-form :model="form" label-width="120px">
@@ -37,8 +50,9 @@
<script setup>
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Promotion } from '@element-plus/icons-vue'
import { Delete, Promotion } from '@element-plus/icons-vue'
import { multiDeviceTaskApi } from '@/api/device/multiDeviceTask'
import { deviceGroupApi, deviceInteractionApi } from '@/api/device/deviceManagement'
const props = defineProps({
  group: {
@@ -57,11 +71,16 @@
const glassIdsInput = ref('')
const loading = ref(false)
const clearLoading = ref(false)
const loadDeviceId = ref(null)
const loadDeviceName = ref('')
const loadDeviceLoading = ref(false)
watch(
  () => props.group,
  () => {
    glassIdsInput.value = ''
    fetchLoadDevice()
  }
)
@@ -72,6 +91,42 @@
    .map((item) => item.trim())
    .filter((item) => item.length > 0)
})
const fetchLoadDevice = async () => {
  loadDeviceId.value = null
  loadDeviceName.value = ''
  if (!props.group) {
    return
  }
  const groupId = props.group.id || props.group.groupId
  if (!groupId) {
    return
  }
  loadDeviceLoading.value = true
  try {
    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
      : []
    const targetDevice =
      deviceList.find((item) => (item.deviceType || '').toUpperCase() === 'LOAD_VEHICLE') ||
      deviceList[0]
    if (targetDevice && targetDevice.id) {
      loadDeviceId.value = targetDevice.id
      loadDeviceName.value = targetDevice.deviceName || targetDevice.deviceCode || `ID: ${targetDevice.id}`
    }
  } catch (error) {
    console.error('加载设备信息失败:', error)
    ElMessage.error(error?.message || '获取设备信息失败')
  } finally {
    loadDeviceLoading.value = false
  }
}
const handleSubmit = async () => {
  if (!props.group) {
@@ -99,6 +154,42 @@
    ElMessage.error(error?.message || '任务启动失败')
  } finally {
    loading.value = false
  }
}
const handleClearPlc = async () => {
  if (!props.group) {
    ElMessage.warning('请先选择设备组')
    return
  }
  if (!loadDeviceId.value) {
    ElMessage.warning('未找到上大车设备,无法清空PLC')
    return
  }
  try {
    clearLoading.value = true
    const response = await deviceInteractionApi.executeOperation({
      deviceId: loadDeviceId.value,
      operation: 'clearGlass',
      params: {
        positionCode: form.positionCode || null
      }
    })
    if (response?.code !== 200) {
      throw new Error(response?.message || 'PLC清空失败')
    }
    const result = response?.data
    if (result?.success) {
      ElMessage.success(result?.message || 'PLC已清空')
      glassIdsInput.value = ''
    } else {
      throw new Error(result?.message || 'PLC清空失败')
    }
  } catch (error) {
    console.error('清空PLC失败:', error)
    ElMessage.error(error?.message || 'PLC清空失败')
  } finally {
    clearLoading.value = false
  }
}
</script>
@@ -131,5 +222,17 @@
.panel-header .warning {
  color: #f56c6c;
}
.panel-header .sub-info {
  margin-top: 4px;
  color: #606266;
  font-size: 12px;
}
.action-buttons {
  display: flex;
  gap: 12px;
  align-items: center;
}
</style>