hou 2 дней назад
Родитель
Сommit
c1e109aa42

+ 97 - 15
src/com/mes/netty/MesMsgUtils.java

@@ -1,90 +1,172 @@
 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;
-    public static int MKSW_LEN = 96;
-    public static int MSBW_LEN = 96;
-    public static int MCSW_LEN = 96;
-    public static int AQRW_LEN = 96;
+
+    public static int MCJW_LEN = 124;
+
+    public static int AQDW_LEN = 124;
+
+    public static int MBDW_LEN = 124;
+
+    public static int MJBW_LEN = 124;
+
+    public static int MQDW_LEN = 124;
+
+    public static int MKSW_LEN = 124;
+
+    public static int MSBW_LEN = 124;
+
+    public static int MCSW_LEN = 124;
+
+    public static int AQRW_LEN = 124;
+
+
 
     public static String MSG_TYPE[] = {
+
         "SYNR",
+
         "AXTW",
+
         "ACLW",
+
         "MCJW",
+
         "AQDW",
+
         "MBDW",
+
         "MJBW",
+
         "MQDW",
+
         "MKSW",
+
         "MSBW",
+
         "MCSW",
+
         "AQRW",
+
     };
 
+
+
     public static int isMsgContentOK(String msg, String msg_type) {
-        //ret=0OK,1 空内容,2 长度不符
+
         int ret = 0;
+
         if(msg==null||msg.equalsIgnoreCase("")) {
+
             ret = 1;
+
             return ret;
+
         }
 
+
+
         int len = msg.length();
-        //System.out.println("len="+len);
+
         switch(msg_type) {
+
             case "SYNR":
+
             case "AXTW":
+
             case "ACLW":
+
                 if(len==SYNR_LEN) {
+
                     ret = 0;
+
                 }else {
+
                     ret = 2;
+
                 }
+
                 break;
+
             default:
-                if(len==AQDW_LEN) {
+
+                if(len >= ProtocolParam.shortReplyLength && len <= ProtocolParam.fixedLength){
+
+                    ret = 0;
+
+                }else if(len == ProtocolParam.fixedLength){
+
                     ret = 0;
+
                 }else {
+
                     ret = 2;
+
                 }
+
                 break;
+
         }
+
         return ret;
+
     }
 
-    // 判断报文类型是否在清单里
+
+
     public static boolean isMsgTypeOk(String msg_type) {
+
         for (String str : MSG_TYPE) {
+
             if (str.equals(msg_type)) {
+
                 return true;
+
             }
+
         }
+
         return false;
+
     }
 
-    // 处理报文数据
+
+
     public static String processMsg(String msg, String msg_type) {
+
         String processMsgRet = "";
+
         if(msg_type.equalsIgnoreCase("SYNR")) {
 
+
+
         }else if(msg_type.equalsIgnoreCase("AXTW")) {
 
+
+
         }else if(msg_type.equalsIgnoreCase("ACLW")) {
 
+
+
         }else{
+
             processMsgRet = ProtocolParam.getResult(msg);
+
         }
+
         return processMsgRet;
-    }
 
+    }
 
 }
+

+ 136 - 21
src/com/mes/netty/NettyClient.java

@@ -1,63 +1,178 @@
 package com.mes.netty;
 
+
+
 import io.netty.bootstrap.Bootstrap;
+
 import io.netty.channel.*;
+
 import io.netty.channel.nio.NioEventLoopGroup;
+
 import io.netty.channel.socket.SocketChannel;
+
 import io.netty.channel.socket.nio.NioSocketChannel;
+
 import io.netty.handler.codec.string.StringEncoder;
+
+
+
 import java.util.concurrent.TimeUnit;
 
+
+
 import com.mes.ui.MesClient;
 
+
+
 public class NettyClient {
+
     public SocketChannel socketChannel;
-    public static ChannelFuture future;
 
-    public  void run(Object msg){
-        //配置线程组
-        EventLoopGroup group = new NioEventLoopGroup();
-        //创建服务启动器
-        Bootstrap bootstrap = new Bootstrap();
+    public ChannelFuture future;
+
+    private static EventLoopGroup group;
+
+    private static Bootstrap bootstrap;
+
+    private volatile boolean connecting = false;
+
+
+
+    private void ensureBootstrap() {
+
+        if (group != null && !group.isShutdown()) {
+
+            return;
+
+        }
+
+        group = new NioEventLoopGroup();
+
+        bootstrap = new Bootstrap();
 
-        //配置参数
         bootstrap.group(group)
+
                 .channel(NioSocketChannel.class)
-                .option(ChannelOption.TCP_NODELAY,true)
+
+                .option(ChannelOption.TCP_NODELAY, true)
+
+                .option(ChannelOption.SO_KEEPALIVE, true)
+
                 .handler(new ChannelInitializer<SocketChannel>() {
-                    protected void initChannel(SocketChannel socketChannel) throws Exception {
-                        socketChannel.pipeline()
+
+                    @Override
+
+                    protected void initChannel(SocketChannel ch) {
+
+                        ch.pipeline()
+
                                 .addLast(new XDecoder())
+
                                 .addLast(new StringEncoder())
-//                                    .addLast(new StringDecoder())
+
                                 .addLast(new NettyClientHandler());
+
                     }
+
                 })
-                .remoteAddress(MesClient.mes_server_ip,MesClient.mes_tcp_port);
 
-        //连接
-        future = bootstrap.connect();
+                .remoteAddress(MesClient.mes_server_ip, MesClient.mes_tcp_port);
+
+    }
+
+
+
+    public void resetConnection() {
+
+        connecting = false;
+
+        future = null;
+
+        socketChannel = null;
+
+    }
+
+
+
+    public boolean isChannelActive() {
+
+        return future != null && future.channel() != null && future.channel().isActive();
+
+    }
+
+
+
+    public void run(Object msg) {
+
+        if (isChannelActive()) {
+
+            future.channel().writeAndFlush(msg);
+
+            return;
+
+        }
+
+        if (connecting) {
+
+            if (group != null && !group.isShutdown()) {
+
+                group.schedule(() -> run(msg), 300, TimeUnit.MILLISECONDS);
+
+            }
+
+            return;
+
+        }
+
+        connecting = true;
+
+        ensureBootstrap();
+
         System.out.println("客户端正在连接服务端...");
-        //客户端断线重连逻辑
+
+        future = bootstrap.connect();
+
         future.addListener((ChannelFutureListener) future1 -> {
+
+            connecting = false;
+
             if (future1.isSuccess()) {
-                //tcp连接成功
+
                 MesClient.tcp_connect_flag = true;
-                //设置TCP请求状态
+
                 MesClient.connect_request_flag = false;
+
                 System.out.println("连接Netty服务端成功");
-                future.channel().writeAndFlush(msg);
+
+                future1.channel().writeAndFlush(msg);
+
             } else {
-                //tcp连接失败
+
                 MesClient.tcp_connect_flag = false;
+
                 MesClient.connect_request_flag = true;
+
                 System.out.println("连接失败,进行断线重连");
-                future1.channel().eventLoop().schedule(() -> run(msg), 10, TimeUnit.SECONDS);
+
+                if (future1.channel() != null) {
+
+                    future1.channel().eventLoop().schedule(() -> run(msg), 10, TimeUnit.SECONDS);
+
+                } else if (group != null) {
+
+                    group.schedule(() -> run(msg), 10, TimeUnit.SECONDS);
+
+                }
+
             }
-            //设置tcp连接状态
+
             MesClient.setTcpStatus();
+
         });
+
         socketChannel = (SocketChannel) future.channel();
 
     }
+
 }
+

+ 102 - 5
src/com/mes/netty/NettyClientHandler.java

@@ -1,87 +1,184 @@
 package com.mes.netty;
 
+
+
 import com.mes.ui.MesClient;
+
 import com.mes.ui.MesRevice;
+
 import io.netty.channel.Channel;
+
 import io.netty.channel.ChannelHandlerContext;
+
 import io.netty.channel.ChannelInboundHandlerAdapter;
+
 import org.slf4j.Logger;
+
 import org.slf4j.LoggerFactory;
 
+
+
 import java.text.SimpleDateFormat;
+
 import java.util.Date;
 
+
+
 public class NettyClientHandler extends ChannelInboundHandlerAdapter {
 
+
+
     public static final Logger log = LoggerFactory.getLogger(NettyClientHandler.class);
 
+
+
     private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
     public static byte[] responseByte;
 
+
+
     @Override
+
     public void channelActive(ChannelHandlerContext ctx) throws Exception {
+
         System.out.println("mes connecting:" + sdf.format(new Date()));
+
     }
 
+
+
     @Override
+
     public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
+
         Channel channel = ctx.channel();
+
         System.err.println("close tcp, ip:" + channel.remoteAddress());
+
         MesClient.tcp_connect_flag = false;
+
+        MesClient.connect_request_flag = false;
+
         MesClient.setTcpStatus();
-        // 关闭通道
-        channel.close();
+
+        if (MesClient.nettyClient != null) {
+
+            MesClient.nettyClient.resetConnection();
+
+        }
+
+        MesClient.scheduleTcpReconnect();
+
     }
 
+
+
     @Override
+
     public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+
         System.out.println("msg:"+msg);
+
         log.info("REVICE:"+msg);
 
+
+
         String mes_msg = formatResult(msg.toString());
-        if(mes_msg == null || mes_msg.isEmpty()){ // error msg
+
+        if(mes_msg == null || mes_msg.isEmpty()){
+
             return;
+
         }
 
+
+
         String msg_type = ProtocolParam.getMsgType(mes_msg);
+
         System.out.println("msg_type="+msg_type);
 
+
+
         String processMsgRet = MesMsgUtils.processMsg(mes_msg, msg_type);
+
         System.out.println("processMsgRet="+processMsgRet);
+
         switch(msg_type) {
-            case "AQDW": // 查询质量
+
+            case "AQDW":
+
                 MesRevice.checkQualityRevice(processMsgRet,mes_msg);
+
                 break;
+
             case "MKSW":
+
                 MesRevice.startRevice(processMsgRet,mes_msg);
+
                 break;
+
             case "MBDW":
+
                 MesRevice.bindRevice(processMsgRet,mes_msg);
+
                 break;
+
             case "MJBW":
+
                 MesRevice.unbindRevice(processMsgRet,mes_msg);
+
                 break;
+
             case "MQDW":
+
                 MesRevice.updateResultRevice(processMsgRet,mes_msg);
+
                 break;
-        }
 
+        }
 
     }
 
+
+
     @Override
+
     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+
+        System.err.println("Netty异常: " + cause.getMessage());
+
+        cause.printStackTrace();
+
+        if (MesClient.nettyClient != null) {
+
+            MesClient.nettyClient.resetConnection();
+
+        }
+
         ctx.close();
+
     }
 
