jingbo 2 dní pred
commit
22812bc1c4
99 zmenil súbory, kde vykonal 5238 pridanie a 0 odobranie
  1. 25 0
      .classpath
  2. 8 0
      .gitignore
  3. 28 0
      .project
  4. 2 0
      image/.gitignore
  5. BIN
      image/background.png
  6. BIN
      image/bg/a_side.png
  7. BIN
      image/bg/b_side.png
  8. BIN
      image/bg/bar_add.png
  9. BIN
      image/bg/bar_bookmark.png
  10. BIN
      image/bg/bar_cmd_go.png
  11. BIN
      image/bg/bar_edit.png
  12. BIN
      image/bg/bar_pause.png
  13. BIN
      image/bg/bar_sampling.png
  14. BIN
      image/bg/bar_setting.png
  15. BIN
      image/bg/bar_stop.png
  16. BIN
      image/bg/close_bt.png
  17. BIN
      image/bg/company_setting_logo.png
  18. BIN
      image/bg/delete.png
  19. BIN
      image/bg/department_setting_logo.png
  20. BIN
      image/bg/download.png
  21. BIN
      image/bg/equipment_setting_logo.png
  22. BIN
      image/bg/gj_001_1_20.png
  23. BIN
      image/bg/gj_001_21_40.png
  24. BIN
      image/bg/gj_001_41_60.png
  25. BIN
      image/bg/green_dot.png
  26. BIN
      image/bg/grey_dot.png
  27. BIN
      image/bg/icon.png
  28. BIN
      image/bg/inductance_logo.png
  29. BIN
      image/bg/insulation_logo.png
  30. BIN
      image/bg/login.png
  31. BIN
      image/bg/logo.png
  32. BIN
      image/bg/logoff.png
  33. BIN
      image/bg/menu_data_analysis.png
  34. BIN
      image/bg/menu_data_preprocess.png
  35. BIN
      image/bg/menu_file.png
  36. BIN
      image/bg/menu_setting.png
  37. BIN
      image/bg/min_bt.png
  38. BIN
      image/bg/ng_bg.png
  39. BIN
      image/bg/ok_bg.png
  40. BIN
      image/bg/open_file.png
  41. BIN
      image/bg/refresh.png
  42. BIN
      image/bg/reset_logo.png
  43. BIN
      image/bg/resistance_logo.png
  44. BIN
      image/bg/save_bg.png
  45. BIN
      image/bg/scan_barcode.png
  46. BIN
      image/bg/tree_folder_icon.png
  47. BIN
      image/bg/tree_leaf_icon.png
  48. BIN
      image/bg/user.png
  49. BIN
      image/bg/vertical_line.png
  50. BIN
      image/bg/zoom_in.png
  51. BIN
      image/bg/zoom_out.png
  52. BIN
      image/close_bt.png
  53. 0 0
      image/cmd.txt
  54. BIN
      image/icon.ico
  55. BIN
      image/info_bt.png
  56. BIN
      image/min_bt.png
  57. BIN
      lib/commons-codec-1.15.jar
  58. BIN
      lib/fastjson2-2.0.16.jar
  59. BIN
      lib/gson-2.10.jar
  60. BIN
      lib/iot-communication-1.4.4.jar
  61. BIN
      lib/jSerialComm-2.6.2.jar
  62. BIN
      lib/jfreechart-1.5.4.jar
  63. BIN
      lib/jshortcut-0.4-oberzalek.jar
  64. BIN
      lib/kotlin-stdlib-1.8.10.jar
  65. BIN
      lib/logback-classic-1.2.13.jar
  66. BIN
      lib/logback-core-1.2.13.jar
  67. BIN
      lib/netty-all-4.1.48.Final.jar
  68. BIN
      lib/okhttp-4.10.0.jar
  69. BIN
      lib/okio-jvm-3.3.0.jar
  70. BIN
      lib/s7connector-2.1.jar
  71. BIN
      lib/slf4j-api-1.7.36.jar
  72. BIN
      lib/sqlite-jdbc-3.36.0.3.jar
  73. 10 0
      src/com/mes/component/CallFromJs.java
  74. 57 0
      src/com/mes/component/MesRadio.java
  75. 44 0
      src/com/mes/component/MesWebView.java
  76. 30 0
      src/com/mes/component/MyDialog.java
  77. 155 0
      src/com/mes/device/DeviceStateReporter.java
  78. 158 0
      src/com/mes/ui/BindMaterialPanel.java
  79. 58 0
      src/com/mes/ui/BindMaterialResp.java
  80. 58 0
      src/com/mes/ui/CmtReq.java
  81. 374 0
      src/com/mes/ui/DataUtil.java
  82. 85 0
      src/com/mes/ui/LockUtil.java
  83. 225 0
      src/com/mes/ui/LoginFarme.java
  84. 1576 0
      src/com/mes/ui/MesClient.java
  85. 92 0
      src/com/mes/ui/OprnoUtil.java
  86. 21 0
      src/com/mes/ui/PlcReadTest.java
  87. 407 0
      src/com/mes/ui/PlcReadTestFrame.java
  88. 755 0
      src/com/mes/ui/PlcUtil.java
  89. 14 0
      src/com/mes/ui/TableCellRendererButton.java
  90. 61 0
      src/com/mes/ui/WorkRecordData.java
  91. 403 0
      src/com/mes/ui/WorkRecordPanel.java
  92. 63 0
      src/com/mes/ui/WorkRecordResp.java
  93. 14 0
      src/com/mes/util/Base64Utils.java
  94. 32 0
      src/com/mes/util/DateLocalUtils.java
  95. 53 0
      src/com/mes/util/ErrorMsg.java
  96. 157 0
      src/com/mes/util/HttpUtils.java
  97. 216 0
      src/com/mes/util/JdbcUtils.java
  98. 13 0
      src/resources/config/config.properties
  99. 44 0
      src/resources/logback.xml

+ 25 - 0
.classpath

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+    <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
+		<attributes>
+			<attribute name="module" value="true"/>
+		</attributes>
+	</classpathentry>
+    <classpathentry kind="src" path="image"/>
+	<classpathentry excluding="resources/" kind="src" path="src"/>
+	<classpathentry kind="src" path="src/resources"/>
+	<classpathentry kind="output" path="bin"/>
+	<classpathentry kind="lib" path="lib/s7connector-2.1.jar"/>
+	<classpathentry kind="lib" path="lib/slf4j-api-2.0.6.jar"/>
+	<classpathentry kind="lib" path="lib/okhttp-4.10.0.jar"/>
+	<classpathentry kind="lib" path="lib/kotlin-stdlib-1.8.10.jar"/>
+	<classpathentry kind="lib" path="lib/okio-jvm-3.3.0.jar"/>
+	<classpathentry kind="lib" path="lib/gson-2.10.jar"/>
+	<classpathentry kind="lib" path="lib/jfreechart-1.5.4.jar"/>
+	<classpathentry kind="lib" path="lib/jshortcut-0.4-oberzalek.jar"/>
+	<classpathentry kind="lib" path="lib/netty-all-4.1.48.Final.jar"/>
+	<classpathentry kind="lib" path="lib/sqlite-jdbc-3.36.0.3.jar"/>
+	<classpathentry kind="lib" path="lib/commons-codec-1.15.jar"/>
+	<classpathentry kind="lib" path="lib/fastjson2-2.0.16.jar"/>
+	<classpathentry kind="lib" path="lib/iot-communication-1.4.4.jar"/>
+</classpath>

+ 8 - 0
.gitignore

@@ -0,0 +1,8 @@
+.idea
+.settings
+classes
+bin
+out
+*.db
+*.iml
+.lock

+ 28 - 0
.project

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>mesclient-sd</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+	<filteredResources>
+		<filter>
+			<id>1783319195628</id>
+			<name></name>
+			<type>30</type>
+			<matcher>
+				<id>org.eclipse.core.resources.regexFilterMatcher</id>
+				<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
+			</matcher>
+		</filter>
+	</filteredResources>
+</projectDescription>

+ 2 - 0
image/.gitignore

@@ -0,0 +1,2 @@
+META-INF
+_system~.ini

BIN
image/background.png


BIN
image/bg/a_side.png


BIN
image/bg/b_side.png


BIN
image/bg/bar_add.png


BIN
image/bg/bar_bookmark.png


BIN
image/bg/bar_cmd_go.png


BIN
image/bg/bar_edit.png


BIN
image/bg/bar_pause.png


BIN
image/bg/bar_sampling.png


BIN
image/bg/bar_setting.png


BIN
image/bg/bar_stop.png


BIN
image/bg/close_bt.png


BIN
image/bg/company_setting_logo.png


BIN
image/bg/delete.png


BIN
image/bg/department_setting_logo.png


BIN
image/bg/download.png


BIN
image/bg/equipment_setting_logo.png


BIN
image/bg/gj_001_1_20.png


BIN
image/bg/gj_001_21_40.png


BIN
image/bg/gj_001_41_60.png


BIN
image/bg/green_dot.png


BIN
image/bg/grey_dot.png


BIN
image/bg/icon.png


BIN
image/bg/inductance_logo.png


BIN
image/bg/insulation_logo.png


BIN
image/bg/login.png


BIN
image/bg/logo.png


BIN
image/bg/logoff.png


BIN
image/bg/menu_data_analysis.png


BIN
image/bg/menu_data_preprocess.png


BIN
image/bg/menu_file.png


BIN
image/bg/menu_setting.png


BIN
image/bg/min_bt.png


BIN
image/bg/ng_bg.png


BIN
image/bg/ok_bg.png


BIN
image/bg/open_file.png


BIN
image/bg/refresh.png


BIN
image/bg/reset_logo.png


BIN
image/bg/resistance_logo.png


BIN
image/bg/save_bg.png


BIN
image/bg/scan_barcode.png


BIN
image/bg/tree_folder_icon.png


BIN
image/bg/tree_leaf_icon.png


BIN
image/bg/user.png


BIN
image/bg/vertical_line.png


BIN
image/bg/zoom_in.png


BIN
image/bg/zoom_out.png


BIN
image/close_bt.png


+ 0 - 0
image/cmd.txt


BIN
image/icon.ico


BIN
image/info_bt.png


BIN
image/min_bt.png


BIN
lib/commons-codec-1.15.jar


BIN
lib/fastjson2-2.0.16.jar


BIN
lib/gson-2.10.jar


BIN
lib/iot-communication-1.4.4.jar


BIN
lib/jSerialComm-2.6.2.jar


BIN
lib/jfreechart-1.5.4.jar


BIN
lib/jshortcut-0.4-oberzalek.jar


BIN
lib/kotlin-stdlib-1.8.10.jar


BIN
lib/logback-classic-1.2.13.jar


BIN
lib/logback-core-1.2.13.jar


BIN
lib/netty-all-4.1.48.Final.jar


BIN
lib/okhttp-4.10.0.jar


BIN
lib/okio-jvm-3.3.0.jar


BIN
lib/s7connector-2.1.jar


BIN
lib/slf4j-api-1.7.36.jar


BIN
lib/sqlite-jdbc-3.36.0.3.jar


+ 10 - 0
src/com/mes/component/CallFromJs.java

@@ -0,0 +1,10 @@
+package com.mes.component;
+
+import com.mes.ui.MesClient;
+
+public class CallFromJs {
+    public String callFromJs(String info){
+        System.out.print("ss:"+info);
+        return MesClient.sessionid+":java:" + info;
+    }
+}

+ 57 - 0
src/com/mes/component/MesRadio.java

@@ -0,0 +1,57 @@
+package com.mes.component;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+public class MesRadio extends JPanel {
+
+    private String result = "";
+    private Integer i = 0;
+
+    public MesRadio(String[] radioTitles,String[] radioVals){
+        this.setLayout(new FlowLayout());
+        ButtonGroup group = new ButtonGroup();
+        i = 0;
+        for (String title:radioTitles){
+            JRadioButton radioButton = new JRadioButton(title);
+            radioButton.setFont(new Font("微软雅黑", Font.PLAIN, 28));
+
+            String val = radioVals[i];
+            radioButton.setName(val);
+
+            this.add(radioButton);
+            group.add(radioButton);
+
+            radioButton.addActionListener(new ActionListener() {
+                @Override
+                public void actionPerformed(ActionEvent e) {
+                    result = val;
+                    System.out.println("选择:"+result);
+                }
+            });
+            i++;
+        }
+    }
+
+    public void setResult(String ret){
+        this.result = "";
+        Component[] components = this.getComponents();
+        for (Component jRadioButton:components){
+            if(jRadioButton instanceof JRadioButton){
+                JRadioButton jRadioButtonParam = (JRadioButton)jRadioButton;
+                if(ret.equals(jRadioButtonParam.getName())){
+                    jRadioButtonParam.setSelected(true);
+                    this.result = jRadioButtonParam.getName();
+                }else{
+                    jRadioButtonParam.setSelected(false);
+                }
+            }
+        }
+    }
+
+    public String getResult(){
+        return this.result;
+    }
+}

+ 44 - 0
src/com/mes/component/MesWebView.java

@@ -0,0 +1,44 @@
+package com.mes.component;
+
+import javafx.application.Platform;
+import javafx.beans.value.ObservableValue;
+import javafx.concurrent.Worker;
+import javafx.embed.swing.JFXPanel;
+import javafx.scene.Scene;
+import javafx.scene.web.WebEngine;
+import javafx.scene.web.WebView;
+import netscape.javascript.JSObject;
+
+import java.util.Date;
+
+public class MesWebView extends JFXPanel {
+
+    public static WebEngine engine = null;
+
+    public MesWebView(String url){
+
+        Platform.runLater(() -> {
+            WebView webView = new WebView();
+            setScene(new Scene(webView));
+            Date date = new Date();
+//            String url = "http://"+mes_server_ip+":8980/js/a/mes/mesQuality/check?__sid="+sessionid+"&oprno="+mes_gw+"&t="+date.getTime();
+            System.out.print(url);
+            engine = webView.getEngine();
+            CallFromJs bridge = new CallFromJs();
+            engine.getLoadWorker().stateProperty().addListener(
+                    (ObservableValue<? extends Worker.State> ov, Worker.State oldState, Worker.State newState) -> {
+                        if (newState == Worker.State.SUCCEEDED) {
+                            JSObject window = (JSObject) engine.executeScript("window");
+                            window.setMember("apps", bridge);
+                            window.setMember("sout", System.out);
+                        }
+                    }
+            );
+            engine.load(url);
+        });
+    }
+
+    public void reloadWeb(){
+        engine.reload();
+    }
+}

+ 30 - 0
src/com/mes/component/MyDialog.java

@@ -0,0 +1,30 @@
+package com.mes.component;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class MyDialog extends JDialog {
+
+    public MyDialog(Frame owner, String title, String msg){
+        super(owner, title);
+        init(msg);
+    }
+
+    private void init(String msg){
+        Container container = this.getContentPane();
+        container.setLayout(null);
+        container.setBackground(Color.BLACK);
+        this.setSize(800,220);
+        this.setLocationRelativeTo(null);
+
+        JLabel tslabel = new JLabel(msg);
+        tslabel.setHorizontalAlignment(SwingConstants.CENTER);
+        tslabel.setForeground(Color.RED);
+        tslabel.setFont(new Font("微软雅黑", Font.PLAIN,48));
+        tslabel.setBounds(0,30,800,100);
+
+        container.add(tslabel);
+        this.setModal(true);
+        this.setVisible(true);
+    }
+}

+ 155 - 0
src/com/mes/device/DeviceStateReporter.java

