Просмотр исходного кода

V2通讯协议+镭雕工位(OP040)对接新供应商镭雕软件
- 新增com.mes.laser包:LaserClient(调镭雕软件获取码/开始镭雕接口)、
LaserCompleteServer(本地HTTP服务接收镭雕完成上报)、
LaserFlowManager(状态机:获取码->开始镭雕->等待完成->生成客户编码->建档->打印)、
LaserCardPrintUtil(流转卡打印,二维码内容=客户编码,右侧文字=钢印码+客户编码)
- UI调整:隐藏原扫码/OK/NG按钮(逻辑保留复用),新增获取镭雕码/复位按钮,
新增调试面板模拟镭雕软件各阶段信号(供应商真实接口未接通前验证流程)
- 引入zxing二维码库(core/javase 3.5.1)
- 新增config.properties镭雕相关配置项(prod_code/laser server地址/超时/打印机名)

wangxichen 6 дней назад
Родитель
Сommit
d0b4cf0443

+ 2 - 0
.classpath

@@ -23,4 +23,6 @@
 	<classpathentry kind="lib" path="lib/fastjson2-2.0.16.jar"/>
 	<classpathentry kind="lib" path="lib/jlibmodbus-1.2.9.2.jar"/>
 	<classpathentry kind="lib" path="lib/jSerialComm-2.6.2.jar"/>
+	<classpathentry kind="lib" path="lib/core-3.5.1.jar"/>
+	<classpathentry kind="lib" path="lib/javase-3.5.1.jar"/>
 </classpath>

BIN
lib/core-3.5.1.jar


BIN
lib/javase-3.5.1.jar


+ 136 - 0
src/com/mes/laser/LaserCardPrintUtil.java

