Files
biz_xfj_cloud/lib/common/utils/sync/sync_print_tool.dart
T
2026-09-04 13:15:55 +08:00

290 lines
9.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<void> _enqueueTail = Future<void>.value();
/// 三次 USB 尝试前的等待时间:首次立即执行,后两次失败后分别等待 5 秒和 30 秒。
///
/// 方案同时限定“最多三次 USB 尝试”,因此不会再安排会形成第 4 次出纸的 120 秒重试。
static const attemptDelays = <Duration>[
Duration.zero,
Duration(seconds: 5),
Duration(seconds: 30),
];
/// 将 MQTT 或 HTTP 待办按服务端任务 ID 转换为唯一的本地任务。
///
/// [orderId] 是服务端订单 ID,[androidOrderId] 允许线上预约订单为空;[historyId]
/// 是本地唯一去重键,[retryVersion] 只有高于本地失败版本时才允许再次进入打印队列。
static Future<void> 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<void>(
(_) {},
onError: (Object _, StackTrace __) {},
);
await operation;
}
/// 在串行入队区内执行任务新增或失败版本更新。
static Future<void> _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<AppControl>().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<void> pullPendingTasks() async {
if (_isPulling || !AppNetworkTool.isAutoNetwork()) {
return;
}
final setting = await AppSettingTool.getSysSetting();
if (setting.terminalRole != TerminalRole.printer ||
setting.serviceId == null) {
return;
}
_isPulling = true;
try {
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<void> doCheckPrint() async {
if (_isPrint) {
return;
}
_isPrint = true;
final dao = Get.find<AppControl>().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<void> doCheckSync() async {
if (_isSync || !AppNetworkTool.isAutoNetwork()) {
return;
}
_isSync = true;
final dao = Get.find<AppControl>().dataBase.getPrintHistoryDao;
try {
for (final task in await dao.getPendingResults()) {
await _report(task);
}
} finally {
_isSync = false;
}
}
/// 串行执行单个任务的最多三次 USB 尝试,并在最终状态落库后补报服务端。
static Future<void> _print(PrintHistoryEntity task) async {
final app = Get.find<AppControl>();
final dao = app.dataBase.getPrintHistoryDao;
for (var index = 0; index < attemptDelays.length; index++) {
final delay = attemptDelays[index];
if (delay > Duration.zero) {
await Future<void>.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<void> _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<AppControl>().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);
}
}