wangxichen 1 неделя назад
Сommit
c7137cf79e
100 измененных файлов с 1797 добавлено и 0 удалено
  1. 32 0
      .classpath
  2. 7 0
      .gitignore
  3. 0 0
      .lock
  4. 28 0
      .project
  5. BIN
      config/liuzhuanka_template.pdf
  6. 8 0
      config/print.properties
  7. 2 0
      image/.gitignore
  8. BIN
      image/background.png
  9. BIN
      image/bg/a_side.png
  10. BIN
      image/bg/b_side.png
  11. BIN
      image/bg/bar_add.png
  12. BIN
      image/bg/bar_bookmark.png
  13. BIN
      image/bg/bar_cmd_go.png
  14. BIN
      image/bg/bar_edit.png
  15. BIN
      image/bg/bar_pause.png
  16. BIN
      image/bg/bar_sampling.png
  17. BIN
      image/bg/bar_setting.png
  18. BIN
      image/bg/bar_stop.png
  19. BIN
      image/bg/close_bt.png
  20. BIN
      image/bg/company_setting_logo.png
  21. BIN
      image/bg/delete.png
  22. BIN
      image/bg/department_setting_logo.png
  23. BIN
      image/bg/download.png
  24. BIN
      image/bg/equipment_setting_logo.png
  25. BIN
      image/bg/gj_001_1_20.png
  26. BIN
      image/bg/gj_001_21_40.png
  27. BIN
      image/bg/gj_001_41_60.png
  28. BIN
      image/bg/green_dot.png
  29. BIN
      image/bg/grey_dot.png
  30. BIN
      image/bg/icon.png
  31. BIN
      image/bg/inductance_logo.png
  32. BIN
      image/bg/insulation_logo.png
  33. BIN
      image/bg/login.png
  34. BIN
      image/bg/logo.png
  35. BIN
      image/bg/logoff.png
  36. BIN
      image/bg/menu_data_analysis.png
  37. BIN
      image/bg/menu_data_preprocess.png
  38. BIN
      image/bg/menu_file.png
  39. BIN
      image/bg/menu_setting.png
  40. BIN
      image/bg/min_bt.png
  41. BIN
      image/bg/ng_bg.png
  42. BIN
      image/bg/ok_bg.png
  43. BIN
      image/bg/open_file.png
  44. BIN
      image/bg/refresh.png
  45. BIN
      image/bg/reset_logo.png
  46. BIN
      image/bg/resistance_logo.png
  47. BIN
      image/bg/save_bg.png
  48. BIN
      image/bg/scan_barcode.png
  49. BIN
      image/bg/tree_folder_icon.png
  50. BIN
      image/bg/tree_leaf_icon.png
  51. BIN
      image/bg/user.png
  52. BIN
      image/bg/vertical_line.png
  53. BIN
      image/bg/zoom_in.png
  54. BIN
      image/bg/zoom_out.png
  55. BIN
      image/close_bt.png
  56. 0 0
      image/cmd.txt
  57. BIN
      image/icon.ico
  58. BIN
      image/info_bt.png
  59. BIN
      image/min_bt.png
  60. BIN
      lib/commons-codec-1.15.jar
  61. BIN
      lib/commons-logging-1.2.jar
  62. BIN
      lib/core-3.5.1.jar
  63. BIN
      lib/fastjson2-2.0.16.jar
  64. BIN
      lib/fontbox-2.0.30.jar
  65. BIN
      lib/gson-2.10.jar
  66. BIN
      lib/iot-communication-1.4.4.jar
  67. BIN
      lib/jSerialComm-2.6.2.jar
  68. BIN
      lib/javase-3.5.1.jar
  69. BIN
      lib/jfreechart-1.5.4.jar
  70. BIN
      lib/jshortcut-0.4-oberzalek.jar
  71. BIN
      lib/kotlin-stdlib-1.8.10.jar
  72. BIN
      lib/logback-classic-1.2.13.jar
  73. BIN
      lib/logback-core-1.2.13.jar
  74. BIN
      lib/netty-all-4.1.48.Final.jar
  75. BIN
      lib/okhttp-4.10.0.jar
  76. BIN
      lib/okio-jvm-3.3.0.jar
  77. BIN
      lib/pdfbox-2.0.30.jar
  78. BIN
      lib/s7connector-2.1.jar
  79. BIN
      lib/slf4j-api-1.7.36.jar
  80. BIN
      lib/snmp4j-3.7.7.jar
  81. BIN
      lib/sqlite-jdbc-3.36.0.3.jar
  82. 10 0
      src/com/mes/component/CallFromJs.java
  83. 57 0
      src/com/mes/component/MesRadio.java
  84. 44 0
      src/com/mes/component/MesWebView.java
  85. 30 0
      src/com/mes/component/MyDialog.java
  86. 91 0
      src/com/mes/component/QRCodeDialog.java
  87. 106 0
      src/com/mes/netty/MesMsgUtils.java
  88. 63 0
      src/com/mes/netty/NettyClient.java
  89. 82 0
      src/com/mes/netty/NettyClientHandler.java
  90. 68 0
      src/com/mes/netty/ProtocolParam.java
  91. 176 0
      src/com/mes/netty/XDecoder.java
  92. 530 0
      src/com/mes/print/CoordinateAdjustTool.java
  93. 152 0
      src/com/mes/print/PrintHelper.java
  94. 100 0
      src/com/mes/print/Printer.java
  95. 17 0
      src/com/mes/print/PrinterException.java
  96. 51 0
      src/com/mes/print/PrinterFactory.java
  97. 48 0
      src/com/mes/print/TestMain.java
  98. 40 0
      src/com/mes/print/TestPrintPreview.java
  99. 55 0
      src/com/mes/print/TestSnmp.java
  100. 0 0
      src/com/mes/print/config/PrinterConfig.java

+ 32 - 0
.classpath

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="image"/>
+	<classpathentry excluding="resources/" kind="src" path="src"/>
+	<classpathentry kind="src" path="src/resources"/>
+	<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="lib" path="lib/logback-core-1.2.13.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/logback-classic-1.2.13.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/jSerialComm-2.6.2.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"/>
+	<classpathentry kind="lib" path="lib/okio-jvm-3.3.0.jar"/>
+	<classpathentry kind="lib" path="lib/s7connector-2.1.jar"/>
+	<classpathentry kind="lib" path="lib/slf4j-api-1.7.36.jar"/>
+	<classpathentry kind="lib" path="lib/sqlite-jdbc-3.36.0.3.jar"/>
+	<classpathentry kind="lib" path="lib/mes-print.jar"/>
+	<classpathentry kind="lib" path="lib/pdfbox-2.0.30.jar"/>
+	<classpathentry kind="lib" path="lib/fontbox-2.0.30.jar"/>
+	<classpathentry kind="lib" path="lib/commons-logging-1.2.jar"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>

+ 7 - 0
.gitignore

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


+ 28 - 0
.project

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>mesclient-op130A</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>1757488872243</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>

BIN
config/liuzhuanka_template.pdf


+ 8 - 0
config/print.properties

@@ -0,0 +1,8 @@
+# 打印机配置文件
+# printer.type: hp=HP激光(流转卡) | tsc=TSC热敏(标签) | none=不打印
+# printer.name: 打印机名称(Windows 设备管理器显示名称)
+# printer.paper: A4 | A5(仅 hp 类型有效,流转卡模板是 A5 设计)
+
+printer.type=hp
+printer.name=HP-3004dn
+printer.paper=A5

+ 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/commons-logging-1.2.jar


BIN
lib/core-3.5.1.jar


BIN
lib/fastjson2-2.0.16.jar


BIN
lib/fontbox-2.0.30.jar