@@ -0,0 +1,155 @@
+package com.mes.device;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.mes.util.HttpUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Properties;
+
+/**
+ * 工位客户端设备状态上报(按 gw + lineSn)
+ */
+public class DeviceStateReporter {
+
+    public static final Logger log = LoggerFactory.getLogger(DeviceStateReporter.class);
+
+    /** 正常运行 */
+    public static final String STATE_RUNNING = "1";
+    /** 工位退出/收班 */
+    public static final String STATE_STOP = "7";
+
+    private static DeviceStateReporter instance;
+
+    private String serverIp;
+    private String gw;
+    private String gw2;
+    private String lineSn;
+
+    private DeviceStateReporter() {
+    }
+
+    public static DeviceStateReporter getInstance() {
+        if (instance == null) {
+            synchronized (DeviceStateReporter.class) {
+                if (instance == null) {
+                    instance = new DeviceStateReporter();
+                }
+            }
+        }
+        return instance;
+    }
+
+    /**
+     * 从配置文件初始化
+     */
+    public void initFromConfig() {
+        try {
+            InputStream is = ClassLoader.getSystemResourceAsStream("config/config.properties");
+            Properties pro = new Properties();
+            BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
+            pro.load(br);
+            br.close();
+            is.close();
+            init(
+                    pro.getProperty("mes.server_ip", "").trim(),
+                    pro.getProperty("mes.gw", "").trim(),
+                    pro.getProperty("mes.line_sn", "").trim()
+            );
+        } catch (Exception e) {
+            log.error("初始化设备状态上报失败", e);
+        }
+    }
+
+    public void init(String serverIp, String gw, String lineSn) {
+        this.serverIp = serverIp;
+        this.gw = gw;
+        this.gw2 = gw;
+        this.lineSn = lineSn;
+        log.info("设备状态上报初始化: gw={}, lineSn={}", gw, lineSn);
+    }
+
+    /**
+     * 双工位初始化
+     */
+    public void initDual(String serverIp, String gw1, String gw2, String lineSn) {
+        this.serverIp = serverIp;
+        this.gw = gw1;
+        this.gw2 = gw2;
+        this.lineSn = lineSn;
+        log.info("设备状态上报初始化: gw1={}, gw2={}, lineSn={}", gw1, gw2, lineSn);
+    }
+
+    /**
+     * 异步上报(单工位)
+     */
+    public void reportAsync(final String state, final String content) {
+        reportBothAsync(state, content);
+    }
+
+    /**
+     * 异步上报(双工位)
+     */
+    public void reportBothAsync(final String state, final String content) {
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                reportBothSync(state, content);
+            }
+        }, "device-state-report-thread").start();
+    }
+
+    /**
+     * 同步上报(单工位,兼容旧调用)
+     */
+    public boolean reportSync(String state, String content) {
+        return reportBothSync(state, content);
+    }
+
+    /**
+     * 同步上报(双工位各报一条)
+     */
+    public boolean reportBothSync(String state, String content) {
+        boolean ok1 = reportSyncForGw(gw, state, content);
+        if (isBlank(gw2) || gw2.equals(gw)) {
+            return ok1;
+        }
+        boolean ok2 = reportSyncForGw(gw2, state, content);
+        return ok1 && ok2;
+    }
+
+    /**
+     * 同步上报指定工位
+     */
+    public boolean reportSyncForGw(String targetGw, String state, String content) {
+        if (isBlank(serverIp) || isBlank(targetGw) || isBlank(lineSn)) {
+            log.warn("设备状态上报跳过:配置不完整 serverIp={}, gw={}, lineSn={}", serverIp, targetGw, lineSn);
+            return false;
+        }
+        try {
+            String url = "http://" + serverIp + ":8980/js/a/mes/mesDevice/reportState";
+            JSONObject body = new JSONObject();
+            body.put("gw", targetGw);
+            body.put("lineSn", lineSn);
+            body.put("state", state);
+            body.put("content", content == null ? "" : content);
+            String result = HttpUtils.sendPostRequestJson(url, body.toJSONString());
+            log.info("设备状态上报: gw={}, state={}, content={}, result={}", targetGw, state, content, result);
+            if (isBlank(result) || "false".equalsIgnoreCase(result)) {
+                return false;
+            }
+            JSONObject retObj = JSONObject.parseObject(result);
+            return retObj != null && "true".equalsIgnoreCase(String.valueOf(retObj.get("result")));
+        } catch (Exception e) {
+            log.error("设备状态上报异常: gw={}, state={}, content={}", targetGw, state, content, e);
+            return false;
+        }
+    }
+
+    private boolean isBlank(String value) {
+        return value == null || value.trim().isEmpty();
+    }
+}

+ 158 - 0
src/com/mes/ui/BindMaterialPanel.java

@@ -0,0 +1,158 @@
+package com.mes.ui;
+
+import com.alibaba.fastjson2.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.*;
+import javax.swing.table.DefaultTableModel;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.List;
+
+/**
+ * 绑定物料面板(按工位)
+ */
+public class BindMaterialPanel extends JPanel {
+
+    private static final Logger log = LoggerFactory.getLogger(BindMaterialPanel.class);
+
+    private static final String[] COLUMN_NAMES = {"物料名称", "绑定批次", "剩余次数", "操作"};
+
+    private final String oprno;
+    private JTable table;
+    private DefaultTableModel tableModel;
+    private Object[][] rowData;
+
+    public BindMaterialPanel(String oprno) {
+        this.oprno = oprno;
+        initUI();
+        refreshData();
+    }
+
+    private void initUI() {
+        setLayout(new BorderLayout());
+        tableModel = new DefaultTableModel(COLUMN_NAMES, 0) {
+            @Override
+            public boolean isCellEditable(int row, int column) {
+                return column == 3;
+            }
+        };
+        table = new JTable(tableModel);
+        table.setRowHeight(40);
+        table.setFont(new Font("微软雅黑", Font.PLAIN, 14));
+        table.getColumnModel().getColumn(3).setCellRenderer(new TableCellRendererButton());
+        table.getColumnModel().getColumn(3).setCellEditor(new BindMaterialCellEditor(this));
+        add(new JScrollPane(table), BorderLayout.CENTER);
+    }
+
+    /**
+     * 刷新物料列表
+     */
+    public void refreshData() {
+        try {
+            JSONObject retObj = DataUtil.getBindMaterail(oprno);
+            tableModel.setRowCount(0);
+            if (retObj == null || retObj.get("result") == null
+                    || !retObj.get("result").toString().equalsIgnoreCase("true")) {
+                return;
+            }
+            List<BindMaterialResp> arrs = retObj.getList("data", BindMaterialResp.class);
+            if (arrs == null) {
+                return;
+            }
+            rowData = new Object[arrs.size()][7];
+            int i = 0;
+            for (BindMaterialResp item : arrs) {
+                rowData[i][0] = item.getMaterialTitle();
+                rowData[i][1] = item.getBatchSn();
+                rowData[i][2] = item.getLastTimes();
+                rowData[i][3] = "";
+                rowData[i][4] = item.getCraft();
+                rowData[i][5] = item.getMaterialId();
+                rowData[i][6] = item.getType();
+                tableModel.addRow(new Object[]{
+                        rowData[i][0], rowData[i][1], rowData[i][2], rowData[i][3]
+                });
+                i++;
+            }
+            table.repaint();
+        } catch (Exception e) {
+            log.error("加载工位 {} 物料失败", oprno, e);
+        }
+    }
+
+    /**
+     * 扫码绑定物料
+     */
+    public void scanBatchSn(int selectedRow) {
+        if (rowData == null || selectedRow < 0 || selectedRow >= rowData.length) {
+            return;
+        }
+        BindMaterialResp bindMaterialResp = new BindMaterialResp();
+        bindMaterialResp.setMaterialTitle(rowData[selectedRow][0] + "");
+        bindMaterialResp.setBatchSn(rowData[selectedRow][1] + "");
+        bindMaterialResp.setLastTimes(rowData[selectedRow][2] + "");
+        bindMaterialResp.setCraft(rowData[selectedRow][4] + "");
+        bindMaterialResp.setMaterialId(rowData[selectedRow][5] + "");
+        bindMaterialResp.setType(rowData[selectedRow][6] + "");
+
+        String scanBarcodeTitle = "请扫物料:" + bindMaterialResp.getMaterialTitle() + "(" + oprno + ")";
+        String scanBarcode = JOptionPane.showInputDialog(null, scanBarcodeTitle);
+        if (scanBarcode == null || scanBarcode.equalsIgnoreCase("")) {
+            return;
+        }
+
+        JSONObject retObj = DataUtil.saveBindMaterail(
+                scanBarcode,
+                bindMaterialResp.getCraft(),
+                bindMaterialResp.getMaterialId(),
+                bindMaterialResp.getType(),
+                oprno
+        );
+        if (retObj != null && retObj.get("result") != null
+                && retObj.get("result").toString().equalsIgnoreCase("true")) {
+            MesClient.setMenuStatus("扫物料:" + bindMaterialResp.getMaterialTitle() + "成功", 0);
+            refreshData();
+        } else if (retObj == null || retObj.get("result") == null) {
+            MesClient.setMenuStatus("请求失败,请重试", -1);
+        } else if (retObj.get("result").toString().equalsIgnoreCase("false")) {
+            MesClient.setMenuStatus(retObj.getString("message"), -1);
+        }
+    }
+
+    public String getOprno() {
+        return oprno;
+    }
+
+    /**
+     * 物料扫码按钮编辑器
+     */
+    private static class BindMaterialCellEditor extends DefaultCellEditor {
+        private final BindMaterialPanel panel;
+        private final JButton btn;
+
+        BindMaterialCellEditor(BindMaterialPanel panel) {
+            super(new JTextField());
+            this.panel = panel;
+            setClickCountToStart(1);
+            btn = new JButton("扫码");
+            btn.addActionListener(new ActionListener() {
+                @Override
+                public void actionPerformed(ActionEvent e) {
+                    int selectedRow = panel.table.getSelectedRow();
+                    panel.scanBatchSn(selectedRow);
+                    fireEditingStopped();
+                }
+            });
+        }
+
+        @Override
+        public Component getTableCellEditorComponent(JTable table, Object value,
+                                                     boolean isSelected, int row, int column) {
+            panel.table.setRowSelectionInterval(row, row);
+            return btn;
+        }
+    }
+}

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