+
+
     private String formatResult(String msg){
+
         System.err.println("length:" + msg.length() + " content:" + msg);
+
         String msgType = msg.substring(0,2).trim();
+
         if(msgType.equals("OK")){
+
             return msg.substring(3,msg.length());
+
         }else{
+
             return "";
+
         }
+
     }
 
 }
+

+ 150 - 9
src/com/mes/netty/ProtocolParam.java

@@ -1,73 +1,214 @@
 package com.mes.netty;
 
 
+
+
+
 // 固定格式报文各参数获取方法
+
 public class ProtocolParam {
-    // bbbbfffffARWAQDWGWOP100 GY100000ID151245P00000106200123062900001      RSOKDA2023-09-07ZT10:16:58
-    public static Integer fixedLength = 96; // 固定长度
 
-    // 获取消息类型  所有报文都可使用
+    /** 与 MES 服务端一致:含 YH+参数个数字段的基础长度 */
+
+    public static Integer fixedLength = 124;
+
+    public static Integer shortReplyLength = 96;
+
+
+
     public static String getMsgType(String msg){
+
         System.out.print(msg);
+
         if(msg.length() < 16){
+
             return "";
+
         }
+
         return msg.substring(12,16);
+
     }
 
-    // 获取工位号
+
+
     public static String getOprno(String msg){
+
         if(msg.length() < 24){
+
             return "";
+
         }
+
         return msg.substring(18,24);
+
     }
 
-    // 获取工艺号
+
+
     public static String getCraft(String msg){
+
         if(msg.length() < 32){
+
             return "";
+
         }
+
         return msg.substring(26,32);
+
     }
 
-    // 获取镭雕码或设备报警故障代码
+
+
     public static String getSn(String msg){
+
         if(msg.length() < 70){
+
             return "";
+
         }
+
         return msg.substring(34,70);
+
     }
 
+
+
     public static String getLx(String msg){
+
         if(msg.length() < 72){
+
             return "";
+
         }
+
         return msg.substring(70,72);
+
     }
 
-    // 获取结果
+
+
     public static String getResult(String msg){
+
         if(msg.length() < 74){
+
             return "";
+
         }
+
         return msg.substring(72,74);
+
     }
 
-    // 获取日期
+
+
     public static String getDay(String msg){
+
         if(msg.length() < 86){
+
             return "";
+
         }
+
         return msg.substring(76,86);
+
     }
 
-    // 获取时间
+
+
     public static String getTime(String msg){
+
         if(msg.length() < 96){
+
             return "";
+
         }
+
         return msg.substring(88,96);
+
+    }
+
+
+
+    public static Integer getParamNums(String msg){
+
+        if(msg.length() < fixedLength){
+
+            return 0;
+
+        }
+
+        String nn = msg.substring(122, fixedLength).trim();
+
+        if(nn.isEmpty()){
+
+            return 0;
+
+        }
+
+        try{
+
+            return Integer.parseInt(nn);
+
+        }catch (NumberFormatException e){
+
+            return 0;
+
+        }
+
+    }
+
+
+
+    public static Integer getParamLenth(String msg){
+
+        if(msg.length() < fixedLength){
+
+            return 0;
+
+        }
+
+        Integer length = getParamNums(msg);
+
+        if(length == 0){
+
+            return 0;
+
+        }
+
+        String msgType = getMsgType(msg);
+
+        if("MBDW".equals(msgType) || "MJBW".equals(msgType) || "AQDW".equals(msgType)
+
+                || "MCJW".equals(msgType) || "MKSW".equals(msgType)){
+
+            if(msg.length() >= fixedLength + length * 36){
+
+                return length * 36;
+
+            }
+
+        }else if("MSBW".equals(msgType)){
+
+            if(msg.length() >= fixedLength + length * 4){
+
+                return length * 4;
+
+            }
+
+        }else{
+
+            if(msg.length() >= fixedLength + length * 12){
+
+                return length * 12;
+
+            }
+
+        }
+
+        return 0;
+
     }
 
 }
+

+ 18 - 3
src/com/mes/netty/XDecoder.java

