huang
2025-11-18 1566e4c7604d85737ea67fe6757e71b8185fa48e
添加设备管理页面,添加测试设备任务监控页面
32个文件已修改
36个文件已添加
1个文件已删除
6880 ■■■■■ 已修改文件
mes-processes/mes-plcSend/README_PLC_ADDRESS_MAPPING.md 238 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/config/MybatisMetaObjectHandler.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/controller/DeviceConfigController.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/controller/DeviceControlProfileController.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/controller/DeviceGroupController.java 50 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/controller/DeviceInteractionController.java 46 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/controller/DevicePlcController.java 20 ●●●● 补丁 | 查看 | 原始文档 | 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/DeviceConfig.java 47 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DeviceGroupConfig.java 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DeviceGroupRelation.java 33 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DeviceInteractionExecution.java 55 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DeviceInteractionLogic.java 45 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/mapper/DeviceGroupRelationMapper.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/request/DeviceConfigRequest.java 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/request/DeviceGlassFeedRequest.java 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/request/DeviceGroupRequest.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/request/DevicePlcBatchRequest.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/DeviceInteractionService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceInteractionServiceImpl.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DevicePlcOperationServiceImpl.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/util/ConfigJsonHelper.java 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/vo/DeviceControlProfile.java 33 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/vo/DevicePlcVO.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/多设备联合测试扩展方案.md 1153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/BaseDeviceLogicHandler.java 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceInteraction.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceInteractionRegistry.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceLogicHandler.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceLogicHandlerFactory.java 77 ●●●●● 补丁 | 查看 | 原始文档 | 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/base/InteractionContext.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/base/InteractionResult.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/GlassStorageInteraction.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/LargeGlassInteraction.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/LoadVehicleInteraction.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/impl/GlassStorageLogicHandler.java 241 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/impl/LargeGlassLogicHandler.java 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/impl/LoadVehicleLogicHandler.java 372 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/service/PlcDynamicDataService.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/service/PlcTestWriteService.java 504 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/service/impl/PlcAutoTestServiceImpl.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/service/impl/PlcDynamicDataServiceImpl.java 299 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/service/impl/PlcTestWriteServiceImpl.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/controller/MultiDeviceTaskController.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/dto/MultiDeviceTaskQuery.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/dto/MultiDeviceTaskRequest.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/dto/TaskParameters.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/entity/MultiDeviceTask.java 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/entity/TaskStepDetail.java 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/mapper/MultiDeviceTaskMapper.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/mapper/TaskStepDetailMapper.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/model/TaskExecutionContext.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/model/TaskExecutionResult.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/MultiDeviceTaskService.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java 378 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/MultiDeviceTaskServiceImpl.java 160 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/resources/application-dev.yml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/api/device/deviceManagement.js 60 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/api/device/multiDeviceTask.js 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/router/index.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/utils/constants.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/DeviceEditDialog.vue 485 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/DeviceGroupEditDialog.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/DeviceGroupList.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/MultiDeviceWorkbench.vue 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/components/DeviceGroup/GroupList.vue 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/README_PLC_ADDRESS_MAPPING.md
File was deleted
mes-processes/mes-plcSend/src/main/java/com/mes/config/MybatisMetaObjectHandler.java
New file
@@ -0,0 +1,37 @@
package com.mes.config;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
 * ç»Ÿä¸€å¤„理公共字段(created_time、updated_time、created_by、updated_by)的自动填充
 */
