LaserFlowManager.java 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. package com.mes.laser;
  2. import com.alibaba.fastjson2.JSONArray;
  3. import com.alibaba.fastjson2.JSONObject;
  4. import com.mes.ui.DataUtil;
  5. import com.mes.print.PrintHelper;
  6. import java.io.BufferedReader;
  7. import java.io.InputStream;
  8. import java.io.InputStreamReader;
  9. import java.util.Properties;
  10. import java.util.Set;
  11. import java.util.concurrent.ConcurrentHashMap;
  12. import java.util.concurrent.ExecutorService;
  13. import java.util.concurrent.Executors;
  14. import java.util.concurrent.ScheduledExecutorService;
  15. import java.util.concurrent.TimeUnit;
  16. /**
  17. * 镭雕流程管理器 —— 新方案:客户端不再联动镭雕机,改为定时轮询 mescloud
  18. * 拉取本产线待打印列表 → 建档 → 打印流转卡 → 上报 ack。
  19. *
  20. * 类名和公共 API 保持与旧版一致,MesClient.java 里的调用点不用改。
  21. * 旧的"获取镭雕码 / 完成回调 / 模拟"等语义在新流程下已经没有意义,
  22. * 相关方法保留但退化为空操作或转发到轮询开关。
  23. *
  24. * 数据流:定时器 → GET /mesLaser/pendingPrint → 内存 Set 去重 → 单线程消费
  25. * → onSubmitResult 建档(走 sendCreate 报文)→ LaserCardPrintUtil.printCard
  26. * → POST /mesLaser/printAck
  27. * @author hzd
  28. */
  29. public class LaserFlowManager {
  30. public interface UiCallback {
  31. /** 状态栏提示,error=true 走错误样式 */
  32. void onStatus(String msg, boolean error);
  33. /** 打印开始/结束时的按钮锁定信号,UI 用来防止操作员打印进行中重复点击 */
  34. void onLockButton(boolean lock);
  35. /** 客户码回显到扫码框 */
  36. void onCodeReceived(String code);
  37. /** 客户端建档回调(走 op040 sendCreate 报文),true=建档成功 */
  38. boolean onSubmitResult(String customerSn, String qret);
  39. /** 待打印数量变化通知,UI 用来更新 pending label */
  40. void onPendingCount(int count);
  41. /**
  42. * 队列快照更新:3 行队列
  43. * rows[0] = 最近打完的一条(done=true,UI 显示绿框),首次启动没打过时为 null
  44. * rows[1..N] = 服务端 pending 最早的几条(done=false,UI 显示灰框)
  45. */
  46. void onQueueUpdated(java.util.List<QueueRow> rows);
  47. }
  48. /** 队列行数据:钢印码 + 客户码 + 是否已打印 */
  49. public static class QueueRow {
  50. public final String steelSn;
  51. public final String customerSn;
  52. public final boolean done;
  53. public QueueRow(String steelSn, String customerSn, boolean done) {
  54. this.steelSn = steelSn;
  55. this.customerSn = customerSn;
  56. this.done = done;
  57. }
  58. }
  59. private final UiCallback uiCallback;
  60. private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
  61. Thread t = new Thread(r, "LaserPoll-Scheduler");
  62. t.setDaemon(true);
  63. return t;
  64. });
  65. // 单线程打印,避免并发抢打印机队列,也保证扫码框回显顺序
  66. private final ExecutorService printerExecutor = Executors.newSingleThreadExecutor(r -> {
  67. Thread t = new Thread(r, "LaserPoll-Printer");
  68. t.setDaemon(true);
  69. return t;
  70. });
  71. // 内存去重:已经在处理(打印中/待 ack)的客户码,防止 ack 之前被同一次拉取里的下一批重复消费
  72. private final Set<String> processing = ConcurrentHashMap.newKeySet();
  73. // 队列快照:最近打完的一条 + 上次轮询拿到的 pending 列表
  74. private volatile QueueRow lastPrintedRow = null;
  75. private volatile java.util.List<QueueRow> lastPending = new java.util.ArrayList<>();
  76. // paused = true 等价于手动模式(轮询定时器空转)
  77. private volatile boolean paused = false;
  78. private volatile boolean running = false;
  79. // ==== 配置项 ====
  80. private String lineSn = "XT";
  81. private String prodCode = "T09";
  82. private long pollIntervalMs = 3000;
  83. private int batchSize = 10;
  84. private String printerName = "";
  85. // 启动时默认模式:auto=自动轮询打印,manual=只在点击后打印一次
  86. private String defaultMode = "manual";
  87. public LaserFlowManager(UiCallback uiCallback) {
  88. this.uiCallback = uiCallback;
  89. loadConfig();
  90. }
  91. private void loadConfig() {
  92. try (InputStream is = ClassLoader.getSystemResourceAsStream("config/config.properties")) {
  93. if (is == null) return;
  94. Properties pro = new Properties();
  95. try (BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"))) {
  96. pro.load(br);
  97. }
  98. lineSn = pro.getProperty("mes.line_sn", "T9").trim();
  99. prodCode = pro.getProperty("mes.prod_code", "T09").trim();
  100. pollIntervalMs = Long.parseLong(pro.getProperty("mes.laser.poll_interval", "3000").trim());
  101. batchSize = Integer.parseInt(pro.getProperty("mes.laser.poll_batch_size", "10").trim());
  102. printerName = pro.getProperty("mes.laser.printer_name", "").trim();
  103. defaultMode = pro.getProperty("mes.laser.print_mode", "manual").trim().toLowerCase();
  104. paused = "manual".equals(defaultMode);
  105. } catch (Exception e) {
  106. e.printStackTrace();
  107. }
  108. }
  109. // ==== 对外 API(兼容 MesClient.java 原调用点)====
  110. /** 应用启动时调一次。旧名字沿用,实际启动的是轮询定时器 */
  111. public void startCompleteServer() {
  112. if (running) return;
  113. running = true;
  114. scheduler.scheduleWithFixedDelay(this::pollOnce, 500, pollIntervalMs, TimeUnit.MILLISECONDS);
  115. String mode = paused ? "手动模式(点按钮打一次)" : "自动模式(间隔 " + pollIntervalMs + " ms)";
  116. uiCallback.onStatus("镭雕轮询已启动 - " + mode, false);
  117. }
  118. /** 应用关闭时调一次 */
  119. public void stopCompleteServer() {
  120. running = false;
  121. scheduler.shutdownNow();
  122. printerExecutor.shutdown();
  123. }
  124. /**
  125. * 旧 UI 用来判断"镭雕进行中"以禁用按钮。新方案下轮询是常态、单件打印是异步,
  126. * 直接返回是否有正在处理的记录即可。为了不误锁 UI,保守返回 false。
  127. */
  128. public boolean isWaitingComplete() {
  129. return false;
  130. }
  131. /** 设置自动打印开关:true=开启定时轮询自动打印,false=只更新 UI 不自动打(靠队列行按钮打) */
  132. public void setAutoMode(boolean auto) {
  133. paused = !auto;
  134. uiCallback.onStatus(auto ? "自动打印:开" : "自动打印:关", false);
  135. }
  136. public boolean isAutoMode() {
  137. return !paused;
  138. }
  139. /** 提交钢印码并打印,独立于轮询队列。操作员手工输入 steelSn 后调用 */
  140. public void submitAndPrint(String steelSn) {
  141. if (steelSn == null || steelSn.trim().isEmpty()) {
  142. uiCallback.onStatus("请输入钢印码", true);
  143. return;
  144. }
  145. final String ss = steelSn.trim();
  146. printerExecutor.submit(() -> {
  147. uiCallback.onLockButton(true);
  148. try {
  149. uiCallback.onStatus("正在提交钢印码到服务端:" + ss, false);
  150. JSONObject resp = DataUtil.submitSteelSn(prodCode, ss);
  151. if (resp == null || resp.get("result") == null
  152. || !"true".equalsIgnoreCase(resp.get("result").toString())) {
  153. String msg = resp != null && resp.get("message") != null
  154. ? resp.get("message").toString() : "服务端无响应";
  155. uiCallback.onStatus("提交失败:" + msg, true);
  156. return;
  157. }
  158. JSONObject data = resp.getJSONObject("data");
  159. String customerSn = data == null ? null : data.getString("customerSn");
  160. if (customerSn == null || customerSn.isEmpty()) {
  161. uiCallback.onStatus("服务端未返回客户码,无法打印", true);
  162. return;
  163. }
  164. // 回显客户码到扫码框
  165. uiCallback.onCodeReceived(customerSn);
  166. uiCallback.onStatus("已入库,正在建档并打印:" + customerSn, false);
  167. // 直接打印(此时已在 printerExecutor 单线程里,直接 printOne 串行执行)
  168. if (!processing.add(customerSn)) return;
  169. try {
  170. printOne(customerSn, ss);
  171. } finally {
  172. processing.remove(customerSn);
  173. }
  174. } finally {
  175. uiCallback.onLockButton(false);
  176. }
  177. });
  178. }
  179. // ==== 内部实现 ====
  180. /** 定时器每 pollIntervalMs 触发一次。无论开关状态都拉数据更新 UI,只有开关 ON 时才自动打印 */
  181. private void pollOnce() {
  182. pollBatch(batchSize, !paused);
  183. }
  184. /**
  185. * 拉待打印列表并更新 UI 队列;printAll=true 时把拉到的每一条都送去打印(仅自动模式用)
  186. */
  187. private void pollBatch(int limit, boolean printAll) {
  188. try {
  189. JSONObject resp = DataUtil.listPendingPrint(lineSn, limit);
  190. if (resp == null || resp.get("result") == null
  191. || !"true".equalsIgnoreCase(resp.get("result").toString())) {
  192. return;
  193. }
  194. Object dataObj = resp.get("data");
  195. if (!(dataObj instanceof JSONArray)) return;
  196. JSONArray list = (JSONArray) dataObj;
  197. uiCallback.onPendingCount(list.size());
  198. // 更新 pending 快照供 UI 展示:取前 5 条足够填 5 行 UI(即使 Row0 让给了 lastPrinted)
  199. java.util.List<QueueRow> newPending = new java.util.ArrayList<>();
  200. for (int i = 0; i < Math.min(5, list.size()); i++) {
  201. JSONObject r = list.getJSONObject(i);
  202. newPending.add(new QueueRow(r.getString("steel_sn"), r.getString("customer_sn"), false));
  203. }
  204. lastPending = newPending;
  205. pushQueueSnapshot();
  206. if (!printAll || list.isEmpty()) return;
  207. // 自动模式:把拉到的每一条都送去打印(内存 processing 去重防止重复)
  208. for (int i = 0; i < list.size(); i++) {
  209. JSONObject row = list.getJSONObject(i);
  210. submitPrintJob(row.getString("customer_sn"), row.getString("steel_sn"));
  211. }
  212. } catch (Exception e) {
  213. e.printStackTrace();
  214. }
  215. }
  216. /** 打印指定的一条:队列 UI 行点「打印」按钮走这里 */
  217. public void printSpecific(String customerSn, String steelSn) {
  218. if (customerSn == null || customerSn.isEmpty() || steelSn == null || steelSn.isEmpty()) {
  219. uiCallback.onStatus("参数缺失,无法打印", true);
  220. return;
  221. }
  222. submitPrintJob(customerSn, steelSn);
  223. }
  224. /** 把一次打印任务塞到单线程 executor,内存去重防止同一 sn 并发打两次 */
  225. private void submitPrintJob(String customerSn, String steelSn) {
  226. if (customerSn == null || steelSn == null) return;
  227. if (!processing.add(customerSn)) return;
  228. printerExecutor.submit(() -> {
  229. try {
  230. uiCallback.onLockButton(true);
  231. uiCallback.onCodeReceived(customerSn);
  232. uiCallback.onStatus("正在建档并打印:" + customerSn, false);
  233. printOne(customerSn, steelSn);
  234. } finally {
  235. processing.remove(customerSn);
  236. uiCallback.onLockButton(false);
  237. }
  238. });
  239. }
  240. private void printOne(String customerSn, String steelSn) {
  241. // 1. 建档(走 op040 现有 sendCreate 报文)
  242. boolean buildOk = uiCallback.onSubmitResult(customerSn, "OK");
  243. if (!buildOk) {
  244. uiCallback.onStatus("建档失败:" + customerSn + ",下次继续重试", true);
  245. DataUtil.ackPrint(customerSn, "NG", "客户端建档失败");
  246. return;
  247. }
  248. // 2. 打印流转卡(通过 mes-print 模块,保留 PDF 模板 + 二维码格式)
  249. boolean printOk = PrintHelper.printFlowCard(customerSn, steelSn);
  250. // 3. ack:让服务端把 print_status 从 0 更新为 1
  251. if (printOk) {
  252. boolean ackOk = DataUtil.ackPrint(customerSn, "OK", null);
  253. if (ackOk) {
  254. uiCallback.onStatus("打印完成并已入库:" + customerSn, false);
  255. } else {
  256. uiCallback.onStatus("打印完成但 ack 失败(服务端会保留待打印状态):" + customerSn, true);
  257. }
  258. // 顶部绿框显示刚打完的这条
  259. lastPrintedRow = new QueueRow(steelSn, customerSn, true);
  260. // 先清空 lastPending 里跟刚打完这条重复的记录,避免 UI 出现 "已打印+待打印" 两行内容一样
  261. java.util.List<QueueRow> filtered = new java.util.ArrayList<>();
  262. for (QueueRow r : lastPending) {
  263. if (r != null && !customerSn.equals(r.customerSn)) filtered.add(r);
  264. }
  265. lastPending = filtered;
  266. pushQueueSnapshot();
  267. // 立即触发一次刷新(printAll=false 只更新 UI 不打印,避免重复打)
  268. scheduler.execute(() -> pollBatch(batchSize, false));
  269. } else {
  270. DataUtil.ackPrint(customerSn, "NG", "客户端打印失败");
  271. uiCallback.onStatus("打印失败:" + customerSn, true);
  272. }
  273. }
  274. /**
  275. * 合成 5 行队列推给 UI。策略:
  276. * 有 lastPrinted:Row 0 = lastPrinted(绿框),Row 1-4 = pending[0..3]
  277. * 没有:Row 0-4 全部是 pending[0..4]("只有 1 条就在第一行")
  278. */
  279. private void pushQueueSnapshot() {
  280. final int total = 5;
  281. java.util.List<QueueRow> rows = new java.util.ArrayList<>(total);
  282. java.util.List<QueueRow> p = lastPending;
  283. if (lastPrintedRow != null) {
  284. rows.add(lastPrintedRow);
  285. for (int i = 0; i < total - 1; i++) {
  286. rows.add(i < p.size() ? p.get(i) : null);
  287. }
  288. } else {
  289. for (int i = 0; i < total; i++) {
  290. rows.add(i < p.size() ? p.get(i) : null);
  291. }
  292. }
  293. uiCallback.onQueueUpdated(rows);
  294. }
  295. }