@@ -0,0 +1,58 @@
+package com.mes.ui;
+
+public class BindMaterialResp {
+    private String materialId;
+    private String materialTitle;
+    private String craft;
+    private String batchSn;
+    private String lastTimes;
+    private String type;
+
+    public String getMaterialId() {
+        return materialId;
+    }
+
+    public void setMaterialId(String materialId) {
+        this.materialId = materialId;
+    }
+
+    public String getMaterialTitle() {
+        return materialTitle;
+    }
+
+    public void setMaterialTitle(String materialTitle) {
+        this.materialTitle = materialTitle;
+    }
+
+    public String getCraft() {
+        return craft;
+    }
+
+    public void setCraft(String craft) {
+        this.craft = craft;
+    }
+
+    public String getBatchSn() {
+        return batchSn;
+    }
+
+    public void setBatchSn(String batchSn) {
+        this.batchSn = batchSn;
+    }
+
+    public String getLastTimes() {
+        return lastTimes;
+    }
+
+    public void setLastTimes(String lastTimes) {
+        this.lastTimes = lastTimes;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+}

+ 58 - 0
src/com/mes/ui/CmtReq.java

@@ -0,0 +1,58 @@
+package com.mes.ui;
+
+public class CmtReq {
+    private Integer id;
+    private String oprno;
+    private String lineSn;
+    private String sn;
+    private String content;
+    private Integer sync;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public String getOprno() {
+        return oprno;
+    }
+
+    public void setOprno(String oprno) {
+        this.oprno = oprno;
+    }
+
+    public String getLineSn() {
+        return lineSn;
+    }
+
+    public void setLineSn(String lineSn) {
+        this.lineSn = lineSn;
+    }
+
+    public String getSn() {
+        return sn;
+    }
+
+    public void setSn(String sn) {
+        this.sn = sn;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    public Integer getSync() {
+        return sync;
+    }
+
+    public void setSync(Integer sync) {
+        this.sync = sync;
+    }
+}

+ 374 - 0
src/com/mes/ui/DataUtil.java

@@ -0,0 +1,374 @@
+package com.mes.ui;
+
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.mes.util.JdbcUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+
+public class DataUtil {
+    public static final Logger log = LoggerFactory.getLogger(DataUtil.class);
+
+    //删除当前工位工件码
+    public static JSONObject delCurSn(String oprno,String serverIp,String lineSn){
+        try {
+            String url = "http://"+serverIp+":8980/js/a/mes/mesProductRecord/cancelCurSn";
+            String params = "__ajax=json&oprno="+oprno+"&lineSn="+lineSn+"&__sid="+MesClient.sessionid;
+            JSONObject result = JSONObject.parseObject(doPost(url, params));
+//            log.info("请求参数:"+params);
+            return result;
+        } catch (Exception e) {
+            log.info("e="+e.getMessage());
+            return null;
+        }
+    }
+
+    //获取当前工位工件码
+    public static JSONObject getCurSn(String oprno,String serverIp,String lineSn){
+        try {
+            String url = "http://"+serverIp+":8980/js/a/mes/mesProductRecord/getCurSn";
+            String params = "__ajax=json&oprno="+oprno+"&lineSn="+lineSn+"&__sid="+MesClient.sessionid;
+            JSONObject result = JSONObject.parseObject(doPost(url, params));
+//            log.info("请求参数:"+params);
+            return result;
+        } catch (Exception e) {
+            log.info("e="+e.getMessage());
+            return null;
+        }
+    }
+
+    public static JSONObject checkQuality(String sn, String user){
+        return checkQuality(sn, user, MesClient.mes_gw, MesClient.mes_line_sn, MesClient.mes_server_ip);
+    }
+
+    public static JSONObject checkQuality(String sn, String user, String oprno, String lineSn, String serverIp){
+        try{
+            String url = "http://"+serverIp+":8980/js/a/mes/mesProductRecord/pccheck";
+            String params = "__ajax=json&oprno="+oprno+"&lineSn="+lineSn+"&sn="+sn+"&ucode="+user+"&__sid="+MesClient.sessionid;
+            log.info("params="+params);
+            String result = doPost(url,params);
+            log.info("result="+result);
+            if(result == null || result.equalsIgnoreCase("false")) {
+                return null;
+            }else {
+                JdbcUtils.insertData(oprno, "100000", params, "AQDW", sn);
+                return JSONObject.parseObject(result);
+            }
+        }catch (Exception e){
+            log.info("e="+e.getMessage());
+            return null;
+        }
+    }
+
+    public static JSONObject sendQuality(String sn,String ret,String user){
+        return sendQualityPc(sn, ret, user, MesClient.mes_gw, MesClient.mes_line_sn, MesClient.mes_server_ip);
+    }
+
+    public static JSONObject sendQualityPc(String sn, String ret, String user, String oprno, String lineSn, String serverIp){
+        try{
+            // 提交前确保操作员不为空
+            if (user == null || user.trim().isEmpty()) {
+                MesClient.getUser();
+                user = MesClient.user20;
+            }
+            String url = "http://"+serverIp+":8980/js/a/mes/mesProductRecord/pcresult";
+            String params = "__ajax=json&oprno="+oprno+"&lineSn="+lineSn+"&sn="+sn+"&result="+ret+"&ucode="+user+"&__sid="+MesClient.sessionid;
+            log.info("sendQualityPc params="+params);
+            String result = doPost(url,params);
+            log.info("sendQualityPc result="+result);
+            if(result == null || result.trim().isEmpty() || result.equalsIgnoreCase("false")) {
+                return null;
+            }
+            JSONObject json = JSONObject.parseObject(result);
+            if (json == null) {
+                return null;
+            }
+            // 本地入库失败不影响判定 MES 是否已提交成功
+            try {
+                JdbcUtils.insertData(oprno, "100000", params, "MQDW", sn);
+            } catch (Exception ex) {
+                log.info("sendQualityPc本地入库异常(不影响结果)="+ex.getMessage());
+            }
+            return json;
+        }catch (Exception e){
+            log.info("sendQualityPc异常="+e.getMessage());
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /** 判断 MES 返回是否成功(兼容 result 为 true/"true") */
+    public static boolean isMesSuccess(JSONObject result) {
+        if (result == null) {
+            return false;
+        }
+        Object r = result.get("result");
+        if (r == null) {
+            return false;
+        }
+        return "true".equalsIgnoreCase(String.valueOf(r).trim());
+    }
+
+    public static JSONObject upParams(String upparams) {
+        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 url = "http://"+mes_server_ip+":8980/js/a/mes/mesProductCmt/batchsave";
+            // 与 OP040 一致:结果 sendQualityPc 与参数 batchsave 分开上传,params 原样提交
+            String params = "__ajax=json&params="+upparams;
+            log.info("params="+params);
+            String result = doPost(url,params);
+            log.info("result="+result);
+            if(result == null || result.equalsIgnoreCase("false")) {
+                return null;
+            }else {
+                return JSONObject.parseObject(result);
+            }
+        }catch (Exception e){
+            log.info("upParams异常="+e.getMessage());
+            return null;
+        }
+    }
+
+    public static String rightPad(final String str, final int size) {
+        if (str == null) {
+            return null;
+        }
+
+        String strret = str;
+        if(str.length() > size){
+            strret = str.substring(0,size);
+        }
+        return String.format("%-"+size+"s", strret);
+    }
+
+    public static JSONObject getBindMaterail() {
+        return getBindMaterail(MesClient.mes_gw);
+    }
+
+    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);
+            String mes_server_ip = pro.getProperty("mes.server_ip");
+            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;
+            System.out.println("params="+params);
+            String result = doPost(url,params);
+            System.out.println("result="+result);
+
+            if(result.equalsIgnoreCase("false")) {
+                return null;
+            }else {
+                return JSONObject.parseObject(result);
+            }
+        }catch (Exception e){
+            return null;
+        }
+    }
+
+    /**
+     * 工位是否配置了待绑定物料
+     */
+    public static boolean hasBindMaterial(String oprno) {
+        try {
+            JSONObject retObj = getBindMaterail(oprno);
+            if (retObj == null || retObj.get("result") == null
+                    || !retObj.get("result").toString().equalsIgnoreCase("true")) {
+                return false;
+            }
+            List<BindMaterialResp> arrs = retObj.getList("data", BindMaterialResp.class);
+            return arrs != null && !arrs.isEmpty();
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    public static JSONObject saveBindMaterail(String batchSn,String craft,String materialId,String type) {
+        return saveBindMaterail(batchSn, craft, materialId, type, MesClient.mes_gw);
+    }
+
+    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);
+            String mes_server_ip = pro.getProperty("mes.server_ip");
+            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;
+            System.out.println("params="+params);
+            String result = doPost(url,params);
+            System.out.println("result="+result);
+
+            if(result.equalsIgnoreCase("false")) {
+                return null;
+            }else {
+                return JSONObject.parseObject(result);
+            }
+        }catch (Exception e){
+            return null;
+        }
+    }
+
+    public static String doPost(String httpUrl, String param) {
+        HttpURLConnection connection = null;
+        InputStream is = null;
+        OutputStream os = null;
+        BufferedReader br = null;
+        String result = null;
+        try {
+            URL url = new URL(httpUrl);
+            connection = (HttpURLConnection) url.openConnection();
+            connection.setRequestMethod("POST");
+            connection.setConnectTimeout(15000);
+            connection.setReadTimeout(60000);
+            connection.setDoOutput(true);
+            connection.setDoInput(true);
+            // 必须显式 UTF-8:打包 exe 后 JVM 默认编码常为 GBK,getBytes() 会把中文参数打成乱码
+            connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
+            connection.setRequestProperty("Authorization", "Bearer da3efcbf-0845-4fe3-8aba-ee040be542c0");
+            os = connection.getOutputStream();
+            os.write(param.getBytes("UTF-8"));
+            if (connection.getResponseCode() == 200) {
+                is = connection.getInputStream();
+                br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
+                StringBuffer sbf = new StringBuffer();
+                String temp = null;
+                while ((temp = br.readLine()) != null) {
+                    sbf.append(temp);
+                    sbf.append("\r\n");
+                }
+                result = sbf.toString();
+            }
+        } catch (MalformedURLException e) {
+            e.printStackTrace();
+        } catch (IOException e) {
+            e.printStackTrace();
+        } finally {
+            if (null != br) {
+                try {
+                    br.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+            if (null != os) {
+                try {
+                    os.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+            if (null != is) {
+                try {
+                    is.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+            connection.disconnect();
+        }
+        return result;
+    }
+
+    /**
+     * 查询工作记录
+     * @param oprno 工位号(空则查所有工位)
+     * @param sn 工件码(可选,用于搜索)
+     * @param pageNo 页码
+     * @param pageSize 每页条数
+     * @return 工作记录响应
+     */
+    public static WorkRecordResp getWorkRecordList(String oprno, String sn, int pageNo, int pageSize) {
+        WorkRecordResp resp = new WorkRecordResp();
+        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 lineSn = pro.getProperty("mes.line_sn").trim();
+            
+            String url = "http://" + mes_server_ip + ":8980/js/a/mes/mesProductRecord/listData";
+            StringBuilder params = new StringBuilder();
+            params.append("__ajax=json");
+            params.append("&lineSn=").append(lineSn);
+            params.append("&pageNo=").append(pageNo);
+            params.append("&pageSize=").append(pageSize);
+            
+            if (oprno != null && !oprno.isEmpty()) {
+                params.append("&oprno=").append(oprno);
+            }
+            // 注意:不传auto参数,这样"所有工位"查询时会返回整条产线所有工位的数据
+            if (sn != null && !sn.isEmpty()) {
+                params.append("&sn=").append(sn);
+            }
+            
+            log.info("查询工作记录: url=" + url + ", params=" + params.toString());
+            String result = doPost(url, params.toString());
+            log.info("查询工作记录结果: result=" + result);
+            
+            if (result == null || result.trim().isEmpty()) {
+                resp.setResult(false);
+                resp.setMessage("请求返回空结果");
+                return resp;
+            }
+            
+            JSONObject jsonObj = JSONObject.parseObject(result);
+            if (jsonObj == null) {
+                resp.setResult(false);
+                resp.setMessage("解析响应失败");
+                return resp;
+            }
+            
+            resp.setResult(true);
+            resp.setPageNo(jsonObj.getIntValue("pageNo"));
+            resp.setPageSize(jsonObj.getIntValue("pageSize"));
+            resp.setCount(jsonObj.getLongValue("count"));
+            
+            List<WorkRecordData> list = new ArrayList<>();
+            JSONArray listArray = jsonObj.getJSONArray("list");
+            if (listArray != null) {
+                for (int i = 0; i < listArray.size(); i++) {
+                    JSONObject item = listArray.getJSONObject(i);
+                    WorkRecordData data = new WorkRecordData();
+                    data.setId(item.getString("id"));
+                    data.setSn(item.getString("sn"));
+                    data.setOprno(item.getString("oprno"));
+                    data.setUpdateBy(item.getString("updateBy"));
+                    data.setUpdateDate(item.getString("updateDate"));
+                    data.setContent(item.getString("content"));
+                    list.add(data);
+                }
+            }
+            resp.setList(list);
+            
+        } catch (Exception e) {
+            log.error("查询工作记录异常: " + e.getMessage());
+            e.printStackTrace();
+            resp.setResult(false);
+            resp.setMessage("查询异常: " + e.getMessage());
+        }
+        return resp;
+    }
+}

+ 85 - 0
src/com/mes/ui/LockUtil.java

@@ -0,0 +1,85 @@
+package com.mes.ui;
+
+import java.io.File;
+import java.io.RandomAccessFile;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.nio.channels.OverlappingFileLockException;
+
+public class LockUtil {
+    private FileChannel channel;
+
+    private FileLock lock;
+
+    private File file;
+
+    private static LockUtil instance = null;
+
+    /**
+     * 获取唯一进程限制的实例
+     * @Title: getInstance
+     * @return  唯一进程实例
+     */
+    public static LockUtil getInstance(){
+        if(instance == null){
+            instance = new LockUtil();
+        }
+        return instance;
+    }
+
+    /**
+     * 进程是否正在运行
+     * @Title: isAppActive
+     * @return  运行与否
+     */
+    public boolean isAppActive() {
+        File file = new File(".lock");
+        try {
+            channel = new RandomAccessFile(file, "rw").getChannel();
+            try {
+                lock = channel.tryLock();
+
+            } catch (OverlappingFileLockException e) {
+                closeLock();
+                return true;
+            }
+            if (lock == null) {
+                closeLock();
+                return true;
+            }
+            Runtime.getRuntime().addShutdownHook(new Thread() {
+                public void run() {
+                    closeLock();
+                    deleteFile();
+                }
+            });
+            return false;
+        } catch (Exception e) {
+            closeLock();
+            return true;
+        }
+    }
+
+    protected void deleteFile() {
+        try {
+            file.delete();
+        } catch (Exception e) {
+
+        }
+
+    }
+
+    private void closeLock() {
+        try {
+            lock.release();
+        } catch (Exception e) {
+
+        }
+
+        try {
+            channel.close();
+        } catch (Exception e) {
+
+        }
+    }
+}

+ 225 - 0
src/com/mes/ui/LoginFarme.java

@@ -0,0 +1,225 @@
+package com.mes.ui;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.mes.util.Base64Utils;
+import com.mes.util.HttpUtils;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.time.LocalDateTime;
+
+public class LoginFarme extends JFrame {
+    //登录模块组件
+    static JLabel userNameLabel= new JLabel("<html><body>用户名:</body></html>",JLabel.LEFT);//用户名
+    static JLabel userPasswordLabel= new JLabel("<html><body>密码:</body></html>",JLabel.LEFT);//用户名
+    public static JTextField userNameTxt;
+    public static JPasswordField userPasswordTxt;
+    static JButton loginButton = new JButton("用户密码登录");
+    static JButton scanLoginButton = new JButton("扫  码  登  录");
+
+    public LoginFarme(){
+        String titleGw = MesClient.isDualStation()
+                ? MesClient.mes_gw1 + " / " + MesClient.mes_gw2
+                : MesClient.mes_gw;
+        setTitle("MES系统客户端115:" + titleGw + " - " + MesClient.mes_gw_des);
+
+        ImageIcon bg = new ImageIcon(MesClient.class.getResource("/background.png"));
+        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+        JLabel imgLabel = new JLabel(bg);//将背景图放在标签里。
+        getLayeredPane().add(imgLabel, new Integer(Integer.MIN_VALUE));//注意这里是关键,将背景标签添加到jfram的LayeredPane面板里。
+        imgLabel.setBounds(0,0,bg.getIconWidth(), bg.getIconHeight());//设置背景标签的位置
+        Container contentPane=getContentPane();
+        contentPane.setLayout(null);//布局,很重要
+        JPanel welcomePanel = new JPanel();
+        //welcomePanel.setLayout(new GridLayout(3, 1));
+        welcomePanel.setLayout(null);
+        //welcomeLable.setBounds(10, 5, 700, 100);
+        welcomePanel.setBounds(30, 330, 890, 300);
+        welcomePanel.setOpaque(false);//背景透明
+        contentPane.add(welcomePanel);
+
+        //登录页面
+        userNameLabel.setBounds(300, 100, 120, 40);
+        userNameLabel.setFont(new java.awt.Font("Dialog",   1,   16));
+        userPasswordLabel.setBounds(300, 150, 120, 40);
+        userPasswordLabel.setFont(new java.awt.Font("Dialog",   1,   16));
+        userNameTxt = new JTextField(20);
+        userNameTxt.setText("");
+        userNameTxt.setBounds(400, 105, 150, 30);
+        userPasswordTxt = new JPasswordField(20);
+        userPasswordTxt.setBounds(400, 155, 150, 30);
+        userPasswordTxt.setText("");
+        loginButton.setFont(new java.awt.Font("Dialog",   1,   16));
+        loginButton.setBounds(300, 200, 255, 40);
+        loginButton.setIcon(new ImageIcon(MesClient.class.getResource("/bg/user.png")));
+
+//        scanLoginButton.setVisible(false);
+        scanLoginButton.setFont(new java.awt.Font("Dialog",   1,   16));
+        scanLoginButton.setBounds(300, 250, 255, 40);
+        scanLoginButton.setIcon(new ImageIcon(MesClient.class.getResource("/bg/scan_barcode.png")));
+
+        welcomePanel.add(userNameLabel);
+        welcomePanel.add(userPasswordLabel);
+        welcomePanel.add(userNameTxt);
+        welcomePanel.add(userPasswordTxt);
+        welcomePanel.add(loginButton);
+        welcomePanel.add(scanLoginButton);
+        loginButton.addActionListener(new ActionListener(){
+            public void actionPerformed(ActionEvent e) {
+                login();
+            }
+        });
+        scanLoginButton.addActionListener(new ActionListener(){
+            public void actionPerformed(ActionEvent e) {
+                scanLogin();
+            }
+        });
+
+        ((JPanel)contentPane).setOpaque(false); //注意这里,将内容面板设为透明。这样LayeredPane面板中的背景才能显示出来。
+        //welcomeWin.setSize(902,678);
+        setResizable(false);//禁止最大化
+        setIconImage(Toolkit.getDefaultToolkit().getImage(MesClient.class.getResource("/bg/logo.png")));
+
+        Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
+        int width = 902;
+        int height = 678;
+        setBounds((d.width-width)/2, (d.height-height)/2-(d.height-height)/4, width, height);
+        /*****正常模式******/
+        setVisible(true);
+    }
+
+    //登录
+    public static void login() {
+        String user_str = userNameTxt.getText().toString();
+        String password_str = userPasswordTxt.getText().toString();
+        if(user_str.equalsIgnoreCase("")||password_str.equalsIgnoreCase("")) {
+            JOptionPane.showMessageDialog(MesClient.mesClientFrame,"用户名或密码不能为空","提示窗口", JOptionPane.INFORMATION_MESSAGE);
+            return;
+        }
+        String username = Base64Utils.getBase64(user_str);
+        String password = Base64Utils.getBase64(password_str);
+        System.out.println("&username=" + username + "&password=" + password);
+        String url = "http://"+MesClient.mes_server_ip+":8980/js/a/login?__ajax=json&username="+username+"&password="+password+"&validCode=&__sid=";
+        String loginResult = HttpUtils.sendRequest(url);
+        if(loginResult.equalsIgnoreCase("false")) {
+            JOptionPane.showMessageDialog(MesClient.mesClientFrame,"登录异常,请检查网络或联系网络管理员","提示窗口", JOptionPane.INFORMATION_MESSAGE);
+            return;
+        }else {
+            JSONObject retObj = JSONObject.parseObject(loginResult);
+            if(retObj.get("result")!=null&&retObj.get("result").toString().equalsIgnoreCase("true")) {
+                //检查用户权限是否可登录界面
+                checkUserAuthority(retObj);
+            }else {
+                //ret = "msg save error";
+                //ret = false;
+                JOptionPane.showMessageDialog(MesClient.mesClientFrame,"登录失败,用户名或密码错误","提示窗口", JOptionPane.INFORMATION_MESSAGE);
+                return;
+            }
+        }
+    }
+    //扫码登录
+    public static void scanLogin() {
+        //userNameTxt.setText("");
+        //userPasswordTxt.setText("");
+        String scanContent = JOptionPane.showInputDialog(null, "请扫码工牌二维码");
+        System.out.println("scanContent="+scanContent);
+        if(scanContent!=null&&!scanContent.equalsIgnoreCase("")) {
+            String url = "http://"+MesClient.mes_server_ip+":8980/js/a/mes/mesLogin/login?__login=true&__ajax=json&username="+scanContent;
+            String loginResult = HttpUtils.sendRequest(url);
+            System.out.println("loginResult="+loginResult);
+            if(loginResult.equalsIgnoreCase("false")) {
+                JOptionPane.showMessageDialog(MesClient.mesClientFrame,"登录异常,请检查网络或联系网络管理员","提示窗口", JOptionPane.INFORMATION_MESSAGE);
+                return;
+            }else {
+                JSONObject retObj = JSONObject.parseObject(loginResult);
+                if(retObj.get("result")!=null&&retObj.get("result").toString().equalsIgnoreCase("true")) {
+                    //检查用户权限是否可登录界面
+                    checkUserAuthority(retObj);
+                }else {
+                    JOptionPane.showMessageDialog(MesClient.mesClientFrame,"登录失败,用户名或密码错误","提示窗口", JOptionPane.INFORMATION_MESSAGE);
+                    return;
+                }
+            }
+        }else {
+            JOptionPane.showMessageDialog(MesClient.mesClientFrame,"扫码内容错误","提示窗口", JOptionPane.INFORMATION_MESSAGE);
+            return;
+        }
+    }
+
+    //检查用户权限是否可登录界面
+    public static void checkUserAuthority(JSONObject retObj) {
+        //设置登录用户名
+        JSONObject userObj = JSONObject.parseObject(retObj.get("user").toString());
+        String user_id = userObj.getString("id").toString();
+
+        //获取sessionid,判断权限
+        MesClient.sessionid = retObj.get("sessionid").toString();
+        if(MesClient.sessionid!=null&&!MesClient.sessionid.equalsIgnoreCase("")) {
+            int auth1 = getUserAuthForStation(MesClient.mes_gw1);
+            if (auth1 == 0) {
+                JOptionPane.showMessageDialog(MesClient.mesClientFrame,
+                        "您无权登录工位:" + MesClient.mes_gw1, "提示窗口", JOptionPane.INFORMATION_MESSAGE);
+                return;
+            }
+            int auth2 = auth1;
+            if (MesClient.isDualStation()) {
+                auth2 = getUserAuthForStation(MesClient.mes_gw2);
+                if (auth2 == 0) {
+                    JOptionPane.showMessageDialog(MesClient.mesClientFrame,
+                            "您无权登录工位:" + MesClient.mes_gw2, "提示窗口", JOptionPane.INFORMATION_MESSAGE);
+                    return;
+                }
+            }
+            MesClient.mes_auth = Math.max(auth1, auth2);
+            if(MesClient.mes_auth==1||MesClient.mes_auth==2) {
+                // 获取等于所处时间-当前小时
+                LocalDateTime now = LocalDateTime.now();
+                MesClient.userLoginHours = now.getHour();
+                //启动timer心跳包(界面)
+                MesClient.startHeartBeatTimer();
+                // 上报设备运行状态并启动定时上报
+                MesClient.reportDeviceRunning();
+
+                //1操作工人,2管理员
+                //登录成功
+                MesClient.user_menu.setText(user_id);
+                MesClient.welcomeWin.setVisible(false);
+                MesClient.mesClientFrame.setVisible(true);
+                MesClient.initDualStationCheckTabs(user_id);
+                MesClient.initDualStationWorkRecordTabs();
+                MesClient.startGetCurSn();
+                MesClient.initWarehouseData();
+            }
+        }else {
+            JOptionPane.showMessageDialog(MesClient.mesClientFrame,"登录失败,用户名或密码错误","提示窗口", JOptionPane.INFORMATION_MESSAGE);
+            return;
+        }
+    }
+
+    /**
+     * 查询用户对指定工位的登录权限
+     */
+    private static int getUserAuthForStation(String oprno) {
+        try {
+            String urlAuthority = "http://" + MesClient.mes_server_ip
+                    + ":8980/js/a/mes/mesLineProcess/userAuth?__ajax=json&type=0&__sid="
+                    + MesClient.sessionid + "&oprno=" + oprno + "&lineSn=" + MesClient.mes_line_sn;
+            String authorityResult = HttpUtils.sendRequest(urlAuthority);
+            System.out.println("authorityResult(" + oprno + ")=" + authorityResult);
+            if (authorityResult == null || authorityResult.equalsIgnoreCase("false")) {
+                return 0;
+            }
+            JSONObject authorityObj = JSONObject.parseObject(authorityResult);
+            if (authorityObj.get("result") != null
+                    && authorityObj.get("result").toString().equalsIgnoreCase("true")) {
+                JSONObject authObjTmp = JSONObject.parseObject(authorityObj.get("data").toString());
+                return Integer.parseInt(authObjTmp.getString("auth").toString());
+            }
+        } catch (Exception e) {
+            System.out.println("getUserAuthForStation error: " + e.getMessage());
+        }
+        return 0;
+    }
+}

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1576 - 0
src/com/mes/ui/MesClient.java


+ 92 - 0
src/com/mes/ui/OprnoUtil.java

@@ -0,0 +1,92 @@
+package com.mes.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class OprnoUtil {
+    public static String[] xtoprnos = new String[]{
+            "OP010",
+            "OP030",
+            "OP050",
+            "OP060",
+            "OP070",
+            "OP080",
+            "OP090",
+            "OP100",
+            "OP110",
+            "OP120",
+            "OP130",
+            "OP140",
+            "OP150",
+            "OP160",
+            "OP190",
+            "OP210",
+            "OP220"
+    };
+
+    public static String[] xtoprnodes = new String[]{
+            "镭雕",
+            "凸焊",
+            "左右吊耳组件弧焊",
+            "左右吊耳组件点焊",
+            "电池壳分总成1点焊",
+            "电池壳分总成1点焊补焊",
+            "电池壳分总成2点焊",
+            "电池壳分总成2点焊补焊",
+            "电池壳分总成3点焊",
+            "电池壳分总成4点焊",
+            "电池壳分总成5点焊补焊",
+            "电池壳分总成6弧焊",
+            "电池壳分总成7弧焊",
+            "电池壳分总成8弧焊",
+            "气密检测",
+            "拉铆",
+            "总成气密检测"
+    };
+    public static String[] lboprnos = new String[]{
+            "OP010","OP040", "OP060", "OP070", "OP080",
+            "OP110", "OP130", "OP150", "OP180", "OP190",
+    };
+    public static String[] lboprnodes = new String[]{
+            "镭雕二维码","左右边框总成", "前后边框总成", "总成框架拼焊-A", "总成框架弧焊-B",
+            "气密检测", "拉铆框架正面", "液冷板装配拉铆", "总成气密检测", "在线检测"
+    };
+    public static String getGwDes(String lineSn,String oprno){
+        String des = "";
+        oprno = formatOprno(oprno);
+        if(lineSn.equals("HEVXT")){
+            int i = 0;
+            for (String gw:xtoprnos){
+                if(gw.equals(oprno)){
+                    des = xtoprnodes[i];
+                    break;
+                }
+                i++;
+            }
+        }else if(lineSn.equals("115XT")){
+            int i = 0;
+            for (String gw:lboprnos){
+                if(gw.equals(oprno)){
+                    des = lboprnodes[i];
+                    break;
+                }
+                i++;
+            }
+        }
+        return des;
+    }
+
+    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;
+            }
+        }
+
+        return  oprno;
+    }
+}

+ 21 - 0
src/com/mes/ui/PlcReadTest.java

@@ -0,0 +1,21 @@
+package com.mes.ui;
+
+import javax.swing.*;
+
+/**
+ * PLC 点位读取测试入口,启动可视化监控窗口。
+ */
+public class PlcReadTest {
+
+    public static void main(String[] args) {
+        SwingUtilities.invokeLater(() -> {
+            try {
+                UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+            } catch (Exception ignored) {
+            }
+            String plcIp = PlcReadTestFrame.loadPlcIp();
+            PlcReadTestFrame frame = new PlcReadTestFrame(plcIp);
+            frame.setVisible(true);
+        });
+    }
+}

+ 407 - 0
src/com/mes/ui/PlcReadTestFrame.java

@@ -0,0 +1,407 @@
+package com.mes.ui;
+
+import com.github.xingshuangs.iot.protocol.s7.enums.EPlcType;
+import com.github.xingshuangs.iot.protocol.s7.service.S7PLC;
+
+import javax.swing.*;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.TitledBorder;
+import javax.swing.table.DefaultTableCellRenderer;
+import javax.swing.table.DefaultTableModel;
+import java.awt.*;
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+import java.util.Timer;
+import java.util.TimerTask;
+
+/**
+ * PLC 点位实时监控测试窗口(DB108 全部点位)
+ */
+public class PlcReadTestFrame extends JFrame {
+
+    private static final String DEFAULT_PLC_IP = "192.168.1.1";
+    private static final int DB_NO = 108;
+    private static final long READ_INTERVAL_MS = 1000;
+    private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    private final S7PLC s7PLC;
+    private final String db;
+    private final List<PlcPointDef> pointDefs = buildPointDefs();
+
+    private JLabel lblPlcInfo;
+    private JLabel lblLastUpdate;
+    private JLabel lblReadStatus;
+    private JButton btnAllowA;
+    private JButton btnAllowB;
+    private DefaultTableModel tableModel;
+    private Timer readTimer;
+
+    public PlcReadTestFrame(String plcIp) {
+        this.s7PLC = new S7PLC(EPlcType.S1200, plcIp);
+        this.db = "DB" + DB_NO;
+
+        setTitle("PLC 点位监控测试 - OP070/OP080 弧焊 DB108");
+        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+        setBounds(100, 80, 980, 760);
+        setMinimumSize(new Dimension(860, 600));
+
+        initUI(plcIp);
+        startReadTimer();
+        addWindowListener(new java.awt.event.WindowAdapter() {
+            @Override
+            public void windowClosing(java.awt.event.WindowEvent e) {
+                stopReadTimer();
+            }
+        });
+    }
+
+    private void initUI(String plcIp) {
+        JPanel content = new JPanel(new BorderLayout(8, 8));
+        content.setBorder(new EmptyBorder(10, 10, 10, 10));
+        setContentPane(content);
+
+        content.add(buildTopPanel(plcIp), BorderLayout.NORTH);
+        content.add(buildTablePanel(), BorderLayout.CENTER);
+        content.add(buildBottomPanel(), BorderLayout.SOUTH);
+    }
+
+    private JPanel buildTopPanel(String plcIp) {
+        JPanel panel = new JPanel(new BorderLayout(8, 4));
+        panel.setBorder(new TitledBorder("连接信息"));
+
+        lblPlcInfo = new JLabel("PLC IP: " + plcIp + "    数据块: " + db + "    刷新间隔: "
+                + (READ_INTERVAL_MS / 1000) + " 秒    点位数量: " + pointDefs.size());
+        lblPlcInfo.setFont(new Font("微软雅黑", Font.PLAIN, 15));
+        panel.add(lblPlcInfo, BorderLayout.WEST);
+
+        lblLastUpdate = new JLabel("最后更新: --");
+        lblLastUpdate.setFont(new Font("微软雅黑", Font.PLAIN, 15));
+        lblLastUpdate.setHorizontalAlignment(SwingConstants.RIGHT);
+        panel.add(lblLastUpdate, BorderLayout.EAST);
+        return panel;
+    }
+
+    private JScrollPane buildTablePanel() {
+        String[] columns = {"分组", "名称", "PLC地址", "类型", "当前值"};
+        tableModel = new DefaultTableModel(columns, 0) {
+            @Override
+            public boolean isCellEditable(int row, int column) {
+                return false;
+            }
+        };
+
+        JTable table = new JTable(tableModel);
+        table.setRowHeight(28);
+        table.setFont(new Font("微软雅黑", Font.PLAIN, 14));
+        table.getTableHeader().setFont(new Font("微软雅黑", Font.BOLD, 14));
+        table.getTableHeader().setReorderingAllowed(false);
+        table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+
+        table.getColumnModel().getColumn(0).setPreferredWidth(120);
+        table.getColumnModel().getColumn(1).setPreferredWidth(180);
+        table.getColumnModel().getColumn(2).setPreferredWidth(120);
+        table.getColumnModel().getColumn(3).setPreferredWidth(70);
+        table.getColumnModel().getColumn(4).setPreferredWidth(140);
+
+        DefaultTableCellRenderer centerRenderer = new DefaultTableCellRenderer();
+        centerRenderer.setHorizontalAlignment(SwingConstants.CENTER);
+        table.getColumnModel().getColumn(2).setCellRenderer(centerRenderer);
+        table.getColumnModel().getColumn(3).setCellRenderer(centerRenderer);
+        table.getColumnModel().getColumn(4).setCellRenderer(new PlcValueCellRenderer());
+
+        DefaultTableCellRenderer leftRenderer = new DefaultTableCellRenderer();
+        leftRenderer.setHorizontalAlignment(SwingConstants.LEFT);
+        table.getColumnModel().getColumn(1).setCellRenderer(leftRenderer);
+
+        for (PlcPointDef def : pointDefs) {
+            tableModel.addRow(new Object[]{def.group, def.name, db + "." + def.offset, def.type, "--"});
+        }
+
+        JScrollPane scrollPane = new JScrollPane(table);
+        scrollPane.setBorder(new TitledBorder("DB108 全部点位(实时)"));
+        return scrollPane;
+    }
+
+    private JPanel buildBottomPanel() {
+        JPanel panel = new JPanel(new BorderLayout());
+        lblReadStatus = new JLabel("状态: 等待首次读取...");
+        lblReadStatus.setFont(new Font("微软雅黑", Font.PLAIN, 14));
+        lblReadStatus.setForeground(new Color(0, 100, 0));
+        panel.add(lblReadStatus, BorderLayout.WEST);
+
+        JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 0));
+
+        btnAllowA = new JButton("测试: A允许启动 → TRUE");
+        btnAllowA.setFont(new Font("微软雅黑", Font.PLAIN, 14));
+        btnAllowA.setForeground(new Color(0, 100, 0));
+        btnAllowA.addActionListener(e -> toggleAllowStart("A"));
+        btnPanel.add(btnAllowA);
+
+        btnAllowB = new JButton("测试: B允许启动 → TRUE");
+        btnAllowB.setFont(new Font("微软雅黑", Font.PLAIN, 14));
+        btnAllowB.setForeground(new Color(0, 100, 0));
+        btnAllowB.addActionListener(e -> toggleAllowStart("B"));
+        btnPanel.add(btnAllowB);
+
+        JButton refreshBtn = new JButton("立即刷新");
+        refreshBtn.setFont(new Font("微软雅黑", Font.PLAIN, 14));
+        refreshBtn.addActionListener(e -> readPointsAsync(true));
+        btnPanel.add(refreshBtn);
+
+        panel.add(btnPanel, BorderLayout.EAST);
+        return panel;
+    }
+
+    /**
+     * 测试写入允许启动(A=DB108.2.0,B=DB108.4.0)
+     */
+    private void toggleAllowStart(String side) {
+        final JButton btn = "A".equals(side) ? btnAllowA : btnAllowB;
+        final String offset = "A".equals(side) ? "2.0" : "4.0";
+        final String name = "A".equals(side) ? "工位A允许启动" : "工位B允许启动";
+        btn.setEnabled(false);
+        new Thread(() -> {
+            try {
+                Boolean current = s7PLC.readBoolean(db + "." + offset);
+                boolean newVal = !Boolean.TRUE.equals(current);
+                s7PLC.writeBoolean(db + "." + offset, newVal);
+                final String msg = name + " 已写入: " + (newVal ? "TRUE" : "FALSE");
+                SwingUtilities.invokeLater(() -> {
+                    updateAllowButtonText(side, newVal);
+                    lblReadStatus.setText("状态: " + msg);
+                    lblReadStatus.setForeground(new Color(0, 100, 0));
+                    btn.setEnabled(true);
+                    readPointsAsync(true);
+                });
+            } catch (Exception e) {
+                SwingUtilities.invokeLater(() -> {
+                    lblReadStatus.setText("状态: " + name + "写入失败 - " + e.getMessage());
+                    lblReadStatus.setForeground(Color.RED);
+                    btn.setEnabled(true);
+                });
+            }
+        }, "PlcRead-Allow" + side).start();
+    }
+
+    /** 根据当前值更新测试按钮文案 */
+    private void updateAllowButtonText(String side, boolean currentVal) {
+        JButton btn = "A".equals(side) ? btnAllowA : btnAllowB;
+        String prefix = "A".equals(side) ? "测试: A允许启动" : "测试: B允许启动";
+        if (currentVal) {
+            btn.setText(prefix + " → FALSE");
+            btn.setForeground(new Color(180, 80, 0));
+        } else {
+            btn.setText(prefix + " → TRUE");
+            btn.setForeground(new Color(0, 100, 0));
+        }
+    }
+
+    private void startReadTimer() {
+        readTimer = new Timer("PlcReadTestFrame-Timer", true);
+        readTimer.schedule(new TimerTask() {
+            @Override
+            public void run() {
+                readPointsAsync(false);
+            }
+        }, 0, READ_INTERVAL_MS);
+    }
+
+    private void stopReadTimer() {
+        if (readTimer != null) {
+            readTimer.cancel();
+            readTimer = null;
+        }
+    }
+
+    private void readPointsAsync(boolean manual) {
+        new Thread(() -> {
+            List<String[]> rows = new ArrayList<>();
+            int errorCount = 0;
+            for (PlcPointDef def : pointDefs) {
+                String value = readPointValue(def);
+                if (value.startsWith("读取异常")) {
+                    errorCount++;
+                }
+                rows.add(new String[]{def.group, def.name, db + "." + def.offset, def.type, value});
+            }
+            final int errors = errorCount;
+            final String time = LocalDateTime.now().format(TIME_FMT);
+            SwingUtilities.invokeLater(() -> updateTable(rows, time, errors, manual));
+        }, manual ? "PlcRead-Manual" : "PlcRead-Timer").start();
+    }
+
+    private String readPointValue(PlcPointDef def) {
+        try {
+            String addr = db + "." + def.offset;
+            switch (def.type) {
+                case "Bool":
+                    Boolean b = s7PLC.readBoolean(addr);
+                    return b == null ? "null" : (b ? "TRUE" : "FALSE");
+                case "Word":
+                    Short w = s7PLC.readInt16(addr);
+                    return w == null ? "null" : String.valueOf(w);
+                case "Real":
+                    Float r = s7PLC.readFloat32(addr);
+                    return r == null ? "null" : String.format("%.2f", r);
+                default:
+                    return "未知类型";
+            }
+        } catch (Exception e) {
+            return "读取异常: " + e.getMessage();
+        }
+    }
+
+    private void updateTable(List<String[]> rows, String time, int errorCount, boolean manual) {
+        for (int i = 0; i < rows.size() && i < tableModel.getRowCount(); i++) {
+            String[] row = rows.get(i);
+            for (int col = 0; col < row.length; col++) {
+                tableModel.setValueAt(row[col], i, col);
+            }
+        }
+        lblLastUpdate.setText("最后更新: " + time + (manual ? "(手动)" : ""));
+        if (errorCount > 0) {
+            lblReadStatus.setText("状态: 读取完成," + errorCount + " 个点位异常");
+            lblReadStatus.setForeground(Color.RED);
+        } else if (!manual) {
+            lblReadStatus.setText("状态: 读取正常,共 " + pointDefs.size() + " 个点位");
+            lblReadStatus.setForeground(new Color(0, 128, 0));
+        }
+        syncAllowButtonsFromTable();
+    }
+
+    /** 从表格同步 A/B 允许启动按钮显示 */
+    private void syncAllowButtonsFromTable() {
+        for (int i = 0; i < tableModel.getRowCount(); i++) {
+            String name = String.valueOf(tableModel.getValueAt(i, 1));
+            String val = String.valueOf(tableModel.getValueAt(i, 4));
+            if ("工位A允许启动".equals(name)) {
+                updateAllowButtonText("A", "TRUE".equals(val));
+            } else if ("工位B允许启动".equals(name)) {
+                updateAllowButtonText("B", "TRUE".equals(val));
+            }
+        }
+    }
+
+    /** DB108 点位定义(按对接文档) */
+    private static List<PlcPointDef> buildPointDefs() {
+        List<PlcPointDef> list = new ArrayList<>();
+        // MES→PLC
+        list.add(new PlcPointDef("MES→PLC公共", "MES心跳", "0.0", "Bool"));
+        list.add(new PlcPointDef("MES→PLC工位A", "工位A允许启动", "2.0", "Bool"));
+        list.add(new PlcPointDef("MES→PLC工位B", "工位B允许启动", "4.0", "Bool"));
+        // PLC→MES 公共
+        list.add(new PlcPointDef("PLC→MES公共", "PLC心跳", "6.0", "Bool"));
+        list.add(new PlcPointDef("PLC→MES公共", "故障报警状态", "6.1", "Bool"));
+        // 工位A
+        list.add(new PlcPointDef("PLC→MES工位A", "A联机屏蔽", "8.0", "Bool"));
+        list.add(new PlcPointDef("PLC→MES工位A", "A上料位", "8.1", "Bool"));
+        list.add(new PlcPointDef("PLC→MES工位A", "A待加工", "8.2", "Bool"));
+        list.add(new PlcPointDef("PLC→MES工位A", "A作业中", "8.3", "Bool"));
+        list.add(new PlcPointDef("PLC→MES工位A", "A作业完成", "8.4", "Bool"));
+        // 工位B
+        list.add(new PlcPointDef("PLC→MES工位B", "B联机屏蔽", "10.0", "Bool"));
+        list.add(new PlcPointDef("PLC→MES工位B", "B上料位", "10.1", "Bool"));
+        list.add(new PlcPointDef("PLC→MES工位B", "B待加工", "10.2", "Bool"));
+        list.add(new PlcPointDef("PLC→MES工位B", "B作业中", "10.3", "Bool"));
+        list.add(new PlcPointDef("PLC→MES工位B", "B作业完成", "10.4", "Bool"));
+        // R1
+        list.add(new PlcPointDef("工艺数据R1", "R1起弧中", "12.0", "Bool"));
+        list.add(new PlcPointDef("工艺数据R1", "R1 JOB号", "14", "Real"));
+        list.add(new PlcPointDef("工艺数据R1", "R1电流", "18", "Real"));
+        list.add(new PlcPointDef("工艺数据R1", "R1电压", "22", "Real"));
+        list.add(new PlcPointDef("工艺数据R1", "R1送丝速度", "26", "Real"));
+        list.add(new PlcPointDef("工艺数据R1", "R1作业速度", "30", "Real"));
+        // R2
+        list.add(new PlcPointDef("工艺数据R2", "R2起弧中", "34.0", "Bool"));
+        list.add(new PlcPointDef("工艺数据R2", "R2 JOB号", "36", "Real"));
+        list.add(new PlcPointDef("工艺数据R2", "R2电流", "40", "Real"));
+        list.add(new PlcPointDef("工艺数据R2", "R2电压", "44", "Real"));
+        list.add(new PlcPointDef("工艺数据R2", "R2送丝速度", "48", "Real"));
+        list.add(new PlcPointDef("工艺数据R2", "R2作业速度", "52", "Real"));
+        // R3
+        list.add(new PlcPointDef("工艺数据R3", "R3起弧中", "56.0", "Bool"));
+        list.add(new PlcPointDef("工艺数据R3", "R3 JOB号", "58", "Real"));
+        list.add(new PlcPointDef("工艺数据R3", "R3电流", "62", "Real"));
+        list.add(new PlcPointDef("工艺数据R3", "R3电压", "66", "Real"));
+        list.add(new PlcPointDef("工艺数据R3", "R3送丝速度", "70", "Real"));
+        list.add(new PlcPointDef("工艺数据R3", "R3作业速度", "74", "Real"));
+        // R4
+        list.add(new PlcPointDef("工艺数据R4", "R4起弧中", "78.0", "Bool"));
+        list.add(new PlcPointDef("工艺数据R4", "R4 JOB号", "80", "Real"));
+        list.add(new PlcPointDef("工艺数据R4", "R4电流", "84", "Real"));
+        list.add(new PlcPointDef("工艺数据R4", "R4电压", "88", "Real"));
+        list.add(new PlcPointDef("工艺数据R4", "R4送丝速度", "92", "Real"));
+        list.add(new PlcPointDef("工艺数据R4", "R4作业速度", "96", "Real"));
+        return list;
+    }
+
+    public static String loadPlcIp() {
+        try {
+            InputStream is = ClassLoader.getSystemResourceAsStream("config/config.properties");
+            if (is != null) {
+                Properties pro = new Properties();
+                BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
+                pro.load(br);
+                br.close();
+                String ip = pro.getProperty("mes.plc_ip");
+                if (ip != null && !ip.trim().isEmpty()) {
+                    return ip.trim();
+                }
+            }
+        } catch (Exception ignored) {
+        }
+        return DEFAULT_PLC_IP;
+    }
+
+    /** 点位定义 */
+    private static class PlcPointDef {
+        final String group;
+        final String name;
+        final String offset;
+        final String type;
+
+        PlcPointDef(String group, String name, String offset, String type) {
+            this.group = group;
+            this.name = name;
+            this.offset = offset;
+            this.type = type;
+        }
+    }
+
+    /** 当前值列渲染:TRUE/FALSE/异常 分色显示 */
+    private static class PlcValueCellRenderer extends DefaultTableCellRenderer {
+        @Override
+        public Component getTableCellRendererComponent(JTable table, Object value,
+                                                       boolean isSelected, boolean hasFocus,
+                                                       int row, int column) {
+            Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
+            setHorizontalAlignment(SwingConstants.CENTER);
+            if (!isSelected) {
+                String text = value == null ? "" : value.toString();
+                if ("TRUE".equals(text)) {
+                    setBackground(new Color(210, 245, 210));
+                    setForeground(new Color(0, 120, 0));
+                    setFont(getFont().deriveFont(Font.BOLD));
+                } else if ("FALSE".equals(text)) {
+                    setBackground(Color.WHITE);
+                    setForeground(new Color(100, 100, 100));
+                    setFont(getFont().deriveFont(Font.PLAIN));
+                } else if (text.startsWith("读取异常")) {
+                    setBackground(new Color(255, 230, 230));
+                    setForeground(Color.RED);
+                } else {
+                    setBackground(new Color(240, 248, 255));
+                    setForeground(new Color(0, 70, 140));
+                    setFont(getFont().deriveFont(Font.BOLD));
+                }
+            }
+            return c;
+        }
+    }
+}