@@ -30,6 +30,14 @@ public class XDecoder extends ByteToMessageDecoder {
         // 合并报文
         ByteBuf message = null;
         int tmpMsgSize = tempMsg.readableBytes();
+        // 丢弃上一次解码残留的无效尾包,避免与下一条 MES 报文错误合并
+        if (tmpMsgSize > 0 && tmpMsgSize < 12) {
+            String pending = hexStringToAscii(ByteBufUtil.hexDump(tempMsg));
+            if (!pending.contains("bbbbfffffARW") && !pending.contains("aaaabbbbbABW")) {
+                tempMsg.clear();
+                tmpMsgSize = 0;
+            }
+        }
         // 如果暂存有上一次余下的请求报文,则合并
         if (tmpMsgSize > 0) {
             message = Unpooled.buffer();
@@ -144,9 +152,16 @@ public class XDecoder extends ByteToMessageDecoder {
             case "MSBW":
             case "MCSW":
             case "AQRW":
-            default: // 默认新报文
-                if(str.length() >= ProtocolParam.fixedLength){ // 大于固定长度
-                    tpsize = ProtocolParam.fixedLength;
+            default:
+                if(str.length() >= ProtocolParam.fixedLength){
+                    Integer paramLen = ProtocolParam.getParamLenth(str);
+                    int total = ProtocolParam.fixedLength + paramLen;
+                    if(str.length() >= total){
+                        tpsize = total;
+                    }
+                }
+                if(tpsize == 0 && str.length() >= ProtocolParam.shortReplyLength){
+                    tpsize = ProtocolParam.shortReplyLength;
                 }
                 break;
         }

+ 18 - 0
src/com/mes/ui/BindMaterialResp.java

@@ -1,6 +1,8 @@
 package com.mes.ui;
 
 public class BindMaterialResp {
+    private String oprno;
+    private String oprnoLabel;
     private String materialId;
     private String materialTitle;
     private String craft;
@@ -8,6 +10,22 @@ public class BindMaterialResp {
     private String lastTimes;
     private String type;
 
+    public String getOprno() {
+        return oprno;
+    }
+
+    public void setOprno(String oprno) {
+        this.oprno = oprno;
+    }
+
+    public String getOprnoLabel() {
+        return oprnoLabel;
+    }
+
+    public void setOprnoLabel(String oprnoLabel) {
+        this.oprnoLabel = oprnoLabel;
+    }
+
     public String getMaterialId() {
         return materialId;
     }

+ 49 - 47
src/com/mes/ui/DataUtil.java

@@ -2,6 +2,7 @@ package com.mes.ui;
 
 import com.alibaba.fastjson2.JSONObject;
 import com.mes.netty.NettyClient;
+import com.mes.util.ConfigUtil;
 import com.mes.util.DateLocalUtils;
 import com.mes.util.JdbcUtils;
 import org.slf4j.Logger;
@@ -52,7 +53,11 @@ public class DataUtil {
             String gy = "";
             String sn = "";
             JdbcUtils.insertData(gw, gy, axtw_str, msgType, sn);
-            nettyClient.future.channel().writeAndFlush(axtw_str);
+            if(nettyClient.isChannelActive()){
+                nettyClient.future.channel().writeAndFlush(axtw_str);
+            }else{
+                nettyClient.run(axtw_str);
+            }
 
             return true;
         }catch (Exception e){
@@ -161,11 +166,7 @@ public class DataUtil {
 
     public static Boolean sendMessage(NettyClient nettyClient,String msgType,String craft,String lx,String sn,String result,String user,String paramNums,String params,String op){
         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);
+            Properties pro = ConfigUtil.loadProperties();
 
             if(MesClient.mes_gwflag.equals("B")){
                 if(op.equals("A")){
@@ -188,7 +189,11 @@ public class DataUtil {
             System.out.println("message="+aqdw_str);
             JdbcUtils.insertData(gw, gy, aqdw_str, msgType, sn);
             log.info("SEND:"+aqdw_str);
-            nettyClient.future.channel().writeAndFlush(aqdw_str);
+            if(nettyClient.isChannelActive()){
+                nettyClient.future.channel().writeAndFlush(aqdw_str);
+            }else{
+                nettyClient.run(aqdw_str);
+            }
             return true;
         }catch (Exception e){
             return false;
@@ -209,12 +214,7 @@ public class DataUtil {
 
     public static JSONObject upParams(String upparams) {
         try{
-            String enconding = "UTF-8";    //涓嶇windows杩樻槸linux閮芥槸UTF-8
-            InputStream is = ClassLoader.getSystemResourceAsStream("config/config.properties");
-            Properties pro = new Properties();
-            //閬垮厤涓枃涔辩爜
-            BufferedReader br = new BufferedReader(new InputStreamReader(is, enconding));
-            pro.load(br);
+            Properties pro = ConfigUtil.loadProperties();
             String mes_server_ip = pro.getProperty("mes.server_ip");
             String oprno = pro.getProperty("mes.gw").trim();
             String url = "http://"+mes_server_ip+":8980/js/a/mes/mesProductCmt/batchsave";
@@ -233,15 +233,10 @@ public class DataUtil {
         }
     }
 
-    public static JSONObject getBindMaterail() {
+    public static JSONObject getBindMaterail(String oprno) {
         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);
+            Properties pro = ConfigUtil.loadProperties();
             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/mesLineProcessMaterial/materials";
             String params = "__ajax=json&oprno="+oprno+"&lineSn="+lineSn;
@@ -259,15 +254,10 @@ public class DataUtil {
         }
     }
 
-    public static JSONObject saveBindMaterail(String batchSn,String craft,String materialId,String type) {
+    public static JSONObject saveBindMaterail(String batchSn,String craft,String materialId,String type,String oprno) {
         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);
+            Properties pro = ConfigUtil.loadProperties();
             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/mesMaterialPrebind/bind";
             String params = "__ajax=json&oprno="+oprno+"&lineSn="+lineSn+"&batchSn="+batchSn+"&craft="+craft+"&materialId="+materialId+"&type="+type;
@@ -288,11 +278,7 @@ public class DataUtil {
     //无点检项点检
     public static JSONObject saveDj(){
         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);
+            Properties pro = ConfigUtil.loadProperties();
             String mes_server_ip = pro.getProperty("mes.server_ip");
             String mes_gw = pro.getProperty("mes.gwflag").trim();
             String oprno = pro.getProperty("mes.gw").trim();
@@ -324,13 +310,37 @@ public class DataUtil {
         }
     }
 
+    /**
+     * 提交设备保养记录(按当前工位配置,A/B 面分别对应 OP030A / OP030B 等)
+     */
+    public static JSONObject saveMaintenance(String oprnoSuffix, String content, String remark){
+        try{
+            Properties pro = ConfigUtil.loadProperties();
+            String mes_server_ip = pro.getProperty("mes.server_ip");
+            String mes_gw = pro.getProperty("mes.gw").trim();
+            String lineSn = pro.getProperty("mes.line_sn").trim();
+            String oprno = mes_gw + oprnoSuffix;
+            String url = "http://"+mes_server_ip+":8980/js/a/mes/mesDeviceMaintenanceRecord/addRecord";
+            String params = "__ajax=json&oprno="+oprno+"&lineSn="+lineSn;
+            if(content != null){
+                params += "&content="+java.net.URLEncoder.encode(content, "UTF-8");
+            }
+            if(remark != null){
+                params += "&remark="+java.net.URLEncoder.encode(remark, "UTF-8");
+            }
+            String result = doPost(url,params);
+            if(result.equalsIgnoreCase("false")) {
+                return null;
+            }
+            return JSONObject.parseObject(result);
+        }catch (Exception e){
+            return null;
+        }
+    }
+
     public static JSONObject saveGzCheck(String content,String userCode) {
         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);
+            Properties pro = ConfigUtil.loadProperties();
             String mes_server_ip = pro.getProperty("mes.server_ip");
             String oprno = pro.getProperty("mes.gw").trim();
             String lineSn = pro.getProperty("mes.line_sn").trim();
@@ -412,11 +422,7 @@ public class DataUtil {
 
     public static JSONObject bindWarehouseData(String sn,String wsn,String userCode,String craft){
         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);
+            Properties pro = ConfigUtil.loadProperties();
             String mes_server_ip = pro.getProperty("mes.server_ip");
             String oprno = pro.getProperty("mes.gw").trim();
             String lineSn = pro.getProperty("mes.line_sn").trim();
@@ -438,11 +444,7 @@ public class DataUtil {
 
     public static JSONObject unBindWarehouseData(String sn,String wsn,String userCode,String craft){
         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);
+            Properties pro = ConfigUtil.loadProperties();
             String mes_server_ip = pro.getProperty("mes.server_ip");
             String oprno = pro.getProperty("mes.gw").trim();
             String lineSn = pro.getProperty("mes.line_sn").trim();

+ 2 - 0
src/com/mes/ui/ErrorMsg.java

@@ -33,6 +33,8 @@ public class ErrorMsg {
                 lmsg = "更换配件首件检查不合格停机";
             }else if(processMsgRet.equalsIgnoreCase("DJ")) {
                 lmsg = "未进行开班点检";
+            }else if(processMsgRet.equalsIgnoreCase("BY")) {
+                lmsg = "设备保养到期,请先完成保养";
             }else if(processMsgRet.equalsIgnoreCase("BM")) {
                 lmsg = "未绑定物料";
             }else if(processMsgRet.equalsIgnoreCase("PL")) {

+ 268 - 246
src/com/mes/ui/MesClient.java

@@ -11,6 +11,7 @@ import com.mes.component.MesRadio;
 import com.mes.component.MesWebView;
 import com.mes.component.MyDialog;
 import com.mes.netty.NettyClient;
+import com.mes.util.ConfigUtil;
 import com.mes.util.DateLocalUtils;
 import com.mes.util.HttpUtils;
 import com.mes.util.IweldCloudUtil;
@@ -37,10 +38,12 @@ import java.sql.SQLException;
 import java.sql.Statement;
 import java.time.LocalDateTime;
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.Properties;
 import java.util.Timer;
 import java.util.TimerTask;
-import java.util.List;
 
 public class MesClient extends JFrame {
     public static final Logger log = LoggerFactory.getLogger(MesClient.class);
@@ -59,6 +62,36 @@ public class MesClient extends JFrame {
     public static NettyClient nettyClient;
     public static boolean tcp_connect_flag = false;
     public static boolean connect_request_flag = false;
+    private static volatile long lastTcpReconnectMs = 0;
+    private static final Object tcpReconnectLock = new Object();
+
+    /** 断线后延迟重连,避免报文尚未回包就被 SYNR 新连接打断 */
+    public static void scheduleTcpReconnect(){
+        synchronized (tcpReconnectLock){
+            long now = System.currentTimeMillis();
+            if(now - lastTcpReconnectMs < 3000){
+                return;
+            }
+            if(connect_request_flag || nettyClient == null){
+                return;
+            }
+            connect_request_flag = true;
+            lastTcpReconnectMs = now;
+        }
+        new Timer(true).schedule(new TimerTask() {
+            @Override
+            public void run() {
+                try{
+                    if(nettyClient != null && !tcp_connect_flag){
+                        System.out.println("TCP断线,延迟重连...");
+                        DataUtil.synrTcp(nettyClient, mes_gw);
+                    }
+                }finally {
+                    connect_request_flag = false;
+                }
+            }
+        }, 2000);
+    }
 
     //session
     public static String sessionid = "";
@@ -85,17 +118,12 @@ public class MesClient extends JFrame {
     public static JButton user_menu;
 
     public static int scan_type = 0;
-    public static JButton finish_ok_bt;
-    public static JButton finish_ok_bt2;
-    public static JButton finish_ng_bt;
-    public static JButton finish_ng_bt2;
+    public static JButton batch_scan_1;
+    public static JButton batch_scan_2;
     public static JTextField product_sn;
     public static JLabel pxstatus1;
     public static JLabel pxstatus2;
 
-    public static JButton batch_scan_1;
-    public static JButton batch_scan_2;
-
     public static Integer tjFlag = 0; // 0=未开始 1=已扫码,设备未启动 2=设备运行中 3=设备运行结束
 
     public static Integer tjFlaga = 0; // 0=未开始 1=已扫码,设备未启动 2=设备运行中 3=设备运行结束
@@ -277,16 +305,13 @@ public class MesClient extends JFrame {
                     e.printStackTrace();
                 }
             }
-        }, 1000,500);
+        }, 1000, IweldCloudUtil.getCollectIntervalMs());
     }
 
     //鐠囧鍘ょ純顔芥瀮娴狅拷
     private static void readProperty() throws IOException{
-        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);
+        Properties pro = ConfigUtil.loadProperties();
+        ConfigUtil.printStartupInfo();
         mes_gw =  pro.getProperty("mes.gw");
 
 //        mes_gw_des = pro.getProperty("mes.gw_des");
@@ -371,18 +396,9 @@ public class MesClient extends JFrame {
                     heart_beat_menu.setText(DateLocalUtils.getCurrentTime());
                     heart_beat_menu.repaint();
 
-                    //閼汇儲婀潻鐐村复閸掓瑥鐨剧拠鏇$箾閹猴拷
-                    if(nettyClient!=null&&!tcp_connect_flag){
-                        System.out.println("TCP瀹稿弶鏌囧锟�");
-                        //TCP闁插秵鏌婇崥灞绢劄鏉╃偞甯�
-                        if(!connect_request_flag) {
-                            //System.out.println("TCP闁插秵鏌婇崥灞绢劄鏉╃偞甯�");
-                            //鐠佸墽鐤員CP鐠囬攱鐪伴悩鑸碉拷锟�,閸欘亪鍣搁弬鏉挎倱濮濄儴绻涢幒銉ょ濞嗭拷
-                            connect_request_flag = true;
-
-                            DataUtil.synrTcp(nettyClient,mes_gw);
-                        }
-
+                    // TCP 断线时延迟重连
+                    if(nettyClient!=null && !tcp_connect_flag){
+                        scheduleTcpReconnect();
                     }
                 }
             }
@@ -425,7 +441,6 @@ public class MesClient extends JFrame {
     public static void resetScanA() {
         work_status = 0;
         check_quality_result = false;
-        MesClient.finish_ok_bt.setEnabled(false);
         MesClient.mesQualityFlagA = false;
         product_sn.setText("");
         MesClient.pxstatus1.setText("A");
@@ -448,9 +463,6 @@ public class MesClient extends JFrame {
         MesClient.formatScanType(1);
 //        PlcUtil.changeEnable(MesClient.s7PLC,false);
 
-        finish_ok_bt.setEnabled(false);
-        finish_ng_bt.setEnabled(false);
-
         updateMaterailData(); // 更新物料
 
         shiftUserCheck();
@@ -483,7 +495,6 @@ public class MesClient extends JFrame {
     public static void resetScanB() {
         work_status2 = 0;
         check_quality_result2 = false;
-        MesClient.finish_ng_bt.setEnabled(false);
         MesClient.mesQualityFlagB = false;
         product_sn2.setText("");
 
@@ -504,9 +515,6 @@ public class MesClient extends JFrame {
 //        MesClient.batch_scan_2.setEnabled(false);
         MesClient.formatScanType(1);
 //        PlcUtil.changeEnable(MesClient.s7PLC,false);
-        finish_ok_bt2.setEnabled(false);
-        finish_ng_bt2.setEnabled(false);
-
 
         updateMaterailData(); // 更新物料
 
@@ -525,7 +533,84 @@ public class MesClient extends JFrame {
         user20 = user20 + space_tmp1;
     }
 
-    //閼惧嘲褰嘼arcode閸愬懎顔�36娴o拷
+    /** 提交质量结果,并将焊接实时参数落本地库 */
+    public static boolean submitQualityAndFlush(String face, String result) {
+        getUser();
+        String rawSn = "A".equals(face) ? product_sn.getText() : product_sn2.getText();
+        if(rawSn == null || rawSn.trim().isEmpty()){
+            return false;
+        }
+        String sn = getBarcode(rawSn.trim());
+        Boolean sendret = DataUtil.sendQuality(nettyClient, sn, result, user20, face);
+        IweldCloudUtil.flushWeldingParamsOnFinish(face);
+        return sendret != null && sendret;
+    }
+
+    /** manualImmediateReset=true 时发送成功后立即复位;自动流程由 MesRevice 等待 MES 回包后复位 */
+    public static void handleSubmitResult(String face, String result, boolean manualImmediateReset) {
+        if(!tcp_connect_flag){
+            setMenuStatus("未连接MES服务器", -1);
+            return;
+        }
+        String label = "A".equals(face) ? "A面" : "B面";
+        String rawSn = "A".equals(face) ? product_sn.getText() : product_sn2.getText();
+        if(rawSn == null || rawSn.trim().isEmpty()){
+            setMenuStatus(label + "无工件码,请先扫码", -1);
+            return;
+        }
+
+        boolean sendret = submitQualityAndFlush(face, result);
+        if(!sendret){
+            if("A".equals(face)){
+                pxstatus1.setText("A:结果上传MES失败");
+                tjStatusa = 1;
+            }else{
+                pxstatus2.setText("B:结果上传MES失败");
+                tjStatusb = 1;
+            }
+            setMenuStatus(label + "提交失败,请重试", -1);
+            return;
+        }
+
+        if(manualImmediateReset){
+            if("A".equals(face)){
+                pxstatus1.setText("OK".equals(result) ? "A:提交成功" : "A:已提交NG");
+                status_menu.setText("OK".equals(result) ? "A件提交成功" : "A件已提交NG");
+                resetScanA();
+            }else{
+                pxstatus2.setText("OK".equals(result) ? "B:提交成功" : "B:已提交NG");
+                status_menu.setText("OK".equals(result) ? "B件提交成功" : "B件已提交NG");
+                resetScanB();
+            }
+            setMenuStatus(label + ("OK".equals(result) ? "提交成功" : "已提交NG"), 0);
+        }
+    }
+
+    public static void promptManualSubmit(String face) {
+        String label = "A".equals(face) ? "A面" : "B面";
+        String rawSn = "A".equals(face) ? product_sn.getText() : product_sn2.getText();
+        if(rawSn == null || rawSn.trim().isEmpty()){
+            setMenuStatus(label + "无工件码,请先扫码", -1);
+            return;
+        }
+
+        int choice = JOptionPane.showOptionDialog(
+                mesClientFrame,
+                label + " 工件: " + rawSn.trim() + "\n请选择提交结果",
+                "手动提交-" + label,
+                JOptionPane.DEFAULT_OPTION,
+                JOptionPane.QUESTION_MESSAGE,
+                null,
+                new Object[]{"OK", "NG", "取消"},
+                "OK"
+        );
+        if(choice == 0){
+            handleSubmitResult(face, "OK", true);
+        }else if(choice == 1){
+            handleSubmitResult(face, "NG", true);
+        }
+    }
+
     public static String getBarcode(String barcodeTmp) {
         String barcodeRet = barcodeTmp;
         if(barcodeTmp.equalsIgnoreCase("")) {
@@ -601,7 +686,9 @@ public class MesClient extends JFrame {
             public void mousePressed(MouseEvent e) {
                 super.mouseClicked(e);
                 //闁插秷绻沵es
-                nettyClient.future.channel().close();
+                if(nettyClient != null && nettyClient.isChannelActive()){
+                    nettyClient.future.channel().close();
+                }
             }
         });
         settingMenu.add(resetTcpMenu);
@@ -637,16 +724,7 @@ public class MesClient extends JFrame {
             @Override
             public void mousePressed(MouseEvent e) {
                 super.mouseClicked(e);
-                finish_ok_bt.setEnabled(true);
-                finish_ng_bt.setEnabled(true);
-//                MesClient.finish_ok_bt.setEnabled(true);
-//                if(MesClient.work_status == 1){
-//                    Boolean sendret = DataUtil.sendQuality(MesClient.nettyClient,MesClient.product_sn.getText(),"OK",MesClient.user20,"A");
-//                    if(!sendret){
-//                        MesClient.pxstatus1.setText("A:结果上传MES失败");
-//                        MesClient.tjStatusa = 1;
-//                    }
-//                }
+                promptManualSubmit("A");
             }
         });
         settingMenu.add(resetTcpMenu_1sd);
@@ -658,14 +736,7 @@ public class MesClient extends JFrame {
             @Override
             public void mousePressed(MouseEvent e) {
                 super.mouseClicked(e);
-                finish_ok_bt2.setEnabled(true);
-                finish_ng_bt2.setEnabled(true);
-//                MesClient.finish_ng_bt.setEnabled(true);
-//                Boolean sendret = DataUtil.sendQuality(MesClient.nettyClient,MesClient.product_sn2.getText(),"OK",MesClient.user20,"B");
-//                if(!sendret){
-//                    MesClient.pxstatus2.setText("B:结果上传MES失败");
-//                    MesClient.tjStatusb = 1;
-//                }
+                promptManualSubmit("B");
             }
         });
         settingMenu.add(resetTcpMenu_1_1sd);
@@ -767,152 +838,6 @@ public class MesClient extends JFrame {
 //        mesRadioHj.setBounds(190,170,500,50);
 //        indexPanelA.add(mesRadioHj);
 
-        finish_ok_bt = new JButton("OK");
-        finish_ok_bt.setEnabled(false);
-        finish_ok_bt.addActionListener(new ActionListener() {
-            public void actionPerformed(ActionEvent e) {
-//                if(work_status == 1 && check_quality_result){
-//
-//
-//                }
-                String sn = getBarcode(product_sn.getText());
-                getUser();
-
-                if(!sn.isEmpty()){
-                    String qret = "OK";
-                    Boolean sendret = DataUtil.sendQuality(nettyClient,sn,qret,user20,"A");
-                    if(!sendret){
-                        MesClient.pxstatus1.setText("A:结果上传MES失败");
-                        MesClient.tjStatusa = 1;
-                    }else{
-                        MesClient.pxstatus1.setText("A:提交成功");
-                        MesClient.status_menu.setText("A件提交成功");
-                        MesClient.resetScanA();
-                    }
-                }
-
-
-
-            }
-        });
-        finish_ok_bt.setIcon(new ImageIcon(MesClient.class.getResource("/bg/ok_bg.png")));
-        finish_ok_bt.setFont(new Font("Microsoft YaHei UI", Font.PLAIN, 32));
-        finish_ok_bt.setBounds(50, 380, 160, 80);
-        finish_ok_bt.setEnabled(false);
-//        indexPanelA.add(finish_ok_bt);
-
-        finish_ng_bt = new JButton("NG");
-        finish_ng_bt.setEnabled(false);
-        finish_ng_bt.addActionListener(new ActionListener() {
-            public void actionPerformed(ActionEvent e) {
-//                if(work_status == 1 && check_quality_result){
-//
-//                    String sn = getBarcode(product_sn.getText());
-//                    getUser();
-//                    String qret = "OK";
-//                    Boolean sendret = DataUtil.sendQuality(nettyClient,sn,qret,user20,"B");
-//                    if(!sendret){
-//                        JOptionPane.showMessageDialog(mesClientFrame,"上传MES失败,请重试","提醒", JOptionPane.INFORMATION_MESSAGE);
-//                        return;
-//                    }
-//                }
-                String sn = getBarcode(product_sn2.getText());
-                getUser();
-
-                if(!sn.isEmpty()){
-                    String qret = "OK";
-                    Boolean sendret = DataUtil.sendQuality(nettyClient,sn,qret,user20,"B");
-                    if(!sendret){
-                        MesClient.pxstatus2.setText("B:结果上传MES失败");
-                        MesClient.tjStatusb = 1;
-                    }else{
-                        MesClient.pxstatus2.setText("B:提交成功");
-                        MesClient.status_menu.setText("B件提交成功");
-                        MesClient.resetScanB();
-                    }
-                }
-            }
-        });
-        finish_ng_bt.setIcon(new ImageIcon(MesClient.class.getResource("/bg/ok_bg.png")));
-        finish_ng_bt.setFont(new Font("Microsoft YaHei UI", Font.PLAIN, 32));
-        finish_ng_bt.setBounds(280, 380, 160, 80);
-        finish_ng_bt.setEnabled(false);
-//        indexPanelA.add(finish_ng_bt);
-
-
-
-        finish_ok_bt2 = new JButton("OK");
-        finish_ok_bt2.setEnabled(false);
-        finish_ok_bt2.addActionListener(new ActionListener() {
-            public void actionPerformed(ActionEvent e) {
-//                if(work_status == 1 && check_quality_result){
-//
-//
-//                }
-                String sn = getBarcode(product_sn.getText());
-                getUser();
-
-                if(!sn.isEmpty()){
-                    String qret = "OK";
-                    Boolean sendret = DataUtil.sendQuality(nettyClient,sn,qret,user20,"B");
-                    if(!sendret){
-                        MesClient.pxstatus2.setText("B:结果上传MES失败");
-                        MesClient.tjStatusb = 1;
-                    }else{
-                        MesClient.pxstatus2.setText("B:提交成功");
-                        MesClient.status_menu.setText("B件提交成功");
-                        MesClient.resetScanB();
-                    }
-                }
-
-
-
-            }
-        });
-        finish_ok_bt2.setIcon(new ImageIcon(MesClient.class.getResource("/bg/ok_bg.png")));
-        finish_ok_bt2.setFont(new Font("Microsoft YaHei UI", Font.PLAIN, 32));
-        finish_ok_bt2.setBounds(550, 380, 160, 80);
-        finish_ok_bt2.setEnabled(false);
-//        indexPanelA.add(finish_ok_bt2);
-
-        finish_ng_bt2 = new JButton("NG");
-        finish_ng_bt2.setEnabled(false);
-        finish_ng_bt2.addActionListener(new ActionListener() {
-            public void actionPerformed(ActionEvent e) {
-//                if(work_status == 1 && check_quality_result){
-//
-//                    String sn = getBarcode(product_sn.getText());
-//                    getUser();
-//                    String qret = "OK";
-//                    Boolean sendret = DataUtil.sendQuality(nettyClient,sn,qret,user20,"B");
-//                    if(!sendret){
-//                        JOptionPane.showMessageDialog(mesClientFrame,"上传MES失败,请重试","提醒", JOptionPane.INFORMATION_MESSAGE);
-//                        return;
-//                    }
-//                }
-                String sn = getBarcode(product_sn2.getText());
-                getUser();
-
-                if(!sn.isEmpty()){
-                    String qret = "NG";
-                    Boolean sendret = DataUtil.sendQuality(nettyClient,sn,qret,user20,"B");
-                    if(!sendret){
-                        MesClient.pxstatus2.setText("B:结果上传MES失败");
-                        MesClient.tjStatusb = 1;
-                    }else{
-                        MesClient.pxstatus2.setText("B:提交成功");
-                        MesClient.status_menu.setText("B件提交成功");
-                        MesClient.resetScanB();
-                    }
-                }
-            }
-        });
-        finish_ng_bt2.setIcon(new ImageIcon(MesClient.class.getResource("/bg/ok_bg.png")));
-        finish_ng_bt2.setFont(new Font("Microsoft YaHei UI", Font.PLAIN, 32));
-        finish_ng_bt2.setBounds(780, 380, 160, 80);
-        finish_ng_bt2.setEnabled(false);
-//        indexPanelA.add(finish_ng_bt2);
-
         product_sn2 = new JTextField();
         product_sn2.setText("");
         product_sn2.setHorizontalAlignment(SwingConstants.CENTER);
@@ -1393,67 +1318,164 @@ public class MesClient extends JFrame {
         MesClient.status_menu.setText(msg);
     }
 
-    public static void getMaterailData(){
-        JSONObject retObj = DataUtil.getBindMaterail();
-        if(retObj.get("result")!=null&&retObj.get("result").toString().equalsIgnoreCase("true")) {
-            List<BindMaterialResp> arrs = retObj.getList("data",BindMaterialResp.class);
-            int i = 0;
-            rowData = new Object[arrs.size()][7];
-            for (BindMaterialResp bindMaterialResp:arrs){
-                rowData[i][0] = bindMaterialResp.getMaterialTitle();
-                rowData[i][1] = bindMaterialResp.getBatchSn();
-                rowData[i][2] = bindMaterialResp.getLastTimes();
-                rowData[i][3] = "";
-                rowData[i][4] = bindMaterialResp.getCraft();
-                rowData[i][5] = bindMaterialResp.getMaterialId();
-                rowData[i][6] = bindMaterialResp.getType();
-                i++;
-            }
-            bindBatchPanel();
+    /** A/B 面绑定工位:gwflag=A 绑 OP060A/B,gwflag=B 绑 OP060C/D */
+    public static String[] getMaterialOprnos(){
+        if("A".equals(mes_gwflag)){
+            return new String[]{mes_gw + "A", mes_gw + "B"};
+        }else if("B".equals(mes_gwflag)){
+            return new String[]{mes_gw + "C", mes_gw + "D"};
         }
+        return new String[]{mes_gw};
     }
 
-    public static void updateMaterailData(){
-        JSONObject retObj = DataUtil.getBindMaterail();
-        if(retObj.get("result")!=null&&retObj.get("result").toString().equalsIgnoreCase("true")) {
-            List<BindMaterialResp> arrs = retObj.getList("data",BindMaterialResp.class);
-
-            int i = 0;
-            for (BindMaterialResp bindMaterialResp:arrs){
-                rowData[i][0] = bindMaterialResp.getMaterialTitle();
-                rowData[i][1] = bindMaterialResp.getBatchSn();
-                rowData[i][2] = bindMaterialResp.getLastTimes();
-                rowData[i][3] = "";
-                rowData[i][4] = bindMaterialResp.getCraft();
-                rowData[i][5] = bindMaterialResp.getMaterialId();
-                rowData[i][6] = bindMaterialResp.getType();
-                i++;
+    /** A/B 共用库存:合并各面查询结果,剩余次数取较小值 */
+    private static String materialRowKey(BindMaterialResp item){
+        return String.valueOf(item.getMaterialId()) + "|" + String.valueOf(item.getCraft()) + "|" + String.valueOf(item.getType());
+    }
+
+    private static Integer parseLastTimes(String lastTimes){
+        if(lastTimes == null || lastTimes.trim().isEmpty()){
+            return null;
+        }
+        try{
+            return Integer.parseInt(lastTimes.trim());
+        }catch (NumberFormatException e){
+            return null;
+        }
+    }
+
+    private static String mergeSharedLastTimes(String left, String right){
+        Integer leftVal = parseLastTimes(left);
+        Integer rightVal = parseLastTimes(right);
+        if(leftVal == null){
+            return right != null ? right : "";
+        }
+        if(rightVal == null){
+            return left;
+        }
+        return String.valueOf(Math.min(leftVal, rightVal));
+    }
+
+    private static String pickSharedBatchSn(String left, String right){
+        if(left != null && !left.trim().isEmpty()){
+            return left;
+        }
+        return right != null ? right : "";
+    }
+
+    private static void mergeSharedMaterial(BindMaterialResp target, BindMaterialResp source){
+        target.setLastTimes(mergeSharedLastTimes(target.getLastTimes(), source.getLastTimes()));
+        target.setBatchSn(pickSharedBatchSn(target.getBatchSn(), source.getBatchSn()));
+    }
+
+    private static List<BindMaterialResp> loadSharedMaterialList(){
+        Map<String, BindMaterialResp> merged = new LinkedHashMap<>();
+        for(String oprno : getMaterialOprnos()){
+            JSONObject retObj = DataUtil.getBindMaterail(oprno);
+            if(retObj == null || retObj.get("result") == null || !retObj.get("result").toString().equalsIgnoreCase("true")){
+                continue;
+            }
+            List<BindMaterialResp> arrs = retObj.getList("data", BindMaterialResp.class);
+            if(arrs == null || arrs.isEmpty()){
+                continue;
+            }
+            for(BindMaterialResp item : arrs){
+                String key = materialRowKey(item);
+                BindMaterialResp existing = merged.get(key);
+                if(existing == null){
+                    merged.put(key, item);
+                }else{
+                    mergeSharedMaterial(existing, item);
+                }
             }
+        }
+        return new ArrayList<>(merged.values());
+    }
+
+    private static Object[][] buildMaterialRowData(){
+        List<BindMaterialResp> arrs = loadSharedMaterialList();
+        if(arrs.isEmpty()){
+            return new Object[0][7];
+        }
+        Object[][] rows = new Object[arrs.size()][7];
+        int i = 0;
+        for (BindMaterialResp bindMaterialResp : arrs){
+            rows[i][0] = bindMaterialResp.getMaterialTitle();
+            rows[i][1] = bindMaterialResp.getBatchSn() != null ? bindMaterialResp.getBatchSn() : "";
+            rows[i][2] = bindMaterialResp.getLastTimes() != null ? bindMaterialResp.getLastTimes() : "";
+            rows[i][3] = "";
+            rows[i][4] = bindMaterialResp.getCraft();
+            rows[i][5] = bindMaterialResp.getMaterialId();
+            rows[i][6] = bindMaterialResp.getType();
+            i++;
+        }
+        return rows;
+    }
 
+    public static void getMaterailData(){
+        rowData = buildMaterialRowData();
+        bindBatchPanel();
+    }
+
+    public static void updateMaterailData(){
+        rowData = buildMaterialRowData();
+        if(MesClient.table != null){
+            MesClient.table.setModel(new javax.swing.table.DefaultTableModel(rowData, columnNames){
+                public boolean isCellEditable(int row, int column) {
+                    return column == 3;
+                }
+            });
+            MesClient.table.getColumnModel().getColumn(3).setCellRenderer(new TableCellRendererButton());
+            MesClient.table.getColumnModel().getColumn(3).setCellEditor(new TableCellEditorButton());
+            MesClient.table.revalidate();
             MesClient.table.repaint();
         }
     }
 
-    // 绑定物料批次码
-    public static void scanBatchSn(BindMaterialResp bindMaterialResp) { // 扫码类型
-        //弹窗扫工件码
-        String scanBarcodeTitle = "请扫物料:"+bindMaterialResp.getMaterialTitle();
+    // 绑定物料批次码(A/B 共用,扫码一次同步绑定各面工位)
+    public static void scanBatchSn(BindMaterialResp bindMaterialResp) {
+        String scanBarcodeTitle = "请扫物料:" + bindMaterialResp.getMaterialTitle();
         String scanBarcode = JOptionPane.showInputDialog(null, scanBarcodeTitle);
         if(scanBarcode!=null&&!scanBarcode.equalsIgnoreCase("")) {
+            String[] oprnos = getMaterialOprnos();
+            if(oprnos.length == 0){
+                MesClient.setMenuStatus("工位配置异常,请检查 config.properties", -1);
+                return;
+            }
 
-            JSONObject retObj = DataUtil.saveBindMaterail(scanBarcode,bindMaterialResp.getCraft(),bindMaterialResp.getMaterialId(),bindMaterialResp.getType());
-            if(retObj.get("result")!=null&&retObj.get("result").toString().equalsIgnoreCase("true")) {
-                MesClient.setMenuStatus("扫物料:"+bindMaterialResp.getMaterialTitle()+"成功",0);
-                updateMaterailData();
-            }else{
-                if(retObj.get("result")==null){
-                    MesClient.setMenuStatus("请求失败,请重试",-1);
-                }else{
-                    if(retObj.get("result").toString().equalsIgnoreCase("false")){
-                        MesClient.setMenuStatus(retObj.getString("message"),-1);
+            String failOprno = null;
+            String failMsg = null;
+            for(String oprno : oprnos){
+                JSONObject retObj = DataUtil.saveBindMaterail(
+                        scanBarcode,
+                        bindMaterialResp.getCraft(),
+                        bindMaterialResp.getMaterialId(),
+                        bindMaterialResp.getType(),
+                        oprno.trim());
+                if(retObj == null || retObj.get("result") == null){
+                    failOprno = oprno;
+                    failMsg = "请求失败,请重试";
+                    break;
+                }
+                if(!retObj.get("result").toString().equalsIgnoreCase("true")){
+                    failOprno = oprno;
+                    failMsg = retObj.getString("message");
+                    if(failMsg == null || failMsg.isEmpty()){
+                        failMsg = "绑定失败";
                     }
+                    break;
                 }
             }
+
+            if(failOprno == null){
+                MesClient.setMenuStatus("扫物料:" + bindMaterialResp.getMaterialTitle() + "成功(A/B共用)", 0);
+                updateMaterailData();
+            }else if(oprnos.length > 1){
+                MesClient.setMenuStatus(failOprno + "绑定失败:" + failMsg, -1);
+                updateMaterailData();
+            }else{
+                MesClient.setMenuStatus(failMsg, -1);
+            }
         }
     }
 

+ 3 - 10
src/com/mes/ui/PlcUtil.java

@@ -5,6 +5,7 @@ import com.github.xingshuangs.iot.protocol.modbus.service.ModbusTcp;
 import com.github.xingshuangs.iot.protocol.s7.service.S7PLC;
 import com.mes.component.MyDialog;
 import com.mes.util.DateLocalUtils;
+import com.mes.util.IweldCloudUtil;
 import com.mes.util.JdbcUtils;
 
 import javax.swing.*;
@@ -32,7 +33,6 @@ public class PlcUtil {
             if(starta || startb){ // B启动中,A当做完成
                 MesClient.tjFlaga = 3;
                 MesClient.pxstatus1.setText("A:设备运行结束,提交结果中");
-                MesClient.finish_ok_bt.setEnabled(true);
 
                 Boolean sendret = DataUtil.sendQuality(MesClient.nettyClient,MesClient.product_sn.getText(),"OK",MesClient.user20,"A");
                 if(!sendret){
@@ -93,7 +93,6 @@ public class PlcUtil {
             if(starta || startb){
                 MesClient.tjFlagb = 3;
                 MesClient.pxstatus2.setText("B:设备运行结束,提交结果中");
-                MesClient.finish_ng_bt.setEnabled(true);
 
                 Boolean sendret = DataUtil.sendQuality(MesClient.nettyClient,MesClient.product_sn2.getText(),"OK",MesClient.user20,"B");
                 if(!sendret){
@@ -124,11 +123,8 @@ public class PlcUtil {
                 PlcUtil.changeEnableA(s7PLC,false);
                 MesClient.tjFlaga = 3;
                 MesClient.pxstatus1.setText("A:设备运行结束,提交结果中");
-                MesClient.finish_ok_bt.setEnabled(true);
-                MesClient.finish_ng_bt.setEnabled(true);
 
-                Boolean sendret = DataUtil.sendQuality(MesClient.nettyClient,MesClient.product_sn.getText(),"OK",MesClient.user20,"A");
-                if(!sendret){
+                if(!MesClient.submitQualityAndFlush("A", "OK")){
                     MesClient.pxstatus1.setText("A:结果上传MES失败");
                     MesClient.tjStatusa = 1;
                 }
@@ -152,11 +148,8 @@ public class PlcUtil {
                 PlcUtil.changeEnableB(s7PLC,false);
                 MesClient.tjFlagb = 3;
                 MesClient.pxstatus2.setText("B:设备运行结束,提交结果中");
-                MesClient.finish_ng_bt2.setEnabled(true);
-                MesClient.finish_ok_bt2.setEnabled(true);
 
-                Boolean sendret = DataUtil.sendQuality(MesClient.nettyClient,MesClient.product_sn2.getText(),"OK",MesClient.user20,"B");
-                if(!sendret){
+                if(!MesClient.submitQualityAndFlush("B", "OK")){
                     MesClient.pxstatus2.setText("B:结果上传MES失败");
                     MesClient.tjStatusb = 1;
                 }

+ 28 - 15
src/com/mes/ui/TableCellEditorButton.java

@@ -6,38 +6,51 @@ import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 
 public class TableCellEditorButton extends DefaultCellEditor {
+
     private JButton btn;
+    private int editingRow = -1;
+
     public TableCellEditorButton() {
         super(new JTextField());
-        //设置点击一次就激活,否则默认好像是点击2次激活。
         this.setClickCountToStart(1);
         btn = new JButton("扫码");
         btn.addActionListener(new ActionListener() {
-
             @Override
             public void actionPerformed(ActionEvent e) {
-                System.out.println("按钮事件触发----");
-                int selectedRow = MesClient.table.getSelectedRow();//获得选中行的索引
-//                MesClient.rowData[selectedRow][1] = (new Date()).getTime();
-//                MesClient.table.repaint(); //重绘
+                int row = editingRow;
+                if(row < 0){
+                    row = MesClient.table.getSelectedRow();
+                }
+                if(row < 0 || MesClient.rowData == null || row >= MesClient.rowData.length){
+                    MesClient.setMenuStatus("请先选中物料行再扫码", 1);
+                    cancelCellEditing();
+                    return;
+                }
+
+                Object[] rowData = MesClient.rowData[row];
+                if(rowData.length < 7){
+                    MesClient.setMenuStatus("物料数据异常,请刷新页面", 1);
+                    cancelCellEditing();
+                    return;
+                }
 
-//                MesClient.scan_type = selectedRow + 4;
                 BindMaterialResp bindMaterialResp = new BindMaterialResp();
-                bindMaterialResp.setMaterialTitle(MesClient.rowData[selectedRow][0] + "");
-                bindMaterialResp.setBatchSn(MesClient.rowData[selectedRow][1] + "");
-                bindMaterialResp.setLastTimes(MesClient.rowData[selectedRow][2] + "");
-                bindMaterialResp.setCraft(MesClient.rowData[selectedRow][4] + "");
-                bindMaterialResp.setMaterialId(MesClient.rowData[selectedRow][5] + "");
-                bindMaterialResp.setType(MesClient.rowData[selectedRow][6] + "");
+                bindMaterialResp.setMaterialTitle(String.valueOf(rowData[0]));
+                bindMaterialResp.setBatchSn(String.valueOf(rowData[1]));
+                bindMaterialResp.setLastTimes(String.valueOf(rowData[2]));
+                bindMaterialResp.setCraft(String.valueOf(rowData[4]));
+                bindMaterialResp.setMaterialId(String.valueOf(rowData[5]));
+                bindMaterialResp.setType(String.valueOf(rowData[6]));
 
+                cancelCellEditing();
                 MesClient.scanBatchSn(bindMaterialResp);
             }
         });
-
     }
+
     @Override
     public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
-
+        editingRow = row;
         return btn;
     }
 }

+ 2 - 0
src/com/mes/util/ErrorMsg.java

@@ -36,6 +36,8 @@ public class ErrorMsg {
                 lmsg = "更换配件首件检查不合格停机";
             }else if(processMsgRet.equalsIgnoreCase("DJ")) {
                 lmsg = "未进行开班点检";
+            }else if(processMsgRet.equalsIgnoreCase("BY")) {
+                lmsg = "设备保养到期,请先完成保养";
             }else if(processMsgRet.equalsIgnoreCase("BM")) {
                 lmsg = "未绑定物料";
             }else if(processMsgRet.equalsIgnoreCase("PL")) {

+ 245 - 34
src/com/mes/util/IweldCloudUtil.java

@@ -16,6 +16,16 @@ import java.util.Properties;
 public class IweldCloudUtil {
     public static final Logger log = LoggerFactory.getLogger(IweldCloudUtil.class);
 
+    private static final String DEFAULT_LOGIN_URL = "https://api.iweldcloud.com/ApiServer/Login";
+    private static final String DEFAULT_ROBOT_RUN_INFO_URL =
+            "https://api.iweldcloud.com/ApiServer/rest/WeldWebService/getRobotRunInfo";
+    private static final String DEFAULT_WELDER_RUN_INFO_URL =
+            "https://api.iweldcloud.com/ApiServer/rest/WeldWebService/getWeldRunInfo";
+    private static final String DEFAULT_WELD_LIST_URL =
+            "https://api.iweldcloud.com/ApiServer/rest/WeldWebService/getWeldListInfo";
+    /** 云平台 API 要求请求间隔不小于 1 秒 */
+    public static final long API_MIN_INTERVAL_MS = 1100L;
+
     private static volatile String sessionKey = "";
     private static volatile String robotRunInfo = "";
 
@@ -27,6 +37,23 @@ public class IweldCloudUtil {
         public boolean hasData() {
             return !voltage.isEmpty() || !current.isEmpty() || !wireSpeed.isEmpty();
         }
+
+        @Override
+        public String toString() {
+            return "电压=" + voltage + ", 电流=" + current + ", 送丝速度=" + wireSpeed;
+        }
+    }
+
+    public static long getCollectIntervalMs() {
+        try {
+            Properties pro = loadConfig();
+            if (!pro.getProperty("iweld.prodCode2", "").trim().isEmpty()) {
+                return API_MIN_INTERVAL_MS * 2 + 300L;
+            }
+        } catch (Exception e) {
+            log.warn("IweldCloud read collect interval config failed", e);
+        }
+        return API_MIN_INTERVAL_MS;
     }
 
     public static String getSessionKey() {
@@ -41,7 +68,7 @@ public class IweldCloudUtil {
         try {
             Properties pro = loadConfig();
             String accessKey = pro.getProperty("iweld.accessKey", "").trim();
-            String loginUrl = pro.getProperty("iweld.login.url", "https://api.iweldcloud.com/ApiServer/Login").trim();
+            String loginUrl = pro.getProperty("iweld.login.url", DEFAULT_LOGIN_URL).trim();
             if (accessKey.isEmpty()) {
                 log.warn("IweldCloud login skipped: iweld.accessKey is empty");
                 return false;
@@ -56,18 +83,23 @@ public class IweldCloudUtil {
             }
 
             JSONObject json = JSONObject.parseObject(response);
-            if (json.getIntValue("execution") != 0) {
-                log.error("IweldCloud login failed: execution={}", json.getIntValue("execution"));
+            JSONObject result = json.getJSONObject("result");
+            if (result == null) {
+                log.error("IweldCloud login failed: missing result");
                 return false;
             }
-
-            JSONObject result = json.getJSONObject("result");
-            if (result == null || result.getIntValue("status") != 1) {
-                log.error("IweldCloud login failed: invalid result status");
+            if (result.getIntValue("status") != 1) {
+                JSONObject error = result.getJSONObject("error");
+                String message = error != null ? error.getString("message") : "unknown";
+                log.error("IweldCloud login failed: status={}, message={}", result.getIntValue("status"), message);
                 return false;
             }
 
             sessionKey = result.getString("sessionKey");
+            if (sessionKey == null || sessionKey.isEmpty()) {
+                log.error("IweldCloud login failed: empty sessionKey");
+                return false;
+            }
             log.info("IweldCloud login success, sessionKey={}", sessionKey);
             return true;
         } catch (Exception e) {
@@ -84,19 +116,91 @@ public class IweldCloudUtil {
 
             Properties pro = loadConfig();
             String prodCode = pro.getProperty("iweld.prodCode", "0").trim();
-            String baseUrl = pro.getProperty("iweld.robot.run.info.url",
-                    "https://api.iweldcloud.com/ApiServer/rest/WeldWebService/getRobotRunInfo").trim();
-            String url = baseUrl + "?prodCode=" + prodCode;
-            String response = HttpUtils.sendGetRequestWithHeader(url, "app-token", sessionKey);
-            if (response == null || response.isEmpty() || "false".equalsIgnoreCase(response)) {
-                log.warn("IweldCloud getRobotRunInfo failed: empty response");
+            String response = fetchRunInfoRaw(prodCode);
+            if (response == null) {
                 return false;
             }
 
             robotRunInfo = response;
             return true;
         } catch (Exception e) {
-            log.error("IweldCloud getRobotRunInfo error", e);
+            log.error("IweldCloud fetch run info error", e);
+            return false;
+        }
+    }
+
+    public static String fetchRunInfoForProdCode(String prodCode) {
+        if (sessionKey == null || sessionKey.isEmpty()) {
+            return null;
+        }
+        return fetchRunInfoRaw(prodCode);
+    }
+
+    public static String fetchRunInfoForProdCodeWithUrl(String prodCode, String baseUrl) {
+        if (sessionKey == null || sessionKey.isEmpty()) {
+            return null;
+        }
+        try {
+            String url = baseUrl.trim() + "?prodCode=" + prodCode + "&output=json";
+            String response = HttpUtils.sendGetRequestWithHeader(url, "app-token", sessionKey);
+            if (!isResponseBodyValid(response)) {
+                return null;
+            }
+            return response;
+        } catch (Exception e) {
+            log.error("IweldCloud fetch run info with url error, prodCode={}", prodCode, e);
+            return null;
+        }
+    }
+
+    public static String fetchWeldListInfo() {
+        try {
+            if (sessionKey == null || sessionKey.isEmpty()) {
+                return null;
+            }
+            Properties pro = loadConfig();
+            String baseUrl = pro.getProperty("iweld.weld.list.url", DEFAULT_WELD_LIST_URL).trim();
+            String url = baseUrl + "?output=json";
+            String response = HttpUtils.sendGetRequestWithHeader(url, "app-token", sessionKey);
+            if (!isResponseBodyValid(response)) {
+                log.warn("IweldCloud getWeldListInfo failed: invalid response");
+                return null;
+            }
+            return response;
+        } catch (Exception e) {
+            log.error("IweldCloud getWeldListInfo error", e);
+            return null;
+        }
+    }
+
+    public static String describeApiStatus(String response) {
+        if (response == null || response.isEmpty()) {
+            return "响应为空";
+        }
+        try {
+            JSONObject json = JSONObject.parseObject(response);
+            if (json.containsKey("exception")) {
+                int code = json.getIntValue("exception");
+                return code == 0 ? "正常(exception=0)" : "异常(exception=" + code + ")";
+            }
+            if (json.containsKey("Execution")) {
+                int code = json.getIntValue("Execution");
+                return code == 0 ? "正常(Execution=0)" : "异常(Execution=" + code + ")";
+            }
+            return "未找到 exception/Execution 状态字段";
+        } catch (Exception e) {
+            return "响应不是有效JSON";
+        }
+    }
+
+    public static boolean isApiResponseOk(String response) {
+        if (response == null || response.isEmpty()) {
+            return false;
+        }
+        try {
+            JSONObject json = JSONObject.parseObject(response);
+            return isApiResponseOk(json);
+        } catch (Exception e) {
             return false;
         }
     }
@@ -136,17 +240,46 @@ public class IweldCloudUtil {
         );
 
         if (MesClient.hjparams.size() == 60) {
-            try {
-                String oprno = buildOprno();
-                if (MesClient.curFlag.equals("A")) {
-                    JdbcUtils.insertCmtData(oprno, MesClient.mes_line_sn, MesClient.product_sn.getText(), JSON.toJSONString(MesClient.hjparams));
-                } else {
-                    JdbcUtils.insertCmtData(oprno, MesClient.mes_line_sn, MesClient.product_sn2.getText(), JSON.toJSONString(MesClient.hjparams));
-                }
-                MesClient.hjparams = new ArrayList<>();
-            } catch (Exception e) {
-                e.printStackTrace();
+            persistHjParams(MesClient.curFlag);
+        }
+    }
+
+    /**
+     * 焊接结束时将不足 60 条的参数落库。
+     * 若另一面正在焊接且缓存归属另一面,则跳过,避免串数据。
+     */
+    public static void flushWeldingParamsOnFinish(String face) {
+        if (MesClient.hjparams == null || MesClient.hjparams.isEmpty()) {
+            return;
+        }
+        boolean otherSideWelding = ("A".equals(face) && MesClient.tjFlagb == 2)
+                || ("B".equals(face) && MesClient.tjFlaga == 2);
+        if(otherSideWelding && !face.equals(MesClient.curFlag)){
+            log.info("IweldCloud skip flush for face {} because other side is welding", face);
+            return;
+        }
+        persistHjParams(face);
+    }
+
+    private static void persistHjParams(String face) {
+        if (MesClient.hjparams == null || MesClient.hjparams.isEmpty()) {
+            return;
+        }
+        try {
+            String sn = "A".equals(face)
+                    ? MesClient.product_sn.getText()
+                    : MesClient.product_sn2.getText();
+            if (sn == null || sn.trim().isEmpty()) {
+                log.warn("IweldCloud persist hjparams skipped: empty sn, face={}", face);
+                return;
             }
+            String oprno = buildOprnoForFace(face);
+            int count = MesClient.hjparams.size();
+            JdbcUtils.insertCmtData(oprno, MesClient.mes_line_sn, sn, JSON.toJSONString(MesClient.hjparams));
+            MesClient.hjparams = new ArrayList<>();
+            log.info("IweldCloud persisted {} welding param records, face={}, oprno={}", count, face, oprno);
+        } catch (Exception e) {
+            log.error("IweldCloud persist hjparams error, face={}", face, e);
         }
     }
 
@@ -162,9 +295,9 @@ public class IweldCloudUtil {
             }
 
             WelderParams[] welders = new WelderParams[2];
-            welders[0] = parseWelderItem(dataList.getJSONObject(0));
+            welders[0] = parseWelderItem(extractDataItem(dataList.getJSONObject(0)));
             if (dataList.size() > 1) {
-                welders[1] = parseWelderItem(dataList.getJSONObject(1));
+                welders[1] = parseWelderItem(extractDataItem(dataList.getJSONObject(1)));
             } else {
                 welders[1] = new WelderParams();
             }
@@ -175,17 +308,29 @@ public class IweldCloudUtil {
         }
     }
 
+    private static JSONObject extractDataItem(JSONObject wrapper) {
+        if (wrapper == null) {
+            return null;
+        }
+        JSONArray nestedList = wrapper.getJSONArray("list");
+        if (nestedList != null && !nestedList.isEmpty()) {
+            return nestedList.getJSONObject(0);
+        }
+        return wrapper;
+    }
+
     private static WelderParams parseWelderItem(JSONObject item) {
         WelderParams params = new WelderParams();
         if (item == null) {
             return params;
         }
+        // 机器人实时数据: weldingAv/weldingAi/silkRate;焊机实时数据: D08/D07/D18
         params.voltage = readField(item,
-                "weldingVoltage", "voltage", "vol", "v_meas", "actualVoltage", "actualVol", "U", "dy", "voltageValue");
+                "weldingAv", "D08", "WeldV", "weldingVoltage", "voltage", "actualVoltage", "actualVol");
         params.current = readField(item,
-                "weldingCurrent", "current", "cur", "c_meas", "actualCurrent", "actualCur", "I", "dl", "currentValue", "electricity");
+                "weldingAi", "D07", "WeldA", "weldingCurrent", "current", "actualCurrent", "actualCur");
         params.wireSpeed = readField(item,
-                "wireFeedSpeed", "wireSpeed", "feedingSpeed", "f_meas", "speed", "ss", "feedSpeed", "wireFeed", "feeding");
+                "silkRate", "D18", "SilkRate", "wireFeedSpeed", "wireSpeed", "feedingSpeed", "feedSpeed");
         return params;
     }
 
@@ -208,9 +353,8 @@ public class IweldCloudUtil {
             if (welders == null) {
                 welders = new WelderParams[]{new WelderParams(), new WelderParams()};
             }
-            String baseUrl = pro.getProperty("iweld.robot.run.info.url",
-                    "https://api.iweldcloud.com/ApiServer/rest/WeldWebService/getRobotRunInfo").trim();
-            String response = HttpUtils.sendGetRequestWithHeader(baseUrl + "?prodCode=" + prodCode2, "app-token", sessionKey);
+            Thread.sleep(API_MIN_INTERVAL_MS);
+            String response = fetchRunInfoRaw(prodCode2);
             WelderParams[] second = parseWelderParamsFromResponse(response);
             if (second != null && second[0].hasData()) {
                 welders[1] = second[0];
@@ -221,10 +365,77 @@ public class IweldCloudUtil {
         return welders;
     }
 
+    private static String fetchRunInfoRaw(String prodCode) {
+        try {
+            String url = buildRunInfoUrl(prodCode);
+            String response = HttpUtils.sendGetRequestWithHeader(url, "app-token", sessionKey);
+            if (!isResponseBodyValid(response)) {
+                log.warn("IweldCloud run info failed: prodCode={}, status={}", prodCode, describeApiStatus(response));
+                return null;
+            }
+            return response;
+        } catch (Exception e) {
+            log.error("IweldCloud fetch run info raw error, prodCode={}", prodCode, e);
+            return null;
+        }
+    }
+
+    private static String buildRunInfoUrl(String prodCode) {
+        try {
+            Properties pro = loadConfig();
+            String baseUrl = resolveRunInfoUrl(pro);
+            return baseUrl + "?prodCode=" + prodCode + "&output=json";
+        } catch (Exception e) {
+            return DEFAULT_ROBOT_RUN_INFO_URL + "?prodCode=" + prodCode + "&output=json";
+        }
+    }
+
+    private static String resolveRunInfoUrl(Properties pro) {
+        String explicitUrl = pro.getProperty("iweld.run.info.url", "").trim();
+        if (!explicitUrl.isEmpty()) {
+            return explicitUrl;
+        }
+        String legacyUrl = pro.getProperty("iweld.robot.run.info.url", "").trim();
+        if (!legacyUrl.isEmpty()) {
+            return legacyUrl;
+        }
+        String deviceType = pro.getProperty("iweld.device.type", "robot").trim();
+        if ("welder".equalsIgnoreCase(deviceType)) {
+            return DEFAULT_WELDER_RUN_INFO_URL;
+        }
+        return DEFAULT_ROBOT_RUN_INFO_URL;
+    }
+
+    private static boolean isResponseBodyValid(String response) {
+        if (response == null || response.isEmpty() || "false".equalsIgnoreCase(response)) {
+            return false;
+        }
+        try {
+            JSONObject json = JSONObject.parseObject(response);
+            return isApiResponseOk(json);
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    private static boolean isApiResponseOk(JSONObject json) {
+        if (json.containsKey("exception")) {
+            return json.getIntValue("exception") == 0;
+        }
+        if (json.containsKey("Execution")) {
+            return json.getIntValue("Execution") == 0;
+        }
+        return false;
+    }
+
     private static String buildOprno() {
-        String side = MesClient.curFlag.equals("A") ? "A" : "B";
+        return buildOprnoForFace(MesClient.curFlag);
+    }
+
+    private static String buildOprnoForFace(String face) {
+        String side = "A".equals(face) ? "A" : "B";
         if (MesClient.mes_gwflag.equals("B")) {
-            side = MesClient.curFlag.equals("A") ? "C" : "D";
+            side = "A".equals(face) ? "C" : "D";
         }
         return MesClient.mes_gw + side;
     }

+ 197 - 16
src/com/mes/util/IweldCloudUtilTest.java

@@ -1,5 +1,6 @@
 package com.mes.util;
 
+import com.alibaba.fastjson2.JSONArray;
 import com.alibaba.fastjson2.JSONObject;
 
 import java.io.BufferedReader;
@@ -9,21 +10,20 @@ import java.util.Properties;
 
 public class IweldCloudUtilTest {
 
+    private static final long API_INTERVAL_MS = IweldCloudUtil.API_MIN_INTERVAL_MS;
+
     public static void main(String[] args) {
         System.out.println("========== IweldCloud 逻辑测试开始 ==========");
 
         try {
             Properties pro = loadConfig();
-            System.out.println("accessKey: " + mask(pro.getProperty("iweld.accessKey", "")));
-            System.out.println("loginUrl: " + pro.getProperty("iweld.login.url"));
-            System.out.println("prodCode: " + pro.getProperty("iweld.prodCode", "0"));
-            System.out.println("robotRunInfoUrl: " + pro.getProperty("iweld.robot.run.info.url"));
+            printConfig(pro);
             System.out.println();
 
             System.out.println("--- 1. 测试 Login ---");
             Boolean loginOk = IweldCloudUtil.login();
             System.out.println("Login结果: " + (loginOk ? "成功" : "失败"));
-            System.out.println("sessionKey: " + IweldCloudUtil.getSessionKey());
+            System.out.println("sessionKey: " + mask(IweldCloudUtil.getSessionKey()));
             System.out.println();
 
             if (!loginOk) {
@@ -31,29 +31,210 @@ public class IweldCloudUtilTest {
                 return;
             }
 
-            System.out.println("--- 2. 测试 getRobotRunInfo ---");
+            sleepBetweenRequests();
+
+            System.out.println("--- 2. 测试 getWeldListInfo(设备列表) ---");
+            String weldListInfo = IweldCloudUtil.fetchWeldListInfo();
+            if (weldListInfo == null) {
+                System.out.println("getWeldListInfo结果: 失败");
+            } else {
+                System.out.println("getWeldListInfo结果: 成功");
+                System.out.println("接口状态: " + IweldCloudUtil.describeApiStatus(weldListInfo));
+                printDeviceList(weldListInfo, pro);
+            }
+            System.out.println();
+
+            sleepBetweenRequests();
+
+            System.out.println("--- 3. 测试实时数据接口 ---");
+            String prodCode = pro.getProperty("iweld.prodCode", "0").trim();
             Boolean fetchOk = IweldCloudUtil.fetchRobotRunInfo();
-            System.out.println("getRobotRunInfo结果: " + (fetchOk ? "成功" : "失败"));
-            String robotInfo = IweldCloudUtil.getRobotRunInfo();
-            System.out.println("robotRunInfo原始数据:");
-            System.out.println(robotInfo);
+            String runInfo = IweldCloudUtil.getRobotRunInfo();
+            System.out.println("prodCode=" + prodCode + " 拉取结果: " + (fetchOk ? "成功" : "失败"));
+            if (fetchOk && runInfo != null && !runInfo.isEmpty()) {
+                System.out.println("接口状态: " + IweldCloudUtil.describeApiStatus(runInfo));
+            } else {
+                System.out.println("接口状态: 响应为空");
+                runInfo = tryAlternateRunInfoApi(pro, prodCode);
+            }
+            System.out.println("原始数据:");
+            System.out.println(runInfo);
             System.out.println();
 
-            try {
-                JSONObject json = JSONObject.parseObject(robotInfo);
-                System.out.println("robotRunInfo格式化输出:");
-                System.out.println(json.toJSONString());
-            } catch (Exception e) {
-                System.out.println("robotRunInfo不是有效JSON,已输出原始字符串");
+            if (runInfo != null && !runInfo.isEmpty()) {
+                try {
+                    JSONObject json = JSONObject.parseObject(runInfo);
+                    System.out.println("格式化JSON:");
+                    System.out.println(json.toJSONString());
+                } catch (Exception e) {
+                    System.out.println("响应不是有效JSON");
+                }
+            }
+            System.out.println();
+
+            System.out.println("--- 4. 测试参数解析(prodCode) ---");
+            IweldCloudUtil.WelderParams[] welders = IweldCloudUtil.parseWelderParamsFromResponse(runInfo);
+            printWelderParams("焊机1(prodCode=" + prodCode + ")", welders, 0);
+
+            String prodCode2 = pro.getProperty("iweld.prodCode2", "").trim();
+            if (!prodCode2.isEmpty()) {
+                sleepBetweenRequests();
+                System.out.println();
+                System.out.println("--- 5. 测试参数解析(prodCode2) ---");
+                String runInfo2 = IweldCloudUtil.fetchRunInfoForProdCode(prodCode2);
+                System.out.println("prodCode2=" + prodCode2 + " 拉取结果: "
+                        + (runInfo2 != null ? "成功" : "失败"));
+                if (runInfo2 != null) {
+                    System.out.println("接口状态: " + IweldCloudUtil.describeApiStatus(runInfo2));
+                }
+                IweldCloudUtil.WelderParams[] welders2 = IweldCloudUtil.parseWelderParamsFromResponse(runInfo2);
+                printWelderParams("焊机2(prodCode2=" + prodCode2 + ")", welders2, 0);
+
+                if (welders != null && welders2 != null && welders2[0].hasData()) {
+                    welders[1] = welders2[0];
+                }
+                System.out.println();
+                System.out.println("合并后双焊机参数:");
+                printWelderParams("焊机1", welders, 0);
+                printWelderParams("焊机2", welders, 1);
+            } else {
+                printWelderParams("焊机2(dataList第2条)", welders, 1);
             }
         } catch (Exception e) {
             System.out.println("测试异常: " + e.getMessage());
             e.printStackTrace();
         }
 
+        System.out.println();
         System.out.println("========== IweldCloud 逻辑测试结束 ==========");
     }
 
+    private static void printConfig(Properties pro) {
+        System.out.println("accessKey: " + mask(pro.getProperty("iweld.accessKey", "")));
+        System.out.println("loginUrl: " + pro.getProperty("iweld.login.url"));
+        System.out.println("device.type: " + pro.getProperty("iweld.device.type", "robot"));
+        System.out.println("prodCode: " + pro.getProperty("iweld.prodCode", "0"));
+        System.out.println("prodCode2: " + pro.getProperty("iweld.prodCode2", "(未配置)"));
+        String runInfoUrl = pro.getProperty("iweld.run.info.url", "").trim();
+        if (runInfoUrl.isEmpty()) {
+            runInfoUrl = pro.getProperty("iweld.robot.run.info.url", "(默认getRobotRunInfo)");
+        }
+        System.out.println("runInfoUrl: " + runInfoUrl);
+    }
+
+    private static String tryAlternateRunInfoApi(Properties pro, String prodCode) {
+        String currentType = pro.getProperty("iweld.device.type", "robot").trim();
+        String alternateType = "robot".equalsIgnoreCase(currentType) ? "welder" : "robot";
+        System.out.println();
+        System.out.println("提示: 当前接口拉取失败,尝试备用接口(" + alternateType + ")...");
+        sleepBetweenRequests();
+        String alternateUrl = "welder".equalsIgnoreCase(alternateType)
+                ? "https://api.iweldcloud.com/ApiServer/rest/WeldWebService/getWeldRunInfo"
+                : "https://api.iweldcloud.com/ApiServer/rest/WeldWebService/getRobotRunInfo";
+        String response = IweldCloudUtil.fetchRunInfoForProdCodeWithUrl(prodCode, alternateUrl);
+        if (response != null) {
+            System.out.println("备用接口拉取成功,建议将 iweld.device.type 改为 " + alternateType);
+            System.out.println("或设置 iweld.run.info.url=" + alternateUrl);
+            System.out.println("接口状态: " + IweldCloudUtil.describeApiStatus(response));
+            return response;
+        }
+        System.out.println("备用接口也失败,请检查 prodCode 是否正确、设备是否在线");
+        return null;
+    }
+
+    private static void printDeviceList(String weldListInfo, Properties pro) {
+        try {
+            JSONObject json = JSONObject.parseObject(weldListInfo);
+            JSONArray dataList = json.getJSONArray("dataList");
+            if (dataList == null || dataList.isEmpty()) {
+                System.out.println("设备列表为空");
+                return;
+            }
+
+            String prodCode = pro.getProperty("iweld.prodCode", "").trim();
+            String prodCode2 = pro.getProperty("iweld.prodCode2", "").trim();
+            int totalDevices = 0;
+            boolean matched = false;
+            System.out.println("当前配置设备在列表中的类型:");
+            for (int i = 0; i < dataList.size(); i++) {
+                JSONObject block = dataList.getJSONObject(i);
+                JSONArray list = block.getJSONArray("list");
+                if (list == null) {
+                    continue;
+                }
+                totalDevices += list.size();
+                for (int j = 0; j < list.size(); j++) {
+                    JSONObject device = list.getJSONObject(j);
+                    String code = device.getString("D01");
+                    if (code == null) {
+                        continue;
+                    }
+                    if (code.equals(prodCode) || code.equals(prodCode2)) {
+                        matched = true;
+                        System.out.println("  " + code + " -> D04=" + device.getString("D04")
+                                + " (" + deviceTypeName(device.getString("D04")) + "), D03="
+                                + device.getString("D03"));
+                    }
+                }
+            }
+            System.out.println("设备总数: " + totalDevices);
+            if (!matched) {
+                System.out.println("未在设备列表中找到 prodCode/prodCode2,请核对配置编码");
+                System.out.println("账号下全部设备(D01=制造编码, D03=自定义名称, D04=类型):");
+                for (int i = 0; i < dataList.size(); i++) {
+                    JSONObject block = dataList.getJSONObject(i);
+                    JSONArray list = block.getJSONArray("list");
+                    if (list == null) {
+                        continue;
+                    }
+                    for (int j = 0; j < list.size(); j++) {
+                        JSONObject device = list.getJSONObject(j);
+                        System.out.println("  D01=" + device.getString("D01")
+                                + ", D03=" + device.getString("D03")
+                                + ", D04=" + device.getString("D04")
+                                + " (" + deviceTypeName(device.getString("D04")) + ")");
+                    }
+                }
+            }
+        } catch (Exception e) {
+            System.out.println("解析设备列表失败: " + e.getMessage());
+        }
+    }
+
+    private static String deviceTypeName(String d04) {
+        if ("1".equals(d04)) {
+            return "焊机";
+        }
+        if ("2".equals(d04)) {
+            return "机器人";
+        }
+        if ("3".equals(d04)) {
+            return "激光";
+        }
+        return "未知";
+    }
+
+    private static void printWelderParams(String label, IweldCloudUtil.WelderParams[] welders, int index) {
+        if (welders == null || welders.length <= index) {
+            System.out.println(label + ": 无数据");
+            return;
+        }
+        IweldCloudUtil.WelderParams params = welders[index];
+        if (!params.hasData()) {
+            System.out.println(label + ": 未解析到电压/电流/送丝速度(请检查接口类型与字段映射)");
+            return;
+        }
+        System.out.println(label + ": " + params);
+    }
+
+    private static void sleepBetweenRequests() {
+        try {
+            Thread.sleep(API_INTERVAL_MS);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+    }
+
     private static Properties loadConfig() throws Exception {
         Properties pro = new Properties();
         InputStream is = ClassLoader.getSystemResourceAsStream("config/config.properties");

+ 15 - 5
src/resources/config/config.properties

@@ -1,6 +1,6 @@
-mes.gw=OP032
-mes.server_ip=127.0.0.1
-#mes.server_ip=192.168.16.99
+mes.gw=OP050
+#mes.server_ip=127.0.0.1
+mes.server_ip=192.168.16.99
 mes.tcp_port=3000
 mes.heart_beat_cycle=60
 mes.line_sn=XT
@@ -9,7 +9,17 @@ mes.gwflag=A
 # IweldCloud Login
 iweld.accessKey=19353:BN15MTY2NA==08X
 iweld.login.url=https://api.iweldcloud.com/ApiServer/Login
-iweld.prodCode=D2626S0083
+# 实时数据接口类型:robot=机器人(getRobotRunInfo),welder=焊机(getWeldRunInfo)
+iweld.device.type=robot
+# 3号机(D01 以云平台设备制造编码为准)
+iweld.prodCode=2026S0038
+# 4号机
+#iweld.prodCode=2026S0065
 # 焊机2产品编码,留空则仅使用 dataList 第2条或焊机2显示为空
-iweld.prodCode2=
+# 3号机
+iweld.prodCode2=2026S0082
+# 4号机
+#iweld.prodCode2=2026S0084
+# 实时数据接口地址(留空则按 iweld.device.type 自动选择)
+#iweld.run.info.url=
 iweld.robot.run.info.url=https://api.iweldcloud.com/ApiServer/rest/WeldWebService/getRobotRunInfo