wangxichen 1 semana atrás
commit
2e0ee91166

+ 6 - 0
.classpath

@@ -0,0 +1,6 @@
+<?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"/>
+	<classpathentry kind="src" path="src"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+/bin/
+/build/
+/dist/
+mes-print.jar
+.idea/
+*.iml

+ 17 - 0
.project

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>mes-print</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>
+</projectDescription>

+ 119 - 0
README.md

@@ -0,0 +1,119 @@
+# mes-print
+
+通用打印库。一个 jar 搞定 HP 激光打印机(流转卡)和 TSC 热敏标签机(标签)。
+
+## 特性
+
+- 一份 jar 通用,全公司客户端复用
+- 0 第三方依赖(纯 JDK)
+- 门面 API 极简:`Printer.printFlowCard()` / `Printer.printLabel()`
+- 通过 `config/print.properties` 声明本机打印机型号,无需改代码
+- 支持的打印机:
+  - HP 3004dn 及同类走 Windows 驱动的激光/喷墨打印机
+  - TSC TTP-244 Pro 及同类支持 TSPL 的热敏标签机
+
+## 使用(客户端接入)
+
+### 1. 拷贝 jar
+把编译产出的 `mes-print.jar` 放到客户端的 `lib/` 目录。
+
+### 2. 加配置
+拷贝 `config/print.properties.example` 到客户端的 `config/print.properties`,按本机情况改:
+
+```properties
+# HP 流转卡工位
+printer.type=hp
+printer.name=HP LaserJet 3004
+
+# TSC 标签工位
+printer.type=tsc
+printer.name=TSC TTP-244 Pro
+```
+
+### 3. 调用(业务代码)
+
+```java
+import com.mes.print.Printer;
+import com.mes.print.model.FlowCardData;
+import com.mes.print.model.LabelData;
+
+// 流转卡
+FlowCardData d = new FlowCardData();
+d.setProduct("+KB24");
+d.setSn("A2026072200001");
+d.setOpNo("OP130B");
+Printer.printFlowCard(d);
+
+// 标签
+LabelData l = new LabelData();
+l.setSn("A2026072200001");
+l.setBarcode("A2026072200001");
+Printer.printLabel(l);
+```
+
+打印失败会抛 `PrinterException`(继承 `RuntimeException`),业务自行决定 try/catch。
+
+## 构建
+
+```
+ant jar
+```
+产出 `dist/mes-print.jar`。
+
+或者直接用 javac:
+
+```
+javac -encoding UTF-8 -source 1.8 -target 1.8 -d build/classes ^
+      src/com/mes/print/*.java ^
+      src/com/mes/print/*/*.java
+jar cf dist/mes-print.jar -C build/classes .
+```
+
+## 装机验证
+
+先跑一下 TestMain 看本机识别到的打印机:
+
+```
+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     # 打测试流转卡
+```
+
+## 项目结构
+
+```
+src/com/mes/print/
+├── Printer.java                门面(对外唯一入口)
+├── PrinterException.java       统一异常
+├── PrinterFactory.java         按 config 选实现
+├── TestMain.java               手动测试入口
+├── config/PrinterConfig.java   config 加载
+├── impl/
+│   ├── PrinterImpl.java        实现接口
+│   ├── HpPrinter.java          Java2D → HP
+│   └── TscPrinter.java         TSPL → TSC
+├── template/
+│   ├── FlowCardRenderer.java   A4 流转卡渲染
+│   └── LabelTspl.java          TSPL 生成
+├── model/
+│   ├── FlowCardData.java
+│   └── LabelData.java
+└── util/
+    └── PrinterUtil.java        打印机查找
+```
+
+## 常见问题
+
+**Q: `未找到打印机: xxx`**
+异常里会列出本机所有可用打印机名字。把 `printer.name` 改成列出的某个名字即可(支持模糊 contains 匹配)。建议装机时把打印机重命名成固定名字。
+
+**Q: TSC 打中文乱码**
+TTP-244 Pro 默认不带中文字库。三种解决:① 用 DiagTool 上传中文字库到打印机 ② 把中文渲染成位图用 TSPL BITMAP 发过去 ③ 标签内容只用数字/英文/条码。当前实现走方案 ③。
+
+**Q: 换打印机型号了怎么办**
+改 `config/print.properties` 里的 `printer.name`,重启客户端即可。