+ 755 - 0
src/com/mes/ui/PlcUtil.java

@@ -0,0 +1,755 @@
+package com.mes.ui;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.github.xingshuangs.iot.protocol.s7.service.S7PLC;
+import com.mes.util.DateLocalUtils;
+import com.mes.util.JdbcUtils;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.List;
+
+/**
+ * OP070-OP080 弧焊工作站 PLC 读写工具(DB108 非优化块)
+ */
+public class PlcUtil {
+
+    private static final String DB = "DB108";
+
+    /** MES→PLC 公共区:心跳 */
+    private static final String ADDR_MES_HEARTBEAT = DB + ".0.0";
+    /** MES→PLC 工位A:允许启动 */
+    private static final String ADDR_ALLOW_A = DB + ".2.0";
+    /** MES→PLC 工位B:允许启动 */
+    private static final String ADDR_ALLOW_B = DB + ".4.0";
+
+    /** PLC→MES 公共区:心跳 */
+    private static final String ADDR_PLC_HEARTBEAT = DB + ".6.0";
+    /** PLC→MES 公共区:故障报警状态 */
+    private static final String ADDR_FAULT = DB + ".6.1";
+
+    /** PLC→MES 工位A */
+    private static final String ADDR_SHIELD_A = DB + ".8.0";
+    private static final String ADDR_LOAD_A = DB + ".8.1";
+    private static final String ADDR_WAIT_A = DB + ".8.2";
+    private static final String ADDR_WORK_A = DB + ".8.3";
+    private static final String ADDR_DONE_A = DB + ".8.4";
+
+    /** PLC→MES 工位B */
+    private static final String ADDR_SHIELD_B = DB + ".10.0";
+    private static final String ADDR_LOAD_B = DB + ".10.1";
+    private static final String ADDR_WAIT_B = DB + ".10.2";
+    private static final String ADDR_WORK_B = DB + ".10.3";
+    private static final String ADDR_DONE_B = DB + ".10.4";
+
+    /** 工艺数据 R1(1号机/3号机为双机工位) */
+    private static final String ADDR_R1_ARC = DB + ".12.0";
+    private static final String ADDR_R1_JOB = DB + ".14";
+    private static final String ADDR_R1_CURRENT = DB + ".18";
+    private static final String ADDR_R1_VOLTAGE = DB + ".22";
+    private static final String ADDR_R1_WIRE = DB + ".26";
+    private static final String ADDR_R1_SPEED = DB + ".30";
+
+    /** 工艺数据 R2 */
+    private static final String ADDR_R2_ARC = DB + ".34.0";
+    private static final String ADDR_R2_JOB = DB + ".36";
+    private static final String ADDR_R2_CURRENT = DB + ".40";
+    private static final String ADDR_R2_VOLTAGE = DB + ".44";
+    private static final String ADDR_R2_WIRE = DB + ".48";
+    private static final String ADDR_R2_SPEED = DB + ".52";
+
+    /** 工艺数据 R3(2号机为四机工位) */
+    private static final String ADDR_R3_ARC = DB + ".56.0";
+    private static final String ADDR_R3_JOB = DB + ".58";
+    private static final String ADDR_R3_CURRENT = DB + ".62";
+    private static final String ADDR_R3_VOLTAGE = DB + ".66";
+    private static final String ADDR_R3_WIRE = DB + ".70";
+    private static final String ADDR_R3_SPEED = DB + ".74";
+
+    /** 工艺数据 R4 */
+    private static final String ADDR_R4_ARC = DB + ".78.0";
+    private static final String ADDR_R4_JOB = DB + ".80";
+    private static final String ADDR_R4_CURRENT = DB + ".84";
+    private static final String ADDR_R4_VOLTAGE = DB + ".88";
+    private static final String ADDR_R4_WIRE = DB + ".92";
+    private static final String ADDR_R4_SPEED = DB + ".96";
+
+    /** MES 心跳翻转状态 */
+    private static boolean mesHeartbeatState = false;
+
+    /**
+     * 翻转并写入 MES→PLC 心跳(DB108.0.0),供 PLC 判断 MES 在线
+     */
+    public static void pulseMesHeartbeat(S7PLC s7PLC) {
+        try {
+            mesHeartbeatState = !mesHeartbeatState;
+            s7PLC.writeBoolean(ADDR_MES_HEARTBEAT, mesHeartbeatState);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 读取 PLC→MES 心跳
+     */
+    public static Boolean getPlcHeartbeat(S7PLC s7PLC) {
+        try {
+            return s7PLC.readBoolean(ADDR_PLC_HEARTBEAT);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 读取故障报警状态
+     */
+    public static Boolean getFaultAlarm(S7PLC s7PLC) {
+        try {
+            return s7PLC.readBoolean(ADDR_FAULT);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 获取工作站外面当前是哪一面(上料位点位)
+     */
+    public static String getOutsideSide(S7PLC s7PLC) {
+        try {
+            Boolean stationA = s7PLC.readBoolean(ADDR_LOAD_A);
+            Boolean stationB = s7PLC.readBoolean(ADDR_LOAD_B);
+            if (Boolean.TRUE.equals(stationA) && !Boolean.TRUE.equals(stationB)) {
+                return "A";
+            }
+            if (Boolean.TRUE.equals(stationB) && !Boolean.TRUE.equals(stationA)) {
+                return "B";
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return MesClient.curPage.isEmpty() ? "A" : MesClient.curPage;
+    }
+
+    /**
+     * 读取联机屏蔽(任一面为1则视为屏蔽)
+     */
+    public static Boolean getShieldSignal(S7PLC s7PLC) {
+        try {
+            Boolean a = s7PLC.readBoolean(ADDR_SHIELD_A);
+            Boolean b = s7PLC.readBoolean(ADDR_SHIELD_B);
+            return Boolean.TRUE.equals(a) || Boolean.TRUE.equals(b);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 读取指定面联机屏蔽
+     */
+    public static Boolean getShieldSignal(S7PLC s7PLC, String side) {
+        try {
+            String addr = "A".equals(side) ? ADDR_SHIELD_A : ADDR_SHIELD_B;
+            return s7PLC.readBoolean(addr);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * DB108 联机屏蔽为 PLC→MES 只读点位,MES 不能写入
+     */
+    public static boolean setShieldSignal(S7PLC s7PLC, boolean shield) {
+        return false;
+    }
+
+    /**
+     * 按面写入允许启动(工位A=DB108.2.0,工位B=DB108.4.0)
+     */
+    public static boolean setMesAllowStart(S7PLC s7PLC, String side, boolean allow) {
+        try {
+            String addr = "A".equals(side) ? ADDR_ALLOW_A : ADDR_ALLOW_B;
+            s7PLC.writeBoolean(addr, allow);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 兼容旧接口:同时写 A/B 允许启动
+     */
+    public static boolean setMesAllowEntry(S7PLC s7PLC, boolean allow) {
+        boolean a = setMesAllowStart(s7PLC, "A", allow);
+        boolean b = setMesAllowStart(s7PLC, "B", allow);
+        return a && b;
+    }
+
+    /**
+     * 作业中时立即撤销对应面的允许启动(A/B 独立)
+     */
+    public static void syncMesAllowOnSideStart(S7PLC s7PLC) {
+        try {
+            if (Boolean.TRUE.equals(getAStart(s7PLC))) {
+                setMesAllowStart(s7PLC, "A", false);
+            }
+            if (Boolean.TRUE.equals(getBStart(s7PLC))) {
+                setMesAllowStart(s7PLC, "B", false);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * A面作业中
+     */
+    public static Boolean getAStart(S7PLC s7PLC) {
+        try {
+            return s7PLC.readBoolean(ADDR_WORK_A);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * B面作业中
+     */
+    public static Boolean getBStart(S7PLC s7PLC) {
+        try {
+            return s7PLC.readBoolean(ADDR_WORK_B);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * A面作业完成
+     */
+    public static Boolean getAFinish(S7PLC s7PLC) {
+        try {
+            return s7PLC.readBoolean(ADDR_DONE_A);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * B面作业完成
+     */
+    public static Boolean getBFinish(S7PLC s7PLC) {
+        try {
+            return s7PLC.readBoolean(ADDR_DONE_B);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 质量检查通过后:发送对应面允许启动,进入等待启动状态
+     */
+    public static void onQualityPass(String side) {
+        onQualityPass(side, MesClient.getSideSn(side));
+    }
+
+    public static void onQualityPass(String side, String sn) {
+        if (sn != null && !sn.trim().isEmpty()) {
+            MesClient.setSideSn(side, sn);
+        }
+        final String statusText = "质量合格,已发送允许启动,等待启动";
+        if ("A".equals(side)) {
+            MesClient.tjFlaga = 1;
+            MesClient.mesQualityFlagA = true;
+            MesClient.hjparamsA.clear();
+        } else {
+            MesClient.tjFlagb = 1;
+            MesClient.mesQualityFlagB = true;
+            MesClient.hjparamsB.clear();
+        }
+        setMesAllowStart(MesClient.s7PLC, side, true);
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                MesClient.setSideStatus(side, statusText);
+                MesClient.setMenuStatus(side + ":" + statusText, 0);
+            }
+        });
+    }
+
+    /**
+     * 免扫码工位:空闲时直接发送允许启动,不扫码、不质检(A/B 独立)
+     */
+    public static void syncAutoAllowStations(S7PLC s7PLC) {
+        try {
+            Boolean aStart = getAStart(s7PLC);
+            Boolean bStart = getBStart(s7PLC);
+            if (MesClient.isAutoAllowSide("A") && !Boolean.TRUE.equals(aStart)) {
+                if (MesClient.tjFlaga == 0) {
+                    enterAutoAllowWait("A");
+                } else if (MesClient.tjFlaga == 1) {
+                    setMesAllowStart(s7PLC, "A", true);
+                }
+            }
+            if (MesClient.isAutoAllowSide("B") && !Boolean.TRUE.equals(bStart)) {
+                if (MesClient.tjFlagb == 0) {
+                    enterAutoAllowWait("B");
+                } else if (MesClient.tjFlagb == 1) {
+                    setMesAllowStart(s7PLC, "B", true);
+                }
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 免扫码工位进入等待启动:写对应面允许启动
+     */
+    private static void enterAutoAllowWait(String side) {
+        final String statusText = "免扫码工位,已允许启动,可直接启动";
+        if ("A".equals(side)) {
+            MesClient.tjFlaga = 1;
+            MesClient.mesQualityFlagA = true;
+            MesClient.hjparamsA.clear();
+            MesClient.workSnA = "";
+        } else {
+            MesClient.tjFlagb = 1;
+            MesClient.mesQualityFlagB = true;
+            MesClient.hjparamsB.clear();
+            MesClient.workSnB = "";
+        }
+        MesClient.work_status = 1;
+        setMesAllowStart(MesClient.s7PLC, side, true);
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                MesClient.setSideSn(side, "免扫码");
+                MesClient.setSideStatus(side, statusText);
+                MesClient.setMenuStatus(side + ":" + statusText, 0);
+                if ("A".equals(side)) {
+                    if (MesClient.scan_bt_a != null) {
+                        MesClient.scan_bt_a.setEnabled(false);
+                    }
+                } else if (MesClient.scan_bt_b != null) {
+                    MesClient.scan_bt_b.setEnabled(false);
+                }
+            }
+        });
+    }
+
+    /**
+     * 免扫码工位加工完成:不采数、不提交,复位后由 syncAutoAllow 再次允许启动
+     */
+    private static void finishAutoAllowSide(String side) {
+        if ("A".equals(side)) {
+            MesClient.tjFlaga = 0;
+            MesClient.mesQualityFlagA = false;
+            MesClient.tjStatusa = 0;
+            MesClient.workSnA = "";
+            MesClient.hjparamsA.clear();
+        } else {
+            MesClient.tjFlagb = 0;
+            MesClient.mesQualityFlagB = false;
+            MesClient.tjStatusb = 0;
+            MesClient.workSnB = "";
+            MesClient.hjparamsB.clear();
+        }
+        if (MesClient.tjFlaga == 0 && MesClient.tjFlagb == 0) {
+            MesClient.work_status = 0;
+        }
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                MesClient.setSideSn(side, "免扫码");
+                MesClient.setSideStatus(side, "免扫码工位加工完成,等待下一件");
+                MesClient.setMenuStatus(side + ":免扫码工位加工完成,不提交数据", 0);
+                if ("A".equals(side)) {
+                    if (MesClient.finish_ok_bt_a != null) {
+                        MesClient.finish_ok_bt_a.setEnabled(false);
+                    }
+                    if (MesClient.finish_ng_bt_a != null) {
+                        MesClient.finish_ng_bt_a.setEnabled(false);
+                    }
+                } else {
+                    if (MesClient.finish_ok_bt_b != null) {
+                        MesClient.finish_ok_bt_b.setEnabled(false);
+                    }
+                    if (MesClient.finish_ng_bt_b != null) {
+                        MesClient.finish_ng_bt_b.setEnabled(false);
+                    }
+                }
+            }
+        });
+    }
+
+    /**
+     * A 面状态机:等待启动 -> 运行采数 -> 完成提交
+     */
+    public static void getStatusA(S7PLC s7PLC) {
+        if (Boolean.TRUE.equals(getAStart(s7PLC))) {
+            setMesAllowStart(s7PLC, "A", false);
+        }
+        boolean autoAllow = MesClient.isAutoAllowSide("A");
+        if (MesClient.tjFlaga == 1) {
+            if (Boolean.TRUE.equals(getAStart(s7PLC))) {
+                MesClient.curFlag = "A";
+                MesClient.tjFlaga = 2;
+                MesClient.hjparamsA.clear();
+                final String runText = autoAllow ? "免扫码工位运行中(不采集)" : "设备运行中,采集中...";
+                SwingUtilities.invokeLater(new Runnable() {
+                    @Override
+                    public void run() {
+                        MesClient.setSideStatus("A", runText);
+                    }
+                });
+            }
+        } else if (MesClient.tjFlaga == 2) {
+            if (Boolean.TRUE.equals(getAFinish(s7PLC))) {
+                if (autoAllow) {
+                    finishAutoAllowSide("A");
+                    return;
+                }
+                collectWeldingParams(s7PLC);
+                MesClient.tjFlaga = 3;
+                SwingUtilities.invokeLater(new Runnable() {
+                    @Override
+                    public void run() {
+                        MesClient.setSideStatus("A", "焊接完成,提交结果中...");
+                    }
+                });
+                submitResult("A", MesClient.getSideSn("A"), MesClient.mes_gw1);
+            }
+        }
+    }
+
+    /**
+     * B 面状态机
+     */
+    public static void getStatusB(S7PLC s7PLC) {
+        if (Boolean.TRUE.equals(getBStart(s7PLC))) {
+            setMesAllowStart(s7PLC, "B", false);
+        }
+        boolean autoAllow = MesClient.isAutoAllowSide("B");
+        if (MesClient.tjFlagb == 1) {
+            if (Boolean.TRUE.equals(getBStart(s7PLC))) {
+                MesClient.curFlag = "B";
+                MesClient.tjFlagb = 2;
+                MesClient.hjparamsB.clear();
+                final String runText = autoAllow ? "免扫码工位运行中(不采集)" : "设备运行中,采集中...";
+                SwingUtilities.invokeLater(new Runnable() {
+                    @Override
+                    public void run() {
+                        MesClient.setSideStatus("B", runText);
+                    }
+                });
+            }
+        } else if (MesClient.tjFlagb == 2) {
+            if (Boolean.TRUE.equals(getBFinish(s7PLC))) {
+                if (autoAllow) {
+                    finishAutoAllowSide("B");
+                    return;
+                }
+                collectWeldingParams(s7PLC);
+                MesClient.tjFlagb = 3;
+                SwingUtilities.invokeLater(new Runnable() {
+                    @Override
+                    public void run() {
+                        MesClient.setSideStatus("B", "焊接完成,提交结果中...");
+                    }
+                });
+                submitResult("B", MesClient.getSideSn("B"), MesClient.mes_gw2);
+            }
+        }
+    }
+
+    /**
+     * 运行中每秒采集 R1~R4 焊接参数(DB108)
+     * 只写入内存,不中途落库;加工结束整包一条上传
+     * 免扫码工位不采集
+     */
+    public static void collectWeldingParams(S7PLC s7PLC) {
+        boolean collectA = MesClient.tjFlaga == 2 && !MesClient.isAutoAllowSide("A");
+        boolean collectB = MesClient.tjFlagb == 2 && !MesClient.isAutoAllowSide("B");
+        if (!collectA && !collectB) {
+            return;
+        }
+
+        try {
+            Float r1Current = s7PLC.readFloat32(ADDR_R1_CURRENT);
+            Float r1Voltage = s7PLC.readFloat32(ADDR_R1_VOLTAGE);
+            Float r1WireFeed = s7PLC.readFloat32(ADDR_R1_WIRE);
+            Float r1Speed = s7PLC.readFloat32(ADDR_R1_SPEED);
+            Float r1Job = s7PLC.readFloat32(ADDR_R1_JOB);
+
+            Float r2Current = s7PLC.readFloat32(ADDR_R2_CURRENT);
+            Float r2Voltage = s7PLC.readFloat32(ADDR_R2_VOLTAGE);
+            Float r2WireFeed = s7PLC.readFloat32(ADDR_R2_WIRE);
+            Float r2Speed = s7PLC.readFloat32(ADDR_R2_SPEED);
+            Float r2Job = s7PLC.readFloat32(ADDR_R2_JOB);
+
+            Float r3Current = s7PLC.readFloat32(ADDR_R3_CURRENT);
+            Float r3Voltage = s7PLC.readFloat32(ADDR_R3_VOLTAGE);
+            Float r3WireFeed = s7PLC.readFloat32(ADDR_R3_WIRE);
+            Float r3Speed = s7PLC.readFloat32(ADDR_R3_SPEED);
+            Float r3Job = s7PLC.readFloat32(ADDR_R3_JOB);
+
+            Float r4Current = s7PLC.readFloat32(ADDR_R4_CURRENT);
+            Float r4Voltage = s7PLC.readFloat32(ADDR_R4_VOLTAGE);
+            Float r4WireFeed = s7PLC.readFloat32(ADDR_R4_WIRE);
+            Float r4Speed = s7PLC.readFloat32(ADDR_R4_SPEED);
+            Float r4Job = s7PLC.readFloat32(ADDR_R4_JOB);
+
+            String recordTime = DateLocalUtils.getCurrentTime();
+            String paramLine = r1Current + "|" + r1Voltage + "|" + r1WireFeed + "|" + r1Speed + "|" + r1Job + "|"
+                    + r2Current + "|" + r2Voltage + "|" + r2WireFeed + "|" + r2Speed + "|" + r2Job + "|"
+                    + r3Current + "|" + r3Voltage + "|" + r3WireFeed + "|" + r3Speed + "|" + r3Job + "|"
+                    + r4Current + "|" + r4Voltage + "|" + r4WireFeed + "|" + r4Speed + "|" + r4Job + "|"
+                    + recordTime;
+
+            if (collectA) {
+                MesClient.hjparamsA.add(paramLine);
+            }
+            if (collectB) {
+                MesClient.hjparamsB.add(paramLine);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 刷新左右面 PLC 布尔点位显示(上料位 / 待加工 / 作业中 / 作业完成 / 联机屏蔽)
+     */
+    public static void refreshSidePointLabels(S7PLC s7PLC) {
+        try {
+            final boolean loadA = Boolean.TRUE.equals(s7PLC.readBoolean(ADDR_LOAD_A));
+            final boolean waitA = Boolean.TRUE.equals(s7PLC.readBoolean(ADDR_WAIT_A));
+            final boolean startA = Boolean.TRUE.equals(s7PLC.readBoolean(ADDR_WORK_A));
+            final boolean finishA = Boolean.TRUE.equals(s7PLC.readBoolean(ADDR_DONE_A));
+            final boolean shieldA = Boolean.TRUE.equals(s7PLC.readBoolean(ADDR_SHIELD_A));
+
+            final boolean loadB = Boolean.TRUE.equals(s7PLC.readBoolean(ADDR_LOAD_B));
+            final boolean waitB = Boolean.TRUE.equals(s7PLC.readBoolean(ADDR_WAIT_B));
+            final boolean startB = Boolean.TRUE.equals(s7PLC.readBoolean(ADDR_WORK_B));
+            final boolean finishB = Boolean.TRUE.equals(s7PLC.readBoolean(ADDR_DONE_B));
+            final boolean shieldB = Boolean.TRUE.equals(s7PLC.readBoolean(ADDR_SHIELD_B));
+
+            SwingUtilities.invokeLater(new Runnable() {
+                @Override
+                public void run() {
+                    updateBoolLabel(MesClient.lblPointIdA, loadA);
+                    updateBoolLabel(MesClient.lblPointWaitA, waitA);
+                    updateBoolLabel(MesClient.lblPointStartA, startA);
+                    updateBoolLabel(MesClient.lblPointFinishA, finishA);
+                    updateBoolLabel(MesClient.lblPointShieldA, shieldA);
+                    updateBoolLabel(MesClient.lblPointIdB, loadB);
+                    updateBoolLabel(MesClient.lblPointWaitB, waitB);
+                    updateBoolLabel(MesClient.lblPointStartB, startB);
+                    updateBoolLabel(MesClient.lblPointFinishB, finishB);
+                    updateBoolLabel(MesClient.lblPointShieldB, shieldB);
+                }
+            });
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 刷新底部公共焊机参数(DB108 R1~R4 工艺参数)
+     */
+    public static void refreshWelderParamLabels(S7PLC s7PLC) {
+        try {
+            final Float r1Current = s7PLC.readFloat32(ADDR_R1_CURRENT);
+            final Float r1Voltage = s7PLC.readFloat32(ADDR_R1_VOLTAGE);
+            final Float r1WireFeed = s7PLC.readFloat32(ADDR_R1_WIRE);
+            final Float r1Job = s7PLC.readFloat32(ADDR_R1_JOB);
+
+            final Float r2Current = s7PLC.readFloat32(ADDR_R2_CURRENT);
+            final Float r2Voltage = s7PLC.readFloat32(ADDR_R2_VOLTAGE);
+            final Float r2WireFeed = s7PLC.readFloat32(ADDR_R2_WIRE);
+            final Float r2Job = s7PLC.readFloat32(ADDR_R2_JOB);
+
+            final Float r3Current = s7PLC.readFloat32(ADDR_R3_CURRENT);
+            final Float r3Voltage = s7PLC.readFloat32(ADDR_R3_VOLTAGE);
+            final Float r3WireFeed = s7PLC.readFloat32(ADDR_R3_WIRE);
+            final Float r3Job = s7PLC.readFloat32(ADDR_R3_JOB);
+
+            final Float r4Current = s7PLC.readFloat32(ADDR_R4_CURRENT);
+            final Float r4Voltage = s7PLC.readFloat32(ADDR_R4_VOLTAGE);
+            final Float r4WireFeed = s7PLC.readFloat32(ADDR_R4_WIRE);
+            final Float r4Job = s7PLC.readFloat32(ADDR_R4_JOB);
+
+            SwingUtilities.invokeLater(new Runnable() {
+                @Override
+                public void run() {
+                    setParamText(MesClient.r1_voltage, r1Voltage);
+                    setParamText(MesClient.r1_current, r1Current);
+                    setParamText(MesClient.r1_wire, r1WireFeed);
+                    setParamText(MesClient.r1_bead, r1Job);
+
+                    setParamText(MesClient.r2_voltage, r2Voltage);
+                    setParamText(MesClient.r2_current, r2Current);
+                    setParamText(MesClient.r2_wire, r2WireFeed);
+                    setParamText(MesClient.r2_bead, r2Job);
+
+                    setParamText(MesClient.r3_voltage, r3Voltage);
+                    setParamText(MesClient.r3_current, r3Current);
+                    setParamText(MesClient.r3_wire, r3WireFeed);
+                    setParamText(MesClient.r3_bead, r3Job);
+
+                    setParamText(MesClient.r4_voltage, r4Voltage);
+                    setParamText(MesClient.r4_current, r4Current);
+                    setParamText(MesClient.r4_wire, r4WireFeed);
+                    setParamText(MesClient.r4_bead, r4Job);
+                }
+            });
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    private static void updateBoolLabel(JLabel label, boolean value) {
+        if (label == null) {
+            return;
+        }
+        if (value) {
+            label.setText("TRUE");
+            label.setForeground(new Color(0, 140, 0));
+            label.setBackground(new Color(200, 255, 200));
+        } else {
+            label.setText("FALSE");
+            label.setForeground(Color.GRAY);
+            label.setBackground(new Color(230, 230, 230));
+        }
+    }
+
+    private static void setParamText(JTextField field, Object value) {
+        if (field == null) {
+            return;
+        }
+        if (value == null) {
+            field.setText("0");
+            return;
+        }
+        if (value instanceof Float) {
+            field.setText(String.format("%.2f", (Float) value));
+        } else {
+            field.setText(String.valueOf(value));
+        }
+    }
+
+    /**
+     * 焊接完成:先提交质量结果,再整包上传焊接参数(两套接口分开)
+     * 1) sendQualityPc → MES 质量结果(pcresult)
+     * 2) flushHjParams → 本地 bw_cmt(一个工件一条,多条采数合并)
+     * 3) uploadPendingCmtParams → batchsave
+     */
+    private static void submitResult(String side, String sn, String oprno) {
+        if (sn == null || sn.trim().isEmpty()) {
+            sn = MesClient.getSideSn(side);
+        }
+        if (sn == null || sn.trim().isEmpty()) {
+            MesClient.log.error("{}面提交结果失败:工件码为空 workSnA={} workSnB={}",
+                    side, MesClient.workSnA, MesClient.workSnB);
+            MesClient.setMenuStatus(side + "面工件码为空,无法提交结果,请手动OK/NG", -1);
+            SwingUtilities.invokeLater(new Runnable() {
+                @Override
+                public void run() {
+                    MesClient.setSideStatus(side, "工件码为空,提交失败");
+                    enableManualButtons(side);
+                }
+            });
+            return;
+        }
+        final String finalSn = sn.trim();
+
+        MesClient.getUser();
+        MesClient.log.info("{}面开始提交结果 sn={} oprno={} user={}",
+                side, finalSn, oprno, MesClient.user20);
+
+        JSONObject result = DataUtil.sendQualityPc(finalSn, "OK", MesClient.user20, oprno,
+                MesClient.mes_line_sn, MesClient.mes_server_ip);
+        boolean ok = DataUtil.isMesSuccess(result);
+
+        flushHjParams(side, oprno, finalSn);
+        MesClient.uploadPendingCmtParams();
+
+        if (ok) {
+            MesClient.setMenuStatus(side + "面结果提交成功", 0);
+            SwingUtilities.invokeLater(new Runnable() {
+                @Override
+                public void run() {
+                    MesClient.setSideStatus(side, "提交成功");
+                }
+            });
+            MesClient.resetScan(side);
+        } else {
+            String msg = (result == null) ? "请求失败或返回空" : result.getString("message");
+            if (msg == null || msg.isEmpty()) {
+                msg = "提交失败";
+            }
+            MesClient.log.error("{}面结果提交失败 sn={} oprno={} ret={}",
+                    side, finalSn, oprno, result == null ? "null" : result.toJSONString());
+            MesClient.setMenuStatus(side + "面结果" + msg + ",可手动重试", -1);
+            SwingUtilities.invokeLater(new Runnable() {
+                @Override
+                public void run() {
+                    MesClient.setSideStatus(side, "结果提交失败,可手动OK/NG");
+                    enableManualButtons(side);
+                }
+            });
+        }
+    }
+
+    private static void enableManualButtons(String side) {
+        if ("A".equals(side)) {
+            if (MesClient.finish_ok_bt_a != null) {
+                MesClient.finish_ok_bt_a.setEnabled(true);
+            }
+            if (MesClient.finish_ng_bt_a != null) {
+                MesClient.finish_ng_bt_a.setEnabled(true);
+            }
+            MesClient.tjStatusa = 1;
+        } else {
+            if (MesClient.finish_ok_bt_b != null) {
+                MesClient.finish_ok_bt_b.setEnabled(true);
+            }
+            if (MesClient.finish_ng_bt_b != null) {
+                MesClient.finish_ng_bt_b.setEnabled(true);
+            }
+            MesClient.tjStatusb = 1;
+        }
+    }
+
+    /**
+     * 刷写指定面焊接参数到本地库(整包一条,不中途分片)
+     */
+    public static void flushHjParams(String oprno, String sn) {
+        String side = oprno != null && oprno.equals(MesClient.mes_gw2) ? "B" : "A";
+        flushHjParams(side, oprno, sn);
+    }
+
+    public static void flushHjParams(String side, String oprno, String sn) {
+        List<String> list = MesClient.getSideHjParams(side);
+        if (list == null || list.isEmpty()) {
+            return;
+        }
+        try {
+            boolean ok = JdbcUtils.insertCmtData(oprno, MesClient.mes_line_sn, sn, JSON.toJSONString(list));
+            if (ok) {
+                list.clear();
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+}

+ 14 - 0
src/com/mes/ui/TableCellRendererButton.java

@@ -0,0 +1,14 @@
+package com.mes.ui;
+
+import javax.swing.*;
+import javax.swing.table.TableCellRenderer;
+import java.awt.*;
+
+public class TableCellRendererButton implements TableCellRenderer {
+    @Override
+    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus,
+                                                   int row, int column) {
+        JButton button = new JButton("扫码");
+        return button;
+    }
+}

+ 61 - 0
src/com/mes/ui/WorkRecordData.java

@@ -0,0 +1,61 @@
+package com.mes.ui;
+
+/**
+ * 工作记录数据实体类
+ */
+public class WorkRecordData {
+    private String id;
+    private String sn;          // 工件码
+    private String oprno;       // 工位号
+    private String updateBy;    // 作业人员
+    private String updateDate;  // 日期
+    private String content;     // 工位结果
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getSn() {
+        return sn;
+    }
+
+    public void setSn(String sn) {
+        this.sn = sn;
+    }
+
+    public String getOprno() {
+        return oprno;
+    }
+
+    public void setOprno(String oprno) {
+        this.oprno = oprno;
+    }
+
+    public String getUpdateBy() {
+        return updateBy;
+    }
+
+    public void setUpdateBy(String updateBy) {
+        this.updateBy = updateBy;
+    }
+
+    public String getUpdateDate() {
+        return updateDate;
+    }
+
+    public void setUpdateDate(String updateDate) {
+        this.updateDate = updateDate;
+    }
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+}

+ 403 - 0
src/com/mes/ui/WorkRecordPanel.java

@@ -0,0 +1,403 @@
+package com.mes.ui;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.swing.*;
+import javax.swing.table.DefaultTableCellRenderer;
+import javax.swing.table.DefaultTableModel;
+import javax.swing.table.TableColumn;
+import java.awt.*;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.util.List;
+
+/**
+ * 工作记录面板组件
+ * 支持分页查询和展示工作记录数据
+ */
+public class WorkRecordPanel extends JPanel {
+
+    private static final Logger log = LoggerFactory.getLogger(WorkRecordPanel.class);
+
+    // 表格列名
+    private static final String[] COLUMN_NAMES = {"工件码", "工位号", "工位结果", "作业人员", "日期"};
+
+    // 分页配置
+    private static final int PAGE_SIZE = 20;
+
+    // 查询条件
+    private String oprno;  // 工位号(null或空则查全部)
+    private String lineSn; // 产线编号
+    private boolean isAllStation; // 是否是"所有工位"模式
+
+    // 分页状态
+    private int pageNo = 1;
+    private long totalCount = 0;
+    private int totalPages = 0;
+
+    // UI组件
+    private JTable table;
+    private DefaultTableModel tableModel;
+    private JLabel pageLabel;
+    private JButton btnPrevious;
+    private JButton btnNext;
+    private JButton btnRefresh;
+    private JButton btnSearch;
+    private JLabel statusLabel;
+    private JTextField snSearchField;      // 工件码搜索框
+    private JTextField oprnoSearchField;   // 工位号搜索框(仅"所有工位"模式)
+    private JLabel snLabel;
+    private JLabel oprnoLabel;
+
+    /**
+     * 构造函数
+     * @param oprno 工位号(如OP150A查询该工位,null或空则查所有工位)
+     * @param lineSn 产线编号
+     */
+    public WorkRecordPanel(String oprno, String lineSn) {
+        this.oprno = oprno;
+        this.lineSn = lineSn;
+        this.isAllStation = (oprno == null || oprno.isEmpty());
+        initUI();
+    }
+
+    /**
+     * 初始化UI
+     */
+    private void initUI() {
+        setLayout(null);
+        setBackground(Color.WHITE);
+
+        // 顶部控制区
+        JPanel topPanel = new JPanel();
+        topPanel.setLayout(null);
+        topPanel.setBounds(0, 0, 1200, 50);
+        topPanel.setBackground(new Color(248, 248, 248));
+        add(topPanel);
+
+        int xPos = 10;
+
+        // 状态标签
+        String displayOprno = isAllStation ? "所有工位" : oprno;
+        statusLabel = new JLabel("工位:" + displayOprno);
+        statusLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
+        statusLabel.setBounds(xPos, 10, 160, 30);
+        topPanel.add(statusLabel);
+        xPos += 165;
+
+        // 工件码搜索
+        snLabel = new JLabel("工件码:");
+        snLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
+        snLabel.setBounds(xPos, 10, 50, 30);
+        topPanel.add(snLabel);
+        xPos += 52;
+
+        snSearchField = new JTextField();
+        snSearchField.setFont(new Font("微软雅黑", Font.PLAIN, 13));
+        snSearchField.setBounds(xPos, 10, 200, 30);
+        snSearchField.addActionListener(e -> doSearch()); // 回车搜索
+        topPanel.add(snSearchField);
+        xPos += 210;
+
+        // 工位号搜索(仅"所有工位"模式显示)
+        if (isAllStation) {
+            oprnoLabel = new JLabel("工位:");
+            oprnoLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
+            oprnoLabel.setBounds(xPos, 10, 40, 30);
+            topPanel.add(oprnoLabel);
+            xPos += 42;
+
+            oprnoSearchField = new JTextField();
+            oprnoSearchField.setFont(new Font("微软雅黑", Font.PLAIN, 13));
+            oprnoSearchField.setBounds(xPos, 10, 100, 30);
+            oprnoSearchField.addActionListener(e -> doSearch()); // 回车搜索
+            topPanel.add(oprnoSearchField);
+            xPos += 110;
+        }
+
+        // 查询按钮
+        btnSearch = new JButton("查询");
+        btnSearch.setFont(new Font("微软雅黑", Font.PLAIN, 13));
+        btnSearch.setBounds(xPos, 10, 70, 30);
+        btnSearch.addActionListener(e -> doSearch());
+        topPanel.add(btnSearch);
+        xPos += 80;
+
+        // 刷新按钮
+        btnRefresh = new JButton("刷新");
+        btnRefresh.setFont(new Font("微软雅黑", Font.PLAIN, 13));
+        btnRefresh.setBounds(xPos, 10, 70, 30);
+        btnRefresh.addActionListener(e -> {
+            clearSearchFields();
+            refreshData();
+        });
+        topPanel.add(btnRefresh);
+        xPos += 80;
+
+        // 上一页按钮
+        btnPrevious = new JButton("上一页");
+        btnPrevious.setFont(new Font("微软雅黑", Font.PLAIN, 13));
+        btnPrevious.setBounds(xPos, 10, 80, 30);
+        btnPrevious.addActionListener(e -> {
+            if (pageNo > 1) {
+                pageNo--;
+                loadData();
+            }
+        });
+        topPanel.add(btnPrevious);
+        xPos += 85;
+
+        // 页码显示
+        pageLabel = new JLabel("0/0");
+        pageLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
+        pageLabel.setHorizontalAlignment(SwingConstants.CENTER);
+        pageLabel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
+        pageLabel.setBounds(xPos, 10, 70, 30);
+        topPanel.add(pageLabel);
+        xPos += 75;
+
+        // 下一页按钮
+        btnNext = new JButton("下一页");
+        btnNext.setFont(new Font("微软雅黑", Font.PLAIN, 13));
+        btnNext.setBounds(xPos, 10, 80, 30);
+        btnNext.addActionListener(e -> {
+            if (pageNo < totalPages) {
+                pageNo++;
+                loadData();
+            }
+        });
+        topPanel.add(btnNext);
+
+        // 表格面板
+        JPanel tablePanel = new JPanel();
+        tablePanel.setBounds(0, 50, 1200, 518);
+        tablePanel.setLayout(new GridLayout(1, 1, 0, 0));
+        add(tablePanel);
+
+        // 创建表格模型
+        tableModel = new DefaultTableModel(COLUMN_NAMES, 0) {
+            @Override
+            public boolean isCellEditable(int row, int column) {
+                return false;
+            }
+        };
+
+        // 创建表格
+        table = new JTable(tableModel);
+        table.setRowHeight(32);
+        table.setFont(new Font("微软雅黑", Font.PLAIN, 13));
+        table.getTableHeader().setFont(new Font("微软雅黑", Font.BOLD, 14));
+        table.getTableHeader().setReorderingAllowed(false);
+
+        // 设置列宽
+        setColumnWidths();
+
+        // 设置居中渲染器
+        DefaultTableCellRenderer centerRenderer = new DefaultTableCellRenderer();
+        centerRenderer.setHorizontalAlignment(JLabel.CENTER);
+        for (int i = 0; i < table.getColumnCount(); i++) {
+            table.getColumnModel().getColumn(i).setCellRenderer(centerRenderer);
+        }
+
+        // 添加滚动面板
+        JScrollPane scrollPane = new JScrollPane(table);
+        tablePanel.add(scrollPane);
+
+        // 添加组件大小变化监听器
+        addComponentListener(new ComponentAdapter() {
+            @Override
+            public void componentResized(ComponentEvent e) {
+                int w = getWidth();
+                int h = getHeight();
+
+                // 调整顶部面板宽度
+                topPanel.setBounds(0, 0, w, 50);
+
+                // 调整表格面板
+                if (h > 50) {
+                    tablePanel.setBounds(0, 50, w, h - 50);
+                }
+
+                topPanel.revalidate();
+                tablePanel.revalidate();
+            }
+        });
+    }
+
+    /**
+     * 设置列宽
+     */
+    private void setColumnWidths() {
+        TableColumn column;
+        // 工件码
+        column = table.getColumnModel().getColumn(0);
+        column.setPreferredWidth(260);
+        // 工位号
+        column = table.getColumnModel().getColumn(1);
+        column.setPreferredWidth(90);
+        // 工位结果
+        column = table.getColumnModel().getColumn(2);
+        column.setPreferredWidth(70);
+        // 作业人员
+        column = table.getColumnModel().getColumn(3);
+        column.setPreferredWidth(100);
+        // 日期
+        column = table.getColumnModel().getColumn(4);
+        column.setPreferredWidth(150);
+    }
+
+    /**
+     * 清空搜索框
+     */
+    private void clearSearchFields() {
+        snSearchField.setText("");
+        if (oprnoSearchField != null) {
+            oprnoSearchField.setText("");
+        }
+    }
+
+    /**
+     * 执行搜索
+     */
+    private void doSearch() {
+        pageNo = 1;
+        loadData();
+    }
+
+    /**
+     * 刷新数据(重置到第一页)
+     */
+    public void refreshData() {
+        pageNo = 1;
+        loadData();
+    }
+
+    /**
+     * 加载数据
+     */
+    public void loadData() {
+        // 获取搜索条件
+        String searchSn = snSearchField.getText().trim();
+        String searchOprno = oprno; // 默认使用构造函数传入的工位号
+
+        // 如果是"所有工位"模式,检查是否有工位号搜索条件
+        if (isAllStation && oprnoSearchField != null) {
+            String inputOprno = oprnoSearchField.getText().trim();
+            if (!inputOprno.isEmpty()) {
+                searchOprno = inputOprno;
+            }
+        }
+
+        final String finalSearchOprno = searchOprno;
+        final String finalSearchSn = searchSn;
+
+        // 在后台线程中执行数据加载
+        SwingWorker<WorkRecordResp, Void> worker = new SwingWorker<WorkRecordResp, Void>() {
+            @Override
+            protected WorkRecordResp doInBackground() throws Exception {
+                // 更新状态
+                SwingUtilities.invokeLater(() -> {
+                    statusLabel.setText("正在加载...");
+                    setButtonsEnabled(false);
+                });
+
+                // 调用接口查询数据
+                return DataUtil.getWorkRecordList(finalSearchOprno, finalSearchSn, pageNo, PAGE_SIZE);
+            }
+
+            @Override
+            protected void done() {
+                try {
+                    WorkRecordResp resp = get();
+                    updateUI(resp, finalSearchOprno);
+                } catch (Exception e) {
+                    log.error("加载工作记录失败: {}", e.getMessage());
+                    updateUIError("加载失败: " + e.getMessage());
+                }
+            }
+        };
+        worker.execute();
+    }
+
+    /**
+     * 设置按钮启用状态
+     */
+    private void setButtonsEnabled(boolean enabled) {
+        btnSearch.setEnabled(enabled);
+        btnRefresh.setEnabled(enabled);
+        btnPrevious.setEnabled(enabled && pageNo > 1);
+        btnNext.setEnabled(enabled && pageNo < totalPages);
+    }
+
+    /**
+     * 更新UI显示数据
+     */
+    private void updateUI(WorkRecordResp resp, String queryOprno) {
+        // 清空表格
+        tableModel.setRowCount(0);
+
+        if (resp == null || !resp.isResult()) {
+            String msg = resp != null ? resp.getMessage() : "请求失败";
+            updateUIError(msg);
+            return;
+        }
+
+        // 更新分页信息
+        totalCount = resp.getCount();
+        totalPages = (int) Math.ceil((double) totalCount / PAGE_SIZE);
+        if (totalPages == 0) totalPages = 1;
+
+        // 更新页码显示
+        pageLabel.setText(pageNo + "/" + totalPages);
+
+        // 更新按钮状态
+        setButtonsEnabled(true);
+
+        // 更新状态标签
+        String oprnoDisplay;
+        if (isAllStation) {
+            oprnoDisplay = (queryOprno == null || queryOprno.isEmpty()) ? "所有工位" : queryOprno;
+        } else {
+            oprnoDisplay = oprno;
+        }
+        statusLabel.setText("工位:" + oprnoDisplay + " 共" + totalCount + "条");
+
+        // 填充表格数据
+        List<WorkRecordData> list = resp.getList();
+        if (list != null && !list.isEmpty()) {
+            for (WorkRecordData data : list) {
+                Object[] row = {
+                        data.getSn(),
+                        data.getOprno(),
+                        data.getContent(),
+                        data.getUpdateBy(),
+                        data.getUpdateDate()
+                };
+                tableModel.addRow(row);
+            }
+        }
+
+        // 刷新表格
+        table.repaint();
+    }
+
+    /**
+     * 更新UI显示错误信息
+     */
+    private void updateUIError(String message) {
+        statusLabel.setText("错误: " + message);
+        pageLabel.setText("0/0");
+        setButtonsEnabled(true);
+        btnPrevious.setEnabled(false);
+        btnNext.setEnabled(false);
+        tableModel.setRowCount(0);
+    }
+
+    /**
+     * 获取当前工位号
+     */
+    public String getOprno() {
+        return oprno;
+    }
+}

+ 63 - 0
src/com/mes/ui/WorkRecordResp.java

@@ -0,0 +1,63 @@
+package com.mes.ui;
+
+import java.util.List;
+
+/**
+ * 工作记录查询响应实体类
+ */
+public class WorkRecordResp {
+    private boolean result;
+    private String message;
+    private int pageNo;
+    private int pageSize;
+    private long count;
+    private List<WorkRecordData> list;
+
+    public boolean isResult() {
+        return result;
+    }
+
+    public void setResult(boolean result) {
+        this.result = result;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+
+    public int getPageNo() {
+        return pageNo;
+    }
+
+    public void setPageNo(int pageNo) {
+        this.pageNo = pageNo;
+    }
+
+    public int getPageSize() {
+        return pageSize;
+    }
+
+    public void setPageSize(int pageSize) {
+        this.pageSize = pageSize;
+    }
+
+    public long getCount() {
+        return count;
+    }
+
+    public void setCount(long count) {
+        this.count = count;
+    }
+
+    public List<WorkRecordData> getList() {
+        return list;
+    }
+
+    public void setList(List<WorkRecordData> list) {
+        this.list = list;
+    }
+}

+ 14 - 0
src/com/mes/util/Base64Utils.java

@@ -0,0 +1,14 @@
+package com.mes.util;
+
+import org.apache.commons.codec.binary.Base64;
+
+import java.nio.charset.StandardCharsets;
+
+public class Base64Utils {
+	public static String getBase64(String str) {
+        // 显式 UTF-8,避免打包 exe 后默认 GBK 导致中文用户名/密码 Base64 不一致
+        byte[] binaryData = str.getBytes(StandardCharsets.UTF_8);
+        String encodedString = Base64.encodeBase64String(binaryData);
+        return encodedString;
+    }
+}

+ 32 - 0
src/com/mes/util/DateLocalUtils.java

@@ -0,0 +1,32 @@
+package com.mes.util;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+public class DateLocalUtils {
+	public static SimpleDateFormat DATA_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//设置日期格式
+	public static SimpleDateFormat DATA_FORMAT1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+	
+	
+	public static String getCurrentTime() {
+		String currentTime = DATA_FORMAT.format(new Date());
+		return currentTime;
+	}
+	
+	public static String getCurrentTime1() {
+		String currentTime = DATA_FORMAT1.format(new Date());
+		return currentTime;
+	}
+
+	public static SimpleDateFormat DATA_FORMAT2 = new SimpleDateFormat("yyyy-MM-dd");
+	public static String getCurrentDate() {
+		String currentTime = DATA_FORMAT2.format(new Date());
+		return currentTime;
+	}
+
+	public static SimpleDateFormat DATA_FORMAT3 = new SimpleDateFormat("HH:mm:ss");
+	public static String getCurrentTimeHMS() {
+		String currentTime = DATA_FORMAT3.format(new Date());
+		return currentTime;
+	}
+}

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

@@ -0,0 +1,53 @@
+package com.mes.util;
+
+public class ErrorMsg {
+    public static String getErrorMsg(String processMsgRet,String lx){
+        String lmsg = "该工件本工位不可加工";
+        try{
+            if(processMsgRet.equalsIgnoreCase("OK")||processMsgRet.equalsIgnoreCase("NG")) {
+                lmsg = "该工件本工位已加工,结果:"+processMsgRet;
+            }else if(processMsgRet.equalsIgnoreCase("NE")) {
+                lmsg = "该工件未录入系统";
+            }else if(processMsgRet.equalsIgnoreCase("NN")) {
+                lmsg = "该工件跳过该工位";
+            }else if(processMsgRet.equalsIgnoreCase("QN")) {
+                lmsg = "该工件OP"+ lx+"0加工NG";
+            }else if(processMsgRet.equalsIgnoreCase("QD")) {
+                lmsg = "该工件OP"+ lx+"0未加工";
+            }else if(processMsgRet.equalsIgnoreCase("NF")) {
+                lmsg = "该工件已合格下线";
+            }else if(processMsgRet.equalsIgnoreCase("NR")) {
+                lmsg = "该工件离线返修中";
+            }else if(processMsgRet.equalsIgnoreCase("NB")) {
+                lmsg = "该工件已报废";
+            }else if(processMsgRet.equalsIgnoreCase("ND")) {
+                lmsg = "该工件NG待处理";
+            }else if(processMsgRet.equalsIgnoreCase("FN")) {
+                lmsg = "首件检查工件不合格停机";
+            }else if(processMsgRet.equalsIgnoreCase("GN")) {
+                lmsg = "更换配件首件检查不合格停机";
+            }else if(processMsgRet.equalsIgnoreCase("CS")) {
+                lmsg = "首件检查两小时内未检查,超时停机";
+            }else if(processMsgRet.equalsIgnoreCase("DJ")) {
+                lmsg = "未进行开班点检";
+            }else if(processMsgRet.equalsIgnoreCase("BM")) {
+                lmsg = "未绑定物料";
+            }else if(processMsgRet.equalsIgnoreCase("PL")) {
+                lmsg = "配件寿命不足";
+            }else if(processMsgRet.equalsIgnoreCase("QM")) {
+                lmsg = "两次气密必须间隔15分钟";
+            }else if(processMsgRet.equalsIgnoreCase("GS")) {
+                lmsg = "工件码格式不正确";
+            }else if(processMsgRet.equalsIgnoreCase("CF")) {
+                lmsg = "工件码重复";
+            }else if(processMsgRet.equalsIgnoreCase("AL")) {
+                lmsg = "该工件存在未处理的报警信息";
+            } else if (processMsgRet.equalsIgnoreCase("PZ")) {
+                lmsg = "该工件未拍照";
+            } else if (processMsgRet.equalsIgnoreCase("CS")) {
+                lmsg = "超时";
+            }
+        }catch (Exception e){ }
+        return lmsg;
+    }
+}

+ 157 - 0
src/com/mes/util/HttpUtils.java

@@ -0,0 +1,157 @@
+package com.mes.util;
+
+import java.io.*;
+import java.net.HttpURLConnection;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+public class HttpUtils {
+	/**
+	 * HTTP请求,正确报文解析,并保存数据到生产记录数据
+	 * @return 
+	 * @throws URISyntaxException
+	 */
+    //http post请求
+    public static String sendRequest(String urlParam) {
+		String requestType = "POST";
+        //根据接收内容返回数据结果
+    	String ret = "";
+    	System.out.println(urlParam);
+        HttpURLConnection con = null;
+        BufferedReader buffer = null;
+        StringBuffer resultBuffer = null;
+        try {
+            URL url = new URL(urlParam);
+            //得到连接对象
+            con = (HttpURLConnection) url.openConnection();
+            //设置请求类型
+            con.setRequestMethod(requestType);
+            //设置请求需要返回的数据类型和字符集类型
+            con.setRequestProperty("Content-Type", "application/json;charset=UTF-8");  
+            //允许写出
+            con.setDoOutput(true);
+            //允许读入
+            con.setDoInput(true);
+            //不使用缓存
+            con.setUseCaches(false);
+            //得到响应码
+            int responseCode = con.getResponseCode();
+            System.out.println("responseCode="+responseCode);
+            if(responseCode == HttpURLConnection.HTTP_OK){
+                //得到响应流
+                InputStream inputStream = con.getInputStream();
+                //将响应流转换成字符串
+                resultBuffer = new StringBuffer();
+                String line;
+                buffer = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
+                while ((line = buffer.readLine()) != null) {
+                    resultBuffer.append(line);
+                }
+                System.out.println(resultBuffer.toString());
+                ret = resultBuffer.toString();
+            }else {
+            	//ret = String.valueOf(responseCode);
+            	ret = "false";
+            }
+            con.disconnect();
+        }catch(Exception e) {
+        	//ret = false;
+        	//System.out.println("e.toString()="+e.toString());
+        	//return e.toString();
+            //e.printStackTrace();
+        	ret = "false";
+        }
+        return ret;
+    }
+
+
+    public static String sendPostRequest(String urlParam, String params) {
+        String requestType = "POST";
+        //根据接收内容返回数据结果
+        String ret = "";
+        System.out.println(urlParam);
+        HttpURLConnection con = null;
+        BufferedReader buffer = null;
+        StringBuffer resultBuffer = null;
+        try {
+            URL url = new URL(urlParam);
+            //得到连接对象
+            con = (HttpURLConnection) url.openConnection();
+            //设置请求类型
+            con.setRequestMethod(requestType);
+            //设置请求需要返回的数据类型和字符集类型
+            con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
+            //允许写出
+            con.setDoOutput(true);
+            //允许读入
+            con.setDoInput(true);
+            //不使用缓存
+            con.setUseCaches(false);
+
+            // 获取URLConnection对象对应的输出流
+            OutputStreamWriter out = new OutputStreamWriter( con.getOutputStream(),"UTF-8");// utf-8编码
+            // 发送请求参数
+            out.write(params);
+
+            //得到响应码
+            int responseCode = con.getResponseCode();
+            System.out.println("responseCode="+responseCode);
+            if(responseCode == HttpURLConnection.HTTP_OK){
+                //得到响应流
+                InputStream inputStream = con.getInputStream();
+                //将响应流转换成字符串
+                resultBuffer = new StringBuffer();
+                String line;
+                buffer = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
+                while ((line = buffer.readLine()) != null) {
+                    resultBuffer.append(line);
+                }
+                System.out.println(resultBuffer.toString());
+                ret = resultBuffer.toString();
+            }else {
+                //ret = String.valueOf(responseCode);
+                ret = "false";
+            }
+            con.disconnect();
+        }catch(Exception e) {
+            //ret = false;
+            //System.out.println("e.toString()="+e.toString());
+            //return e.toString();
+            //e.printStackTrace();
+            ret = "false";
+        }
+        return ret;
+    }
+
+    public static String sendPostRequestJson(String apiUrl, String jsonData) throws IOException {
+        URL url = new URL(apiUrl);
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod("POST");
+        conn.setRequestProperty("Content-Type", "application/json");
+        conn.setDoOutput(true);
+
+        // 发送请求数据
+        try (OutputStream os = conn.getOutputStream()) {
+            byte[] input = jsonData.getBytes("utf-8");
+            os.write(input, 0, input.length);
+        }
+
+        // 获取响应数据
+        StringBuilder response = new StringBuilder();
+        int responseCode = conn.getResponseCode();
+//        System.out.println("Sending POST request to URL: " + apiUrl);
+//        System.out.println("Post parameters: " + jsonData);
+//        System.out.println("Response Code: " + responseCode);
+
+        try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"))) {
+            String responseLine;
+            while ((responseLine = br.readLine()) != null) {
+                response.append(responseLine.trim());
+            }
+        }
+
+        conn.disconnect();
+        return response.toString();
+    }
+
+}

+ 216 - 0
src/com/mes/util/JdbcUtils.java

@@ -0,0 +1,216 @@
+package com.mes.util;
+
+import com.mes.ui.CmtReq;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+
+public class JdbcUtils {
+	public static final Logger log =  LoggerFactory.getLogger(JdbcUtils.class);
+	//通过上面的工具就可以获取到properties文件中的键值从而可以加载驱动 获取链接 从而 可以增删改查
+	public static Connection conn = null;
+    public static String Drivde="org.sqlite.JDBC";
+	public static String DATABASE_URL="jdbc:sqlite:mes_db.db";
+    
+    public static Connection getConn(){
+        try {
+            Class.forName(Drivde);// 加载驱动,连接sqlite的jdbc
+            conn = DriverManager.getConnection(DATABASE_URL);//连接数据库zhou.db,不存在则创建
+            System.out.println("连接到SQLite数据库成功!");
+            create_bw_record();//初始化结构表
+            create_cmt_data();//初始化焊接参数表
+        } catch (Exception e) {
+            // TODO Auto-generated catch block
+        	close();//关闭数据库连接
+            e.printStackTrace();
+        }
+        return conn;
+    }
+
+	public static void openConnection() {
+		try {
+			Class.forName(Drivde);// 加载驱动,连接sqlite的jdbc
+			conn = DriverManager.getConnection(DATABASE_URL);
+		} catch (ClassNotFoundException e) {
+			e.printStackTrace();
+		} catch (SQLException e) {
+			e.printStackTrace();
+			// 如果连接失败,尝试重连
+			reconnect();
+		}
+	}
+
+	private static void reconnect() {
+		try {
+			// 关闭旧连接
+			if (conn != null && !conn.isClosed()) {
+				conn.close();
+			}
+			// 重新建立连接
+			conn = DriverManager.getConnection(DATABASE_URL);
+		} catch (SQLException e) {
+			e.printStackTrace();
+			// 如果重连失败,可以进一步处理异常,比如记录日志、通知管理员等
+		}
+	}
+    
+    public static void create_bw_record() throws SQLException {
+    	Statement statement=conn.createStatement();   //创建连接对象,是Java的一个操作数据库的重要接口
+        //设备结构数据库
+        String sqlEquipment = "CREATE TABLE if not exists bw_record("
+        		+ "id INTEGER PRIMARY KEY AUTOINCREMENT,gw VARCHAR(20),gy VARCHAR(20),message_type VARCHAR(20),sn VARCHAR(48),bw VARCHAR(1000),record_time DATETIME,"
+        		+ "info_01 VARCHAR(200),info_02 VARCHAR(200),info_03 VARCHAR(200))";
+//        statement.executeUpdate("drop table if exists bw_record");//判断是否有表tables的存在。有则删除
+        statement.executeUpdate(sqlEquipment);
+
+		// 创建 提交记录表
+		String submitRecord = "CREATE TABLE if not exists submit_record(\n" +
+				"   id INTEGER PRIMARY KEY AUTOINCREMENT, -- 自增ID\n" +
+				"   oprno VARCHAR(20),                    -- 工位号 \n" +
+				"   sn VARCHAR(48),                       -- 二维码\n" +
+				"   bw VARCHAR(1000),                     -- 报文 \n" +
+				"   record_time DATETIME,                 -- 记录时间\n" +
+				"   state CHAR(1)      -- 状态(0 ->未提交, 1 ->已提交)\n" +
+				")";
+		statement.executeUpdate(submitRecord);
+
+        statement.close();
+    }
+    
+    //插入数据
+    public static boolean insertData(String gw, String gy, String bw, String message_type, String sn) {
+    	boolean ret = false;
+    	String record_time = DateLocalUtils.getCurrentTime();
+		try {
+			// 确保连接已经打开
+			if (JdbcUtils.conn == null || JdbcUtils.conn.isClosed()) {
+				JdbcUtils.openConnection();
+			}
+			//创建连接对象,是Java的一个操作数据库的重要接口
+			Statement statement=conn.createStatement();
+			statement.executeUpdate("INSERT INTO bw_record (gw,gy,bw,record_time,message_type,sn) VALUES"
+					+ " ('"+gw+"', '"+gy+"', '"+bw+"', '"+record_time+"','"+message_type+"','"+sn+"')");//向数据库中插入数据
+			statement.close();
+			ret = true;
+		} catch (SQLException e) {
+			// TODO Auto-generated catch block
+			//e.printStackTrace();
+			ret = false;
+		}
+
+		return ret;
+    }
+
+	// 向 submit_record表 插入提交记录数据
+	public static boolean insertSubmitRecord(String oprno, String sn, String bw){
+		boolean ret = false;
+		String record_time = DateLocalUtils.getCurrentTime();
+		try {
+			// 确保连接已经打开
+			if (JdbcUtils.conn == null || JdbcUtils.conn.isClosed()) {
+				JdbcUtils.openConnection();
+			}
+			Statement statement=conn.createStatement();
+			String insertSQL = "INSERT INTO submit_record (oprno, sn, bw, record_time, state)" +
+					"VALUES('" + oprno + "', '" + sn + "', '" + bw + "', '" + record_time + "', '0')";
+			statement.executeUpdate(insertSQL);
+			statement.close();
+			ret = true;
+			log.info("向submit_record表插入数据成功: {}", insertSQL);
+		} catch (SQLException e) {
+			ret = false;
+			log.info("向submit_record表插入数据失败");
+		}
+		return ret;
+	}
+
+	public static void create_cmt_data() throws SQLException {
+		Statement statement = conn.createStatement();
+		String sqlEquipment = "CREATE TABLE if not exists bw_cmt("
+				+ "id INTEGER PRIMARY KEY AUTOINCREMENT,oprno VARCHAR(20),line_sn VARCHAR(20),"
+				+ "sn VARCHAR(50),content MEDIUMTEXT,create_time DATETIME,sync int(10) NULL DEFAULT 0"
+				+ ")";
+		statement.executeUpdate(sqlEquipment);
+		statement.close();
+	}
+
+	public static boolean insertCmtData(String oprno, String line_sn, String sn, String content) {
+		boolean ret = false;
+		String record_time = DateLocalUtils.getCurrentTime();
+		try {
+			if (JdbcUtils.conn == null || JdbcUtils.conn.isClosed()) {
+				JdbcUtils.openConnection();
+			}
+			// 转义单引号,避免 JSON content 破坏 SQL
+			String safeOprno = oprno == null ? "" : oprno.replace("'", "''");
+			String safeLine = line_sn == null ? "" : line_sn.replace("'", "''");
+			String safeSn = sn == null ? "" : sn.replace("'", "''");
+			String safeContent = content == null ? "" : content.replace("'", "''");
+			Statement statement = conn.createStatement();
+			statement.executeUpdate("INSERT INTO bw_cmt (oprno,line_sn,sn,content,create_time) VALUES"
+					+ " ('" + safeOprno + "', '" + safeLine + "', '" + safeSn + "', '" + safeContent + "','" + record_time + "')");
+			statement.close();
+			ret = true;
+		} catch (SQLException e) {
+			ret = false;
+			e.printStackTrace();
+		}
+		return ret;
+	}
+
+	public static void updateProdSync(Integer id, Integer sync) throws SQLException {
+		Statement statement = conn.createStatement();
+		statement.executeUpdate("update bw_cmt set sync = " + sync + " where id = " + id);
+		statement.close();
+	}
+
+	public static List<CmtReq> getCmtParams() {
+		String sql = "select id,oprno,line_sn,sn,content,sync from bw_cmt where sync = 0 order by id asc limit 100";
+		List<CmtReq> prods = new ArrayList<>();
+		try {
+			if (JdbcUtils.conn == null || JdbcUtils.conn.isClosed()) {
+				JdbcUtils.openConnection();
+			}
+			Statement stmt = conn.createStatement();
+			ResultSet ret = stmt.executeQuery(sql);
+			while (ret.next()) {
+				CmtReq cmtReq = new CmtReq();
+				cmtReq.setId(ret.getInt(1));
+				cmtReq.setOprno(ret.getString(2));
+				cmtReq.setLineSn(ret.getString(3));
+				cmtReq.setSn(ret.getString(4));
+				cmtReq.setContent(ret.getString(5));
+				cmtReq.setSync(ret.getInt(6));
+				prods.add(cmtReq);
+			}
+			ret.close();
+			stmt.close();
+		} catch (SQLException e) {
+			e.printStackTrace();
+		}
+		return prods;
+	}
+
+    
+    public static void close(){
+        try {
+        	if(conn!=null) {
+        		conn.close();
+        	}
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+    }
+    
+    
+
+}
+ 
+

+ 13 - 0
src/resources/config/config.properties

@@ -0,0 +1,13 @@
+mes.gw=OP070A
+mes.gw1=OP070A
+mes.gw2=OP080A
+mes.plc_ip=192.168.1.1
+#mes.server_ip=127.0.0.1
+mes.server_ip=192.168.9.180
+mes.line_sn=115XT
+# 设备状态定时上报间隔(分钟)
+mes.device.heartbeat.minutes=5
+# 免扫码自动允许启动工位(逗号分隔)。这些工位不扫码、不采数、不提交结果,直接写允许启动
+# 示例:mes.auto_allow_stations=OP070A  或多个:OP070A,OP080A
+# 留空则全部工位走正常扫码/采数/提交流程
+mes.auto_allow_stations=

+ 44 - 0
src/resources/logback.xml

@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<configuration>
+
+    <!-- 定义日志文件的存储位置及文件名 -->
+    <property name="LOG_HOME" value="D:/Logs"/>
+    <property name="PROJECT_NAME" value="log"/>
+
+    <!-- 控制台输出 -->
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level  %logger{36} - %msg%n</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>INFO</level>
+        </filter>
+    </appender>
+
+    <!-- 文件输出 -->
+    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${LOG_HOME}/${PROJECT_NAME}/${PROJECT_NAME}.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${LOG_HOME}/${PROJECT_NAME}/archived/${PROJECT_NAME}_%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 保留最近 1个月内的日志-->
+            <maxHistory>30</maxHistory>  <!-- 如果删除该项配置,则logback无法自动删除旧日志, 导致totalSizeCap也将失去作用-->
+            <!-- 滚动文件总大小不超过1GB-->
+            <totalSizeCap>1GB</totalSizeCap>
+        </rollingPolicy>
+        <encoder>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level  %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <!-- 根Logger -->
+    <root level="info">
+        <appender-ref ref="STDOUT" />
+        <appender-ref ref="FILE" />
+    </root>
+
+    <logger name="org.slf4j" level="ERROR"/>
+    <logger name="ch.qos.logback" level="ERROR"/>
+    <logger name="ch.qos.logback.classic.joran.action.ConfigurationAction" level="OFF"/>
+
+</configuration>