import 'dart:async'; import 'package:common_utils/common_utils.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:forui/forui.dart'; import 'package:get/get.dart'; import 'package:pj_supermarket_cloud/common/index.dart'; class SettingController extends GetxController { static const String _tag = "设置 Controller"; final appC = Get.find(); Rx sysSetting = Rx(null); Rx userSetting = Rx(null); /// 是否使用人工输入代替扫码、读卡和人脸等真实硬件输入。 final manualHardwareInput = false.obs; /// Android Kiosk 当前能力与运行状态,应用恢复前台时刷新。 final kioskStatus = AppRuntimeTool.kioskStatus.obs; /// Root 手动刷新是否正在执行,用于禁用重复点击并展示局部进度。 final rootRefreshing = false.obs; /// Root 一键配置 Device Owner 是否正在执行,命令期间禁止重复提交。 final kioskProvisioning = false.obs; /// Kiosk 开关是否正在同步原生 Lock Task,防止连续点击形成状态反转。 final kioskSwitching = false.obs; /// 最近一次 Root 配置 Kiosk 的失败处理建议;成功或重新加载设置后清空。 final kioskProvisionMessage = ''.obs; // 全局文字大小 var globalFontSizeC = FRadioSelectGroupController( value: SettingGlobalFontSize.xl3.code, ); /// 支付设置的界面排序,初始顺序来自枚举并由本地持久化覆盖。 final payTypeOrder = [ ...OrderPayDisplayType.values, ].obs; /// 允许在收银弹框内重新识别人员的支付方式集合;初始化采用业务默认值,配置加载后以持久化结果为准。 final cashierReidentifiablePayTypes = AppSettingTool.defaultCashierReidentifiablePayTypes.toSet().obs; /// 支付偏好串行保存中;保存期间的新变更会继续下一轮写入。 bool _payPersisting = false; /// 支付偏好存在待写入变更;连续拖动或切换开关时合并为一次落盘。 bool _payPendingPersist = false; /// 商品餐段时间配置;餐段校验弹框和自动切换按该时间范围判定。 final productMealPeriodSchedule = ProductMealPeriodSchedule.defaults.obs; /// 餐段配置正在保存时禁用重复提交并展示按钮状态。 final productMealPeriodSaving = false.obs; _initData() { update(["setting"]); } RxList menuList = [ SettingMenu( prefixIcon: FAssets.icons.bookCopy, title: "基本信息", type: SettingMenuType.baseInfo, ), SettingMenu( prefixIcon: FAssets.icons.databaseBackup, title: "资源备份", type: SettingMenuType.backup, ), SettingMenu( prefixIcon: FAssets.icons.refreshCw, title: "定时任务", type: SettingMenuType.syncSetting, ), SettingMenu( prefixIcon: FAssets.icons.japaneseYen, title: "支付设置", type: SettingMenuType.paySetting, ), SettingMenu( prefixIcon: FAssets.icons.users, title: "离线购买人员", type: SettingMenuType.offlineUserSetting, ), SettingMenu( prefixIcon: FAssets.icons.utensils, title: "餐段时间配置", type: SettingMenuType.mealPeriodSetting, ), SettingMenu( prefixIcon: FAssets.icons.scanFace, title: "人脸管理", type: SettingMenuType.faceSetting, ), SettingMenu( prefixIcon: FAssets.icons.settings2, title: "人脸设置", type: SettingMenuType.facePluginSetting, ), SettingMenu( prefixIcon: FAssets.icons.shoppingCart, title: "商品管理", type: SettingMenuType.productSetting, ), SettingMenu( prefixIcon: FAssets.icons.logs, title: "日志预览", type: SettingMenuType.logPreview, ), SettingMenu( prefixIcon: FAssets.icons.rocket, title: "版本管理", type: SettingMenuType.versionSetting, ), ].obs; Rx cuurentMenuType = SettingMenuType.baseInfo.obs; /// 各任务尚未保存的 Cron 输入草稿,以任务类型隔离编辑状态。 final scheduledTaskCronDrafts = {}.obs; /// 各任务 Cron 草稿的本地校验错误,保存成功或继续输入后移除。 final scheduledTaskCronErrors = {}.obs; /// 正在持久化配置的任务集合,用于禁止同任务重复提交并局部禁用控件。 final savingScheduledTasks = {}.obs; late StreamSubscription mqUpdateListen; @override void onInit() async { super.onInit(); await getSetting(); await _initScheduledTasks(); globalFontSizeC.addValueListener((v) { final fontSizeType = SettingGlobalFontSize.values.fromCode( v.first, ); if (fontSizeType == null) return; final appTheme = Get.find(); appTheme.setSizeScalar(fontSizeType); }); mqUpdateListen = appC.mqUpdateEventBus.on().listen( (event) async { if (event == MqtActionEnum.CONFIG) { sysSetting.value = _orderedSysSetting( await AppSettingTool.getSysSetting(), ); } }, ); } @override void onReady() { super.onReady(); _initData(); } @override void onClose() { super.onClose(); globalFontSizeC.dispose(); mqUpdateListen.cancel(); // 隐藏键盘 // FocusManager.instance.primaryFocus?.unfocus(); // SystemChannels.textInput.invokeMethod('TextInput.hide'); } void doMenu(SettingMenu v) { if (v.type == cuurentMenuType.value) { return; } cuurentMenuType.value = v.type; menuList.refresh(); } /// 按菜单类型直接切换(供人脸设置页跳转人脸管理等场景使用)。 void openMenu(SettingMenuType type) { cuurentMenuType.value = type; menuList.refresh(); } /// 启动统一调度器并用已保存的 Cron 规则初始化草稿。 Future _initScheduledTasks() async { try { await SyncCheckTool.instance.startAll(); scheduledTaskCronDrafts.assignAll({ for (final entry in SyncCheckTool.instance.configs.entries) entry.key: entry.value.cron, }); } catch (e, stack) { logger.e("|$_tag|定时任务初始化失败error:|$e|stack:|$stack|"); } } /// 更新指定任务的 Cron 草稿,输入变化后清除该任务旧校验错误。 void setScheduledTaskCronDraft(ScheduledTaskType type, String value) { scheduledTaskCronDrafts[type] = value; scheduledTaskCronErrors.remove(type); } /// 切换任务自动调度开关;关闭运行保障任务前要求操作人员确认。 Future setScheduledTaskEnabled( ScheduledTaskType type, bool enabled, ) async { final config = SyncCheckTool.instance.configs[type]; if (config == null || config.enabled == enabled) return; if (!enabled && _isRuntimeGuardTask(type)) { final confirmed = await appMessageDialog( Get.context!, '关闭后终端将不再自动执行该运行保障任务,是否继续?', ); if (!confirmed) return; } await _saveScheduledTaskConfig(config.copyWith(enabled: enabled)); } /// 切换任务是否仅在完整空闲窗口自动执行,并持久化到结构化任务表。 Future setScheduledTaskIdleOnly( ScheduledTaskType type, bool idleOnly, ) async { final config = SyncCheckTool.instance.configs[type]; if (config == null || config.idleOnly == idleOnly) return; await _saveScheduledTaskConfig(config.copyWith(idleOnly: idleOnly)); } /// Cron 弹窗返回经过本地解析校验的表达式,Controller 仍在持久化边界再次校验。 Future setScheduledTaskCron( ScheduledTaskType type, String expression, ) async { final config = SyncCheckTool.instance.configs[type]; if (config == null) return; final cron = expression.trim(); if (!SyncCheckTool.isValidCron(cron)) { Toast.error(msg: '请输入有效的 6 段 Cron 表达式'); return; } if (cron == config.cron) return; await _saveScheduledTaskConfig(config.copyWith(cron: cron)); } /// 串联调度重建和 SQLite 保存,负责重复提交保护、草稿回退及用户反馈。 Future _saveScheduledTaskConfig(ScheduledTaskConfig config) async { if (savingScheduledTasks.contains(config.type)) return; final previous = SyncCheckTool.instance.configs[config.type]; savingScheduledTasks.add(config.type); try { await SyncCheckTool.instance.updateConfig(config); scheduledTaskCronDrafts[config.type] = config.cron; scheduledTaskCronErrors.remove(config.type); Toast.success(msg: '任务配置已保存'); } catch (e, stack) { if (previous != null) { scheduledTaskCronDrafts[config.type] = previous.cron; } logger.e( "|$_tag|保存定时任务失败type:${config.type.name}|error:$e|stack:$stack|"); Toast.error(msg: '任务配置保存失败,请重试'); } finally { savingScheduledTasks.remove(config.type); } } /// 手动触发指定任务,并根据立即执行、排队或合并结果向操作人员反馈。 Future runScheduledTaskNow(ScheduledTaskType type) async { final scheduler = SyncCheckTool.instance; if (scheduler.runningTasks.contains(type) || scheduler.pendingTasks.contains(type)) { await scheduler.trigger(type); Toast.success(msg: '已合并到待执行任务'); return; } await scheduler.trigger(type); if (scheduler.pendingTasks.contains(type)) { Toast.success(msg: '任务已排队,将在安全窗口执行'); return; } final record = scheduler.records[type]; if (record?.success == true) { Toast.success(msg: record?.message ?? '任务执行完成'); } else { Toast.error(msg: record?.message ?? '任务执行失败'); } } /// 返回关闭自动调度前需要二次确认的 MQTT、打印、广播和设备状态保障任务。 bool _isRuntimeGuardTask(ScheduledTaskType type) => const { ScheduledTaskType.mqReconnect, ScheduledTaskType.printCheck, ScheduledTaskType.broadcastCheck, ScheduledTaskType.deviceStatus, }.contains(type); // 同步设备信息(人脸设置页等入口使用) void syncConfig() async { final isNetwork = AppNetworkTool.isAutoNetworkMsg(); if (!isNetwork) return; EasyLoading.show( status: '同步中...', maskType: EasyLoadingMaskType.black, ); try { final setting = await SyncConfigTool.autoSyncSysInfo(); if (setting != null) sysSetting.value = setting; Toast.success(msg: "同步设备信息成功"); EasyLoading.dismiss(); } catch (e, stack) { Toast.error(msg: "同步设备信息错误"); logger.e("|$_tag|同步设备信息error:|$e|stack:|$stack|"); EasyLoading.dismiss(); } } // 备份sql void backupsSql() async { EasyLoading.show( status: '备份中...', maskType: EasyLoadingMaskType.black, ); try { final bool = await AppFileTool.backupSqlFile(); if (!bool) throw Error(); EasyLoading.dismiss(); Toast.success(msg: "备份中sql成功"); EasyLoading.dismiss(); } catch (e) { Toast.error(msg: "备份中sql失败"); logger.e("|$_tag|备份中sqlerror:|$e|"); EasyLoading.dismiss(); } } // 备份日志 void backupsLog() async { Toast.success(msg: "当前日志为实时备份"); } // sql恢复 Future recoverSql() async { EasyLoading.show( status: '恢复中...', maskType: EasyLoadingMaskType.black, ); try { await AppFileTool.useLastBackDatabase(); EasyLoading.dismiss(); Toast.success(msg: "sql恢复成功"); EasyLoading.dismiss(); } catch (e) { Toast.error(msg: "sql恢复失败"); logger.e("|$_tag|sql恢复失败error:|$e|"); EasyLoading.dismiss(); } } Future getSetting() async { final setting = await AppSettingTool.getSysSetting(); final storedPayTypeOrder = await AppSettingTool.getPayTypeOrder(); final reidentifiablePayTypes = await AppSettingTool.getCashierReidentifiablePayTypes(); final byCode = { for (final item in OrderPayDisplayType.values) item.code: item, }; final restoredPayTypeOrder = []; for (final code in storedPayTypeOrder) { final item = byCode[code]; if (item != null && !restoredPayTypeOrder.contains(item)) { restoredPayTypeOrder.add(item); } } for (final item in OrderPayDisplayType.values) { if (!restoredPayTypeOrder.contains(item)) { restoredPayTypeOrder.add(item); } } payTypeOrder.assignAll(restoredPayTypeOrder); cashierReidentifiablePayTypes.assignAll(reidentifiablePayTypes); sysSetting.value = _orderedSysSetting(setting); productMealPeriodSchedule.value = await AppSettingTool.getProductMealPeriodSchedule(); manualHardwareInput.value = AppRuntimeTool.isEmulator || sysSetting.value!.manualHardwareInput; AppRuntimeTool.applyManualHardwareInput( sysSetting.value!.manualHardwareInput, ); if (sysSetting.value!.globalFontSize != null) { globalFontSizeC.value = {sysSetting.value!.globalFontSize!.code}; } } /// 将设置中的启用支付方式按本地排序重排;null 保持 null 由收银页按默认处理。 SysSetting _orderedSysSetting(SysSetting setting) { if (setting.usePayDisplayType != null) { setting.usePayDisplayType = payTypeOrder .where(setting.usePayDisplayType!.contains) .toList(); } return setting; } /// 启用或停用支付方式,始终至少保留一种并修正失效的默认方式。 void togglePayType(OrderPayDisplayType type) { final setting = sysSetting.value; if (setting == null) return; final enabled = List.from( setting.usePayDisplayType ?? OrderPayDisplayType.values, ); if (enabled.contains(type)) { if (enabled.length == 1) { Toast.error(msg: '至少保留一种支付方式'); return; } enabled.remove(type); } else { enabled.add(type); } final orderedEnabled = payTypeOrder.where(enabled.contains).toList(); // 默认支付方式必须属于启用集合,否则收银页会出现默认值不可选择的矛盾状态。 if (!orderedEnabled.contains(setting.defaultPayDisplayType)) { setting.defaultPayDisplayType = orderedEnabled.first; } setting.usePayDisplayType = orderedEnabled; sysSetting.refresh(); _schedulePayPreferencesPersist(); } /// 切换指定支付方式能否在收银弹框中重新识别人员,并立即安排本机持久化。 void togglePayTypeReidentification(OrderPayDisplayType type) { if (cashierReidentifiablePayTypes.contains(type)) { cashierReidentifiablePayTypes.remove(type); } else { cashierReidentifiablePayTypes.add(type); } _schedulePayPreferencesPersist(); } /// 调整支付方式展示顺序,同时保持启用集合与完整排序一致。 void reorderPayType(int oldIndex, int newIndex) { if (oldIndex == newIndex || oldIndex < 0 || oldIndex >= payTypeOrder.length) { return; } if (newIndex > oldIndex) newIndex -= 1; if (newIndex < 0 || newIndex >= payTypeOrder.length) return; final updated = payTypeOrder.toList(); final item = updated.removeAt(oldIndex); updated.insert(newIndex, item); payTypeOrder.assignAll(updated); final setting = sysSetting.value; if (setting != null) { final visible = setting.usePayDisplayType ?? OrderPayDisplayType.values; setting.usePayDisplayType = updated.where(visible.contains).toList(); sysSetting.refresh(); } _schedulePayPreferencesPersist(); } /// 恢复全部支付方式、默认枚举顺序和默认重新识别范围,并安排持久化。 void resetPayTypes() { payTypeOrder.assignAll(OrderPayDisplayType.values); cashierReidentifiablePayTypes.assignAll( AppSettingTool.defaultCashierReidentifiablePayTypes, ); final setting = sysSetting.value; if (setting != null) { setting.usePayDisplayType = OrderPayDisplayType.values.toList(); if (!setting.usePayDisplayType!.contains(setting.defaultPayDisplayType)) { setting.defaultPayDisplayType = setting.usePayDisplayType!.first; } sysSetting.refresh(); } _schedulePayPreferencesPersist(); } /// 设置默认支付方式;未启用的支付方式不能成为默认值。 void setDefaultPayType(OrderPayDisplayType type) { final setting = sysSetting.value; if (setting == null) return; final enabled = setting.usePayDisplayType ?? OrderPayDisplayType.values; if (!enabled.contains(type)) { Toast.error(msg: '请先启用该支付方式'); return; } setting.defaultPayDisplayType = type; sysSetting.refresh(); _schedulePayPreferencesPersist(); } /// 支付设置表格“是否默认”开关入口;默认方式唯一,关闭当前默认时提示改选其他方式。 void toggleDefaultPayType(OrderPayDisplayType type, bool value) { if (value) { setDefaultPayType(type); return; } if (sysSetting.value?.defaultPayDisplayType == type) { Toast.error(msg: '默认支付方式不能取消,请选择其他支付方式'); } } /// 支付方式连续拖动或切换时串行保存,始终以最后一次界面状态为准。 void _schedulePayPreferencesPersist() { _payPendingPersist = true; if (!_payPersisting) unawaited(_flushPayPreferences()); } /// 串行写入支付方式偏好,保存期间的新变更会继续下一轮写入。 Future _flushPayPreferences() async { _payPersisting = true; while (_payPendingPersist) { _payPendingPersist = false; final setting = sysSetting.value; if (setting == null) continue; try { await AppSettingTool.setPayTypePreferences( sysInfo: setting, order: payTypeOrder, reidentifiableTypes: cashierReidentifiablePayTypes, ); } catch (e, stack) { logger.e('|$_tag|保存支付管理配置失败error:$e|stack:$stack|'); Toast.error(msg: '支付设置保存失败,请重试'); } } _payPersisting = false; } Future checkIsBind() async { final sysSetting = await AppSettingTool.getSysSetting(); final id = sysSetting.serviceMerchantId; if (ObjectUtil.isEmpty(id)) { return false; } return true; } /// 校验并保存餐段时间;空档允许保留,相邻或交叉范围由强类型模型统一处理。 Future saveProductMealPeriods(List periods) async { if (productMealPeriodSaving.value) return false; final error = ProductMealPeriodSchedule.validate(periods); if (error != null) { Toast.error(msg: error); return false; } productMealPeriodSaving.value = true; try { final schedule = ProductMealPeriodSchedule.checked(periods); await AppSettingTool.setProductMealPeriodSchedule(schedule); productMealPeriodSchedule.value = schedule; Toast.success(msg: '餐段配置已保存'); return true; } catch (e, stack) { logger.e('|$_tag|保存餐段配置失败error:$e|stack:$stack|'); Toast.error(msg: '餐段配置保存失败,请重试'); return false; } finally { productMealPeriodSaving.value = false; } } /// 从 Android 原生能力重新读取当前 Kiosk 状态。 Future refreshKioskStatus() async { kioskStatus.value = await AppRuntimeTool.refreshKioskStatus(); } /// 立即执行真实 Root 命令并局部持久化结果;失败状态也作为有效检查结果保存。 Future refreshRootAccess({bool showFeedback = true}) async { if (rootRefreshing.value) return; rootRefreshing.value = true; try { final result = await AppRuntimeTool.checkRootAccess(); await AppSettingTool.setRootAccessResult(result); final setting = sysSetting.value; if (setting != null) { setting.rootAccessStatus = result.status; setting.rootAccessCheckedAt = result.checkedAt; setting.rootAccessDiagnostic = result.diagnostic; sysSetting.refresh(); } if (showFeedback) { if (result.available) { Toast.success(msg: 'Root 权限可用'); } else { Toast.error(msg: result.diagnostic); } } } catch (e, stack) { logger.e('|$_tag|刷新Root状态失败error:|$e|stack:$stack|'); if (showFeedback) Toast.error(msg: 'Root 状态检查失败,请稍后重试'); } finally { rootRefreshing.value = false; } } /// 经用户二次确认后通过 Root 配置 Device Owner;不删除账户、不移除其他 Owner。 Future enableKioskWithRoot() async { final setting = sysSetting.value; if (setting == null || kioskProvisioning.value) return; if (setting.rootAccessStatus != RootAccessStatus.available) { kioskProvisionMessage.value = 'Root 不可用,请先刷新 Root 状态并完成授权'; return; } final confirmed = await appMessageDialog( Get.context!, '将使用 Root 把当前应用配置为 Android Device Owner,并立即进入 Kiosk。该操作不会删除账户或移除其他 Device Owner,系统拒绝时只会返回原因。是否继续?', ); if (!confirmed) return; kioskProvisioning.value = true; kioskProvisionMessage.value = ''; EasyLoading.show( status: '正在启用专用设备模式...', maskType: EasyLoadingMaskType.black, ); try { final result = await AppRuntimeTool.enableKioskWithRoot(); kioskStatus.value = result.status; if (result.success) { kioskProvisionMessage.value = ''; Toast.success(msg: 'Kiosk 已启用并自动进入'); } else { kioskProvisionMessage.value = result.message; Toast.error(msg: result.message); } } catch (e, stack) { logger.e('|$_tag|Root启用Kiosk失败error:|$e|stack:$stack|'); kioskProvisionMessage.value = '启用失败,请稍后重试并检查系统 Device Owner 状态'; Toast.error(msg: kioskProvisionMessage.value); } finally { EasyLoading.dismiss(); kioskProvisioning.value = false; } } /// 切换主控手动硬件输入;虚拟机强制开启,开启前需操作员确认。 Future setManualHardwareInput(bool enabled) async { if (AppRuntimeTool.isEmulator && !enabled) { Toast.error(msg: '虚拟机必须使用手动硬件输入模式'); manualHardwareInput.value = true; return; } if (enabled && !manualHardwareInput.value) { final confirmed = await appMessageDialog( Get.context!, '设置为主控后,扫码、饭卡读写和人脸识别将改为人工输入,是否继续?', ); if (!confirmed) return; } final setting = sysSetting.value; if (setting == null) return; setting.manualHardwareInput = enabled; await AppSettingTool.setSysSetting(setting); AppRuntimeTool.applyManualHardwareInput(enabled); manualHardwareInput.value = enabled; sysSetting.refresh(); Toast.success(msg: enabled ? '已设置为主控手动输入模式' : '已恢复真实硬件模式'); } /// 立即开启或关闭 Kiosk 自动进入,并同步原生 Lock Task 状态。 Future setKioskAutoEnterEnabled(bool enabled) async { if (!kioskStatus.value.available || kioskSwitching.value) return; kioskSwitching.value = true; try { final status = await AppRuntimeTool.setKioskAutoEnterEnabled(enabled); kioskStatus.value = status; final applied = status.autoEnterEnabled == enabled && status.active == enabled; if (!applied) { Toast.error( msg: enabled ? 'Kiosk 未能立即开启,请检查设备策略' : 'Kiosk 未能立即关闭,请重试', ); return; } Toast.success(msg: enabled ? 'Kiosk 已立即开启' : 'Kiosk 已立即关闭'); } finally { kioskSwitching.value = false; } } /// 保存全局字体倍率并立即应用到运行中主题。 Future setGlobalFontSize(SettingGlobalFontSize fontSize) async { final setting = sysSetting.value; if (setting == null || setting.globalFontSize == fontSize) return; setting.globalFontSize = fontSize; await AppSettingTool.setSysSetting(setting); Get.find().setSizeScalar(fontSize); sysSetting.refresh(); } /// 保存按钮圆角并立即应用到运行中主题。 Future setButtonRadius(SettingButtonRadius value) async { final setting = sysSetting.value; if (setting == null || setting.buttonRadius == value) return; setting.buttonRadius = value; await AppSettingTool.setSysSetting(setting); Get.find().setButtonRadius(value); sysSetting.refresh(); } /// 保存卡片圆角并立即应用到运行中主题。 Future setCardRadius(SettingCardRadius value) async { final setting = sysSetting.value; if (setting == null || setting.cardRadius == value) return; setting.cardRadius = value; await AppSettingTool.setSysSetting(setting); Get.find().setCardRadius(value); sysSetting.refresh(); } /// 保存按钮尺寸档位并立即应用到运行中主题。 Future setButtonSize(SettingButtonSize value) async { final setting = sysSetting.value; if (setting == null || setting.buttonSize == value) return; setting.buttonSize = value; await AppSettingTool.setSysSetting(setting); Get.find().setButtonSize(value); sysSetting.refresh(); } }