+ 38 - 0
build.xml

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project name="mes-print" default="jar" basedir=".">
+
+	<property name="src.dir"     value="src"/>
+	<property name="build.dir"   value="build"/>
+	<property name="classes.dir" value="${build.dir}/classes"/>
+	<property name="dist.dir"    value="dist"/>
+	<property name="jar.file"    value="${dist.dir}/mes-print.jar"/>
+
+	<target name="clean">
+		<delete dir="${build.dir}"/>
+		<delete dir="${dist.dir}"/>
+	</target>
+
+	<target name="compile">
+		<mkdir dir="${classes.dir}"/>
+		<javac srcdir="${src.dir}"
+		       destdir="${classes.dir}"
+		       encoding="UTF-8"
+		       source="1.8"
+		       target="1.8"
+		       includeantruntime="false"
+		       debug="true">
+		</javac>
+	</target>
+
+	<target name="jar" depends="compile">
+		<mkdir dir="${dist.dir}"/>
+		<jar destfile="${jar.file}" basedir="${classes.dir}">
+			<manifest>
+				<attribute name="Implementation-Title"   value="mes-print"/>
+				<attribute name="Implementation-Version" value="1.0.0"/>
+			</manifest>
+		</jar>
+		<echo>OK -> ${jar.file}</echo>
+	</target>
+
+</project>

+ 12 - 0
config/print.properties.example

@@ -0,0 +1,12 @@
+# 打印机配置示例,装机时改名为 print.properties 放到客户端的 config/ 目录
+
+# 打印机类型:hp(走Java2D,打流转卡)或 tsc(走TSPL,打标签)
+printer.type=tsc
+
+# 打印机名字(Windows 设备与打印机里显示的名字,支持 contains 模糊匹配)
+# 建议装机时把打印机重命名成固定的名字,比如 TSC_LABEL_01
+printer.name=TSC TTP-244 Pro
+
+# ---- 只接 HP 流转卡打印机的工位改成以下配置 ----
+# printer.type=hp
+# printer.name=HP LaserJet 3004

+ 3 - 0
src/META-INF/MANIFEST.MF

@@ -0,0 +1,3 @@
+Manifest-Version: 1.0
+Main-Class: com.mes.print.TestMain
+

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