@Component
public class MybatisMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        LocalDateTime now = LocalDateTime.now();
        String operator = resolveOperator();
        strictInsertFill(metaObject, "createdTime", LocalDateTime.class, now);
        strictInsertFill(metaObject, "updatedTime", LocalDateTime.class, now);
        strictInsertFill(metaObject, "createdBy", String.class, operator);
        strictInsertFill(metaObject, "updatedBy", String.class, operator);
    }
    @Override
    public void updateFill(MetaObject metaObject) {
        strictUpdateFill(metaObject, "updatedTime", LocalDateTime.class, LocalDateTime.now());
        strictUpdateFill(metaObject, "updatedBy", String.class, resolveOperator());
    }
    private String resolveOperator() {
        // TODO: ä¹‹åŽå¯æŽ¥å…¥ç™»å½•上下文,这里临时回退为 system
        return "system";
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/controller/DeviceConfigController.java
@@ -1,15 +1,16 @@
package com.mes.device.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mes.device.entity.DeviceConfig;
import com.mes.device.request.DeviceConfigRequest;
import com.mes.device.service.DeviceConfigService;
import com.mes.device.vo.DeviceConfigVO;
import com.mes.device.vo.StatisticsVO;
import com.mes.vo.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@@ -27,17 +28,20 @@
@Slf4j
@RestController
@RequestMapping("device/config")
@Tag(name = "设备配置管理", description = "设备配置管理相关接口")
@Api(tags = "设备配置管理")
public class DeviceConfigController {
    @Autowired
    private DeviceConfigService deviceConfigService;
    @Autowired
    private ObjectMapper objectMapper;
    /**
     * åˆ›å»ºè®¾å¤‡é…ç½®
     */
    @PostMapping("/devices")
    @Operation(summary = "创建设备配置", description = "创建新的设备配置")
    @ApiOperation("创建设备配置")
    public Result<DeviceConfig> createDevice(
            @Valid @RequestBody DeviceConfig deviceConfig) {
        try {
@@ -59,11 +63,23 @@
     * æ›´æ–°è®¾å¤‡é…ç½®
     */
    @PostMapping("/devices/update")
    @Operation(summary = "更新设备配置", description = "更新指定ID的设备配置")
    @ApiOperation("更新设备配置")
    public Result<DeviceConfig> updateDevice(
            @Valid @RequestBody DeviceConfigRequest request) {
        try {
            DeviceConfig deviceConfig = (DeviceConfig) request.getDeviceConfig();
            DeviceConfig deviceConfig;
            Object deviceConfigObj = request.getDeviceConfig();
            // å¦‚æžœ deviceConfig æ˜¯ Map ç±»åž‹ï¼ˆJSON ååºåˆ—化后的 LinkedHashMap),需要转换为 DeviceConfig
            if (deviceConfigObj instanceof Map) {
                deviceConfig = objectMapper.convertValue(deviceConfigObj, DeviceConfig.class);
            } else if (deviceConfigObj instanceof DeviceConfig) {
                deviceConfig = (DeviceConfig) deviceConfigObj;
            } else {
                log.error("不支持的 deviceConfig ç±»åž‹: {}", deviceConfigObj != null ? deviceConfigObj.getClass() : "null");
                return Result.error("设备配置数据格式错误");
            }
            deviceConfig.setId(request.getDeviceId());
            boolean success = deviceConfigService.updateDevice(deviceConfig);
            if (success) {
@@ -75,7 +91,7 @@
            }
        } catch (Exception e) {
            log.error("更新设备配置失败", e);
            return Result.error("更新设备配置失败");
            return Result.error("更新设备配置失败: " + e.getMessage());
        }
    }
@@ -83,7 +99,7 @@
     * åˆ é™¤è®¾å¤‡é…ç½®
     */
    @PostMapping("/devices/delete")
    @Operation(summary = "删除设备配置", description = "删除指定ID的设备配置")
    @ApiOperation("删除设备配置")
    public Result<Void> deleteDevice(
            @Valid @RequestBody DeviceConfigRequest request) {
        try {
@@ -99,7 +115,7 @@
     * æ ¹æ®ID获取设备配置
     */
    @PostMapping("/devices/detail")
    @Operation(summary = "获取设备配置详情", description = "根据ID获取设备配置的详细信息")
    @ApiOperation("获取设备配置详情")
    public Result<DeviceConfig> getDeviceById(
            @Valid @RequestBody DeviceConfigRequest request) {
        try {
@@ -115,7 +131,7 @@
     * åˆ†é¡µæŸ¥è¯¢è®¾å¤‡é…ç½®åˆ—表
     */
    @PostMapping("/devices/list")
    @Operation(summary = "分页查询设备配置", description = "分页查询设备配置列表")
    @ApiOperation("分页查询设备配置")
    public Result<Page<DeviceConfigVO.DeviceInfo>> getDeviceList(
            @Valid @RequestBody DeviceConfigRequest request) {
        try {
@@ -137,7 +153,7 @@
     * å¯ç”¨è®¾å¤‡
     */
    @PostMapping("/devices/enable")
    @Operation(summary = "启用设备", description = "启用指定ID的设备")
    @ApiOperation("启用设备")
    public Result<Void> enableDevice(
            @Valid @RequestBody DeviceConfigRequest request) {
        try {
@@ -153,7 +169,7 @@
     * ç¦ç”¨è®¾å¤‡
     */
    @PostMapping("/devices/disable")
    @Operation(summary = "禁用设备", description = "禁用指定ID的设备")
    @ApiOperation("禁用设备")
    public Result<Void> disableDevice(
            @Valid @RequestBody DeviceConfigRequest request) {
        try {
@@ -169,7 +185,7 @@
     * æ‰¹é‡å¯ç”¨è®¾å¤‡
     */
    @PostMapping("/devices/batch-enable")
    @Operation(summary = "批量启用设备", description = "批量启用指定ID列表的设备")
    @ApiOperation("批量启用设备")
    public Result<Void> batchEnableDevices(
            @Valid @RequestBody DeviceConfigRequest request) {
        try {
@@ -185,7 +201,7 @@
     * æ‰¹é‡ç¦ç”¨è®¾å¤‡
     */
    @PostMapping("/devices/batch-disable")
    @Operation(summary = "批量禁用设备", description = "批量禁用指定ID列表的设备")
    @ApiOperation("批量禁用设备")
    public Result<Void> batchDisableDevices(
            @Valid @RequestBody DeviceConfigRequest request) {
        try {
@@ -201,9 +217,9 @@
     * èŽ·å–è®¾å¤‡ç»Ÿè®¡ä¿¡æ¯
     */
    @PostMapping("/statistics/devices")
    @Operation(summary = "获取设备统计信息", description = "获取设备相关的统计信息")
    @ApiOperation("获取设备统计信息")
    public Result<StatisticsVO.DeviceStatistics> getDeviceStatistics(
            @Parameter(description = "设备配置请求") @RequestBody(required = false) DeviceConfigRequest request) {
            @ApiParam("设备配置请求") @RequestBody(required = false) DeviceConfigRequest request) {
        try {
            StatisticsVO.DeviceStatistics statistics = deviceConfigService.getDeviceStatistics(request != null ? request.getProjectId() : null);
            return Result.success(statistics);
@@ -217,9 +233,9 @@
     * æ£€æŸ¥è®¾å¤‡ç¼–码是否已存在
     */
    @PostMapping("/devices/check-code")
    @Operation(summary = "检查设备编码", description = "检查设备编码是否已存在")
    @ApiOperation("检查设备编码")
    public Result<Boolean> checkDeviceCodeExists(
            @Parameter(description = "设备配置请求") @RequestBody DeviceConfigRequest request) {
            @ApiParam("设备配置请求") @RequestBody DeviceConfigRequest request) {
        try {
            boolean exists = deviceConfigService.isDeviceCodeExists(request.getDeviceCode(), request.getDeviceId());
            return Result.success(exists);
@@ -233,7 +249,7 @@
     * èŽ·å–è®¾å¤‡ç±»åž‹åˆ—è¡¨
     */
    @PostMapping("/devices/types")
    @Operation(summary = "获取设备类型列表", description = "获取所有可用的设备类型")
    @ApiOperation("获取设备类型列表")
    public Result<List<String>> getDeviceTypes(@RequestBody(required = false) Map<String, Object> request) {
        try {
            List<String> deviceTypes = deviceConfigService.getAllDeviceTypes();
@@ -248,7 +264,7 @@
     * èŽ·å–è®¾å¤‡çŠ¶æ€åˆ—è¡¨
     */
    @PostMapping("/devices/statuses")
    @Operation(summary = "获取设备状态列表", description = "获取所有可用的设备状态")
    @ApiOperation("获取设备状态列表")
    public Result<List<String>> getDeviceStatuses(@RequestBody(required = false) Map<String, Object> request) {
        try {
            List<String> deviceStatuses = deviceConfigService.getAllDeviceStatuses();
@@ -263,9 +279,9 @@
     * èŽ·å–è®¾å¤‡é…ç½®æ ‘ç»“æž„
     */
    @PostMapping("/devices/tree")
    @Operation(summary = "获取设备配置树结构", description = "获取设备和设备组的树形结构数据")
    @ApiOperation("获取设备配置树结构")
    public Result<List<DeviceConfigVO.DeviceTreeNode>> getDeviceTree(
            @Parameter(description = "设备配置请求") @RequestBody(required = false) DeviceConfigRequest request) {
            @ApiParam("设备配置请求") @RequestBody(required = false) DeviceConfigRequest request) {
        try {
            List<DeviceConfigVO.DeviceTreeNode> treeData = deviceConfigService.getDeviceTree(request != null ? request.getProjectId() : null);
            return Result.success(treeData);
@@ -279,7 +295,7 @@
     * è®¾å¤‡å¥åº·æ£€æŸ¥
     */
    @PostMapping("/devices/health-check")
    @Operation(summary = "设备健康检查", description = "对指定设备进行健康检查")
    @ApiOperation("设备健康检查")
    public Result<DeviceConfigVO.HealthCheckResult> performHealthCheck(
            @Valid @RequestBody DeviceConfigRequest request) {
        try {
@@ -298,7 +314,7 @@
     * 2. ç›´æŽ¥ä¼ å…¥ plcIp / plcPort / timeout è¿›è¡Œä¸€æ¬¡æ€§æµ‹è¯•
     */
    @PostMapping("/devices/test-connection")
    @Operation(summary = "测试设备PLC连接", description = "根据设备配置测试PLC连接是否可达")
    @ApiOperation("测试设备PLC连接")
    public Result<String> testDeviceConnection(@RequestBody Map<String, Object> body) {
        try {
            String plcIp = null;
mes-processes/mes-plcSend/src/main/java/com/mes/device/controller/DeviceControlProfileController.java
@@ -3,15 +3,15 @@
import com.mes.device.service.DeviceControlProfileService;
import com.mes.device.vo.DeviceControlProfile;
import com.mes.vo.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("device/control")
@Tag(name = "设备控制参数", description = "设备控制参数配置接口")
@Api(tags = "设备控制参数")
@RequiredArgsConstructor
@Validated
public class DeviceControlProfileController {
@@ -19,13 +19,13 @@
    private final DeviceControlProfileService controlProfileService;
    @GetMapping("/{deviceId}")
    @Operation(summary = "获取设备控制参数")
    @ApiOperation("获取设备控制参数")
    public Result<DeviceControlProfile> getProfile(@PathVariable Long deviceId) {
        return Result.success(controlProfileService.getProfile(deviceId));
    }
    @PostMapping("/{deviceId}")
    @Operation(summary = "更新设备控制参数")
    @ApiOperation("更新设备控制参数")
    public Result<Void> saveProfile(@PathVariable Long deviceId,
                                    @RequestBody DeviceControlProfile profile) {
        controlProfileService.updateProfile(deviceId, profile);
mes-processes/mes-plcSend/src/main/java/com/mes/device/controller/DeviceGroupController.java
@@ -8,8 +8,8 @@
import com.mes.device.vo.DeviceGroupVO;
import com.mes.device.vo.StatisticsVO;
import com.mes.vo.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@@ -29,7 +29,7 @@
@Slf4j
@RestController
@RequestMapping("device/group")
@Tag(name = "设备组管理", description = "设备组管理相关接口")
@Api(tags = "设备组管理")
public class DeviceGroupController {
    @Resource
@@ -42,7 +42,7 @@
     * åˆ›å»ºè®¾å¤‡ç»„
     */
    @PostMapping("/create")
    @Operation(summary = "创建设备组", description = "创建设备组信息")
    @ApiOperation("创建设备组")
    public Result<DeviceGroupConfig> createGroup(
            @Valid @RequestBody DeviceGroupRequest request) {
        try {
@@ -65,7 +65,7 @@
     * æ›´æ–°è®¾å¤‡ç»„配置
     */
    @PostMapping("/update")
    @Operation(summary = "更新设备组配置", description = "更新指定ID的设备组配置")
    @ApiOperation("更新设备组配置")
    public Result<DeviceGroupConfig> updateGroup(
            @Valid @RequestBody DeviceGroupRequest request) {
        try {
@@ -89,7 +89,7 @@
     * åˆ é™¤è®¾å¤‡ç»„配置
     */
    @PostMapping("/delete")
    @Operation(summary = "删除设备组配置", description = "删除指定ID的设备组配置")
    @ApiOperation("删除设备组配置")
    public Result<Void> deleteGroup(
            @Valid @RequestBody DeviceGroupRequest request) {
        try {
@@ -105,7 +105,7 @@
     * æ ¹æ®ID获取设备组配置
     */
    @PostMapping("/detail")
    @Operation(summary = "获取设备组配置详情", description = "根据ID获取设备组配置的详细信息")
    @ApiOperation("获取设备组配置详情")
    public Result<DeviceGroupConfig> getGroupById(
            @Valid @RequestBody DeviceGroupRequest request) {
        try {
@@ -121,7 +121,7 @@
     * åˆ†é¡µæŸ¥è¯¢è®¾å¤‡ç»„列表
     */
    @PostMapping("/list")
    @Operation(summary = "分页查询设备组列表", description = "分页查询设备组列表")
    @ApiOperation("分页查询设备组列表")
    public Result<Page<DeviceGroupVO.GroupInfo>> getGroupList(
            @Valid @RequestBody DeviceGroupRequest request) {
        try {
@@ -146,7 +146,7 @@
     * å¯ç”¨è®¾å¤‡ç»„
     */
    @PostMapping("/enable")
    @Operation(summary = "启用设备组", description = "启用指定设备组")
    @ApiOperation("启用设备组")
    public Result<Void> enableGroup(
            @Valid @RequestBody DeviceGroupRequest request) {
        try {
@@ -162,7 +162,7 @@
     * ç¦ç”¨è®¾å¤‡ç»„
     */
    @PostMapping("/disable")
    @Operation(summary = "禁用设备组", description = "禁用指定设备组")
    @ApiOperation("禁用设备组")
    public Result<Void> disableGroup(
            @Valid @RequestBody DeviceGroupRequest request) {
        try {
@@ -178,7 +178,7 @@
     * æ‰¹é‡å¯ç”¨è®¾å¤‡ç»„
     */
    @PostMapping("/batch-enable")
    @Operation(summary = "批量启用设备组", description = "批量启用指定ID列表的设备组")
    @ApiOperation("批量启用设备组")
    public Result<Void> batchEnableGroups(
            @Valid @RequestBody DeviceGroupRequest request) {
        try {
@@ -194,7 +194,7 @@
     * æ‰¹é‡ç¦ç”¨è®¾å¤‡ç»„
     */
    @PostMapping("/batch-disable")
    @Operation(summary = "批量禁用设备组", description = "批量禁用指定ID列表的设备组")
    @ApiOperation("批量禁用设备组")
    public Result<Void> batchDisableGroups(
            @Valid @RequestBody DeviceGroupRequest request) {
        try {
@@ -210,7 +210,7 @@
     * èŽ·å–è®¾å¤‡ç»„ç»Ÿè®¡ä¿¡æ¯
     */
    @PostMapping("/statistics/groups")
    @Operation(summary = "获取设备组统计信息", description = "获取设备组相关的统计信息")
    @ApiOperation("获取设备组统计信息")
    public Result<StatisticsVO.GroupStatistics> getGroupStatistics(
            @RequestBody(required = false) Map<String, Object> request) {
        try {
@@ -227,7 +227,7 @@
     * æ£€æŸ¥è®¾å¤‡ç»„编码是否已存在
     */
    @PostMapping("/check-code")
    @Operation(summary = "检查设备组编码", description = "检查设备组编码是否已存在")
    @ApiOperation("检查设备组编码")
    public Result<Boolean> checkGroupCodeExists(
            @Valid @RequestBody DeviceGroupRequest request) {
        try {
@@ -247,7 +247,7 @@
     * èŽ·å–è®¾å¤‡ç»„ç±»åž‹åˆ—è¡¨
     */
    @PostMapping("/types")
    @Operation(summary = "获取设备组类型列表", description = "获取所有可用的设备组类型")
    @ApiOperation("获取设备组类型列表")
    public Result<List<String>> getGroupTypes() {
        try {
            List<String> groupTypes = deviceGroupConfigService.getAllGroupTypes();
@@ -262,7 +262,7 @@
     * èŽ·å–è®¾å¤‡ç»„çŠ¶æ€åˆ—è¡¨
     */
    @PostMapping("/statuses")
    @Operation(summary = "获取设备组状态列表", description = "获取所有可用的设备组状态")
    @ApiOperation("获取设备组状态列表")
    public Result<List<String>> getGroupStatuses() {
        try {
            List<String> groupStatuses = deviceGroupConfigService.getAllGroupStatuses();
@@ -277,7 +277,7 @@
     * æ·»åŠ è®¾å¤‡åˆ°è®¾å¤‡ç»„
     */
    @PostMapping("/devices")
    @Operation(summary = "添加设备到设备组", description = "将指定设备添加到设备组中")
    @ApiOperation("添加设备到设备组")
    public Result<Void> addDeviceToGroup(
            @Valid @RequestBody DeviceGroupRequest request) {
        try {
@@ -294,7 +294,7 @@
     * ä»Žè®¾å¤‡ç»„移除设备
     */
    @PostMapping("/devices/remove")
    @Operation(summary = "从设备组移除设备", description = "从设备组中移除指定设备")
    @ApiOperation("从设备组移除设备")
    public Result<Void> removeDeviceFromGroup(
            @Valid @RequestBody DeviceGroupRequest request) {
        try {
@@ -310,7 +310,7 @@
     * æ›´æ–°è®¾å¤‡è§’色
     */
    @PostMapping("/devices/role")
    @Operation(summary = "更新设备角色", description = "更新设备在设备组中的角色")
    @ApiOperation("更新设备角色")
    public Result<Void> updateDeviceRole(
            @Valid @RequestBody DeviceGroupRequest request) {
        try {
@@ -327,7 +327,7 @@
     * èŽ·å–è®¾å¤‡ç»„è®¾å¤‡åˆ—è¡¨
     */
    @PostMapping("/devices/list")
    @Operation(summary = "获取设备组设备列表", description = "获取指定设备组下的所有设备")
    @ApiOperation("获取设备组设备列表")
    public Result<List<DeviceGroupVO.DeviceInfo>> getGroupDevices(
            @Valid @RequestBody DeviceGroupRequest request) {
        try {
@@ -343,7 +343,7 @@
     * èŽ·å–è®¾å¤‡è®¾å¤‡ç»„åˆ—è¡¨
     */
    @PostMapping("/devices/groups")
    @Operation(summary = "获取设备设备组列表", description = "获取指定设备所属的所有设备组")
    @ApiOperation("获取设备设备组列表")
    public Result<List<DeviceGroupVO.GroupInfo>> getDeviceGroups(
            @Valid @RequestBody DeviceGroupRequest request) {
        try {
@@ -359,7 +359,7 @@
     * æ‰¹é‡æ·»åŠ è®¾å¤‡åˆ°è®¾å¤‡ç»„
     */
    @PostMapping("/batch-add-devices")
    @Operation(summary = "批量添加设备到设备组", description = "批量将指定设备列表添加到设备组中")
    @ApiOperation("批量添加设备到设备组")
    public Result<Void> batchAddDevicesToGroup(
            @Valid @RequestBody DeviceGroupRequest request) {
        try {
@@ -375,7 +375,7 @@
     * æ‰¹é‡ä»Žè®¾å¤‡ç»„移除设备
     */
    @PostMapping("/devices/batch-remove")
    @Operation(summary = "批量从设备组移除设备", description = "批量从设备组中移除指定设备列表")
    @ApiOperation("批量从设备组移除设备")
    public Result<Void> batchRemoveDevicesFromGroup(
            @Valid @RequestBody DeviceGroupRequest request) {
        try {
@@ -391,7 +391,7 @@
     * è®¾å¤‡ç»„健康检查
     */
    @PostMapping("/health-check")
    @Operation(summary = "设备组健康检查", description = "对指定设备组进行健康检查")
    @ApiOperation("设备组健康检查")
    public Result<DeviceGroupVO.HealthCheckResult> performGroupHealthCheck(
            @Valid @RequestBody DeviceGroupRequest request) {
        try {
@@ -407,7 +407,7 @@
     * èŽ·å–è®¾å¤‡ç»„æ€§èƒ½ç»Ÿè®¡
     */
    @PostMapping("/performance")
    @Operation(summary = "获取设备组性能统计", description = "获取指定设备组的性能统计信息")
    @ApiOperation("获取设备组性能统计")
    public Result<DeviceGroupVO.PerformanceStats> getGroupPerformance(
            @Valid @RequestBody DeviceGroupRequest request) {
        try {
mes-processes/mes-plcSend/src/main/java/com/mes/device/controller/DeviceInteractionController.java
@@ -4,20 +4,22 @@
import com.mes.device.service.DeviceInteractionService;
import com.mes.device.vo.DevicePlcVO;
import com.mes.vo.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("device/interaction")
@Tag(name = "设备交互", description = "设备交互逻辑执行接口")
@Api(tags = "设备交互")
@Validated
@RequiredArgsConstructor
public class DeviceInteractionController {
@@ -25,9 +27,37 @@
    private final DeviceInteractionService deviceInteractionService;
    @PostMapping("/glass-feed")
    @Operation(summary = "玻璃上料写入")
    @ApiOperation("玻璃上料写入")
    public Result<DevicePlcVO.OperationResult> feedGlass(@Valid @RequestBody DeviceGlassFeedRequest request) {
        return Result.success(deviceInteractionService.feedGlass(request));
    }
    @PostMapping("/execute")
    @ApiOperation("执行设备逻辑操作")
    public Result<DevicePlcVO.OperationResult> executeOperation(
            @Valid @RequestBody DeviceOperationRequest request) {
        return Result.success(deviceInteractionService.executeOperation(
                request.getDeviceId(),
                request.getOperation(),
                request.getParams()
        ));
    }
    /**
     * è®¾å¤‡æ“ä½œè¯·æ±‚
     */
    @Data
    public static class DeviceOperationRequest {
        @NotNull(message = "设备ID不能为空")
        @ApiParam(value = "设备ID", required = true)
        private Long deviceId;
        @NotNull(message = "操作类型不能为空")
        @ApiParam(value = "操作类型(如:feedGlass, triggerRequest, triggerReport等)", required = true)
        private String operation;
        @ApiParam(value = "操作参数")
        private Map<String, Object> params;
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/controller/DevicePlcController.java
@@ -4,8 +4,8 @@
import com.mes.device.service.DevicePlcOperationService;
import com.mes.device.vo.DevicePlcVO;
import com.mes.vo.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
@@ -25,54 +25,54 @@
@RestController
@RequiredArgsConstructor
@RequestMapping("device/plc")
@Tag(name = "设备PLC操作", description = "多设备PLC写入与状态查询接口")
@Api(tags = "设备PLC操作")
public class DevicePlcController {
    private final DevicePlcOperationService devicePlcOperationService;
    @PostMapping("/requests")
    @Operation(summary = "批量触发PLC请求", description = "对指定设备发送PLC请求字")
    @ApiOperation("批量触发PLC请求")
    public Result<List<DevicePlcVO.OperationResult>> triggerRequests(
            @Valid @RequestBody DevicePlcBatchRequest request) {
        return Result.success(devicePlcOperationService.triggerRequest(request.getDeviceIds()));
    }
    @PostMapping("/reports")
    @Operation(summary = "批量模拟PLC汇报", description = "对指定设备模拟PLC任务完成汇报")
    @ApiOperation("批量模拟PLC汇报")
    public Result<List<DevicePlcVO.OperationResult>> triggerReports(
            @Valid @RequestBody DevicePlcBatchRequest request) {
        return Result.success(devicePlcOperationService.triggerReport(request.getDeviceIds()));
    }
    @PostMapping("/resets")
    @Operation(summary = "批量重置PLC状态", description = "重置指定设备关联PLC的关键字段")
    @ApiOperation("批量重置PLC状态")
    public Result<List<DevicePlcVO.OperationResult>> resetPlc(
            @Valid @RequestBody DevicePlcBatchRequest request) {
        return Result.success(devicePlcOperationService.resetDevices(request.getDeviceIds()));
    }
    @PostMapping("/groups/{groupId}/request")
    @Operation(summary = "设备组触发PLC请求", description = "对设备组内所有设备发送PLC请求字")
    @ApiOperation("设备组触发PLC请求")
    public Result<List<DevicePlcVO.OperationResult>> triggerGroupRequest(
            @PathVariable Long groupId) {
        return Result.success(devicePlcOperationService.triggerRequestByGroup(groupId));
    }
    @PostMapping("/groups/{groupId}/report")
    @Operation(summary = "设备组模拟PLC汇报", description = "对设备组内所有设备模拟任务完成汇报")
    @ApiOperation("设备组模拟PLC汇报")
    public Result<List<DevicePlcVO.OperationResult>> triggerGroupReport(
            @PathVariable Long groupId) {
        return Result.success(devicePlcOperationService.triggerReportByGroup(groupId));
    }
    @GetMapping("/status/{deviceId}")
    @Operation(summary = "查询设备PLC状态", description = "读取单台设备的PLC数据")
    @ApiOperation("查询设备PLC状态")
    public Result<DevicePlcVO.StatusInfo> readStatus(@PathVariable Long deviceId) {
        return Result.success(devicePlcOperationService.readStatus(deviceId));
    }
    @GetMapping("/groups/{groupId}/status")
    @Operation(summary = "查询设备组PLC状态", description = "读取设备组内所有设备的PLC数据")
    @ApiOperation("查询设备组PLC状态")
    public Result<List<DevicePlcVO.StatusInfo>> readGroupStatus(@PathVariable Long groupId) {
        return Result.success(devicePlcOperationService.readStatusByGroup(groupId));
    }
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DEVICE_CONFIG_FIELDS.md
New file
@@ -0,0 +1,152 @@
# DeviceConfig è¡¨å­—段说明
## æ•°æ®åº“表:device_config
### å­—段结构说明
#### 1. `config_json` åˆ—
**用途**:存储**通用配置参数**(前端"配置参数"卡片中的键值对)
**数据结构**:
```json
[
  {
    "paramKey": "参数名1",
    "paramValue": "参数值1",
    "description": "描述1"
  },
  {
    "paramKey": "参数名2",
    "paramValue": "参数值2",
    "description": "描述2"
  }
]
```
**前端位置**:设备编辑弹窗 â†’ "配置参数"卡片
**特点**:
- é€šç”¨çš„键值对配置
- å¯ä»¥æ·»åŠ ä»»æ„å‚æ•°
- ç”¨äºŽå­˜å‚¨è®¾å¤‡ç‰¹å®šçš„自定义参数
---
#### 2. `extra_params` åˆ—
**用途**:存储**扩展参数**(结构化的JSON对象)
**数据结构**:
```json
{
  "connectionConfig": {
    "moduleCode": "模块编号",
    "protocolType": "通讯协议",
    "timeout": 30,
    "retryCount": 3,
    "heartbeatInterval": 30
  },
  "plcConfig": {
    "dbArea": "DB1",
    "beginIndex": 0,
    "autoModeInterval": 5000,
    "plcType": "S7-1200"
  },
  "plcProjectId": "项目ID",
  "deviceLogic": {
    // æ ¹æ®è®¾å¤‡ç±»åž‹ä¸åŒè€Œä¸åŒ
    // ä¸Šå¤§è½¦è®¾å¤‡ï¼š
    "vehicleCapacity": 6000,
    "glassIntervalMs": 1000,
    "autoFeed": true,
    "maxRetryCount": 5,
    "positionMapping": {
      "POS1": 1,
      "POS2": 2
    }
    // å¤§ç†ç‰‡è®¾å¤‡ï¼š
    // "glassSize": 2000,
    // "processingTime": 5000,
    // "autoProcess": true
    // çŽ»ç’ƒå­˜å‚¨è®¾å¤‡ï¼š
    // "storageCapacity": 100,
    // "retrievalMode": "FIFO",
    // "autoStore": true,
    // "autoRetrieve": true
  }
}
```
**前端位置**:设备编辑弹窗中的多个配置区域
---
### extra_params è¯¦ç»†è¯´æ˜Ž
#### 2.1 `connectionConfig` - è¿žæŽ¥é…ç½®
**前端位置**:"连接配置"卡片
| å­—段 | è¯´æ˜Ž | å‰ç«¯æ˜¾ç¤ºä½ç½® |
|------|------|------------|
| `moduleCode` | æ¨¡å—编号 | è¿žæŽ¥é…ç½® â†’ æ¨¡å—编号 |
| `protocolType` | é€šè®¯åè®® | è¿žæŽ¥é…ç½® â†’ é€šè®¯åè®® |
| `timeout` | è¶…æ—¶æ—¶é—´(秒) | è¿žæŽ¥é…ç½® â†’ è¶…æ—¶æ—¶é—´ |
| `retryCount` | é‡è¯•次数 | è¿žæŽ¥é…ç½® â†’ é‡è¯•次数 |
| `heartbeatInterval` | å¿ƒè·³é—´éš”(秒) | è¿žæŽ¥é…ç½® â†’ å¿ƒè·³é—´éš” |
#### 2.2 `plcConfig` - PLC地址配置
**前端位置**:"PLC åœ°å€é…ç½®"卡片
| å­—段 | è¯´æ˜Ž | å‰ç«¯æ˜¾ç¤ºä½ç½® |
|------|------|------------|
| `dbArea` | DB块 | PLC地址配置 â†’ DB块 |
| `beginIndex` | èµ·å§‹ç´¢å¼• | PLC地址配置 â†’ èµ·å§‹ç´¢å¼• |
| `autoModeInterval` | è‡ªåŠ¨é—´éš”(ms) | PLC地址配置 â†’ è‡ªåŠ¨é—´éš” |
| `plcType` | PLC类型 | åŸºæœ¬ä¿¡æ¯ â†’ PLC类型(但保存在这里) |
#### 2.3 `plcProjectId` - PLC项目ID
**说明**:用于标识PLC项目,在PLC操作时使用
#### 2.4 `deviceLogic` - è®¾å¤‡é€»è¾‘参数
**前端位置**:"设备逻辑配置"卡片(根据设备类型动态显示)
**上大车设备**:
- `vehicleCapacity`: è½¦è¾†å®¹é‡
- `glassIntervalMs`: çŽ»ç’ƒé—´éš”(ms)
- `autoFeed`: è‡ªåŠ¨ä¸Šæ–™
- `maxRetryCount`: æœ€å¤§é‡è¯•次数
- `positionMapping`: ä½ç½®æ˜ å°„对象
**大理片设备**:
- `glassSize`: çŽ»ç’ƒå°ºå¯¸
- `processingTime`: å¤„理时间(ms)
- `autoProcess`: è‡ªåŠ¨å¤„ç†
- `maxRetryCount`: æœ€å¤§é‡è¯•次数
**玻璃存储设备**:
- `storageCapacity`: å­˜å‚¨å®¹é‡
- `retrievalMode`: å–货模式 (FIFO/LIFO/RANDOM)
- `autoStore`: è‡ªåŠ¨å­˜å‚¨
- `autoRetrieve`: è‡ªåŠ¨å–è´§
- `maxRetryCount`: æœ€å¤§é‡è¯•次数
---
## å­—段对比总结
| å­—段 | å­˜å‚¨å†…容 | å‰ç«¯ä½ç½® | ç”¨é€” |
|------|---------|---------|------|
| `config_json` | é€šç”¨é”®å€¼å¯¹å‚æ•° | "配置参数"卡片 | è‡ªå®šä¹‰å‚数,灵活配置 |
| `extra_params.connectionConfig` | è¿žæŽ¥ç›¸å…³é…ç½® | "连接配置"卡片 | é€šè®¯è¿žæŽ¥å‚æ•° |
| `extra_params.plcConfig` | PLC地址配置 | "PLC地址配置"卡片 | PLC读写地址参数 |
| `extra_params.deviceLogic` | è®¾å¤‡é€»è¾‘参数 | "设备逻辑配置"卡片 | è®¾å¤‡ä¸šåŠ¡é€»è¾‘å‚æ•° |
| `extra_params.plcProjectId` | é¡¹ç›®ID | éšè—å­—段 | å†…部使用 |
---
## ä½¿ç”¨å»ºè®®
1. **config_json**:用于存储不常用的、自定义的配置参数
2. **extra_params.connectionConfig**:用于存储连接相关的标准配置
3. **extra_params.plcConfig**:用于存储PLC地址相关的配置
4. **extra_params.deviceLogic**:用于存储设备业务逻辑相关的配置(根据设备类型不同)
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DeviceConfig.java
@@ -2,7 +2,8 @@
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -15,93 +16,93 @@
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("device_config")
@Schema(name = "DeviceConfig", description = "设备配置信息")
@ApiModel(value = "设备配置信息")
public class DeviceConfig {
    @Schema(description = "设备ID", example = "1")
    @ApiModelProperty(value = "设备ID", example = "1")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @Schema(description = "设备唯一标识", example = "DEVICE_001")
    @ApiModelProperty(value = "设备唯一标识", example = "DEVICE_001")
    @TableField("device_id")
    private String deviceId;
    @Schema(description = "设备名称", example = "上大车设备1")
    @ApiModelProperty(value = "设备名称", example = "上大车设备1")
    @TableField("device_name")
    private String deviceName;
    @Schema(description = "设备编号", example = "DEV_001")
    @ApiModelProperty(value = "设备编号", example = "DEV_001")
    @TableField("device_code")
    private String deviceCode;
    @Schema(description = "设备类型", example = "上大车/大理片/玻璃存储")
    @ApiModelProperty(value = "设备类型", example = "上大车/大理片/玻璃存储")
    @TableField("device_type")
    private String deviceType;
    @Schema(description = "所属项目ID", example = "1")
    @ApiModelProperty(value = "所属项目ID", example = "1")
    @TableField("project_id")
    private Long projectId;
    @Schema(description = "PLC IP地址", example = "192.168.1.100")
    @ApiModelProperty(value = "PLC IP地址", example = "192.168.1.100")
    @TableField("plc_ip")
    private String plcIp;
    @Schema(description = "PLC端口", example = "102")
    @ApiModelProperty(value = "PLC端口", example = "102")
    @TableField("plc_port")
    private Integer plcPort;
    @Schema(description = "设备状态", example = "在线/离线/维护中/故障")
    @ApiModelProperty(value = "设备状态", example = "在线/离线/维护中/故障")
    @TableField("status")
    private String status;
    @Schema(description = "PLC类型", example = "S7-1200/S7-1500")
    @ApiModelProperty(value = "PLC类型", example = "S7-1200/S7-1500")
    @TableField("plc_type")
    private String plcType;
    @Schema(description = "模块名称", example = "上大车模块")
    @ApiModelProperty(value = "模块名称", example = "上大车模块")
    @TableField("module_name")
    private String moduleName;
    @Schema(description = "是否主控设备", example = "true")
    @ApiModelProperty(value = "是否主控设备", example = "true")
    @TableField("is_primary")
    private Boolean isPrimary;
    @Schema(description = "是否启用", example = "true")
    @ApiModelProperty(value = "是否启用", example = "true")
    @TableField("enabled")
    private Boolean enabled;
    @Schema(description = "设备特定配置JSON", example = "{\"vehicleCapacity\": 6000, \"glassIntervalMs\": 1000}")
    @ApiModelProperty(value = "设备特定配置JSON", example = "{\"vehicleCapacity\": 6000, \"glassIntervalMs\": 1000}")
    @TableField("config_json")
    private String configJson;
    @Schema(description = "设备描述", example = "上大车设备1")
    @ApiModelProperty(value = "设备描述", example = "上大车设备1")
    @TableField("description")
    private String description;
    @Schema(description = "扩展参数JSON", example = "{\"timeout\": 5000, \"retries\": 3}")
    @ApiModelProperty(value = "扩展参数JSON", example = "{\"timeout\": 5000, \"retries\": 3}")
    @TableField("extra_params")
    private String extraParams;
    @Schema(description = "是否删除:0-否,1-是", example = "0")
    @ApiModelProperty(value = "是否删除:0-否,1-是", example = "0")
    @TableField("is_deleted")
    @TableLogic
    private Integer isDeleted;
    @Schema(description = "创建时间")
    @ApiModelProperty(value = "创建时间")
    @TableField(value = "created_time", fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createdTime;
    @Schema(description = "更新时间")
    @ApiModelProperty(value = "更新时间")
    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updatedTime;
    @Schema(description = "创建人", example = "system")
    @ApiModelProperty(value = "创建人", example = "system")
    @TableField(value = "created_by", fill = FieldFill.INSERT)
    private String createdBy;
    @Schema(description = "更新人", example = "system")
    @ApiModelProperty(value = "更新人", example = "system")
    @TableField(value = "updated_by", fill = FieldFill.INSERT_UPDATE)
    private String updatedBy;
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DeviceGroupConfig.java
@@ -2,7 +2,8 @@
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -15,72 +16,72 @@
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("device_group_config")
@Schema(name = "DeviceGroupConfig", description = "设备组配置信息")
@ApiModel(value = "DeviceGroupConfig", description = "设备组配置信息")
public class DeviceGroupConfig {
    @Schema(description = "设备组ID", example = "1")
    @ApiModelProperty(value = "设备组ID", example = "1")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @Schema(description = "设备组名称", example = "生产线A")
    @ApiModelProperty(value = "设备组名称", example = "生产线A")
    @TableField("group_name")
    private String groupName;
    @Schema(description = "设备组编号", example = "GROUP_001")
    @ApiModelProperty(value = "设备组编号", example = "GROUP_001")
    @TableField("group_code")
    private String groupCode;
    @Schema(description = "设备组类型:1-生产线,2-测试线,3-辅助设备组", example = "1")
    @ApiModelProperty(value = "设备组类型:1-生产线,2-测试线,3-辅助设备组", example = "1")
    @TableField("group_type")
    private Integer groupType;
    @Schema(description = "所属项目ID", example = "1")
    @ApiModelProperty(value = "所属项目ID", example = "1")
    @TableField("project_id")
    private Long projectId;
    @Schema(description = "设备组状态:0-停用,1-启用,3-维护中", example = "1")
    @ApiModelProperty(value = "设备组状态:0-停用,1-启用,3-维护中", example = "1")
    @TableField("status")
    private Integer status;
    @Schema(description = "最大并发设备数", example = "3")
    @ApiModelProperty(value = "最大并发设备数", example = "3")
    @TableField("max_concurrent_devices")
    private Integer maxConcurrentDevices;
    @Schema(description = "心跳检测间隔(秒)", example = "30")
    @ApiModelProperty(value = "心跳检测间隔(秒)", example = "30")
    @TableField("heartbeat_interval")
    private Integer heartbeatInterval;
    @Schema(description = "通信超时时间(毫秒)", example = "5000")
    @ApiModelProperty(value = "通信超时时间(毫秒)", example = "5000")
    @TableField("communication_timeout")
    private Integer communicationTimeout;
    @Schema(description = "设备组描述", example = "生产线A设备组")
    @ApiModelProperty(value = "设备组描述", example = "生产线A设备组")
    @TableField("description")
    private String description;
    @Schema(description = "扩展配置JSON", example = "{\"retryTimes\": 3, \"batchSize\": 100}")
    @ApiModelProperty(value = "扩展配置JSON", example = "{\"retryTimes\": 3, \"batchSize\": 100}")
    @TableField("extra_config")
    private String extraConfig;
    @Schema(description = "创建时间")
    @ApiModelProperty(value = "创建时间")
    @TableField(value = "created_time", fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createdTime;
    @Schema(description = "更新时间")
    @ApiModelProperty(value = "更新时间")
    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updatedTime;
    @Schema(description = "创建人", example = "system")
    @ApiModelProperty(value = "创建人", example = "system")
    @TableField(value = "created_by", fill = FieldFill.INSERT)
    private String createdBy;
    @Schema(description = "更新人", example = "system")
    @ApiModelProperty(value = "更新人", example = "system")
    @TableField(value = "updated_by", fill = FieldFill.INSERT_UPDATE)
    private String updatedBy;
    @Schema(description = "是否删除:0-否,1-是", example = "0")
    @ApiModelProperty(value = "是否删除:0-否,1-是", example = "0")
    @TableField("is_deleted")
    @TableLogic
    private Integer isDeleted;
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DeviceGroupRelation.java
@@ -2,7 +2,8 @@
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -15,64 +16,64 @@
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("device_group_relation")
@Schema(name = "DeviceGroupRelation", description = "设备组与设备关联关系")
@ApiModel(value = "DeviceGroupRelation", description = "设备组与设备关联关系")
public class DeviceGroupRelation {
    @Schema(description = "关联ID", example = "1")
    @ApiModelProperty(value = "关联ID", example = "1")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @Schema(description = "设备组ID", example = "1")
    @ApiModelProperty(value = "设备组ID", example = "1")
    @TableField("group_id")
    private Long groupId;
    @Schema(description = "设备ID", example = "1")
    @ApiModelProperty(value = "设备ID", example = "1")
    @TableField("device_id")
    private Long deviceId;
    @Schema(description = "设备在组内的优先级:1-最高,10-最低", example = "1")
    @ApiModelProperty(value = "设备在组内的优先级:1-最高,10-最低", example = "1")
    @TableField("priority")
    private Integer priority;
    @Schema(description = "设备在组内的角色:1-主控,2-协作,3-监控", example = "1")
    @ApiModelProperty(value = "设备在组内的角色:1-主控,2-协作,3-监控", example = "1")
    @TableField("role")
    private Integer role;
    @Schema(description = "设备在该组中的状态:0-未配置,1-正常,2-故障,3-维护", example = "1")
    @ApiModelProperty(value = "设备在该组中的状态:0-未配置,1-正常,2-故障,3-维护", example = "1")
    @TableField("status")
    private Integer status;
    @Schema(description = "连接顺序:数值越小越先连接", example = "1")
    @ApiModelProperty(value = "连接顺序:数值越小越先连接", example = "1")
    @TableField("connection_order")
    private Integer connectionOrder;
    @Schema(description = "关联描述", example = "主控设备,负责整体协调")
    @ApiModelProperty(value = "关联描述", example = "主控设备,负责整体协调")
    @TableField("relation_desc")
    private String relationDesc;
    @Schema(description = "扩展参数JSON", example = "{\"timeout\": 5000, \"retryPolicy\": \"exponential\"}")
    @ApiModelProperty(value = "扩展参数JSON", example = "{\"timeout\": 5000, \"retryPolicy\": \"exponential\"}")
    @TableField("extra_params")
    private String extraParams;
    @Schema(description = "创建时间")
    @ApiModelProperty(value = "创建时间")
    @TableField(value = "created_time", fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createdTime;
    @Schema(description = "更新时间")
    @ApiModelProperty(value = "更新时间")
    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updatedTime;
    @Schema(description = "创建人", example = "system")
    @ApiModelProperty(value = "创建人", example = "system")
    @TableField(value = "created_by", fill = FieldFill.INSERT)
    private String createdBy;
    @Schema(description = "更新人", example = "system")
    @ApiModelProperty(value = "更新人", example = "system")
    @TableField(value = "updated_by", fill = FieldFill.INSERT_UPDATE)
    private String updatedBy;
    @Schema(description = "是否删除:0-否,1-是", example = "0")
    @ApiModelProperty(value = "是否删除:0-否,1-是", example = "0")
    @TableField("is_deleted")
    @TableLogic
    private Integer isDeleted;
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DeviceInteractionExecution.java
@@ -2,7 +2,8 @@
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -15,110 +16,110 @@
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("device_interaction_execution")
@Schema(name = "DeviceInteractionExecution", description = "设备交互执行记录")
@ApiModel(value = "DeviceInteractionExecution", description = "设备交互执行记录")
public class DeviceInteractionExecution {
    @Schema(description = "执行记录ID", example = "1")
    @ApiModelProperty(value = "执行记录ID", example = "1")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @Schema(description = "关联的交互逻辑ID", example = "1")
    @ApiModelProperty(value = "关联的交互逻辑ID", example = "1")
    @TableField("logic_id")
    private Long logicId;
    @Schema(description = "设备组ID", example = "1")
    @ApiModelProperty(value = "设备组ID", example = "1")
    @TableField("group_id")
    private Long groupId;
    @Schema(description = "所属项目ID", example = "1")
    @ApiModelProperty(value = "所属项目ID", example = "1")
    @TableField("project_id")
    private Long projectId;
    @Schema(description = "执行批次号", example = "EXEC_20241030_001")
    @ApiModelProperty(value = "执行批次号", example = "EXEC_20241030_001")
    @TableField("batch_no")
    private String batchNo;
    @Schema(description = "执行状态:0-等待,1-执行中,2-成功,3-失败,4-超时,5-取消", example = "0")
    @ApiModelProperty(value = "执行状态:0-等待,1-执行中,2-成功,3-失败,4-超时,5-取消", example = "0")
    @TableField("status")
    private Integer status;
    @Schema(description = "执行模式:1-手动,2-自动,3-定时", example = "2")
    @ApiModelProperty(value = "执行模式:1-手动,2-自动,3-定时", example = "2")
    @TableField("execution_mode")
    private Integer executionMode;
    @Schema(description = "开始执行时间")
    @ApiModelProperty(value = "开始执行时间")
    @TableField("start_time")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date startTime;
    @Schema(description = "结束执行时间")
    @ApiModelProperty(value = "结束执行时间")
    @TableField("end_time")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date endTime;
    @Schema(description = "执行耗时(毫秒)", example = "25000")
    @ApiModelProperty(value = "执行耗时(毫秒)", example = "25000")
    @TableField("execution_duration")
    private Long executionDuration;
    @Schema(description = "执行进度:0-100", example = "80")
    @ApiModelProperty(value = "执行进度:0-100", example = "80")
    @TableField("progress")
    private Integer progress;
    @Schema(description = "当前执行的步骤序号", example = "3")
    @ApiModelProperty(value = "当前执行的步骤序号", example = "3")
    @TableField("current_step")
    private Integer currentStep;
    @Schema(description = "总步骤数", example = "10")
    @ApiModelProperty(value = "总步骤数", example = "10")
    @TableField("total_steps")
    private Integer totalSteps;
    @Schema(description = "成功执行的设备数量", example = "3")
    @ApiModelProperty(value = "成功执行的设备数量", example = "3")
    @TableField("success_devices")
    private Integer successDevices;
    @Schema(description = "失败的设备数量", example = "0")
    @ApiModelProperty(value = "失败的设备数量", example = "0")
    @TableField("failed_devices")
    private Integer failedDevices;
    @Schema(description = "触发执行的操作人", example = "admin")
    @ApiModelProperty(value = "触发执行的操作人", example = "admin")
    @TableField("triggered_by")
    private String triggeredBy;
    @Schema(description = "执行结果描述", example = "所有设备成功完成自动化测试")
    @ApiModelProperty(value = "执行结果描述", example = "所有设备成功完成自动化测试")
    @TableField("result_message")
    private String resultMessage;
    @Schema(description = "错误信息JSON", example = "{\"deviceId\": 2, \"error\": \"Connection timeout\"}")
    @ApiModelProperty(value = "错误信息JSON", example = "{\"deviceId\": 2, \"error\": \"Connection timeout\"}")
    @TableField("error_details")
    private String errorDetails;
    @Schema(description = "执行数据统计JSON", example = "{\"totalTime\": 25000, \"avgResponseTime\": 120}")
    @ApiModelProperty(value = "执行数据统计JSON", example = "{\"totalTime\": 25000, \"avgResponseTime\": 120}")
    @TableField("execution_stats")
    private String executionStats;
    @Schema(description = "扩展参数JSON", example = "{\"testDataId\": \"TD_001\", \"environment\": \"prod\"}")
    @ApiModelProperty(value = "扩展参数JSON", example = "{\"testDataId\": \"TD_001\", \"environment\": \"prod\"}")
    @TableField("extra_params")
    private String extraParams;
    @Schema(description = "创建时间")
    @ApiModelProperty(value = "创建时间")
    @TableField(value = "created_time", fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createdTime;
    @Schema(description = "更新时间")
    @ApiModelProperty(value = "更新时间")
    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updatedTime;
    @Schema(description = "创建人", example = "system")
    @ApiModelProperty(value = "创建人", example = "system")
    @TableField(value = "created_by", fill = FieldFill.INSERT)
    private String createdBy;
    @Schema(description = "更新人", example = "system")
    @ApiModelProperty(value = "更新人", example = "system")
    @TableField(value = "updated_by", fill = FieldFill.INSERT_UPDATE)
    private String updatedBy;
    @Schema(description = "是否删除:0-否,1-是", example = "0")
    @ApiModelProperty(value = "是否删除:0-否,1-是", example = "0")
    @TableField("is_deleted")
    @TableLogic
    private Integer isDeleted;
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/DeviceInteractionLogic.java
@@ -2,7 +2,8 @@
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -15,88 +16,88 @@
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("device_interaction_logic")
@Schema(name = "DeviceInteractionLogic", description = "设备交互逻辑配置")
@ApiModel(value = "DeviceInteractionLogic", description = "设备交互逻辑配置")
public class DeviceInteractionLogic {
    @Schema(description = "逻辑ID", example = "1")
    @ApiModelProperty(value = "逻辑ID", example = "1")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @Schema(description = "逻辑名称", example = "大车自动化测试逻辑")
    @ApiModelProperty(value = "逻辑名称", example = "大车自动化测试逻辑")
    @TableField("logic_name")
    private String logicName;
    @Schema(description = "逻辑编号", example = "TRUCK_AUTO_TEST_001")
    @ApiModelProperty(value = "逻辑编号", example = "TRUCK_AUTO_TEST_001")
    @TableField("logic_code")
    private String logicCode;
    @Schema(description = "所属模块:1-上大车,2-下大车,3-转运,4-测试", example = "1")
    @ApiModelProperty(value = "所属模块:1-上大车,2-下大车,3-转运,4-测试", example = "1")
    @TableField("module_type")
    private Integer moduleType;
    @Schema(description = "所属设备组ID", example = "1")
    @ApiModelProperty(value = "所属设备组ID", example = "1")
    @TableField("group_id")
    private Long groupId;
    @Schema(description = "逻辑类型:1-顺序执行,2-并行执行,3-条件执行,4-循环执行", example = "1")
    @ApiModelProperty(value = "逻辑类型:1-顺序执行,2-并行执行,3-条件执行,4-循环执行", example = "1")
    @TableField("logic_type")
    private Integer logicType;
    @Schema(description = "逻辑状态:0-禁用,1-启用,3-调试中", example = "1")
    @ApiModelProperty(value = "逻辑状态:0-禁用,1-启用,3-调试中", example = "1")
    @TableField("status")
    private Integer status;
    @Schema(description = "设备组内该逻辑的优先级:1-最高,10-最低", example = "1")
    @ApiModelProperty(value = "设备组内该逻辑的优先级:1-最高,10-最低", example = "1")
    @TableField("priority")
    private Integer priority;
    @Schema(description = "执行超时时间(毫秒)", example = "30000")
    @ApiModelProperty(value = "执行超时时间(毫秒)", example = "30000")
    @TableField("execution_timeout")
    private Integer executionTimeout;
    @Schema(description = "重试次数", example = "3")
    @ApiModelProperty(value = "重试次数", example = "3")
    @TableField("retry_times")
    private Integer retryTimes;
    @Schema(description = "逻辑描述", example = "大车自动化测试的完整流程控制")
    @ApiModelProperty(value = "逻辑描述", example = "大车自动化测试的完整流程控制")
    @TableField("description")
    private String description;
    @Schema(description = "交互步骤JSON数组", example = "[{\"step\": 1, \"deviceId\": 1, \"action\": \"START\", \"params\": {}}]")
    @ApiModelProperty(value = "交互步骤JSON数组", example = "[{\"step\": 1, \"deviceId\": 1, \"action\": \"START\", \"params\": {}}]")
    @TableField("interaction_steps")
    private String interactionSteps;
    @Schema(description = "条件判断逻辑JSON", example = "{\"conditions\": [{\"field\": \"status\", \"operator\": \"eq\", \"value\": 1}]}")
    @ApiModelProperty(value = "条件判断逻辑JSON", example = "{\"conditions\": [{\"field\": \"status\", \"operator\": \"eq\", \"value\": 1}]}")
    @TableField("condition_logic")
    private String conditionLogic;
    @Schema(description = "扩展参数JSON", example = "{\"parallelLimit\": 5, \"errorHandling\": \"retry\"}")
    @ApiModelProperty(value = "扩展参数JSON", example = "{\"parallelLimit\": 5, \"errorHandling\": \"retry\"}")
    @TableField("extra_params")
    private String extraParams;
    @Schema(description = "版本号", example = "1.0.0")
    @ApiModelProperty(value = "版本号", example = "1.0.0")
    @TableField("version")
    private String version;
    @Schema(description = "创建时间")
    @ApiModelProperty(value = "创建时间")
    @TableField(value = "created_time", fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createdTime;
    @Schema(description = "更新时间")
    @ApiModelProperty(value = "更新时间")
    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updatedTime;
    @Schema(description = "创建人", example = "system")
    @ApiModelProperty(value = "创建人", example = "system")
    @TableField(value = "created_by", fill = FieldFill.INSERT)
    private String createdBy;
    @Schema(description = "更新人", example = "system")
    @ApiModelProperty(value = "更新人", example = "system")
    @TableField(value = "updated_by", fill = FieldFill.INSERT_UPDATE)
    private String updatedBy;
    @Schema(description = "是否删除:0-否,1-是", example = "0")
    @ApiModelProperty(value = "是否删除:0-否,1-是", example = "0")
    @TableField("is_deleted")
    @TableLogic
    private Integer isDeleted;
mes-processes/mes-plcSend/src/main/java/com/mes/device/mapper/DeviceGroupRelationMapper.java
@@ -1,6 +1,7 @@
package com.mes.device.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mes.device.entity.DeviceConfig;
import com.mes.device.entity.DeviceGroupRelation;
import com.mes.device.vo.DeviceGroupVO;
import org.apache.ibatis.annotations.Mapper;
@@ -77,4 +78,19 @@
            "INNER JOIN device_group_relation dgr ON dgc.id = dgr.group_id " +
            "WHERE dgr.device_id = #{deviceId} AND dgr.is_deleted = 0 AND dgc.is_deleted = 0")
    List<DeviceGroupVO.GroupInfo> getDeviceGroups(@Param("deviceId") Long deviceId);
    /**
     * èŽ·å–æŒ‰è¿žæŽ¥é¡ºåºæŽ’åºçš„è®¾å¤‡é…ç½®åˆ—è¡¨
     *
     * @param groupId è®¾å¤‡ç»„ID
     * @return è®¾å¤‡é…ç½®é›†åˆ
     */
    @Select("SELECT d.* " +
            "FROM device_group_relation dgr " +
            "INNER JOIN device_config d ON dgr.device_id = d.id " +
            "WHERE dgr.group_id = #{groupId} " +
            "  AND dgr.is_deleted = 0 " +
            "  AND d.is_deleted = 0 " +
            "ORDER BY IFNULL(dgr.connection_order, 0) ASC, dgr.id ASC")
    List<DeviceConfig> getOrderedDeviceConfigs(@Param("groupId") Long groupId);
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/request/DeviceConfigRequest.java
@@ -1,6 +1,7 @@
package com.mes.device.request;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@@ -12,37 +13,37 @@
 * @since 2025-07-12
 */
@Data
@Schema(description = "设备配置操作请求体")
@ApiModel(description = "设备配置操作请求体")
public class DeviceConfigRequest {
    @Schema(description = "设备ID", example = "1")
    @ApiModelProperty(value = "设备ID", example = "1")
    private Long deviceId;
    @Schema(description = "设备配置信息")
    @ApiModelProperty(value = "设备配置信息")
    private Object deviceConfig;
    @Schema(description = "设备ID列表")
    @ApiModelProperty(value = "设备ID列表")
    private List<Long> deviceIds;
    @Schema(description = "项目ID", example = "1")
    @ApiModelProperty(value = "项目ID", example = "1")
    private Long projectId;
    @Schema(description = "设备类型", example = "1")
    @ApiModelProperty(value = "设备类型", example = "1")
    private String deviceType;
    @Schema(description = "设备状态", example = "1")
    @ApiModelProperty(value = "设备状态", example = "1")
    private String deviceStatus;
    @Schema(description = "搜索关键词", example = "设备1")
    @ApiModelProperty(value = "搜索关键词", example = "设备1")
    private String keyword;
    @Schema(description = "设备编码", example = "DEVICE001")
    @ApiModelProperty(value = "设备编码", example = "DEVICE001")
    private String deviceCode;
    @Schema(description = "页码", example = "1")
    @ApiModelProperty(value = "页码", example = "1")
    private Integer page;
    @Schema(description = "每页大小", example = "10")
    @ApiModelProperty(value = "每页大小", example = "10")
    private Integer size;
    // æž„造函数
mes-processes/mes-plcSend/src/main/java/com/mes/device/request/DeviceGlassFeedRequest.java
@@ -1,7 +1,7 @@
package com.mes.device.request;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
@@ -12,23 +12,23 @@
 * çŽ»ç’ƒå†™å…¥è¯·æ±‚
 */
@Data
@Schema(name = "DeviceGlassFeedRequest", description = "设备玻璃写入请求")
@ApiModel(value = "DeviceGlassFeedRequest", description = "设备玻璃写入请求")
public class DeviceGlassFeedRequest {
    @NotNull
    @Schema(description = "设备ID", required = true)
    @ApiModelProperty(value = "设备ID", required = true)
    private Long deviceId;
    @ArraySchema(schema = @Schema(description = "玻璃ID列表", example = "GLS001"), minItems = 1)
    @ApiModelProperty(value = "玻璃ID列表", example = "GLS001")
    private List<String> glassIds;
    @Schema(description = "进片位置标识(与控制参数中的 positionMappings å¯¹åº”)")
    @ApiModelProperty(value = "进片位置标识(与控制参数中的 positionMappings å¯¹åº”)")
    private String positionCode;
    @Schema(description = "直接指定的位置值(优先级高于 positionCode)")
    @ApiModelProperty(value = "直接指定的位置值(优先级高于 positionCode)")
    private Integer positionValue;
    @Schema(description = "是否自动写入请求字", defaultValue = "true")
    @ApiModelProperty(value = "是否自动写入请求字", example = "true")
    private Boolean triggerRequest = true;
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/request/DeviceGroupRequest.java
@@ -1,6 +1,7 @@
package com.mes.device.request;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@@ -12,25 +13,25 @@
 * @since 2025-07-12
 */
@Data
@Schema(description = "设备组操作请求体")
@ApiModel(description = "设备组操作请求体")
public class DeviceGroupRequest {
    @Schema(description = "设备组ID", example = "1")
    @ApiModelProperty(value = "设备组ID", example = "1")
    private Long groupId;
    @Schema(description = "设备ID", example = "1")
    @ApiModelProperty(value = "设备ID", example = "1")
    private Long deviceId;
    @Schema(description = "设备ID列表")
    @ApiModelProperty(value = "设备ID列表")
    private List<Long> deviceIds;
    @Schema(description = "设备组ID列表")
    @ApiModelProperty(value = "设备组ID列表")
    private List<Long> groupIds;
    
    @Schema(description = "设备组配置信息")
    @ApiModelProperty(value = "设备组配置信息")
    private Object groupConfig;
    @Schema(description = "设备角色", example = "MEMBER")
    @ApiModelProperty(value = "设备角色", example = "MEMBER")
    private String deviceRole;
    // æž„造函数
mes-processes/mes-plcSend/src/main/java/com/mes/device/request/DevicePlcBatchRequest.java
@@ -1,6 +1,7 @@
package com.mes.device.request;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
@@ -14,11 +15,11 @@
 * @since 2025-11-17
 */
@Data
@Schema(name = "DevicePlcBatchRequest", description = "设备 PLC æ‰¹é‡æ“ä½œè¯·æ±‚")
@ApiModel(value = "DevicePlcBatchRequest", description = "设备 PLC æ‰¹é‡æ“ä½œè¯·æ±‚")
public class DevicePlcBatchRequest implements Serializable {
    @NotEmpty(message = "设备ID列表不能为空")
    @Schema(description = "设备ID列表", required = true)
    @ApiModelProperty(value = "设备ID列表", required = true)
    private List<Long> deviceIds;
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/DeviceInteractionService.java
@@ -3,14 +3,26 @@
import com.mes.device.request.DeviceGlassFeedRequest;
import com.mes.device.vo.DevicePlcVO;
import java.util.Map;
/**
 * è®¾å¤‡äº¤äº’逻辑服务
 */
public interface DeviceInteractionService {
    /**
     * æ‰§è¡ŒçŽ»ç’ƒä¸Šæ–™å†™å…¥
     * æ‰§è¡ŒçŽ»ç’ƒä¸Šæ–™å†™å…¥ï¼ˆå…¼å®¹æ—§æŽ¥å£ï¼‰
     */
    DevicePlcVO.OperationResult feedGlass(DeviceGlassFeedRequest request);
    /**
     * æ‰§è¡Œè®¾å¤‡é€»è¾‘操作(新接口,使用处理器架构)
     *
     * @param deviceId è®¾å¤‡ID
     * @param operation æ“ä½œç±»åž‹ï¼ˆå¦‚:feedGlass, triggerRequest, triggerReport等)
     * @param params æ“ä½œå‚æ•°
     * @return æ“ä½œç»“æžœ
     */
    DevicePlcVO.OperationResult executeOperation(Long deviceId, String operation, Map<String, Object> params);
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DeviceInteractionServiceImpl.java
@@ -1,6 +1,10 @@
package com.mes.device.service.impl;
import com.mes.device.entity.DeviceConfig;
import com.mes.interaction.DeviceLogicHandler;
import com.mes.interaction.DeviceLogicHandlerFactory;
import com.mes.device.request.DeviceGlassFeedRequest;
import com.mes.device.service.DeviceConfigService;
import com.mes.device.service.DeviceControlProfileService;
import com.mes.device.service.DeviceInteractionService;
import com.mes.device.service.DevicePlcOperationService;
@@ -25,9 +29,30 @@
    private final DeviceControlProfileService controlProfileService;
    private final DevicePlcOperationService devicePlcOperationService;
    private final DeviceConfigService deviceConfigService;
    private final DeviceLogicHandlerFactory handlerFactory;
    /**
     * æ‰§è¡ŒçŽ»ç’ƒä¸Šæ–™å†™å…¥ï¼ˆå…¼å®¹æ—§æŽ¥å£ï¼Œä¿ç•™åŽŸæœ‰é€»è¾‘ï¼‰
     */
    @Override
    public DevicePlcVO.OperationResult feedGlass(DeviceGlassFeedRequest request) {
        // ä¼˜å…ˆä½¿ç”¨æ–°çš„处理器架构
        DeviceConfig deviceConfig = deviceConfigService.getDeviceById(request.getDeviceId());
        if (deviceConfig != null) {
            DeviceLogicHandler handler = handlerFactory.getHandler(deviceConfig.getDeviceType());
            if (handler != null) {
                // ä½¿ç”¨æ–°æž¶æž„执行
                Map<String, Object> params = new HashMap<>();
                params.put("glassIds", request.getGlassIds());
                params.put("positionCode", request.getPositionCode());
                params.put("positionValue", request.getPositionValue());
                params.put("triggerRequest", request.getTriggerRequest());
                return handler.execute(deviceConfig, "feedGlass", params);
            }
        }
        // é™çº§åˆ°åŽŸæœ‰é€»è¾‘ï¼ˆå…¼å®¹æ—§ä»£ç ï¼‰
        DeviceControlProfile profile = controlProfileService.getProfile(request.getDeviceId());
        Map<String, Object> payload = buildGlassPayload(profile, request);
        String opName = "玻璃上料";
@@ -37,6 +62,36 @@
        return devicePlcOperationService.writeFields(request.getDeviceId(), payload, opName);
    }
    /**
     * æ‰§è¡Œè®¾å¤‡é€»è¾‘操作(新接口,使用处理器架构)
     */
    @Override
    public DevicePlcVO.OperationResult executeOperation(Long deviceId, String operation, Map<String, Object> params) {
        // èŽ·å–è®¾å¤‡é…ç½®
        DeviceConfig deviceConfig = deviceConfigService.getDeviceById(deviceId);
        if (deviceConfig == null) {
            return DevicePlcVO.OperationResult.builder()
                    .success(false)
                    .message("设备不存在: " + deviceId)
                    .build();
        }
        // èŽ·å–å¯¹åº”çš„å¤„ç†å™¨
        DeviceLogicHandler handler = handlerFactory.getHandler(deviceConfig.getDeviceType());
        if (handler == null) {
            return DevicePlcVO.OperationResult.builder()
                    .success(false)
                    .message("不支持的设备类型: " + deviceConfig.getDeviceType())
                    .build();
        }
        // æ‰§è¡Œæ“ä½œ
        return handler.execute(deviceConfig, operation, params != null ? params : new HashMap<>());
    }
    /**
     * æž„建玻璃上料数据(兼容旧逻辑)
     */
    private Map<String, Object> buildGlassPayload(DeviceControlProfile profile, DeviceGlassFeedRequest request) {
        if (CollectionUtils.isEmpty(profile.getGlassSlots())) {
            throw new IllegalStateException("设备未配置玻璃槽位信息");
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/DevicePlcOperationServiceImpl.java
@@ -6,6 +6,7 @@
import com.mes.device.service.DeviceConfigService;
import com.mes.device.service.DeviceGroupRelationService;
import com.mes.device.service.DevicePlcOperationService;
import com.mes.device.util.ConfigJsonHelper;
import com.mes.device.vo.DeviceGroupVO;
import com.mes.device.vo.DevicePlcVO;
import com.mes.service.PlcTestWriteService;
@@ -247,13 +248,21 @@
            throw new IllegalArgumentException("设备信息为空");
        }
        // ä¼˜å…ˆä»ŽconfigJson中获取
        Map<String, Object> configParams = ConfigJsonHelper.parseToMap(device.getConfigJson(), objectMapper);
        Object plcProjectId = configParams.get(PLC_PROJECT_ID_KEY);
        if (plcProjectId != null) {
            return String.valueOf(plcProjectId);
        }
        // å…¶æ¬¡ä»Žæ‰©å±•参数中获取(兼容旧配置)
        String extra = device.getExtraParams();
        if (extra != null && !extra.isEmpty()) {
            try {
                Map<String, Object> extraParams = objectMapper.readValue(extra, new TypeReference<Map<String, Object>>() {});
                Object plcProjectId = extraParams.get(PLC_PROJECT_ID_KEY);
                if (plcProjectId != null) {
                    return String.valueOf(plcProjectId);
                Object plcProjectIdFromExtra = extraParams.get(PLC_PROJECT_ID_KEY);
                if (plcProjectIdFromExtra != null) {
                    return String.valueOf(plcProjectIdFromExtra);
                }
            } catch (Exception e) {
                log.warn("解析设备扩展参数失败, deviceId={}", device.getId(), e);
@@ -271,7 +280,7 @@
        throw new IllegalStateException("无法解析设备的 PLC é¡¹ç›®æ ‡è¯†, deviceId=" + device.getId());
    }
    private enum PlcOperationType {
    public enum PlcOperationType {
        REQUEST("PLC请求", "PLC è¯·æ±‚发送成功", "PLC è¯·æ±‚发送失败"),
        REPORT("PLC汇报", "PLC æ±‡æŠ¥æ¨¡æ‹ŸæˆåŠŸ", "PLC æ±‡æŠ¥æ¨¡æ‹Ÿå¤±è´¥"),
        RESET("PLC重置", "PLC çŠ¶æ€å·²é‡ç½®", "PLC çŠ¶æ€é‡ç½®å¤±è´¥");
mes-processes/mes-plcSend/src/main/java/com/mes/device/util/ConfigJsonHelper.java
New file
@@ -0,0 +1,71 @@
package com.mes.device.util;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
 * å·¥å…·ç±»ï¼šè§£æž DeviceConfig.configJson çš„多种格式
 * å…¼å®¹å¯¹è±¡ç»“构和 [{paramKey,paramValue}] æ•°ç»„结构
 */
@Slf4j
public final class ConfigJsonHelper {
    private static final TypeReference<Map<String, Object>> MAP_TYPE =
            new TypeReference<Map<String, Object>>() {};
    private static final TypeReference<List<Map<String, Object>>> LIST_TYPE =
            new TypeReference<List<Map<String, Object>>>() {};
    private ConfigJsonHelper() {
    }
    /**
     * å°† configJson è§£æžä¸º key-value çš„ Map
     *
     * @param configJson   åŽŸå§‹ JSON å­—符串
     * @param objectMapper å…¬å…± ObjectMapper
     * @return å‚数映射,不可修改
     */
    public static Map<String, Object> parseToMap(String configJson, ObjectMapper objectMapper) {
        if (configJson == null || configJson.trim().isEmpty()) {
            return Collections.emptyMap();
        }
        String trimmed = configJson.trim();
        try {
            if (trimmed.startsWith("[")) {
                List<Map<String, Object>> items = objectMapper.readValue(trimmed, LIST_TYPE);
                Map<String, Object> result = new LinkedHashMap<>();
                for (Map<String, Object> item : items) {
                    if (item == null) {
                        continue;
                    }
                    Object keyObj = firstNonNull(item.get("paramKey"), item.get("key"));
                    if (keyObj == null) {
                        continue;
                    }
                    Object valueObj = firstNonNull(item.get("paramValue"), item.get("value"));
                    result.put(String.valueOf(keyObj), valueObj);
                }
                return result;
            } else if (trimmed.startsWith("{")) {
                return objectMapper.readValue(trimmed, MAP_TYPE);
            } else {
                log.warn("未知的 configJson æ ¼å¼: {}", trimmed);
            }
        } catch (Exception e) {
            log.warn("解析 configJson å¤±è´¥: {}", trimmed, e);
        }
        return Collections.emptyMap();
    }
    private static Object firstNonNull(Object first, Object second) {
        return Objects.nonNull(first) ? first : second;
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/vo/DeviceControlProfile.java
@@ -1,6 +1,7 @@
package com.mes.device.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -17,52 +18,52 @@
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(name = "DeviceControlProfile", description = "设备控制参数配置")
@ApiModel(value = "DeviceControlProfile", description = "设备控制参数配置")
public class DeviceControlProfile implements Serializable {
    @Schema(description = "节拍/线速度(mm/s)")
    @ApiModelProperty(value = "节拍/线速度(mm/s)")
    private Integer lineSpeed;
    @Schema(description = "玻璃长度(mm)")
    @ApiModelProperty(value = "玻璃长度(mm)")
    private Integer glassLength;
    @Schema(description = "缓存数量/槽位数量")
    @ApiModelProperty(value = "缓存数量/槽位数量")
    private Integer bufferCount;
    @Schema(description = "是否自动触发PLC请求")
    @ApiModelProperty(value = "是否自动触发PLC请求")
    private Boolean autoRequest;
    @Schema(description = "PLC请求字段名", defaultValue = "plcRequest")
    @ApiModelProperty(value = "PLC请求字段名")
    private String requestField = "plcRequest";
    @Schema(description = "进片位置字段名", defaultValue = "inPosition")
    @ApiModelProperty(value = "进片位置字段名")
    private String positionField = "inPosition";
    @Schema(description = "玻璃数量字段名", defaultValue = "plcGlassCount")
    @ApiModelProperty(value = "玻璃数量字段名")
    private String glassCountField = "plcGlassCount";
    @Schema(description = "玻璃ID槽位字段定义")
    @ApiModelProperty(value = "玻璃ID槽位字段定义")
    private List<GlassSlot> glassSlots;
    @Schema(description = "位置映射,如:{ \"station1\":1 }")
    @ApiModelProperty(value = "位置映射,如:{ \"station1\":1 }")
    private Map<String, Integer> positionMappings;
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    @Schema(name = "GlassSlot", description = "玻璃ID槽位")
    @ApiModel(value = "GlassSlot", description = "玻璃ID槽位")
    public static class GlassSlot implements Serializable {
        @Schema(description = "槽位序号,从1开始")
        @ApiModelProperty(value = "槽位序号,从1开始")
        private Integer order;
        @Schema(description = "PLC字段名,例如 plcGlassId1")
        @ApiModelProperty(value = "PLC字段名,例如 plcGlassId1")
        private String field;
        @Schema(description = "字段长度,字符串长度等")
        @ApiModelProperty(value = "字段长度,字符串长度等")
        private Integer length;
        @Schema(description = "槽位描述")
        @ApiModelProperty(value = "槽位描述")
        private String description;
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/vo/DevicePlcVO.java
@@ -1,6 +1,7 @@
package com.mes.device.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -25,7 +26,7 @@
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Schema(name = "DevicePlcOperationResult", description = "PLC æ“ä½œç»“æžœ")
    @ApiModel(value = "DevicePlcOperationResult", description = "PLC æ“ä½œç»“æžœ")
    public static class OperationResult implements Serializable {
        private Long deviceId;
        private String deviceName;
@@ -44,7 +45,7 @@
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Schema(name = "DevicePlcStatus", description = "PLC çŠ¶æ€æ•°æ®")
    @ApiModel(value = "DevicePlcStatus", description = "PLC çŠ¶æ€æ•°æ®")
    public static class StatusInfo implements Serializable {
        private Long deviceId;
        private String deviceName;
mes-processes/mes-plcSend/src/main/java/com/mes/device/¶àÉ豸ÁªºÏ²âÊÔÀ©Õ¹·½°¸.md
New file
@@ -0,0 +1,1153 @@
# MES Test Project å¤šè®¾å¤‡è”合测试扩展方案
## ðŸ“‹ é¡¹ç›®æ¦‚è¿°
基于现有的MES Test Project(mes-web + mes-plcSend),扩展支持多设备联合测试功能,实现"上大车设备 â†’ å¤§ç†ç‰‡è®¾å¤‡ â†’ çŽ»ç’ƒå­˜å‚¨è®¾å¤‡"的完整生产流程自动化测试。
## ðŸŽ¯ æ ¸å¿ƒéœ€æ±‚
### ä¸šåŠ¡åœºæ™¯
1. **上大车前请求**:检测车辆容量(6000mm可配置)、玻璃规格匹配、节拍控制
2. **大理片交互**:与MES大理片信息比对验证、批量处理逻辑
3. **多设备协调**:设备间数据传递、状态同步、依赖管理
### æŠ€æœ¯éœ€æ±‚
- æ”¯æŒå¤šPLC设备地址映射
- è®¾å¤‡ç»„配置和管理
- ä¸²è¡Œ/并行执行模式
- è®¾å¤‡é—´æ•°æ®å…±äº«
- å®žæ—¶çŠ¶æ€ç›‘æŽ§
## ðŸ“ æ‰©å±•架构设计
### 1. åŽç«¯æ‰©å±•结构
#### 1.1 æ–°å¢žç›®å½•结构
```
mes-plcSend/src/main/java/com/mes/
├── device/                          # è®¾å¤‡ç®¡ç†å±‚
│   â”œâ”€â”€ entity/
│   â”‚   â”œâ”€â”€ DeviceConfig.java        # è®¾å¤‡é…ç½®å®žä½“
│   â”‚   â”œâ”€â”€ DeviceGroup.java         # è®¾å¤‡ç»„实体
│   â”‚   â””── DeviceStatus.java        # è®¾å¤‡çŠ¶æ€å®žä½“
│   â”œâ”€â”€ service/
│   â”‚   â”œâ”€â”€ DeviceService.java       # è®¾å¤‡ç®¡ç†æœåŠ¡
│   â”‚   â”œâ”€â”€ DeviceGroupService.java  # è®¾å¤‡ç»„服务
│   â”‚   â””── DeviceCoordinationService.java # è®¾å¤‡åè°ƒæœåŠ¡
│   â””── controller/
│       â”œâ”€â”€ DeviceController.java    # è®¾å¤‡ç®¡ç†API
│       â””── DeviceGroupController.java # è®¾å¤‡ç»„管理API
├── interaction/                     # äº¤äº’逻辑模块
│   â”œâ”€â”€ base/
│   â”‚   â”œâ”€â”€ BaseInteraction.java     # åŸºç¡€äº¤äº’抽象
│   â”‚   â”œâ”€â”€ InteractionContext.java  # äº¤äº’上下文
│   â”‚   â””── InteractionResult.java   # äº¤äº’结果
│   â”œâ”€â”€ ä¸Šå¤§è½¦/
│   â”‚   â”œâ”€â”€ ä¸Šå¤§è½¦Interaction.java   # ä¸Šå¤§è½¦äº¤äº’逻辑
│   â”‚   â””── ä¸Šå¤§è½¦Config.java        # ä¸Šå¤§è½¦é…ç½®
│   â”œâ”€â”€ å¤§ç†ç‰‡/
│   â”‚   â”œâ”€â”€ å¤§ç†ç‰‡Interaction.java   # å¤§ç†ç‰‡äº¤äº’逻辑
│   â”‚   â””── å¤§ç†ç‰‡Config.java        # å¤§ç†ç‰‡é…ç½®
│   â””── çŽ»ç’ƒå­˜å‚¨/
│       â”œâ”€â”€ çŽ»ç’ƒå­˜å‚¨Interaction.java # çŽ»ç’ƒå­˜å‚¨äº¤äº’é€»è¾‘
│       â””── çŽ»ç’ƒå­˜å‚¨Config.java      # çŽ»ç’ƒå­˜å‚¨é…ç½®
└── task/                           # ä»»åŠ¡ç®¡ç†å±‚
    â”œâ”€â”€ entity/
    â”‚   â”œâ”€â”€ MultiDeviceTask.java     # å¤šè®¾å¤‡ä»»åŠ¡å®žä½“
    â”‚   â””── TaskStep.java            # ä»»åŠ¡æ­¥éª¤å®žä½“
    â”œâ”€â”€ service/
    â”‚   â”œâ”€â”€ MultiDeviceTaskService.java # å¤šè®¾å¤‡ä»»åŠ¡æœåŠ¡
    â”‚   â””── TaskExecutionEngine.java    # ä»»åŠ¡æ‰§è¡Œå¼•æ“Ž
    â””── controller/
        â””── MultiDeviceTaskController.java # å¤šè®¾å¤‡ä»»åŠ¡API
```
#### 1.2 æ•°æ®åº“表扩展
```sql
-- è®¾å¤‡é…ç½®è¡¨
CREATE TABLE device_config (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    device_id VARCHAR(50) UNIQUE NOT NULL COMMENT '设备ID',
    device_name VARCHAR(100) NOT NULL COMMENT '设备名称',
    device_type VARCHAR(50) NOT NULL COMMENT '设备类型(上大车/大理片/玻璃存储)',
    plc_ip VARCHAR(15) NOT NULL COMMENT 'PLC IP地址',
    plc_type VARCHAR(20) NOT NULL COMMENT 'PLC类型',
    module_name VARCHAR(50) NOT NULL COMMENT '模块名称',
    is_primary BOOLEAN DEFAULT FALSE COMMENT '是否主控设备',
    enabled BOOLEAN DEFAULT TRUE COMMENT '是否启用',
    config_json TEXT COMMENT '设备特定配置(JSON)',
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- è®¾å¤‡ç»„配置表
CREATE TABLE device_group (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    group_id VARCHAR(50) UNIQUE NOT NULL COMMENT '设备组ID',
    group_name VARCHAR(100) NOT NULL COMMENT '设备组名称',
    project_id VARCHAR(50) NOT NULL COMMENT '关联项目ID',
    execution_mode ENUM('SERIAL', 'PARALLEL') DEFAULT 'SERIAL' COMMENT '执行模式',
    execution_config JSON COMMENT '执行配置',
    dependencies JSON COMMENT '设备依赖关系',
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- è®¾å¤‡ç»„与设备关系表
CREATE TABLE device_group_mapping (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    group_id VARCHAR(50) NOT NULL,
    device_id VARCHAR(50) NOT NULL,
    execution_order INT NOT NULL COMMENT '执行顺序',
    FOREIGN KEY (group_id) REFERENCES device_group(group_id) ON DELETE CASCADE,
    FOREIGN KEY (device_id) REFERENCES device_config(device_id) ON DELETE CASCADE,
    UNIQUE KEY uk_group_device (group_id, device_id)
);
-- å¤šè®¾å¤‡ä»»åŠ¡è¡¨
CREATE TABLE multi_device_task (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    task_id VARCHAR(50) UNIQUE NOT NULL COMMENT '任务ID',
    group_id VARCHAR(50) NOT NULL COMMENT '设备组ID',
    project_id VARCHAR(50) NOT NULL COMMENT '项目ID',
    status ENUM('PENDING', 'RUNNING', 'COMPLETED', 'FAILED', 'CANCELLED') DEFAULT 'PENDING',
    current_step INT DEFAULT 0 COMMENT '当前步骤',
    total_steps INT DEFAULT 0 COMMENT '总步骤数',
    start_time DATETIME COMMENT '开始时间',
    end_time DATETIME COMMENT '结束时间',
    error_message TEXT COMMENT '错误信息',
    result_data JSON COMMENT '结果数据',
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- ä»»åŠ¡æ­¥éª¤è¯¦æƒ…è¡¨
CREATE TABLE task_step_detail (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    task_id VARCHAR(50) NOT NULL COMMENT '任务ID',
    step_order INT NOT NULL COMMENT '步骤顺序',
    device_id VARCHAR(50) NOT NULL COMMENT '设备ID',
    step_name VARCHAR(100) NOT NULL COMMENT '步骤名称',
    status ENUM('PENDING', 'RUNNING', 'COMPLETED', 'FAILED', 'SKIPPED') DEFAULT 'PENDING',
    start_time DATETIME COMMENT '步骤开始时间',
    end_time DATETIME COMMENT '步骤结束时间',
    duration_ms BIGINT COMMENT '执行耗时(毫秒)',
    input_data JSON COMMENT '输入数据',
    output_data JSON COMMENT '输出数据',
    error_message TEXT COMMENT '错误信息',
    retry_count INT DEFAULT 0 COMMENT '重试次数',
    created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (task_id) REFERENCES multi_device_task(task_id) ON DELETE CASCADE
);
```
### 2. å‰ç«¯æ‰©å±•结构
#### 2.1 æ–°å¢žå‰ç«¯ç›®å½•
```
mes-web/src/views/plcTest/
├── components/
│   â”œâ”€â”€ DeviceManagement/          # è®¾å¤‡ç®¡ç†ç»„ä»¶
│   â”‚   â”œâ”€â”€ DeviceList.vue         # è®¾å¤‡åˆ—表
│   â”‚   â”œâ”€â”€ DeviceConfig.vue       # è®¾å¤‡é…ç½®
│   â”‚   â””── DeviceStatus.vue       # è®¾å¤‡çŠ¶æ€
│   â”œâ”€â”€ DeviceGroup/              # è®¾å¤‡ç»„组件
│   â”‚   â”œâ”€â”€ GroupList.vue          # è®¾å¤‡ç»„列表
│   â”‚   â”œâ”€â”€ GroupConfig.vue        # è®¾å¤‡ç»„配置
│   â”‚   â””── GroupTopology.vue      # è®¾å¤‡ç»„拓扑图
│   â”œâ”€â”€ MultiDeviceTest/          # å¤šè®¾å¤‡æµ‹è¯•组件
│   â”‚   â”œâ”€â”€ TestOrchestration.vue  # æµ‹è¯•编排
│   â”‚   â”œâ”€â”€ ExecutionMonitor.vue   # æ‰§è¡Œç›‘控
│   â”‚   â””── ResultAnalysis.vue     # ç»“果分析
│   â””── InteractionLogic/         # äº¤äº’逻辑组件
│       â”œâ”€â”€ ä¸Šå¤§è½¦Config.vue       # ä¸Šå¤§è½¦é…ç½®
│       â”œâ”€â”€ å¤§ç†ç‰‡Config.vue       # å¤§ç†ç‰‡é…ç½®
│       â””── çŽ»ç’ƒå­˜å‚¨Config.vue     # çŽ»ç’ƒå­˜å‚¨é…ç½®
```
## ðŸ”§ æ ¸å¿ƒå®žçŽ°è®¾è®¡
### 3.1 è®¾å¤‡ç®¡ç†å®žçް
#### è®¾å¤‡é…ç½®å®žä½“
```java
@Data
@Entity
@TableName("device_config")
public class DeviceConfig {
    @TableId(type = IdType.AUTO)
    private Long id;
    @TableField("device_id")
    private String deviceId;
    @TableField("device_name")
    private String deviceName;
    @TableField("device_type")
    private String deviceType; // "上大车" / "大理片" / "玻璃存储"
    @TableField("plc_ip")
    private String plcIp;
    @TableField("plc_type")
    private String plcType;
    @TableField("module_name")
    private String moduleName;
    @TableField("is_primary")
    private Boolean isPrimary;
    @TableField("config_json")
    private String configJson; // è®¾å¤‡ç‰¹å®šé…ç½®
    // é…ç½®è§£æžæ–¹æ³•
    public <T> T getConfig(Class<T> clazz) {
        if (StringUtils.isBlank(configJson)) {
            return null;
        }
        try {
            return JsonUtils.fromJson(configJson, clazz);
        } catch (Exception e) {
            log.error("解析设备配置失败: {}", deviceId, e);
            return null;
        }
    }
}
```
#### è®¾å¤‡ç®¡ç†æœåŠ¡
```java
@Service
public class DeviceService {
    @Resource
    private DeviceConfigMapper deviceConfigMapper;
    /**
     * æ³¨å†Œè®¾å¤‡
     */
    @Transactional
    public void registerDevice(DeviceConfig device) {
        // éªŒè¯PLC连接
        validatePlcConnection(device.getPlcIp(), device.getPlcType());
        // ä¿å­˜è®¾å¤‡é…ç½®
        deviceConfigMapper.insert(device);
        // æ›´æ–°åœ°å€æ˜ å°„
        updatePlcAddressMapping(device);
        log.info("设备注册成功: {}", device.getDeviceId());
    }
    /**
     * èŽ·å–è®¾å¤‡çš„PLC地址映射
     */
    public Map<String, Integer> getDeviceAddressMapping(String deviceId) {
        DeviceConfig device = getDeviceById(deviceId);
        if (device == null) {
            throw new RuntimeException("设备不存在: " + deviceId);
        }
        // æ ¹æ®è®¾å¤‡ç±»åž‹èŽ·å–å¯¹åº”çš„åœ°å€æ˜ å°„é…ç½®
        String mappingKey = device.getDeviceType() + "_" + device.getModuleName();
        return plcAddressService.getMappingByKey(mappingKey);
    }
}
```
### 3.2 è®¾å¤‡ç»„管理实现
#### è®¾å¤‡ç»„实体
```java
@Data
@TableName("device_group")
public class DeviceGroup {
    @TableId(type = IdType.AUTO)
    private Long id;
    @TableField("group_id")
    private String groupId;
    @TableField("group_name")
    private String groupName;
    @TableField("project_id")
    private String projectId;
    @TableField("execution_mode")
    private ExecutionMode executionMode; // SERIAL / PARALLEL
    @TableField("execution_config")
    private String executionConfig;
    @TableField("dependencies")
    private String dependencies; // JSON格式的依赖关系
    // èŽ·å–è®¾å¤‡ç»„ä¸­çš„æ‰€æœ‰è®¾å¤‡
    public List<DeviceConfig> getDevices() {
        return deviceGroupService.getDevicesByGroupId(groupId);
    }
    // èŽ·å–æ‰§è¡Œé¡ºåº
    public List<DeviceConfig> getExecutionSequence() {
        return deviceGroupService.getDevicesByOrder(groupId);
    }
}
```
### 3.3 äº¤äº’逻辑实现
#### åŸºç¡€äº¤äº’接口
```java
/**
 * è®¾å¤‡äº¤äº’逻辑接口
 */
public interface DeviceInteraction {
    /**
     * æ‰§è¡Œäº¤äº’逻辑
     */
    InteractionResult execute(InteractionContext context);
    /**
     * éªŒè¯å‰ç½®æ¡ä»¶
     */
    boolean validatePreCondition(InteractionContext context);
    /**
     * èŽ·å–è®¾å¤‡ç±»åž‹
     */
    String getDeviceType();
    /**
     * èŽ·å–é»˜è®¤é…ç½®
     */
    DeviceInteractionConfig getDefaultConfig();
}
```
#### ä¸Šå¤§è½¦äº¤äº’实现
```java
@Component("上大车Interaction")
public class ä¸Šå¤§è½¦Interaction implements DeviceInteraction {
    @Override
    public InteractionResult execute(InteractionContext context) {
        ä¸Šå¤§è½¦Config config = context.getConfig(上大车Config.class);
        try {
            // 1. éªŒè¯å‰ç½®æ¡ä»¶
            if (!validatePreCondition(context)) {
                return InteractionResult.fail("前置条件验证失败");
            }
            // 2. èŽ·å–è½¦è¾†è§„æ ¼ï¼ˆå¯é…ç½®ï¼‰
            VehicleSpec vehicle = context.getVehicleSpec();
            double vehicleCapacity = config.getVehicleCapacity(); // é»˜è®¤6000mm
            // 3. æ£€æŸ¥è½¦è¾†å®¹é‡
            if (vehicle.getMaxCapacity() > vehicleCapacity) {
                return InteractionResult.fail("车辆容量超出限制: " + vehicle.getMaxCapacity());
            }
            // 4. èŽ·å–å½“å‰çŽ»ç’ƒä¿¡æ¯
            GlassSpec currentGlass = context.getCurrentGlass();
            // 5. è®¡ç®—装载空间
            double usedCapacity = calculateUsedCapacity(context);
            double remainingCapacity = vehicleCapacity - usedCapacity;
            if (remainingCapacity >= currentGlass.getLength()) {
                // 6. åˆ†é…è½¦è¾†ç©ºé—´
                allocateVehicleSpace(context, currentGlass);
                // 7. èŠ‚æ‹æŽ§åˆ¶ï¼ˆå¯é…ç½®é—´éš”æ—¶é—´ï¼‰
                sleep(config.getGlassIntervalMs()); // é»˜è®¤1000ms
                // 8. è§¦å‘PLC写入
                triggerPlcWrite(context, "车辆装载", currentGlass);
                return InteractionResult.success("上大车成功",
                    Map.of("remainingCapacity", remainingCapacity,
                           "allocatedGlass", currentGlass));
            }
            return InteractionResult.wait("等待下一辆车上大车",
                Map.of("remainingCapacity", remainingCapacity));
        } catch (Exception e) {
            log.error("上大车交互执行失败", e);
            return InteractionResult.fail("执行异常: " + e.getMessage());
        }
    }
    @Override
    public boolean validatePreCondition(InteractionContext context) {
        // éªŒè¯è½¦è¾†ä¿¡æ¯ã€çŽ»ç’ƒä¿¡æ¯ã€PLC连接等
        return context.getVehicleSpec() != null &&
               context.getCurrentGlass() != null &&
               validatePlcConnection(context.getDeviceId());
    }
    @Override
    public String getDeviceType() {
        return "上大车";
    }
}
```
#### å¤§ç†ç‰‡äº¤äº’实现
```java
@Component("大理片Interaction")
public class å¤§ç†ç‰‡Interaction implements DeviceInteraction {
    @Override
    public InteractionResult execute(InteractionContext context) {
        å¤§ç†ç‰‡Config config = context.getConfig(大理片Config.class);
        try {
            // 1. èŽ·å–ä¸Šå¤§è½¦é˜¶æ®µä¼ é€’çš„æ•°æ®
            List<GlassSpec> glassesFromVehicle = context.getSharedData("glassesFromVehicle", List.class);
            VehicleSpec vehicleInfo = context.getSharedData("vehicleInfo", VehicleSpec.class);
            if (glassesFromVehicle == null || glassesFromVehicle.isEmpty()) {
                return InteractionResult.wait("等待上大车数据");
            }
            // 2. ä¸ŽMES大理片信息比对验证
            List<GlassSpec> mesGlasses = fetchMesGlassesInfo(vehicleInfo.getVehicleId());
            for (GlassSpec vehicleGlass : glassesFromVehicle) {
                boolean matched = false;
                for (GlassSpec mesGlass : mesGlasses) {
                    if (isGlassMatched(vehicleGlass, mesGlass, config)) {
                        matched = true;
                        break;
                    }
                }
                if (!matched && config.isGlassMatchingEnabled()) {
                    return InteractionResult.fail("玻璃信息不匹配: " + vehicleGlass.getGlassId());
                }
            }
            // 3. æ‰¹é‡å¤„理(可配置)
            if (config.isBatchProcessing()) {
                return processBatch(glassesFromVehicle, context, config);
            } else {
                return processIndividual(glassesFromVehicle, context, config);
            }
        } catch (Exception e) {
            log.error("大理片交互执行失败", e);
            return InteractionResult.fail("执行异常: " + e.getMessage());
        }
    }
    private InteractionResult processBatch(List<GlassSpec> glasses, InteractionContext context, å¤§ç†ç‰‡Config config) {
        // æ‰¹é‡å¤„理逻辑
        for (GlassSpec glass : glasses) {
            // æ¯ç‰‡çŽ»ç’ƒå¤„ç†é—´éš”
            sleep(config.getProcessingInterval()); // é»˜è®¤2000ms
            triggerPlcWrite(context, "大理片处理", glass);
        }
        // ä¼ é€’处理结果到下一个设备
        context.setSharedData("processedGlasses", glasses);
        return InteractionResult.success("大理片批量处理完成",
            Map.of("processedCount", glasses.size()));
    }
}
```
### 3.4 å¤šè®¾å¤‡ä»»åŠ¡æ‰§è¡Œå¼•æ“Ž
#### ä»»åŠ¡æ‰§è¡Œå¼•æ“Ž
```java
@Service
public class TaskExecutionEngine {
    @Resource
    private DeviceGroupService deviceGroupService;
    /**
     * æ‰§è¡Œå¤šè®¾å¤‡è”合测试
     */
    @Transactional
    public MultiDeviceTaskResult executeMultiDeviceTask(String groupId, TaskParameters parameters) {
        DeviceGroup group = deviceGroupService.getDeviceGroupById(groupId);
        if (group == null) {
            throw new RuntimeException("设备组不存在: " + groupId);
        }
        // 1. åˆ›å»ºä»»åŠ¡è®°å½•
        MultiDeviceTask task = createTaskRecord(group, parameters);
        try {
            task.setStatus("RUNNING");
            task.setStartTime(new Date());
            // 2. æž„建交互上下文
            InteractionContext context = buildInteractionContext(group, parameters);
            // 3. æŒ‰æ‰§è¡Œæ¨¡å¼æ‰§è¡Œ
            MultiDeviceTaskResult result;
            if (group.getExecutionMode() == ExecutionMode.SERIAL) {
                result = executeSerialDevices(group, context, task);
            } else {
                result = executeParallelDevices(group, context, task);
            }
            // 4. æ›´æ–°ä»»åŠ¡ç»“æžœ
            task.setStatus(result.isSuccess() ? "COMPLETED" : "FAILED");
            task.setEndTime(new Date());
            task.setResultData(JsonUtils.toJson(result));
            return result;
        } catch (Exception e) {
            log.error("多设备任务执行失败: {}", groupId, e);
            task.setStatus("FAILED");
            task.setEndTime(new Date());
            task.setErrorMessage(e.getMessage());
            return MultiDeviceTaskResult.fail("任务执行失败: " + e.getMessage());
        }
    }
    /**
     * ä¸²è¡Œè®¾å¤‡æ‰§è¡Œï¼šä¸Šå¤§è½¦ â†’ å¤§ç†ç‰‡ â†’ çŽ»ç’ƒå­˜å‚¨
     */
    private MultiDeviceTaskResult executeSerialDevices(DeviceGroup group, InteractionContext context, MultiDeviceTask task) {
        MultiDeviceTaskResult finalResult = new MultiDeviceTaskResult();
        List<DeviceConfig> executionSequence = group.getExecutionSequence();
        for (int i = 0; i < executionSequence.size(); i++) {
            DeviceConfig device = executionSequence.get(i);
            TaskStep step = createTaskStep(task.getTaskId(), i + 1, device);
            try {
                step.setStatus("RUNNING");
                step.setStartTime(new Date());
                // è®¾ç½®å½“前设备上下文
                context.setCurrentDevice(device);
                context.setDeviceId(device.getDeviceId());
                // æ‰§è¡Œè®¾å¤‡äº¤äº’逻辑
                DeviceInteraction interaction = getInteraction(device.getDeviceType());
                InteractionResult stepResult = interaction.execute(context);
                step.setEndTime(new Date());
                step.setDurationMs(System.currentTimeMillis() - step.getStartTime().getTime());
                if (stepResult.isSuccess()) {
                    step.setStatus("COMPLETED");
                    step.setOutputData(JsonUtils.toJson(stepResult.getData()));
                    // ä¼ é€’数据到下一个设备
                    passDataToNextDevice(context, device, stepResult);
                    finalResult.addStepResult(device.getDeviceName(), stepResult);
                } else {
                    step.setStatus("FAILED");
                    step.setErrorMessage(stepResult.getMessage());
                    finalResult.addStepResult(device.getDeviceName(), stepResult);
                    finalResult.fail("设备 " + device.getDeviceName() + " æ‰§è¡Œå¤±è´¥: " + stepResult.getMessage());
                    break;
                }
            } catch (Exception e) {
                step.setStatus("FAILED");
                step.setErrorMessage(e.getMessage());
                step.setEndTime(new Date());
                log.error("设备执行异常: {}", device.getDeviceName(), e);
                finalResult.fail("设备 " + device.getDeviceName() + " æ‰§è¡Œå¼‚常: " + e.getMessage());
                break;
            }
        }
        return finalResult;
    }
    /**
     * æ•°æ®ä¼ é€’到下一个设备
     */
    private void passDataToNextDevice(InteractionContext context, DeviceConfig currentDevice, InteractionResult result) {
        String deviceType = currentDevice.getDeviceType();
        if ("上大车".equals(deviceType)) {
            // ä¸Šå¤§è½¦ â†’ å¤§ç†ç‰‡ï¼šä¼ é€’玻璃列表和车辆信息
            context.setSharedData("glassesFromVehicle", result.getData("glasses"));
            context.setSharedData("vehicleInfo", result.getData("vehicle"));
        } else if ("大理片".equals(deviceType)) {
            // å¤§ç†ç‰‡ â†’ çŽ»ç’ƒå­˜å‚¨ï¼šä¼ é€’å¤„ç†å®Œæˆçš„çŽ»ç’ƒ
            context.setSharedData("processedGlasses", result.getData("processedGlasses"));
        }
    }
}
```
## ðŸŽ¨ å‰ç«¯ç•Œé¢è®¾è®¡
### 4.1 è®¾å¤‡ç®¡ç†ç•Œé¢
#### è®¾å¤‡é…ç½®é¡µé¢
```vue
<template>
  <div class="device-management">
    <!-- è®¾å¤‡åˆ—表 -->
    <div class="device-list">
      <el-table :data="devices" style="width: 100%">
        <el-table-column prop="deviceName" label="设备名称" />
        <el-table-column prop="deviceType" label="设备类型" />
        <el-table-column prop="plcIp" label="PLC IP" />
        <el-table-column prop="status" label="状态">
          <template #default="{ row }">
            <el-tag :type="getStatusType(row.status)">{{ row.status }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作">
          <template #default="{ row }">
            <el-button @click="editDevice(row)">编辑</el-button>
            <el-button @click="testConnection(row)">测试连接</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <!-- è®¾å¤‡é…ç½®è¡¨å• -->
    <el-dialog v-model="showConfigDialog" title="设备配置">
      <el-form :model="deviceForm" label-width="120px">
        <el-form-item label="设备ID">
          <el-input v-model="deviceForm.deviceId" />
        </el-form-item>
        <el-form-item label="设备名称">
          <el-input v-model="deviceForm.deviceName" />
        </el-form-item>
        <el-form-item label="设备类型">
          <el-select v-model="deviceForm.deviceType">
            <el-option label="上大车设备" value="上大车" />
            <el-option label="大理片设备" value="大理片" />
            <el-option label="玻璃存储设备" value="玻璃存储" />
          </el-select>
        </el-form-item>
        <el-form-item label="PLC配置">
          <div class="plc-config">
            <el-input v-model="deviceForm.plcIp" placeholder="PLC IP地址" />
            <el-select v-model="deviceForm.plcType" placeholder="PLC类型">
              <el-option label="S7-1200" value="S7-1200" />
              <el-option label="S7-1500" value="S7-1500" />
            </el-select>
          </div>
        </el-form-item>
        <!-- è®¾å¤‡ç‰¹å®šé…ç½® -->
        <div v-if="deviceForm.deviceType === '上大车'">
          <el-form-item label="车辆容量(mm)">
            <el-input-number v-model="deviceForm.config.vehicleCapacity" :min="1000" :max="12000" />
          </el-form-item>
          <el-form-item label="玻璃间隔(ms)">
            <el-input-number v-model="deviceForm.config.glassIntervalMs" :min="100" :max="10000" />
          </el-form-item>
        </div>
        <div v-if="deviceForm.deviceType === '大理片'">
          <el-form-item label="启用玻璃比对">
            <el-switch v-model="deviceForm.config.glassMatchingEnabled" />
          </el-form-item>
          <el-form-item label="批量处理">
            <el-switch v-model="deviceForm.config.batchProcessing" />
          </el-form-item>
        </div>
      </el-form>
      <template #footer>
        <el-button @click="showConfigDialog = false">取消</el-button>
        <el-button type="primary" @click="saveDevice">保存</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script>
export default {
  data() {
    return {
      devices: [],
      showConfigDialog: false,
      deviceForm: {
        deviceId: '',
        deviceName: '',
        deviceType: '',
        plcIp: '',
        plcType: '',
        config: {
          vehicleCapacity: 6000,      // é»˜è®¤6ç±³
          glassIntervalMs: 1000,     // é»˜è®¤1秒
          glassMatchingEnabled: true,
          batchProcessing: true
        }
      }
    }
  },
  methods: {
    async loadDevices() {
      // åŠ è½½è®¾å¤‡åˆ—è¡¨
      const response = await this.$api.device.getDeviceList();
      this.devices = response.data;
    },
    async saveDevice() {
      try {
        if (this.deviceForm.deviceId) {
          await this.$api.device.updateDevice(this.deviceForm);
        } else {
          await this.$api.device.createDevice(this.deviceForm);
        }
        this.$message.success('保存成功');
        this.showConfigDialog = false;
        this.loadDevices();
      } catch (error) {
        this.$message.error('保存失败: ' + error.message);
      }
    }
  }
}
</script>
```
### 4.2 è®¾å¤‡ç»„配置界面
```vue
<template>
  <div class="device-group-management">
    <!-- è®¾å¤‡ç»„列表 -->
    <div class="group-list">
      <div class="header">
        <h3>设备组配置</h3>
        <el-button type="primary" @click="createGroup">新建设备组</el-button>
      </div>
      <div class="groups">
        <el-card v-for="group in deviceGroups" :key="group.groupId" class="group-card">
          <template #header>
            <div class="card-header">
              <span>{{ group.groupName }}</span>
              <el-tag :type="group.executionMode === 'SERIAL' ? 'primary' : 'success'">
                {{ group.executionMode === 'SERIAL' ? '串行执行' : '并行执行' }}
              </el-tag>
            </div>
          </template>
          <div class="group-content">
            <!-- è®¾å¤‡æ‹“扑图 -->
            <div class="topology">
              <div v-for="(device, index) in group.devices" :key="device.deviceId" class="device-node">
                <div class="device-box" :class="device.deviceType">
                  <div class="device-name">{{ device.deviceName }}</div>
                  <div class="device-type">{{ device.deviceType }}</div>
                </div>
                <div v-if="index < group.devices.length - 1" class="arrow">→</div>
              </div>
            </div>
            <!-- ä¾èµ–关系 -->
            <div class="dependencies" v-if="group.dependencies">
              <h4>设备依赖关系:</h4>
              <div v-for="(deps, device) in group.dependencies" :key="device" class="dependency-item">
                {{ device }} ä¾èµ–于: {{ deps.join(', ') }}
              </div>
            </div>
          </div>
          <template #footer>
            <div class="card-footer">
              <el-button @click="editGroup(group)">编辑</el-button>
              <el-button @click="startTest(group)">开始测试</el-button>
              <el-button @click="deleteGroup(group)">删除</el-button>
            </div>
          </template>
        </el-card>
      </div>
    </div>
    <!-- è®¾å¤‡ç»„配置对话框 -->
    <el-dialog v-model="showGroupDialog" title="设备组配置" width="800px">
      <el-form :model="groupForm" label-width="120px">
        <el-form-item label="设备组名称">
          <el-input v-model="groupForm.groupName" />
        </el-form-item>
        <el-form-item label="关联项目">
          <el-select v-model="groupForm.projectId">
            <el-option v-for="project in projects" :key="project.id"
                      :label="project.projectName" :value="project.id" />
          </el-select>
        </el-form-item>
        <el-form-item label="执行模式">
          <el-radio-group v-model="groupForm.executionMode">
            <el-radio label="SERIAL">串行执行</el-radio>
            <el-radio label="PARALLEL">并行执行</el-radio>
          </el-radio-group>
        </el-form-item>
        <!-- è®¾å¤‡é€‰æ‹© -->
        <el-form-item label="包含设备">
          <el-transfer
            v-model="selectedDevices"
            :data="availableDevices"
            :titles="['可用设备', '已选设备']"
            @change="updateDeviceOrder" />
        </el-form-item>
        <!-- æ‰§è¡Œé¡ºåºè°ƒæ•´ -->
        <el-form-item label="执行顺序" v-if="groupForm.executionMode === 'SERIAL'">
          <div class="execution-order">
            <div v-for="(deviceId, index) in deviceOrder" :key="deviceId" class="order-item">
              <span>{{ index + 1 }}. {{ getDeviceName(deviceId) }}</span>
              <el-button-group>
                <el-button @click="moveUp(index)" :disabled="index === 0">↑</el-button>
                <el-button @click="moveDown(index)" :disabled="index === deviceOrder.length - 1">↓</el-button>
              </el-button-group>
            </div>
          </div>
        </el-form-item>
      </el-form>
    </el-dialog>
  </div>
</template>
<script>
export default {
  data() {
    return {
      deviceGroups: [],
      availableDevices: [],
      selectedDevices: [],
      deviceOrder: [],
      showGroupDialog: false,
      groupForm: {
        groupId: '',
        groupName: '',
        projectId: '',
        executionMode: 'SERIAL'
      }
    }
  },
  methods: {
    async loadDeviceGroups() {
      const response = await this.$api.deviceGroup.getGroupList();
      this.deviceGroups = response.data;
    },
    async startTest(group) {
      try {
        // è·³è½¬åˆ°æµ‹è¯•执行页面
        this.$router.push({
          name: 'MultiDeviceTest',
          query: { groupId: group.groupId }
        });
      } catch (error) {
        this.$message.error('启动测试失败: ' + error.message);
      }
    }
  }
}
</script>
```
### 4.3 å¤šè®¾å¤‡æµ‹è¯•执行界面
```vue
<template>
  <div class="multi-device-test">
    <!-- æµ‹è¯•配置 -->
    <div class="test-config">
      <el-card>
        <template #header>
          <h3>测试配置 - {{ groupInfo.groupName }}</h3>
        </template>
        <div class="config-content">
          <div class="execution-mode">
            <el-tag :type="groupInfo.executionMode === 'SERIAL' ? 'primary' : 'success'">
              {{ groupInfo.executionMode === 'SERIAL' ? '串行执行' : '并行执行' }}
            </el-tag>
          </div>
          <div class="parameters">
            <h4>测试参数:</h4>
            <el-form label-width="150px">
              <el-form-item label="车辆规格">
                <el-input-number v-model="testParams.vehicleLength" :min="1000" :max="12000" /> mm
              </el-form-item>
              <el-form-item label="测试玻璃数量">
                <el-input-number v-model="testParams.glassCount" :min="1" :max="50" />
              </el-form-item>
              <el-form-item label="执行间隔">
                <el-input-number v-model="testParams.executionInterval" :min="500" :max="10000" /> ms
              </el-form-item>
            </el-form>
          </div>
          <div class="test-controls">
            <el-button type="primary" size="large" @click="startTest" :loading="isRunning">
              {{ isRunning ? '测试执行中...' : '开始测试' }}
            </el-button>
            <el-button @click="stopTest" :disabled="!isRunning">停止测试</el-button>
            <el-button @click="pauseTest" :disabled="!isRunning">暂停测试</el-button>
          </div>
        </div>
      </el-card>
    </div>
    <!-- æ‰§è¡Œç›‘控 -->
    <div class="execution-monitor" v-if="isRunning || currentTask">
      <el-card>
        <template #header>
          <h3>执行监控</h3>
        </template>
        <div class="monitor-content">
          <!-- ä»»åŠ¡è¿›åº¦ -->
          <div class="task-progress">
            <el-progress
              :percentage="getOverallProgress()"
              :status="getTaskStatus()" />
            <div class="progress-info">
              æ­¥éª¤ {{ currentStep }} / {{ totalSteps }} - {{ getCurrentStepName() }}
            </div>
          </div>
          <!-- è®¾å¤‡çŠ¶æ€ -->
          <div class="device-status-grid">
            <div v-for="device in groupInfo.devices" :key="device.deviceId"
                 class="device-status-card">
              <div class="device-header">
                <span class="device-name">{{ device.deviceName }}</span>
                <el-tag :type="getDeviceStatusType(device.deviceId)">
                  {{ getDeviceStatus(device.deviceId) }}
                </el-tag>
              </div>
              <div class="device-details" v-if="getDeviceDetails(device.deviceId)">
                <div class="detail-item">
                  <span>当前玻璃:</span> {{ getDeviceDetails(device.deviceId).currentGlass || '-' }}
                </div>
                <div class="detail-item">
                  <span>处理数量:</span> {{ getDeviceDetails(device.deviceId).processedCount || 0 }}
                </div>
                <div class="detail-item">
                  <span>剩余容量:</span> {{ getDeviceDetails(device.deviceId).remainingCapacity || '-' }} mm
                </div>
              </div>
              <!-- å®žæ—¶æ—¥å¿— -->
              <div class="device-logs">
                <div v-for="log in getDeviceLogs(device.deviceId)" :key="log.timestamp"
                     class="log-item" :class="log.level">
                  <span class="log-time">{{ formatTime(log.timestamp) }}</span>
                  <span class="log-message">{{ log.message }}</span>
                </div>
              </div>
            </div>
          </div>
          <!-- æ•°æ®æµå¯è§†åŒ– -->
          <div class="data-flow">
            <h4>数据流状态:</h4>
            <div class="flow-diagram">
              <div v-for="(device, index) in groupInfo.devices" :key="device.deviceId" class="flow-node">
                <div class="node" :class="['device-' + device.deviceType.toLowerCase(), getNodeStatus(device.deviceId)]">
                  {{ device.deviceName }}
                </div>
                <div class="data-indicator" v-if="index < groupInfo.devices.length - 1">
                  <div class="data-flow-arrow" :class="getFlowStatus(device.deviceId, index + 1)">
                    {{ getFlowData(device.deviceId) }}
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </el-card>
    </div>
    <!-- ç»“果分析 -->
    <div class="test-results" v-if="testResults">
      <el-card>
        <template #header>
          <h3>测试结果</h3>
        </template>
        <div class="results-content">
          <!-- æ€»ä½“结果 -->
          <div class="overall-result">
            <el-result
              :icon="testResults.success ? 'success' : 'error'"
              :title="testResults.success ? '测试成功' : '测试失败'"
              :sub-title="testResults.message">
              <template #extra>
                <div class="result-stats">
                  <div class="stat-item">
                    <span class="label">执行时间:</span>
                    <span class="value">{{ formatDuration(testResults.duration) }}</span>
                  </div>
                  <div class="stat-item">
                    <span class="label">处理玻璃:</span>
                    <span class="value">{{ testResults.processedGlassCount }} ç‰‡</span>
                  </div>
                  <div class="stat-item">
                    <span class="label">成功率:</span>
                    <span class="value">{{ testResults.successRate }}%</span>
                  </div>
                </div>
              </template>
            </el-result>
          </div>
          <!-- è¯¦ç»†ç»“æžœ -->
          <div class="detailed-results">
            <h4>各设备执行详情:</h4>
            <el-collapse>
              <el-collapse-item v-for="(result, deviceName) in testResults.deviceResults"
                               :key="deviceName" :title="deviceName">
                <div class="device-result-detail">
                  <div class="result-summary">
                    <el-tag :type="result.success ? 'success' : 'error'">
                      {{ result.success ? '执行成功' : '执行失败' }}
                    </el-tag>
                    <span class="duration">耗时: {{ formatDuration(result.duration) }}</span>
                  </div>
                  <div class="result-data">
                    <h5>输出数据:</h5>
                    <pre>{{ JSON.stringify(result.outputData, null, 2) }}</pre>
                  </div>
                  <div class="result-logs">
                    <h5>执行日志:</h5>
                    <div class="log-list">
                      <div v-for="log in result.logs" :key="log.timestamp" class="log-line">
                        <span class="log-time">{{ formatTime(log.timestamp) }}</span>
                        <span class="log-level">{{ log.level }}</span>
                        <span class="log-message">{{ log.message }}</span>
                      </div>
                    </div>
                  </div>
                </div>
              </el-collapse-item>
            </el-collapse>
          </div>
          <!-- å¯¼å‡ºç»“æžœ -->
          <div class="export-results">
            <el-button @click="exportResults('json')">导出JSON</el-button>
            <el-button @click="exportResults('excel')">导出Excel</el-button>
            <el-button @click="exportResults('pdf')">导出PDF报告</el-button>
          </div>
        </div>
      </el-card>
    </div>
  </div>
</template>
```
## ðŸš€ å®žæ–½è®¡åˆ’
### ç¬¬ä¸€é˜¶æ®µï¼šåŸºç¡€æž¶æž„搭建(1-2周)
1. **数据库表创建**
   - åˆ›å»ºè®¾å¤‡ç®¡ç†ç›¸å…³è¡¨
   - å»ºç«‹è¡¨å…³ç³»å’Œç´¢å¼•
   - è¿ç§»çŽ°æœ‰æ•°æ®ï¼ˆå¦‚éœ€è¦ï¼‰
2. **后端基础组件**
   - è®¾å¤‡é…ç½®å®žä½“和管理服务
   - è®¾å¤‡ç»„管理组件
   - åŸºç¡€äº¤äº’接口定义
3. **前端基础界面**
   - è®¾å¤‡ç®¡ç†é¡µé¢
   - è®¾å¤‡ç»„配置页面
   - åŸºç¡€ç»„件开发
### ç¬¬äºŒé˜¶æ®µï¼šæ ¸å¿ƒäº¤äº’逻辑(2-3周)
1. **设备交互实现**
   - ä¸Šå¤§è½¦äº¤äº’逻辑
   - å¤§ç†ç‰‡äº¤äº’逻辑
   - çŽ»ç’ƒå­˜å‚¨äº¤äº’é€»è¾‘
2. **任务执行引擎**
   - ä¸²è¡Œæ‰§è¡Œå¼•擎
   - å¤šè®¾å¤‡åè°ƒæœºåˆ¶
   - é”™è¯¯å¤„理和重试逻辑
3. **PLC地址映射扩展**
   - å¤šè®¾å¤‡åœ°å€æ˜ å°„支持
   - åœ°å€é…ç½®ç®¡ç†
   - PLC通信适配
### ç¬¬ä¸‰é˜¶æ®µï¼šå‰ç«¯å®Œå–„(1-2周)
1. **测试执行界面**
   - å¤šè®¾å¤‡æµ‹è¯•编排
   - å®žæ—¶ç›‘控面板
   - ç»“果分析展示
2. **用户交互优化**
   - é…ç½®å‘导
   - å¯è§†åŒ–设备拓扑
   - å®žæ—¶æ•°æ®æµå±•示
### ç¬¬å››é˜¶æ®µï¼šé›†æˆæµ‹è¯•(1周)
1. **功能测试**
   - å¤šè®¾å¤‡è”合测试流程
   - å¼‚常情况处理
   - æ€§èƒ½æµ‹è¯•
2. **系统集成**
   - ä¸ŽçŽ°æœ‰ç³»ç»Ÿé›†æˆ
   - æ•°æ®è¿ç§»éªŒè¯
   - ç”¨æˆ·éªŒæ”¶æµ‹è¯•
## ðŸ“Š æŠ€æœ¯è¦ç‚¹æ€»ç»“
### æ ¸å¿ƒä¼˜åŠ¿
1. **模块化设计**:每个设备类型独立实现,便于扩展
2. **配置驱动**:所有参数可配置,支持不同业务场景
3. **数据流管理**:设备间数据传递和状态同步
4. **可视化监控**:实时显示设备状态和数据流
5. **灵活扩展**:支持新增设备类型和交互逻辑
### æŠ€æœ¯éš¾ç‚¹
1. **设备协调**:确保多设备执行的正确顺序
2. **数据同步**:设备间数据的准确传递
3. **异常处理**:单个设备失败对整个流程的影响
4. **性能优化**:大批量数据的处理性能
### é£Žé™©æŽ§åˆ¶
1. **向后兼容**:不破坏现有单设备功能
2. **渐进式升级**:分阶段实施,降低风险
3. **充分测试**:每个阶段的全面测试验证
4. **回滚机制**:出现问题时的快速回滚方案
---
## ðŸ“ ç»“论
通过这个扩展方案,MES Test Project将具备完整的多设备联合测试能力,支持复杂的生产流程自动化测试。方案基于现有架构设计,风险可控,实施难度适中,能够很好地满足业务需求。
建议优先实现第一阶段的基础架构,然后逐步完善交互逻辑和前端界面,确保每个阶段都能交付可用的功能。
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/BaseDeviceLogicHandler.java
New file
@@ -0,0 +1,124 @@
package com.mes.interaction;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mes.device.entity.DeviceConfig;
import com.mes.device.service.DevicePlcOperationService;
import com.mes.device.vo.DevicePlcVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
/**
 * è®¾å¤‡é€»è¾‘处理器基类
 * æä¾›é€šç”¨çš„功能实现
 *
 * @author mes
 * @since 2025-01-XX
 */
@Slf4j
@RequiredArgsConstructor
public abstract class BaseDeviceLogicHandler implements DeviceLogicHandler {
    protected final DevicePlcOperationService devicePlcOperationService;
    protected final ObjectMapper objectMapper = new ObjectMapper();
    @Override
    public DevicePlcVO.OperationResult execute(DeviceConfig deviceConfig, String operation, Map<String, Object> params) {
        try {
            // éªŒè¯è®¾å¤‡é…ç½®
            String validationError = validateLogicParams(deviceConfig);
            if (validationError != null) {
                return DevicePlcVO.OperationResult.builder()
                        .success(false)
                        .message("设备逻辑参数验证失败: " + validationError)
                        .build();
            }
            // è§£æžè®¾å¤‡é€»è¾‘参数(从 extraParams.deviceLogic ä¸­è¯»å–)
            Map<String, Object> logicParams = parseLogicParams(deviceConfig);
            // æ‰§è¡Œå…·ä½“操作(由子类实现)
            return doExecute(deviceConfig, operation, params, logicParams);
        } catch (Exception e) {
            log.error("执行设备逻辑操作失败, deviceId={}, operation={}", deviceConfig.getId(), operation, e);
            return DevicePlcVO.OperationResult.builder()
                    .success(false)
                    .message("执行失败: " + e.getMessage())
                    .build();
        }
    }
    /**
     * å­ç±»å®žçŽ°å…·ä½“çš„æ“ä½œé€»è¾‘
     *
     * @param deviceConfig è®¾å¤‡é…ç½®
     * @param operation æ“ä½œç±»åž‹
     * @param params è¿è¡Œæ—¶å‚数(动态传入)
     * @param logicParams é€»è¾‘配置参数(从 extraParams.deviceLogic è§£æžï¼‰
     * @return æ“ä½œç»“æžœ
     */
    protected abstract DevicePlcVO.OperationResult doExecute(
            DeviceConfig deviceConfig,
            String operation,
            Map<String, Object> params,
            Map<String, Object> logicParams
    );
    /**
     * è§£æžè®¾å¤‡é€»è¾‘参数(从 extraParams ä¸­æå– deviceLogic)
     */
    protected Map<String, Object> parseLogicParams(DeviceConfig deviceConfig) {
        String extraParams = deviceConfig.getExtraParams();
        if (extraParams == null || extraParams.trim().isEmpty()) {
            return new HashMap<>();
        }
        try {
            Map<String, Object> extra = objectMapper.readValue(extraParams, new TypeReference<Map<String, Object>>() {});
            @SuppressWarnings("unchecked")
            Map<String, Object> deviceLogic = (Map<String, Object>) extra.get("deviceLogic");
            return deviceLogic != null ? deviceLogic : new HashMap<>();
        } catch (Exception e) {
            log.warn("解析设备逻辑参数失败, deviceId={}", deviceConfig.getId(), e);
            return new HashMap<>();
        }
    }
    /**
     * èŽ·å–é€»è¾‘å‚æ•°ä¸­çš„å€¼ï¼ˆå¸¦é»˜è®¤å€¼ï¼‰
     */
    @SuppressWarnings("unchecked")
    protected <T> T getLogicParam(Map<String, Object> logicParams, String key, T defaultValue) {
        Object value = logicParams.get(key);
        if (value == null) {
            return defaultValue;
        }
        try {
            return (T) value;
        } catch (ClassCastException e) {
            log.warn("逻辑参数类型转换失败, key={}, value={}", key, value, e);
            return defaultValue;
        }
    }
    /**
     * èŽ·å–é€»è¾‘å‚æ•°ä¸­çš„å€¼ï¼ˆä¸å¸¦é»˜è®¤å€¼ï¼‰
     */
    @SuppressWarnings("unchecked")
    protected <T> T getLogicParam(Map<String, Object> logicParams, String key) {
        Object value = logicParams.get(key);
        if (value == null) {
            return null;
        }
        try {
            return (T) value;
        } catch (ClassCastException e) {
            log.warn("逻辑参数类型转换失败, key={}, value={}", key, value, e);
            return null;
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceInteraction.java
New file
@@ -0,0 +1,28 @@
package com.mes.interaction;
import com.mes.interaction.base.InteractionContext;
import com.mes.interaction.base.InteractionResult;
/**
 * å¤šè®¾å¤‡äº¤äº’接口
 */
public interface DeviceInteraction {
    /**
     * äº¤äº’对应的设备类型
     */
    String getDeviceType();
    /**
     * æ‰§è¡Œäº¤äº’逻辑
     */
    InteractionResult execute(InteractionContext context);
    /**
     * æ˜¯å¦æ”¯æŒæŒ‡å®šæ“ä½œ
     */
    default boolean supportsOperation(String operation) {
        return true;
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceInteractionRegistry.java
New file
@@ -0,0 +1,42 @@
package com.mes.interaction;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * äº¤äº’注册中心
 */
@Slf4j
@Component
public class DeviceInteractionRegistry {
    private final Map<String, DeviceInteraction> interactionMap = new HashMap<>();
    public DeviceInteractionRegistry(List<DeviceInteraction> interactions) {
        if (interactions != null) {
            for (DeviceInteraction interaction : interactions) {
                if (interaction.getDeviceType() != null) {
                    interactionMap.put(interaction.getDeviceType(), interaction);
                    log.info("注册设备交互: {}", interaction.getDeviceType());
                }
            }
        }
    }
    public DeviceInteraction getInteraction(String deviceType) {
        if (deviceType == null) {
            return null;
        }
        return interactionMap.get(deviceType);
    }
    public Map<String, DeviceInteraction> getInteractions() {
        return Collections.unmodifiableMap(interactionMap);
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceLogicHandler.java
New file
@@ -0,0 +1,49 @@
package com.mes.interaction;
import com.mes.device.entity.DeviceConfig;
import com.mes.device.vo.DevicePlcVO;
import java.util.Map;
/**
 * è®¾å¤‡é€»è¾‘处理器接口
 * ä¸åŒè®¾å¤‡ç±»åž‹å®žçŽ°æ­¤æŽ¥å£æ¥å¤„ç†å„è‡ªçš„ä¸šåŠ¡é€»è¾‘
 *
 * @author mes
 * @since 2025-01-XX
 */
public interface DeviceLogicHandler {
    /**
     * èŽ·å–è®¾å¤‡ç±»åž‹ï¼ˆç”¨äºŽåŒ¹é…å¤„ç†å™¨ï¼‰
     *
     * @return è®¾å¤‡ç±»åž‹ï¼Œå¦‚:"上大车"、"大理片"、"玻璃存储"
     */
    String getDeviceType();
    /**
     * æ‰§è¡Œè®¾å¤‡é€»è¾‘操作
     *
     * @param deviceConfig è®¾å¤‡é…ç½®ä¿¡æ¯
     * @param operation æ“ä½œç±»åž‹ï¼ˆå¦‚:feedGlass, triggerRequest, triggerReport等)
     * @param params æ“ä½œå‚数(运行时传入的动态参数)
     * @return æ“ä½œç»“æžœ
     */
    DevicePlcVO.OperationResult execute(DeviceConfig deviceConfig, String operation, Map<String, Object> params);
    /**
     * éªŒè¯è®¾å¤‡é€»è¾‘参数配置是否有效
     *
     * @param deviceConfig è®¾å¤‡é…ç½®
     * @return éªŒè¯ç»“果,null表示验证通过,否则返回错误信息
     */
    String validateLogicParams(DeviceConfig deviceConfig);
    /**
     * èŽ·å–è®¾å¤‡é€»è¾‘å‚æ•°çš„é»˜è®¤é…ç½®
     *
     * @return é»˜è®¤é…ç½®çš„JSON字符串
     */
    String getDefaultLogicParams();
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/DeviceLogicHandlerFactory.java
New file
@@ -0,0 +1,77 @@
package com.mes.interaction;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * è®¾å¤‡é€»è¾‘处理器工厂类
 * æ ¹æ®è®¾å¤‡ç±»åž‹èŽ·å–å¯¹åº”çš„å¤„ç†å™¨
 *
 * @author mes
 * @since 2025-01-XX
 */
@Slf4j
@Component
public class DeviceLogicHandlerFactory {
    @Autowired
    private List<DeviceLogicHandler> handlers;
    private final Map<String, DeviceLogicHandler> handlerMap = new HashMap<>();
    /**
     * åˆå§‹åŒ–处理器映射
     */
    @PostConstruct
    public void init() {
        if (handlers != null) {
            for (DeviceLogicHandler handler : handlers) {
                String deviceType = handler.getDeviceType();
                if (deviceType != null && !deviceType.isEmpty()) {
                    handlerMap.put(deviceType, handler);
                    log.info("注册设备逻辑处理器: {} -> {}", deviceType, handler.getClass().getSimpleName());
                }
            }
        }
        log.info("设备逻辑处理器初始化完成,共注册 {} ä¸ªå¤„理器", handlerMap.size());
    }
    /**
     * æ ¹æ®è®¾å¤‡ç±»åž‹èŽ·å–å¯¹åº”çš„å¤„ç†å™¨
     *
     * @param deviceType è®¾å¤‡ç±»åž‹
     * @return è®¾å¤‡é€»è¾‘处理器,如果未找到返回null
     */
    public DeviceLogicHandler getHandler(String deviceType) {
        if (deviceType == null || deviceType.isEmpty()) {
            return null;
        }
        return handlerMap.get(deviceType);
    }
    /**
     * æ£€æŸ¥æ˜¯å¦æ”¯æŒæŒ‡å®šçš„设备类型
     *
     * @param deviceType è®¾å¤‡ç±»åž‹
     * @return true表示支持,false表示不支持
     */
    public boolean supports(String deviceType) {
        return deviceType != null && handlerMap.containsKey(deviceType);
    }
    /**
     * èŽ·å–æ‰€æœ‰å·²æ³¨å†Œçš„è®¾å¤‡ç±»åž‹
     *
     * @return è®¾å¤‡ç±»åž‹é›†åˆ
     */
    public java.util.Set<String> getSupportedDeviceTypes() {
        return handlerMap.keySet();
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/README.md
New file
@@ -0,0 +1,168 @@
# è®¾å¤‡é€»è¾‘处理器架构说明
## æž¶æž„概述
采用**策略模式 + å·¥åŽ‚æ¨¡å¼**实现设备逻辑处理,支持不同设备类型的差异化逻辑处理。
## ç›®å½•结构
```
com.mes.interaction/
├── DeviceLogicHandler.java              # å¤„理器接口
├── BaseDeviceLogicHandler.java          # åŸºç¡€æŠ½è±¡ç±»
├── DeviceLogicHandlerFactory.java       # å·¥åŽ‚ç±»
└── impl/
    â”œâ”€â”€ LoadVehicleLogicHandler.java     # ä¸Šå¤§è½¦é€»è¾‘处理器
    â”œâ”€â”€ LargeGlassLogicHandler.java     # å¤§ç†ç‰‡é€»è¾‘处理器
    â””── GlassStorageLogicHandler.java   # çŽ»ç’ƒå­˜å‚¨é€»è¾‘å¤„ç†å™¨
```
## æ ¸å¿ƒæ¦‚念
### 1. å‚数配置(extraParams.deviceLogic)
设备逻辑参数存储在 `DeviceConfig.extraParams` çš„ `deviceLogic` èŠ‚ç‚¹ä¸­ï¼š
```json
{
  "connectionConfig": { ... },
  "plcConfig": { ... },
  "deviceLogic": {
    "vehicleCapacity": 6000,
    "glassIntervalMs": 1000,
    "autoFeed": true,
    "positionMapping": {
      "POS1": 1,
      "POS2": 2
    }
  }
}
```
### 2. æ‰§è¡Œæµç¨‹
```
用户调用接口
    â†“
Service å±‚获取设备配置(包含 extraParams)
    â†“
Factory æ ¹æ® deviceType é€‰æ‹©å¯¹åº”çš„ Handler
    â†“
Handler ä»Ž extraParams.deviceLogic è¯»å–配置参数
    â†“
Handler æ ¹æ®é…ç½®å‚数和运行时参数执行逻辑
    â†“
调用 DevicePlcOperationService.writeFields() å†™å…¥PLC
```
## ä½¿ç”¨æ–¹å¼
### æ–¹å¼1:通过 DeviceInteractionService(推荐)
```java
@Autowired
private DeviceInteractionService deviceInteractionService;
// æ‰§è¡Œæ“ä½œ
Map<String, Object> params = new HashMap<>();
params.put("glassIds", Arrays.asList("GLS001", "GLS002"));
params.put("positionCode", "POS1");
OperationResult result = deviceInteractionService.executeOperation(
    deviceId,
    "feedGlass",
    params
);
```
### æ–¹å¼2:直接使用 Handler
```java
@Autowired
private DeviceLogicHandlerFactory handlerFactory;
@Autowired
private DeviceConfigService deviceConfigService;
// èŽ·å–è®¾å¤‡å’Œå¤„ç†å™¨
DeviceConfig device = deviceConfigService.getDeviceById(deviceId);
DeviceLogicHandler handler = handlerFactory.getHandler(device.getDeviceType());
// æ‰§è¡Œæ“ä½œ
Map<String, Object> params = new HashMap<>();
params.put("glassIds", Arrays.asList("GLS001"));
OperationResult result = handler.execute(device, "feedGlass", params);
```
## æ”¯æŒçš„æ“ä½œç±»åž‹
### ä¸Šå¤§è½¦ï¼ˆLoadVehicleLogicHandler)
- `feedGlass` - çŽ»ç’ƒä¸Šæ–™
- `triggerRequest` - è§¦å‘请求
- `triggerReport` - è§¦å‘汇报
- `reset` - é‡ç½®
### å¤§ç†ç‰‡ï¼ˆLargeGlassLogicHandler)
- `processGlass` - çŽ»ç’ƒåŠ å·¥
- `triggerRequest` - è§¦å‘请求
- `triggerReport` - è§¦å‘汇报
- `reset` - é‡ç½®
### çŽ»ç’ƒå­˜å‚¨ï¼ˆGlassStorageLogicHandler)
- `storeGlass` - å­˜å‚¨çŽ»ç’ƒ
- `retrieveGlass` - å–è´§
- `triggerRequest` - è§¦å‘请求
- `triggerReport` - è§¦å‘汇报
- `reset` - é‡ç½®
## æ‰©å±•新设备类型
### æ­¥éª¤1:创建处理器类
```java
@Component
public class NewDeviceLogicHandler extends BaseDeviceLogicHandler {
    public NewDeviceLogicHandler(DevicePlcOperationService devicePlcOperationService) {
        super(devicePlcOperationService);
    }
    @Override
    public String getDeviceType() {
        return "新设备类型";
    }
    @Override
    protected OperationResult doExecute(...) {
        // å®žçŽ°å…·ä½“é€»è¾‘
    }
}
```
### æ­¥éª¤2:在 DeviceConfig.DeviceType ä¸­æ·»åŠ å¸¸é‡
```java
public static final String NEW_DEVICE = "新设备类型";
```
### æ­¥éª¤3:配置默认参数
实现 `getDefaultLogicParams()` æ–¹æ³•,返回默认的JSON配置。
## å‚数说明
### é…ç½®å‚数(从 extraParams.deviceLogic è¯»å–)
- é™æ€é…ç½®ï¼Œåœ¨è®¾å¤‡é…ç½®æ—¶è®¾ç½®
- å­˜å‚¨åœ¨æ•°æ®åº“中
- ç¤ºä¾‹ï¼švehicleCapacity, glassIntervalMs
### è¿è¡Œæ—¶å‚数(从方法参数传入)
- åŠ¨æ€å‚æ•°ï¼Œæ¯æ¬¡è°ƒç”¨æ—¶ä¼ å…¥
- ç¤ºä¾‹ï¼šglassIds, positionCode
## æ³¨æ„äº‹é¡¹
1. **参数验证**:每个 Handler å®žçް `validateLogicParams()` æ–¹æ³•进行参数验证
2. **默认值处理**:使用 `getLogicParam()` æ–¹æ³•获取参数,支持默认值
3. **错误处理**:所有异常都会被捕获并返回 `OperationResult`
4. **向后兼容**:保留了原有的 `feedGlass()` æ–¹æ³•,优先使用新架构,失败时降级到旧逻辑
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/base/InteractionContext.java
New file
@@ -0,0 +1,49 @@
package com.mes.interaction.base;
import com.mes.device.entity.DeviceConfig;
import com.mes.task.model.TaskExecutionContext;
import com.mes.task.dto.TaskParameters;
import lombok.Getter;
import java.util.List;
import java.util.Map;
/**
 * äº¤äº’上下文
 */
@Getter
public class InteractionContext {
    private final DeviceConfig currentDevice;
    private final TaskExecutionContext taskContext;
    public InteractionContext(DeviceConfig currentDevice, TaskExecutionContext taskContext) {
        this.currentDevice = currentDevice;
        this.taskContext = taskContext;
    }
    public TaskParameters getParameters() {
        return taskContext.getParameters();
    }
    public Map<String, Object> getSharedData() {
        return taskContext.getSharedData();
    }
    public void setLoadedGlassIds(List<String> glassIds) {
        taskContext.setLoadedGlassIds(glassIds);
    }
    public void setProcessedGlassIds(List<String> glassIds) {
        taskContext.setProcessedGlassIds(glassIds);
    }
    public List<String> getLoadedGlassIds() {
        return taskContext.getSafeLoadedGlassIds();
    }
    public List<String> getProcessedGlassIds() {
        return taskContext.getSafeProcessedGlassIds();
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/base/InteractionResult.java
New file
@@ -0,0 +1,60 @@
package com.mes.interaction.base;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
 * äº¤äº’执行结果
 */
@Getter
@Builder
@AllArgsConstructor
public class InteractionResult {
    public enum Status {
        SUCCESS, WAITING, FAILED
        }
    private final Status status;
    private final String message;
    @Builder.Default
    private final Map<String, Object> data = new HashMap<>();
    public static InteractionResult success(Map<String, Object> payload) {
        return InteractionResult.builder()
                .status(Status.SUCCESS)
                .message("success")
                .data(payload != null ? payload : Collections.emptyMap())
                .build();
    }
    public static InteractionResult waitResult(String message, Map<String, Object> payload) {
        return InteractionResult.builder()
                .status(Status.WAITING)
                .message(message)
                .data(payload != null ? payload : Collections.emptyMap())
                .build();
    }
    public static InteractionResult fail(String message) {
        return InteractionResult.builder()
                .status(Status.FAILED)
                .message(message)
                .data(Collections.emptyMap())
                .build();
    }
    public boolean isSuccess() {
        return status == Status.SUCCESS;
    }
    public boolean isWaiting() {
        return status == Status.WAITING;
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/GlassStorageInteraction.java
New file
@@ -0,0 +1,38 @@
package com.mes.interaction.flow;
import com.mes.device.entity.DeviceConfig;
import com.mes.interaction.DeviceInteraction;
import com.mes.interaction.base.InteractionContext;
import com.mes.interaction.base.InteractionResult;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * çŽ»ç’ƒå­˜å‚¨äº¤äº’å®žçŽ°
 */
@Component
public class GlassStorageInteraction implements DeviceInteraction {
    @Override
    public String getDeviceType() {
        return DeviceConfig.DeviceType.GLASS_STORAGE;
    }
    @Override
    public InteractionResult execute(InteractionContext context) {
        List<String> processed = context.getProcessedGlassIds();
        if (CollectionUtils.isEmpty(processed)) {
            return InteractionResult.waitResult("没有可存储的玻璃", null);
        }
        Map<String, Object> data = new HashMap<>();
        data.put("storedCount", processed.size());
        data.put("storedGlasses", processed);
        return InteractionResult.success(data);
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/LargeGlassInteraction.java
New file
@@ -0,0 +1,52 @@
package com.mes.interaction.flow;
import com.mes.device.entity.DeviceConfig;
import com.mes.interaction.DeviceInteraction;
import com.mes.interaction.base.InteractionContext;
import com.mes.interaction.base.InteractionResult;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * å¤§ç†ç‰‡äº¤äº’实现
 */
@Component
public class LargeGlassInteraction implements DeviceInteraction {
    @Override
    public String getDeviceType() {
        return DeviceConfig.DeviceType.LARGE_GLASS;
    }
    @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);
        }
        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")
    private List<String> castList(Object value) {
        if (value instanceof List) {
            return (List<String>) value;
        }
        return null;
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/flow/LoadVehicleInteraction.java
New file
@@ -0,0 +1,43 @@
package com.mes.interaction.flow;
import com.mes.device.entity.DeviceConfig;
import com.mes.interaction.DeviceInteraction;
import com.mes.interaction.base.InteractionContext;
import com.mes.interaction.base.InteractionResult;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
 * ä¸Šå¤§è½¦äº¤äº’实现
 */
@Component
public class LoadVehicleInteraction implements DeviceInteraction {
    @Override
    public String getDeviceType() {
        return DeviceConfig.DeviceType.LOAD_VEHICLE;
    }
    @Override
    public InteractionResult execute(InteractionContext context) {
        List<String> glassIds = context.getParameters().getGlassIds();
        if (CollectionUtils.isEmpty(glassIds)) {
            return InteractionResult.waitResult("未提供玻璃ID,等待输入", null);
        }
        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/GlassStorageLogicHandler.java
New file
@@ -0,0 +1,241 @@
package com.mes.interaction.impl;
import com.mes.device.entity.DeviceConfig;
import com.mes.interaction.BaseDeviceLogicHandler;
import com.mes.device.service.DevicePlcOperationService;
import com.mes.device.vo.DevicePlcVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
 * çŽ»ç’ƒå­˜å‚¨è®¾å¤‡é€»è¾‘å¤„ç†å™¨
 *
 * @author mes
 * @since 2025-01-XX
 */
@Slf4j
@Component
public class GlassStorageLogicHandler extends BaseDeviceLogicHandler {
    public GlassStorageLogicHandler(DevicePlcOperationService devicePlcOperationService) {
        super(devicePlcOperationService);
    }
    @Override
    public String getDeviceType() {
        return DeviceConfig.DeviceType.GLASS_STORAGE;
    }
    @Override
    protected DevicePlcVO.OperationResult doExecute(
            DeviceConfig deviceConfig,
            String operation,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        log.info("执行玻璃存储设备操作: deviceId={}, operation={}", deviceConfig.getId(), operation);
        switch (operation) {
            case "storeGlass":
                return handleStoreGlass(deviceConfig, params, logicParams);
            case "retrieveGlass":
                return handleRetrieveGlass(deviceConfig, params, logicParams);
            case "triggerRequest":
                return handleTriggerRequest(deviceConfig, params, logicParams);
            case "triggerReport":
                return handleTriggerReport(deviceConfig, params, logicParams);
            case "reset":
                return handleReset(deviceConfig, params, logicParams);
            default:
                log.warn("不支持的操作类型: {}", operation);
                return DevicePlcVO.OperationResult.builder()
                        .success(false)
                        .message("不支持的操作: " + operation)
                        .build();
        }
    }
    /**
     * å¤„理存储玻璃操作
     */
    private DevicePlcVO.OperationResult handleStoreGlass(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        // ä»Žé€»è¾‘参数中获取配置
        Integer storageCapacity = getLogicParam(logicParams, "storageCapacity", 100);
        String retrievalMode = getLogicParam(logicParams, "retrievalMode", "FIFO");
        Boolean autoStore = getLogicParam(logicParams, "autoStore", true);
        // ä»Žè¿è¡Œæ—¶å‚数中获取数据
        String glassId = (String) params.get("glassId");
        Integer storagePosition = (Integer) params.get("storagePosition");
        Boolean triggerRequest = (Boolean) params.getOrDefault("triggerRequest", autoStore);
        // æž„建写入数据
        Map<String, Object> payload = new HashMap<>();
        if (glassId != null) {
            payload.put("plcGlassId", glassId);
        }
        if (storagePosition != null) {
            payload.put("storagePosition", storagePosition);
        }
        // è‡ªåŠ¨è§¦å‘è¯·æ±‚
        if (triggerRequest != null && triggerRequest) {
            payload.put("plcRequest", 1);
        }
        log.info("玻璃存储: deviceId={}, glassId={}, position={}",
                deviceConfig.getId(), glassId, storagePosition);
        return devicePlcOperationService.writeFields(
                deviceConfig.getId(),
                payload,
                "玻璃存储-存储玻璃"
        );
    }
    /**
     * å¤„理取货操作
     */
    private DevicePlcVO.OperationResult handleRetrieveGlass(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        // ä»Žé€»è¾‘参数中获取配置
        String retrievalMode = getLogicParam(logicParams, "retrievalMode", "FIFO");
        Boolean autoRetrieve = getLogicParam(logicParams, "autoRetrieve", true);
        // ä»Žè¿è¡Œæ—¶å‚数中获取数据
        Integer storagePosition = (Integer) params.get("storagePosition");
        String glassId = (String) params.get("glassId");
        Boolean triggerRequest = (Boolean) params.getOrDefault("triggerRequest", autoRetrieve);
        // æž„建写入数据
        Map<String, Object> payload = new HashMap<>();
        if (storagePosition != null) {
            payload.put("retrievePosition", storagePosition);
        }
        if (glassId != null) {
            payload.put("retrieveGlassId", glassId);
        }
        // è‡ªåŠ¨è§¦å‘è¯·æ±‚
        if (triggerRequest != null && triggerRequest) {
            payload.put("plcRequest", 1);
        }
        log.info("玻璃取货: deviceId={}, position={}, glassId={}",
                deviceConfig.getId(), storagePosition, glassId);
        return devicePlcOperationService.writeFields(
                deviceConfig.getId(),
                payload,
                "玻璃存储-取货"
        );
    }
    /**
     * å¤„理触发请求操作
     */
    private DevicePlcVO.OperationResult handleTriggerRequest(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        Map<String, Object> payload = new HashMap<>();
        payload.put("plcRequest", 1);
        log.info("玻璃存储触发请求: deviceId={}", deviceConfig.getId());
        return devicePlcOperationService.writeFields(
                deviceConfig.getId(),
                payload,
                "玻璃存储-触发请求"
        );
    }
    /**
     * å¤„理触发汇报操作
     */
    private DevicePlcVO.OperationResult handleTriggerReport(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        Map<String, Object> payload = new HashMap<>();
        payload.put("plcReport", 1);
        log.info("玻璃存储触发汇报: deviceId={}", deviceConfig.getId());
        return devicePlcOperationService.writeFields(
                deviceConfig.getId(),
                payload,
                "玻璃存储-触发汇报"
        );
    }
    /**
     * å¤„理重置操作
     */
    private DevicePlcVO.OperationResult handleReset(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        Map<String, Object> payload = new HashMap<>();
        payload.put("plcRequest", 0);
        payload.put("plcReport", 0);
        log.info("玻璃存储重置: deviceId={}", deviceConfig.getId());
        return devicePlcOperationService.writeFields(
                deviceConfig.getId(),
                payload,
                "玻璃存储-重置"
        );
    }
    @Override
    public String validateLogicParams(DeviceConfig deviceConfig) {
        Map<String, Object> logicParams = parseLogicParams(deviceConfig);
        // éªŒè¯å¿…填参数
        Integer storageCapacity = getLogicParam(logicParams, "storageCapacity", null);
        if (storageCapacity != null && storageCapacity <= 0) {
            return "存储容量(storageCapacity)必须大于0";
        }
        String retrievalMode = getLogicParam(logicParams, "retrievalMode", null);
        if (retrievalMode != null && !retrievalMode.matches("FIFO|LIFO|RANDOM")) {
            return "取货模式(retrievalMode)必须是FIFO、LIFO或RANDOM";
        }
        return null; // éªŒè¯é€šè¿‡
    }
    @Override
    public String getDefaultLogicParams() {
        Map<String, Object> defaultParams = new HashMap<>();
        defaultParams.put("storageCapacity", 100);
        defaultParams.put("retrievalMode", "FIFO");
        defaultParams.put("autoStore", true);
        defaultParams.put("autoRetrieve", true);
        defaultParams.put("maxRetryCount", 3);
        try {
            return objectMapper.writeValueAsString(defaultParams);
        } catch (Exception e) {
            log.error("生成默认逻辑参数失败", e);
            return "{}";
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/impl/LargeGlassLogicHandler.java
New file
@@ -0,0 +1,195 @@
package com.mes.interaction.impl;
import com.mes.device.entity.DeviceConfig;
import com.mes.interaction.BaseDeviceLogicHandler;
import com.mes.device.service.DevicePlcOperationService;
import com.mes.device.vo.DevicePlcVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
 * å¤§ç†ç‰‡è®¾å¤‡é€»è¾‘处理器
 *
 * @author mes
 * @since 2025-01-XX
 */
@Slf4j
@Component
public class LargeGlassLogicHandler extends BaseDeviceLogicHandler {
    public LargeGlassLogicHandler(DevicePlcOperationService devicePlcOperationService) {
        super(devicePlcOperationService);
    }
    @Override
    public String getDeviceType() {
        return DeviceConfig.DeviceType.LARGE_GLASS;
    }
    @Override
    protected DevicePlcVO.OperationResult doExecute(
            DeviceConfig deviceConfig,
            String operation,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        log.info("执行大理片设备操作: deviceId={}, operation={}", deviceConfig.getId(), operation);
        switch (operation) {
            case "processGlass":
                return handleProcessGlass(deviceConfig, params, logicParams);
            case "triggerRequest":
                return handleTriggerRequest(deviceConfig, params, logicParams);
            case "triggerReport":
                return handleTriggerReport(deviceConfig, params, logicParams);
            case "reset":
                return handleReset(deviceConfig, params, logicParams);
            default:
                log.warn("不支持的操作类型: {}", operation);
                return DevicePlcVO.OperationResult.builder()
                        .success(false)
                        .message("不支持的操作: " + operation)
                        .build();
        }
    }
    /**
     * å¤„理玻璃加工操作
     */
    private DevicePlcVO.OperationResult handleProcessGlass(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        // ä»Žé€»è¾‘参数中获取配置
        Integer glassSize = getLogicParam(logicParams, "glassSize", 2000);
        Integer processingTime = getLogicParam(logicParams, "processingTime", 5000);
        Boolean autoProcess = getLogicParam(logicParams, "autoProcess", true);
        // ä»Žè¿è¡Œæ—¶å‚数中获取数据
        String glassId = (String) params.get("glassId");
        Integer processType = (Integer) params.get("processType");
        Boolean triggerRequest = (Boolean) params.getOrDefault("triggerRequest", autoProcess);
        // æž„建写入数据
        Map<String, Object> payload = new HashMap<>();
        if (glassId != null) {
            payload.put("plcGlassId", glassId);
        }
        if (processType != null) {
            payload.put("processType", processType);
        }
        // è‡ªåŠ¨è§¦å‘è¯·æ±‚
        if (triggerRequest != null && triggerRequest) {
            payload.put("plcRequest", 1);
        }
        log.info("大理片玻璃加工: deviceId={}, glassId={}, processType={}",
                deviceConfig.getId(), glassId, processType);
        return devicePlcOperationService.writeFields(
                deviceConfig.getId(),
                payload,
                "大理片-玻璃加工"
        );
    }
    /**
     * å¤„理触发请求操作
     */
    private DevicePlcVO.OperationResult handleTriggerRequest(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        Map<String, Object> payload = new HashMap<>();
        payload.put("plcRequest", 1);
        log.info("大理片触发请求: deviceId={}", deviceConfig.getId());
        return devicePlcOperationService.writeFields(
                deviceConfig.getId(),
                payload,
                "大理片-触发请求"
        );
    }
    /**
     * å¤„理触发汇报操作
     */
    private DevicePlcVO.OperationResult handleTriggerReport(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        Map<String, Object> payload = new HashMap<>();
        payload.put("plcReport", 1);
        log.info("大理片触发汇报: deviceId={}", deviceConfig.getId());
        return devicePlcOperationService.writeFields(
                deviceConfig.getId(),
                payload,
                "大理片-触发汇报"
        );
    }
    /**
     * å¤„理重置操作
     */
    private DevicePlcVO.OperationResult handleReset(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        Map<String, Object> payload = new HashMap<>();
        payload.put("plcRequest", 0);
        payload.put("plcReport", 0);
        log.info("大理片重置: deviceId={}", deviceConfig.getId());
        return devicePlcOperationService.writeFields(
                deviceConfig.getId(),
                payload,
                "大理片-重置"
        );
    }
    @Override
    public String validateLogicParams(DeviceConfig deviceConfig) {
        Map<String, Object> logicParams = parseLogicParams(deviceConfig);
        // éªŒè¯å¿…填参数
        Integer glassSize = getLogicParam(logicParams, "glassSize", null);
        if (glassSize != null && glassSize <= 0) {
            return "玻璃尺寸(glassSize)必须大于0";
        }
        Integer processingTime = getLogicParam(logicParams, "processingTime", null);
        if (processingTime != null && processingTime < 0) {
            return "处理时间(processingTime)不能为负数";
        }
        return null; // éªŒè¯é€šè¿‡
    }
    @Override
    public String getDefaultLogicParams() {
        Map<String, Object> defaultParams = new HashMap<>();
        defaultParams.put("glassSize", 2000);
        defaultParams.put("processingTime", 5000);
        defaultParams.put("autoProcess", true);
        defaultParams.put("maxRetryCount", 3);
        try {
            return objectMapper.writeValueAsString(defaultParams);
        } catch (Exception e) {
            log.error("生成默认逻辑参数失败", e);
            return "{}";
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/impl/LoadVehicleLogicHandler.java
New file
@@ -0,0 +1,372 @@
package com.mes.interaction.impl;
import com.mes.device.entity.DeviceConfig;
import com.mes.interaction.BaseDeviceLogicHandler;
import com.mes.device.service.DevicePlcOperationService;
import com.mes.device.vo.DevicePlcVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
 * ä¸Šå¤§è½¦è®¾å¤‡é€»è¾‘处理器
 *
 * @author mes
 * @since 2025-01-XX
 */
@Slf4j
@Component
public class LoadVehicleLogicHandler extends BaseDeviceLogicHandler {
    public LoadVehicleLogicHandler(DevicePlcOperationService devicePlcOperationService) {
        super(devicePlcOperationService);
    }
    @Override
    public String getDeviceType() {
        return DeviceConfig.DeviceType.LOAD_VEHICLE;
    }
    @Override
    protected DevicePlcVO.OperationResult doExecute(
            DeviceConfig deviceConfig,
            String operation,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        log.info("执行上大车设备操作: deviceId={}, operation={}", deviceConfig.getId(), operation);
        switch (operation) {
            case "feedGlass":
                return handleFeedGlass(deviceConfig, params, logicParams);
            case "triggerRequest":
                return handleTriggerRequest(deviceConfig, params, logicParams);
            case "triggerReport":
                return handleTriggerReport(deviceConfig, params, logicParams);
            case "reset":
                return handleReset(deviceConfig, params, logicParams);
            default:
                log.warn("不支持的操作类型: {}", operation);
                return DevicePlcVO.OperationResult.builder()
                        .success(false)
                        .message("不支持的操作: " + operation)
                        .build();
        }
    }
    /**
     * å¤„理玻璃上料操作
     */
    private DevicePlcVO.OperationResult handleFeedGlass(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        // ä»Žé€»è¾‘参数中获取配置(从 extraParams.deviceLogic è¯»å–)
        Integer vehicleCapacity = getLogicParam(logicParams, "vehicleCapacity", 6000);
        Integer glassIntervalMs = getLogicParam(logicParams, "glassIntervalMs", 1000);
        Boolean autoFeed = getLogicParam(logicParams, "autoFeed", true);
        Integer maxRetryCount = getLogicParam(logicParams, "maxRetryCount", 5);
        // ä»Žè¿è¡Œæ—¶å‚数中获取数据(从接口调用时传入)
        List<GlassInfo> glassInfos = extractGlassInfos(params);
        if (glassInfos.isEmpty()) {
            return DevicePlcVO.OperationResult.builder()
                    .success(false)
                    .message("未提供有效的玻璃信息")
                    .build();
        }
        String positionCode = (String) params.get("positionCode");
        Integer positionValue = (Integer) params.get("positionValue");
        Boolean triggerRequest = (Boolean) params.getOrDefault("triggerRequest", autoFeed);
        List<GlassInfo> plannedGlasses = planGlassLoading(glassInfos, vehicleCapacity,
                getLogicParam(logicParams, "defaultGlassLength", 2000));
        if (plannedGlasses.isEmpty()) {
            return DevicePlcVO.OperationResult.builder()
                    .success(false)
                    .message("当前玻璃尺寸超出车辆容量,无法装载")
                    .build();
        }
        // æž„建写入数据
        Map<String, Object> payload = new HashMap<>();
        // å†™å…¥çŽ»ç’ƒID
        int plcSlots = Math.min(plannedGlasses.size(), 6);
        for (int i = 0; i < plcSlots; i++) {
            String fieldName = "plcGlassId" + (i + 1);
            payload.put(fieldName, plannedGlasses.get(i).getGlassId());
        }
        payload.put("plcGlassCount", plcSlots);
        // å†™å…¥ä½ç½®ä¿¡æ¯
        if (positionValue != null) {
            payload.put("inPosition", positionValue);
        } else if (positionCode != null) {
            // ä»Žä½ç½®æ˜ å°„中获取位置值
            @SuppressWarnings("unchecked")
            Map<String, Integer> positionMapping = getLogicParam(logicParams, "positionMapping", new HashMap<>());
            Integer mappedValue = positionMapping.get(positionCode);
            if (mappedValue != null) {
                payload.put("inPosition", mappedValue);
            }
        }
        // è‡ªåŠ¨è§¦å‘è¯·æ±‚å­—
        if (triggerRequest != null && triggerRequest) {
            payload.put("plcRequest", 1);
        }
        String operationName = "上大车-玻璃上料";
        if (positionCode != null) {
            operationName += "(" + positionCode + ")";
        }
        log.info("上大车玻璃上料: deviceId={}, glassCount={}, position={}, plannedGlassIds={}",
                deviceConfig.getId(), plcSlots, positionCode, plannedGlasses);
        if (glassIntervalMs != null && glassIntervalMs > 0) {
            try {
                Thread.sleep(glassIntervalMs);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        return devicePlcOperationService.writeFields(deviceConfig.getId(), payload, operationName);
    }
    /**
     * å¤„理触发请求操作
     */
    private DevicePlcVO.OperationResult handleTriggerRequest(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        Map<String, Object> payload = new HashMap<>();
        payload.put("plcRequest", 1);
        log.info("上大车触发请求: deviceId={}", deviceConfig.getId());
        return devicePlcOperationService.writeFields(
                deviceConfig.getId(),
                payload,
                "上大车-触发请求"
        );
    }
    /**
     * å¤„理触发汇报操作
     */
    private DevicePlcVO.OperationResult handleTriggerReport(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        Map<String, Object> payload = new HashMap<>();
        payload.put("plcReport", 1);
        log.info("上大车触发汇报: deviceId={}", deviceConfig.getId());
        return devicePlcOperationService.writeFields(
                deviceConfig.getId(),
                payload,
                "上大车-触发汇报"
        );
    }
    /**
     * å¤„理重置操作
     */
    private DevicePlcVO.OperationResult handleReset(
            DeviceConfig deviceConfig,
            Map<String, Object> params,
            Map<String, Object> logicParams) {
        Map<String, Object> payload = new HashMap<>();
        payload.put("plcRequest", 0);
        payload.put("plcReport", 0);
        log.info("上大车重置: deviceId={}", deviceConfig.getId());
        return devicePlcOperationService.writeFields(
                deviceConfig.getId(),
                payload,
                "上大车-重置"
        );
    }
    @Override
    public String validateLogicParams(DeviceConfig deviceConfig) {
        Map<String, Object> logicParams = parseLogicParams(deviceConfig);
        // éªŒè¯å¿…填参数
        Integer vehicleCapacity = getLogicParam(logicParams, "vehicleCapacity", null);
        if (vehicleCapacity == null || vehicleCapacity <= 0) {
            return "车辆容量(vehicleCapacity)必须大于0";
        }
        Integer glassIntervalMs = getLogicParam(logicParams, "glassIntervalMs", null);
        if (glassIntervalMs != null && glassIntervalMs < 0) {
            return "玻璃间隔时间(glassIntervalMs)不能为负数";
        }
        return null; // éªŒè¯é€šè¿‡
    }
    @Override
    public String getDefaultLogicParams() {
        Map<String, Object> defaultParams = new HashMap<>();
        defaultParams.put("vehicleCapacity", 6000);
        defaultParams.put("glassIntervalMs", 1000);
        defaultParams.put("autoFeed", true);
        defaultParams.put("maxRetryCount", 5);
        defaultParams.put("defaultGlassLength", 2000);
        Map<String, Integer> positionMapping = new HashMap<>();
        positionMapping.put("POS1", 1);
        positionMapping.put("POS2", 2);
        defaultParams.put("positionMapping", positionMapping);
        try {
            return objectMapper.writeValueAsString(defaultParams);
        } catch (Exception e) {
            log.error("生成默认逻辑参数失败", e);
            return "{}";
        }
    }
    @SuppressWarnings("unchecked")
    private List<GlassInfo> extractGlassInfos(Map<String, Object> params) {
        List<GlassInfo> result = new ArrayList<>();
        Object rawGlassInfos = params.get("glassInfos");
        if (rawGlassInfos instanceof List) {
            List<?> list = (List<?>) rawGlassInfos;
            for (Object item : list) {
                GlassInfo info = convertToGlassInfo(item);
                if (info != null) {
                    result.add(info);
                }
            }
        }
        if (result.isEmpty()) {
            List<String> glassIds = (List<String>) params.get("glassIds");
            if (glassIds != null) {
                for (String glassId : glassIds) {
                    result.add(new GlassInfo(glassId, null));
                }
            }
        }
        return result;
    }
    private GlassInfo convertToGlassInfo(Object source) {
        if (source instanceof GlassInfo) {
            return (GlassInfo) source;
        }
        if (source instanceof Map) {
            Map<?, ?> map = (Map<?, ?>) source;
            Object id = map.get("glassId");
            if (id == null) {
                id = map.get("id");
            }
            if (id == null) {
                return null;
            }
            Integer length = parseLength(map.get("length"));
            if (length == null) {
                length = parseLength(map.get("size"));
            }
            return new GlassInfo(String.valueOf(id), length);
        }
        if (source instanceof String) {
            return new GlassInfo((String) source, null);
        }
        return null;
    }
    private Integer parseLength(Object value) {
        if (value instanceof Number) {
            return ((Number) value).intValue();
        }
        if (value instanceof String) {
            try {
                return Integer.parseInt((String) value);
            } catch (NumberFormatException ignored) {
            }
        }
        return null;
    }
    private List<GlassInfo> planGlassLoading(List<GlassInfo> source,
                                             int vehicleCapacity,
                                             Integer defaultGlassLength) {
        List<GlassInfo> planned = new ArrayList<>();
        int usedLength = 0;
        int capacity = Math.max(vehicleCapacity, 1);
        int fallbackLength = defaultGlassLength != null && defaultGlassLength > 0 ? defaultGlassLength : 2000;
        for (GlassInfo info : source) {
            int length = info.getLength() != null && info.getLength() > 0 ? info.getLength() : fallbackLength;
            if (planned.isEmpty()) {
                planned.add(info.withLength(length));
                usedLength = length;
                continue;
            }
            if (usedLength + length <= capacity) {
                planned.add(info.withLength(length));
                usedLength += length;
            } else {
                break;
            }
        }
        return planned;
    }
    private static class GlassInfo {
        private final String glassId;
        private final Integer length;
        GlassInfo(String glassId, Integer length) {
            this.glassId = glassId;
            this.length = length;
        }
        public String getGlassId() {
            return glassId;
        }
        public Integer getLength() {
            return length;
        }
        public GlassInfo withLength(Integer newLength) {
            return new GlassInfo(this.glassId, newLength);
        }
        @Override
        public String toString() {
            return glassId;
        }
        @Override
        public int hashCode() {
            return Objects.hash(glassId, length);
        }
        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null || getClass() != obj.getClass()) return false;
            GlassInfo other = (GlassInfo) obj;
            return Objects.equals(glassId, other.glassId) && Objects.equals(length, other.length);
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/service/PlcDynamicDataService.java
@@ -3,6 +3,7 @@
import com.alibaba.fastjson.JSONObject;
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;
@@ -66,4 +67,52 @@
     * @param s7Serializer S7序列化器
     */
    void writePlcField(PlcAddress config, String fieldName, Object value, EnhancedS7Serializer s7Serializer);
    /**
     * æ ¹æ®DeviceConfig配置和字段名称读取PLC数据
     *
     * @param device è®¾å¤‡é…ç½®
     * @param fieldNames è¦è¯»å–的字段名称列表
     * @param s7Serializer S7序列化器
     * @return å­—段名->值 çš„Map
     */
    Map<String, Object> readPlcData(DeviceConfig device, List<String> fieldNames, EnhancedS7Serializer s7Serializer);
    /**
     * æ ¹æ®DeviceConfig配置和数据Map写入PLC
     *
     * @param device è®¾å¤‡é…ç½®
     * @param dataMap å­—段名->值 çš„Map
     * @param s7Serializer S7序列化器
     */
    void writePlcData(DeviceConfig device, Map<String, Object> dataMap, EnhancedS7Serializer s7Serializer);
    /**
     * è¯»å–PLC所有字段(基于DeviceConfig)
     *
     * @param device è®¾å¤‡é…ç½®
     * @param s7Serializer S7序列化器
     * @return æ‰€æœ‰å­—段的值
     */
    Map<String, Object> readAllPlcData(DeviceConfig device, EnhancedS7Serializer s7Serializer);
    /**
     * è¯»å–单个字段(基于DeviceConfig)
     *
     * @param device è®¾å¤‡é…ç½®
     * @param fieldName å­—段名
     * @param s7Serializer S7序列化器
     * @return å­—段值
     */
    Object readPlcField(DeviceConfig device, String fieldName, EnhancedS7Serializer s7Serializer);
    /**
     * å†™å…¥å•个字段(基于DeviceConfig)
     *
     * @param device è®¾å¤‡é…ç½®
     * @param fieldName å­—段名
     * @param value å­—段值
     * @param s7Serializer S7序列化器
     */
    void writePlcField(DeviceConfig device, String fieldName, Object value, EnhancedS7Serializer s7Serializer);
}
mes-processes/mes-plcSend/src/main/java/com/mes/service/PlcTestWriteService.java
@@ -1,14 +1,22 @@
package com.mes.service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.xingshuangs.iot.protocol.s7.enums.EPlcType;
import com.github.xingshuangs.iot.protocol.s7.service.S7PLC;
import com.mes.device.entity.DeviceConfig;
import com.mes.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;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -25,6 +33,15 @@
    @Resource
    private PlcAddressService plcAddressService;
    @Resource
    private DeviceConfigService deviceConfigService;
    @Resource
    private PlcDynamicDataService plcDynamicDataService;
    private final ObjectMapper objectMapper = new ObjectMapper();
    private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<Map<String, Object>>() {};
    private static final int ON = 1;
    private static final int OFF = 0;
@@ -47,33 +64,43 @@
     */
    public boolean simulatePlcRequest(String projectId) {
        try {
            // èŽ·å–é¡¹ç›®é…ç½®ï¼ˆæ•°æ®åº“å®žä½“ï¼‰
            PlcAddress config = plcAddressService.getProjectConfigWithMapping(projectId);
            // èŽ·å–å¯¹åº”çš„S7Serializer
            EnhancedS7Serializer s7Serializer = getSerializerForProject(projectId, config);
            // è¯»å–当前PLC状态
            PlcBaseData currentData = s7Serializer.read(PlcBaseData.class, config.getDbArea(), config.getBeginIndex());
            if (currentData.getOnlineState() == OFF) {
                log.info("当前PLC联机模式为0,停止联机");
            if (config == null) {
                log.error("项目配置不存在: projectId={}", projectId);
                return false;
            }else if (currentData.getPlcReport() == ON){
                log.info("当前上片PLC汇报字为1,重置为0");
                currentData.setPlcReport(OFF);
            }
            // è®¾ç½®PLC请求字为1(触发MES任务处理)
            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;
            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;
    }
    /**
@@ -90,29 +117,41 @@
        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;
            }
            
            PlcBaseData currentData = s7Serializer.read(PlcBaseData.class, config.getDbArea(), config.getBeginIndex());
            // è®¾ç½®PLC汇报字为1(任务完成)
            currentData.setPlcReport(ON);
            // è¯·æ±‚字清0
            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;
            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;
    }
    /**
@@ -129,24 +168,38 @@
        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;
            }
            
            PlcBaseData currentData = s7Serializer.read(PlcBaseData.class, config.getDbArea(), config.getBeginIndex());
            // 1:联机 0:脱机
            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;
            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;
    }
    /**
@@ -163,32 +216,37 @@
        try {
            // èŽ·å–é¡¹ç›®é…ç½®ï¼ˆæ•°æ®åº“å®žä½“ï¼‰
            PlcAddress config = plcAddressService.getProjectConfigWithMapping(projectId);
            if (config == null) {
                log.error("项目配置不存在: projectId={}", projectId);
                return false;
            }
            
            // èŽ·å–å¯¹åº”çš„S7Serializer
            EnhancedS7Serializer s7Serializer = getSerializerForProject(projectId, config);
            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;
            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;
    }
    /**
@@ -205,15 +263,31 @@
        try {
            // èŽ·å–é¡¹ç›®é…ç½®ï¼ˆæ•°æ®åº“å®žä½“ï¼‰
            PlcAddress config = plcAddressService.getProjectConfigWithMapping(projectId);
            if (config == null) {
                log.error("项目配置不存在: projectId={}", projectId);
                return null;
            }
            
            // èŽ·å–å¯¹åº”çš„S7Serializer
            EnhancedS7Serializer s7Serializer = getSerializerForProject(projectId, config);
            if (s7Serializer == null) {
                log.error("无法创建S7Serializer: projectId={}", projectId);
                return null;
            }
            
            return s7Serializer.read(PlcBaseData.class, config.getDbArea(), config.getBeginIndex());
            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;
    }
    
    /**
@@ -276,4 +350,300 @@
        serializerCache.clear();
        log.info("已清除所有S7Serializer缓存");
    }
    /**
     * æ ¹æ®è®¾å¤‡ID模拟PLC发送请求字
     *
     * @param deviceId è®¾å¤‡ID
     * @return æ˜¯å¦æˆåŠŸ
     */
    public boolean simulatePlcRequestByDevice(Long deviceId) {
        DeviceConfig device = deviceConfigService.getDeviceById(deviceId);
        if (device == null) {
            log.error("设备不存在: deviceId={}", deviceId);
            return false;
        }
        try {
            String projectId = resolveProjectId(device);
            PlcAddress config = buildPlcAddressFromDevice(device);
            EnhancedS7Serializer s7Serializer = getSerializerForDevice(device);
            return simulatePlcRequestInternal(projectId, config, s7Serializer);
        } catch (Exception e) {
            log.error("根据设备模拟PLC请求字失败: deviceId={}", deviceId, e);
            return false;
        }
    }
    /**
     * æ ¹æ®è®¾å¤‡ID模拟PLC任务完成汇报
     *
     * @param deviceId è®¾å¤‡ID
     * @return æ˜¯å¦æˆåŠŸ
     */
    public boolean simulatePlcReportByDevice(Long deviceId) {
        DeviceConfig device = deviceConfigService.getDeviceById(deviceId);
        if (device == null) {
            log.error("设备不存在: deviceId={}", deviceId);
            return false;
        }
        try {
            String projectId = resolveProjectId(device);
            PlcAddress config = buildPlcAddressFromDevice(device);
            EnhancedS7Serializer s7Serializer = getSerializerForDevice(device);
            return simulatePlcReportInternal(projectId, config, s7Serializer);
        } catch (Exception e) {
            log.error("根据设备模拟PLC汇报失败: deviceId={}", deviceId, e);
            return false;
        }
    }
    /**
     * æ ¹æ®è®¾å¤‡ID重置PLC所有状态
     *
     * @param deviceId è®¾å¤‡ID
     * @return æ˜¯å¦æˆåŠŸ
     */
    public boolean resetPlcByDevice(Long deviceId) {
        DeviceConfig device = deviceConfigService.getDeviceById(deviceId);
        if (device == null) {
            log.error("设备不存在: deviceId={}", deviceId);
            return false;
        }
        try {
            String projectId = resolveProjectId(device);
            PlcAddress config = buildPlcAddressFromDevice(device);
            EnhancedS7Serializer s7Serializer = getSerializerForDevice(device);
            return resetPlcInternal(projectId, config, s7Serializer);
        } catch (Exception e) {
            log.error("根据设备重置PLC状态失败: deviceId={}", deviceId, e);
            return false;
        }
    }
    /**
     * æ ¹æ®è®¾å¤‡ID读取PLC当前状态
     *
     * @param deviceId è®¾å¤‡ID
     * @return PLC状态数据
     */
    public Map<String, Object> readPlcStatusByDevice(Long deviceId) {
        DeviceConfig device = deviceConfigService.getDeviceById(deviceId);
        if (device == null) {
            log.error("设备不存在: deviceId={}", deviceId);
            return null;
        }
        try {
            String projectId = resolveProjectId(device);
            PlcAddress config = buildPlcAddressFromDevice(device);
            EnhancedS7Serializer s7Serializer = getSerializerForDevice(device);
            PlcBaseData data = readPlcStatusInternal(projectId, config, s7Serializer);
            if (data == null) {
                return null;
            }
            String json = objectMapper.writeValueAsString(data);
            return objectMapper.readValue(json, MAP_TYPE);
        } catch (Exception e) {
            log.error("读取设备PLC状态失败: deviceId={}", deviceId, e);
            return null;
        }
    }
    /**
     * æ ¹æ®è®¾å¤‡ID写入PLC字段
     *
     * @param deviceId è®¾å¤‡ID
     * @param fieldValues å­—段名->值 çš„Map
     * @return æ˜¯å¦æˆåŠŸ
     */
    public boolean writeFieldsByDevice(Long deviceId, Map<String, Object> fieldValues) {
        DeviceConfig device = deviceConfigService.getDeviceById(deviceId);
        if (device == null) {
            log.error("设备不存在: deviceId={}", deviceId);
            return false;
        }
        try {
            // ä»Žè®¾å¤‡é…ç½®ä¸­èŽ·å–é¡¹ç›®æ ‡è¯†
            String projectId = resolveProjectId(device);
            // èŽ·å–å¯¹åº”çš„S7Serializer(使用设备配置)
            EnhancedS7Serializer s7Serializer = getSerializerForDevice(device);
            // ä½¿ç”¨åŠ¨æ€æ•°æ®æœåŠ¡å†™å…¥å­—æ®µï¼ˆåŸºäºŽDeviceConfig)
            plcDynamicDataService.writePlcData(device, fieldValues, s7Serializer);
            log.info("写入PLC字段成功: deviceId={}, projectId={}, fields={}", deviceId, projectId, fieldValues.keySet());
            return true;
        } catch (Exception e) {
            log.error("写入PLC字段失败: deviceId={}", deviceId, e);
            return false;
        }
    }
    /**
     * èŽ·å–è®¾å¤‡å¯¹åº”çš„S7Serializer实例
     *
     * @param device è®¾å¤‡é…ç½®
     * @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());
            } 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实例
            return EnhancedS7Serializer.newInstance(s7Plc);
        });
    }
    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()) {
            return Collections.emptyMap();
        }
        try {
            return objectMapper.readValue(device.getExtraParams(), MAP_TYPE);
        } catch (Exception e) {
            log.warn("解析设备extraParams失败, deviceId={}", device.getId(), e);
            return Collections.emptyMap();
        }
    }
    @SuppressWarnings("unchecked")
    private Map<String, Object> getPlcConfigParams(DeviceConfig device) {
        Map<String, Object> extraParams = parseExtraParams(device);
        Object plcConfig = extraParams.get("plcConfig");
        if (plcConfig instanceof Map) {
            return (Map<String, Object>) plcConfig;
        }
        if (plcConfig instanceof String) {
            try {
                return objectMapper.readValue((String) plcConfig, MAP_TYPE);
            } catch (Exception e) {
                log.warn("解析extraParams.plcConfig失败, deviceId={}", device.getId(), e);
            }
        }
        return Collections.emptyMap();
    }
    private int parseInteger(Object value) {
        if (value instanceof Number) {
            return ((Number) value).intValue();
        }
        try {
            return Integer.parseInt(String.valueOf(value));
        } catch (NumberFormatException ex) {
            log.warn("无法解析整型值: {}", value);
            return 0;
        }
    }
    /**
     * ä»Žè®¾å¤‡é…ç½®ä¸­è§£æžé¡¹ç›®æ ‡è¯†
     *
     * @param device è®¾å¤‡é…ç½®
     * @return é¡¹ç›®æ ‡è¯†
     */
    private String resolveProjectId(DeviceConfig device) {
        if (device == null) {
            throw new IllegalArgumentException("设备信息为空");
        }
        // 1. ä¼˜å…ˆä½¿ç”¨å®žä½“上的projectId
        if (device.getProjectId() != null) {
            return String.valueOf(device.getProjectId());
        }
        // 2. ä»ŽextraParams中读取
        Map<String, Object> extraParams = parseExtraParams(device);
        Object plcProjectId = extraParams.get("plcProjectId");
        if (plcProjectId != null) {
            return String.valueOf(plcProjectId);
        }
        // 3. å…¼å®¹æ—§ç»“构:configJson或extraParams内嵌
        Map<String, Object> configParams = ConfigJsonHelper.parseToMap(device.getConfigJson(), objectMapper);
        Object legacyProjectId = configParams.get("plcProjectId");
        if (legacyProjectId != null) {
            return String.valueOf(legacyProjectId);
        }
        // æœ€åŽä½¿ç”¨è®¾å¤‡ç¼–号
        if (device.getDeviceCode() != null && !device.getDeviceCode().isEmpty()) {
            return device.getDeviceCode();
        }
        throw new IllegalStateException("无法解析设备的PLC项目标识, deviceId=" + device.getId());
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/service/impl/PlcAutoTestServiceImpl.java
@@ -1,7 +1,7 @@
package com.mes.service.impl;
import com.mes.service.IPlcAutoTestService;
import com.mes.service.IPlcTestWriteService;
import com.mes.service.PlcAutoTestService;
import com.mes.service.PlcTestWriteService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
@@ -18,10 +18,10 @@
 */
@Slf4j
@Service
public class PlcAutoTestServiceImpl implements PlcAutoTestService {
public class PlcAutoTestServiceImpl extends PlcAutoTestService {
    @Resource
    private IPlcTestWriteService plcTestWriteService;
    private PlcTestWriteService plcTestWriteService;
    // è‡ªåŠ¨æµ‹è¯•å¼€å…³
    @Value("${plc.auto.test.enabled:false}")
mes-processes/mes-plcSend/src/main/java/com/mes/service/impl/PlcDynamicDataServiceImpl.java
@@ -1,8 +1,12 @@
package com.mes.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
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.device.util.ConfigJsonHelper;
import com.mes.entity.PlcAddress;
import com.mes.s7.enhanced.EnhancedS7Serializer;
import com.mes.service.PlcDynamicDataService;
@@ -10,6 +14,7 @@
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -24,6 +29,9 @@
@Slf4j
@Service
public class PlcDynamicDataServiceImpl implements PlcDynamicDataService {
    private final ObjectMapper objectMapper = new ObjectMapper();
    private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<Map<String, Object>>() {};
    /**
     * æ ¹æ®PlcAddress配置和字段名称读取PLC数据
@@ -59,8 +67,8 @@
            
            return resultMap;
        } catch (Exception e) {
            log.error("读取PLC数据失败,请检查:1.PLC IP地址是否正确[{}] 2.PLC设备是否在线 3.网络连接是否正常,module: {}, è¯¦ç»†é”™è¯¯: {}",
                config.getPlcIp(), config.getModule(), e.getMessage(), e);
            log.error("读取PLC数据失败,请检查:1.PLC IP地址是否正确[{}] 2.PLC设备是否在线 3.网络连接是否正常,详细错误: {}",
                config.getPlcIp(), e.getMessage(), e);
            return new HashMap<>();
        }
    }
@@ -88,8 +96,8 @@
            // å†™å…¥PLC
            s7Serializer.write(parameters);
        } catch (Exception e) {
            log.error("写入PLC数据失败,请检查:1.PLC IP地址是否正确[{}] 2.PLC设备是否在线 3.网络连接是否正常,module: {}, è¯¦ç»†é”™è¯¯: {}",
                config.getPlcIp(), config.getModule(), e.getMessage(), e);
            log.error("写入PLC数据失败,请检查:1.PLC IP地址是否正确[{}] 2.PLC设备是否在线 3.网络连接是否正常,详细错误: {}",
                config.getPlcIp(), e.getMessage(), e);
        }
    }
@@ -212,4 +220,287 @@
        
        return parameters;
    }
    /**
     * ä»ŽDeviceConfig中提取地址映射配置
     *
     * @param device è®¾å¤‡é…ç½®
     * @return åœ°å€æ˜ å°„JSON字符串
     */
    private String extractAddressMapping(DeviceConfig device) {
        // configJson çŽ°åœ¨ä»…å­˜æ”¾å­—æ®µåœ°å€æ˜ å°„ï¼ˆæ•°ç»„å½¢å¼ï¼‰
        Map<String, Object> configParams = ConfigJsonHelper.parseToMap(device.getConfigJson(), objectMapper);
        if (!configParams.isEmpty()) {
            try {
                return objectMapper.writeValueAsString(configParams);
            } catch (Exception e) {
                log.warn("序列化configJson地址映射失败, deviceId={}", device.getId(), e);
            }
        }
        // å…¶æ¬¡ä»ŽextraParams中获取(兼容旧结构)
        Map<String, Object> extraParams = parseExtraParams(device);
        Object addressMapping = extraParams.get("addressMapping");
        if (addressMapping != null) {
            if (addressMapping instanceof String) {
                return (String) addressMapping;
            } else {
                try {
                    return objectMapper.writeValueAsString(addressMapping);
                } catch (Exception e) {
                    log.warn("序列化extraParams.addressMapping失败, deviceId={}", device.getId(), e);
                }
            }
        }
        throw new IllegalArgumentException("设备配置中未找到addressMapping, deviceId=" + device.getId());
    }
    /**
     * ä»ŽDeviceConfig中提取dbArea
     *
     * @param device è®¾å¤‡é…ç½®
     * @return dbArea
     */
    private String extractDbArea(DeviceConfig device) {
        // ä»ŽextraParams.plcConfig中获取(新结构)
        Map<String, Object> plcConfig = getPlcConfig(device);
        Object dbArea = plcConfig.get("dbArea");
        if (dbArea != null) {
            return String.valueOf(dbArea);
        }
        // å…¼å®¹æ—§ç»“构:extraParams根节点
        Map<String, Object> extraParams = parseExtraParams(device);
        Object legacyDbArea = extraParams.get("dbArea");
        if (legacyDbArea != null) {
            return String.valueOf(legacyDbArea);
        }
        // é»˜è®¤å€¼
        return "DB12";
    }
    /**
     * ä»ŽDeviceConfig中提取beginIndex
     *
     * @param device è®¾å¤‡é…ç½®
     * @return beginIndex
     */
    private int extractBeginIndex(DeviceConfig device) {
        // ä»ŽextraParams.plcConfig中获取
        Map<String, Object> plcConfig = getPlcConfig(device);
        Object beginIndex = plcConfig.get("beginIndex");
        if (beginIndex != null) {
            return parseInteger(beginIndex);
        }
        // å…¼å®¹æ—§ç»“构:extraParams根节点
        Map<String, Object> extraParams = parseExtraParams(device);
        Object legacyBeginIndex = extraParams.get("beginIndex");
        if (legacyBeginIndex != null) {
            return parseInteger(legacyBeginIndex);
        }
        // é»˜è®¤å€¼
        return 0;
    }
    private Map<String, Object> parseExtraParams(DeviceConfig device) {
        if (device.getExtraParams() == null || device.getExtraParams().trim().isEmpty()) {
            return Collections.emptyMap();
        }
        try {
            return objectMapper.readValue(device.getExtraParams(), MAP_TYPE);
        } catch (Exception e) {
            log.warn("解析设备extraParams失败, deviceId={}", device.getId(), e);
            return Collections.emptyMap();
        }
    }
    @SuppressWarnings("unchecked")
    private Map<String, Object> getPlcConfig(DeviceConfig device) {
        Map<String, Object> extraParams = parseExtraParams(device);
        Object plcConfig = extraParams.get("plcConfig");
        if (plcConfig instanceof Map) {
            return (Map<String, Object>) plcConfig;
        }
        if (plcConfig instanceof String) {
            try {
                return objectMapper.readValue((String) plcConfig, MAP_TYPE);
            } catch (Exception e) {
                log.warn("解析extraParams.plcConfig失败, deviceId={}", device.getId(), e);
            }
        }
        return Collections.emptyMap();
    }
    private int parseInteger(Object value) {
        if (value instanceof Number) {
            return ((Number) value).intValue();
        }
        try {
            return Integer.parseInt(String.valueOf(value));
        } catch (NumberFormatException ex) {
            log.warn("无法解析整数值: {}", value);
            return 0;
        }
    }
    @Override
    public Map<String, Object> readPlcData(DeviceConfig device, List<String> fieldNames, EnhancedS7Serializer s7Serializer) {
        if (device == null) {
            throw new IllegalArgumentException("设备配置不能为空");
        }
        String addressMapping = extractAddressMapping(device);
        if (addressMapping == null || addressMapping.isEmpty()) {
            throw new IllegalArgumentException("设备配置中addressMapping不能为空");
        }
        try {
            // è§£æžaddressMapping JSON配置
            JSONObject addressMappingObj = JSONObject.parseObject(addressMapping);
            // æž„建S7Parameter列表
            String dbArea = extractDbArea(device);
            List<S7Parameter> parameters = buildS7ParametersForDevice(device, dbArea, addressMappingObj, 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.网络连接是否正常,deviceId: {}, è¯¦ç»†é”™è¯¯: {}",
                device.getPlcIp(), device.getId(), e.getMessage(), e);
            return new HashMap<>();
        }
    }
    @Override
    public void writePlcData(DeviceConfig device, Map<String, Object> dataMap, EnhancedS7Serializer s7Serializer) {
        if (device == null) {
            throw new IllegalArgumentException("设备配置不能为空");
        }
        String addressMapping = extractAddressMapping(device);
        if (addressMapping == null || addressMapping.isEmpty()) {
            throw new IllegalArgumentException("设备配置中addressMapping不能为空");
        }
        try {
            // è§£æžaddressMapping JSON配置
            JSONObject addressMappingObj = JSONObject.parseObject(addressMapping);
            // æž„建S7Parameter列表,并填充值
            String dbArea = extractDbArea(device);
            List<S7Parameter> parameters = buildS7ParametersWithValuesForDevice(device, dbArea, addressMappingObj, dataMap);
            // å†™å…¥PLC
            s7Serializer.write(parameters);
        } catch (Exception e) {
            log.error("写入PLC数据失败,请检查:1.PLC IP地址是否正确[{}] 2.PLC设备是否在线 3.网络连接是否正常,deviceId: {}, è¯¦ç»†é”™è¯¯: {}",
                device.getPlcIp(), device.getId(), e.getMessage(), e);
        }
    }
    @Override
    public Map<String, Object> readAllPlcData(DeviceConfig device, EnhancedS7Serializer s7Serializer) {
        if (device == null) {
            throw new IllegalArgumentException("设备配置不能为空");
        }
        String addressMapping = extractAddressMapping(device);
        if (addressMapping == null || addressMapping.isEmpty()) {
            throw new IllegalArgumentException("设备配置中addressMapping不能为空");
        }
        // èŽ·å–æ‰€æœ‰å­—æ®µå
        JSONObject addressMappingObj = JSONObject.parseObject(addressMapping);
        List<String> allFields = new ArrayList<>(addressMappingObj.keySet());
        // è¯»å–所有字段
        return readPlcData(device, allFields, s7Serializer);
    }
    @Override
    public Object readPlcField(DeviceConfig device, String fieldName, EnhancedS7Serializer s7Serializer) {
        List<String> fields = new ArrayList<>();
        fields.add(fieldName);
        Map<String, Object> result = readPlcData(device, fields, s7Serializer);
        return result.get(fieldName);
    }
    @Override
    public void writePlcField(DeviceConfig device, String fieldName, Object value, EnhancedS7Serializer s7Serializer) {
        Map<String, Object> dataMap = new HashMap<>();
        dataMap.put(fieldName, value);
        writePlcData(device, dataMap, s7Serializer);
    }
    /**
     * æž„建S7Parameter列表(不包含值)- åŸºäºŽDeviceConfig
     */
    private List<S7Parameter> buildS7ParametersForDevice(DeviceConfig device, String dbArea, 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 = dbArea + "." + offset;
            // åˆ›å»ºS7Parameter,默认使用UINT16类型(16位无符号整数)
            S7Parameter parameter = new S7Parameter(fullAddress, EDataType.UINT16, 1);
            parameters.add(parameter);
        }
        return parameters;
    }
    /**
     * æž„建S7Parameter列表(包含值)- åŸºäºŽDeviceConfig
     */
    private List<S7Parameter> buildS7ParametersWithValuesForDevice(DeviceConfig device, String dbArea, 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 = dbArea + "." + offset;
            // åˆ›å»ºS7Parameter,设置值
            S7Parameter parameter = new S7Parameter(fullAddress, EDataType.UINT16, 1);
            parameter.setValue(value);
            parameters.add(parameter);
        }
        return parameters;
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/service/impl/PlcTestWriteServiceImpl.java
@@ -2,11 +2,11 @@
import com.github.xingshuangs.iot.protocol.s7.enums.EPlcType;
import com.github.xingshuangs.iot.protocol.s7.service.S7PLC;
import com.mes.entity.PlcBaseData;
import com.mes.entity.PlcAddress;
import com.mes.entity.PlcBaseData;
import com.mes.s7.enhanced.EnhancedS7Serializer;
import com.mes.service.PlcAddressService;
import com.mes.service.IPlcTestWriteService;
import com.mes.service.PlcTestWriteService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -23,7 +23,7 @@
 */
@Slf4j
@Service
public class PlcTestWriteServiceImpl implements PlcTestWriteService {
public class PlcTestWriteServiceImpl extends PlcTestWriteService {
    @Resource
    private PlcAddressService plcAddressService;
mes-processes/mes-plcSend/src/main/java/com/mes/task/controller/MultiDeviceTaskController.java
New file
@@ -0,0 +1,62 @@
package com.mes.task.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.mes.task.dto.MultiDeviceTaskQuery;
import com.mes.task.dto.MultiDeviceTaskRequest;
import com.mes.task.entity.MultiDeviceTask;
import com.mes.task.entity.TaskStepDetail;
import com.mes.task.service.MultiDeviceTaskService;
import com.mes.vo.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
/**
 * å¤šè®¾å¤‡ä»»åŠ¡æŽ§åˆ¶å™¨
 */
@RestController
@RequestMapping("device/task")
@Api(tags = "多设备任务管理")
@Validated
@RequiredArgsConstructor
public class MultiDeviceTaskController {
    private final MultiDeviceTaskService multiDeviceTaskService;
    @PostMapping("/start")
    @ApiOperation("启动多设备联合测试任务")
    public Result<MultiDeviceTask> startTask(@Valid @RequestBody MultiDeviceTaskRequest request) {
        return Result.success(multiDeviceTaskService.startTask(request));
    }
    @PostMapping("/list")
    @ApiOperation("分页查询任务列表")
    public Result<Page<MultiDeviceTask>> listTasks(@RequestBody(required = false) MultiDeviceTaskQuery query) {
        MultiDeviceTaskQuery finalQuery = query != null ? query : new MultiDeviceTaskQuery();
        return Result.success(multiDeviceTaskService.queryTasks(finalQuery));
    }
    @GetMapping("/{taskId}")
    @ApiOperation("查询任务详情")
    public Result<MultiDeviceTask> getTask(@PathVariable String taskId) {
        return Result.success(multiDeviceTaskService.getTaskByTaskId(taskId));
    }
    @GetMapping("/{taskId}/steps")
    @ApiOperation("查询任务步骤详情")
    public Result<List<TaskStepDetail>> getTaskSteps(@PathVariable String taskId) {
        return Result.success(multiDeviceTaskService.getTaskSteps(taskId));
    }
    @PostMapping("/{taskId}/cancel")
    @ApiOperation("取消正在运行的任务")
    public Result<Boolean> cancelTask(@PathVariable String taskId) {
        return Result.success(multiDeviceTaskService.cancelTask(taskId));
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/task/dto/MultiDeviceTaskQuery.java
New file
@@ -0,0 +1,26 @@
package com.mes.task.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
 * å¤šè®¾å¤‡ä»»åŠ¡æŸ¥è¯¢æ¡ä»¶
 */
@Data
@ApiModel(value = "MultiDeviceTaskQuery", description = "多设备任务分页查询条件")
public class MultiDeviceTaskQuery {
    @ApiModelProperty(value = "设备组ID", example = "1")
    private Long groupId;
    @ApiModelProperty(value = "任务状态(PENDING/RUNNING/COMPLETED/FAILED/CANCELLED)")
    private String status;
    @ApiModelProperty(value = "页码,从1开始", example = "1")
    private Integer page = 1;
    @ApiModelProperty(value = "每页数量", example = "10")
    private Integer size = 10;
}
mes-processes/mes-plcSend/src/main/java/com/mes/task/dto/MultiDeviceTaskRequest.java
New file
@@ -0,0 +1,32 @@
package com.mes.task.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
/**
 * å¤šè®¾å¤‡ä»»åŠ¡å¯åŠ¨è¯·æ±‚
 */
@Data
@ApiModel(value = "MultiDeviceTaskRequest", description = "多设备联合测试任务启动请求")
public class MultiDeviceTaskRequest {
    @ApiModelProperty(value = "设备组ID", example = "1", required = true)
    @NotNull(message = "设备组ID不能为空")
    private Long groupId;
    @ApiModelProperty(value = "任务显示名称")
    private String taskName;
    @ApiModelProperty(value = "触发人")
    private String triggeredBy;
    @ApiModelProperty(value = "任务参数", required = true)
    @Valid
    @NotNull(message = "任务参数不能为空")
    private TaskParameters parameters;
}
mes-processes/mes-plcSend/src/main/java/com/mes/task/dto/TaskParameters.java
New file
@@ -0,0 +1,50 @@
package com.mes.task.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotEmpty;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
/**
 * å¤šè®¾å¤‡ä»»åŠ¡å‚æ•°
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "TaskParameters", description = "多设备任务执行参数")
public class TaskParameters implements Serializable {
    @ApiModelProperty(value = "玻璃ID列表(保持执行顺序)", required = true)
    @NotEmpty(message = "玻璃ID列表不能为空")
    private List<String> glassIds;
    @ApiModelProperty(value = "上大车位置编码")
    private String positionCode;
    @ApiModelProperty(value = "上大车位置值")
    private Integer positionValue;
    @ApiModelProperty(value = "大理片加工类型")
    private Integer processType;
    @ApiModelProperty(value = "玻璃存储位置")
    private Integer storagePosition;
    @ApiModelProperty(value = "执行间隔(毫秒)")
    private Integer executionInterval;
    @ApiModelProperty(value = "设备级别参数覆盖,key可以是设备类型或设备编码")
    private Map<String, Map<String, Object>> deviceOverrides;
    @ApiModelProperty(value = "额外透传参数")
    private Map<String, Object> extra;
}
mes-processes/mes-plcSend/src/main/java/com/mes/task/entity/MultiDeviceTask.java
New file
@@ -0,0 +1,85 @@
package com.mes.task.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
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 io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.Date;
/**
 * å¤šè®¾å¤‡è”合测试任务实体
 */
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("multi_device_task")
@ApiModel(value = "MultiDeviceTask", description = "多设备联合测试任务记录")
public class MultiDeviceTask implements Serializable {
    @TableId(value = "id", type = IdType.AUTO)
    @ApiModelProperty("自增主键")
    private Long id;
    @TableField("task_id")
    @ApiModelProperty("任务唯一编号")
    private String taskId;
    @TableField("group_id")
    @ApiModelProperty("设备组ID(字符串)")
    private String groupId;
    @TableField("project_id")
    @ApiModelProperty("所属项目ID(字符串)")
    private String projectId;
    @TableField("status")
    @ApiModelProperty("任务状态")
    private String status;
    @TableField("current_step")
    @ApiModelProperty("当前执行步骤")
    private Integer currentStep;
    @TableField("total_steps")
    @ApiModelProperty("总步骤数")
    private Integer totalSteps;
    @TableField("start_time")
    @ApiModelProperty("开始时间")
    private Date startTime;
    @TableField("end_time")
    @ApiModelProperty("结束时间")
    private Date endTime;
    @TableField("error_message")
    @ApiModelProperty("错误信息")
    private String errorMessage;
    @TableField("result_data")
    @ApiModelProperty("执行结果数据(JSON)")
    private String resultData;
    @TableField(value = "created_time", fill = FieldFill.INSERT)
    @ApiModelProperty("创建时间")
    private Date createdTime;
    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
    @ApiModelProperty("更新时间")
    private Date updatedTime;
    public enum Status {
        PENDING,
        RUNNING,
        COMPLETED,
        FAILED,
        CANCELLED
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/task/entity/TaskStepDetail.java
New file
@@ -0,0 +1,89 @@
package com.mes.task.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
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 io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.Date;
/**
 * å¤šè®¾å¤‡ä»»åŠ¡æ­¥éª¤å®žä½“
 */
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("task_step_detail")
@ApiModel(value = "TaskStepDetail", description = "多设备任务步骤详情")
public class TaskStepDetail implements Serializable {
    @TableId(value = "id", type = IdType.AUTO)
    @ApiModelProperty("主键ID")
    private Long id;
    @TableField("task_id")
    @ApiModelProperty("关联任务ID")
    private String taskId;
    @TableField("step_order")
    @ApiModelProperty("步骤顺序")
    private Integer stepOrder;
    @TableField("device_id")
    @ApiModelProperty("设备ID(字符串)")
    private String deviceId;
    @TableField("step_name")
    @ApiModelProperty("步骤名称")
    private String stepName;
    @TableField("status")
    @ApiModelProperty("步骤状态")
    private String status;
    @TableField("start_time")
    @ApiModelProperty("开始时间")
    private Date startTime;
    @TableField("end_time")
    @ApiModelProperty("结束时间")
    private Date endTime;
    @TableField("duration_ms")
    @ApiModelProperty("执行耗时(毫秒)")
    private Long durationMs;
    @TableField("input_data")
    @ApiModelProperty("输入数据(JSON)")
    private String inputData;
    @TableField("output_data")
    @ApiModelProperty("输出数据(JSON)")
    private String outputData;
    @TableField("error_message")
    @ApiModelProperty("错误信息")
    private String errorMessage;
    @TableField("retry_count")
    @ApiModelProperty("重试次数")
    private Integer retryCount;
    @TableField(value = "created_time", fill = FieldFill.INSERT)
    @ApiModelProperty("记录创建时间")
    private Date createdTime;
    public enum Status {
        PENDING,
        RUNNING,
        COMPLETED,
        FAILED,
        SKIPPED
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/task/mapper/MultiDeviceTaskMapper.java
New file
@@ -0,0 +1,13 @@
package com.mes.task.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mes.task.entity.MultiDeviceTask;
import org.apache.ibatis.annotations.Mapper;
/**
 * å¤šè®¾å¤‡ä»»åŠ¡ Mapper
 */
@Mapper
public interface MultiDeviceTaskMapper extends BaseMapper<MultiDeviceTask> {
}
mes-processes/mes-plcSend/src/main/java/com/mes/task/mapper/TaskStepDetailMapper.java
New file
@@ -0,0 +1,13 @@
package com.mes.task.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mes.task.entity.TaskStepDetail;
import org.apache.ibatis.annotations.Mapper;
/**
 * ä»»åŠ¡æ­¥éª¤ Mapper
 */
@Mapper
public interface TaskStepDetailMapper extends BaseMapper<TaskStepDetail> {
}
mes-processes/mes-plcSend/src/main/java/com/mes/task/model/TaskExecutionContext.java
New file
@@ -0,0 +1,54 @@
package com.mes.task.model;
import com.mes.task.dto.TaskParameters;
import lombok.Getter;
import lombok.Setter;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
 * å¤šè®¾å¤‡ä»»åŠ¡æ‰§è¡Œä¸Šä¸‹æ–‡
 */
@Getter
public class TaskExecutionContext {
    private final TaskParameters parameters;
    @Setter
    private List<String> loadedGlassIds;
    @Setter
    private List<String> processedGlassIds;
    private final Map<String, Object> sharedData = new ConcurrentHashMap<>();
    public TaskExecutionContext(TaskParameters parameters) {
        if (parameters == null) {
            this.parameters = new TaskParameters();
        } else {
            this.parameters = parameters;
        }
        if (CollectionUtils.isEmpty(this.parameters.getGlassIds())) {
            this.parameters.setGlassIds(new ArrayList<>());
        }
        this.sharedData.put("initialGlassIds", new ArrayList<>(this.parameters.getGlassIds()));
    }
    public Map<String, Object> getSharedData() {
        return sharedData;
    }
    public List<String> getSafeLoadedGlassIds() {
        return loadedGlassIds == null ? Collections.emptyList() : loadedGlassIds;
    }
    public List<String> getSafeProcessedGlassIds() {
        return processedGlassIds == null ? Collections.emptyList() : processedGlassIds;
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/task/model/TaskExecutionResult.java
New file
@@ -0,0 +1,42 @@
package com.mes.task.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.HashMap;
import java.util.Map;
/**
 * ä»»åŠ¡æ‰§è¡Œç»“æžœ
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TaskExecutionResult {
    private boolean success;
    private String message;
    private Map<String, Object> data;
    public static TaskExecutionResult success(Map<String, Object> payload) {
        return TaskExecutionResult.builder()
                .success(true)
                .message("执行完成")
                .data(payload != null ? payload : new HashMap<>())
                .build();
    }
    public static TaskExecutionResult failure(String message, Map<String, Object> payload) {
        return TaskExecutionResult.builder()
                .success(false)
                .message(message)
                .data(payload != null ? payload : new HashMap<>())
                .build();
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/MultiDeviceTaskService.java
New file
@@ -0,0 +1,42 @@
package com.mes.task.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.mes.task.dto.MultiDeviceTaskQuery;
import com.mes.task.dto.MultiDeviceTaskRequest;
import com.mes.task.entity.MultiDeviceTask;
import com.mes.task.entity.TaskStepDetail;
import java.util.List;
/**
 * å¤šè®¾å¤‡ä»»åŠ¡æœåŠ¡
 */
public interface MultiDeviceTaskService extends IService<MultiDeviceTask> {
    /**
     * å¯åŠ¨å¤šè®¾å¤‡æµ‹è¯•ä»»åŠ¡
     */
    MultiDeviceTask startTask(MultiDeviceTaskRequest request);
    /**
     * æ ¹æ®ä»»åŠ¡ç¼–å·èŽ·å–ä»»åŠ¡
     */
    MultiDeviceTask getTaskByTaskId(String taskId);
    /**
     * æŸ¥è¯¢ä»»åŠ¡æ­¥éª¤
     */
    List<TaskStepDetail> getTaskSteps(String taskId);
    /**
     * å–消任务
     */
    boolean cancelTask(String taskId);
    /**
     * åˆ†é¡µæŸ¥è¯¢ä»»åŠ¡
     */
    Page<MultiDeviceTask> queryTasks(MultiDeviceTaskQuery query);
}
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java
New file
@@ -0,0 +1,378 @@
package com.mes.task.service;
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.databind.ObjectMapper;
import com.mes.device.entity.DeviceConfig;
import com.mes.device.entity.DeviceGroupConfig;
import com.mes.interaction.DeviceInteraction;
import com.mes.interaction.DeviceInteractionRegistry;
import com.mes.interaction.DeviceLogicHandler;
import com.mes.interaction.DeviceLogicHandlerFactory;
import com.mes.interaction.base.InteractionContext;
import com.mes.interaction.base.InteractionResult;
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.TaskExecutionContext;
import com.mes.task.model.TaskExecutionResult;
import com.mes.device.vo.DevicePlcVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.*;
/**
 * å¤šè®¾å¤‡ä»»åŠ¡æ‰§è¡Œå¼•æ“Ž
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class TaskExecutionEngine {
    private static final Map<String, String> DEFAULT_OPERATIONS = new HashMap<>();
    static {
        DEFAULT_OPERATIONS.put(DeviceConfig.DeviceType.LOAD_VEHICLE, "feedGlass");
        DEFAULT_OPERATIONS.put(DeviceConfig.DeviceType.LARGE_GLASS, "processGlass");
        DEFAULT_OPERATIONS.put(DeviceConfig.DeviceType.GLASS_STORAGE, "storeGlass");
    }
    private final TaskStepDetailMapper taskStepDetailMapper;
    private final MultiDeviceTaskMapper multiDeviceTaskMapper;
    private final DeviceInteractionService deviceInteractionService;
    private final DeviceInteractionRegistry interactionRegistry;
    private final DeviceLogicHandlerFactory handlerFactory;
    private final ObjectMapper objectMapper;
    public TaskExecutionResult execute(MultiDeviceTask task,
                                       DeviceGroupConfig groupConfig,
                                       List<DeviceConfig> devices,
                                       TaskParameters parameters) {
        if (CollectionUtils.isEmpty(devices)) {
            return TaskExecutionResult.failure("设备组未配置设备,无法执行任务", Collections.emptyMap());
        }
        TaskExecutionContext context = new TaskExecutionContext(parameters);
        task.setTotalSteps(devices.size());
        task.setStatus(MultiDeviceTask.Status.RUNNING.name());
        multiDeviceTaskMapper.updateById(task);
        List<Map<String, Object>> stepSummaries = new ArrayList<>();
        boolean success = true;
        String 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;
            }
        }
        Map<String, Object> payload = new HashMap<>();
        payload.put("steps", stepSummaries);
        payload.put("groupId", groupConfig.getId());
        payload.put("deviceCount", devices.size());
        if (success) {
            return TaskExecutionResult.success(payload);
        }
        return TaskExecutionResult.failure(failureMessage != null ? failureMessage : "任务执行失败", payload);
    }
    private TaskStepDetail createStepRecord(MultiDeviceTask task, DeviceConfig device, int order) {
        TaskStepDetail step = new TaskStepDetail();
        step.setTaskId(task.getTaskId());
        step.setStepOrder(order);
        step.setDeviceId(String.valueOf(device.getId()));
        step.setStepName(device.getDeviceName());
        step.setStatus(TaskStepDetail.Status.PENDING.name());
        step.setRetryCount(0);
        taskStepDetailMapper.insert(step);
        return step;
    }
    private StepResult executeStep(MultiDeviceTask task,
                                   TaskStepDetail step,
                                   DeviceConfig device,
                                   TaskExecutionContext context) {
        Date startTime = new Date();
        step.setStartTime(startTime);
        step.setStatus(TaskStepDetail.Status.RUNNING.name());
        DeviceInteraction deviceInteraction = interactionRegistry.getInteraction(device.getDeviceType());
        if (deviceInteraction != null) {
            return executeInteractionStep(task, step, device, context, deviceInteraction);
        }
        Map<String, Object> params = buildOperationParams(device, context);
        step.setInputData(toJson(params));
        taskStepDetailMapper.updateById(step);
        String operation = determineOperation(device, params);
        DeviceLogicHandler handler = handlerFactory.getHandler(device.getDeviceType());
        DevicePlcVO.OperationResult result;
        try {
            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);
            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());
        }
    }
    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);
            if (success) {
                return StepResult.success(device.getDeviceName(), interactionResult.getMessage());
            }
            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());
        }
    }
    private void updateStepAfterOperation(TaskStepDetail step,
                                          DevicePlcVO.OperationResult result,
                                          boolean success) {
        step.setEndTime(new Date());
        if (step.getStartTime() != null) {
            step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime());
        }
        step.setStatus(success ? TaskStepDetail.Status.COMPLETED.name() : TaskStepDetail.Status.FAILED.name());
        step.setErrorMessage(success ? null : result.getMessage());
        step.setOutputData(toJson(result));
        taskStepDetailMapper.updateById(step);
    }
    private void updateStepAfterInteraction(TaskStepDetail step,
                                            InteractionResult result) {
        step.setEndTime(new Date());
        if (step.getStartTime() != null) {
            step.setDurationMs(step.getEndTime().getTime() - step.getStartTime().getTime());
        }
        boolean success = result != null && result.isSuccess();
        step.setStatus(success ? TaskStepDetail.Status.COMPLETED.name() : TaskStepDetail.Status.FAILED.name());
        step.setErrorMessage(success ? null : (result != null ? result.getMessage() : "交互执行失败"));
        step.setOutputData(result != null ? toJson(result.getData()) : "{}");
        taskStepDetailMapper.updateById(step);
    }
    private void updateTaskProgress(MultiDeviceTask task, int currentStep, boolean success) {
        task.setCurrentStep(currentStep);
        if (!success) {
            task.setStatus(MultiDeviceTask.Status.FAILED.name());
        }
        LambdaUpdateWrapper<MultiDeviceTask> update = Wrappers.<MultiDeviceTask>lambdaUpdate()
                .eq(MultiDeviceTask::getId, task.getId())
                .set(MultiDeviceTask::getCurrentStep, currentStep);
        if (!success) {
            update.set(MultiDeviceTask::getStatus, MultiDeviceTask.Status.FAILED.name());
        }
        multiDeviceTaskMapper.update(null, update);
    }
    private String determineOperation(DeviceConfig device, Map<String, Object> params) {
        if (params != null && params.containsKey("operation")) {
            Object op = params.get("operation");
            if (op != null) {
                return String.valueOf(op);
            }
        }
        return DEFAULT_OPERATIONS.getOrDefault(device.getDeviceType(), "feedGlass");
    }
    private Map<String, Object> buildOperationParams(DeviceConfig device, TaskExecutionContext context) {
        Map<String, Object> params = new HashMap<>();
        TaskParameters taskParams = context.getParameters();
        switch (device.getDeviceType()) {
            case DeviceConfig.DeviceType.LOAD_VEHICLE:
                params.put("glassIds", new ArrayList<>(taskParams.getGlassIds()));
                if (StringUtils.hasText(taskParams.getPositionCode())) {
                    params.put("positionCode", taskParams.getPositionCode());
                }
                if (taskParams.getPositionValue() != null) {
                    params.put("positionValue", taskParams.getPositionValue());
                }
                params.put("triggerRequest", true);
                break;
            case DeviceConfig.DeviceType.LARGE_GLASS:
                List<String> source = context.getSafeLoadedGlassIds();
                if (CollectionUtils.isEmpty(source)) {
                    source = taskParams.getGlassIds();
                }
                if (!CollectionUtils.isEmpty(source)) {
                    params.put("glassId", source.get(0));
                    params.put("glassIds", new ArrayList<>(source));
                }
                params.put("processType", taskParams.getProcessType() != null ? taskParams.getProcessType() : 1);
                params.put("triggerRequest", true);
                break;
            case DeviceConfig.DeviceType.GLASS_STORAGE:
                List<String> processed = context.getSafeProcessedGlassIds();
                if (CollectionUtils.isEmpty(processed)) {
                    processed = context.getSafeLoadedGlassIds();
                }
                if (!CollectionUtils.isEmpty(processed)) {
                    params.put("glassId", processed.get(0));
                    params.put("glassIds", new ArrayList<>(processed));
                }
                if (taskParams.getStoragePosition() != null) {
                    params.put("storagePosition", taskParams.getStoragePosition());
                }
                params.put("triggerRequest", true);
                break;
            default:
                if (!CollectionUtils.isEmpty(taskParams.getExtra())) {
                    params.putAll(taskParams.getExtra());
                }
        }
        mergeOverrides(device, taskParams, params);
        return params;
    }
    private void mergeOverrides(DeviceConfig device, TaskParameters taskParameters, Map<String, Object> params) {
        if (CollectionUtils.isEmpty(taskParameters.getDeviceOverrides())) {
            return;
        }
        Map<String, Object> override = taskParameters.getDeviceOverrides().get(device.getDeviceType());
        if (override == null && StringUtils.hasText(device.getDeviceCode())) {
            override = taskParameters.getDeviceOverrides().get(device.getDeviceCode());
        }
        if (override != null) {
            params.putAll(override);
        }
    }
    private void updateContextAfterSuccess(DeviceConfig device,
                                           TaskExecutionContext context,
                                           Map<String, Object> params) {
        switch (device.getDeviceType()) {
            case DeviceConfig.DeviceType.LOAD_VEHICLE:
                context.setLoadedGlassIds(extractGlassIds(params));
                break;
            case DeviceConfig.DeviceType.LARGE_GLASS:
                context.setProcessedGlassIds(extractGlassIds(params));
                break;
            default:
                break;
        }
    }
    private List<String> extractGlassIds(Map<String, Object> params) {
        if (params == null) {
            return Collections.emptyList();
        }
        Object glassIds = params.get("glassIds");
        if (glassIds instanceof List) {
            @SuppressWarnings("unchecked")
            List<String> cast = (List<String>) glassIds;
            return new ArrayList<>(cast);
        }
        Object glassId = params.get("glassId");
        if (glassId != null) {
            return Collections.singletonList(String.valueOf(glassId));
        }
        return Collections.emptyList();
    }
    private String toJson(Object value) {
        try {
            return objectMapper.writeValueAsString(value);
        } catch (JsonProcessingException e) {
            return "{}";
        }
    }
    private static class StepResult {
        private final boolean success;
        private final String message;
        private final String deviceName;
        private StepResult(boolean success, String message, String deviceName) {
            this.success = success;
            this.message = message;
            this.deviceName = deviceName;
        }
        public static StepResult success(String deviceName, String message) {
            return new StepResult(true, message, deviceName);
        }
        public static StepResult failure(String deviceName, String message) {
            return new StepResult(false, message, deviceName);
        }
        public boolean isSuccess() {
            return success;
        }
        public String getMessage() {
            return message;
        }
        public Map<String, Object> toSummary() {
            Map<String, Object> summary = new HashMap<>();
            summary.put("deviceName", deviceName);
            summary.put("success", success);
            summary.put("message", message);
            return summary;
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/impl/MultiDeviceTaskServiceImpl.java
New file
@@ -0,0 +1,160 @@
package com.mes.task.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mes.device.entity.DeviceConfig;
import com.mes.device.entity.DeviceGroupConfig;
import com.mes.device.mapper.DeviceGroupRelationMapper;
import com.mes.device.service.DeviceGroupConfigService;
import com.mes.task.dto.MultiDeviceTaskQuery;
import com.mes.task.dto.MultiDeviceTaskRequest;
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.TaskExecutionResult;
import com.mes.task.service.MultiDeviceTaskService;
import com.mes.task.service.TaskExecutionEngine;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
/**
 * å¤šè®¾å¤‡ä»»åŠ¡æœåŠ¡å®žçŽ°
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class MultiDeviceTaskServiceImpl extends ServiceImpl<MultiDeviceTaskMapper, MultiDeviceTask>
        implements MultiDeviceTaskService {
    private final DeviceGroupConfigService deviceGroupConfigService;
    private final DeviceGroupRelationMapper deviceGroupRelationMapper;
    private final TaskStepDetailMapper taskStepDetailMapper;
    private final TaskExecutionEngine taskExecutionEngine;
    private final ObjectMapper objectMapper;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MultiDeviceTask startTask(MultiDeviceTaskRequest request) {
        DeviceGroupConfig groupConfig = deviceGroupConfigService.getDeviceGroupById(request.getGroupId());
        if (groupConfig == null) {
            throw new IllegalArgumentException("设备组不存在: " + request.getGroupId());
        }
        if (groupConfig.getStatus() != DeviceGroupConfig.Status.ENABLED) {
            throw new IllegalStateException("设备组未启用,无法执行任务");
        }
        List<DeviceConfig> devices = deviceGroupRelationMapper.getOrderedDeviceConfigs(groupConfig.getId());
        if (CollectionUtils.isEmpty(devices)) {
            throw new IllegalStateException("设备组未配置任何设备,无法执行任务");
        }
        TaskParameters parameters = request.getParameters();
        if (parameters == null || CollectionUtils.isEmpty(parameters.getGlassIds())) {
            throw new IllegalArgumentException("至少需要配置一条玻璃ID");
        }
        MultiDeviceTask task = new MultiDeviceTask();
        task.setTaskId(generateTaskId(groupConfig));
        task.setGroupId(String.valueOf(groupConfig.getId()));
        task.setProjectId(String.valueOf(groupConfig.getProjectId()));
        task.setStatus(MultiDeviceTask.Status.PENDING.name());
        task.setCurrentStep(0);
        task.setTotalSteps(devices.size());
        task.setStartTime(new Date());
        save(task);
        try {
            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);
            return task;
        } catch (Exception ex) {
            log.error("多设备任务执行异常, taskId={}", task.getTaskId(), ex);
            task.setStatus(MultiDeviceTask.Status.FAILED.name());
            task.setErrorMessage(ex.getMessage());
            task.setEndTime(new Date());
            updateById(task);
            throw new RuntimeException("多设备任务执行失败: " + ex.getMessage(), ex);
        }
    }
    @Override
    public MultiDeviceTask getTaskByTaskId(String taskId) {
        LambdaQueryWrapper<MultiDeviceTask> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(MultiDeviceTask::getTaskId, taskId);
        return getOne(wrapper);
    }
    @Override
    public List<TaskStepDetail> getTaskSteps(String taskId) {
        LambdaQueryWrapper<TaskStepDetail> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(TaskStepDetail::getTaskId, taskId);
        wrapper.orderByAsc(TaskStepDetail::getStepOrder);
        return taskStepDetailMapper.selectList(wrapper);
    }
    @Override
    public boolean cancelTask(String taskId) {
        MultiDeviceTask task = getTaskByTaskId(taskId);
        if (task == null) {
            return false;
        }
        if (!MultiDeviceTask.Status.RUNNING.name().equals(task.getStatus())) {
            return false;
        }
        task.setStatus(MultiDeviceTask.Status.CANCELLED.name());
        task.setEndTime(new Date());
        return updateById(task);
    }
    @Override
    public Page<MultiDeviceTask> queryTasks(MultiDeviceTaskQuery query) {
        int page = query.getPage() != null && query.getPage() > 0 ? query.getPage() : 1;
        int size = query.getSize() != null && query.getSize() > 0 ? query.getSize() : 10;
        Page<MultiDeviceTask> pageParam = new Page<>(page, size);
        LambdaQueryWrapper<MultiDeviceTask> wrapper = new LambdaQueryWrapper<>();
        if (query.getGroupId() != null) {
            wrapper.eq(MultiDeviceTask::getGroupId, String.valueOf(query.getGroupId()));
        }
        if (StringUtils.hasText(query.getStatus())) {
            wrapper.eq(MultiDeviceTask::getStatus, query.getStatus().toUpperCase(Locale.ROOT));
        }
        wrapper.orderByDesc(MultiDeviceTask::getCreatedTime);
        return page(pageParam, wrapper);
    }
    private String generateTaskId(DeviceGroupConfig groupConfig) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss", Locale.CHINA);
        return "TASK_" + groupConfig.getId() + "_" + sdf.format(new Date());
    }
    private String writeJson(Object data) {
        if (data == null) {
            return "{}";
        }
        try {
            return objectMapper.writeValueAsString(data);
        } catch (JsonProcessingException e) {
            return "{}";
        }
    }
}
mes-processes/mes-plcSend/src/main/resources/application-dev.yml
@@ -8,7 +8,7 @@
      strict: false #设置严格模式,默认false不启动. å¯åŠ¨åŽåœ¨æœªåŒ¹é…åˆ°æŒ‡å®šæ•°æ®æºæ—¶å€™å›žæŠ›å‡ºå¼‚å¸¸,不启动会使用默认数据源.
      datasource:
        northGlassMes:
          url: jdbc:mysql://${ip}:${port}/mes_modular?serverTimezone=GMT%2b8
          url: jdbc:mysql://${ip}:${port}/mes_test?serverTimezone=GMT%2b8
          username: root
          password: beibo.123/
          driver-class-name: com.mysql.cj.jdbc.Driver
mes-web/src/api/device/deviceManagement.js
@@ -200,33 +200,38 @@
  /**
   * åˆ›å»ºè®¾å¤‡ç»„配置
   */
  create(data) {
  create(config) {
    return request({
      url: '/api/plcSend/device/group/create',
      method: 'post',
      data
      data: {
        groupConfig: config
      }
    })
  },
  /**
   * æ›´æ–°è®¾å¤‡ç»„配置
   */
  update(data) {
  update(id, config) {
    return request({
      url: '/api/plcSend/device/group/update',
      method: 'post',
      data
      data: {
        groupId: id,
        groupConfig: config
      }
    })
  },
  /**
   * åˆ é™¤è®¾å¤‡ç»„配置
   */
  delete(data) {
  delete(id) {
    return request({
      url: '/api/plcSend/device/group/delete',
      method: 'post',
      data
      data: { groupId: id }
    })
  },
@@ -263,44 +268,44 @@
  /**
   * å¯ç”¨è®¾å¤‡ç»„
   */
  enable(data) {
  enable(id) {
    return request({
      url: '/api/plcSend/device/group/enable',
      method: 'post',
      data
      data: { groupId: id }
    })
  },
  /**
   * ç¦ç”¨è®¾å¤‡ç»„
   */
  disable(data) {
  disable(id) {
    return request({
      url: '/api/plcSend/device/group/disable',
      method: 'post',
      data
      data: { groupId: id }
    })
  },
  /**
   * æ‰¹é‡å¯ç”¨è®¾å¤‡ç»„
   */
  batchEnable(data) {
  batchEnable(groupIds) {
    return request({
      url: '/api/plcSend/device/group/batch-enable',
      method: 'post',
      data
      data: { groupIds }
    })
  },
  /**
   * æ‰¹é‡ç¦ç”¨è®¾å¤‡ç»„
   */
  batchDisable(data) {
  batchDisable(groupIds) {
    return request({
      url: '/api/plcSend/device/group/batch-disable',
      method: 'post',
      data
      data: { groupIds }
    })
  },
@@ -526,6 +531,32 @@
  }
}
// è®¾å¤‡äº¤äº’操作API
export const deviceInteractionApi = {
  /**
   * æ‰§è¡Œè®¾å¤‡é€»è¾‘操作
   * @param {Object} data - { deviceId, operation, params }
   */
  executeOperation(data) {
    return request({
      url: '/api/plcSend/device/interaction/execute',
      method: 'post',
      data
    })
  },
  /**
   * çŽ»ç’ƒä¸Šæ–™å†™å…¥
   */
  feedGlass(data) {
    return request({
      url: '/api/plcSend/device/interaction/glass-feed',
      method: 'post',
      data
    })
  }
}
// ç»Ÿè®¡API
export const getDeviceStatistics = (data) => {
  return request({
@@ -547,6 +578,7 @@
  deviceConfigApi,
  deviceGroupApi,
  devicePlcApi,
  deviceInteractionApi,
  getDeviceStatistics,
  getDeviceGroupStatistics
}
mes-web/src/api/device/multiDeviceTask.js
New file
@@ -0,0 +1,58 @@
import request from '@/utils/request'
const BASE_URL = '/api/plcSend/device/task'
export const multiDeviceTaskApi = {
  /**
   * å¯åŠ¨å¤šè®¾å¤‡ä»»åŠ¡
   */
  startTask(data) {
    return request({
      url: `${BASE_URL}/start`,
      method: 'post',
      data
    })
  },
  /**
   * æŸ¥è¯¢ä»»åŠ¡åˆ—è¡¨
   */
  getTaskList(params) {
    return request({
      url: `${BASE_URL}/list`,
      method: 'post',
      data: params
    })
  },
  /**
   * æŸ¥è¯¢ä»»åŠ¡è¯¦æƒ…
   */
  getTaskById(taskId) {
    return request({
      url: `${BASE_URL}/${taskId}`,
      method: 'get'
    })
  },
  /**
   * æŸ¥è¯¢ä»»åŠ¡æ­¥éª¤
   */
  getTaskSteps(taskId) {
    return request({
      url: `${BASE_URL}/${taskId}/steps`,
      method: 'get'
    })
  },
  /**
   * å–消任务
   */
  cancelTask(taskId) {
    return request({
      url: `${BASE_URL}/${taskId}/cancel`,
      method: 'post'
    })
  }
}
mes-web/src/router/index.js
@@ -34,6 +34,16 @@
              name: 'plcTest',
              component: () => import('../views/plcTest/Test.vue')
            },
            {
              path: '/plcTest/MultiDeviceWorkbench',
              name: 'MultiDeviceWorkbench',
              component: () => import('../views/plcTest/MultiDeviceWorkbench.vue')
            },
            {
              path: '/device/DeviceManagement',
              name: 'DeviceManagement',
              component: () => import('../views/device/DeviceManagement.vue')
            }
          ]
        },
mes-web/src/utils/constants.js
@@ -1,6 +1,6 @@
// export const WebSocketHost = "10.153.19.150";
// export const WebSocketHost = "172.17.2.7";
export const WebSocketHost = "10.153.19.213";//hxl
export const WebSocketHost = "10.153.19.49";//hxl
// export const WebSocketHost = "10.153.19.2";//zt
//export const WebSocketHost = "10.153.19.20";//wsx
// export const WebSocketHost = "127.0.0.1";
mes-web/src/views/device/DeviceEditDialog.vue
@@ -106,13 +106,20 @@
            </el-form-item>
            <el-form-item label="通讯协议" prop="protocolType">
              <el-select v-model="deviceForm.protocolType" placeholder="选择通讯协议" style="width: 100%;">
              <el-select
                v-model="deviceForm.protocolType"
                placeholder="选择通讯协议"
                style="width: 100%;"
                @change="handleProtocolTypeChange"
              >
                <el-option label="S7 Communication" value="S7 Communication" />
                <el-option label="Modbus TCP" value="Modbus TCP" />
                <el-option label="OPC UA" value="OPC UA" />
                <el-option label="EtherNet/IP" value="EtherNet/IP" />
                <el-option label="Profinet" value="Profinet" />
                <el-option label="其他" value="其他" />
              </el-select>
              <span class="form-tip">S7系列PLC通常使用S7 Communication协议</span>
            </el-form-item>
            <el-form-item label="超时时间(秒)" prop="timeout">
@@ -240,6 +247,200 @@
        </div>
      </el-card>
      <!-- è®¾å¤‡é€»è¾‘配置 -->
      <el-card class="form-section" shadow="never" style="margin-top: 20px;" v-if="deviceForm.deviceType">
        <template #header>
          <span class="section-title">设备逻辑配置</span>
          <span class="form-tip">根据设备类型配置特定的业务逻辑参数</span>
        </template>
        <!-- ä¸Šå¤§è½¦è®¾å¤‡é€»è¾‘配置 -->
        <div v-if="deviceForm.deviceType === '上大车'">
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="车辆容量">
                <el-input-number
                  v-model="deviceLogicParams.vehicleCapacity"
                  :min="1"
                  :max="10000"
                  :step="100"
                  style="width: 100%;"
                />
                <span class="form-tip">车辆最大容量</span>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="玻璃间隔(ms)">
                <el-input-number
                  v-model="deviceLogicParams.glassIntervalMs"
                  :min="100"
                  :max="10000"
                  :step="100"
                  style="width: 100%;"
                />
                <span class="form-tip">玻璃上料间隔时间(毫秒)</span>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="自动上料">
                <el-switch v-model="deviceLogicParams.autoFeed" />
                <span class="form-tip">是否自动触发上料请求</span>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="最大重试次数">
                <el-input-number
                  v-model="deviceLogicParams.maxRetryCount"
                  :min="0"
                  :max="10"
                  :step="1"
                  style="width: 100%;"
                />
              </el-form-item>
            </el-col>
          </el-row>
          <el-form-item label="位置映射">
            <div class="position-mapping">
              <div
                v-for="(value, key, index) in deviceLogicParams.positionMapping"
                :key="index"
                class="mapping-item"
              >
                <el-input
                  v-model="mappingKeys[index]"
                  placeholder="位置代码"
                  size="small"
                  style="width: 150px; margin-right: 10px;"
                  @input="updatePositionMapping(index, $event, value)"
                />
                <el-input-number
                  v-model="deviceLogicParams.positionMapping[mappingKeys[index] || key]"
                  :min="0"
                  :max="100"
                  size="small"
                  style="width: 120px; margin-right: 10px;"
                />
                <el-button
                  type="danger"
                  size="small"
                  @click="removePositionMapping(key)"
                >
                  åˆ é™¤
                </el-button>
              </div>
              <el-button type="primary" size="small" @click="addPositionMapping">
                æ·»åŠ ä½ç½®æ˜ å°„
              </el-button>
            </div>
          </el-form-item>
        </div>
        <!-- å¤§ç†ç‰‡è®¾å¤‡é€»è¾‘配置 -->
        <div v-if="deviceForm.deviceType === '大理片'">
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="玻璃尺寸">
                <el-input-number
                  v-model="deviceLogicParams.glassSize"
                  :min="100"
                  :max="5000"
                  :step="100"
                  style="width: 100%;"
                />
                <span class="form-tip">玻璃尺寸(mm)</span>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="处理时间(ms)">
                <el-input-number
                  v-model="deviceLogicParams.processingTime"
                  :min="1000"
                  :max="60000"
                  :step="1000"
                  style="width: 100%;"
                />
                <span class="form-tip">玻璃处理时间(毫秒)</span>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="自动处理">
                <el-switch v-model="deviceLogicParams.autoProcess" />
                <span class="form-tip">是否自动触发处理请求</span>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="最大重试次数">
                <el-input-number
                  v-model="deviceLogicParams.maxRetryCount"
                  :min="0"
                  :max="10"
                  :step="1"
                  style="width: 100%;"
                />
              </el-form-item>
            </el-col>
          </el-row>
        </div>
        <!-- çŽ»ç’ƒå­˜å‚¨è®¾å¤‡é€»è¾‘é…ç½® -->
        <div v-if="deviceForm.deviceType === '玻璃存储'">
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="存储容量">
                <el-input-number
                  v-model="deviceLogicParams.storageCapacity"
                  :min="1"
                  :max="1000"
                  :step="1"
                  style="width: 100%;"
                />
                <span class="form-tip">最大存储数量</span>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="取货模式">
                <el-select v-model="deviceLogicParams.retrievalMode" style="width: 100%;">
                  <el-option label="先进先出 (FIFO)" value="FIFO" />
                  <el-option label="后进先出 (LIFO)" value="LIFO" />
                  <el-option label="随机 (RANDOM)" value="RANDOM" />
                </el-select>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="自动存储">
                <el-switch v-model="deviceLogicParams.autoStore" />
                <span class="form-tip">是否自动触发存储请求</span>
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="自动取货">
                <el-switch v-model="deviceLogicParams.autoRetrieve" />
                <span class="form-tip">是否自动触发取货请求</span>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="最大重试次数">
                <el-input-number
                  v-model="deviceLogicParams.maxRetryCount"
                  :min="0"
                  :max="10"
                  :step="1"
                  style="width: 100%;"
                />
              </el-form-item>
            </el-col>
          </el-row>
        </div>
      </el-card>
      <!-- æè¿°ä¿¡æ¯ -->
      <el-card class="form-section" shadow="never" style="margin-top: 20px;">
        <template #header>
@@ -317,6 +518,28 @@
const saving = ref(false)
const testing = ref(false)
const testResult = ref(null)
// è®¾å¤‡é€»è¾‘参数(根据设备类型动态显示)
const deviceLogicParams = reactive({
  // ä¸Šå¤§è½¦å‚æ•°
  vehicleCapacity: 6000,
  glassIntervalMs: 1000,
  autoFeed: true,
  maxRetryCount: 5,
  positionMapping: {},
  // å¤§ç†ç‰‡å‚æ•°
  glassSize: 2000,
  processingTime: 5000,
  autoProcess: true,
  // çŽ»ç’ƒå­˜å‚¨å‚æ•°
  storageCapacity: 100,
  retrievalMode: 'FIFO',
  autoStore: true,
  autoRetrieve: true
})
// ä½ç½®æ˜ å°„的键数组(用于v-for)
const mappingKeys = ref([])
// è®¾å¤‡è¡¨å•数据
const getDefaultForm = () => ({
@@ -420,6 +643,27 @@
  emit('update:modelValue', newVal)
})
// ç›‘听PLC类型变化,自动设置通讯协议
watch(() => deviceForm.plcType, (newPlcType) => {
  // å¦‚果选择的是S7系列PLC,自动设置通讯协议为S7 Communication
  if (newPlcType && (newPlcType.startsWith('S') || newPlcType.includes('S7'))) {
    if (!deviceForm.protocolType || deviceForm.protocolType === '其他') {
      deviceForm.protocolType = 'S7 Communication'
    }
  }
})
// å¤„理通讯协议变化
const handleProtocolTypeChange = (value) => {
  // å¦‚果选择了非S7协议,但PLC类型是S7系列,给出提示
  if (value && value !== 'S7 Communication' && deviceForm.plcType) {
    const s7Types = ['S1200', 'S1500', 'S400', 'S300', 'S200', 'S200_SMART']
    if (s7Types.includes(deviceForm.plcType)) {
      ElMessage.warning('S7系列PLC通常使用S7 Communication协议,请确认协议选择是否正确')
    }
  }
}
// æ–¹æ³•定义
const parseJsonSafe = (str, defaultValue = null) => {
  if (!str) return defaultValue
@@ -453,11 +697,182 @@
  deviceForm.dbArea = plcConfig.dbArea || 'DB1'
  deviceForm.beginIndex = plcConfig.beginIndex ?? 0
  deviceForm.autoModeInterval = plcConfig.autoModeInterval ?? 5000
  // åŠ è½½é…ç½®å‚æ•°ï¼ˆä»Ž configJson)
  // å…¼å®¹ä¸¤ç§æ ¼å¼ï¼š
  // 1. æ•°ç»„格式:[{ paramKey, paramValue, description }]
  // 2. å¯¹è±¡æ ¼å¼ï¼ˆæ—§æ ¼å¼ï¼‰ï¼š{ fieldName: offset } - è‡ªåŠ¨è½¬æ¢ä¸ºæ•°ç»„æ ¼å¼
  loadConfigParams(data?.configJson)
  // åŠ è½½è®¾å¤‡é€»è¾‘å‚æ•°
  const deviceLogic = extraObj.deviceLogic || {}
  loadDeviceLogicParams(deviceLogic, data?.deviceType)
}
// åŠ è½½é…ç½®å‚æ•°ï¼ˆå…¼å®¹æ—§çš„å¯¹è±¡æ ¼å¼ï¼‰
const loadConfigParams = (configJson) => {
  if (!configJson) {
    deviceForm.configParams = []
    return
  }
  try {
    const parsed = typeof configJson === 'string' ? JSON.parse(configJson) : configJson
    // å¦‚果是数组格式,直接使用
    if (Array.isArray(parsed)) {
      deviceForm.configParams = parsed
    }
    // å¦‚果是对象格式(字段名 â†’ åç§»é‡ï¼‰ï¼Œè½¬æ¢ä¸ºæ•°ç»„格式
    else if (typeof parsed === 'object' && parsed !== null) {
      // å­—段名到中文描述的映射
      const fieldDescriptionMap = {
        'plcRequest': 'PLC请求字',
        'inPosition': '进片位置',
        'plcGlassId1': '玻璃id1',
        'plcGlassId2': '玻璃id2',
        'plcGlassId3': '玻璃id3',
        'plcGlassId4': '玻璃id4',
        'plcGlassId5': '玻璃id5',
        'plcGlassId6': '玻璃id6',
        'plcGlassCount': '玻璃数量',
        'onlineState': '联机状态',
        'plcReport': 'PLC汇报',
        'state1': '状态1',
        'state2': '状态2',
        'state3': '状态3',
        'state4': '状态4',
        'state5': '状态5',
        'state6': '状态6',
        'mesSend': 'MES发送',
        'mesConfirm': 'MES确认',
        'trainInfo': '列车信息',
        'start1': '起始1',
        'start2': '起始2',
        'start3': '起始3',
        'start4': '起始4',
        'start5': '起始5',
        'start6': '起始6',
        'target1': '目标1',
        'target2': '目标2',
        'target3': '目标3',
        'target4': '目标4',
        'target5': '目标5',
        'target6': '目标6',
        'mesWidth1': 'MES宽度1',
        'mesWidth2': 'MES宽度2',
        'mesWidth3': 'MES宽度3',
        'mesWidth4': 'MES宽度4',
        'mesWidth5': 'MES宽度5',
        'mesWidth6': 'MES宽度6',
        'mesHeight1': 'MES高度1',
        'mesHeight2': 'MES高度2',
        'mesHeight3': 'MES高度3',
        'mesHeight4': 'MES高度4',
        'mesHeight5': 'MES高度5',
        'mesHeight6': 'MES高度6',
        'mesThickness1': 'MES厚度1',
        'mesThickness2': 'MES厚度2',
        'mesThickness3': 'MES厚度3',
        'mesThickness4': 'MES厚度4',
        'mesThickness5': 'MES厚度5',
        'mesThickness6': 'MES厚度6',
        'edgeDistance1': '边缘距离1',
        'edgeDistance2': '边缘距离2',
        'edgeDistance3': '边缘距离3',
        'edgeDistance4': '边缘距离4',
        'edgeDistance5': '边缘距离5',
        'edgeDistance6': '边缘距离6',
        'targetEdgeDistance1': '目标边缘距离1',
        'targetEdgeDistance2': '目标边缘距离2',
        'targetEdgeDistance3': '目标边缘距离3',
        'targetEdgeDistance4': '目标边缘距离4',
        'targetEdgeDistance5': '目标边缘距离5',
        'targetEdgeDistance6': '目标边缘距离6',
        'alarmInfo': '报警信息'
      }
      // è½¬æ¢ä¸ºæ•°ç»„格式
      deviceForm.configParams = Object.keys(parsed).map(fieldName => ({
        paramKey: fieldName,
        paramValue: String(parsed[fieldName]),
        description: fieldDescriptionMap[fieldName] || fieldName
      }))
    } else {
      deviceForm.configParams = []
    }
  } catch (error) {
    console.warn('解析configJson失败', error)
    deviceForm.configParams = []
  }
}
// åŠ è½½è®¾å¤‡é€»è¾‘å‚æ•°
const loadDeviceLogicParams = (deviceLogic, deviceType) => {
  if (deviceType === '上大车') {
    deviceLogicParams.vehicleCapacity = deviceLogic.vehicleCapacity ?? 6000
    deviceLogicParams.glassIntervalMs = deviceLogic.glassIntervalMs ?? 1000
    deviceLogicParams.autoFeed = deviceLogic.autoFeed ?? true
    deviceLogicParams.maxRetryCount = deviceLogic.maxRetryCount ?? 5
    deviceLogicParams.positionMapping = deviceLogic.positionMapping || {}
    mappingKeys.value = Object.keys(deviceLogicParams.positionMapping)
  } else if (deviceType === '大理片') {
    deviceLogicParams.glassSize = deviceLogic.glassSize ?? 2000
    deviceLogicParams.processingTime = deviceLogic.processingTime ?? 5000
    deviceLogicParams.autoProcess = deviceLogic.autoProcess ?? true
    deviceLogicParams.maxRetryCount = deviceLogic.maxRetryCount ?? 3
  } else if (deviceType === '玻璃存储') {
    deviceLogicParams.storageCapacity = deviceLogic.storageCapacity ?? 100
    deviceLogicParams.retrievalMode = deviceLogic.retrievalMode || 'FIFO'
    deviceLogicParams.autoStore = deviceLogic.autoStore ?? true
    deviceLogicParams.autoRetrieve = deviceLogic.autoRetrieve ?? true
    deviceLogicParams.maxRetryCount = deviceLogic.maxRetryCount ?? 3
  }
}
// ä½ç½®æ˜ å°„相关方法
const addPositionMapping = () => {
  const newKey = `POS${Object.keys(deviceLogicParams.positionMapping).length + 1}`
  deviceLogicParams.positionMapping[newKey] = 1
  mappingKeys.value.push(newKey)
}
const removePositionMapping = (key) => {
  delete deviceLogicParams.positionMapping[key]
  mappingKeys.value = mappingKeys.value.filter(k => k !== key)
}
const updatePositionMapping = (index, newKey, oldValue) => {
  const oldKey = mappingKeys.value[index]
  if (oldKey && oldKey !== newKey) {
    delete deviceLogicParams.positionMapping[oldKey]
  }
  mappingKeys.value[index] = newKey
  if (newKey) {
    deviceLogicParams.positionMapping[newKey] = oldValue || 1
  }
}
const resetForm = () => {
  Object.assign(deviceForm, getDefaultForm())
  deviceFormRef.value?.clearValidate()
  // é‡ç½®è®¾å¤‡é€»è¾‘参数
  deviceLogicParams.vehicleCapacity = 6000
  deviceLogicParams.glassIntervalMs = 1000
  deviceLogicParams.autoFeed = true
  deviceLogicParams.maxRetryCount = 5
  deviceLogicParams.positionMapping = {}
  mappingKeys.value = []
  deviceLogicParams.glassSize = 2000
  deviceLogicParams.processingTime = 5000
  deviceLogicParams.autoProcess = true
  deviceLogicParams.storageCapacity = 100
  deviceLogicParams.retrievalMode = 'FIFO'
  deviceLogicParams.autoStore = true
  deviceLogicParams.autoRetrieve = true
}
const addConfigParam = () => {
@@ -531,6 +946,44 @@
    plcType: deviceForm.plcType
  }
    // ä¿å­˜è®¾å¤‡é€»è¾‘参数
    const deviceLogic = {}
    if (deviceForm.deviceType === '上大车') {
      deviceLogic.vehicleCapacity = deviceLogicParams.vehicleCapacity
      deviceLogic.glassIntervalMs = deviceLogicParams.glassIntervalMs
      deviceLogic.autoFeed = deviceLogicParams.autoFeed
      deviceLogic.maxRetryCount = deviceLogicParams.maxRetryCount
      deviceLogic.positionMapping = deviceLogicParams.positionMapping
    } else if (deviceForm.deviceType === '大理片') {
      deviceLogic.glassSize = deviceLogicParams.glassSize
      deviceLogic.processingTime = deviceLogicParams.processingTime
      deviceLogic.autoProcess = deviceLogicParams.autoProcess
      deviceLogic.maxRetryCount = deviceLogicParams.maxRetryCount
    } else if (deviceForm.deviceType === '玻璃存储') {
      deviceLogic.storageCapacity = deviceLogicParams.storageCapacity
      deviceLogic.retrievalMode = deviceLogicParams.retrievalMode
      deviceLogic.autoStore = deviceLogicParams.autoStore
      deviceLogic.autoRetrieve = deviceLogicParams.autoRetrieve
      deviceLogic.maxRetryCount = deviceLogicParams.maxRetryCount
    }
    if (Object.keys(deviceLogic).length > 0) {
      extraObj.deviceLogic = deviceLogic
    }
    // æž„建 configJson:将 configParams æ•°ç»„转换为 JSON å­—符串
    // configParams ç»“æž„: [{ paramKey: '', paramValue: '', description: '' }]
    let configJsonValue = null
    if (deviceForm.configParams && deviceForm.configParams.length > 0) {
      // è¿‡æ»¤æŽ‰ç©ºå‚æ•°
      const validParams = deviceForm.configParams.filter(
        param => param.paramKey && param.paramKey.trim() !== ''
      )
      if (validParams.length > 0) {
        configJsonValue = JSON.stringify(validParams)
      }
  }
    const saveData = {
      deviceName: deviceForm.deviceName,
      deviceCode: deviceForm.deviceCode,
@@ -542,9 +995,7 @@
      isPrimary: deviceForm.isPrimary,
      enabled: deviceForm.enabled,
      description: deviceForm.description,
      configJson: deviceForm.configParams.length > 0
        ? JSON.stringify(deviceForm.configParams)
        : null,
      configJson: configJsonValue,  // ä¿å­˜é…ç½®å‚æ•°JSON
      extraParams: JSON.stringify(extraObj)
    }
@@ -562,6 +1013,18 @@
    handleClose()
  } catch (error) {
    console.error('保存设备配置失败:', error)
    // å¦‚果是表单验证错误,显示更详细的错误信息
    if (error && typeof error === 'object' && !error.response) {
      const errorFields = Object.keys(error)
      if (errorFields.length > 0) {
        const firstError = error[errorFields[0]]
        const errorMessage = Array.isArray(firstError)
          ? firstError[0]?.message || firstError[0]
          : firstError?.message || firstError
        ElMessage.error(`表单验证失败: ${errorMessage}`)
        return
      }
    }
    ElMessage.error(isEdit.value ? '更新设备配置失败' : '创建设备配置失败')
  } finally {
    saving.value = false
@@ -647,4 +1110,18 @@
:deep(.el-card__body) {
  padding: 20px;
}
.position-mapping {
  width: 100%;
}
.mapping-item {
  display: flex;
  align-items: center;
  margin-bottom: 12px;
  padding: 12px;
  border: 1px solid #ebeef5;
  border-radius: 6px;
  background-color: #fafafa;
}
</style>
mes-web/src/views/device/DeviceGroupEditDialog.vue
@@ -600,19 +600,17 @@
      customParams: form.customParams
    }
    
    let response
    if (isEdit.value) {
      response = await deviceGroupApi.update(props.data.id, config)
    } else {
      response = await deviceGroupApi.create(config)
    }
    if (response.success) {
    const response = isEdit.value
      ? await deviceGroupApi.update(props.data.id, config)
      : await deviceGroupApi.create(config)
    const ok = response && (response.success || response.code === 200 || response.isSuccess)
    if (ok) {
      ElMessage.success(isEdit.value ? '设备组更新成功' : '设备组创建成功')
      emit('success', isEdit.value ? 'update' : 'create')
      handleClose()
    } else {
      ElMessage.error(response.message || (isEdit.value ? '更新失败' : '创建失败'))
      ElMessage.error(response?.message || (isEdit.value ? '更新失败' : '创建失败'))
    }
  } catch (error) {
    console.error('保存配置失败:', error)
mes-web/src/views/device/DeviceGroupList.vue
@@ -398,10 +398,10 @@
  try {
    const groupId = row.id || row.groupId
    if (row.enabled) {
      await deviceGroupApi.enable({ groupId })
      await deviceGroupApi.enable(groupId)
      ElMessage.success('设备组启用成功')
    } else {
      await deviceGroupApi.disable({ groupId })
      await deviceGroupApi.disable(groupId)
      ElMessage.success('设备组禁用成功')
    }
    emit('refresh-statistics')
@@ -416,7 +416,7 @@
const batchEnable = async () => {
  try {
    const groupIds = selectedGroups.value.map(item => item.id || item.groupId)
    await deviceGroupApi.batchEnable({ groupIds })
    await deviceGroupApi.batchEnable(groupIds)
    ElMessage.success(`成功启用 ${groupIds.length} ä¸ªè®¾å¤‡ç»„`)
    clearSelection()
    loadGroupList()
@@ -430,7 +430,7 @@
const batchDisable = async () => {
  try {
    const groupIds = selectedGroups.value.map(item => item.id || item.groupId)
    await deviceGroupApi.batchDisable({ groupIds })
    await deviceGroupApi.batchDisable(groupIds)
    ElMessage.success(`成功禁用 ${groupIds.length} ä¸ªè®¾å¤‡ç»„`)
    clearSelection()
    loadGroupList()
mes-web/src/views/plcTest/MultiDeviceWorkbench.vue
New file
@@ -0,0 +1,67 @@
<template>
  <div class="multi-device-workbench">
    <div class="main-grid">
      <div class="left-panel">
        <GroupList @select="handleGroupSelect" />
      </div>
      <div class="right-panel">
        <TaskOrchestration :group="selectedGroup" @task-started="refreshMonitor" />
        <ExecutionMonitor ref="monitorRef" :group-id="selectedGroupId" class="monitor-panel" />
      </div>
    </div>
  </div>
</template>
<script setup>
import { computed, ref } from 'vue'
import GroupList from './components/DeviceGroup/GroupList.vue'
import TaskOrchestration from './components/MultiDeviceTest/TaskOrchestration.vue'
import ExecutionMonitor from './components/MultiDeviceTest/ExecutionMonitor.vue'
const selectedGroup = ref(null)
const monitorRef = ref(null)
const selectedGroupId = computed(() => {
  if (!selectedGroup.value) return null
  return selectedGroup.value.id || selectedGroup.value.groupId
})
const handleGroupSelect = (group) => {
  selectedGroup.value = group
}
const refreshMonitor = () => {
  monitorRef.value?.fetchTasks?.()
}
</script>
<style scoped>
.multi-device-workbench {
  padding: 24px;
  min-height: 100%;
  background: linear-gradient(180deg, #f6f9ff 0%, #f4f6fb 100%);
}
.main-grid {
  display: grid;
  grid-template-columns: 360px 1fr;
  gap: 24px;
}
.right-panel {
  display: flex;
  flex-direction: column;
  gap: 24px;
}
.monitor-panel {
  flex: 1;
}
@media (max-width: 1200px) {
  .main-grid {
    grid-template-columns: 1fr;
  }
}
</style>
mes-web/src/views/plcTest/components/DeviceGroup/GroupList.vue
New file
@@ -0,0 +1,144 @@
<template>
  <div class="group-list-panel">
    <div class="panel-header">
      <div>
        <h3>设备组列表</h3>
        <p>选择一个设备组进行编排测试</p>
      </div>
      <div class="actions">
        <el-input
          v-model="filters.keyword"
          placeholder="搜索设备组名称/编码"
          clearable
          @clear="fetchGroups"
          @keyup.enter="fetchGroups"
          class="search-input"
        >
          <template #prefix>
            <el-icon><Search /></el-icon>
          </template>
        </el-input>
        <el-button :loading="loading" @click="fetchGroups">
          <el-icon><Refresh /></el-icon>
        </el-button>
      </div>
    </div>
    <el-table
      v-loading="loading"
      :data="groups"
      height="320"
      stripe
      class="group-table"
      @row-click="handleRowClick"
    >
      <el-table-column prop="groupName" label="设备组" min-width="160" />
      <el-table-column prop="groupCode" label="编码" min-width="120" />
      <el-table-column prop="status" label="状态" width="100">
        <template #default="{ row }">
          <el-tag :type="formatStatus(row.status).type">{{ formatStatus(row.status).label }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="deviceCount" label="设备数量" width="100" />
      <el-table-column label="最后更新时间" min-width="160">
        <template #default="{ row }">
          {{ row.updatedTime || row.createTime || '-' }}
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue'
import { Refresh, Search } from '@element-plus/icons-vue'
import { deviceGroupApi } from '@/api/device/deviceManagement'
const emit = defineEmits(['select'])
const loading = ref(false)
const groups = ref([])
const filters = reactive({
  keyword: ''
})
const fetchGroups = async () => {
  try {
    loading.value = true
    const payload = {
      page: 1,
      size: 20,
      keyword: filters.keyword
    }
    const { data } = await deviceGroupApi.getList(payload)
    const records = data?.records || data?.data || data || []
    groups.value = Array.isArray(records) ? records : []
  } finally {
    loading.value = false
  }
}
const handleRowClick = (row) => {
  emit('select', row)
}
const formatStatus = (status) => {
  const value = typeof status === 'number' ? status : String(status || '').toUpperCase()
  if (value === 1 || value === '启用' || value === 'ENABLED') {
    return { label: '启用', type: 'success' }
  }
  if (value === 0 || value === '停用' || value === 'DISABLED') {
    return { label: '停用', type: 'info' }
  }
  if (value === 2 || value === '维护中' || value === 'MAINTENANCE') {
    return { label: '维护', type: 'warning' }
  }
  return { label: status || '未知', type: 'default' }
}
onMounted(fetchGroups)
</script>
<style scoped>
.group-list-panel {
  background: #fff;
  border-radius: 12px;
  padding: 20px;
  box-shadow: 0 8px 32px rgba(15, 18, 63, 0.08);
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.panel-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.panel-header h3 {
  margin: 0;
  font-size: 18px;
}
.panel-header p {
  margin: 2px 0 0;
  color: #909399;
  font-size: 13px;
}
.actions {
  display: flex;
  align-items: center;
  gap: 12px;
}
.search-input {
  width: 240px;
}
.group-table {
  flex: 1;
}
</style>
mes-web/src/views/plcTest/components/MultiDeviceTest/ExecutionMonitor.vue
New file
@@ -0,0 +1,176 @@
<template>
  <div class="execution-monitor">
    <div class="panel-header">
      <div>
        <h3>任务执行监控</h3>
        <p>实时查看最新的多设备任务</p>
      </div>
      <el-button :loading="loading" @click="fetchTasks">
        <el-icon><Refresh /></el-icon>
        åˆ·æ–°
      </el-button>
    </div>
    <el-table
      v-loading="loading"
      :data="tasks"
      height="300"
      stripe
      @row-click="handleRowClick"
    >
      <el-table-column prop="taskId" label="任务编号" min-width="160" />
      <el-table-column prop="groupId" label="设备组ID" width="120" />
      <el-table-column prop="status" label="状态" width="120">
        <template #default="{ row }">
          <el-tag :type="statusType(row.status)">{{ row.status }}</el-tag>
        </template>
      </el-table-column>
      <el-table-column prop="currentStep" label="进度" width="120">
        <template #default="{ row }">
          {{ 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>
    <el-drawer v-model="drawerVisible" size="40%" title="任务步骤详情">
      <el-timeline v-loading="stepsLoading" :reverse="false">
        <el-timeline-item
          v-for="step in steps"
          :key="step.id"
          :timestamp="step.startTime || '-'"
          :type="step.status === 'COMPLETED' ? 'success' : step.status === 'FAILED' ? 'danger' : 'primary'"
        >
          <div class="step-title">{{ step.stepName }}</div>
          <div class="step-desc">状态:{{ step.status }}</div>
          <div class="step-desc">耗时:{{ formatDuration(step.durationMs) }}</div>
          <div class="step-desc" v-if="step.errorMessage">
            é”™è¯¯ï¼š{{ step.errorMessage }}
          </div>
        </el-timeline-item>
      </el-timeline>
    </el-drawer>
  </div>
</template>
<script setup>
import { onMounted, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import { multiDeviceTaskApi } from '@/api/device/multiDeviceTask'
const props = defineProps({
  groupId: {
    type: [String, Number],
    default: null
  }
})
const loading = ref(false)
const tasks = ref([])
const drawerVisible = ref(false)
const stepsLoading = ref(false)
const steps = ref([])
const currentTaskId = ref(null)
const fetchTasks = async () => {
  try {
    loading.value = true
    const { data } = await multiDeviceTaskApi.getTaskList({
      groupId: props.groupId,
      page: 1,
      size: 10
    })
    tasks.value = data?.records || data?.data || data || []
  } catch (error) {
    ElMessage.error(error?.message || '加载任务列表失败')
  } finally {
    loading.value = false
  }
}
const handleRowClick = async (row) => {
  currentTaskId.value = row.taskId
  drawerVisible.value = true
  stepsLoading.value = true
  try {
    const { data } = await multiDeviceTaskApi.getTaskSteps(row.taskId)
    steps.value = Array.isArray(data) ? data : (data?.data || [])
  } catch (error) {
    ElMessage.error(error?.message || '加载任务步骤失败')
  } finally {
    stepsLoading.value = false
  }
}
const statusType = (status) => {
  switch ((status || '').toUpperCase()) {
    case 'COMPLETED':
      return 'success'
    case 'FAILED':
      return 'danger'
    case 'RUNNING':
      return 'warning'
    default:
      return 'info'
  }
}
const formatDuration = (ms) => {
  if (!ms) return '-'
  if (ms < 1000) return `${ms} ms`
  return `${(ms / 1000).toFixed(1)} s`
}
watch(
  () => props.groupId,
  () => {
    fetchTasks()
  },
  { immediate: true }
)
onMounted(fetchTasks)
defineExpose({
  fetchTasks
})
</script>
<style scoped>
.execution-monitor {
  background: #fff;
  border-radius: 12px;
  padding: 20px;
  box-shadow: 0 8px 32px rgba(15, 18, 63, 0.08);
}
.panel-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
}
.panel-header h3 {
  margin: 0;
}
.panel-header p {
  margin: 4px 0 0;
  color: #909399;
  font-size: 13px;
}
.step-title {
  font-weight: 600;
  margin-bottom: 4px;
}
.step-desc {
  font-size: 13px;
  color: #606266;
}
</style>
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue
New file
@@ -0,0 +1,135 @@
<template>
  <div class="task-orchestration">
    <div class="panel-header">
      <div>
        <h3>多设备测试编排</h3>
        <p v-if="group">当前设备组:{{ group.groupName }}({{ group.deviceCount || '-' }} å°è®¾å¤‡ï¼‰</p>
        <p v-else class="warning">请先在左侧选择一个设备组</p>
      </div>
      <el-button type="primary" :disabled="!group" :loading="loading" @click="handleSubmit">
        <el-icon><Promotion /></el-icon>
        å¯åŠ¨æµ‹è¯•
      </el-button>
    </div>
    <el-form :model="form" label-width="120px">
      <el-form-item label="玻璃ID列表">
        <el-input
          v-model="glassIdsInput"
          type="textarea"
          :rows="4"
          placeholder="请输入玻璃条码,支持多行或逗号分隔"
        />
      </el-form-item>
      <el-form-item label="位置编码">
        <el-input v-model="form.positionCode" placeholder="例如:POS1" />
      </el-form-item>
      <el-form-item label="存储位置">
        <el-input-number v-model="form.storagePosition" :min="1" :max="200" />
      </el-form-item>
      <el-form-item label="执行间隔 (ms)">
        <el-input-number v-model="form.executionInterval" :min="100" :max="10000" />
      </el-form-item>
    </el-form>
  </div>
</template>
<script setup>
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Promotion } from '@element-plus/icons-vue'
import { multiDeviceTaskApi } from '@/api/device/multiDeviceTask'
const props = defineProps({
  group: {
    type: Object,
    default: null
  }
})
const emit = defineEmits(['task-started'])
const form = reactive({
  positionCode: '',
  storagePosition: null,
  executionInterval: 1000
})
const glassIdsInput = ref('')
const loading = ref(false)
watch(
  () => props.group,
  () => {
    glassIdsInput.value = ''
  }
)
const glassIds = computed(() => {
  if (!glassIdsInput.value) return []
  return glassIdsInput.value
    .split(/[\n,,]/)
    .map((item) => item.trim())
    .filter((item) => item.length > 0)
})
const handleSubmit = async () => {
  if (!props.group) {
    ElMessage.warning('请先选择设备组')
    return
  }
  if (glassIds.value.length === 0) {
    ElMessage.warning('请至少输入一个玻璃ID')
    return
  }
  try {
    loading.value = true
    await multiDeviceTaskApi.startTask({
      groupId: props.group.id || props.group.groupId,
      parameters: {
        glassIds: glassIds.value,
        positionCode: form.positionCode || null,
        storagePosition: form.storagePosition,
        executionInterval: form.executionInterval
      }
    })
    ElMessage.success('任务已启动')
    emit('task-started')
  } catch (error) {
    ElMessage.error(error?.message || '任务启动失败')
  } finally {
    loading.value = false
  }
}
</script>
<style scoped>
.task-orchestration {
  background: #fff;
  border-radius: 12px;
  padding: 20px;
  box-shadow: 0 8px 32px rgba(15, 18, 63, 0.08);
}
.panel-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
}
.panel-header h3 {
  margin: 0;
}
.panel-header p {
  margin: 4px 0 0;
  color: #909399;
  font-size: 13px;
}
.panel-header .warning {
  color: #f56c6c;
}
</style>