290 lines
9.9 KiB
Dart
290 lines
9.9 KiB
Dart
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);
|
||
}
|
||
}
|