import 'dart:async'; import 'package:common_utils/common_utils.dart'; import 'package:get/get.dart'; import 'package:pj_supermarket_cloud/common/index.dart'; /// 业务终端的 EY_PRINT 代理队列,负责双通道入队、版本幂等、串行 USB 重试和结果补报。 abstract class SyncPrintTool { /// 打印队列诊断日志使用的稳定模块标签。 static const _tag = 'sync打印工具'; /// 是否已有串行打印循环正在执行,防止两个任务同时占用 USB 通道。 static bool _isPrint = false; /// 是否已有打印结果补报正在执行,防止网络恢复和定时任务重复提交。 static bool _isSync = false; /// 是否已有 HTTP 待办补拉正在执行,防止 MQTT 重连和页面恢复重复拉取。 static bool _isPulling = false; /// 将 MQTT 与 HTTP 的并发入队串行化,避免唯一索引插入竞争。 static Future _enqueueTail = Future.value(); /// 三次 USB 尝试前的等待时间:首次立即执行,后两次失败后分别等待 5 秒和 30 秒。 /// /// 方案同时限定“最多三次 USB 尝试”,因此不会再安排会形成第 4 次出纸的 120 秒重试。 static const attemptDelays = [ Duration.zero, Duration(seconds: 5), Duration(seconds: 30), ]; /// 将 MQTT 或 HTTP 待办按服务端任务 ID 转换为唯一的本地任务。 /// /// [orderId] 是服务端订单 ID,[androidOrderId] 允许线上预约订单为空;[historyId] /// 是本地唯一去重键,[retryVersion] 只有高于本地失败版本时才允许再次进入打印队列。 static Future addTask({ required String orderId, String? androidOrderId, required String historyId, required String pic, String printType = '1', int printNo = 1, int retryVersion = 0, }) async { final operation = _enqueueTail.then((_) => _addTask( orderId: orderId, androidOrderId: androidOrderId, historyId: historyId, pic: pic, printType: printType, printNo: printNo, retryVersion: retryVersion, )); // 队尾吞掉上一项异常以保证后续任务仍可入队,当前调用者仍等待原 operation 获得真实结果。 _enqueueTail = operation.then( (_) {}, onError: (Object _, StackTrace __) {}, ); await operation; } /// 在串行入队区内执行任务新增或失败版本更新。 static Future _addTask({ required String orderId, String? androidOrderId, required String historyId, required String pic, required String printType, required int printNo, required int retryVersion, }) async { final dao = Get.find().dataBase.getPrintHistoryDao; final existing = await dao.findByServicePrintId(historyId); if (existing != null) { if (!canApplyTaskVersion(existing, retryVersion)) { if (retryVersion > existing.retryVersion && existing.status != PrintStatus.ERROR) { logger.w( '|$_tag|忽略非失败任务的超前版本|task:$historyId|status:${existing.status.name}|'); return; } logger.i('|$_tag|忽略重复打印任务|task:$historyId|version:$retryVersion|'); return; } existing ..orderId = androidOrderId?.trim().isNotEmpty == true ? androidOrderId! : orderId ..serviceOrderId = orderId ..invoiceUrl = pic ..printType = printType ..printNo = printNo ..retryVersion = retryVersion ..attemptCount = 0 ..lastError = null ..resultPending = false ..status = PrintStatus.NONE ..printTime = null ..syncTime = null; await dao.edit(existing); } else { await dao.save(PrintHistoryEntity( orderId: androidOrderId?.trim().isNotEmpty == true ? androidOrderId! : orderId, serviceOrderId: orderId, servicePrintId: historyId, invoiceUrl: pic, printType: printType, printNo: printNo, retryVersion: retryVersion, status: PrintStatus.NONE, createTime: DateTime.now(), )); } logger.i('|$_tag|打印任务已入队|task:$historyId|version:$retryVersion|'); unawaited(doCheckPrint()); } /// 只有本地任务已失败且服务端版本更高时,才允许同一任务重新出纸。 static bool canApplyTaskVersion( PrintHistoryEntity existing, int incomingVersion) => existing.status == PrintStatus.ERROR && incomingVersion > existing.retryVersion; /// 在登录、MQTT 重连、应用恢复和周期检查时补拉当前业务终端代理的待办。 static Future pullPendingTasks() async { if (_isPulling || !AppNetworkTool.isAutoNetwork()) { return; } var setting = await AppSettingTool.getSysSetting(); if (setting.serviceId == null) return; _isPulling = true; try { setting = await AppSettingTool.updatePrinterBinding( await HardApi.getPrinterBinding(), ); if (setting.printerHardId == null) return; final tasks = await HardApi.getPrinterTasks(); for (final task in tasks) { if (task.historyId == null || task.orderId == null || task.pic == null) { continue; } await addTask( orderId: task.orderId!, androidOrderId: task.androidOrderCode, historyId: task.historyId!, pic: task.pic!, printType: task.printType ?? '1', printNo: task.printNo ?? 1, retryVersion: task.retryVersion ?? 0, ); } } catch (error, stack) { logger.w('|$_tag|补拉打印待办失败|error:$error|stack:$stack|'); } finally { _isPulling = false; } } /// 串行处理全部待打印任务,同一时间只允许一个 USB 写入流程。 static Future doCheckPrint() async { if (_isPrint) { return; } _isPrint = true; final dao = Get.find().dataBase.getPrintHistoryDao; try { // 循环查库可接住打印期间新入队的 MQTT/HTTP 任务,不必等待下一次十分钟补偿扫描。 while (true) { final list = await dao.getAll(PrintStatus.NONE.name); if (list.isEmpty) { break; } for (final task in list) { await _print(task); } } } catch (error, stack) { logger.e('|$_tag|打印队列执行异常|error:$error|stack:$stack|'); } finally { _isPrint = false; } } /// 补报本地已经完成的成功或失败结果,断网不会导致再次出纸。 static Future doCheckSync() async { if (_isSync || !AppNetworkTool.isAutoNetwork()) { return; } _isSync = true; final dao = Get.find().dataBase.getPrintHistoryDao; try { for (final task in await dao.getPendingResults()) { await _report(task); } } finally { _isSync = false; } } /// 串行执行单个任务的最多三次 USB 尝试,并在最终状态落库后补报服务端。 static Future _print(PrintHistoryEntity task) async { final app = Get.find(); final dao = app.dataBase.getPrintHistoryDao; for (var index = 0; index < attemptDelays.length; index++) { final delay = attemptDelays[index]; if (delay > Duration.zero) { await Future.delayed(delay); } task.attemptCount = index + 1; await dao.edit(task); var opened = false; try { opened = await app.printManage.open(); if (!opened) { throw StateError(app.printManage.lastError ?? 'USB 打印机连接失败'); } final accepted = await app.printManage.printImg(task.invoiceUrl); if (!accepted) { throw StateError(app.printManage.lastError ?? 'USB 未完整接收打印数据'); } task ..status = PrintStatus.SUCCESS ..printTime = DateUtil.formatDate(DateTime.now()) ..lastError = null ..resultPending = true; await dao.edit(task); await _report(task); return; } catch (error, stack) { task.lastError = _errorText(error); await dao.edit(task); logger.w( '|$_tag|USB 打印尝试失败|task:${task.servicePrintId}|attempt:${task.attemptCount}|error:$error|stack:$stack|'); } finally { if (opened) { await app.printManage.close(); } } } task ..status = PrintStatus.ERROR ..resultPending = true; await dao.edit(task); await _report(task); } /// 向服务端提交当前重试版本的最终结果;失败时保留 resultPending 等待网络恢复补报。 static Future _report(PrintHistoryEntity task) async { if (!AppNetworkTool.isAutoNetwork()) { return; } final success = task.status == PrintStatus.SUCCESS || task.status == PrintStatus.SYNC; try { final accepted = await HardApi.reportPrinterResult( taskId: task.servicePrintId, orderId: task.serviceOrderId, retryVersion: task.retryVersion, success: success, attemptCount: task.attemptCount, errorMessage: success ? null : task.lastError, ); if (!accepted) { throw StateError('服务端拒绝打印结果'); } task ..resultPending = false ..syncTime = DateTime.now(); if (success) { task.status = PrintStatus.SYNC; } await Get.find().dataBase.getPrintHistoryDao.edit(task); } catch (error, stack) { logger.w( '|$_tag|打印结果等待补报|task:${task.servicePrintId}|error:$error|stack:$stack|'); } } /// 将本地异常压缩成最多 500 个字符的单行可上报文本,避免持久化堆栈或敏感上下文。 static String _errorText(Object error) { final text = error.toString().replaceAll(RegExp(r'\s+'), ' ').trim(); return text.length <= 500 ? text : text.substring(0, 500); } }