Files
biz_xfj_cloud/lib/common/utils/sync/sync_print_tool.dart
T
dzc e68f41375b feat:取消系统自带软键盘设计,改为系统自用键盘组件-初步
feat:将打印分为图片打印和命令式打印-初步
2026-09-20 18:24:01 +08:00

332 lines
12 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 'dart:convert';
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] 只有高于本地失败版本时才允许再次进入打印队列;
/// [printCmd] 是后端拼装的 Base64 指令流,指令打印模式直写 USB,可空。
static Future<void> addTask({
required String orderId,
String? androidOrderId,
required String historyId,
required String pic,
String? printCmd,
String printType = '1',
int printNo = 1,
int retryVersion = 0,
}) async {
final operation = _enqueueTail.then((_) => _addTask(
orderId: orderId,
androidOrderId: androidOrderId,
historyId: historyId,
pic: pic,
printCmd: printCmd,
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? printCmd,
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
..printCmd = printCmd?.trim().isNotEmpty == true ? printCmd : null
..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,
printCmd: printCmd?.trim().isNotEmpty == true ? printCmd : null,
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;
}
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!,
printCmd: task.printCmd,
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;
// 指令解码在尝试循环外完成一次:Base64 损坏属于任务数据问题,重试不会好转,直接回退图片。
final commandBytes = await _resolveCommandBytes(task);
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 = commandBytes != null
? await app.printManage.printCommand(commandBytes)
: 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);
}
/// 指令打印模式下解码任务携带的 Base64 打印指令流。
///
/// 图片模式、任务无指令、内容为空或 Base64 损坏时返回 null,调用方回退图片栅格打印;
/// 解码失败不进入 USB 重试节奏,因为重试同一份损坏数据不会改变结果。
static Future<List<int>?> _resolveCommandBytes(
PrintHistoryEntity task,
) async {
final setting = await AppSettingTool.getSysSetting();
if (setting.printerOutputMode != AppSettingTool.printerOutputModeCommand) {
return null;
}
final raw = task.printCmd?.trim() ?? '';
if (raw.isEmpty) return null;
try {
final decoded = base64.decode(raw.replaceAll(RegExp(r'\s'), ''));
if (decoded.isEmpty) {
logger.w('|$_tag|打印指令内容为空,回退图片打印|task:${task.servicePrintId}|');
return null;
}
return decoded;
} catch (error) {
logger.w(
'|$_tag|打印指令解码失败,回退图片打印|task:${task.servicePrintId}|error:$error|');
return null;
}
}
/// 向服务端提交当前重试版本的最终结果;失败时保留 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);
}
}