BIN
lib/gson-2.10.jar


BIN
lib/iot-communication-1.4.4.jar


BIN
lib/jSerialComm-2.6.2.jar


BIN
lib/javase-3.5.1.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/pdfbox-2.0.30.jar


BIN
lib/s7connector-2.1.jar


BIN
lib/slf4j-api-1.7.36.jar


BIN
lib/snmp4j-3.7.7.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);
+    }
+}

+ 91 - 0
src/com/mes/component/QRCodeDialog.java

@@ -0,0 +1,91 @@
+package com.mes.component;
+
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.EncodeHintType;
+import com.google.zxing.WriterException;
+import com.google.zxing.common.BitMatrix;
+import com.google.zxing.qrcode.QRCodeWriter;
+import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.util.HashMap;
+import java.util.Map;
+
+public class QRCodeDialog extends JDialog {
+    
+    private static final int QR_CODE_SIZE = 400; // 二维码尺寸
+
+    public QRCodeDialog(JFrame owner, String title, String content){
+        super(owner, title, true);
+        init(content, owner);
+    }
+
+    private void init(String content, JFrame owner){
+        Container container = this.getContentPane();
+        container.setLayout(new BorderLayout());
+        container.setBackground(Color.WHITE);
+        
+        // 生成二维码图片
+        BufferedImage qrImage = generateQRCode(content);
+        
+        if(qrImage != null){
+            // 显示二维码
+            JLabel imageLabel = new JLabel(new ImageIcon(qrImage));
+            imageLabel.setHorizontalAlignment(SwingConstants.CENTER);
+            container.add(imageLabel, BorderLayout.CENTER);
+            
+            // 显示文本内容
+            JLabel textLabel = new JLabel(content);
+            textLabel.setHorizontalAlignment(SwingConstants.CENTER);
+            textLabel.setFont(new Font("微软雅黑", Font.PLAIN, 18));
+            textLabel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+            container.add(textLabel, BorderLayout.SOUTH);
+        }else{
+            // 如果生成失败,显示错误信息
+            JLabel errorLabel = new JLabel("二维码生成失败");
+            errorLabel.setHorizontalAlignment(SwingConstants.CENTER);
+            errorLabel.setForeground(Color.RED);
+            errorLabel.setFont(new Font("微软雅黑", Font.PLAIN, 24));
+            container.add(errorLabel, BorderLayout.CENTER);
+        }
+        
+        this.setSize(QR_CODE_SIZE + 50, QR_CODE_SIZE + 100);
+        this.setLocationRelativeTo(owner);
+        this.setResizable(false);
+        this.setVisible(true);
+    }
+
+    /**
+     * 生成二维码图片
+     * @param content 二维码内容
+     * @return BufferedImage
+     */
+    private BufferedImage generateQRCode(String content) {
+        try {
+            // 设置二维码参数
+            Map<EncodeHintType, Object> hints = new HashMap<>();
+            hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); // 容错级别
+            hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); // 字符编码
+            hints.put(EncodeHintType.MARGIN, 1); // 边距
+
+            // 生成二维码矩阵
+            QRCodeWriter qrCodeWriter = new QRCodeWriter();
+            BitMatrix bitMatrix = qrCodeWriter.encode(content, BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, hints);
+
+            // 转换为图片
+            BufferedImage image = new BufferedImage(QR_CODE_SIZE, QR_CODE_SIZE, BufferedImage.TYPE_INT_RGB);
+            for (int x = 0; x < QR_CODE_SIZE; x++) {
+                for (int y = 0; y < QR_CODE_SIZE; y++) {
+                    image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
+                }
+            }
+            return image;
+        } catch (WriterException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+}
+

+ 106 - 0
src/com/mes/netty/MesMsgUtils.java