@@ -0,0 +1,136 @@
+package com.mes.laser;
+
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.EncodeHintType;
+import com.google.zxing.WriterException;
+import com.google.zxing.common.BitMatrix;
+import com.google.zxing.qrcode.QRCodeWriter;
+import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
+
+import javax.print.PrintService;
+import javax.print.PrintServiceLookup;
+import javax.print.attribute.HashPrintRequestAttributeSet;
+import javax.print.attribute.PrintRequestAttributeSet;
+import javax.print.attribute.standard.MediaPrintableArea;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.awt.print.PageFormat;
+import java.awt.print.Printable;
+import java.awt.print.PrinterJob;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 流转卡打印:二维码内容=客户编码,二维码右侧文字=钢印码+客户编码。
+ * 打印机联动逻辑与镭雕供应商无关,纯本地实现。
+ * @author hzd
+ */
+public class LaserCardPrintUtil {
+
+    private static final int QR_CODE_SIZE = 200; // 二维码像素尺寸
+
+    /**
+     * 打印流转卡
+     * @param printerName 打印机名称,为空则使用系统默认打印机
+     * @param steelSn 钢印码
+     * @param customerSn 客户编码(二维码内容)
+     * @return true=打印任务已提交
+     */
+    public static boolean printCard(String printerName, String steelSn, String customerSn) {
+        BufferedImage qrImage = generateQRCode(customerSn);
+        if (qrImage == null) {
+            System.out.println("[流转卡打印] 二维码生成失败,customerSn=" + customerSn);
+            return false;
+        }
+
+        PrintService selectedService = null;
+        if (printerName != null && !printerName.trim().isEmpty()) {
+            PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
+            selectedService = Arrays.stream(services)
+                    .filter(s -> s.getName().equals(printerName.trim()))
+                    .findFirst().orElse(null);
+            if (selectedService == null) {
+                System.out.println("[流转卡打印] 未找到打印机:" + printerName + ",改用系统默认打印机");
+            }
+        }
+
+        try {
+            PrinterJob job = PrinterJob.getPrinterJob();
+            if (selectedService != null) {
+                job.setPrintService(selectedService);
+            }
+            job.setPrintable(new CardPrintable(qrImage, steelSn, customerSn));
+
+            int x = 60; // 标签宽度(mm)
+            int y = 40; // 标签高度(mm)
+            PrintRequestAttributeSet aset = new HashPrintRequestAttributeSet();
+            MediaPrintableArea area = new MediaPrintableArea(0, 0, x, y, MediaPrintableArea.MM);
+            aset.add(area);
+
+            job.print(aset);
+            System.out.println("[流转卡打印] 打印完成:steelSn=" + steelSn + " customerSn=" + customerSn);
+            return true;
+        } catch (Exception e) {
+            System.out.println("[流转卡打印] 打印失败:" + e.getMessage());
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    private static class CardPrintable implements Printable {
+        private final BufferedImage qrImage;
+        private final String steelSn;
+        private final String customerSn;
+
+        CardPrintable(BufferedImage qrImage, String steelSn, String customerSn) {
+            this.qrImage = qrImage;
+            this.steelSn = steelSn;
+            this.customerSn = customerSn;
+        }
+
+        @Override
+        public int print(Graphics g, PageFormat pf, int page) {
+            if (page > 0) {
+                return Printable.NO_SUCH_PAGE;
+            }
+            Graphics2D g2 = (Graphics2D) g;
+            g2.translate(pf.getImageableX(), pf.getImageableY());
+
+            // 左侧画二维码
+            g2.drawImage(qrImage, 0, 0, null);
+
+            // 右侧画文字:钢印码 + 客户编码
+            int textX = qrImage.getWidth() + 20;
+            g2.setFont(new Font("微软雅黑", Font.PLAIN, 14));
+            g2.drawString("钢印码:" + steelSn, textX, 40);
+            g2.drawString("客户码:" + customerSn, textX, 65);
+
+            return Printable.PAGE_EXISTS;
+        }
+    }
+
+    private static BufferedImage generateQRCode(String content) {
+        try {
+            Map<EncodeHintType, Object> hints = new HashMap<>();
+            hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
+            hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
+            hints.put(EncodeHintType.MARGIN, 1);
+
+            QRCodeWriter qrCodeWriter = new QRCodeWriter();
+            BitMatrix bitMatrix = qrCodeWriter.encode(content, BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, hints);
+
+            BufferedImage image = new BufferedImage(QR_CODE_SIZE, QR_CODE_SIZE, BufferedImage.TYPE_INT_RGB);
+            for (int x = 0; x < QR_CODE_SIZE; x++) {
+                for (int y = 0; y < QR_CODE_SIZE; y++) {
+                    image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
+                }
+            }
+            return image;
+        } catch (WriterException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+}

+ 113 - 0
src/com/mes/laser/LaserClient.java

@@ -0,0 +1,113 @@
+package com.mes.laser;
+
+import com.alibaba.fastjson2.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 镭雕软件HTTP接口调用(接口1获取待镭雕编码、接口2开始镭雕)
+ * 都是MES客户端主动调用镭雕软件提供的接口,JSON格式。
+ * @author hzd
+ */
+public class LaserClient {
+
+    /**
+     * 接口1:获取待镭雕编码
+     * @param baseUrl 镭雕软件服务地址
+     * @param path 接口路径
+     * @param lineSn 产线编号
+     * @param oprno 工位号
+     * @param timeoutMs 超时时间
+     * @return 成功返回镭雕软件的data对象(含codeId/code),失败返回null
+     */
+    public static JSONObject getCode(String baseUrl, String path, String lineSn, String oprno, int timeoutMs) {
+        JSONObject req = new JSONObject();
+        req.put("lineSn", lineSn);
+        req.put("oprno", oprno);
+
+        JSONObject resp = postJson(baseUrl + path, req, timeoutMs);
+        if (resp == null) {
+            return null;
+        }
+        Boolean success = resp.getBoolean("success");
+        if (success == null || !success) {
+            return null;
+        }
+        return resp.getJSONObject("data");
+    }
+
+    /**
+     * 接口2:开始镭雕
+     * @return true=镭雕软件成功接收开始指令(不代表打码已完成)
+     */
+    public static boolean start(String baseUrl, String path, String codeId, String code, int timeoutMs) {
+        JSONObject req = new JSONObject();
+        req.put("codeId", codeId);
+        req.put("code", code);
+
+        JSONObject resp = postJson(baseUrl + path, req, timeoutMs);
+        if (resp == null) {
+            return false;
+        }
+        Boolean success = resp.getBoolean("success");
+        return success != null && success;
+    }
+
+    /**
+     * 发送HTTP POST JSON请求,返回解析后的JSON对象,异常/失败返回null
+     */
+    private static JSONObject postJson(String urlStr, JSONObject body, int timeoutMs) {
+        HttpURLConnection conn = null;
+        try {
+            URL url = new URL(urlStr);
+            conn = (HttpURLConnection) url.openConnection();
+            conn.setRequestMethod("POST");
+            conn.setConnectTimeout(timeoutMs);
+            conn.setReadTimeout(timeoutMs);
+            conn.setDoOutput(true);
+            conn.setDoInput(true);
+            conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
+
+            byte[] data = body.toJSONString().getBytes(StandardCharsets.UTF_8);
+            try (OutputStream os = conn.getOutputStream()) {
+                os.write(data);
+            }
+
+            int code = conn.getResponseCode();
+            InputStream is = (code == 200) ? conn.getInputStream() : conn.getErrorStream();
+            if (is == null) {
+                return null;
+            }
+            StringBuilder sb = new StringBuilder();
+            try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
+                String line;
+                while ((line = br.readLine()) != null) {
+                    sb.append(line);
+                }
+            }
+            System.out.println("[镭雕接口] url=" + urlStr + " req=" + body.toJSONString() + " resp=" + sb);
+            if (code != 200) {
+                return null;
+            }
+            return JSONObject.parseObject(sb.toString());
+        } catch (IOException e) {
+            System.out.println("[镭雕接口] 调用失败:url=" + urlStr + " error=" + e.getMessage());
+            return null;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        } finally {
+            if (conn != null) {
+                conn.disconnect();
+            }
+        }
+    }
+
+}

+ 114 - 0
src/com/mes/laser/LaserCompleteServer.java

@@ -0,0 +1,114 @@
+package com.mes.laser;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 接口3:镭雕完成上报本地HTTP服务
+ * MES客户端起一个本地HTTP服务,接收镭雕软件主动调用的完成上报。
+ * @author hzd
+ */
+public class LaserCompleteServer {
+
+    private HttpServer httpServer;
+    private final LaserCompleteHandler handler;
+
+    public interface LaserCompleteHandler {
+        /**
+         * 收到镭雕完成上报后回调
+         * @param codeId 对应的请求标识
+         * @param code 钢印码
+         * @param result OK/NG
+         * @param errorMsg 失败原因
+         */
+        void onComplete(String codeId, String code, String result, String errorMsg);
+    }
+
+    public LaserCompleteServer(LaserCompleteHandler handler) {
+        this.handler = handler;
+    }
+
+    public void start(int port) {
+        try {
+            httpServer = HttpServer.create(new InetSocketAddress(port), 0);
+            httpServer.createContext("/mes/laser/complete", new CompleteHandler());
+            httpServer.setExecutor(null);
+            httpServer.start();
+            System.out.println("[镭雕完成上报服务] 已启动,端口=" + port);
+        } catch (IOException e) {
+            System.out.println("[镭雕完成上报服务] 启动失败:" + e.getMessage());
+            e.printStackTrace();
+        }
+    }
+
+    public void stop() {
+        if (httpServer != null) {
+            httpServer.stop(0);
+        }
+    }
+
+    private class CompleteHandler implements HttpHandler {
+        @Override
+        public void handle(HttpExchange exchange) throws IOException {
+            JSONObject resp = new JSONObject();
+            try {
+                if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
+                    resp.put("success", false);
+                    resp.put("message", "只支持POST");
+                    writeResp(exchange, 405, resp);
+                    return;
+                }
+
+                String body = readBody(exchange.getRequestBody());
+                System.out.println("[镭雕完成上报] 收到:" + body);
+                JSONObject req = JSONObject.parseObject(body);
+
+                String codeId = req.getString("codeId");
+                String code = req.getString("code");
+                String result = req.getString("result");
+                String errorMsg = req.getString("errorMsg");
+
+                if (handler != null) {
+                    handler.onComplete(codeId, code, result, errorMsg);
+                }
+
+                resp.put("success", true);
+                resp.put("message", "已接收");
+                writeResp(exchange, 200, resp);
+            } catch (Exception e) {
+                e.printStackTrace();
+                resp.put("success", false);
+                resp.put("message", "处理失败:" + e.getMessage());
+                writeResp(exchange, 200, resp);
+            }
+        }
+    }
+
+    private static String readBody(InputStream is) throws IOException {
+        StringBuilder sb = new StringBuilder();
+        byte[] buf = new byte[1024];
+        int len;
+        while ((len = is.read(buf)) != -1) {
+            sb.append(new String(buf, 0, len, StandardCharsets.UTF_8));
+        }
+        return sb.toString();
+    }
+
+    private static void writeResp(HttpExchange exchange, int code, JSONObject resp) throws IOException {
+        byte[] data = resp.toJSONString().getBytes(StandardCharsets.UTF_8);
+        exchange.getResponseHeaders().set("Content-Type", "application/json;charset=UTF-8");
+        exchange.sendResponseHeaders(code, data.length);
+        try (OutputStream os = exchange.getResponseBody()) {
+            os.write(data);
+        }
+    }
+
+}

+ 301 - 0
src/com/mes/laser/LaserFlowManager.java

@@ -0,0 +1,301 @@
+package com.mes.laser;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.mes.ui.DataUtil;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Properties;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * 镭雕整体流程状态机:
+ * 获取镭雕码 -> 回显钢印码 -> 开始镭雕 -> (镭雕软件异步)等待完成上报
+ * -> 生成客户编码并绑定 -> 覆盖回显为客户编码 -> 按op040逻辑提交建档 -> 打印流转卡 -> 解锁
+ *
+ * 从点击获取镭雕码到收到完成信号之间,按钮必须锁死,不允许重复触发。
+ * @author hzd
+ */
+public class LaserFlowManager {
+
+    public interface UiCallback {
+        void onStatus(String msg, boolean error);
+        void onLockButton(boolean lock);
+        // 把code内容显示到扫码框:先显示钢印码,完成后覆盖显示客户编码
+        void onCodeReceived(String code);
+        // 按op040既有逻辑提交建档(复用finish_ok_bt里的sendCreate),返回是否提交成功
+        boolean onSubmitResult(String customerSn, String qret);
+    }
+
+    private final UiCallback uiCallback;
+    private final ExecutorService executor = Executors.newSingleThreadExecutor();
+    private LaserCompleteServer completeServer;
+
+    // 当前进行中的请求标识和对应的待镭雕码,用于校验完成上报是否对应本次请求,
+    // 也用于"模拟完成"调试按钮直接复用(真实回调则由镭雕软件POST传入,不依赖这两个字段)
+    private volatile String currentCodeId = null;
+    private volatile String currentCode = null;
+    private volatile boolean waitingComplete = false;
+    private Timer completeTimeoutTimer;
+
+    private String laserServerUrl;
+    private String getCodePath = "/laser/getCode";
+    private String startPath = "/laser/start";
+    private int laserTimeout = 15000;
+    private int localPort = 9002;
+    private int completeTimeoutMs = 60000;
+    private String prodCode = "T09";
+    private String printerName = "";
+
+    public LaserFlowManager(UiCallback uiCallback) {
+        this.uiCallback = uiCallback;
+        loadConfig();
+    }
+
+    private void loadConfig() {
+        try {
+            InputStream is = ClassLoader.getSystemResourceAsStream("config/config.properties");
+            Properties pro = new Properties();
+            BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
+            pro.load(br);
+            laserServerUrl = pro.getProperty("mes.laser.server_url", "").trim();
+            laserTimeout = Integer.parseInt(pro.getProperty("mes.laser.timeout", "15000").trim());
+            localPort = Integer.parseInt(pro.getProperty("mes.laser.local_port", "9002").trim());
+            completeTimeoutMs = Integer.parseInt(pro.getProperty("mes.laser.complete_timeout", "60000").trim());
+            prodCode = pro.getProperty("mes.prod_code", "T09").trim();
+            printerName = pro.getProperty("mes.laser.printer_name", "").trim();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 启动本地HTTP服务,接收镭雕软件的完成上报(应用启动时调用一次)
+     */
+    public void startCompleteServer() {
+        completeServer = new LaserCompleteServer(this::handleComplete);
+        completeServer.start(localPort);
+    }
+
+    public void stopCompleteServer() {
+        if (completeServer != null) {
+            completeServer.stop();
+        }
+    }
+
+    /**
+     * 点击"获取镭雕码"按钮触发的入口
+     * @param oprno 工位号
+     * @param lineSn 产线编号
+     */
+    public void startLaserFlow(String oprno, String lineSn) {
+        if (waitingComplete) {
+            uiCallback.onStatus("镭雕进行中,请等待完成", true);
+            return;
+        }
+        uiCallback.onLockButton(true);
+        uiCallback.onStatus("正在获取镭雕码...", false);
+
+        executor.submit(() -> {
+            JSONObject codeData = LaserClient.getCode(laserServerUrl, getCodePath, lineSn, oprno, laserTimeout);
+            if (codeData == null) {
+                uiCallback.onStatus("获取镭雕码失败,请重试", true);
+                uiCallback.onLockButton(false);
+                return;
+            }
+            String codeId = codeData.getString("codeId");
+            String code = codeData.getString("code");
+            if (codeId == null || code == null) {
+                uiCallback.onStatus("获取镭雕码返回数据不完整", true);
+                uiCallback.onLockButton(false);
+                return;
+            }
+
+            // 回显钢印码到扫码框
+            uiCallback.onCodeReceived(code);
+            uiCallback.onStatus("已获取镭雕码,正在发送开始信号...", false);
+
+            boolean startOk = false;
+            // 接口2失败重试2-3次
+            for (int i = 0; i < 3 && !startOk; i++) {
+                startOk = LaserClient.start(laserServerUrl, startPath, codeId, code, laserTimeout);
+                if (!startOk && i < 2) {
+                    try { Thread.sleep(500); } catch (InterruptedException ignored) {}
+                }
+            }
+
+            if (!startOk) {
+                uiCallback.onStatus("发送开始镭雕信号失败,请人工确认设备状态后重试", true);
+                uiCallback.onLockButton(false);
+                return;
+            }
+
+            // 进入等待完成状态
+            currentCodeId = codeId;
+            currentCode = code;
+            waitingComplete = true;
+            uiCallback.onStatus("镭雕中,请等待完成信号...", false);
+            startCompleteTimeoutTimer(codeId);
+        });
+    }
+
+    // 完成信号超时定时器:超时后视为该codeId废弃,允许人工复位解锁
+    private void startCompleteTimeoutTimer(String codeId) {
+        if (completeTimeoutTimer != null) {
+            completeTimeoutTimer.cancel();
+        }
+        completeTimeoutTimer = new Timer();
+        completeTimeoutTimer.schedule(new TimerTask() {
+            @Override
+            public void run() {
+                if (waitingComplete && codeId.equals(currentCodeId)) {
+                    uiCallback.onStatus("镭雕完成信号超时,可点击复位后重试", true);
+                }
+            }
+        }, completeTimeoutMs);
+    }
+
+    /**
+     * 人工复位:超时或异常时,操作员手动点击解锁按钮
+     */
+    public void manualReset() {
+        currentCodeId = null;
+        currentCode = null;
+        waitingComplete = false;
+        if (completeTimeoutTimer != null) {
+            completeTimeoutTimer.cancel();
+        }
+        uiCallback.onLockButton(false);
+        uiCallback.onStatus("已复位,可重新获取镭雕码", false);
+    }
+
+    /**
+     * 调试用:模拟"获取镭雕码+开始镭雕"整个前半段,完全绕开真实HTTP请求。
+     * 供应商接口未接通前,用于本地走通整体流程:直接进入"镭雕中"状态,
+     * 之后可以点"模拟完成信号"按钮继续走完后半段。
+     * @param steelSn 模拟的钢印码,传空则自动生成一个测试用码
+     */
+    public void simulateGetCode(String steelSn) {
+        if (waitingComplete) {
+            uiCallback.onStatus("镭雕进行中,请等待完成或先复位", true);
+            return;
+        }
+        String code = (steelSn == null || steelSn.trim().isEmpty())
+                ? "JS" + System.currentTimeMillis() % 1000000000L
+                : steelSn.trim();
+        String codeId = UUID.randomUUID().toString();
+
+        uiCallback.onCodeReceived(code);
+        uiCallback.onLockButton(true);
+
+        currentCodeId = codeId;
+        currentCode = code;
+        waitingComplete = true;
+        uiCallback.onStatus("[调试]已模拟获取镭雕码并发送开始信号,镭雕中,可点模拟完成信号", false);
+        startCompleteTimeoutTimer(codeId);
+    }
+
+    /**
+     * 调试用:模拟镭雕软件的完成回调(供应商真实接口未接通前,用于走通整体流程)。
+     * 使用当前进行中的codeId/code,等效于镭雕软件真实POST了完成上报。
+     * @param result OK/NG
+     * @param errorMsg 失败原因(result=NG时使用)
+     */
+    public void simulateComplete(String result, String errorMsg) {
+        if (!waitingComplete || currentCodeId == null) {
+            uiCallback.onStatus("当前没有进行中的镭雕请求,无法模拟完成信号", true);
+            return;
+        }
+        System.out.println("[镭雕流程-调试] 模拟完成信号:codeId=" + currentCodeId + " code=" + currentCode + " result=" + result);
+        handleComplete(currentCodeId, currentCode, result, errorMsg);
+    }
+
+    /**
+     * 调试用:单独测试"生成客户编码"接口,不依赖整体流程状态,不做提交/打印。
+     * @param steelSn 测试用钢印码
+     */
+    public void manualGenCustomerSn(String steelSn) {
+        if (steelSn == null || steelSn.trim().isEmpty()) {
+            uiCallback.onStatus("请输入测试用钢印码", true);
+            return;
+        }
+        uiCallback.onStatus("正在测试生成客户编码...", false);
+        executor.submit(() -> {
+            JSONObject genResp = DataUtil.genCustomerSn(prodCode, steelSn.trim());
+            if (genResp == null || genResp.get("result") == null
+                    || !"true".equalsIgnoreCase(genResp.get("result").toString())) {
+                String msg = genResp != null && genResp.get("message") != null
+                        ? genResp.get("message").toString() : "生成客户编码失败";
+                uiCallback.onStatus("测试失败:" + msg, true);
+                return;
+            }
+            String customerSn = genResp.get("data") == null ? "" : genResp.get("data").toString();
+            uiCallback.onCodeReceived(customerSn);
+            uiCallback.onStatus("测试生成客户编码成功:" + customerSn, false);
+        });
+    }
+
+    // 镭雕软件调用接口3上报完成结果的回调(真实HTTP回调 或 模拟按钮 都走这里)
+    private void handleComplete(String codeId, String steelSn, String result, String errorMsg) {
+        if (completeTimeoutTimer != null) {
+            completeTimeoutTimer.cancel();
+        }
+
+        if (!waitingComplete || codeId == null || !codeId.equals(currentCodeId)) {
+            // 迟到的旧回调或未处于等待状态,直接忽略(该codeId已被判定废弃或未曾发起)
+            System.out.println("[镭雕流程] 忽略无效/迟到的完成上报,codeId=" + codeId);
+            return;
+        }
+
+        waitingComplete = false;
+        currentCodeId = null;
+        currentCode = null;
+
+        if (!"OK".equalsIgnoreCase(result)) {
+            uiCallback.onStatus("镭雕失败:" + (errorMsg == null ? "" : errorMsg), true);
+            uiCallback.onLockButton(false);
+            return;
+        }
+
+        uiCallback.onStatus("镭雕完成,正在生成客户编码...", false);
+
+        executor.submit(() -> {
+            JSONObject genResp = DataUtil.genCustomerSn(prodCode, steelSn);
+            if (genResp == null || genResp.get("result") == null
+                    || !"true".equalsIgnoreCase(genResp.get("result").toString())) {
+                String msg = genResp != null && genResp.get("message") != null
+                        ? genResp.get("message").toString() : "生成客户编码失败";
+                uiCallback.onStatus(msg, true);
+                uiCallback.onLockButton(false);
+                return;
+            }
+
+            String customerSn = genResp.get("data") == null ? "" : genResp.get("data").toString();
+            // 覆盖回显为客户编码,随后按此编码提交建档
+            uiCallback.onCodeReceived(customerSn);
+
+            boolean submitOk = uiCallback.onSubmitResult(customerSn, "OK");
+            if (!submitOk) {
+                uiCallback.onStatus("客户编码生成成功,但提交MES建档失败:" + customerSn, true);
+                uiCallback.onLockButton(false);
+                return;
+            }
+
+            boolean printOk = LaserCardPrintUtil.printCard(printerName, steelSn, customerSn);
+            if (!printOk) {
+                uiCallback.onStatus("已提交建档,但流转卡打印失败:" + customerSn, true);
+            } else {
+                uiCallback.onStatus("流转卡打印完成,可继续下一件", false);
+            }
+
+            uiCallback.onLockButton(false);
+        });
+    }
+
+}

+ 9 - 8
src/com/mes/netty/MesMsgUtils.java

@@ -2,14 +2,15 @@ package com.mes.netty;
 
 public class MesMsgUtils {
 
-    public static int SYNR_LEN = 46;
-    public static int AXTW_LEN = 46;
-    public static int ACLW_LEN = 46;
-    public static int MCJW_LEN = 96;
-    public static int AQDW_LEN = 96;
-    public static int MBDW_LEN = 96;
-    public static int MJBW_LEN = 96;
-    public static int MQDW_LEN = 96;
+    // 协议 v2(2026-06-29):oprno 字段从 6 字符扩到 8 字符,所有长度 +2
+    public static int SYNR_LEN = 48;
+    public static int AXTW_LEN = 48;
+    public static int ACLW_LEN = 48;
+    public static int MCJW_LEN = 98;
+    public static int AQDW_LEN = 98;
+    public static int MBDW_LEN = 98;
+    public static int MJBW_LEN = 98;
+    public static int MQDW_LEN = 98;
     public static int MKSW_LEN = 96;
     public static int MSBW_LEN = 96;
     public static int MCSW_LEN = 96;

+ 17 - 23
src/com/mes/netty/ProtocolParam.java

@@ -2,11 +2,11 @@ package com.mes.netty;
 
 
 // 固定格式报文各参数获取方法
+// 协议 v2(2026-06-29 修订):oprno 字段从 6 字符扩到 8 字符,后续字段偏移整体 +2
 public class ProtocolParam {
-    // bbbbfffffARWAQDWGWOP100 GY100000ID151245P00000106200123062900001      RSOKDA2023-09-07ZT10:16:58
-    public static Integer fixedLength = 96; // 固定长度
+    // bbbbfffffARWAQDWGWOP100   GY100000ID151245P00000106200123062900001      RSOKDA2023-09-07ZT10:16:58
+    public static Integer fixedLength = 98;
 
-    // 获取消息类型  所有报文都可使用
     public static String getMsgType(String msg){
         System.out.print(msg);
         if(msg.length() < 16){
@@ -15,59 +15,53 @@ public class ProtocolParam {
         return msg.substring(12,16);
     }
 
-    // 获取工位号
     public static String getOprno(String msg){
-        if(msg.length() < 24){
+        if(msg.length() < 26){
             return "";
         }
-        return msg.substring(18,24);
+        return msg.substring(18,26);
     }
 
-    // 获取工艺号
     public static String getCraft(String msg){
-        if(msg.length() < 32){
+        if(msg.length() < 34){
             return "";
         }
-        return msg.substring(26,32);
+        return msg.substring(28,34);
     }
 
-    // 获取镭雕码或设备报警故障代码
     public static String getSn(String msg){
-        if(msg.length() < 70){
+        if(msg.length() < 72){
             return "";
         }
-        return msg.substring(34,70);
+        return msg.substring(36,72);
     }
 
     public static String getLx(String msg){
-        if(msg.length() < 72){
+        if(msg.length() < 74){
             return "";
         }
-        return msg.substring(70,72);
+        return msg.substring(72,74);
     }
 
-    // 获取结果
     public static String getResult(String msg){
-        if(msg.length() < 74){
+        if(msg.length() < 76){
             return "";
         }
-        return msg.substring(72,74);
+        return msg.substring(74,76);
     }
 
-    // 获取日期
     public static String getDay(String msg){
-        if(msg.length() < 86){
+        if(msg.length() < 88){
             return "";
         }
-        return msg.substring(76,86);
+        return msg.substring(78,88);
     }
 
-    // 获取时间
     public static String getTime(String msg){
-        if(msg.length() < 96){
+        if(msg.length() < 98){
             return "";
         }
-        return msg.substring(88,96);
+        return msg.substring(90,98);
     }
 
 }

+ 32 - 3
src/com/mes/ui/DataUtil.java

@@ -18,7 +18,7 @@ public class DataUtil {
             //TCP连接后,直接先发同步报文
             String start = "aaaabbbbbABW";
             String msgType = "SYNR";
-            String gw = "GW"+DataUtil.rightPad(mes_gw, 6);
+            String gw = "GW"+DataUtil.rightPad(mes_gw, 8);
             String da = "DA" + DateLocalUtils.getCurrentDate();
             String zt = "ZT" + DateLocalUtils.getCurrentTimeHMS();
             String synr_str = start + msgType + gw + da + zt;
@@ -40,7 +40,7 @@ public class DataUtil {
             //TCP连接后,直接先发同步报文
             String start = "aaaabbbbbABW";
             String msgType = "AXTW";
-            String gw = "GW"+DataUtil.rightPad(mes_gw, 6);
+            String gw = "GW"+DataUtil.rightPad(mes_gw, 8);
             String da = "DA" + DateLocalUtils.getCurrentDate();
             String zt = "ZT" + DateLocalUtils.getCurrentTimeHMS();
             String axtw_str = start + msgType + gw + da + zt;
@@ -144,7 +144,7 @@ public class DataUtil {
             Properties pro = new Properties();
             BufferedReader br = new BufferedReader(new InputStreamReader(is, enconding));
             pro.load(br);
-            String gw = "GW"+rightPad(pro.getProperty("mes.gw"), 6);
+            String gw = "GW"+rightPad(pro.getProperty("mes.gw"), 8);
             String start = "aaaabbbbbABW";
             String gy = "GY" + rightPad(craft, 6);
             String reslx = "LX" + rightPad(lx, 2);
@@ -253,6 +253,35 @@ public class DataUtil {
         }
     }
 
+    // 调用mescloud生成客户编码并绑定钢印码(镭雕OP040专用)
+    // prodCode: 产品标识 T09/T68;steelSn: 镭雕软件回传的钢印码
+    public static JSONObject genCustomerSn(String prodCode,String steelSn){
+        try{
+            String enconding = "UTF-8";
+            InputStream is = ClassLoader.getSystemResourceAsStream("config/config.properties");
+            Properties pro = new Properties();
+            BufferedReader br = new BufferedReader(new InputStreamReader(is, enconding));
+            pro.load(br);
+            String mes_server_ip = pro.getProperty("mes.server_ip");
+            String oprno = pro.getProperty("mes.gw").trim();
+            String lineSn = pro.getProperty("mes.line_sn").trim();
+            String url = "http://"+mes_server_ip+":8980/js/a/mes/mesLaser/genCustomerSn";
+            String params = "__ajax=json&prodCode="+prodCode+"&steelSn="+steelSn+"&oprno="+oprno+"&lineSn="+lineSn;
+            System.out.println("params="+params);
+            String result = doPost(url,params);
+            System.out.println("result="+result);
+
+            if(result==null||result.equalsIgnoreCase("false")) {
+                return null;
+            }else {
+                return JSONObject.parseObject(result);
+            }
+        }catch (Exception e){
+            e.printStackTrace();
+            return null;
+        }
+    }
+
     public static String doPost(String httpUrl, String param) {
         HttpURLConnection connection = null;
         InputStream is = null;

+ 141 - 2
src/com/mes/ui/MesClient.java

@@ -5,6 +5,7 @@ import com.fazecast.jSerialComm.SerialPort;
 import com.fazecast.jSerialComm.SerialPortEvent;
 import com.mes.component.MesRadio;
 import com.mes.component.MesWebView;
+import com.mes.laser.LaserFlowManager;
 import com.mes.netty.NettyClient;
 import com.mes.util.DateLocalUtils;
 import com.mes.util.JdbcUtils;
@@ -95,6 +96,11 @@ public class MesClient extends JFrame {
 
     public static SerialPort serialPort; // 串口对象
 
+    // 镭雕相关
+    public static JButton laser_get_code_bt; // 获取镭雕码按钮
+    public static JButton laser_reset_bt; // 人工复位按钮
+    public static LaserFlowManager laserFlowManager;
+
     public static void main(String[] args) {
         if (LockUtil.getInstance().isAppActive() == true){
 //            JOptionPane.showMessageDialog(null, "已有一个程序在运行,程序退出");
@@ -126,6 +132,9 @@ public class MesClient extends JFrame {
 
                     checkTimer();
 
+                    // 启动镭雕流程管理器(含本地HTTP服务,接收镭雕软件完成上报)
+                    initLaserFlow();
+
                 }catch (Exception e){
                     e.printStackTrace();
                 }
@@ -423,10 +432,61 @@ public class MesClient extends JFrame {
         }
     }
 
+    // 复用op040既有建档提交逻辑(原finish_ok_bt/finish_ng_bt里的sendCreate调用抽出来)
+    // sn: 用于提交的编码(客户编码),qret: OK/NG
+    public static boolean submitLaserResult(String sn, String qret){
+        if(sn == null || sn.isEmpty()){
+            setMenuStatus("客户编码为空,无法提交",1);
+            return false;
+        }
+        getUser();
+        String snPad = getBarcode(sn);
+        Boolean sendret = DataUtil.sendCreate(nettyClient, snPad, qret, user20);
+        if(!sendret){
+            setMenuStatus("结果提交MES失败,请重试",1);
+            return false;
+        }
+        return true;
+    }
+
+    // 初始化镭雕流程管理器
+    public static void initLaserFlow(){
+        laserFlowManager = new LaserFlowManager(new LaserFlowManager.UiCallback() {
+            @Override
+            public void onStatus(String msg, boolean error) {
+                SwingUtilities.invokeLater(() -> setMenuStatus(msg, error ? 1 : 0));
+            }
+
+            @Override
+            public void onLockButton(boolean lock) {
+                SwingUtilities.invokeLater(() -> {
+                    if(laser_get_code_bt != null){
+                        laser_get_code_bt.setEnabled(!lock);
+                    }
+                });
+            }
+
+            @Override
+            public void onCodeReceived(String code) {
+                SwingUtilities.invokeLater(() -> product_sn.setText(code));
+            }
+
+            @Override
+            public boolean onSubmitResult(String customerSn, String qret) {
+                return submitLaserResult(customerSn, qret);
+            }
+        });
+        laserFlowManager.startCompleteServer();
+    }
+
     public static void logoff() {
 //        welcomeWin.setVisible(true);
         mesClientFrame.setVisible(false);
 
+        if(laserFlowManager != null){
+            laserFlowManager.stopCompleteServer();
+        }
+
         nettyClient = null;
 //        if(heartBeatTimer!=null) {
 //            heartBeatTimer.cancel();
@@ -580,15 +640,37 @@ public class MesClient extends JFrame {
         indexScrollPaneA = new JScrollPane(indexPanelA);
         indexPanelA.setLayout(null);
 
+        // 镭雕:获取镭雕码 / 复位按钮,放在扫码框(回显框)上方
+        laser_get_code_bt = new JButton("获取镭雕码");
+        laser_get_code_bt.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent e) {
+                laserFlowManager.startLaserFlow(mes_gw, mes_line_sn);
+            }
+        });
+        laser_get_code_bt.setFont(new Font("微软雅黑", Font.PLAIN, 24));
+        laser_get_code_bt.setBounds(81, 40, 300, 50);
+        indexPanelA.add(laser_get_code_bt);
+
+        // 镭雕:人工复位按钮(完成信号超时或异常时使用,强制解锁并作废当前请求)
+        laser_reset_bt = new JButton("复位");
+        laser_reset_bt.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent e) {
+                laserFlowManager.manualReset();
+            }
+        });
+        laser_reset_bt.setFont(new Font("微软雅黑", Font.PLAIN, 24));
+        laser_reset_bt.setBounds(400, 40, 150, 50);
+        indexPanelA.add(laser_reset_bt);
+
         product_sn = new JTextField();
         product_sn.setHorizontalAlignment(SwingConstants.CENTER);
         product_sn.setEditable(false);
         product_sn.setFont(new Font("微软雅黑", Font.PLAIN, 28));
-//        product_sn.setBounds(81, 70, 602, 70);
         product_sn.setBounds(81, 100, 602, 70);
         indexPanelA.add(product_sn);
         product_sn.setColumns(10);
 
+        // 原扫码按钮不再需要,隐藏但保留逻辑(scanBarcode等方法仍被其他流程复用)
         f_scan_data_bt_1 = new JButton("扫码");
         f_scan_data_bt_1.addActionListener(new ActionListener() {
             public void actionPerformed(ActionEvent e) {
@@ -598,8 +680,8 @@ public class MesClient extends JFrame {
         });
         f_scan_data_bt_1.setIcon(new ImageIcon(MesClient.class.getResource("/bg/scan_barcode.png")));
         f_scan_data_bt_1.setFont(new Font("微软雅黑", Font.PLAIN, 32));
-//        f_scan_data_bt_1.setBounds(693, 70, 198, 70);
         f_scan_data_bt_1.setBounds(693, 100, 198, 70);
+        f_scan_data_bt_1.setVisible(false);
         indexPanelA.add(f_scan_data_bt_1);
 
 
@@ -667,6 +749,8 @@ public class MesClient extends JFrame {
         finish_ok_bt.setFont(new Font("微软雅黑", Font.PLAIN, 32));
         finish_ok_bt.setBounds(185, 341, 240, 80);
         finish_ok_bt.setEnabled(false);
+        // 镭雕工位不需要手动点OK,完成信号收到后自动提交,隐藏但保留逻辑复用
+        finish_ok_bt.setVisible(false);
         indexPanelA.add(finish_ok_bt);
 
         finish_ng_bt = new JButton("NG");
@@ -698,8 +782,63 @@ public class MesClient extends JFrame {
         finish_ng_bt.setFont(new Font("微软雅黑", Font.PLAIN, 32));
         finish_ng_bt.setBounds(508, 341, 240, 80);
         finish_ng_bt.setEnabled(false);
+        // 镭雕工位不需要手动点NG,隐藏但保留逻辑复用
+        finish_ng_bt.setVisible(false);
         indexPanelA.add(finish_ng_bt);
 
+        // ===== 调试面板:供应商真实接口未接通前,用于模拟镭雕软件回传的各阶段信号 =====
+        JLabel debugTitle = new JLabel("调试面板(模拟镭雕软件信号)");
+        debugTitle.setFont(new Font("微软雅黑", Font.PLAIN, 16));
+        debugTitle.setForeground(Color.GRAY);
+        debugTitle.setBounds(760, 40, 260, 30);
+        indexPanelA.add(debugTitle);
+
+        JTextField debugSteelSnInput = new JTextField();
+        debugSteelSnInput.setFont(new Font("微软雅黑", Font.PLAIN, 16));
+        debugSteelSnInput.setBounds(760, 80, 220, 40);
+        debugSteelSnInput.setToolTipText("输入测试用钢印码,留空则自动生成");
+        indexPanelA.add(debugSteelSnInput);
+
+        JButton debugSimGetCodeBtn = new JButton("模拟获取镭雕码");
+        debugSimGetCodeBtn.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent e) {
+                laserFlowManager.simulateGetCode(debugSteelSnInput.getText());
+            }
+        });
+        debugSimGetCodeBtn.setFont(new Font("微软雅黑", Font.PLAIN, 16));
+        debugSimGetCodeBtn.setBounds(760, 130, 220, 50);
+        indexPanelA.add(debugSimGetCodeBtn);
+
+        JButton debugSimOkBtn = new JButton("模拟完成信号(OK)");
+        debugSimOkBtn.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent e) {
+                laserFlowManager.simulateComplete("OK", "");
+            }
+        });
+        debugSimOkBtn.setFont(new Font("微软雅黑", Font.PLAIN, 16));
+        debugSimOkBtn.setBounds(760, 190, 220, 50);
+        indexPanelA.add(debugSimOkBtn);
+
+        JButton debugSimNgBtn = new JButton("模拟完成信号(NG)");
+        debugSimNgBtn.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent e) {
+                laserFlowManager.simulateComplete("NG", "模拟打码失败");
+            }
+        });
+        debugSimNgBtn.setFont(new Font("微软雅黑", Font.PLAIN, 16));
+        debugSimNgBtn.setBounds(760, 250, 220, 50);
+        indexPanelA.add(debugSimNgBtn);
+
+        JButton debugGenSnBtn = new JButton("测试生成客户编码");
+        debugGenSnBtn.addActionListener(new ActionListener() {
+            public void actionPerformed(ActionEvent e) {
+                laserFlowManager.manualGenCustomerSn(debugSteelSnInput.getText());
+            }
+        });
+        debugGenSnBtn.setFont(new Font("微软雅黑", Font.PLAIN, 16));
+        debugGenSnBtn.setBounds(760, 310, 220, 50);
+        indexPanelA.add(debugGenSnBtn);
+        // ===== 调试面板结束 =====
 
         JButton modbusBtn = new JButton("关");
         modbusBtn.addActionListener(new ActionListener() {

+ 7 - 9
src/com/mes/ui/OprnoUtil.java

@@ -53,15 +53,13 @@ public class OprnoUtil {
     }
 
     public static String formatOprno(String oprno){
-        List<String> lists = new ArrayList<>();
-
-        if(oprno.length() == 6){
-            String ysoprno = oprno.substring(0,5).trim();
-            if(!lists.contains(ysoprno)){
-                oprno = ysoprno;
-            }
+        if(oprno == null) return "";
+        int len = oprno.length();
+        while(len > 1
+                && Character.isLetter(oprno.charAt(len - 1))
+                && Character.isDigit(oprno.charAt(len - 2))){
+            len--;
         }
-
-        return  oprno;
+        return oprno.substring(0, len);
     }
 }

+ 21 - 3
src/resources/config/config.properties

@@ -1,6 +1,24 @@
 mes.gw=OP040A
-#mes.server_ip=127.0.0.1
-mes.server_ip=192.168.114.99
+mes.server_ip=127.0.0.1
+#mes.server_ip=192.168.114.99
 mes.tcp_port=3000
 mes.heart_beat_cycle=60
-mes.line_sn=XT
+mes.line_sn=XT
+
+# prodCode: T09/T68, used for customer sn prefix, independent from mes.line_sn(XT)
+mes.prod_code=T09
+
+# laser software http server url (getCode/start api)
+mes.laser.server_url=http://127.0.0.1:9001
+
+# local http server port for receiving laser complete callback
+mes.laser.local_port=9002
+
+# laser api timeout in ms
+mes.laser.timeout=15000
+
+# laser complete signal wait timeout in ms
+mes.laser.complete_timeout=60000
+
+# printer name for laser card, empty = use default printer
+mes.laser.printer_name=