import 'dart:convert'; import 'dart:typed_data'; import 'package:common_utils/common_utils.dart'; import 'package:get/get.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:pj_supermarket_cloud/common/index.dart'; abstract class AppSettingTool { static const String _tag = "app配置工具"; static const String _settingKey = "setting"; static const String _terminalMenuOrderKey = "terminalMenuOrder"; static const String _hiddenTerminalMenusKey = "hiddenTerminalMenus"; static const String _payTypeOrderKey = "payTypeOrder"; /// 收银支付方式允许重新识别人员的编码列表;缺失时使用默认集合,空列表表示全部关闭。 static const String _cashierReidentifiablePayTypesKey = "cashierReidentifiablePayTypes"; /// 收银支付方式的默认重新识别范围;旧配置缺少对应键及用户执行“还原”时均采用该集合。 static const Set defaultCashierReidentifiablePayTypes = { // 饭卡支付允许重新刷卡,以便纠正首次识别到的人员。 OrderPayDisplayType.walletCard, // 人脸支付允许重新调用摄像头,以便纠正首次识别到的人员。 OrderPayDisplayType.face, // 中医院属于线下渠道,默认允许通过饭卡补充人员身份。 OrderPayDisplayType.zyy, // 人民医院属于线下渠道,默认允许通过饭卡补充人员身份。 OrderPayDisplayType.rmyy, // 超市消费卡属于线下渠道,默认允许通过饭卡补充人员身份。 OrderPayDisplayType.csxfk, }; static const String _cashierShowHoldActionKey = "cashierShowHoldAction"; static const String _cashierShowMoreActionKey = "cashierShowMoreAction"; /// 收银页是否允许主动修改餐段的稳定存储键;缺失时按允许处理,兼容旧终端。 static const String _cashierAllowManualMealPeriodChangeKey = "cashierAllowManualMealPeriodChange"; /// 顶部隐藏设备编码列表在现有 sysJson 中的稳定存储键;缺失或空列表表示五项全部显示。 static const String _hiddenTerminalDeviceStatusesKey = "hiddenTerminalDeviceStatuses"; /// 旧版顶部设备状态总开关键,仅在保存逐项配置时清理,读取时不再用于隐藏全部设备。 static const String _legacyTerminalDeviceStatusVisibleKey = "terminalDeviceStatusVisible"; static const String _reservationQueryMethodOrderKey = "reservationQueryMethodOrder"; static const String _reservationQueryDefaultMethodKey = "reservationQueryDefaultMethod"; /// 查询菜单自动识别开关在现有 sysJson 中的稳定存储键,缺失时按关闭处理。 static const String _reservationQueryAutoRecognitionEnabledKey = "reservationQueryAutoRecognitionEnabled"; /// 查询菜单持续自动识别开关在现有 sysJson 中的稳定存储键,缺失时按开启处理。 static const String _reservationQueryContinuousRecognitionEnabledKey = "reservationQueryContinuousRecognitionEnabled"; static const String _reservationQueryNameMaskEnabledKey = "reservationQueryNameMaskEnabled"; static const String _reservationQueryVerificationEnabledKey = "reservationQueryVerificationEnabled"; /// 查询菜单快速核销开关在现有 sysJson 中的稳定存储键,缺失时按关闭处理。 static const String _reservationQueryQuickVerificationEnabledKey = "reservationQueryQuickVerificationEnabled"; /// 查询菜单是否允许主动修改餐段的稳定存储键;缺失时按允许处理,兼容旧终端。 static const String _reservationQueryAllowManualMealPeriodChangeKey = "reservationQueryAllowManualMealPeriodChange"; /// 查询菜单最近餐段在现有 sysJson 中的稳定存储键,值使用后端餐段编码且不新增数据库字段。 static const String _reservationQuerySelectedMealSlotKey = "reservationQuerySelectedMealSlot"; static const String _reservationQueryVerificationDateScopeKey = "reservationQueryVerificationDateScope"; /// 查询菜单外接快捷小键盘动作绑定在现有 sysJson 中的稳定存储键; /// 值为“动作编码 -> 按键绑定对象”,缺失或空对象表示全部未绑定。 static const String _menuQueryShortcutKeyBindingsKey = "menuQueryShortcutKeyBindings"; /// 查询菜单快捷小键盘模式开关在现有 sysJson 中的稳定存储键; /// 缺失时按开启处理(配合出厂默认键位开箱即用),仅明确保存 false 才关闭。 static const String _reservationQueryKeyboardModeEnabledKey = "reservationQueryKeyboardModeEnabled"; /// 早餐、中餐和晚餐的本地时间范围;值使用现有 ProductHourType 后端编码。 static const String _productMealPeriodsKey = "productMealPeriods"; static const String _serialCardConfigKey = "serialCardConfig"; /// v8 之前短暂保存定时任务配置的旧 sysJson 键,仅供一次性迁移读取。 static const String _scheduledTaskConfigsKey = "scheduledTaskConfigs"; /// v8 之前短暂保存任务最近执行结果的旧 sysJson 键,仅供一次性迁移读取。 static const String _scheduledTaskRecordsKey = "scheduledTaskRecords"; /// v8 之前保存日志文件成功上传状态的旧 sysJson 键,仅供一次性迁移读取。 static const String _terminalLogUploadStatesKey = "terminalLogUploadStates"; /// v8 之前保存最近日志上传汇总的旧 sysJson 键,仅供一次性迁移清理。 static const String _terminalLogUploadSummaryKey = "terminalLogUploadSummary"; /// 人工硬件输入历史在现有 sysJson 扩展配置中的存储键。 static const String _hardwareInputHistoryKey = "hardwareInputHistory"; /// Root 状态在 sysJson 中的稳定键,只允许由局部串行更新方法写入。 static const String _rootAccessStatusKey = 'rootAccessStatus'; /// Root 最近检查时间在 sysJson 中的稳定键,使用 ISO-8601 字符串持久化。 static const String _rootAccessCheckedAtKey = 'rootAccessCheckedAt'; /// Root 诊断摘要在 sysJson 中的稳定键,不保存完整 shell 输出。 static const String _rootAccessDiagnosticKey = 'rootAccessDiagnostic'; // 自定义缓存管理器 static final CacheManager _settingCache = CacheManager( Config( 'setting_cache', stalePeriod: const Duration(days: 7), maxNrOfCacheObjects: 10, ), ); /// setting 单行记录的进程内写入队列,避免 Root 检查、登录刷新和设置页保存相互覆盖。 static Future _settingWriteQueue = Future.value(); /// /// 将 setting 的“读取最新值并写回”过程串行执行。 /// /// setting 表把用户、系统和食堂扩展配置保存在同一行,仅读写前排队才能避免不同字段的 /// 并发更新互相覆盖;业务异常仍返回原调用方,后续任务会忽略前一次失败继续执行。 static Future _serializeSettingWrite( Future Function() action, ) { final operation = _settingWriteQueue.catchError((_) {}).then( (_) => action(), ); _settingWriteQueue = operation.then((_) {}).catchError((_) {}); return operation; } static Future removeSettingCache() async { await _settingCache.emptyCache(); } static Future _getSetting() async { // 尝试从缓存获取 final fileInfo = await _settingCache.getFileFromCache(_settingKey); if (fileInfo != null) { final content = await fileInfo.file.readAsString(); if (ObjectUtil.isNotEmpty(content)) { return SettingEntity.fromJson(json.decode(content)); } } final app = Get.find(); final settingDao = app.dataBase.getSettingDao; final setting = await settingDao.get(); if (setting == null) { final initSysInfo = SysSetting( sysType: SysType.CANTEEN, androidId: await AppSysTool.getAndroidId(), sqlAddress: "", ); final initUserInfo = UserSetting( username: "", password: "", ); SettingEntity addObj = SettingEntity( userJson: json.encode(initUserInfo.toJson()), sysJson: json.encode(initSysInfo.toJson()), ); final addId = await settingDao.save(addObj); if (ObjectUtil.isEmpty(addId)) { throw Exception("|$_tag|_getSetting新增错误"); } addObj.id = addId; return addObj; } return setting; } static Future _setSetting({ SysSetting? sysJson, UserSetting? userJson, CanteenExtraPriceSetting? canteenExtraPriceConfig, }) => _serializeSettingWrite(() async { if (ObjectUtil.isEmpty(sysJson) && ObjectUtil.isEmpty(userJson) && ObjectUtil.isEmpty(canteenExtraPriceConfig)) { return false; } final app = Get.find(); final settingDao = app.dataBase.getSettingDao; final setting = await settingDao.get(); if (setting == null) { logger.e("|$_tag|_setSetting新增错误settingDao.get()方法返回为null"); return false; } if (sysJson != null) { final current = Map.from(json.decode(setting.sysJson)); final updated = sysJson.toJson(); // Root 状态只归后台检查写入;普通设置使用调用前快照时不能回写过期的 Root 值。 for (final key in const [ _rootAccessStatusKey, _rootAccessCheckedAtKey, _rootAccessDiagnosticKey, ]) { if (current.containsKey(key)) updated[key] = current[key]; } // sysJson 允许保存不涉及数据库结构的终端 UI 扩展配置,更新业务设置时必须保留这些键。 setting.sysJson = json.encode({...current, ...updated}); } if (userJson != null) { setting.userJson = json.encode(userJson.toJson()); } if (canteenExtraPriceConfig != null) { setting.canteenExtraPriceConfigJson = json.encode( canteenExtraPriceConfig.toJson(), ); } await settingDao.edit(setting); await _cacheSetting(setting); return true; }); /// 刷新 setting 单行缓存;调用方必须已经完成数据库写入并持有串行写入队列。 static Future _cacheSetting(SettingEntity setting) async { await _settingCache.putFile( _settingKey, Uint8List.fromList(utf8.encode(json.encode(setting.toJson()))), fileExtension: 'json', ); } /// 在串行队列内读取并局部修改最新 sysJson,未涉及的业务键与同一行其他列保持不变。 static Future _updateSysJson( void Function(Map sysJson) update, ) => _serializeSettingWrite(() async { final app = Get.find(); final settingDao = app.dataBase.getSettingDao; final setting = await settingDao.get(); if (setting == null) throw StateError('终端设置尚未初始化'); final sysJson = Map.from(json.decode(setting.sysJson)); update(sysJson); setting.sysJson = json.encode(sysJson); await settingDao.edit(setting); await _cacheSetting(setting); }); static Future getSysSetting() async { final setting = await _getSetting(); return SysSetting.fromJson(json.decode(setting.sysJson)); } static Future getUserSetting() async { final setting = await _getSetting(); return UserSetting.fromJson(json.decode(setting.userJson)); } static Future getCanteenExtraPriceConfig() async { final setting = await _getSetting(); if (ObjectUtil.isEmpty(setting.canteenExtraPriceConfigJson)) { return null; } return CanteenExtraPriceSetting.fromJson( json.decode(setting.canteenExtraPriceConfigJson!), ); } static Future setSysSetting(SysSetting sysInfo) async { await _setSetting(sysJson: sysInfo); } /// 保存全局餐段选择到 SysSetting;不新增字段,兼容所有已有终端缓存。 static Future setSelectedProductHourType(ProductHourType hourType) => _serializeSettingWrite(() async { final app = Get.find(); final settingDao = app.dataBase.getSettingDao; final setting = await settingDao.get(); if (setting == null) throw StateError('终端设置尚未初始化'); final sysJson = Map.from(json.decode(setting.sysJson)); sysJson['hourType'] = hourType.name; setting.sysJson = json.encode(sysJson); await settingDao.edit(setting); await _cacheSetting(setting); }); /// 串行更新 Root 专属键,始终在实际写入前重新读取 sysJson,避免后台检查覆盖设置页并发修改。 static Future setRootAccessResult(RootAccessCheckResult result) => _serializeSettingWrite(() async { final app = Get.find(); final settingDao = app.dataBase.getSettingDao; final setting = await settingDao.get(); if (setting == null) throw StateError('终端设置尚未初始化'); final sysJson = Map.from(json.decode(setting.sysJson)); sysJson[_rootAccessStatusKey] = result.status.name; sysJson[_rootAccessCheckedAtKey] = result.checkedAt.toIso8601String(); sysJson[_rootAccessDiagnosticKey] = result.diagnostic; setting.sysJson = json.encode(sysJson); await settingDao.edit(setting); await _cacheSetting(setting); }); /// 读取完整 sysJson 为可修改映射,仅供旧结构化数据迁移方法使用。 static Future> _getSysExtensionJson() async { final setting = await _getSetting(); return Map.from(json.decode(setting.sysJson)); } /// 仅用于把 v8 之前短暂写入 sysJson 的任务配置导入结构化表,不再提供 JSON 写入口。 static Future> getScheduledTaskConfigs() async { final sysJson = await _getSysExtensionJson(); final raw = sysJson[_scheduledTaskConfigsKey]; if (raw is! List) return const []; return raw .whereType() .map((item) => ScheduledTaskConfig.fromJson( Map.from(item), )) .toList(growable: false); } /// 读取 v8 之前的任务执行快照并按当前枚举过滤未知任务,仅用于迁移。 static Future> getScheduledTaskRecords() async { final sysJson = await _getSysExtensionJson(); final raw = sysJson[_scheduledTaskRecordsKey]; if (raw is! Map) return const {}; final result = {}; for (final entry in raw.entries) { ScheduledTaskType? type; for (final item in ScheduledTaskType.values) { if (item.name == entry.key.toString()) { type = item; break; } } if (type == null || entry.value is! Map) continue; result[type] = ScheduledTaskRecord.fromJson( Map.from(entry.value as Map), ); } return result; } /// 读取 v8 之前按相对路径保存的成功上传状态,仅用于导入文件上传记录表。 static Future> getTerminalLogUploadStates() async { final sysJson = await _getSysExtensionJson(); final raw = sysJson[_terminalLogUploadStatesKey]; if (raw is! Map) return const {}; return raw.map( (key, value) => MapEntry( key.toString(), TerminalLogUploadState.fromJson( Map.from(value as Map), ), ), ); } /// 读取 v8 之前的最近上传汇总;仅保留兼容能力,不再作为运行时数据源。 static Future getTerminalLogUploadSummary() async { final sysJson = await _getSysExtensionJson(); final raw = sysJson[_terminalLogUploadSummaryKey]; if (raw is! Map) return const TerminalLogUploadSummary(); return TerminalLogUploadSummary.fromJson(Map.from(raw)); } /// v8 已将任务与上传状态迁入独立表,导入成功后移除旧 JSON,避免形成双写数据源。 static Future clearLegacyScheduledTaskState({ required bool includeUploadState, }) => _serializeSettingWrite(() async { final app = Get.find(); final settingDao = app.dataBase.getSettingDao; final setting = await settingDao.get(); if (setting == null) throw StateError('终端设置尚未初始化'); final sysJson = Map.from(json.decode(setting.sysJson)); sysJson.remove(_scheduledTaskConfigsKey); sysJson.remove(_scheduledTaskRecordsKey); if (includeUploadState) { sysJson.remove(_terminalLogUploadStatesKey); sysJson.remove(_terminalLogUploadSummaryKey); } setting.sysJson = json.encode(sysJson); await settingDao.edit(setting); await _cacheSetting(setting); }); static Future setUserSetting(UserSetting userInfo) async { final globalUserInfo = await getUserSetting(); if (globalUserInfo == userInfo) return; if (ObjectUtil.isEmpty(userInfo.tenantId) && ObjectUtil.isNotEmpty(globalUserInfo.tenantId)) { userInfo.tenantId = globalUserInfo.tenantId; } await _setSetting(userJson: userInfo); } /// 清除已保存的登录密码和会话令牌,保留用户名与租户便于下次登录快速回填。 static Future forgetSavedPassword() async { final userInfo = await getUserSetting(); if (ObjectUtil.isEmpty(userInfo.password)) return; userInfo ..password = null ..userId = null ..accessToken = null ..refreshToken = null ..expiresTime = null; await setUserSetting(userInfo); } /// 手动退出登录时按设置决定是否忘记密码。 /// /// 关闭开关则完整保留凭据(下次启动自动登录);离线时不清除, /// 保证断网机器仍可凭已保存凭据重新进入离线模式,与基线行为一致。 static Future applyManualLogoutPasswordPolicy() async { final sysInfo = await getSysSetting(); if (!sysInfo.forgetPasswordOnManualLogout) return; if (!AppNetworkTool.isAutoNetwork()) return; await forgetSavedPassword(); } /// 冷启动时执行“突然关机忘记密码”策略。 /// /// 终端常驻运行,除手动退出外的任何结束(断电、强制重启、进程被杀) /// 都属于突然关机;开启开关且当前联网时清除保存的密码,要求重新登录。 /// 返回是否清除了登录凭据,调用方据此回退到登录页。 static Future applyStartupPowerLossPasswordPolicy() async { final userInfo = await getUserSetting(); if (ObjectUtil.isEmpty(userInfo.password)) return false; final sysInfo = await getSysSetting(); if (!sysInfo.forgetPasswordOnPowerLoss) return false; // 冷启动监听可能尚未写入初始网络状态,这里直接探测当前连接。 if (!await AppNetworkTool.isHandNetwork()) { logger.w('|$_tag|启动时网络不可用,保留已保存密码以便离线进入'); return false; } await forgetSavedPassword(); logger.w('|$_tag|检测到上次非手动退出,已按设置清除保存的登录密码'); return true; } static Future setCanteenExtraPriceConfig( CanteenExtraPriceSetting canteenExtraPriceConfig, ) async { final config = await getCanteenExtraPriceConfig(); if (config == canteenExtraPriceConfig) return; await _setSetting(canteenExtraPriceConfig: canteenExtraPriceConfig); } /// 读取左侧菜单的顺序和隐藏状态,未知或缺失项由控制器按默认菜单补齐。 static Future<({List order, Set hidden})> getTerminalMenuPreferences() async { final setting = await _getSetting(); final sysJson = Map.from(json.decode(setting.sysJson)); final order = (sysJson[_terminalMenuOrderKey] as List?) ?.whereType() .toList() ?? const []; final hidden = (sysJson[_hiddenTerminalMenusKey] as List?) ?.whereType() .toSet() ?? const {}; return (order: order, hidden: hidden); } /// 菜单布局属于设备级 UI 设置,直接复用 setting 表中的 sysJson,避免增加数据库字段和迁移。 static Future setTerminalMenuPreferences({ required List order, required Set hidden, }) => _updateSysJson((sysJson) { sysJson[_terminalMenuOrderKey] = order; sysJson[_hiddenTerminalMenusKey] = hidden.toList(); }); /// 读取支付方式的完整顺序;显隐仍由 SysSetting.usePayDisplayType 维护。 static Future> getPayTypeOrder() async { final setting = await _getSetting(); final sysJson = Map.from(json.decode(setting.sysJson)); return (sysJson[_payTypeOrderKey] as List?) ?.whereType() .toList() ?? const []; } /// 读取允许重新识别人员的支付方式;缺少或非列表配置使用默认值,显式空列表保持全部关闭。 static Future> getCashierReidentifiablePayTypes() async { final setting = await _getSetting(); final sysJson = Map.from(json.decode(setting.sysJson)); final payTypeByCode = { for (final payType in OrderPayDisplayType.values) payType.code: payType, }; final source = sysJson[_cashierReidentifiablePayTypesKey]; if (source is! List) { return defaultCashierReidentifiablePayTypes.toSet(); } return source .whereType() .map((code) => payTypeByCode[code]) .whereType() .toSet(); } /// 支付排序、显隐、默认方式及重新识别能力作为同一份终端支付偏好原子保存。 static Future setPayTypePreferences({ required SysSetting sysInfo, required List order, required Set reidentifiableTypes, }) { // 在首次异步等待前生成快照,避免连续拖动时后一次修改污染本次落盘内容。 final nextSysJson = sysInfo.toJson(); final visiblePayTypes = nextSysJson['usePayDisplayType']; final defaultPayType = nextSysJson['defaultPayDisplayType']; final orderCodes = order.map((item) => item.code).toList(); final reidentifiableCodes = OrderPayDisplayType.values .where(reidentifiableTypes.contains) .map((item) => item.code) .toList(); return _updateSysJson((sysJson) { sysJson['usePayDisplayType'] = visiblePayTypes; sysJson['defaultPayDisplayType'] = defaultPayType; sysJson[_payTypeOrderKey] = orderCodes; sysJson[_cashierReidentifiablePayTypesKey] = reidentifiableCodes; }); } /// 读取收银页底部快捷操作显隐及餐段修改权限;旧终端没有配置时保持默认允许。 static Future< ({ bool showHoldAction, bool showMoreAction, bool allowManualMealPeriodChange, })> getCashierPagePreferences() async { final setting = await _getSetting(); final sysJson = Map.from(json.decode(setting.sysJson)); return ( showHoldAction: sysJson[_cashierShowHoldActionKey] != false, showMoreAction: sysJson[_cashierShowMoreActionKey] != false, allowManualMealPeriodChange: sysJson[_cashierAllowManualMealPeriodChangeKey] != false, ); } /// 收银快捷操作与餐段修改权限属于本机页面布局配置,继续复用 sysJson。 static Future setCashierPagePreferences({ required bool showHoldAction, required bool showMoreAction, required bool allowManualMealPeriodChange, }) => _updateSysJson((sysJson) { sysJson[_cashierShowHoldActionKey] = showHoldAction; sysJson[_cashierShowMoreActionKey] = showMoreAction; sysJson[_cashierAllowManualMealPeriodChangeKey] = allowManualMealPeriodChange; }); /// 读取商品餐段时间;旧终端无配置或持久化值损坏时返回产品默认时间。 static Future getProductMealPeriodSchedule() async { final setting = await _getSetting(); final sysJson = Map.from(json.decode(setting.sysJson)); return ProductMealPeriodSchedule.fromJson(sysJson[_productMealPeriodsKey]); } /// 保存经过统一校验的基础三餐时间,不增加 setting 表字段和数据库迁移。 static Future setProductMealPeriodSchedule( ProductMealPeriodSchedule schedule, ) => _updateSysJson((sysJson) { sysJson[_productMealPeriodsKey] = schedule.toJson(); }); /// 按本机当前时间取得配置餐段;未覆盖时段返回空,调用方可保留人工选择。 static Future getCurrentProductHourType({ DateTime? now, }) async { final schedule = await getProductMealPeriodSchedule(); return schedule.resolve(now ?? DateTime.now()); } /// 读取顶部需要隐藏的设备类型;缺键、异常值和未知编码均忽略,因此升级后默认五项全部显示。 static Future> getHiddenTerminalDeviceStatuses() async { final setting = await _getSetting(); final sysJson = Map.from(json.decode(setting.sysJson)); final source = sysJson[_hiddenTerminalDeviceStatusesKey]; if (source is! List) return {}; final deviceByCode = { for (final device in TerminalDeviceType.values) device.code: device, }; return source .whereType() .map((code) => deviceByCode[code]) .whereType() .toSet(); } /// 保存逐项隐藏设备编码并移除旧总开关;新设备因不在隐藏列表中会自动显示。 static Future setHiddenTerminalDeviceStatuses( Set hiddenDevices, ) { final hiddenCodes = TerminalDeviceType.values .where(hiddenDevices.contains) .map((device) => device.code) .toList(growable: false); return _updateSysJson((sysJson) { sysJson[_hiddenTerminalDeviceStatusesKey] = hiddenCodes; sysJson.remove(_legacyTerminalDeviceStatusVisibleKey); }); } /// 读写卡协议配置属于设备本地扩展设置,复用 sysJson 保存且不修改数据库结构。 static Future getSerialCardConfig() async { final setting = await _getSetting(); final sysJson = Map.from(json.decode(setting.sysJson)); final source = sysJson[_serialCardConfigKey]; if (source is! Map) return const SerialCardConfig(); try { return SerialCardConfig.fromJson(Map.from(source)); } on Object { // 旧版本或人工修改的异常配置不能阻断收银读卡,无法解析时回退设备默认参数。 return const SerialCardConfig(); } } /// 密钥只写入本机设置和串口模块,不记录到日志,也不随普通业务接口上传。 static Future setSerialCardConfig(SerialCardConfig config) => _updateSysJson((sysJson) { sysJson[_serialCardConfigKey] = config.toJson(); }); /// 查询方式顺序和默认项属于页面交互配置,继续保存在 sysJson 中,不增加数据库字段。 static Future getReservationQueryPagePreferences() async { final setting = await _getSetting(); final sysJson = Map.from(json.decode(setting.sysJson)); final byCode = { for (final method in ReservationRecognitionMethod.values) method.code: method, }; final restored = []; final storedOrder = (sysJson[_reservationQueryMethodOrderKey] as List?) ?.whereType() ?? const []; for (final code in storedOrder) { final method = byCode[code]; if (method != null && !restored.contains(method)) restored.add(method); } for (final method in ReservationRecognitionMethod.values) { if (!restored.contains(method)) restored.add(method); } final storedDefault = byCode[sysJson[_reservationQueryDefaultMethodKey] as String?]; final defaultMethod = storedDefault != null && restored.contains(storedDefault) ? storedDefault : restored.first; final verificationDateScope = ReservationVerificationDateScope.values .where( (scope) => scope.code == sysJson[_reservationQueryVerificationDateScopeKey], ) .firstOrNull ?? ReservationVerificationDateScope.today; final verificationEnabled = sysJson[_reservationQueryVerificationEnabledKey] == true; final selectedMealSlot = ReservationMealSlot.querySelectableValues .where( (slot) => slot.code == sysJson[_reservationQuerySelectedMealSlotKey], ) .firstOrNull ?? ReservationMealSlot.morning; // 终端从未保存过快捷按键配置(全新安装或旧版本升级)时使用出厂默认字母按键; // 键一旦存在就完全以用户配置为准,即使值为空对象(用户已主动清空全部绑定)。 final shortcutKeyBindings = sysJson.containsKey(_menuQueryShortcutKeyBindingsKey) ? _parseMenuQueryShortcutKeyBindings( sysJson[_menuQueryShortcutKeyBindingsKey], ) : MenuQueryShortcutKeyBinding.defaults; return ReservationQueryPagePreferences( methodOrder: List.unmodifiable(restored), defaultMethod: defaultMethod, // 旧设置没有该键时保持手动启动,避免升级后直接拉起读卡器或相机。 autoRecognitionEnabled: sysJson[_reservationQueryAutoRecognitionEnabledKey] == true, // 自动识别的设计目标是持续接待,旧设置没有该键时默认续开正常等待超时。 continuousRecognitionEnabled: sysJson[_reservationQueryContinuousRecognitionEnabledKey] != false, // 旧设置没有该键时继续脱敏,避免升级后意外展示完整姓名。 nameMaskEnabled: sysJson[_reservationQueryNameMaskEnabledKey] != false, verificationEnabled: verificationEnabled, // 快速核销从属于核销入口,旧设置缺键或核销关闭时均不得隐式生效。 quickVerificationEnabled: verificationEnabled && sysJson[_reservationQueryQuickVerificationEnabledKey] == true, // 旧设置没有该键时保持允许手动修改,避免升级后收银流程被意外锁死。 allowManualMealPeriodChange: sysJson[_reservationQueryAllowManualMealPeriodChangeKey] != false, selectedMealSlot: selectedMealSlot, verificationDateScope: verificationDateScope, shortcutKeyBindings: shortcutKeyBindings, // 键盘模式默认开启:仅操作员明确保存过关闭状态才保持关闭。 keyboardModeEnabled: sysJson[_reservationQueryKeyboardModeEnabledKey] != false, ); } /// 解析快捷小键盘绑定为强类型映射;损坏条目跳过,重复按键只保留首个动作。 /// /// sysJson 属于存储边界,可能被旧版本或人工编辑产生异常结构, /// 单条解析失败不允许阻断其余查询菜单配置读取。 static Map _parseMenuQueryShortcutKeyBindings(Object? source) { // 始终返回可变 Map:调用方可能直接写入,const 空 Map 会导致写入抛异常。 if (source is! Map) { return {}; } final byCode = { for (final action in MenuQueryShortcutAction.values) action.code: action, }; final bindings = {}; for (final entry in source.entries) { final action = byCode[entry.key]; if (action == null || entry.value is! Map) continue; try { final binding = MenuQueryShortcutKeyBinding.fromJson( Map.from(entry.value as Map), ); // 空绑定没有运行期匹配价值;与已恢复动作冲突的按键保留先出现的动作。 if (binding.usbHidUsage == 0 && binding.logicalKeyId == 0) continue; if (bindings.values.any(binding.conflictsWith)) continue; bindings[action] = binding; } on Object { // 忽略无法解析的历史绑定,继续恢复其余动作。 } } return bindings; } /// 顺序与默认项一起保存,避免连续操作后出现“默认项不在当前列表中”的矛盾状态。 static Future setReservationQueryPagePreferences({ required List order, required ReservationRecognitionMethod defaultMethod, required bool autoRecognitionEnabled, required bool continuousRecognitionEnabled, required bool nameMaskEnabled, required bool verificationEnabled, required bool quickVerificationEnabled, required bool allowManualMealPeriodChange, required ReservationVerificationDateScope verificationDateScope, required Map shortcutKeyBindings, required bool keyboardModeEnabled, }) { final normalized = []; for (final method in order) { if (!normalized.contains(method)) normalized.add(method); } for (final method in ReservationRecognitionMethod.values) { if (!normalized.contains(method)) normalized.add(method); } final normalizedDefault = normalized.contains(defaultMethod) ? defaultMethod : normalized.first; return _updateSysJson((sysJson) { sysJson[_reservationQueryMethodOrderKey] = normalized.map((method) => method.code).toList(); sysJson[_reservationQueryDefaultMethodKey] = normalizedDefault.code; sysJson[_reservationQueryAutoRecognitionEnabledKey] = autoRecognitionEnabled; sysJson[_reservationQueryContinuousRecognitionEnabledKey] = continuousRecognitionEnabled; sysJson[_reservationQueryNameMaskEnabledKey] = nameMaskEnabled; sysJson[_reservationQueryVerificationEnabledKey] = verificationEnabled; sysJson[_reservationQueryQuickVerificationEnabledKey] = verificationEnabled && quickVerificationEnabled; sysJson[_reservationQueryAllowManualMealPeriodChangeKey] = allowManualMealPeriodChange; sysJson[_reservationQueryVerificationDateScopeKey] = verificationDateScope.code; // 持久化边界再次收敛空绑定,保证落盘结构与“未绑定即不写入”的读取约定一致。 sysJson[_menuQueryShortcutKeyBindingsKey] = { for (final entry in shortcutKeyBindings.entries) if (entry.value.usbHidUsage != 0 || entry.value.logicalKeyId != 0) entry.key.code: entry.value.toJson(), }; sysJson[_reservationQueryKeyboardModeEnabledKey] = keyboardModeEnabled; }); } /// 保存查询菜单最近选中的基础三餐,并刷新现有 setting 文件缓存供下次进入页面恢复。 static Future setReservationQuerySelectedMealSlot( ReservationMealSlot mealSlot, ) { if (!ReservationMealSlot.querySelectableValues.contains(mealSlot)) { throw ArgumentError.value(mealSlot, 'mealSlot', '仅允许缓存早餐、午餐或晚餐'); } return _updateSysJson((sysJson) { sysJson[_reservationQuerySelectedMealSlotKey] = mealSlot.code; }); } /// 读取指定“路由 + 功能键”场景最近使用的人工硬件输入,异常旧数据会被忽略。 static Future> getHardwareInputHistory( String sceneKey, ) async { final store = await _getHardwareInputHistoryStore(); return List.unmodifiable(store.entriesFor(sceneKey)); } /// 保存当前场景最近 5 条输入;相同输入只更新备注和时间并置顶,不产生重复记录。 static Future saveHardwareInputHistory({ required String sceneKey, required String value, String? remark, }) async { final normalizedValue = value.trim(); if (normalizedValue.isEmpty) return; await _updateSysJson((sysJson) { final store = _decodeHardwareInputHistoryStore( sysJson[_hardwareInputHistoryKey], ); final nextStore = store.save( sceneKey: sceneKey, value: normalizedValue, remark: remark ?? '', updatedAt: DateTime.now(), ); sysJson[_hardwareInputHistoryKey] = nextStore.toJson(); }); } /// 从现有 sysJson 扩展配置读取强类型历史模型,单条异常数据不影响人工输入主流程。 static Future _getHardwareInputHistoryStore() async { final setting = await _getSetting(); final sysJson = Map.from(json.decode(setting.sysJson)); return _decodeHardwareInputHistoryStore(sysJson[_hardwareInputHistoryKey]); } /// JSON Map 仅停留在设置存储边界,弹窗与去重逻辑统一操作强类型模型。 static HardwareInputHistoryStore _decodeHardwareInputHistoryStore( Object? source, ) { if (source is! Map) return HardwareInputHistoryStore.empty; try { return HardwareInputHistoryStore.fromJson( Map.from(source), ); } on Object { return HardwareInputHistoryStore.empty; } } // 根据后台返回的数据更新到缓存 static Future updateSysInfoByService(ServiceHardware data) async { if (ObjectUtil.isEmpty(data)) { final setting = SysSetting( sysType: SysType.CANTEEN, androidId: await AppSysTool.getAndroidId(), sqlAddress: await AppFileTool.getDatabaseBackupPath(), ); await _setSetting(sysJson: setting); return; } final sysSetting = await AppSettingTool.getSysSetting(); final id = data.id; if (ObjectUtil.isNotEmpty(id)) { sysSetting.serviceId = id; } final name = data.name; if (ObjectUtil.isNotEmpty(name)) { sysSetting.serviceName = name; } final type = data.type.toString(); if (ObjectUtil.isNotEmpty(type)) { final t = SysType.values.fromCode(type); if (ObjectUtil.isNotEmpty(t)) { sysSetting.sysType = t!; } } final merchantId = data.merchantId; if (ObjectUtil.isNotEmpty(merchantId)) { sysSetting.serviceMerchantId = merchantId; } final hardGroupId = data.hardGroupId; if (ObjectUtil.isNotEmpty(hardGroupId)) { sysSetting.hardGroupId = hardGroupId; } final merchantDeptId = data.merchantDeptId; if (ObjectUtil.isNotEmpty(merchantDeptId)) { sysSetting.merchantDeptId = merchantDeptId; } final mqClientId = data.mqClientId; if (ObjectUtil.isNotEmpty(mqClientId)) { sysSetting.mqClientId = mqClientId; } final mainTopic = data.mainTopic; if (ObjectUtil.isNotEmpty(mainTopic)) { sysSetting.mainTopic = mainTopic; } final mqSocketAddress = data.mqSocketAddress; if (ObjectUtil.isNotEmpty(mqSocketAddress)) { sysSetting.mqSocketAddress = mqSocketAddress; } final mqSocketPort = data.mqSocketPort; if (ObjectUtil.isNotEmpty(mqSocketPort)) { sysSetting.mqSocketPort = mqSocketPort; } final mqUsername = data.mqUsername; if (ObjectUtil.isNotEmpty(mqUsername)) { sysSetting.mqUsername = mqUsername; } final mqPassword = data.mqPassword; if (ObjectUtil.isNotEmpty(mqPassword)) { sysSetting.mqPassword = mqPassword; } final faceAppId = data.faceAppId; if (ObjectUtil.isNotEmpty(faceAppId)) { sysSetting.faceAppId = faceAppId; } final faceSdkKey = data.faceSdkKey; if (ObjectUtil.isNotEmpty(faceSdkKey)) { sysSetting.faceSdkKey = faceSdkKey; } final faceActiveKey = data.faceActiveKey; if (ObjectUtil.isNotEmpty(faceActiveKey)) { sysSetting.faceActiveKey = faceActiveKey; } final android = data.uniqueCode; if (ObjectUtil.isNotEmpty(android)) { sysSetting.androidId = android; } sysSetting.syncTime = DateTime.now(); await _setSetting(sysJson: sysSetting); } /// 将当前业务终端的打印机绑定和打印参数持久化到本机;空响应表示尚未绑定。 static Future updatePrinterBinding( Map? binding, ) async { final setting = await getSysSetting(); if (binding == null) { setting.printerHardId = null; setting.printerHardName = null; setting.printerModel = null; setting.printerUsbVendorId = null; setting.printerUsbProductId = null; setting.printerPaperWidth = 80; setting.printerAutoCut = true; setting.printerImageMode = 'GS_V0'; setting.printerOutputMode = PrinterOutputMode.image; await _setSetting(sysJson: setting); return setting; } final sourceHardId = binding['sourceHardId']?.toString(); if (sourceHardId != setting.serviceId) { throw const FormatException('服务端打印机绑定与当前业务终端不匹配'); } final sourceType = SysType.values.fromCode( binding['sourceHardType']?.toString() ?? '', ); if (sourceType != setting.sysType) { throw const FormatException('服务端打印机绑定的业务类型与当前终端不匹配'); } final config = binding['printerConfig'] is Map ? Map.from(binding['printerConfig'] as Map) : {}; setting.printerHardId = binding['printerHardId']?.toString(); setting.printerHardName = binding['printerHardName']?.toString(); setting.printerModel = config['printerModel']?.toString(); setting.printerUsbVendorId = int.tryParse('${config['usbVendorId'] ?? ''}'); setting.printerUsbProductId = int.tryParse('${config['usbProductId'] ?? ''}'); setting.printerPaperWidth = int.tryParse('${config['paperWidth'] ?? ''}') ?? 80; setting.printerAutoCut = '${config['autoCut'] ?? '1'}' != '0'; setting.printerImageMode = config['imageMode']?.toString() ?? 'GS_V0'; // 服务端打印方式:图片打印是当前终端唯一支持的方式,未知值按图片打印兜底。 final printMode = config['printMode']?.toString().trim().toUpperCase(); setting.printerOutputMode = printMode == PrinterOutputMode.command ? PrinterOutputMode.command : PrinterOutputMode.image; await _setSetting(sysJson: setting); return setting; } /// 服务端声明的打印方式与终端固定能力不一致时,上报图片打印纠偏。 /// /// 当前构建固定图片打印;若管理端误把打印机配置成指令打印,服务端会持续 /// 生成终端永不消费的指令流。返回是否需要纠偏。 static bool needsPrintModeHeal(SysSetting setting) => setting.printerHardId != null && setting.printerOutputMode != PrinterOutputMode.image; /// 绑定设备信息 static Future bindSys(int serviceId) async { var androidId = await AppSysTool.getAndroidId(); if (ObjectUtil.isEmpty(androidId)) return false; final pa = {"id": serviceId, "uniqueCode": androidId}; try { final data = await HardApi.bind(pa); if (data == null) { logger.e("绑定设备信息pa:|${jsonEncode(pa)}|;res:$data;"); return false; } await AppSettingTool.updateSysInfoByService(data); return true; } catch (e, stack) { logger.e("|$_tag|绑定信息错误error:|$e|stack:$stack"); } return false; } }