@@ -0,0 +1,76 @@
+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);
+	}
+
+	/** 打印标签(小尺寸,走 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", "");
+	}
+}

+ 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);
+	}
+}

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

@@ -0,0 +1,42 @@
+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)) {
+				impl = new HpPrinter();
+			} 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() {
+		impl = null;
+	}
+}

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

@@ -0,0 +1,53 @@
+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.setProduct("+KB24");
+			d.setSn("A2026072200001");
+			d.setOpNo("OP130B");
+			d.setBatch("B2026072201");
+			d.setDate("2026-07-22");
+			d.setOperator("tester");
+			d.setRemark("测试流转卡");
+			Printer.printFlowCard(d);
+			System.out.println("测试流转卡已发送");
+			return;
+		}
+
+		System.out.println("用法: TestMain [list|label|card]");
+	}
+}

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

@@ -0,0 +1,98 @@
+package com.mes.print.config;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.util.Properties;
+
+import com.mes.print.PrinterException;
+
+/**
+ * 配置加载器。查找顺序:
+ * 1. 系统属性 -Dprint.config=xxx 指定的路径
+ * 2. 当前工作目录 ./config/print.properties
+ * 3. classpath 上的 print.properties(打进 jar 里的默认值)
+ */
+public class PrinterConfig {
+
+	private static final String CONFIG_FILE = "config/print.properties";
+	private static Properties props;
+
+	public static synchronized Properties load() {
+		if (props != null) return props;
+		Properties p = new Properties();
+		try (InputStream in = openStream()) {
+			p.load(in);
+		} catch (Exception e) {
+			throw new PrinterException("加载打印配置失败: " + e.getMessage(), e);
+		}
+		props = p;
+		return props;
+	}
+
+	private static InputStream openStream() throws Exception {
+		// 优先 -D 指定
+		String sysPath = System.getProperty("print.config");
+		if (sysPath != null && new File(sysPath).exists()) {
+			return new FileInputStream(sysPath);
+		}
+		// 其次工作目录
+		File f = new File(CONFIG_FILE);
+		if (f.exists()) return new FileInputStream(f);
+		// 最后 classpath
+		InputStream in = PrinterConfig.class.getClassLoader().getResourceAsStream("print.properties");
+		if (in != null) return in;
+		throw new PrinterException("找不到配置文件: " + CONFIG_FILE);
+	}
+
+	public static String get(String key) {
+		return load().getProperty(key);
+	}
+
+	public static String get(String key, String defaultValue) {
+		String v = load().getProperty(key);
+		return v == null ? defaultValue : v;
+	}
+
+	public static int getInt(String key, int defaultValue) {
+		String v = get(key);
+		if (v == null) return defaultValue;
+		try { return Integer.parseInt(v.trim()); } catch (Exception e) { return defaultValue; }
+	}
+
+	/** 测试用:重置缓存,重新加载 */
+	public static synchronized void reset() {
+		props = null;
+	}
+
+	/**
+	 * 保存配置到 ./config/print.properties。传入 null 表示不修改该项。
+	 * 保存后自动 reset(),下次 get 会重新加载。
+	 */
+	public static synchronized void save(String type, String name) {
+		File f = new File(CONFIG_FILE);
+		Properties p = new Properties();
+		// 保留已有其他键
+		if (f.exists()) {
+			try (InputStream in = new FileInputStream(f)) {
+				p.load(in);
+			} catch (Exception ignored) {
+			}
+		}
+		if (type != null) p.setProperty("printer.type", type);
+		if (name != null) p.setProperty("printer.name", name);
+
+		File parent = f.getAbsoluteFile().getParentFile();
+		if (parent != null && !parent.exists()) parent.mkdirs();
+
+		try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f), "UTF-8"))) {
+			p.store(bw, "printer settings");
+		} catch (Exception e) {
+			throw new PrinterException("保存打印配置失败: " + e.getMessage(), e);
+		}
+		reset();
+	}
+}

+ 62 - 0
src/com/mes/print/impl/HpPrinter.java

@@ -0,0 +1,62 @@
+package com.mes.print.impl;
+
+import java.awt.print.PageFormat;
+import java.awt.print.Paper;
+import java.awt.print.Printable;
+import java.awt.print.PrinterJob;
+
+import javax.print.PrintService;
+import javax.print.attribute.HashPrintRequestAttributeSet;
+import javax.print.attribute.PrintRequestAttributeSet;
+import javax.print.attribute.standard.Copies;
+import javax.print.attribute.standard.MediaSizeName;
+
+import com.mes.print.PrinterException;
+import com.mes.print.config.PrinterConfig;
+import com.mes.print.model.FlowCardData;
+import com.mes.print.model.LabelData;
+import com.mes.print.template.FlowCardRenderer;
+import com.mes.print.util.PrinterUtil;
+
+/**
+ * HP 3004dn 及同类激光打印机:走 Java2D + PrinterJob
+ */
+public class HpPrinter implements PrinterImpl {
+
+	@Override
+	public void printFlowCard(FlowCardData data) {
+		if (data == null) throw new PrinterException("流转卡数据为空");
+
+		String name = PrinterConfig.get("printer.name");
+		PrintService svc = PrinterUtil.find(name);
+
+		try {
+			PrinterJob job = PrinterJob.getPrinterJob();
+			job.setPrintService(svc);
+
+			Printable page = FlowCardRenderer.render(data);
+			PageFormat pf = job.defaultPage();
+			// A4 纸,边距 0(打印机自身有物理边距)
+			Paper paper = pf.getPaper();
+			paper.setImageableArea(20, 20, paper.getWidth() - 40, paper.getHeight() - 40);
+			pf.setPaper(paper);
+
+			job.setPrintable(page, pf);
+
+			PrintRequestAttributeSet attrs = new HashPrintRequestAttributeSet();
+			attrs.add(MediaSizeName.ISO_A4);
+			attrs.add(new Copies(1));
+
+			job.print(attrs);
+		} catch (PrinterException e) {
+			throw e;
+		} catch (Exception e) {
+			throw new PrinterException("流转卡打印失败: " + e.getMessage(), e);
+		}
+	}
+
+	@Override
+	public void printLabel(LabelData data) {
+		throw new PrinterException("HP 打印机不支持标签打印,请配置 TSC 打印机");
+	}
+}

