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 _cashierShowHoldActionKey = "cashierShowHoldAction"; static const String _cashierShowMoreActionKey = "cashierShowMoreAction"; static const String _reservationQueryMethodOrderKey = "reservationQueryMethodOrder"; static const String _reservationQueryDefaultMethodKey = "reservationQueryDefaultMethod"; static const String _reservationQueryNameMaskEnabledKey = "reservationQueryNameMaskEnabled"; static const String _reservationQueryVerificationEnabledKey = "reservationQueryVerificationEnabled"; static const String _reservationQueryVerificationDateScopeKey = "reservationQueryVerificationDateScope"; 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"; // 自定义缓存管理器 static final CacheManager _settingCache = CacheManager( Config( 'setting_cache', stalePeriod: const Duration(days: 7), maxNrOfCacheObjects: 10, ), ); /// sysJson 扩展数据的进程内写入队列,避免迁移清理与设置保存相互覆盖。 static Future _extensionWriteQueue = Future.value(); 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, }) async { if (ObjectUtil.isEmpty(sysJson) && ObjectUtil.isEmpty(userJson) && ObjectUtil.isEmpty(canteenExtraPriceConfig)) { return false; } final app = Get.find(); final settingDao = app.dataBase.getSettingDao; SettingEntity? 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)); // sysJson 允许保存不涉及数据库结构的终端 UI 扩展配置,更新业务设置时必须保留这些键。 setting.sysJson = json.encode({...current, ...sysJson.toJson()}); } if (userJson != null) { setting.userJson = json.encode(userJson.toJson()); } if (canteenExtraPriceConfig != null) { setting.canteenExtraPriceConfigJson = json.encode( canteenExtraPriceConfig.toJson(), ); } await settingDao.edit(setting); await _settingCache.putFile( _settingKey, Uint8List.fromList(utf8.encode(json.encode(setting.toJson()))), fileExtension: 'json', ); return true; } 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); } /// 读取完整 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, }) async { final operation = _extensionWriteQueue.catchError((_) {}).then((_) 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 _settingCache.putFile( _settingKey, Uint8List.fromList(utf8.encode(json.encode(setting.toJson()))), fileExtension: 'json', ); }); _extensionWriteQueue = operation; await operation; } 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, }) 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[_terminalMenuOrderKey] = order; sysJson[_hiddenTerminalMenusKey] = hidden.toList(); setting.sysJson = json.encode(sysJson); await settingDao.edit(setting); await _settingCache.putFile( _settingKey, Uint8List.fromList(utf8.encode(json.encode(setting.toJson()))), fileExtension: 'json', ); } /// 读取支付方式的完整顺序;显隐仍由 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 []; } /// 支付排序属于终端 UI 扩展配置,与现有支付显隐和默认方式一起原子保存。 static Future setPayTypePreferences({ required SysSetting sysInfo, required List order, }) async { // 在首次异步等待前生成快照,避免连续拖动时后一次修改污染本次落盘内容。 final nextSysJson = sysInfo.toJson(); final visiblePayTypes = nextSysJson['usePayDisplayType']; final defaultPayType = nextSysJson['defaultPayDisplayType']; final orderCodes = order.map((item) => item.code).toList(); final app = Get.find(); final settingDao = app.dataBase.getSettingDao; final setting = await settingDao.get(); if (setting == null) { throw StateError('终端设置尚未初始化'); } final current = Map.from(json.decode(setting.sysJson)); current['usePayDisplayType'] = visiblePayTypes; current['defaultPayDisplayType'] = defaultPayType; current[_payTypeOrderKey] = orderCodes; setting.sysJson = json.encode(current); await settingDao.edit(setting); await _settingCache.putFile( _settingKey, Uint8List.fromList(utf8.encode(json.encode(setting.toJson()))), fileExtension: 'json', ); } /// 读取收银页底部快捷操作显隐;旧终端没有配置时保持两个入口都显示。 static Future<({bool showHoldAction, bool showMoreAction})> getCashierPagePreferences() async { final setting = await _getSetting(); final sysJson = Map.from(json.decode(setting.sysJson)); return ( showHoldAction: sysJson[_cashierShowHoldActionKey] != false, showMoreAction: sysJson[_cashierShowMoreActionKey] != false, ); } /// 收银快捷操作属于本机页面布局配置,继续复用 sysJson,避免增加数据库字段。 static Future setCashierPagePreferences({ required bool showHoldAction, required bool showMoreAction, }) 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[_cashierShowHoldActionKey] = showHoldAction; sysJson[_cashierShowMoreActionKey] = showMoreAction; setting.sysJson = json.encode(sysJson); await settingDao.edit(setting); await _settingCache.putFile( _settingKey, Uint8List.fromList(utf8.encode(json.encode(setting.toJson()))), fileExtension: 'json', ); } /// 读写卡协议配置属于设备本地扩展设置,复用 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) 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[_serialCardConfigKey] = config.toJson(); setting.sysJson = json.encode(sysJson); await settingDao.edit(setting); await _settingCache.putFile( _settingKey, Uint8List.fromList(utf8.encode(json.encode(setting.toJson()))), fileExtension: 'json', ); } /// 查询方式顺序和默认项属于页面交互配置,继续保存在 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; return ReservationQueryPagePreferences( methodOrder: List.unmodifiable(restored), defaultMethod: defaultMethod, // 旧设置没有该键时继续脱敏,避免升级后意外展示完整姓名。 nameMaskEnabled: sysJson[_reservationQueryNameMaskEnabledKey] != false, verificationEnabled: sysJson[_reservationQueryVerificationEnabledKey] == true, verificationDateScope: verificationDateScope, ); } /// 顺序与默认项一起保存,避免连续操作后出现“默认项不在当前列表中”的矛盾状态。 static Future setReservationQueryPagePreferences({ required List order, required ReservationRecognitionMethod defaultMethod, required bool nameMaskEnabled, required bool verificationEnabled, required ReservationVerificationDateScope verificationDateScope, }) async { 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; 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[_reservationQueryMethodOrderKey] = normalized.map((method) => method.code).toList(); sysJson[_reservationQueryDefaultMethodKey] = normalizedDefault.code; sysJson[_reservationQueryNameMaskEnabledKey] = nameMaskEnabled; sysJson[_reservationQueryVerificationEnabledKey] = verificationEnabled; sysJson[_reservationQueryVerificationDateScopeKey] = verificationDateScope.code; setting.sysJson = json.encode(sysJson); await settingDao.edit(setting); await _settingCache.putFile( _settingKey, Uint8List.fromList(utf8.encode(json.encode(setting.toJson()))), fileExtension: 'json', ); } /// 读取指定“路由 + 功能键”场景最近使用的人工硬件输入,异常旧数据会被忽略。 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; 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)); final store = _decodeHardwareInputHistoryStore( sysJson[_hardwareInputHistoryKey], ); final nextStore = store.save( sceneKey: sceneKey, value: normalizedValue, remark: remark ?? '', updatedAt: DateTime.now(), ); sysJson[_hardwareInputHistoryKey] = nextStore.toJson(); setting.sysJson = json.encode(sysJson); await settingDao.edit(setting); await _settingCache.putFile( _settingKey, Uint8List.fromList(utf8.encode(json.encode(setting.toJson()))), fileExtension: 'json', ); } /// 从现有 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 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; } }