Files
biz_xfj_cloud/lib/common/utils/app/control/app_control.dart
T
2026-09-04 16:23:30 +08:00

440 lines
15 KiB
Dart

import 'dart:async';
import 'package:common_utils/common_utils.dart';
import 'package:event_bus/event_bus.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:get/get.dart';
import 'package:pj_supermarket_cloud/common/index.dart';
enum AppStartupStepStatus {
/// 尚未开始执行当前启动步骤。
waiting,
/// 当前启动步骤正在执行。
running,
/// 当前启动步骤已完整完成。
success,
/// 当前能力受限,但不阻断应用进入业务页面。
warning,
/// 核心启动依赖失败,应用不能继续进入业务页面。
failure,
}
/// Load 页展示的单个启动步骤状态。
class AppStartupStepState {
/// 创建一条不可变的启动步骤展示数据。
const AppStartupStepState({
required this.title,
required this.detail,
this.status = AppStartupStepStatus.waiting,
});
/// 启动步骤标题。
final String title;
/// 当前进度或处理结果说明。
final String detail;
/// 当前步骤状态。
final AppStartupStepStatus status;
/// 复制当前步骤并替换可变的状态和说明。
AppStartupStepState copyWith({
String? detail,
AppStartupStepStatus? status,
}) {
return AppStartupStepState(
title: title,
detail: detail ?? this.detail,
status: status ?? this.status,
);
}
}
/// 应用启动、数据库和终端硬件生命周期的统一控制器。
class AppControl extends GetxController {
/// 启动流程日志标签。
static const String _tag = "AppControl";
/// Load 页按执行顺序展示的启动步骤集合。
final startupSteps = <AppStartupStepState>[
const AppStartupStepState(
title: '加载终端配置',
detail: '等待读取设备模式与本地设置',
),
const AppStartupStepState(
title: '打开业务数据库',
detail: '等待准备订单、商品与用户数据',
),
const AppStartupStepState(
title: '初始化设备服务',
detail: '等待准备打印、读卡与电子秤服务',
),
const AppStartupStepState(
title: '准备人脸识别',
detail: '等待检查激活参数与本地人脸库',
),
].obs;
/// 核心启动失败时面向用户展示的统一说明。
final startupFailureMessage = ''.obs;
/// 所有阻塞启动步骤完成后为 true。
final startupFinished = false.obs;
final startupEnvironmentDescription = '正在识别终端运行环境'.obs;
final startupEnvironmentDetail = '双屏、虚拟机与硬件输入模式将按设备能力自动适配'.obs;
final startupStatusLabel = '正在启动'.obs;
/// 最近一次启动阶段的人脸准备结果;尚未执行时为空。
final faceStartupPreparationResult = Rxn<FaceStartupPreparationResult>();
bool _initializing = false;
bool _databaseInitialized = false;
/// 当前账号是否允许启动需要鉴权或设备身份的 MQTT、心跳和后台同步任务。
bool _authenticatedServicesEnabled = false;
/// 网络监听和消息连接在执行前读取的会话开关;退出动作会先关闭它再释放资源。
bool get authenticatedServicesEnabled => _authenticatedServicesEnabled;
int get startupCompletedCount => startupSteps
.where((item) =>
item.status == AppStartupStepStatus.success ||
item.status == AppStartupStepStatus.warning)
.length;
double get startupProgress =>
startupSteps.isEmpty ? 0 : startupCompletedCount / startupSteps.length;
/// 打印机
late PrintManage printManage;
/// 读卡器串口
late SerialCardManage serialCardManage;
/// 称串口
late SerialSteelyardManage srialSteelyardManage;
/// mq
StreamSubscription? mqListen;
EventBus mqUpdateEventBus = EventBus();
/// 数据库
late final AppDatabase dataBase;
// 是否有网络
RxBool isNetwork = RxBool(false);
@override
void onClose() {
super.onClose();
_authenticatedServicesEnabled = false;
if (printManageInitialized) printManage.close();
if (serialCardManageInitialized) serialCardManage.close();
if (steelyardManageInitialized) srialSteelyardManage.close();
AppNetworkTool.closeListenNetwork();
TtsManage.dispose();
SyncCheckTool.instance.dispose();
TerminalHeartbeatTool.stop();
}
bool get printManageInitialized {
try {
printManage;
return true;
} catch (_) {
return false;
}
}
bool get serialCardManageInitialized {
try {
serialCardManage;
return true;
} catch (_) {
return false;
}
}
bool get steelyardManageInitialized {
try {
srialSteelyardManage;
return true;
} catch (_) {
return false;
}
}
Future<void> initAsync({
String destination = RouteNames.login,
bool force = false,
}) async {
if (_initializing || (startupFinished.value && !force)) return;
_initializing = true;
startupFinished.value = false;
startupFailureMessage.value = '';
faceStartupPreparationResult.value = null;
startupStatusLabel.value = '正在启动';
_resetStartupSteps();
try {
_setStartupStep(
0,
status: AppStartupStepStatus.running,
detail: '正在读取运行环境与本地终端配置',
);
await AppRuntimeTool.initializePlatform();
await AppSettingTool.removeSettingCache();
_setStartupStep(
1,
status: AppStartupStepStatus.running,
detail: '正在打开本地业务数据库',
);
if (!_databaseInitialized) {
dataBase = await AppDatabase.getDataBase();
_databaseInitialized = true;
}
final setting = await AppSettingTool.getSysSetting();
// 插件会在 Android 注册阶段自动创建副屏;这里统一确认可用状态并记录常驻标记,
// 即使无副屏或权限不足也只影响设备状态,不阻塞后续启动流程。
unawaited(AppSubScreenTool.show());
var storageGranted =
await Permission.manageExternalStorage.request().isGranted;
if (!storageGranted) {
storageGranted = await Permission.storage.request().isGranted;
}
AppRuntimeTool.applyManualHardwareInput(setting.manualHardwareInput);
final environment = _resolveEnvironment(setting);
startupEnvironmentDescription.value = environment.title;
startupEnvironmentDetail.value = environment.detail;
_setStartupStep(
0,
status: AppStartupStepStatus.success,
detail: environment.stepDetail,
);
_setStartupStep(
1,
status: storageGranted
? AppStartupStepStatus.success
: AppStartupStepStatus.warning,
detail:
storageGranted ? '数据库已打开,本地备份目录权限可用' : '数据库已打开;未获存储权限,本地备份可能不可用',
);
_setStartupStep(
2,
status: AppStartupStepStatus.running,
detail: '正在创建打印、读卡与电子秤管理服务',
);
if (!printManageInitialized) printManage = PrintManage();
await AppDeviceStatusTool.loadCachedStatus();
if (!serialCardManageInitialized) serialCardManage = SerialCardManage();
if (!steelyardManageInitialized) {
srialSteelyardManage = SerialSteelyardManage();
}
// 硬件检查失败只能影响状态展示,不能阻塞收银终端进入业务页面。
unawaited(
AppDeviceStatusTool.refreshAll()
.catchError((Object error, StackTrace stack) {
logger.w('|$_tag|首次检查设备状态失败error:|$error|stack:|$stack|');
return AppDeviceStatusTool.status.value;
}),
);
await TtsManage.initialize();
_setStartupStep(
2,
status: environment.manualHardwareInput
? AppStartupStepStatus.warning
: AppStartupStepStatus.success,
detail: environment.manualHardwareInput
? '当前为手动硬件输入模式,实体设备将在业务使用时按需连接'
: '设备管理服务已就绪,实体设备将在业务使用时按需连接',
);
_setStartupStep(
3,
status: AppStartupStepStatus.running,
detail: '正在检查激活参数并装载本地人脸库',
);
final facePreparation = await AppFaceTool.prepareForStartup();
faceStartupPreparationResult.value = facePreparation;
_setStartupStep(
3,
status: facePreparation.status == FaceStartupPreparationStatus.success
? AppStartupStepStatus.success
: AppStartupStepStatus.warning,
detail: _facePreparationDetail(facePreparation),
);
final backupSucceeded =
storageGranted ? await AppFileTool.backupSqlFile() : false;
if (storageGranted && !backupSucceeded) {
_setStartupStep(
1,
status: AppStartupStepStatus.warning,
detail: '数据库已打开;本次自动备份未完成,可稍后在设置中重试',
);
}
// 网络、MQTT 与后台同步属于运行期能力,不参与加载页检查或阻塞进入系统。
if (AppNetworkTool.subscription == null) {
AppNetworkTool.listenNetwork();
}
final userSetting = await AppSettingTool.getUserSetting();
_authenticatedServicesEnabled =
ObjectUtil.isNotEmpty(userSetting.accessToken);
if (_authenticatedServicesEnabled) {
await SyncCheckTool.instance.startAll();
TerminalHeartbeatTool.start();
}
await TerminalAutoUpdateTool.recoverPendingUpdate();
startupFinished.value = true;
startupStatusLabel.value = startupSteps
.any((item) => item.status == AppStartupStepStatus.warning)
? '受限启动完成'
: '启动完成';
await Future.delayed(const Duration(milliseconds: 650));
if (Get.currentRoute == RouteNames.load) {
Get.offAllNamed(destination);
}
} catch (e, stack) {
final currentIndex = startupSteps.indexWhere(
(item) => item.status == AppStartupStepStatus.running,
);
if (currentIndex >= 0) {
_setStartupStep(
currentIndex,
status: AppStartupStepStatus.failure,
detail: '启动失败,请检查终端配置或本地设备后重试',
);
}
startupStatusLabel.value = '启动失败';
startupFailureMessage.value = '终端初始化未完成,请检查设备状态后重试。';
_closePartiallyInitializedResources();
logger.e('|$_tag|初始化失败error:|$e|stack:|$stack|');
} finally {
_initializing = false;
}
}
Future<void> retryStartup({String destination = RouteNames.login}) async {
if (_initializing) return;
await initAsync(destination: destination, force: true);
}
/// 登录成功后恢复业务终端的后台任务与在线心跳。
Future<void> resumeAuthenticatedServices() async {
_authenticatedServicesEnabled = true;
await SyncCheckTool.instance.startAll();
TerminalHeartbeatTool.start();
}
/// 退出登录时停止所有需要账号或设备身份的后台任务,并断开消息订阅。
Future<void> suspendAuthenticatedServices() async {
_authenticatedServicesEnabled = false;
await SyncCheckTool.instance.dispose();
TerminalHeartbeatTool.stop();
AppMqTool.mqListenClose();
await MqManager.closeMqtt();
}
/// 关闭已创建的设备和后台资源,供核心启动异常后的重试流程重新初始化。
void _closePartiallyInitializedResources() {
_authenticatedServicesEnabled = false;
if (printManageInitialized) printManage.close();
if (serialCardManageInitialized) serialCardManage.close();
if (steelyardManageInitialized) srialSteelyardManage.close();
AppNetworkTool.closeListenNetwork();
SyncCheckTool.instance.dispose();
TerminalHeartbeatTool.stop();
}
/// 将全部启动步骤恢复到初始文案,供首次启动和人工重试共用。
void _resetStartupSteps() {
for (var index = 0; index < startupSteps.length; index++) {
final item = startupSteps[index];
startupSteps[index] = item.copyWith(
status: AppStartupStepStatus.waiting,
detail: switch (index) {
0 => '等待读取设备模式与本地设置',
1 => '等待准备订单、商品与用户数据',
2 => '等待准备打印、读卡与电子秤服务',
_ => '等待检查激活参数与本地人脸库',
},
);
}
}
/// 更新指定启动步骤的状态和结果文案。
void _setStartupStep(
int index, {
required AppStartupStepStatus status,
required String detail,
}) {
startupSteps[index] = startupSteps[index].copyWith(
status: status,
detail: detail,
);
}
/// 将结构化人脸准备结果转换为不暴露密钥的 Load 页简短说明。
String _facePreparationDetail(FaceStartupPreparationResult result) {
if (result.total <= 0) return result.message;
if (result.failedCount <= 0) {
return '${result.message},已装载 ${result.successCount} 个人脸样本';
}
return '${result.message},成功 ${result.successCount},失败 ${result.failedCount}';
}
({
String title,
String detail,
String stepDetail,
bool manualHardwareInput,
}) _resolveEnvironment(SysSetting setting) {
final manualHardwareInput =
AppRuntimeTool.isEmulator || setting.manualHardwareInput;
if (AppRuntimeTool.isEmulator) {
return (
title: '虚拟机调试模式',
detail: '已切换为手动硬件输入,不启动真实副屏和实体串口设备。',
stepDetail: '已识别虚拟机,终端配置读取完成',
manualHardwareInput: true,
);
}
if (manualHardwareInput) {
return (
title: '手动硬件输入模式',
detail: '终端配置为手动录入硬件结果,真实副屏仍按设备能力启用。',
stepDetail: '终端配置读取完成,已启用手动硬件输入',
manualHardwareInput: true,
);
}
if (AppRuntimeTool.hasMultipleDisplays) {
return (
title: '双屏终端模式',
detail: '已检测到 ${AppRuntimeTool.displayCount} 个显示设备,副屏将在业务页面按需启动。',
stepDetail: '终端配置读取完成,已检测到可用双屏环境',
manualHardwareInput: false,
);
}
return (
title: '单屏终端模式',
detail: '当前仅检测到主屏,收银核心流程可继续使用,副屏功能不会启动。',
stepDetail: '终端配置读取完成,当前为单屏环境',
manualHardwareInput: false,
);
}
}