+ 21 - 0
src/com/mes/print/impl/NonePrinter.java

@@ -0,0 +1,21 @@
+package com.mes.print.impl;
+
+import com.mes.print.model.FlowCardData;
+import com.mes.print.model.LabelData;
+
+/**
+ * 空实现:当 printer.type=none 时使用。所有打印调用直接静默返回,
+ * 便于业务代码无脑调用 Printer.printXxx(),本机没配打印机时自动跳过。
+ */
+public class NonePrinter implements PrinterImpl {
+
+	@Override
+	public void printFlowCard(FlowCardData data) {
+		// no-op
+	}
+
+	@Override
+	public void printLabel(LabelData data) {
+		// no-op
+	}
+}

+ 14 - 0
src/com/mes/print/impl/PrinterImpl.java

@@ -0,0 +1,14 @@
+package com.mes.print.impl;
+
+import com.mes.print.model.FlowCardData;
+import com.mes.print.model.LabelData;
+
+/**
+ * 打印实现接口(内部使用,业务代码请用 com.mes.print.Printer 门面类)
+ */
+public interface PrinterImpl {
+
+	void printFlowCard(FlowCardData data);
+
+	void printLabel(LabelData data);
+}

+ 45 - 0
src/com/mes/print/impl/TscPrinter.java

@@ -0,0 +1,45 @@
+package com.mes.print.impl;
+
+import javax.print.Doc;
+import javax.print.DocFlavor;
+import javax.print.DocPrintJob;
+import javax.print.PrintService;
+import javax.print.SimpleDoc;
+
+import com.mes.print.PrinterException;
+import com.mes.print.config.PrinterConfig;
+import com.mes.print.model.FlowCardData;
+import com.mes.print.model.LabelData;
+import com.mes.print.template.LabelTspl;
+import com.mes.print.util.PrinterUtil;
+
+/**
+ * TSC TTP-244 Pro 及同类热敏标签机:走 TSPL 原始字节流
+ * 需要在 Windows 装 TSC 官方驱动,Java 通过驱动 pass-through 到打印机
+ */
+public class TscPrinter implements PrinterImpl {
+
+	@Override
+	public void printFlowCard(FlowCardData data) {
+		throw new PrinterException("TSC 打印机不支持流转卡打印,请配置 HP 打印机");
+	}
+
+	@Override
+	public void printLabel(LabelData data) {
+		if (data == null) throw new PrinterException("标签数据为空");
+
+		String name = PrinterConfig.get("printer.name");
+		PrintService svc = PrinterUtil.find(name);
+
+		byte[] tspl = LabelTspl.build(data);
+		try {
+			DocPrintJob job = svc.createPrintJob();
+			Doc doc = new SimpleDoc(tspl, DocFlavor.BYTE_ARRAY.AUTOSENSE, null);
+			job.print(doc, null);
+		} catch (PrinterException e) {
+			throw e;
+		} catch (Exception e) {
+			throw new PrinterException("标签打印失败: " + e.getMessage(), e);
+		}
+	}
+}

+ 40 - 0
src/com/mes/print/model/FlowCardData.java

