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"; /// 早餐、中餐和晚餐的本地时间范围;值使用现有 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 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; 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, ); } /// 顺序与默认项一起保存,避免连续操作后出现“默认项不在当前列表中”的矛盾状态。 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, }) { 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; }); } /// 保存查询菜单最近选中的基础三餐,并刷新现有 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'; 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'; await _setSetting(sysJson: setting); return setting; } /// 绑定设备信息 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; } }