package com.mes.laser; import com.alibaba.fastjson2.JSONArray; import com.alibaba.fastjson2.JSONObject; import com.mes.ui.DataUtil; import com.mes.print.PrintHelper; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Properties; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /** * 镭雕流程管理器 —— 新方案:客户端不再联动镭雕机,改为定时轮询 mescloud * 拉取本产线待打印列表 → 建档 → 打印流转卡 → 上报 ack。 * * 类名和公共 API 保持与旧版一致,MesClient.java 里的调用点不用改。 * 旧的"获取镭雕码 / 完成回调 / 模拟"等语义在新流程下已经没有意义, * 相关方法保留但退化为空操作或转发到轮询开关。 * * 数据流:定时器 → GET /mesLaser/pendingPrint → 内存 Set 去重 → 单线程消费 * → onSubmitResult 建档(走 sendCreate 报文)→ LaserCardPrintUtil.printCard * → POST /mesLaser/printAck * @author hzd */ public class LaserFlowManager { public interface UiCallback { /** 状态栏提示,error=true 走错误样式 */ void onStatus(String msg, boolean error); /** 打印开始/结束时的按钮锁定信号,UI 用来防止操作员打印进行中重复点击 */ void onLockButton(boolean lock); /** 客户码回显到扫码框 */ void onCodeReceived(String code); /** 客户端建档回调(走 op040 sendCreate 报文),true=建档成功 */ boolean onSubmitResult(String customerSn, String qret); /** 待打印数量变化通知,UI 用来更新 pending label */ void onPendingCount(int count); /** * 队列快照更新:3 行队列 * rows[0] = 最近打完的一条(done=true,UI 显示绿框),首次启动没打过时为 null * rows[1..N] = 服务端 pending 最早的几条(done=false,UI 显示灰框) */ void onQueueUpdated(java.util.List rows); } /** 队列行数据:钢印码 + 客户码 + 是否已打印 */ public static class QueueRow { public final String steelSn; public final String customerSn; public final boolean done; public QueueRow(String steelSn, String customerSn, boolean done) { this.steelSn = steelSn; this.customerSn = customerSn; this.done = done; } } private final UiCallback uiCallback; private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> { Thread t = new Thread(r, "LaserPoll-Scheduler"); t.setDaemon(true); return t; }); // 单线程打印,避免并发抢打印机队列,也保证扫码框回显顺序 private final ExecutorService printerExecutor = Executors.newSingleThreadExecutor(r -> { Thread t = new Thread(r, "LaserPoll-Printer"); t.setDaemon(true); return t; }); // 内存去重:已经在处理(打印中/待 ack)的客户码,防止 ack 之前被同一次拉取里的下一批重复消费 private final Set processing = ConcurrentHashMap.newKeySet(); // 队列快照:最近打完的一条 + 上次轮询拿到的 pending 列表 private volatile QueueRow lastPrintedRow = null; private volatile java.util.List lastPending = new java.util.ArrayList<>(); // paused = true 等价于手动模式(轮询定时器空转) private volatile boolean paused = false; private volatile boolean running = false; // ==== 配置项 ==== private String lineSn = "XT"; private String prodCode = "T09"; private long pollIntervalMs = 3000; private int batchSize = 10; private String printerName = ""; // 启动时默认模式:auto=自动轮询打印,manual=只在点击后打印一次 private String defaultMode = "manual"; public LaserFlowManager(UiCallback uiCallback) { this.uiCallback = uiCallback; loadConfig(); } private void loadConfig() { try (InputStream is = ClassLoader.getSystemResourceAsStream("config/config.properties")) { if (is == null) return; Properties pro = new Properties(); try (BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"))) { pro.load(br); } lineSn = pro.getProperty("mes.line_sn", "T9").trim(); prodCode = pro.getProperty("mes.prod_code", "T09").trim(); pollIntervalMs = Long.parseLong(pro.getProperty("mes.laser.poll_interval", "3000").trim()); batchSize = Integer.parseInt(pro.getProperty("mes.laser.poll_batch_size", "10").trim()); printerName = pro.getProperty("mes.laser.printer_name", "").trim(); defaultMode = pro.getProperty("mes.laser.print_mode", "manual").trim().toLowerCase(); paused = "manual".equals(defaultMode); } catch (Exception e) { e.printStackTrace(); } } // ==== 对外 API(兼容 MesClient.java 原调用点)==== /** 应用启动时调一次。旧名字沿用,实际启动的是轮询定时器 */ public void startCompleteServer() { if (running) return; running = true; scheduler.scheduleWithFixedDelay(this::pollOnce, 500, pollIntervalMs, TimeUnit.MILLISECONDS); String mode = paused ? "手动模式(点按钮打一次)" : "自动模式(间隔 " + pollIntervalMs + " ms)"; uiCallback.onStatus("镭雕轮询已启动 - " + mode, false); } /** 应用关闭时调一次 */ public void stopCompleteServer() { running = false; scheduler.shutdownNow(); printerExecutor.shutdown(); } /** * 旧 UI 用来判断"镭雕进行中"以禁用按钮。新方案下轮询是常态、单件打印是异步, * 直接返回是否有正在处理的记录即可。为了不误锁 UI,保守返回 false。 */ public boolean isWaitingComplete() { return false; } /** 设置自动打印开关:true=开启定时轮询自动打印,false=只更新 UI 不自动打(靠队列行按钮打) */ public void setAutoMode(boolean auto) { paused = !auto; uiCallback.onStatus(auto ? "自动打印:开" : "自动打印:关", false); } public boolean isAutoMode() { return !paused; } /** 提交钢印码并打印,独立于轮询队列。操作员手工输入 steelSn 后调用 */ public void submitAndPrint(String steelSn) { if (steelSn == null || steelSn.trim().isEmpty()) { uiCallback.onStatus("请输入钢印码", true); return; } final String ss = steelSn.trim(); printerExecutor.submit(() -> { uiCallback.onLockButton(true); try { uiCallback.onStatus("正在提交钢印码到服务端:" + ss, false); JSONObject resp = DataUtil.submitSteelSn(prodCode, ss); if (resp == null || resp.get("result") == null || !"true".equalsIgnoreCase(resp.get("result").toString())) { String msg = resp != null && resp.get("message") != null ? resp.get("message").toString() : "服务端无响应"; uiCallback.onStatus("提交失败:" + msg, true); return; } JSONObject data = resp.getJSONObject("data"); String customerSn = data == null ? null : data.getString("customerSn"); if (customerSn == null || customerSn.isEmpty()) { uiCallback.onStatus("服务端未返回客户码,无法打印", true); return; } // 回显客户码到扫码框 uiCallback.onCodeReceived(customerSn); uiCallback.onStatus("已入库,正在建档并打印:" + customerSn, false); // 直接打印(此时已在 printerExecutor 单线程里,直接 printOne 串行执行) if (!processing.add(customerSn)) return; try { printOne(customerSn, ss); } finally { processing.remove(customerSn); } } finally { uiCallback.onLockButton(false); } }); } // ==== 内部实现 ==== /** 定时器每 pollIntervalMs 触发一次。无论开关状态都拉数据更新 UI,只有开关 ON 时才自动打印 */ private void pollOnce() { pollBatch(batchSize, !paused); } /** * 拉待打印列表并更新 UI 队列;printAll=true 时把拉到的每一条都送去打印(仅自动模式用) */ private void pollBatch(int limit, boolean printAll) { try { JSONObject resp = DataUtil.listPendingPrint(lineSn, limit); if (resp == null || resp.get("result") == null || !"true".equalsIgnoreCase(resp.get("result").toString())) { return; } Object dataObj = resp.get("data"); if (!(dataObj instanceof JSONArray)) return; JSONArray list = (JSONArray) dataObj; uiCallback.onPendingCount(list.size()); // 更新 pending 快照供 UI 展示:取前 5 条足够填 5 行 UI(即使 Row0 让给了 lastPrinted) java.util.List newPending = new java.util.ArrayList<>(); for (int i = 0; i < Math.min(5, list.size()); i++) { JSONObject r = list.getJSONObject(i); newPending.add(new QueueRow(r.getString("steel_sn"), r.getString("customer_sn"), false)); } lastPending = newPending; pushQueueSnapshot(); if (!printAll || list.isEmpty()) return; // 自动模式:把拉到的每一条都送去打印(内存 processing 去重防止重复) for (int i = 0; i < list.size(); i++) { JSONObject row = list.getJSONObject(i); submitPrintJob(row.getString("customer_sn"), row.getString("steel_sn")); } } catch (Exception e) { e.printStackTrace(); } } /** 打印指定的一条:队列 UI 行点「打印」按钮走这里 */ public void printSpecific(String customerSn, String steelSn) { if (customerSn == null || customerSn.isEmpty() || steelSn == null || steelSn.isEmpty()) { uiCallback.onStatus("参数缺失,无法打印", true); return; } submitPrintJob(customerSn, steelSn); } /** 把一次打印任务塞到单线程 executor,内存去重防止同一 sn 并发打两次 */ private void submitPrintJob(String customerSn, String steelSn) { if (customerSn == null || steelSn == null) return; if (!processing.add(customerSn)) return; printerExecutor.submit(() -> { try { uiCallback.onLockButton(true); uiCallback.onCodeReceived(customerSn); uiCallback.onStatus("正在建档并打印:" + customerSn, false); printOne(customerSn, steelSn); } finally { processing.remove(customerSn); uiCallback.onLockButton(false); } }); } private void printOne(String customerSn, String steelSn) { // 1. 建档(走 op040 现有 sendCreate 报文) boolean buildOk = uiCallback.onSubmitResult(customerSn, "OK"); if (!buildOk) { uiCallback.onStatus("建档失败:" + customerSn + ",下次继续重试", true); DataUtil.ackPrint(customerSn, "NG", "客户端建档失败"); return; } // 2. 打印流转卡(通过 mes-print 模块,保留 PDF 模板 + 二维码格式) boolean printOk = PrintHelper.printFlowCard(customerSn, steelSn); // 3. ack:让服务端把 print_status 从 0 更新为 1 if (printOk) { boolean ackOk = DataUtil.ackPrint(customerSn, "OK", null); if (ackOk) { uiCallback.onStatus("打印完成并已入库:" + customerSn, false); } else { uiCallback.onStatus("打印完成但 ack 失败(服务端会保留待打印状态):" + customerSn, true); } // 顶部绿框显示刚打完的这条 lastPrintedRow = new QueueRow(steelSn, customerSn, true); // 先清空 lastPending 里跟刚打完这条重复的记录,避免 UI 出现 "已打印+待打印" 两行内容一样 java.util.List filtered = new java.util.ArrayList<>(); for (QueueRow r : lastPending) { if (r != null && !customerSn.equals(r.customerSn)) filtered.add(r); } lastPending = filtered; pushQueueSnapshot(); // 立即触发一次刷新(printAll=false 只更新 UI 不打印,避免重复打) scheduler.execute(() -> pollBatch(batchSize, false)); } else { DataUtil.ackPrint(customerSn, "NG", "客户端打印失败"); uiCallback.onStatus("打印失败:" + customerSn, true); } } /** * 合成 5 行队列推给 UI。策略: * 有 lastPrinted:Row 0 = lastPrinted(绿框),Row 1-4 = pending[0..3] * 没有:Row 0-4 全部是 pending[0..4]("只有 1 条就在第一行") */ private void pushQueueSnapshot() { final int total = 5; java.util.List rows = new java.util.ArrayList<>(total); java.util.List p = lastPending; if (lastPrintedRow != null) { rows.add(lastPrintedRow); for (int i = 0; i < total - 1; i++) { rows.add(i < p.size() ? p.get(i) : null); } } else { for (int i = 0; i < total; i++) { rows.add(i < p.size() ? p.get(i) : null); } } uiCallback.onQueueUpdated(rows); } }