@@ -0,0 +1,40 @@
+package com.mes.print.model;
+
+/**
+ * 流转卡数据(A4 打印)
+ */
+public class FlowCardData {
+
+	private String product;   // 产品编号
+	private String sn;        // 序列号
+	private String steelSn;   // 钢印码(用于镭雕流程)
+	private String opNo;      // 工序号
+	private String batch;     // 批次
+	private String date;      // 日期
+	private String operator;  // 操作员
+	private String remark;    // 备注
+
+	public String getProduct() { return product; }
+	public void setProduct(String product) { this.product = product; }
+
+	public String getSn() { return sn; }
+	public void setSn(String sn) { this.sn = sn; }
+
+	public String getSteelSn() { return steelSn; }
+	public void setSteelSn(String steelSn) { this.steelSn = steelSn; }
+
+	public String getOpNo() { return opNo; }
+	public void setOpNo(String opNo) { this.opNo = opNo; }
+
+	public String getBatch() { return batch; }
+	public void setBatch(String batch) { this.batch = batch; }
+
+	public String getDate() { return date; }
+	public void setDate(String date) { this.date = date; }
+
+	public String getOperator() { return operator; }
+	public void setOperator(String operator) { this.operator = operator; }
+
+	public String getRemark() { return remark; }
+	public void setRemark(String remark) { this.remark = remark; }
+}

+ 28 - 0
src/com/mes/print/model/LabelData.java

@@ -0,0 +1,28 @@
+package com.mes.print.model;
+
+/**
+ * 标签数据(TSC 热敏打印)
+ */
+public class LabelData {
+
+	private String title;    // 标题(可选)
+	private String sn;       // 序列号
+	private String barcode;  // 一维条码内容
+	private String qrcode;   // 二维码内容(可选)
+	private int copies = 1;  // 打印份数
+
+	public String getTitle() { return title; }
+	public void setTitle(String title) { this.title = title; }
+
+	public String getSn() { return sn; }
+	public void setSn(String sn) { this.sn = sn; }
+
+	public String getBarcode() { return barcode; }
+	public void setBarcode(String barcode) { this.barcode = barcode; }
+
+	public String getQrcode() { return qrcode; }
+	public void setQrcode(String qrcode) { this.qrcode = qrcode; }
+
+	public int getCopies() { return copies; }
+	public void setCopies(int copies) { this.copies = copies; }
+}

+ 57 - 0
src/com/mes/print/template/FlowCardRenderer.java

@@ -0,0 +1,57 @@
+package com.mes.print.template;
+
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.print.PageFormat;
+import java.awt.print.Printable;
+
+import com.mes.print.model.FlowCardData;
+
+/**
+ * 流转卡渲染器(Java2D,A4)
+ * 这是最小可用版本,后续按真实模板调整字段位置/表格样式
+ */
+public class FlowCardRenderer {
+
+	public static Printable render(final FlowCardData data) {
+		return new Printable() {
+			@Override
+			public int print(Graphics g, PageFormat pf, int idx) {
+				if (idx > 0) return NO_SUCH_PAGE;
+				Graphics2D g2 = (Graphics2D) g;
+				g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
+						RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+				g2.translate(pf.getImageableX(), pf.getImageableY());
+				draw(g2, data);
+				return PAGE_EXISTS;
+			}
+		};
+	}
+
+	private static void draw(Graphics2D g, FlowCardData d) {
+		// 标题
+		g.setFont(new Font("宋体", Font.BOLD, 22));
+		g.drawString("生产流转卡", 220, 40);
+
+		// 分隔线
+		g.drawLine(30, 60, 550, 60);
+
+		// 内容
+		g.setFont(new Font("宋体", Font.PLAIN, 14));
+		int y = 100;
+		int step = 32;
+		g.drawString("产品编号: " + safe(d.getProduct()), 50, y); y += step;
+		g.drawString("序列号 : " + safe(d.getSn()),      50, y); y += step;
+		g.drawString("工序号 : " + safe(d.getOpNo()),    50, y); y += step;
+		g.drawString("批次  : " + safe(d.getBatch()),   50, y); y += step;
+		g.drawString("日期  : " + safe(d.getDate()),    50, y); y += step;
+		g.drawString("操作员 : " + safe(d.getOperator()),50, y); y += step;
+		g.drawString("备注  : " + safe(d.getRemark()),  50, y);
+	}
+
+	private static String safe(String s) {
+		return s == null ? "" : s;
+	}
+}

