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 = [ 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(); 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 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 clearedSavedPassword = await AppSettingTool.applyStartupPowerLossPasswordPolicy(); var resolvedDestination = destination; if (clearedSavedPassword && destination == RouteNames.terminal) { // 凭据已按策略清除,跳过更新恢复直达终端的预留路径,回到登录页。 resolvedDestination = RouteNames.login; } 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(resolvedDestination); } } 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 retryStartup({String destination = RouteNames.login}) async { if (_initializing) return; await initAsync(destination: destination, force: true); } /// 登录成功后恢复业务终端的后台任务与在线心跳。 Future resumeAuthenticatedServices() async { _authenticatedServicesEnabled = true; await SyncCheckTool.instance.startAll(); TerminalHeartbeatTool.start(); } /// 退出登录时停止所有需要账号或设备身份的后台任务,并断开消息订阅。 Future 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, ); } }