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); // 全局文字大小 var globalFontSizeC = FRadioSelectGroupController( value: SettingGlobalFontSize.xl3.code, ); // 付款方式 var globalPayTableC = FMultiSelectGroupController( values: {}, ); // 默认付款方式 var defaultGlobalPayTableC = FRadioSelectGroupController(); /// 商品餐段时间配置;餐段校验弹框和自动切换按该时间范围判定。 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.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); }); globalPayTableC.addValueListener((v) async { List usePayList = []; for (final i in v) { final payType = OrderPayDisplayType.values.fromCode( i, ); if (payType == null) continue; usePayList.add(payType); } usePayList.sort((pren, next) => pren.code.compareTo(next.code)); sysSetting.value?.usePayDisplayType = usePayList; await AppSettingTool.setSysSetting(sysSetting.value!); }); defaultGlobalPayTableC.addValueListener((v) async { final payType = OrderPayDisplayType.values.fromCode( v.first, ); if (payType == null) return; sysSetting.value?.defaultPayDisplayType = payType; await AppSettingTool.setSysSetting(sysSetting.value!); }); mqUpdateListen = appC.mqUpdateEventBus.on().listen( (event) async { if (event == MqtActionEnum.CONFIG) { sysSetting.value = await AppSettingTool.getSysSetting(); } }, ); } @override void onReady() { super.onReady(); _initData(); } @override void onClose() { super.onClose(); globalFontSizeC.dispose(); globalPayTableC.dispose(); defaultGlobalPayTableC.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 { sysSetting.value = await AppSettingTool.getSysSetting(); productMealPeriodSchedule.value = await AppSettingTool.getProductMealPeriodSchedule(); if (sysSetting.value!.globalFontSize != null) { globalFontSizeC.value = {sysSetting.value!.globalFontSize!.code}; } if (!ObjectUtil.isEmptyList(sysSetting.value!.usePayDisplayType)) { globalPayTableC.value = sysSetting.value!.usePayDisplayType!.map((v) => v.code).toSet(); } if (sysSetting.value!.defaultPayDisplayType != null) { defaultGlobalPayTableC.value = { sysSetting.value!.defaultPayDisplayType!.code }; } } 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; } } }