+ 58 - 0
src/com/mes/print/template/LabelTspl.java

@@ -0,0 +1,58 @@
+package com.mes.print.template;
+
+import java.io.UnsupportedEncodingException;
+
+import com.mes.print.PrinterException;
+import com.mes.print.model.LabelData;
+
+/**
+ * TSPL 指令生成器(TSC TTP-244 Pro)
+ * 默认标签规格 40x30mm,用户可后续按真实规格调整常量或改为读 config
+ * 注意:TSC 244 默认无中文字库,此模板只使用 ASCII 内容
+ */
+public class LabelTspl {
+
+	// TODO: 后续按真实标签规格调整
+	private static final String SIZE       = "40 mm,30 mm";
+	private static final String GAP        = "2 mm,0 mm";
+	private static final int    DIRECTION  = 1;
+
+	public static byte[] build(LabelData d) {
+		StringBuilder sb = new StringBuilder(256);
+		sb.append("SIZE ").append(SIZE).append("\r\n");
+		sb.append("GAP ").append(GAP).append("\r\n");
+		sb.append("DIRECTION ").append(DIRECTION).append("\r\n");
+		sb.append("CLS\r\n");
+
+		if (nonEmpty(d.getTitle())) {
+			sb.append("TEXT 20,20,\"3\",0,1,1,\"").append(esc(d.getTitle())).append("\"\r\n");
+		}
+		if (nonEmpty(d.getSn())) {
+			sb.append("TEXT 20,60,\"2\",0,1,1,\"SN:").append(esc(d.getSn())).append("\"\r\n");
+		}
+		if (nonEmpty(d.getBarcode())) {
+			sb.append("BARCODE 20,100,\"128\",60,1,0,2,2,\"").append(esc(d.getBarcode())).append("\"\r\n");
+		}
+		if (nonEmpty(d.getQrcode())) {
+			sb.append("QRCODE 260,60,H,5,A,0,\"").append(esc(d.getQrcode())).append("\"\r\n");
+		}
+
+		int copies = d.getCopies() <= 0 ? 1 : d.getCopies();
+		sb.append("PRINT 1,").append(copies).append("\r\n");
+
+		try {
+			return sb.toString().getBytes("US-ASCII");
+		} catch (UnsupportedEncodingException e) {
+			throw new PrinterException("生成 TSPL 失败", e);
+		}
+	}
+
+	private static boolean nonEmpty(String s) {
+		return s != null && !s.isEmpty();
+	}
+
+	/** 简单转义 TSPL 引号 */
+	private static String esc(String s) {
+		return s.replace("\"", "'");
+	}
+}

+ 197 - 0
src/com/mes/print/ui/PrinterSettingDialog.java

