huang
2 天以前 9571229a2013472dc701ecf5767f2873b36d8f90
修复导入Excel功能工程号自增; 添加选择工程号自动填写玻璃id列表
13个文件已修改
745 ■■■■ 已修改文件
mes-processes/mes-plcSend/src/main/java/com/mes/device/controller/GlassInfoImportController.java 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/GlassInfo.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/mapper/DeviceGlassInfoMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/mapper/EngineeringSequenceMapper.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/GlassInfoService.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/EngineeringSequenceServiceImpl.java 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/GlassInfoServiceImpl.java 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/scanner/handler/HorizontalScannerLogicHandler.java 47 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/transfer/handler/HorizontalTransferLogicHandler.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/api/engineering.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/device/components/DeviceLogicConfig/WorkstationTransferConfig.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mes-processes/mes-plcSend/src/main/java/com/mes/device/controller/GlassInfoImportController.java
@@ -1,19 +1,21 @@
package com.mes.device.controller;
import com.mes.device.entity.EngineeringSequence;
import com.mes.device.entity.GlassInfo;
import com.mes.device.service.EngineeringSequenceService;
import com.mes.device.service.GlassInfoService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.CollectionUtils;
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 org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
 * @author :huang
@@ -27,6 +29,7 @@
public class GlassInfoImportController {
    private final GlassInfoService glassInfoService;
    private final EngineeringSequenceService engineeringSequenceService;
    private final RestTemplate restTemplate = new RestTemplate();
@@ -54,12 +57,56 @@
        Map<String, Object> payload = glassInfoService.buildEngineerImportPayload(excelRows);
        log.info("构建的 MES 导入数据: {}", payload);
        // 从payload中提取工程号(payload中使用的是engineerId)
        String engineeringId = (String) payload.get("engineerId");
        if (engineeringId == null || engineeringId.isEmpty()) {
            // 如果payload中没有engineerId,尝试从glassInfolList中获取
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> glassInfoList = (List<Map<String, Object>>) payload.get("glassInfolList");
            if (glassInfoList != null && !glassInfoList.isEmpty()) {
                Object firstEngineerId = glassInfoList.get(0).get("engineerId");
                if (firstEngineerId != null) {
                    engineeringId = firstEngineerId.toString();
                }
            }
        }
        String mesEngineeringImportUrl = glassInfoService.getMesEngineeringImportUrl();
        try {
            ResponseEntity<Map> mesResp = restTemplate.postForEntity(mesEngineeringImportUrl, payload, Map.class);
            Map<String, Object> mesBody = mesResp.getBody();
            // 检查MES响应是否真正成功(不仅检查HTTP状态码,还要检查响应体中的code字段)
            boolean mesSuccess = false;
            if (mesResp.getStatusCode().is2xxSuccessful() && mesBody != null) {
                Object codeObj = mesBody.get("code");
                if (codeObj != null) {
                    int code = codeObj instanceof Number ? ((Number) codeObj).intValue() :
                              Integer.parseInt(String.valueOf(codeObj));
                    // MES成功通常返回code=200或0
                    mesSuccess = (code == 200 || code == 0);
                } else {
                    // 如果没有code字段,认为HTTP 2xx就是成功
                    mesSuccess = true;
                }
            }
            // 只有MES导入真正成功时,才保存玻璃信息到本地数据库,并关联engineering_id
            if (mesSuccess && engineeringId != null) {
                try {
                    glassInfoService.saveGlassInfosFromExcel(excelRows, engineeringId);
                    log.info("MES导入成功,已保存玻璃信息到本地数据库,工程号: {}", engineeringId);
                } catch (Exception e) {
                    log.error("MES导入成功,但保存玻璃信息到本地数据库失败: engineeringId={}", engineeringId, e);
                    // 即使保存失败,也返回MES的成功响应,但记录错误日志
                }
            } else {
                log.warn("MES导入未成功,不保存玻璃信息到本地数据库: engineeringId={}, mesSuccess={}", engineeringId, mesSuccess);
            }
            // 直接返回 MES 的响应,让前端根据响应体中的 code 字段判断是否成功
            return ResponseEntity.status(mesResp.getStatusCode()).body(mesResp.getBody());
            return ResponseEntity.status(mesResp.getStatusCode()).body(mesBody);
        } catch (org.springframework.web.client.ResourceAccessException e) {
            // 连接超时或无法连接
            log.error("转发 MES 导入接口失败(连接问题) url={}, error={}", mesEngineeringImportUrl, e.getMessage(), e);
@@ -78,6 +125,106 @@
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
        }
    }
    /**
     * 查询所有工程号列表
     */
    @GetMapping("/engineering/list")
    public ResponseEntity<?> getEngineeringList() {
        try {
            List<EngineeringSequence> list = engineeringSequenceService.list();
            List<Map<String, Object>> result = list.stream()
                    .map(seq -> {
                        Map<String, Object> map = new HashMap<>();
                        map.put("engineeringId", seq.getEngineeringId());
                        map.put("date", seq.getDate());
                        map.put("sequence", seq.getSequence());
                        return map;
                    })
                    .collect(Collectors.toList());
            return ResponseEntity.ok(result);
        } catch (Exception e) {
            log.error("查询工程号列表失败", e);
            Map<String, Object> errorResponse = new HashMap<>();
            errorResponse.put("code", 500);
            errorResponse.put("message", "查询工程号列表失败: " + e.getMessage());
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
        }
    }
    /**
     * 根据工程号查询对应的玻璃ID列表
     */
    @GetMapping("/engineering/{engineeringId}/glassIds")
    public ResponseEntity<?> getGlassIdsByEngineeringId(@PathVariable String engineeringId) {
        try {
            List<GlassInfo> glassInfos = glassInfoService.getGlassInfosByEngineeringId(engineeringId);
            List<String> glassIds = glassInfos.stream()
                    .map(GlassInfo::getGlassId)
                    .filter(id -> id != null && !id.trim().isEmpty())
                    .collect(Collectors.toList());
            Map<String, Object> result = new HashMap<>();
            result.put("engineeringId", engineeringId);
            result.put("glassIds", glassIds);
            result.put("count", glassIds.size());
            return ResponseEntity.ok(result);
        } catch (Exception e) {
            log.error("根据工程号查询玻璃ID列表失败: engineeringId={}", engineeringId, e);
            Map<String, Object> errorResponse = new HashMap<>();
            errorResponse.put("code", 500);
            errorResponse.put("message", "查询玻璃ID列表失败: " + e.getMessage());
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
        }
    }
    /**
     * 删除工程号及其关联的玻璃信息
     */
    @DeleteMapping("/engineering/{engineeringId}")
    public ResponseEntity<?> deleteEngineering(@PathVariable String engineeringId) {
        try {
            // 1. 删除glass_info表中对应工程号的玻璃信息
            int deletedGlassCount = glassInfoService.deleteGlassInfosByEngineeringId(engineeringId);
            // 2. 删除engineering_sequence表中的工程号记录(逻辑删除)
            EngineeringSequence sequence = engineeringSequenceService.getOne(
                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<EngineeringSequence>()
                    .eq(EngineeringSequence::getEngineeringId, engineeringId)
                    .eq(EngineeringSequence::getIsDeleted, 0) // 只查询未删除的记录
                    .last("LIMIT 1")
            );
            boolean deletedSequence = false;
            if (sequence != null) {
                // removeById 会自动使用逻辑删除(因为实体类有 @TableLogic 注解)
                deletedSequence = engineeringSequenceService.removeById(sequence.getId());
                log.info("已删除工程号记录: engineeringId={}, sequenceId={}", engineeringId, sequence.getId());
            } else {
                log.warn("未找到工程号记录: engineeringId={}", engineeringId);
            }
            Map<String, Object> result = new HashMap<>();
            result.put("engineeringId", engineeringId);
            result.put("deletedGlassCount", deletedGlassCount);
            result.put("deletedSequence", deletedSequence);
            result.put("success", true);
            result.put("message", String.format("已删除工程号 %s,共删除 %d 条玻璃信息", engineeringId, deletedGlassCount));
            log.info("删除工程号成功: engineeringId={}, deletedGlassCount={}, deletedSequence={}",
                    engineeringId, deletedGlassCount, deletedSequence);
            return ResponseEntity.ok(result);
        } catch (Exception e) {
            log.error("删除工程号失败: engineeringId={}", engineeringId, e);
            Map<String, Object> errorResponse = new HashMap<>();
            errorResponse.put("code", 500);
            errorResponse.put("message", "删除工程号失败: " + e.getMessage());
            errorResponse.put("success", false);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/entity/GlassInfo.java
@@ -65,6 +65,14 @@
    @TableField("work_line")
    private Integer workLine;
    @ApiModelProperty(value = "工程号(关联 engineering_sequence 表)", example = "P25010801")
    @TableField("engineering_id")
    private String engineeringId;
    @ApiModelProperty(value = "状态:0-初始保存, 1-已扫码交互", example = "0")
    @TableField("state")
    private Integer state;
    @ApiModelProperty(value = "创建时间")
    @TableField(value = "created_time", fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
mes-processes/mes-plcSend/src/main/java/com/mes/device/mapper/DeviceGlassInfoMapper.java
@@ -42,5 +42,14 @@
     */
    @Select("SELECT * FROM glass_info WHERE status = #{status} AND is_deleted = 0 ORDER BY created_time DESC")
    List<GlassInfo> selectByStatus(@Param("status") String status);
    /**
     * 根据工程号查询玻璃ID列表
     *
     * @param engineeringId 工程号
     * @return 玻璃信息列表
     */
    @Select("SELECT * FROM glass_info WHERE engineering_id = #{engineeringId} AND is_deleted = 0")
    List<GlassInfo> selectByEngineeringId(@Param("engineeringId") String engineeringId);
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/mapper/EngineeringSequenceMapper.java
@@ -23,17 +23,8 @@
     * @param date 日期
     * @return 最大序号,如果没有记录返回0
     */
    @Select("SELECT COALESCE(MAX(sequence), 0) FROM engineering_sequence WHERE date = #{date} AND is_deleted = 0")
    @Select("SELECT COALESCE(MAX(sequence), 0) FROM engineering_sequence WHERE DATE(date) = DATE(#{date})")
    Integer selectMaxSequenceByDate(@Param("date") Date date);
    /**
     * 查询指定日期的最大序号并加行锁,避免并发生成重复序号
     *
     * @param date 日期
     * @return 最大序号,如果没有记录返回0
     */
    @Select("SELECT COALESCE(MAX(sequence), 0) FROM engineering_sequence WHERE date = #{date} AND is_deleted = 0 FOR UPDATE")
    Integer selectMaxSequenceByDateForUpdate(@Param("date") Date date);
    /**
     * 根据工程号查询工程序号信息
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/GlassInfoService.java
@@ -91,5 +91,40 @@
     * @return 符合 MES 接口要求的请求体 Map
     */
    Map<String, Object> buildEngineerImportPayload(List<Map<String, Object>> excelRows);
    /**
     * 根据工程号查询玻璃信息列表
     *
     * @param engineeringId 工程号
     * @return 玻璃信息列表
     */
    List<GlassInfo> getGlassInfosByEngineeringId(String engineeringId);
    /**
     * 从Excel数据保存玻璃信息到本地数据库,并关联engineering_id
     *
     * @param excelRows Excel行数据
     * @param engineeringId 工程号
     */
    void saveGlassInfosFromExcel(List<Map<String, Object>> excelRows, String engineeringId);
    /**
     * 扫码交互后更新玻璃信息状态(将state从0改为1)
     *
     * @param glassId 玻璃ID
     * @param width 宽度(可选)
     * @param height 高度(可选)
     * @param workLine 产线编号(可选)
     * @return 是否更新成功
     */
    boolean updateGlassStateAfterScan(String glassId, Integer width, Integer height, Integer workLine);
    /**
     * 根据工程号删除玻璃信息
     *
     * @param engineeringId 工程号
     * @return 删除的玻璃数量
     */
    int deleteGlassInfosByEngineeringId(String engineeringId);
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/EngineeringSequenceServiceImpl.java
@@ -16,66 +16,54 @@
/**
 * 工程序号信息服务实现类
 *
 *
 * @author mes
 * @since 2024-11-20
 * @since 2025-11-20
 */
@Slf4j
@Service
public class EngineeringSequenceServiceImpl extends ServiceImpl<EngineeringSequenceMapper, EngineeringSequence> implements EngineeringSequenceService {
    // 日期格式化器(线程不安全,使用ThreadLocal保证线程安全)
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyMMdd");
    // 修复:使用ThreadLocal保证DateTimeFormatter的线程安全
    private static final ThreadLocal<DateTimeFormatter> DATE_FORMATTER_THREAD_LOCAL = ThreadLocal.withInitial(
            () -> DateTimeFormatter.ofPattern("yyMMdd")
    );
    // 重试间隔(毫秒),使用随机数避免并发请求同时重试
    private static final int RETRY_INTERVAL_MIN = 50;
    private static final int RETRY_INTERVAL_MAX = 200;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String generateAndSaveEngineeringId(Date date) {
        // 乐观重试,防止并发写入造成重复键
        int retry = 0;
        final int maxRetry = 5;
        while (true) {
            try {
                // 1. 查询当天最大序号,并加行锁避免并发重复
                Integer maxSequence = baseMapper.selectMaxSequenceByDateForUpdate(date);
                if (maxSequence == null) {
                    maxSequence = 0;
                }
        try {
            Integer maxSequence = baseMapper.selectMaxSequenceByDate(date);
            maxSequence = (maxSequence == null) ? 0 : maxSequence;
            int newSequence = maxSequence + 1;
                // 2. 序号自增1
                int newSequence = maxSequence + 1;
            LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
            String dateStr = DATE_FORMATTER_THREAD_LOCAL.get().format(localDate);
            String engineeringId = "P" + dateStr + String.format("%02d", newSequence);
                // 3. 生成工程号:P + yyMMdd + 两位序号
                LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
                String dateStr = localDate.format(DATE_FORMATTER);
                String engineeringId = "P" + dateStr + String.format("%02d", newSequence);
            EngineeringSequence engineeringSequence = new EngineeringSequence();
            engineeringSequence.setEngineeringId(engineeringId);
            engineeringSequence.setDate(date);
            engineeringSequence.setSequence(newSequence);
            engineeringSequence.setCreatedTime(new Date());
            engineeringSequence.setUpdatedTime(new Date());
            engineeringSequence.setCreatedBy("system");
            engineeringSequence.setUpdatedBy("system");
                // 4. 保存到数据库
                EngineeringSequence engineeringSequence = new EngineeringSequence();
                engineeringSequence.setEngineeringId(engineeringId);
                engineeringSequence.setDate(date);
                engineeringSequence.setSequence(newSequence);
                engineeringSequence.setCreatedTime(new Date());
                engineeringSequence.setUpdatedTime(new Date());
                engineeringSequence.setCreatedBy("system");
                engineeringSequence.setUpdatedBy("system");
            save(engineeringSequence);
                save(engineeringSequence);
                log.info("生成工程号成功: engineeringId={}, date={}, sequence={}", engineeringId, date, newSequence);
                return engineeringId;
            } catch (DuplicateKeyException dup) {
                // 并发导致的唯一键冲突,重试
                if (++retry > maxRetry) {
                    log.error("生成工程号重试超过上限, date={}", date, dup);
                    throw new RuntimeException("生成工程号失败", dup);
                }
                log.warn("工程号生成发生并发冲突,准备重试,第{}次,date={}", retry, date);
            } catch (Exception e) {
                log.error("生成工程号失败, date={}", date, e);
                throw new RuntimeException("生成工程号失败", e);
            }
            log.info("生成工程号成功: engineeringId={}, date={}, sequence={}", engineeringId, date, newSequence);
            return engineeringId;
        } catch (DuplicateKeyException dup) {
            log.error("生成工程号唯一键冲突: date={}", date, dup);
            throw new RuntimeException("生成工程号失败", dup);
        } catch (Exception e) {
            log.error("生成工程号失败, date={}", date, e);
            throw new RuntimeException("生成工程号失败", e);
        }
    }
}
}
mes-processes/mes-plcSend/src/main/java/com/mes/device/service/impl/GlassInfoServiceImpl.java
@@ -14,6 +14,7 @@
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
@@ -230,7 +231,7 @@
            return result;
        }
        // 工程号生成:使用数据库自增序号,避免重复
        // 工程号生成:每次导入都生成新的工程号(使用数据库自增序号,避免重复)
        final String engineerId = engineeringSequenceService.generateAndSaveEngineeringId(new Date());
        final String filmsIdDefault = firstValue(excelRows, "filmsId", "白玻");
        final double thicknessDefault = parseDouble(firstValue(excelRows, "thickness"), 0d);
@@ -242,8 +243,9 @@
        List<Map<String, Object>> glassInfolList = excelRows.stream()
                .flatMap(row -> {
                    int qty = (int) parseDouble(row.getOrDefault("quantity", 1), 1);
                    if (qty <= 0) qty = 1;
                    Object qtyObj = row.getOrDefault("quantity", 1);
                    int qty = parseDouble(qtyObj, 1) > 0 ? (int) parseDouble(qtyObj, 1) : 1;
                    String glassId = str(row.get("glassId"));
                    String filmsId = strOrDefault(row.get("filmsId"), filmsIdDefaultFinal);
                    String flowCardId = str(row.get("flowCardId"));
@@ -254,9 +256,15 @@
                    double thickness = parseDouble(row.get("thickness"), thicknessDefaultFinal);
                    int finalQty = qty;
                    log.info("解析到数量:row={}, quantity={}, 最终qty={}", row, qtyObj, finalQty);
                    return range(0, qty).mapToObj(idx -> {
                        String finalGlassId = finalQty > 1 ? glassId + "_" + (idx + 1) : glassId;
                        String finalFlowCardId = flowCardId.isEmpty() ? finalGlassId : flowCardId;
                        String baseGlassId = engineerIdFinal + glassId;
                        String finalGlassId = finalQty > 1 ? baseGlassId + "_" + (idx + 1) : baseGlassId;
                        String baseFlowCardId = flowCardId.isEmpty() ? baseGlassId : flowCardId;
                        String finalFlowCardSequence = baseFlowCardId + "/" + (idx + 1);
                        String finalFlowCardId = baseFlowCardId;
                        Map<String, Object> m = new HashMap<>();
                        m.put("xAxis", 0);
                        m.put("xCoordinate", 0);
@@ -287,7 +295,7 @@
                        m.put("combine", 0);
                        m.put("markIcon", "");
                        m.put("filmRemove", 0);
                        m.put("flowCardSequence", flowCardId + "/" + (idx + 1));
                        m.put("flowCardSequence", finalFlowCardSequence);
                        m.put("process", "");
                        m.put("rawAngle", 0);
                        m.put("graphNo", 0);
@@ -473,5 +481,142 @@
        return Math.round(v * 100.0) / 100.0;
    }
    @Override
    public List<GlassInfo> getGlassInfosByEngineeringId(String engineeringId) {
        if (engineeringId == null || engineeringId.trim().isEmpty()) {
            return Collections.emptyList();
        }
        try {
            return baseMapper.selectByEngineeringId(engineeringId.trim());
        } catch (Exception e) {
            log.error("根据工程号查询玻璃信息失败, engineeringId={}", engineeringId, e);
            return Collections.emptyList();
        }
    }
    @Override
    public void saveGlassInfosFromExcel(List<Map<String, Object>> excelRows, String engineeringId) {
        if (excelRows == null || excelRows.isEmpty() || engineeringId == null || engineeringId.trim().isEmpty()) {
            return;
        }
        List<GlassInfo> glassInfos = new ArrayList<>();
        Date now = new Date();
        for (Map<String, Object> row : excelRows) {
            String glassId = str(row.get("glassId"));
            if (glassId == null || glassId.trim().isEmpty()) {
                continue;
            }
            int qty = (int) parseDouble(row.getOrDefault("quantity", 1), 1);
            if (qty <= 0) qty = 1;
            double width = parseDouble(row.get("width"), 0d);
            double height = parseDouble(row.get("height"), 0d);
            double thickness = parseDouble(row.get("thickness"), 0d);
            // 与导入规则保持一致:glassId 前加工程号前缀,数量>1时追加序号
            String baseGlassId = engineeringId.trim() + glassId;
            for (int idx = 0; idx < qty; idx++) {
                String finalGlassId = qty > 1 ? baseGlassId + "_" + (idx + 1) : baseGlassId;
                GlassInfo glassInfo = new GlassInfo();
                glassInfo.setGlassId(finalGlassId);
                glassInfo.setEngineeringId(engineeringId.trim());
                glassInfo.setGlassLength((int) Math.round(height));
                glassInfo.setGlassWidth((int) Math.round(width));
                glassInfo.setGlassThickness(BigDecimal.valueOf(thickness));
                glassInfo.setStatus(GlassInfo.Status.ACTIVE);
                glassInfo.setState(0);
                glassInfo.setCreatedTime(now);
                glassInfo.setUpdatedTime(now);
                glassInfo.setCreatedBy("system");
                glassInfo.setUpdatedBy("system");
                glassInfos.add(glassInfo);
            }
        }
        if (!glassInfos.isEmpty()) {
            batchSaveOrUpdateGlassInfo(glassInfos);
            log.info("已保存 {} 条玻璃信息到本地数据库,工程号: {}", glassInfos.size(), engineeringId);
        }
    }
    @Override
    public boolean updateGlassStateAfterScan(String glassId, Integer width, Integer height, Integer workLine) {
        if (glassId == null || glassId.trim().isEmpty()) {
            return false;
        }
        try {
            // 查询已存在的玻璃信息
            GlassInfo existing = baseMapper.selectByGlassId(glassId.trim());
            if (existing == null) {
                log.debug("玻璃信息不存在,无法更新状态: glassId={}", glassId);
                return false;
            }
            // 更新状态为1(已扫码交互)
            LambdaUpdateWrapper<GlassInfo> wrapper = new LambdaUpdateWrapper<>();
            wrapper.eq(GlassInfo::getGlassId, glassId.trim())
                   .eq(GlassInfo::getIsDeleted, 0)
                   .set(GlassInfo::getState, 1)
                   .set(GlassInfo::getUpdatedTime, new Date())
                   .set(GlassInfo::getUpdatedBy, "system");
            // 如果提供了尺寸信息,也更新尺寸
            if (width != null) {
                wrapper.set(GlassInfo::getGlassWidth, width);
            }
            if (height != null) {
                wrapper.set(GlassInfo::getGlassLength, height);
            }
            if (workLine != null) {
                wrapper.set(GlassInfo::getWorkLine, workLine);
            }
            boolean updated = this.update(wrapper);
            if (updated) {
                log.info("已更新玻璃信息状态为已扫码交互: glassId={}, state=1", glassId);
            }
            return updated;
        } catch (Exception e) {
            log.error("更新玻璃信息状态失败: glassId={}", glassId, e);
            return false;
        }
    }
    @Override
    public int deleteGlassInfosByEngineeringId(String engineeringId) {
        if (engineeringId == null || engineeringId.trim().isEmpty()) {
            return 0;
        }
        try {
            // 先查询要删除的数量(删除前)
            LambdaQueryWrapper<GlassInfo> countWrapper = new LambdaQueryWrapper<>();
            countWrapper.eq(GlassInfo::getEngineeringId, engineeringId.trim())
                       .eq(GlassInfo::getIsDeleted, 0); // 查询未删除的记录
            long count = this.count(countWrapper);
            // 使用MyBatis-Plus的remove方法,会根据@TableLogic自动进行逻辑删除
            LambdaQueryWrapper<GlassInfo> removeWrapper = new LambdaQueryWrapper<>();
            removeWrapper.eq(GlassInfo::getEngineeringId, engineeringId.trim())
                        .eq(GlassInfo::getIsDeleted, 0); // 只删除未删除的记录
            boolean result = this.remove(removeWrapper);
            if (result) {
                log.info("已删除工程号下的玻璃信息: engineeringId={}, count={}", engineeringId, count);
                return (int) count;
            }
            return 0;
        } catch (Exception e) {
            log.error("删除工程号下的玻璃信息失败: engineeringId={}", engineeringId, e);
            return 0;
        }
    }
}
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/scanner/handler/HorizontalScannerLogicHandler.java
@@ -133,28 +133,29 @@
        // 4. 清空plcRequest和plcGlassId(只清除PLC字段)
        clearPlcRequestFields(deviceConfig, serializer);
        // 5. 更新玻璃信息状态:将state从0改为1(已扫码交互)
        boolean updated = glassInfoService.updateGlassStateAfterScan(glassId, rawWidth, rawHeight, workLine);
        if (!updated) {
            log.warn("更新玻璃信息状态失败,玻璃可能不存在: glassId={}", glassId);
            // 不返回错误,继续执行,因为可能是新玻璃还未导入
        }
        // 6. 将扫描到的玻璃ID保存到共享数据中(供大车设备定时器读取)
        saveScannedGlassId(params, glassId);
            // 5. 保存玻璃信息到数据库
            GlassInfo glassInfo = buildGlassInfo(glassId, rawWidth, rawHeight, workLine);
            boolean saved = glassInfoService.saveOrUpdateGlassInfo(glassInfo);
            if (!saved) {
                return buildResult(deviceConfig, "scanOnce", false, "保存玻璃信息失败: " + glassId, null);
            }
            // 6. 将扫描到的玻璃ID保存到共享数据中(供大车设备定时器读取)
            saveScannedGlassId(params, glassId);
            String msg = String.format("玻璃[%s] 尺寸[表宽:%s x 长:%s] 已接收并入库,workLine=%s",
                    glassId,
                    rawWidth != null ? rawWidth + "mm" : "-",
                    rawHeight != null ? rawHeight + "mm" : "-",
                    workLine != null ? workLine : "-");
            Map<String, Object> resultData = new HashMap<>();
            resultData.put("glassIds", Collections.singletonList(glassId));
            if (workLine != null) {
                resultData.put("workLine", workLine);
            }
            return buildResult(deviceConfig, "scanOnce", true, msg, resultData);
        Integer intervalMs = config != null ? config.getScanIntervalMs() : null;
        String msg = String.format("玻璃[%s] 尺寸[表宽:%s x 长:%s] 已接收,workLine=%s,扫描间隔=%s",
                glassId,
                rawWidth != null ? rawWidth + "mm" : "-",
                rawHeight != null ? rawHeight + "mm" : "-",
                workLine != null ? workLine : "-",
                intervalMs != null ? intervalMs + "ms" : "-");
        Map<String, Object> resultData = new HashMap<>();
        resultData.put("glassIds", Collections.singletonList(glassId));
        if (workLine != null) {
            resultData.put("workLine", workLine);
        }
        return buildResult(deviceConfig, "scanOnce", true, msg, resultData);
    }
    
    /**
@@ -273,10 +274,10 @@
        glassInfo.setGlassId(glassId.trim());
        // mesWidth=表宽 -> glassWidth, mesHeight=长 -> glassLength
        if (width != null) {
            glassInfo.setGlassWidth(width);  // 表宽
            glassInfo.setGlassWidth(width);
        }
        if (height != null) {
            glassInfo.setGlassLength(height); // 长
            glassInfo.setGlassLength(height);
        }
        glassInfo.setStatus(GlassInfo.Status.PENDING);
        if (workLine != null) {
mes-processes/mes-plcSend/src/main/java/com/mes/interaction/workstation/transfer/handler/HorizontalTransferLogicHandler.java
@@ -261,12 +261,10 @@
            // 从配置中获取workLine,用于过滤(配置中是Integer类型)
            Integer workLine = getLogicParam(logicParams, "workLine", null);
            
            // 查询最近2分钟内的玻璃记录(扩大时间窗口,确保不遗漏)
            Date twoMinutesAgo = new Date(System.currentTimeMillis() - 120000);
            // 查询state=1的玻璃记录(已扫码交互完成,等待卧转立处理)
            LambdaQueryWrapper<GlassInfo> wrapper = new LambdaQueryWrapper<>();
            wrapper.in(GlassInfo::getStatus, GlassInfo.Status.PENDING, GlassInfo.Status.ACTIVE)
                   .ge(GlassInfo::getCreatedTime, twoMinutesAgo)
                   .eq(GlassInfo::getState, 1) // 只查询state=1的玻璃(已扫码完成)
                   .orderByDesc(GlassInfo::getCreatedTime)
                   .last("LIMIT 20"); // 限制查询数量,避免过多
            
@@ -419,19 +417,16 @@
        // 写入玻璃数量
        payload.put("plcGlassCount", count);
        
        // 写入卧转立编号(优先从任务参数获取,其次从设备配置获取)
        Integer inPosition = null;
        // 写入卧转立编号(优先从任务参数获取,其次从设备配置获取,直接写入编号,不进行位置映射)
        Object inPosition = null;
        if (params != null) {
            try {
                Object ctxObj = params.get("_taskContext");
                if (ctxObj instanceof com.mes.task.model.TaskExecutionContext) {
                    com.mes.task.model.TaskExecutionContext ctx =
                            (com.mes.task.model.TaskExecutionContext) ctxObj;
                    Object positionObj = ctx.getParameters().getExtra() != null
                    inPosition = ctx.getParameters().getExtra() != null
                            ? ctx.getParameters().getExtra().get("inPosition") : null;
                    if (positionObj instanceof Number) {
                        inPosition = ((Number) positionObj).intValue();
                    }
                }
            } catch (Exception e) {
                log.debug("从任务参数获取卧转立编号失败: deviceId={}", deviceConfig.getId(), e);
@@ -442,6 +437,7 @@
            inPosition = getLogicParam(logicParams, "inPosition", null);
        }
        if (inPosition != null) {
            // 直接写入编号本身,不进行位置映射转换
            payload.put("inPosition", inPosition);
            log.info("写入卧转立编号: deviceId={}, inPosition={}", deviceConfig.getId(), inPosition);
        } else {
mes-processes/mes-plcSend/src/main/java/com/mes/task/service/TaskExecutionEngine.java
@@ -992,6 +992,25 @@
                    
                    log.debug("出片大车设备定时器检测到已处理的玻璃信息: taskId={}, deviceId={}, glassCount={}",
                            task.getTaskId(), device.getId(), processedGlassIds.size());
                    // 需等待大理片笼完成全部玻璃的处理后再出片
                    @SuppressWarnings("unchecked")
                    List<String> initialGlassIds = (List<String>) context.getSharedData().get("initialGlassIds");
                    if (!CollectionUtils.isEmpty(initialGlassIds)
                            && !processedGlassIds.containsAll(initialGlassIds)) {
                        // 部分玻璃尚未由大理片笼处理完成,保持等待
                        deviceCoordinationService.syncDeviceStatus(device,
                                DeviceCoordinationService.DeviceStatus.WAITING, context);
                        if (!TaskStepDetail.Status.PENDING.name().equals(step.getStatus())) {
                            step.setStatus(TaskStepDetail.Status.PENDING.name());
                            step.setSuccessMessage("等待大理片笼处理全部玻璃后再出片");
                            taskStepDetailMapper.updateById(step);
                            notificationService.notifyStepUpdate(task.getTaskId(), step);
                        }
                        log.debug("出片大车等待大理片笼完成全部玻璃: taskId={}, deviceId={}, processed={}, initial={}",
                                task.getTaskId(), device.getId(), processedGlassIds.size(), initialGlassIds.size());
                        return;
                    }
                    
                    // 执行出片操作
                    Map<String, Object> checkParams = new HashMap<>();
mes-web/src/api/engineering.js
@@ -12,6 +12,33 @@
      method: 'post',
      data
    })
  },
  /**
   * 查询所有工程号列表
   */
  getEngineeringList() {
    return request({
      url: `${BASE_URL}/engineering/list`,
      method: 'get'
    })
  },
  /**
   * 根据工程号查询对应的玻璃ID列表
   */
  getGlassIdsByEngineeringId(engineeringId) {
    return request({
      url: `${BASE_URL}/engineering/${engineeringId}/glassIds`,
      method: 'get'
    })
  },
  /**
   * 删除工程号及其关联的玻璃信息
   */
  deleteEngineering(engineeringId) {
    return request({
      url: `${BASE_URL}/engineering/${engineeringId}`,
      method: 'delete'
    })
  }
}
mes-web/src/views/device/components/DeviceLogicConfig/WorkstationTransferConfig.vue
@@ -75,7 +75,7 @@
          <el-input-number
            v-model="config.inPosition"
            :min="0"
            :max="1000"
            :max="1999"
            :step="1"
            style="width: 100%;"
          />
mes-web/src/views/plcTest/components/MultiDeviceTest/TaskOrchestration.vue
@@ -35,12 +35,51 @@
    </div>
    <el-form :model="form" label-width="120px" :rules="rules" ref="formRef">
      <div style="width: 350px; margin-bottom: 12px; margin-left: 120px;">
          <el-select
            v-model="selectedEngineeringId"
            placeholder="选择工程号(选择后自动填充玻璃ID)"
            clearable
            filterable
            :disabled="!group"
            :loading="engineeringListLoading"
            @change="handleEngineeringChange"
            style="width: 100%"
          >
            <el-option
              v-for="item in engineeringList"
              :key="item.engineeringId"
              :label="item.engineeringId"
              :value="item.engineeringId"
            >
              <div style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
                <div style="flex: 1;">
                  <span>{{ item.engineeringId }}</span>
                  <span style="margin-left: 8px; color: #8492a6; font-size: 12px">
                    {{ item.date ? new Date(item.date).toLocaleDateString() : '' }}
                  </span>
                </div>
                <el-button
                  type="danger"
                  link
                  size="small"
                  :loading="deletingEngineeringId === item.engineeringId"
                  @click.stop="handleDeleteEngineering(item.engineeringId)"
                  style="margin-left: 8px; padding: 0 4px;"
                >
                  <el-icon><Delete /></el-icon>
                </el-button>
              </div>
            </el-option>
          </el-select>
        </div>
      <el-form-item label="玻璃ID列表" prop="glassIds">
        <el-input
          v-model="glassIdsInput"
          type="textarea"
          :rows="4"
          placeholder="可选:输入玻璃ID,将使用输入的ID进行测试"
          placeholder="可选:输入玻璃ID,将使用输入的ID进行测试(或通过上方选择工程号自动填充)"
          show-word-limit
          :maxlength="5000"
        />
@@ -57,8 +96,8 @@
</template>
<script setup>
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { computed, reactive, ref, watch, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Promotion, Upload } from '@element-plus/icons-vue'
import * as XLSX from 'xlsx'
import { multiDeviceTaskApi } from '@/api/device/multiDeviceTask'
@@ -116,13 +155,27 @@
const loadDeviceLoading = ref(false)
const fileInputRef = ref(null)
// 工程号相关
const selectedEngineeringId = ref('')
const engineeringList = ref([])
const engineeringListLoading = ref(false)
const glassIdsLoading = ref(false)
const deletingEngineeringId = ref('')
watch(
  () => props.group,
  () => {
    glassIdsInput.value = ''
    selectedEngineeringId.value = ''
    fetchLoadDevice()
    fetchEngineeringList()
  }
)
// 组件挂载时加载工程号列表
onMounted(() => {
  fetchEngineeringList()
})
const glassIds = computed(() => {
  if (!glassIdsInput.value) return []
@@ -131,6 +184,108 @@
    .map((item) => item.trim())
    .filter((item) => item.length > 0)
})
// 获取工程号列表
const fetchEngineeringList = async () => {
  try {
    engineeringListLoading.value = true
    const response = await engineeringApi.getEngineeringList()
    if (Array.isArray(response)) {
      engineeringList.value = response
    } else if (Array.isArray(response?.data)) {
      engineeringList.value = response.data
    } else {
      engineeringList.value = []
    }
    // 按日期倒序排列
    engineeringList.value.sort((a, b) => {
      const dateA = a.date ? new Date(a.date).getTime() : 0
      const dateB = b.date ? new Date(b.date).getTime() : 0
      return dateB - dateA
    })
  } catch (error) {
    console.error('获取工程号列表失败:', error)
    ElMessage.error(error?.message || '获取工程号列表失败')
    engineeringList.value = []
  } finally {
    engineeringListLoading.value = false
  }
}
// 处理工程号选择变化
const handleEngineeringChange = async (engineeringId) => {
  if (!engineeringId) {
    // 清空选择时,不清空已输入的玻璃ID,让用户保留
    return
  }
  try {
    glassIdsLoading.value = true
    const response = await engineeringApi.getGlassIdsByEngineeringId(engineeringId)
    const glassIds = response?.glassIds || response?.data?.glassIds || []
    if (glassIds.length > 0) {
      glassIdsInput.value = glassIds.join('\n')
      ElMessage.success(`已加载工程号 ${engineeringId} 的 ${glassIds.length} 个玻璃ID`)
    } else {
      ElMessage.warning(`工程号 ${engineeringId} 下没有找到玻璃ID`)
    }
  } catch (error) {
    console.error('获取玻璃ID列表失败:', error)
    ElMessage.error(error?.message || '获取玻璃ID列表失败')
  } finally {
    glassIdsLoading.value = false
  }
}
// 处理删除工程号
const handleDeleteEngineering = async (engineeringId) => {
  if (!engineeringId) {
    return
  }
  try {
    await ElMessageBox.confirm(
      `确定要删除工程号 "${engineeringId}" 及其关联的所有玻璃信息吗?此操作不可恢复!`,
      '确认删除',
      {
        confirmButtonText: '确定删除',
        cancelButtonText: '取消',
        type: 'warning',
        dangerouslyUseHTMLString: false
      }
    )
    deletingEngineeringId.value = engineeringId
    const response = await engineeringApi.deleteEngineering(engineeringId)
    const result = response?.data || response
    if (result?.success !== false) {
      const deletedCount = result?.deletedGlassCount || 0
      ElMessage.success(`已删除工程号 ${engineeringId},共删除 ${deletedCount} 条玻璃信息`)
      // 如果删除的是当前选中的工程号,清空选择
      if (selectedEngineeringId.value === engineeringId) {
        selectedEngineeringId.value = ''
        glassIdsInput.value = ''
      }
      // 刷新工程号列表
      await fetchEngineeringList()
    } else {
      throw new Error(result?.message || '删除失败')
    }
  } catch (error) {
    if (error !== 'cancel') {
      console.error('删除工程号失败:', error)
      ElMessage.error(error?.message || '删除工程号失败')
    }
  } finally {
    deletingEngineeringId.value = ''
  }
}
const normalizeType = (type) => (type || '').trim().toUpperCase()
@@ -508,10 +663,16 @@
        : `成功导入 ${glassDataList.length} 条玻璃数据`
      ElMessage.success(successMsg)
      // 将导入的玻璃ID填充到输入框,方便用户查看和编辑
      const glassIds = glassDataList.map(item => item.glassId).filter(id => id)
      if (glassIds.length > 0) {
        glassIdsInput.value = glassIds.join('\n')
      // 成功后刷新工程号下拉列表,并选中最新工程号
      try {
        await fetchEngineeringList()
        if (engineerId) {
          selectedEngineeringId.value = engineerId
          // 刷新并回填后端保存的 glassId(带工程号前缀),避免使用前端原始值
          await handleEngineeringChange(engineerId)
        }
      } catch (refreshErr) {
        console.error('刷新工程号列表失败:', refreshErr)
      }
    } else {
      // MES 接口返回失败