@@ -0,0 +1,106 @@
+package com.mes.netty;
+
+public class MesMsgUtils {
+
+    // 协议 v2(2026-06-29):oprno 字段从 6 字符扩到 8 字符,所有长度 +2
+    // 协议 v3(2026-07-13):LX 字段从 2 字符扩到 3 字符(支持 INSPXXX),AQDW/MQDW 长度 +1
+    public static int SYNR_LEN = 48;
+    public static int AXTW_LEN = 48;
+    public static int ACLW_LEN = 48;
+    public static int MCJW_LEN = 98;
+    public static int AQDW_LEN = 99;
+    public static int MBDW_LEN = 98;
+    public static int MJBW_LEN = 98;
+    public static int MQDW_LEN = 99;
+    public static int MKSW_LEN = 96;
+    public static int MSBW_LEN = 96;
+    public static int MCSW_LEN = 96;
+    public static int AQRW_LEN = 96;
+
+    public static String MSG_TYPE[] = {
+        "SYNR",
+        "AXTW",
+        "ACLW",
+        "MCJW",
+        "AQDW",
+        "MBDW",
+        "MJBW",
+        "MQDW",
+        "MKSW",
+        "MSBW",
+        "MCSW",
+        "AQRW",
+    };
+
+    public static int isMsgContentOK(String msg, String msg_type) {
+        //ret=0OK,1 空内容,2 长度不符
+        int ret = 0;
+        if(msg==null||msg.equalsIgnoreCase("")) {
+            ret = 1;
+            return ret;
+        }
+
+        int len = msg.length();
+        //System.out.println("len="+len);
+        switch(msg_type) {
+            case "SYNR":
+            case "AXTW":
+            case "ACLW":
+                if(len==SYNR_LEN) {
+                    ret = 0;
+                }else {
+                    ret = 2;
+                }
+                break;
+            case "AQDW":
+                if(len==AQDW_LEN) {
+                    ret = 0;
+                }else {
+                    ret = 2;
+                }
+                break;
+            case "MQDW":
+                if(len==MQDW_LEN) {
+                    ret = 0;
+                }else {
+                    ret = 2;
+                }
+                break;
+            default:
+                if(len==MCJW_LEN) {
+                    ret = 0;
+                }else {
+                    ret = 2;
+                }
+                break;
+        }
+        return ret;
+    }
+
+    // 判断报文类型是否在清单里
+    public static boolean isMsgTypeOk(String msg_type) {
+        for (String str : MSG_TYPE) {
+            if (str.equals(msg_type)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    // 处理报文数据
+    public static String processMsg(String msg, String msg_type) {
+        String processMsgRet = "";
+        if(msg_type.equalsIgnoreCase("SYNR")) {
+
+        }else if(msg_type.equalsIgnoreCase("AXTW")) {
+
+        }else if(msg_type.equalsIgnoreCase("ACLW")) {
+
+        }else{
+            processMsgRet = ProtocolParam.getResult(msg);
+        }
+        return processMsgRet;
+    }
+
+
+}

+ 63 - 0
src/com/mes/netty/NettyClient.java

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

+ 82 - 0
src/com/mes/netty/NettyClientHandler.java

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

+ 68 - 0
src/com/mes/netty/ProtocolParam.java

@@ -0,0 +1,68 @@
+package com.mes.netty;
+
+
+// 固定格式报文各参数获取方法
+// 协议 v2(2026-06-29 修订):oprno 字段从 6 字符扩到 8 字符,后续字段偏移整体 +2
+// 协议 v3(2026-07-13 修订):LX 字段从 2 字符扩到 3 字符(支持 INSPXXX),后续字段偏移整体 +1
+public class ProtocolParam {
+    // bbbbfffffARWAQDWGWOP100   GY100000ID151245P00000106200123062900001      RSOKDA2023-09-07ZT10:16:58
+    public static Integer fixedLength = 99;
+
+    public static String getMsgType(String msg){
+        System.out.print(msg);
+        if(msg.length() < 16){
+            return "";
+        }
+        return msg.substring(12,16);
+    }
+
+    public static String getOprno(String msg){
+        if(msg.length() < 26){
+            return "";
+        }
+        return msg.substring(18,26);
+    }
+
+    public static String getCraft(String msg){
+        if(msg.length() < 34){
+            return "";
+        }
+        return msg.substring(28,34);
+    }
+
+    public static String getSn(String msg){
+        if(msg.length() < 72){
+            return "";
+        }
+        return msg.substring(36,72);
+    }
+
+    public static String getLx(String msg){
+        if(msg.length() < 75){
+            return "";
+        }
+        return msg.substring(72,75).trim();
+    }
+
+    public static String getResult(String msg){
+        if(msg.length() < 77){
+            return "";
+        }
+        return msg.substring(75,77);
+    }
+
+    public static String getDay(String msg){
+        if(msg.length() < 89){
+            return "";
+        }
+        return msg.substring(79,89);
+    }
+
+    public static String getTime(String msg){
+        if(msg.length() < 99){
+            return "";
+        }
+        return msg.substring(91,99);
+    }
+
+}

+ 176 - 0
src/com/mes/netty/XDecoder.java

@@ -0,0 +1,176 @@
+package com.mes.netty;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.ByteToMessageDecoder;
+
+import java.util.List;
+
+public class XDecoder extends ByteToMessageDecoder {
+    static final int PACKET_SIZE = 46; // 最短包长度
+    static final int PACKET_MAX_SIZE = 1000; // 最长包长度
+
+    // 用来临时保留没有处理过的请求报文
+    ByteBuf tempMsg = Unpooled.buffer();
+
+    /**
+     * @param ctx
+     * @param in 请求的数据
+     * @param out 将粘在一起的报文拆分后的结果保留起来
+     * @throws Exception
+     */
+    @Override
+    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
+        System.out.println(Thread.currentThread() + "收到了一次数据包,长度是:" + in.readableBytes());
+        String content = hexStringToAscii(ByteBufUtil.hexDump(in));
+        System.out.println("接收包内容:" + hexStringToAscii(ByteBufUtil.hexDump(in)));
+
+        // 合并报文
+        ByteBuf message = null;
+        int tmpMsgSize = tempMsg.readableBytes();
+        // 如果暂存有上一次余下的请求报文,则合并
+        if (tmpMsgSize > 0) {
+            message = Unpooled.buffer();
+            message.writeBytes(tempMsg);
+            message.writeBytes(in);
+            System.out.println("合并:上一数据包余下的长度为:" + tmpMsgSize + ",合并后长度为:" + message.readableBytes());
+            System.out.println("合并后包内容:" + hexStringToAscii(ByteBufUtil.hexDump(message)));
+        } else {
+            message = in;
+        }
+
+//        System.out.println(Thread.currentThread() + "收到了一次数据包,长度是:" + message.readableBytes());
+        int size = 0;
+        String split = "bbbbfffffARW";
+
+        while (true){
+            size = message.readableBytes();
+            if(size >= PACKET_SIZE){
+                String retType = "NG:";
+                int qcsize = 0;
+                String str = hexStringToAscii(ByteBufUtil.hexDump(message));
+                String[] lists = str.split(split);
+
+                if(lists.length == 1 && size > PACKET_MAX_SIZE){ // 大于最大包长度
+                    byte[] request = new byte[size - PACKET_MAX_SIZE];
+                    // 每次从总的消息中读取n个字节的数据
+                    message.readBytes(request);
+                    // 将拆分后的结果放入out列表中,交由后面的业务逻辑去处理
+                    out.add(retType+hexStringToAscii(ByteBufUtil.hexDump(Unpooled.copiedBuffer(request))));
+                    break;
+                }
+
+                if(lists[0].length() > 0){
+                    byte[] request = new byte[lists[0].length()];
+                    // 每次从总的消息中读取n个字节的数据
+                    message.readBytes(request);
+                    // 将拆分后的结果放入out列表中,交由后面的业务逻辑去处理
+                    out.add(retType+hexStringToAscii(ByteBufUtil.hexDump(Unpooled.copiedBuffer(request))));
+                }else{
+                    if(lists.length > 1){
+                        String ss = lists[1];
+                        if(ss.length() <= 4){
+                            qcsize += ss.length();
+                        }else{
+                            String msgType = ProtocolParam.getMsgType(split+ss);
+                            if(!MesMsgUtils.isMsgTypeOk(msgType)){
+                                qcsize += split.length() + ss.length();
+                            }else{
+                                Integer ppsize = checkResultSize(ss,msgType,split);
+                                if(ppsize > 0){
+                                    qcsize = ppsize;
+                                    retType = "OK:";
+                                }else{
+                                    qcsize += split.length() + ss.length();
+                                }
+                            }
+                        }
+                    }
+                }
+
+
+                if(qcsize > 0){
+                    byte[] request = new byte[qcsize];
+                    // 每次从总的消息中读取n个字节的数据
+                    message.readBytes(request);
+                    // 将拆分后的结果放入out列表中,交由后面的业务逻辑去处理
+                    out.add(retType+hexStringToAscii(ByteBufUtil.hexDump(Unpooled.copiedBuffer(request))));
+                }
+            }else{
+                break;
+            }
+        }
+
+        // 多余的报文存起来
+        // 第一个报文: i+  暂存
+        // 第二个报文: 1 与第一次
+        size = message.readableBytes();
+        if (size != 0) {
+            System.out.println("多余的数据长度:" + size);
+            // 剩下来的数据放到tempMsg暂存
+            tempMsg.clear();
+            tempMsg.writeBytes(message.readBytes(size));
+        }
+    }
+
+    private Integer checkResultSize(String str, String msg_type, String split){
+        String oldstr = str;
+        str = split+str;
+        int tpsize = 0;
+        switch(msg_type) {
+            case "SYNR": // 同步
+                if(str.length() >= MesMsgUtils.SYNR_LEN){
+                    tpsize = MesMsgUtils.SYNR_LEN;
+                }
+                break;
+            case "AXTW": // 心跳
+                if(str.length() >= MesMsgUtils.AXTW_LEN){
+                    tpsize = MesMsgUtils.AXTW_LEN;
+                }
+                break;
+            case "ACLW": // 重连
+                if(str.length() >= MesMsgUtils.ACLW_LEN){
+                    tpsize = MesMsgUtils.ACLW_LEN;
+                }
+                break;
+            case "AQDW":
+                if(str.length() >= MesMsgUtils.AQDW_LEN){
+                    tpsize = MesMsgUtils.AQDW_LEN;
+                }
+                break;
+            case "MQDW":
+                if(str.length() >= MesMsgUtils.MQDW_LEN){
+                    tpsize = MesMsgUtils.MQDW_LEN;
+                }
+                break;
+            case "MCJW":
+            case "MBDW":
+            case "MJBW":
+            case "MKSW":
+            case "MSBW":
+            case "MCSW":
+            case "AQRW":
+            default: // default new packet
+                if(str.length() >= MesMsgUtils.MCJW_LEN){
+                    tpsize = MesMsgUtils.MCJW_LEN;
+                }
+                break;
+        }
+
+        return tpsize;
+    }
+
+    // 16字符串转Ascii
+    private String hexStringToAscii(String hexString) {
+        StringBuilder sbuilder = new StringBuilder();
+        for (int i = 0; i < hexString.length(); i += 2) {
+            String hexByte = hexString.substring(i, i + 2);
+            int byteValue = Integer.parseInt(hexByte, 16);
+            char c = (char) byteValue;
+            sbuilder.append(c);
+        }
+        return sbuilder.toString();
+    }
+}

+ 530 - 0
src/com/mes/print/CoordinateAdjustTool.java

@@ -0,0 +1,530 @@
+package com.mes.print;
+
+import com.mes.print.model.FlowCardData;
+import com.mes.print.template.FlowCardRenderer;
+
+import javax.imageio.ImageIO;
+import javax.swing.*;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.File;
+
+/**
+ * 流转卡坐标调整工具
+ * 可视化调整二维码和文字位置
+ */
+public class CoordinateAdjustTool extends JFrame {
+
+    // 当前坐标(初始值从 FlowCardRenderer 获取)
+    private float qrX = 194.1f;
+    private float qrY = 63.3f;
+    private float qrW = 35.7f;
+    private float qrH = 34.6f;
+
+    private float prodTextX = 291.5f;
+    private float prodTextY = 66.0f;
+    private float steelTextX = 291.5f;
+    private float steelTextY = 77.5f;
+    private float snTextX = 286.7f;
+    private float snTextY = 89.4f;
+
+    private JLabel previewLabel;
+    private FlowCardData testData;
+
+    // 拖拽相关
+    private boolean draggingQr = false;
+    private Point dragStart = null;
+
+    public CoordinateAdjustTool() {
+        setTitle("流转卡坐标调整工具");
+        setSize(1200, 900);
+        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+        setLayout(new BorderLayout(10, 10));
+
+        // 测试数据
+        testData = new FlowCardData();
+        testData.setSn("+KB60IS3031T729002101");
+        testData.setSteelSn("T09260729002");
+
+        // 左侧:预览区域
+        previewLabel = new JLabel();
+        previewLabel.setHorizontalAlignment(JLabel.CENTER);
+
+        // 添加鼠标拖拽监听
+        previewLabel.addMouseListener(new java.awt.event.MouseAdapter() {
+            @Override
+            public void mousePressed(java.awt.event.MouseEvent e) {
+                handleMousePressed(e);
+            }
+            @Override
+            public void mouseReleased(java.awt.event.MouseEvent e) {
+                draggingQr = false;
+                dragStart = null;
+            }
+        });
+        previewLabel.addMouseMotionListener(new java.awt.event.MouseMotionAdapter() {
+            @Override
+            public void mouseDragged(java.awt.event.MouseEvent e) {
+                handleMouseDragged(e);
+            }
+        });
+
+        JScrollPane scrollPane = new JScrollPane(previewLabel);
+        add(scrollPane, BorderLayout.CENTER);
+
+        // 右侧:控制面板
+        JPanel controlPanel = new JPanel();
+        controlPanel.setLayout(new BoxLayout(controlPanel, BoxLayout.Y_AXIS));
+        controlPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+
+        // 输入框区域
+        controlPanel.add(createLabel("=== 测试数据 ==="));
+        JTextField snField = new JTextField(testData.getSn(), 20);
+        JTextField steelSnField = new JTextField(testData.getSteelSn(), 15);
+        JPanel inputPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+        inputPanel.add(new JLabel("客户码:"));
+        inputPanel.add(snField);
+        controlPanel.add(inputPanel);
+        JPanel inputPanel2 = new JPanel(new FlowLayout(FlowLayout.LEFT));
+        inputPanel2.add(new JLabel("钢印码:"));
+        inputPanel2.add(steelSnField);
+        controlPanel.add(inputPanel2);
+        JButton updateBtn = new JButton("更新预览");
+        updateBtn.addActionListener(e -> {
+            testData.setSn(snField.getText());
+            testData.setSteelSn(steelSnField.getText());
+            updatePreview();
+        });
+        controlPanel.add(updateBtn);
+        controlPanel.add(Box.createVerticalStrut(20));
+
+        // 二维码坐标控制 - 改为输入框+滑动条
+        controlPanel.add(createLabel("=== 二维码位置 ==="));
+        JTextField qrXField = new JTextField(String.format("%.1f", qrX), 8);
+        JTextField qrYField = new JTextField(String.format("%.1f", qrY), 8);
+
+        JPanel qrXPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+        qrXPanel.add(new JLabel("X:"));
+        qrXPanel.add(qrXField);
+        qrXField.addActionListener(e -> {
+            try {
+                qrX = Float.parseFloat(qrXField.getText());
+                updatePreview();
+            } catch (NumberFormatException ex) {
+                qrXField.setText(String.format("%.1f", qrX));
+            }
+        });
+        controlPanel.add(qrXPanel);
+        controlPanel.add(createSlider("X坐标", qrX, 100f, 300f, v -> {
+            qrX = v;
+            qrXField.setText(String.format("%.1f", v));
+            updatePreview();
+        }));
+
+        JPanel qrYPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+        qrYPanel.add(new JLabel("Y:"));
+        qrYPanel.add(qrYField);
+        qrYField.addActionListener(e -> {
+            try {
+                qrY = Float.parseFloat(qrYField.getText());
+                updatePreview();
+            } catch (NumberFormatException ex) {
+                qrYField.setText(String.format("%.1f", qrY));
+            }
+        });
+        controlPanel.add(qrYPanel);
+        controlPanel.add(createSlider("Y坐标", qrY, 30f, 150f, v -> {
+            qrY = v;
+            qrYField.setText(String.format("%.1f", v));
+            updatePreview();
+        }));
+        controlPanel.add(Box.createVerticalStrut(20));
+
+        // 项目号坐标控制
+        controlPanel.add(createLabel("=== 项目号位置 ==="));
+        JTextField prodXField = new JTextField(String.format("%.1f", prodTextX), 8);
+        JTextField prodYField = new JTextField(String.format("%.1f", prodTextY), 8);
+
+        JPanel prodXPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+        prodXPanel.add(new JLabel("X:"));
+        prodXPanel.add(prodXField);
+        prodXField.addActionListener(e -> {
+            try {
+                prodTextX = Float.parseFloat(prodXField.getText());
+                updatePreview();
+            } catch (NumberFormatException ex) {
+                prodXField.setText(String.format("%.1f", prodTextX));
+            }
+        });
+        controlPanel.add(prodXPanel);
+        controlPanel.add(createSlider("X坐标", prodTextX, 200f, 400f, v -> {
+            prodTextX = v;
+            prodXField.setText(String.format("%.1f", v));
+            updatePreview();
+        }));
+
+        JPanel prodYPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+        prodYPanel.add(new JLabel("Y:"));
+        prodYPanel.add(prodYField);
+        prodYField.addActionListener(e -> {
+            try {
+                prodTextY = Float.parseFloat(prodYField.getText());
+                updatePreview();
+            } catch (NumberFormatException ex) {
+                prodYField.setText(String.format("%.1f", prodTextY));
+            }
+        });
+        controlPanel.add(prodYPanel);
+        controlPanel.add(createSlider("Y坐标", prodTextY, 50f, 150f, v -> {
+            prodTextY = v;
+            prodYField.setText(String.format("%.1f", v));
+            updatePreview();
+        }));
+        controlPanel.add(Box.createVerticalStrut(20));
+
+        // 钢印码坐标控制 - 改为输入框+滑动条
+        controlPanel.add(createLabel("=== 钢印码位置 ==="));
+        JTextField steelXField = new JTextField(String.format("%.1f", steelTextX), 8);
+        JTextField steelYField = new JTextField(String.format("%.1f", steelTextY), 8);
+
+        JPanel steelXPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+        steelXPanel.add(new JLabel("X:"));
+        steelXPanel.add(steelXField);
+        steelXField.addActionListener(e -> {
+            try {
+                steelTextX = Float.parseFloat(steelXField.getText());
+                updatePreview();
+            } catch (NumberFormatException ex) {
+                steelXField.setText(String.format("%.1f", steelTextX));
+            }
+        });
+        controlPanel.add(steelXPanel);
+        controlPanel.add(createSlider("X坐标", steelTextX, 200f, 400f, v -> {
+            steelTextX = v;
+            steelXField.setText(String.format("%.1f", v));
+            updatePreview();
+        }));
+
+        JPanel steelYPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+        steelYPanel.add(new JLabel("Y:"));
+        steelYPanel.add(steelYField);
+        steelYField.addActionListener(e -> {
+            try {
+                steelTextY = Float.parseFloat(steelYField.getText());
+                updatePreview();
+            } catch (NumberFormatException ex) {
+                steelYField.setText(String.format("%.1f", steelTextY));
+            }
+        });
+        controlPanel.add(steelYPanel);
+        controlPanel.add(createSlider("Y坐标", steelTextY, 50f, 150f, v -> {
+            steelTextY = v;
+            steelYField.setText(String.format("%.1f", v));
+            updatePreview();
+        }));
+        controlPanel.add(Box.createVerticalStrut(20));
+
+        // 客户码坐标控制 - 改为输入框+滑动条
+        controlPanel.add(createLabel("=== 客户码位置 ==="));
+        JTextField snXField = new JTextField(String.format("%.1f", snTextX), 8);
+        JTextField snYField = new JTextField(String.format("%.1f", snTextY), 8);
+
+        JPanel snXPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+        snXPanel.add(new JLabel("X:"));
+        snXPanel.add(snXField);
+        snXField.addActionListener(e -> {
+            try {
+                snTextX = Float.parseFloat(snXField.getText());
+                updatePreview();
+            } catch (NumberFormatException ex) {
+                snXField.setText(String.format("%.1f", snTextX));
+            }
+        });
+        controlPanel.add(snXPanel);
+        controlPanel.add(createSlider("X坐标", snTextX, 200f, 400f, v -> {
+            snTextX = v;
+            snXField.setText(String.format("%.1f", v));
+            updatePreview();
+        }));
+
+        JPanel snYPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+        snYPanel.add(new JLabel("Y:"));
+        snYPanel.add(snYField);
+        snYField.addActionListener(e -> {
+            try {
+                snTextY = Float.parseFloat(snYField.getText());
+                updatePreview();
+            } catch (NumberFormatException ex) {
+                snYField.setText(String.format("%.1f", snTextY));
+            }
+        });
+        controlPanel.add(snYPanel);
+        controlPanel.add(createSlider("Y坐标", snTextY, 50f, 150f, v -> {
+            snTextY = v;
+            snYField.setText(String.format("%.1f", v));
+            updatePreview();
+        }));
+        controlPanel.add(Box.createVerticalStrut(20));
+
+        // 按钮
+        JButton exportBtn = new JButton("生成代码");
+        exportBtn.setFont(new Font("Microsoft YaHei UI", Font.BOLD, 14));
+        exportBtn.addActionListener(e -> exportCode());
+        controlPanel.add(exportBtn);
+
+        JButton previewPdfBtn = new JButton("预览 PDF");
+        previewPdfBtn.setFont(new Font("Microsoft YaHei UI", Font.BOLD, 14));
+        previewPdfBtn.addActionListener(e -> previewPdf());
+        controlPanel.add(previewPdfBtn);
+
+        add(controlPanel, BorderLayout.EAST);
+
+        // 初始预览
+        updatePreview();
+    }
+
+    /**
+     * 处理鼠标按下事件 - 判断是否点击在二维码区域
+     */
+    private void handleMousePressed(java.awt.event.MouseEvent e) {
+        if (previewLabel.getIcon() == null) return;
+
+        // 获取图像显示区域
+        ImageIcon icon = (ImageIcon) previewLabel.getIcon();
+        int imgW = icon.getIconWidth();
+        int imgH = icon.getIconHeight();
+        int labelW = previewLabel.getWidth();
+        int labelH = previewLabel.getHeight();
+
+        // 计算图像在 label 中的偏移(居中显示)
+        int offsetX = (labelW - imgW) / 2;
+        int offsetY = (labelH - imgH) / 2;
+
+        // 鼠标点击位置转换为图像坐标
+        int clickX = e.getX() - offsetX;
+        int clickY = e.getY() - offsetY;
+
+        if (clickX < 0 || clickX >= imgW || clickY < 0 || clickY >= imgH) return;
+
+        // 转换为 PDF 坐标系
+        double scale = 300.0 / 72.0;
+        int qrImgX = (int) (qrX * scale);
+        int qrImgY = (int) (qrY * scale);
+        int qrImgW = (int) (qrW * scale);
+        int qrImgH = (int) (qrH * scale);
+
+        // 判断是否点击在二维码区域
+        if (clickX >= qrImgX && clickX <= qrImgX + qrImgW &&
+            clickY >= qrImgY && clickY <= qrImgY + qrImgH) {
+            draggingQr = true;
+            dragStart = new Point(clickX, clickY);
+        }
+    }
+
+    /**
+     * 处理鼠标拖拽事件 - 更新二维码位置
+     */
+    private void handleMouseDragged(java.awt.event.MouseEvent e) {
+        if (!draggingQr || dragStart == null) return;
+
+        if (previewLabel.getIcon() == null) return;
+
+        ImageIcon icon = (ImageIcon) previewLabel.getIcon();
+        int imgW = icon.getIconWidth();
+        int imgH = icon.getIconHeight();
+        int labelW = previewLabel.getWidth();
+        int labelH = previewLabel.getHeight();
+
+        int offsetX = (labelW - imgW) / 2;
+        int offsetY = (labelH - imgH) / 2;
+
+        int currentX = e.getX() - offsetX;
+        int currentY = e.getY() - offsetY;
+
+        // 计算拖动偏移量(图像坐标系)
+        int deltaX = currentX - dragStart.x;
+        int deltaY = currentY - dragStart.y;
+
+        // 转换为 PDF 坐标系偏移
+        double scale = 300.0 / 72.0;
+        float pdfDeltaX = (float) (deltaX / scale);
+        float pdfDeltaY = (float) (deltaY / scale);
+
+        // 更新二维码位置
+        qrX += pdfDeltaX;
+        qrY += pdfDeltaY;
+
+        // 更新拖动起点
+        dragStart = new Point(currentX, currentY);
+
+        // 刷新预览
+        updatePreview();
+    }
+
+    private JLabel createLabel(String text) {
+        JLabel label = new JLabel(text);
+        label.setFont(new Font("Microsoft YaHei UI", Font.BOLD, 14));
+        return label;
+    }
+
+    private JPanel createSlider(String label, float initValue, float min, float max, ValueChangeListener listener) {
+        JPanel panel = new JPanel(new BorderLayout(5, 5));
+
+        JLabel nameLabel = new JLabel(label);
+        nameLabel.setFont(new Font("Microsoft YaHei UI", Font.PLAIN, 12));
+
+        JLabel valueLabel = new JLabel(String.format("%.1f", initValue));
+        valueLabel.setFont(new Font("Consolas", Font.PLAIN, 12));
+        valueLabel.setPreferredSize(new Dimension(50, 20));
+
+        JSlider slider = new JSlider(JSlider.HORIZONTAL, (int)(min * 10), (int)(max * 10), (int)(initValue * 10));
+        slider.addChangeListener(e -> {
+            float value = slider.getValue() / 10.0f;
+            valueLabel.setText(String.format("%.1f", value));
+            listener.onValueChange(value);
+        });
+
+        JPanel topPanel = new JPanel(new BorderLayout());
+        topPanel.add(nameLabel, BorderLayout.WEST);
+        topPanel.add(valueLabel, BorderLayout.EAST);
+
+        panel.add(topPanel, BorderLayout.NORTH);
+        panel.add(slider, BorderLayout.CENTER);
+
+        return panel;
+    }
+
+    private void updatePreview() {
+        try {
+            // 模拟绘制效果
+            BufferedImage preview = createPreviewImage();
+            ImageIcon icon = new ImageIcon(preview);
+            previewLabel.setIcon(icon);
+        } catch (Exception e) {
+            e.printStackTrace();
+            JOptionPane.showMessageDialog(this, "预览失败: " + e.getMessage());
+        }
+    }
+
+    private BufferedImage createPreviewImage() throws Exception {
+        // 获取实际 PDF 模板背景图
+        BufferedImage templateBg = FlowCardRenderer.getTemplateImage();
+
+        // 创建预览画布(与模板同尺寸)
+        BufferedImage img = new BufferedImage(templateBg.getWidth(), templateBg.getHeight(), BufferedImage.TYPE_INT_RGB);
+        Graphics2D g2 = img.createGraphics();
+        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+
+        // 绘制模板背景
+        g2.drawImage(templateBg, 0, 0, null);
+
+        // PDF 坐标转图像坐标(PDF 是 72 DPI,模板图是 300 DPI)
+        double scale = 300.0 / 72.0;
+
+        // 绘制实际二维码(而不是框)
+        try {
+            BufferedImage qrCode = FlowCardRenderer.generateQrCode(testData.getSn(), 600);
+            int imgQrX = (int)(qrX * scale);
+            int imgQrY = (int)(qrY * scale);
+            int imgQrW = (int)(qrW * scale);
+            int imgQrH = (int)(qrH * scale);
+            g2.drawImage(qrCode, imgQrX, imgQrY, imgQrW, imgQrH, null);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        // 绘制实际文字
+        g2.setColor(Color.BLACK);
+        g2.setFont(new Font("SimSun", Font.BOLD, (int)(8 * scale)));
+
+        // 绘制项目号文字
+        int imgProdX = (int)(prodTextX * scale);
+        int imgProdY = (int)(prodTextY * scale);
+        String prodCode = testData.getProdCode();
+        if (prodCode != null && !prodCode.isEmpty()) {
+            g2.drawString(prodCode, imgProdX, imgProdY);
+        }
+
+        // 绘制钢印码文字
+        int imgSteelX = (int)(steelTextX * scale);
+        int imgSteelY = (int)(steelTextY * scale);
+        if (testData.getSteelSn() != null) {
+            g2.drawString(testData.getSteelSn(), imgSteelX, imgSteelY);
+        }
+
+        // 绘制客户码文字
+        int imgSnX = (int)(snTextX * scale);
+        int imgSnY = (int)(snTextY * scale);
+        if (testData.getSn() != null) {
+            g2.drawString(testData.getSn(), imgSnX, imgSnY);
+        }
+
+        // 绘制辅助标记(蓝色十字标记项目号位置)
+        g2.setColor(Color.BLUE);
+        g2.setStroke(new BasicStroke(2));
+        g2.drawLine(imgProdX - 20, imgProdY, imgProdX + 20, imgProdY);
+        g2.drawLine(imgProdX, imgProdY - 20, imgProdX, imgProdY + 20);
+
+        // 绘制辅助标记(红色十字标记钢印码位置)
+        g2.setColor(Color.RED);
+        g2.setStroke(new BasicStroke(2));
+        g2.drawLine(imgSteelX - 20, imgSteelY, imgSteelX + 20, imgSteelY);
+        g2.drawLine(imgSteelX, imgSteelY - 20, imgSteelX, imgSteelY + 20);
+
+        // 绘制辅助标记(绿色十字标记客户码位置)
+        g2.setColor(Color.GREEN.darker());
+        g2.setStroke(new BasicStroke(2));
+        g2.drawLine(imgSnX - 20, imgSnY, imgSnX + 20, imgSnY);
+        g2.drawLine(imgSnX, imgSnY - 20, imgSnX, imgSnY + 20);
+
+        // 显示当前坐标
+        g2.setColor(Color.BLACK);
+        g2.setFont(new Font("Consolas", Font.BOLD, 24));
+        g2.drawString(String.format("QR: (%.1f, %.1f)", qrX, qrY), 20, 50);
+        g2.drawString(String.format("项目号: (%.1f, %.1f) = %s", prodTextX, prodTextY, prodCode != null ? prodCode : ""), 20, 90);
+        g2.drawString(String.format("钢印: (%.1f, %.1f)", steelTextX, steelTextY), 20, 130);
+        g2.drawString(String.format("客户: (%.1f, %.1f)", snTextX, snTextY), 20, 170);
+
+        g2.dispose();
+        return img;
+    }
+
+    private void exportCode() {
+        String code = String.format(
+            "// 坐标配置(复制到 FlowCardRenderer.java 中)\n" +
+            "private static final float QR_X = %.1ff;\n" +
+            "private static final float QR_Y = %.1ff;\n" +
+            "private static final float QR_W = %.1ff;\n" +
+            "private static final float QR_H = %.1ff;\n\n" +
+            "private static final float PROD_TEXT_X = %.1ff;\n" +
+            "private static final float PROD_TEXT_Y = %.1ff;\n" +
+            "private static final float STEEL_TEXT_X = %.1ff;\n" +
+            "private static final float STEEL_TEXT_Y = %.1ff;\n" +
+            "private static final float SN_TEXT_X = %.1ff;\n" +
+            "private static final float SN_TEXT_Y = %.1ff;\n",
+            qrX, qrY, qrW, qrH,
+            prodTextX, prodTextY, steelTextX, steelTextY, snTextX, snTextY
+        );
+
+        JTextArea textArea = new JTextArea(code, 12, 50);
+        textArea.setFont(new Font("Consolas", Font.PLAIN, 12));
+        textArea.setEditable(false);
+        JOptionPane.showMessageDialog(this, new JScrollPane(textArea), "生成的代码", JOptionPane.INFORMATION_MESSAGE);
+    }
+
+    private void previewPdf() {
+        // TODO: 调用实际的预览方法
+        JOptionPane.showMessageDialog(this, "请在 IDE 中运行 TestPrintPreview 查看实际 PDF 效果");
+    }
+
+    interface ValueChangeListener {
+        void onValueChange(float value);
+    }
+
+    public static void main(String[] args) {
+        SwingUtilities.invokeLater(() -> {
+            CoordinateAdjustTool tool = new CoordinateAdjustTool();
+            tool.setVisible(true);
+        });
+    }
+}

+ 152 - 0
src/com/mes/print/PrintHelper.java

@@ -0,0 +1,152 @@
+package com.mes.print;
+
+import com.mes.print.model.FlowCardData;
+import com.mes.print.model.LabelData;
+
+/**
+ * 打印辅助类:封装 mes-print 模块调用
+ *
+ * 根据 print.properties 配置的打印机类型自动选择打印方式:
+ * - hp: 打印流转卡(A4/A5,走 HP 激光)
+ * - tsc: 打印标签(走 TSC 热敏)
+ * - none: 不打印
+ */
+public class PrintHelper {
+
+    /**
+     * 预热打印模块(异步调用,不阻塞主流程)
+     * 首次打印约 3-5 秒的 PDF 模板加载 + 底图渲染放到启动阶段完成
+     */
+    public static void warmUpAsync() {
+        Thread t = new Thread(() -> {
+            try {
+                Printer.warmUp();
+            } catch (Throwable e) {
+                System.err.println("[打印] 预热异常: " + e.getMessage());
+            }
+        }, "print-warmup");
+        t.setDaemon(true);
+        t.start();
+    }
+
+    /**
+     * 打印流转卡(HP 激光打印机)
+     *
+     * @param sn 产品序列号(客户码)
+     * @param steelSn 钢印码(可为 null)
+     * @return true=成功或已跳过;false=失败
+     */
+    public static boolean printFlowCard(String sn, String steelSn) {
+        try {
+            String type = Printer.getCurrentType();
+            if ("none".equals(type)) {
+                System.out.println("[打印] 配置为不打印,跳过流转卡: " + sn);
+                return true;
+            }
+            if (!"hp".equals(type)) {
+                System.out.println("[打印] 打印机类型为 " + type + ",不支持流转卡");
+                return false;
+            }
+
+            FlowCardData data = new FlowCardData();
+            data.setSn(sn);
+            if (steelSn != null) {
+                data.setSteelSn(steelSn);
+            }
+
+            Printer.printFlowCard(data);
+            System.out.println("[打印] 流转卡打印成功: " + sn);
+            return true;
+
+        } catch (PrinterException e) {
+            System.err.println("[打印] 流转卡打印失败: " + e.getMessage());
+            e.printStackTrace();
+            return false;
+        } catch (Exception e) {
+            System.err.println("[打印] 流转卡打印异常: " + e.getMessage());
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 打印标签(TSC 热敏标签机)
+     *
+     * @param sn 产品序列号
+     * @param barcode 条码内容(null 时用 sn)
+     * @param qrcode 二维码内容(null 时用 sn)
+     * @param title 标题(null 时用默认「产品标签」)
+     * @param copies 打印份数(≤0 时按 1 份)
+     * @return true=成功或已跳过;false=失败
+     */
+    public static boolean printLabel(String sn, String barcode, String qrcode, String title, int copies) {
+        try {
+            String type = Printer.getCurrentType();
+            if ("none".equals(type)) {
+                System.out.println("[打印] 配置为不打印,跳过标签: " + sn);
+                return true;
+            }
+            if (!"tsc".equals(type)) {
+                System.out.println("[打印] 打印机类型为 " + type + ",不支持标签");
+                return false;
+            }
+
+            LabelData data = new LabelData();
+            data.setSn(sn);
+            data.setBarcode(barcode != null ? barcode : sn);
+            data.setQrcode(qrcode != null ? qrcode : sn);
+            data.setTitle(title != null ? title : "产品标签");
+            data.setCopies(copies > 0 ? copies : 1);
+
+            Printer.printLabel(data);
+            System.out.println("[打印] 标签打印成功: " + sn);
+            return true;
+
+        } catch (PrinterException e) {
+            System.err.println("[打印] 标签打印失败: " + e.getMessage());
+            e.printStackTrace();
+            return false;
+        } catch (Exception e) {
+            System.err.println("[打印] 标签打印异常: " + e.getMessage());
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 根据当前配置自动打印
+     * - hp: 打流转卡(steelSn 传 null)
+     * - tsc: 打标签(barcode/qrcode 都用 sn)
+     * - none: 跳过
+     */
+    public static boolean autoPrint(String sn) {
+        return autoPrint(sn, null);
+    }
+
+    /**
+     * 根据当前配置自动打印
+     *
+     * @param sn 产品序列号
+     * @param steelSn 钢印码(仅流转卡使用,可为 null)
+     */
+    public static boolean autoPrint(String sn, String steelSn) {
+        try {
+            String type = Printer.getCurrentType();
+            if ("none".equals(type)) {
+                System.out.println("[打印] 配置为不打印,跳过");
+                return true;
+            } else if ("hp".equals(type)) {
+                return printFlowCard(sn, steelSn);
+            } else if ("tsc".equals(type)) {
+                return printLabel(sn, sn, sn, "产品标签", 1);
+            } else {
+                System.err.println("[打印] 未知的打印机类型: " + type);
+                return false;
+            }
+        } catch (Exception e) {
+            System.err.println("[打印] 自动打印失败: " + e.getMessage());
+            e.printStackTrace();
+            return false;
+        }
+    }
+}

+ 100 - 0
src/com/mes/print/Printer.java

@@ -0,0 +1,100 @@
+package com.mes.print;
+
+import java.util.List;
+
+import com.mes.print.model.FlowCardData;
+import com.mes.print.model.LabelData;
+import com.mes.print.util.PrinterUtil;
+
+/**
+ * 对外唯一入口。业务代码只需调这里的静态方法。
+ *
+ * 使用示例:
+ * <pre>
+ *   FlowCardData d = new FlowCardData();
+ *   d.setSn("A2026072200001");
+ *   d.setProduct("+KB24");
+ *   Printer.printFlowCard(d);
+ *
+ *   LabelData l = new LabelData();
+ *   l.setSn("A2026072200001");
+ *   l.setBarcode("A2026072200001");
+ *   Printer.printLabel(l);
+ * </pre>
+ *
+ * 配置文件 config/print.properties 示例:
+ * <pre>
+ *   printer.type=hp        # 或 tsc
+ *   printer.name=HP LaserJet 3004
+ * </pre>
+ */
+public class Printer {
+
+	private Printer() {}
+
+	/** 打印流转卡(A4,走 HP 类打印机) */
+	public static void printFlowCard(FlowCardData data) {
+		PrinterFactory.get().printFlowCard(data);
+	}
+
+	/** 预览流转卡(生成临时 PDF 并用系统默认程序打开) */
+	public static void previewFlowCard(FlowCardData data) {
+		com.mes.print.template.FlowCardRenderer.preview(data);
+	}
+
+	/** 打印标签(小尺寸,走 TSC 类打印机) */
+	public static void printLabel(LabelData data) {
+		PrinterFactory.get().printLabel(data);
+	}
+
+	/** 列出本机所有识别到的打印机(装机排错用) */
+	public static List<String> listAvailablePrinters() {
+		return PrinterUtil.listNames();
+	}
+
+	/** 重新加载配置(配置改动后调用) */
+	public static void reload() {
+		com.mes.print.config.PrinterConfig.reset();
+		PrinterFactory.reset();
+	}
+
+	/**
+	 * 便捷方法:保存打印机配置到 config/print.properties 并立即生效。
+	 * @param type hp / tsc / none
+	 * @param name Windows 打印机名字(none 时可传 null)
+	 */
+	public static void setPrinter(String type, String name) {
+		com.mes.print.config.PrinterConfig.save(type, name);
+		PrinterFactory.reset();
+	}
+
+	/** 当前配置的打印机类型(hp / tsc / none,读不到时返回 none) */
+	public static String getCurrentType() {
+		String t = com.mes.print.config.PrinterConfig.get("printer.type", "none");
+		return t == null || t.trim().isEmpty() ? "none" : t.trim().toLowerCase();
+	}
+
+	/** 当前配置的目标打印机名 */
+	public static String getCurrentName() {
+		return com.mes.print.config.PrinterConfig.get("printer.name", "");
+	}
+
+        /**
+         * 预热:初始化打印实现 + (HP 模式) 提前加载流转卡模板/字体
+         * 建议在主程序启动时异步调用一次,避免首单打印卡顿
+         */
+        public static void warmUp() {
+                long t0 = System.currentTimeMillis();
+                try {
+                        PrinterFactory.get();
+                } catch (Exception e) {
+                        System.err.println("[Printer] 工厂预热失败: " + e.getMessage());
+                        return;
+                }
+                String type = getCurrentType();
+                if ("hp".equals(type)) {
+                        com.mes.print.template.FlowCardRenderer.warmUp();
+                }
+                System.out.println("[Printer] 预热总耗时 " + (System.currentTimeMillis() - t0) + "ms");
+        }
+}

+ 17 - 0
src/com/mes/print/PrinterException.java

@@ -0,0 +1,17 @@
+package com.mes.print;
+
+/**
+ * 打印统一异常。业务代码可选择性 try-catch,也可让它冒泡。
+ */
+public class PrinterException extends RuntimeException {
+
+	private static final long serialVersionUID = 1L;
+
+	public PrinterException(String msg) {
+		super(msg);
+	}
+
+	public PrinterException(String msg, Throwable cause) {
+		super(msg, cause);
+	}
+}

+ 51 - 0
src/com/mes/print/PrinterFactory.java

@@ -0,0 +1,51 @@
+package com.mes.print;
+
+import com.mes.print.config.PrinterConfig;
+import com.mes.print.impl.HpPrinter;
+import com.mes.print.impl.NonePrinter;
+import com.mes.print.impl.PrinterImpl;
+import com.mes.print.impl.TscPrinter;
+
+/**
+ * 按 config 的 printer.type 选择实现(hp / tsc)
+ * 只创建一次,之后复用
+ */
+class PrinterFactory {
+
+	private static volatile PrinterImpl impl;
+
+	static PrinterImpl get() {
+		PrinterImpl r = impl;
+		if (r != null) return r;
+		synchronized (PrinterFactory.class) {
+			if (impl != null) return impl;
+			String type = PrinterConfig.get("printer.type");
+			if (type == null) throw new PrinterException("未配置 printer.type");
+			type = type.trim().toLowerCase();
+			if ("hp".equals(type)) {
+				HpPrinter hp = new HpPrinter();
+				// 如果配置了打印机 IP,启用 SNMP 监控
+				String printerIp = PrinterConfig.get("printer.ip");
+				if (printerIp != null && !printerIp.trim().isEmpty()) {
+					hp.enableSnmpMonitor(printerIp.trim());
+				}
+				impl = hp;
+			} else if ("tsc".equals(type)) {
+				impl = new TscPrinter();
+			} else if ("none".equals(type) || type.isEmpty()) {
+				impl = new NonePrinter();
+			} else {
+				throw new PrinterException("不支持的打印机类型: " + type + "(支持 hp / tsc / none)");
+			}
+			return impl;
+		}
+	}
+
+	/** 测试用:重置单例,重新按 config 创建 */
+	static synchronized void reset() {
+		if (impl instanceof HpPrinter) {
+			((HpPrinter) impl).close();
+		}
+		impl = null;
+	}
+}

+ 48 - 0
src/com/mes/print/TestMain.java

@@ -0,0 +1,48 @@
+package com.mes.print;
+
+import com.mes.print.model.FlowCardData;
+import com.mes.print.model.LabelData;
+
+/**
+ * 手动测试入口。装机时用这个类验证:
+ *   java -cp mes-print.jar com.mes.print.TestMain list     列出本机打印机
+ *   java -cp mes-print.jar com.mes.print.TestMain label    发一张测试标签
+ *   java -cp mes-print.jar com.mes.print.TestMain card     发一张测试流转卡
+ * 需要 config/print.properties 在工作目录下(label/card 时)
+ */
+public class TestMain {
+
+	public static void main(String[] args) {
+		String action = args.length > 0 ? args[0] : "list";
+
+		if ("list".equals(action)) {
+			System.out.println("本机识别到的打印机:");
+			for (String n : Printer.listAvailablePrinters()) {
+				System.out.println("  - " + n);
+			}
+			return;
+		}
+
+		if ("label".equals(action)) {
+			LabelData d = new LabelData();
+			d.setTitle("TEST LABEL");
+			d.setSn("TEST0001");
+			d.setBarcode("TEST0001");
+			d.setQrcode("TEST0001");
+			Printer.printLabel(d);
+			System.out.println("测试标签已发送");
+			return;
+		}
+
+		if ("card".equals(action)) {
+			FlowCardData d = new FlowCardData();
+			d.setSn("+KB24IS3031T721000201");
+			d.setSteelSn("GY100000LX");
+			Printer.printFlowCard(d);
+			System.out.println("测试流转卡已发送");
+			return;
+		}
+
+		System.out.println("用法: TestMain [list|label|card]");
+	}
+}

+ 40 - 0
src/com/mes/print/TestPrintPreview.java

@@ -0,0 +1,40 @@
+package com.mes.print;
+
+import com.mes.print.model.FlowCardData;
+
+/**
+ * 测试打印预览
+ * 快速测试不需要启动主程序
+ */
+public class TestPrintPreview {
+
+    public static void main(String[] args) {
+        try {
+            // 测试数据
+            String customerSn = "+KB60IS3031T729002101";
+            String steelSn = "T09260729002";
+
+            System.out.println("======================================");
+            System.out.println("测试打印预览");
+            System.out.println("客户码: " + customerSn);
+            System.out.println("钢印码: " + steelSn);
+            System.out.println("======================================");
+
+            // 创建流转卡数据
+            FlowCardData data = new FlowCardData();
+            data.setSn(customerSn);
+            data.setSteelSn(steelSn);
+
+            // 预览
+            System.out.println("正在生成预览 PDF...");
+            com.mes.print.template.FlowCardRenderer.preview(data);
+
+            System.out.println("预览完成!PDF 已用默认程序打开");
+            System.out.println("======================================");
+
+        } catch (Exception e) {
+            System.err.println("预览失败: " + e.getMessage());
+            e.printStackTrace();
+        }
+    }
+}

+ 55 - 0
src/com/mes/print/TestSnmp.java

@@ -0,0 +1,55 @@
+package com.mes.print;
+
+import com.mes.print.monitor.PrinterMonitor;
+
+/**
+ * SNMP 打印机监控测试
+ * 用法:java -cp "build/classes;lib/*" com.mes.print.TestSnmp <打印机IP>
+ */
+public class TestSnmp {
+	public static void main(String[] args) throws Exception {
+		if (args.length < 1) {
+			System.out.println("用法: TestSnmp <打印机IP>");
+			System.out.println("示例: TestSnmp 192.168.1.100");
+			return;
+		}
+
+		String printerIp = args[0];
+		System.out.println("=== SNMP 打印机监控测试 ===");
+		System.out.println("打印机 IP: " + printerIp);
+		System.out.println();
+
+		PrinterMonitor monitor = new PrinterMonitor(printerIp);
+		monitor.init();
+
+		// 测试连接
+		System.out.println("1. 测试连接...");
+		boolean connected = monitor.testConnection();
+		if (!connected) {
+			System.err.println("连接失败,请检查:");
+			System.err.println("  - 打印机 IP 是否正确");
+			System.err.println("  - 打印机是否开启 SNMP(默认端口 161)");
+			System.err.println("  - 防火墙是否放行 UDP 161 端口");
+			monitor.close();
+			return;
+		}
+
+		// 查询当前状态
+		System.out.println("\n2. 查询打印机状态...");
+		Long pageCount = monitor.getPageCount();
+		Integer status = monitor.getPrinterStatus();
+		System.out.println("  总打印页数: " + pageCount);
+		System.out.println("  打印机状态: " + status + " (3=空闲, 4=打印中, 5=预热)");
+
+		// 监控打印完成
+		System.out.println("\n3. 开始监控打印完成(请手动发送打印任务)...");
+		System.out.println("  按 Ctrl+C 退出监控");
+		monitor.waitForPrintComplete(1, () -> {
+			System.out.println("\n✓ 检测到打印完成!");
+			System.exit(0);
+		}, 300); // 5 分钟超时
+
+		// 保持程序运行
+		Thread.sleep(Long.MAX_VALUE);
+	}
+}

+ 0 - 0
src/com/mes/print/config/PrinterConfig.java


Некоторые файлы не были показаны из-за большого количества измененных файлов