@@ -0,0 +1,197 @@
+package com.mes.print.ui;
+
+import com.mes.print.Printer;
+import com.mes.print.model.FlowCardData;
+import com.mes.print.model.LabelData;
+
+import javax.swing.*;
+import java.awt.*;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 打印机设置对话框(通用版,跟随 mes-print.jar 一起发布)。
+ * 任何 Swing 客户端都可以直接使用:
+ * <pre>
+ *   new PrinterSettingDialog(parentFrame).setVisible(true);
+ * </pre>
+ * 保存后立即生效,无需重启。
+ */
+public class PrinterSettingDialog extends JDialog {
+
+	private static final Font FONT_LABEL = new Font("微软雅黑", Font.PLAIN, 18);
+	private static final Font FONT_INPUT = new Font("微软雅黑", Font.PLAIN, 16);
+
+	private JRadioButton hpRadio;
+	private JRadioButton tscRadio;
+	private JRadioButton noneRadio;
+	private JComboBox<String> printerCombo;
+
+	public PrinterSettingDialog(Frame owner) {
+		super(owner, "打印机设置", true);
+		buildUI();
+		loadCurrent();
+		reloadPrinters();
+		pack();
+		setLocationRelativeTo(owner);
+	}
+
+	private void buildUI() {
+		setLayout(new BorderLayout(0, 0));
+
+		JPanel center = new JPanel(new GridBagLayout());
+		center.setBorder(BorderFactory.createEmptyBorder(20, 25, 10, 25));
+		GridBagConstraints c = new GridBagConstraints();
+		c.insets = new Insets(10, 8, 10, 8);
+		c.anchor = GridBagConstraints.WEST;
+
+		c.gridx = 0; c.gridy = 0;
+		center.add(newLabel("打印类型:"), c);
+
+		hpRadio   = newRadio("流转卡 (HP)");
+		tscRadio  = newRadio("标签 (TSC)");
+		noneRadio = newRadio("不打印");
+		ButtonGroup g = new ButtonGroup();
+		g.add(hpRadio); g.add(tscRadio); g.add(noneRadio);
+		JPanel typePanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 15, 0));
+		typePanel.add(hpRadio); typePanel.add(tscRadio); typePanel.add(noneRadio);
+		c.gridx = 1; c.fill = GridBagConstraints.HORIZONTAL; c.weightx = 1.0;
+		center.add(typePanel, c);
+
+		c.gridx = 0; c.gridy = 1; c.fill = GridBagConstraints.NONE; c.weightx = 0;
+		center.add(newLabel("目标打印机:"), c);
+
+		printerCombo = new JComboBox<>();
+		printerCombo.setEditable(true);
+		printerCombo.setFont(FONT_INPUT);
+		printerCombo.setPreferredSize(new Dimension(320, 32));
+
+		JButton refreshBtn = newButton("刷新");
+		refreshBtn.addActionListener(e -> reloadPrinters());
+
+		JPanel printerPanel = new JPanel(new BorderLayout(6, 0));
+		printerPanel.add(printerCombo, BorderLayout.CENTER);
+		printerPanel.add(refreshBtn, BorderLayout.EAST);
+		c.gridx = 1; c.fill = GridBagConstraints.HORIZONTAL; c.weightx = 1.0;
+		center.add(printerPanel, c);
+
+		JLabel hint = new JLabel("<html><span style='color:gray'>下拉列出本机所有系统打印机,也可手动输入名字片段(支持模糊匹配)</span></html>");
+		hint.setFont(new Font("微软雅黑", Font.PLAIN, 12));
+		c.gridx = 0; c.gridy = 2; c.gridwidth = 2; c.fill = GridBagConstraints.HORIZONTAL;
+		center.add(hint, c);
+
+		add(center, BorderLayout.CENTER);
+
+		JPanel bottom = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 10));
+		JButton testBtn   = newButton("测试打印");
+		JButton okBtn     = newButton("保存");
+		JButton cancelBtn = newButton("取消");
+		testBtn.addActionListener(e -> doTest());
+		okBtn.addActionListener(e -> doSave());
+		cancelBtn.addActionListener(e -> dispose());
+		bottom.add(testBtn); bottom.add(okBtn); bottom.add(cancelBtn);
+		add(bottom, BorderLayout.SOUTH);
+	}
+
+	private JLabel newLabel(String text) {
+		JLabel l = new JLabel(text);
+		l.setFont(FONT_LABEL);
+		return l;
+	}
+
+	private JRadioButton newRadio(String text) {
+		JRadioButton r = new JRadioButton(text);
+		r.setFont(FONT_LABEL);
+		return r;
+	}
+
+	private JButton newButton(String text) {
+		JButton b = new JButton(text);
+		b.setFont(FONT_INPUT);
+		return b;
+	}
+
+	private void loadCurrent() {
+		String type = Printer.getCurrentType();
+		String name = Printer.getCurrentName();
+		if ("hp".equals(type))       hpRadio.setSelected(true);
+		else if ("tsc".equals(type)) tscRadio.setSelected(true);
+		else                         noneRadio.setSelected(true);
+		if (name != null && !name.isEmpty()) printerCombo.setSelectedItem(name);
+	}
+
+	private void reloadPrinters() {
+		Object cur = printerCombo.getSelectedItem();
+		printerCombo.removeAllItems();
+		List<String> all = Printer.listAvailablePrinters();
+		for (String n : all) printerCombo.addItem(n);
+		if (cur != null && !cur.toString().isEmpty()) {
+			printerCombo.setSelectedItem(cur);
+		}
+	}
+
+	private String currentType() {
+		if (hpRadio.isSelected())  return "hp";
+		if (tscRadio.isSelected()) return "tsc";
+		return "none";
+	}
+
+	private String currentName() {
+		Object v = printerCombo.getSelectedItem();
+		return v == null ? "" : v.toString().trim();
+	}
+
+	private void doTest() {
+		String type = currentType();
+		String name = currentName();
+		if ("none".equals(type)) { msg("请先选择打印类型(流转卡/标签)"); return; }
+		if (name.isEmpty())      { msg("请先选择或填写目标打印机"); return; }
+		try {
+			Printer.setPrinter(type, name);
+			if ("hp".equals(type)) sendTestFlowCard();
+			else                   sendTestLabel();
+			msg("测试打印已发送");
+		} catch (Exception ex) {
+			err("测试打印失败:" + ex.getMessage());
+		}
+	}
+
+	private void sendTestFlowCard() {
+		FlowCardData d = new FlowCardData();
+		d.setSn("TEST0001");
+		d.setProduct("+TEST");
+		d.setOpNo("TEST-OP");
+		d.setBatch("TEST-BATCH");
+		d.setDate(new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new Date()));
+		d.setOperator("tester");
+		d.setRemark("这是一张测试流转卡");
+		Printer.printFlowCard(d);
+	}
+
+	private void sendTestLabel() {
+		LabelData l = new LabelData();
+		l.setTitle("TEST");
+		l.setSn("TEST0001");
+		l.setBarcode("TEST0001");
+		Printer.printLabel(l);
+	}
+
+	private void doSave() {
+		String type = currentType();
+		String name = currentName();
+		if (!"none".equals(type) && name.isEmpty()) {
+			msg("请选择或填写目标打印机"); return;
+		}
+		try {
+			Printer.setPrinter(type, "none".equals(type) ? "" : name);
+			msg("打印机配置已保存");
+			dispose();
+		} catch (Exception ex) {
+			err("保存失败:" + ex.getMessage());
+		}
+	}
+
+	private void msg(String s) { JOptionPane.showMessageDialog(this, s, "提示", JOptionPane.INFORMATION_MESSAGE); }
+	private void err(String s) { JOptionPane.showMessageDialog(this, s, "错误", JOptionPane.ERROR_MESSAGE); }
+}

