| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325 |
- 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<QueueRow> 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<String> processing = ConcurrentHashMap.newKeySet();
- // 队列快照:最近打完的一条 + 上次轮询拿到的 pending 列表
- private volatile QueueRow lastPrintedRow = null;
- private volatile java.util.List<QueueRow> 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<QueueRow> 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<QueueRow> 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<QueueRow> rows = new java.util.ArrayList<>(total);
- java.util.List<QueueRow> 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);
- }
- }
|