+ 41 - 0
src/com/mes/print/util/PrinterUtil.java

@@ -0,0 +1,41 @@
+package com.mes.print.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.print.PrintService;
+import javax.print.PrintServiceLookup;
+
+import com.mes.print.PrinterException;
+
+/**
+ * 打印机查找工具
+ */
+public class PrinterUtil {
+
+	/** 按名字精确或模糊匹配(contains),找不到抛异常 */
+	public static PrintService find(String name) {
+		if (name == null || name.trim().isEmpty()) {
+			throw new PrinterException("未配置打印机名 (printer.name)");
+		}
+		PrintService[] services = PrintServiceLookup.lookupPrintServices(null, null);
+		// 先精确匹配
+		for (PrintService s : services) {
+			if (s.getName().equalsIgnoreCase(name)) return s;
+		}
+		// 再模糊匹配
+		for (PrintService s : services) {
+			if (s.getName().toLowerCase().contains(name.toLowerCase())) return s;
+		}
+		throw new PrinterException("未找到打印机: " + name + ",本机可用: " + listNames());
+	}
+
+	/** 列出本机所有可用打印机名字 */
+	public static List<String> listNames() {
+		List<String> list = new ArrayList<>();
+		for (PrintService s : PrintServiceLookup.lookupPrintServices(null, null)) {
+			list.add(s.getName());
+		}
+		return list;
+	}
+}