872 lines
36 KiB
Dart
872 lines
36 KiB
Dart
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<OrderPayDisplayType> defaultCashierReidentifiablePayTypes = {
|
|
// 饭卡支付允许重新刷卡,以便纠正首次识别到的人员。
|
|
OrderPayDisplayType.walletCard,
|
|
// 人脸支付允许重新调用摄像头,以便纠正首次识别到的人员。
|
|
OrderPayDisplayType.face,
|
|
// 中医院属于线下渠道,默认允许通过饭卡补充人员身份。
|
|
OrderPayDisplayType.zyy,
|
|
// 人民医院属于线下渠道,默认允许通过饭卡补充人员身份。
|
|
OrderPayDisplayType.rmyy,
|
|
// 超市消费卡属于线下渠道,默认允许通过饭卡补充人员身份。
|
|
OrderPayDisplayType.csxfk,
|
|
};
|
|
static const String _cashierShowHoldActionKey = "cashierShowHoldAction";
|
|
static const String _cashierShowMoreActionKey = "cashierShowMoreAction";
|
|
|
|
/// 顶部隐藏设备编码列表在现有 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";
|
|
|
|
/// 查询菜单最近餐段在现有 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<void> _settingWriteQueue = Future.value();
|
|
|
|
///
|
|
/// 将 setting 的“读取最新值并写回”过程串行执行。
|
|
///
|
|
/// setting 表把用户、系统和食堂扩展配置保存在同一行,仅读写前排队才能避免不同字段的
|
|
/// 并发更新互相覆盖;业务异常仍返回原调用方,后续任务会忽略前一次失败继续执行。
|
|
static Future<T> _serializeSettingWrite<T>(
|
|
Future<T> Function() action,
|
|
) {
|
|
final operation = _settingWriteQueue.catchError((_) {}).then(
|
|
(_) => action(),
|
|
);
|
|
_settingWriteQueue = operation.then<void>((_) {}).catchError((_) {});
|
|
return operation;
|
|
}
|
|
|
|
static Future<void> removeSettingCache() async {
|
|
await _settingCache.emptyCache();
|
|
}
|
|
|
|
static Future<SettingEntity> _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<AppControl>();
|
|
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<bool> _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<AppControl>();
|
|
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<String, dynamic>.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<void> _cacheSetting(SettingEntity setting) async {
|
|
await _settingCache.putFile(
|
|
_settingKey,
|
|
Uint8List.fromList(utf8.encode(json.encode(setting.toJson()))),
|
|
fileExtension: 'json',
|
|
);
|
|
}
|
|
|
|
/// 在串行队列内读取并局部修改最新 sysJson,未涉及的业务键与同一行其他列保持不变。
|
|
static Future<void> _updateSysJson(
|
|
void Function(Map<String, dynamic> sysJson) update,
|
|
) =>
|
|
_serializeSettingWrite(() async {
|
|
final app = Get.find<AppControl>();
|
|
final settingDao = app.dataBase.getSettingDao;
|
|
final setting = await settingDao.get();
|
|
if (setting == null) throw StateError('终端设置尚未初始化');
|
|
final sysJson = Map<String, dynamic>.from(json.decode(setting.sysJson));
|
|
update(sysJson);
|
|
setting.sysJson = json.encode(sysJson);
|
|
await settingDao.edit(setting);
|
|
await _cacheSetting(setting);
|
|
});
|
|
|
|
static Future<SysSetting> getSysSetting() async {
|
|
final setting = await _getSetting();
|
|
return SysSetting.fromJson(json.decode(setting.sysJson));
|
|
}
|
|
|
|
static Future<UserSetting> getUserSetting() async {
|
|
final setting = await _getSetting();
|
|
return UserSetting.fromJson(json.decode(setting.userJson));
|
|
}
|
|
|
|
static Future<CanteenExtraPriceSetting?> getCanteenExtraPriceConfig() async {
|
|
final setting = await _getSetting();
|
|
if (ObjectUtil.isEmpty(setting.canteenExtraPriceConfigJson)) {
|
|
return null;
|
|
}
|
|
return CanteenExtraPriceSetting.fromJson(
|
|
json.decode(setting.canteenExtraPriceConfigJson!),
|
|
);
|
|
}
|
|
|
|
static Future<void> setSysSetting(SysSetting sysInfo) async {
|
|
await _setSetting(sysJson: sysInfo);
|
|
}
|
|
|
|
/// 保存全局餐段选择到 SysSetting;不新增字段,兼容所有已有终端缓存。
|
|
static Future<void> setSelectedProductHourType(ProductHourType hourType) =>
|
|
_serializeSettingWrite(() async {
|
|
final app = Get.find<AppControl>();
|
|
final settingDao = app.dataBase.getSettingDao;
|
|
final setting = await settingDao.get();
|
|
if (setting == null) throw StateError('终端设置尚未初始化');
|
|
final sysJson = Map<String, dynamic>.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<void> setRootAccessResult(RootAccessCheckResult result) =>
|
|
_serializeSettingWrite(() async {
|
|
final app = Get.find<AppControl>();
|
|
final settingDao = app.dataBase.getSettingDao;
|
|
final setting = await settingDao.get();
|
|
if (setting == null) throw StateError('终端设置尚未初始化');
|
|
final sysJson = Map<String, dynamic>.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<Map<String, dynamic>> _getSysExtensionJson() async {
|
|
final setting = await _getSetting();
|
|
return Map<String, dynamic>.from(json.decode(setting.sysJson));
|
|
}
|
|
|
|
/// 仅用于把 v8 之前短暂写入 sysJson 的任务配置导入结构化表,不再提供 JSON 写入口。
|
|
static Future<List<ScheduledTaskConfig>> getScheduledTaskConfigs() async {
|
|
final sysJson = await _getSysExtensionJson();
|
|
final raw = sysJson[_scheduledTaskConfigsKey];
|
|
if (raw is! List) return const [];
|
|
return raw
|
|
.whereType<Map>()
|
|
.map((item) => ScheduledTaskConfig.fromJson(
|
|
Map<String, dynamic>.from(item),
|
|
))
|
|
.toList(growable: false);
|
|
}
|
|
|
|
/// 读取 v8 之前的任务执行快照并按当前枚举过滤未知任务,仅用于迁移。
|
|
static Future<Map<ScheduledTaskType, ScheduledTaskRecord>>
|
|
getScheduledTaskRecords() async {
|
|
final sysJson = await _getSysExtensionJson();
|
|
final raw = sysJson[_scheduledTaskRecordsKey];
|
|
if (raw is! Map) return const {};
|
|
final result = <ScheduledTaskType, ScheduledTaskRecord>{};
|
|
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<String, dynamic>.from(entry.value as Map),
|
|
);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// 读取 v8 之前按相对路径保存的成功上传状态,仅用于导入文件上传记录表。
|
|
static Future<Map<String, TerminalLogUploadState>>
|
|
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<String, dynamic>.from(value as Map),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 读取 v8 之前的最近上传汇总;仅保留兼容能力,不再作为运行时数据源。
|
|
static Future<TerminalLogUploadSummary> getTerminalLogUploadSummary() async {
|
|
final sysJson = await _getSysExtensionJson();
|
|
final raw = sysJson[_terminalLogUploadSummaryKey];
|
|
if (raw is! Map) return const TerminalLogUploadSummary();
|
|
return TerminalLogUploadSummary.fromJson(Map<String, dynamic>.from(raw));
|
|
}
|
|
|
|
/// v8 已将任务与上传状态迁入独立表,导入成功后移除旧 JSON,避免形成双写数据源。
|
|
static Future<void> clearLegacyScheduledTaskState({
|
|
required bool includeUploadState,
|
|
}) =>
|
|
_serializeSettingWrite(() async {
|
|
final app = Get.find<AppControl>();
|
|
final settingDao = app.dataBase.getSettingDao;
|
|
final setting = await settingDao.get();
|
|
if (setting == null) throw StateError('终端设置尚未初始化');
|
|
final sysJson = Map<String, dynamic>.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<void> 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<void> setCanteenExtraPriceConfig(
|
|
CanteenExtraPriceSetting canteenExtraPriceConfig,
|
|
) async {
|
|
final config = await getCanteenExtraPriceConfig();
|
|
if (config == canteenExtraPriceConfig) return;
|
|
await _setSetting(canteenExtraPriceConfig: canteenExtraPriceConfig);
|
|
}
|
|
|
|
/// 读取左侧菜单的顺序和隐藏状态,未知或缺失项由控制器按默认菜单补齐。
|
|
static Future<({List<String> order, Set<String> hidden})>
|
|
getTerminalMenuPreferences() async {
|
|
final setting = await _getSetting();
|
|
final sysJson = Map<String, dynamic>.from(json.decode(setting.sysJson));
|
|
final order = (sysJson[_terminalMenuOrderKey] as List<dynamic>?)
|
|
?.whereType<String>()
|
|
.toList() ??
|
|
const <String>[];
|
|
final hidden = (sysJson[_hiddenTerminalMenusKey] as List<dynamic>?)
|
|
?.whereType<String>()
|
|
.toSet() ??
|
|
const <String>{};
|
|
return (order: order, hidden: hidden);
|
|
}
|
|
|
|
/// 菜单布局属于设备级 UI 设置,直接复用 setting 表中的 sysJson,避免增加数据库字段和迁移。
|
|
static Future<void> setTerminalMenuPreferences({
|
|
required List<String> order,
|
|
required Set<String> hidden,
|
|
}) =>
|
|
_updateSysJson((sysJson) {
|
|
sysJson[_terminalMenuOrderKey] = order;
|
|
sysJson[_hiddenTerminalMenusKey] = hidden.toList();
|
|
});
|
|
|
|
/// 读取支付方式的完整顺序;显隐仍由 SysSetting.usePayDisplayType 维护。
|
|
static Future<List<String>> getPayTypeOrder() async {
|
|
final setting = await _getSetting();
|
|
final sysJson = Map<String, dynamic>.from(json.decode(setting.sysJson));
|
|
return (sysJson[_payTypeOrderKey] as List<dynamic>?)
|
|
?.whereType<String>()
|
|
.toList() ??
|
|
const <String>[];
|
|
}
|
|
|
|
/// 读取允许重新识别人员的支付方式;缺少或非列表配置使用默认值,显式空列表保持全部关闭。
|
|
static Future<Set<OrderPayDisplayType>>
|
|
getCashierReidentifiablePayTypes() async {
|
|
final setting = await _getSetting();
|
|
final sysJson = Map<String, dynamic>.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<String>()
|
|
.map((code) => payTypeByCode[code])
|
|
.whereType<OrderPayDisplayType>()
|
|
.toSet();
|
|
}
|
|
|
|
/// 支付排序、显隐、默认方式及重新识别能力作为同一份终端支付偏好原子保存。
|
|
static Future<void> setPayTypePreferences({
|
|
required SysSetting sysInfo,
|
|
required List<OrderPayDisplayType> order,
|
|
required Set<OrderPayDisplayType> 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})>
|
|
getCashierPagePreferences() async {
|
|
final setting = await _getSetting();
|
|
final sysJson = Map<String, dynamic>.from(json.decode(setting.sysJson));
|
|
return (
|
|
showHoldAction: sysJson[_cashierShowHoldActionKey] != false,
|
|
showMoreAction: sysJson[_cashierShowMoreActionKey] != false,
|
|
);
|
|
}
|
|
|
|
/// 收银快捷操作属于本机页面布局配置,继续复用 sysJson,避免增加数据库字段。
|
|
static Future<void> setCashierPagePreferences({
|
|
required bool showHoldAction,
|
|
required bool showMoreAction,
|
|
}) =>
|
|
_updateSysJson((sysJson) {
|
|
sysJson[_cashierShowHoldActionKey] = showHoldAction;
|
|
sysJson[_cashierShowMoreActionKey] = showMoreAction;
|
|
});
|
|
|
|
/// 读取商品餐段时间;旧终端无配置或持久化值损坏时返回产品默认时间。
|
|
static Future<ProductMealPeriodSchedule>
|
|
getProductMealPeriodSchedule() async {
|
|
final setting = await _getSetting();
|
|
final sysJson = Map<String, dynamic>.from(json.decode(setting.sysJson));
|
|
return ProductMealPeriodSchedule.fromJson(sysJson[_productMealPeriodsKey]);
|
|
}
|
|
|
|
/// 保存经过统一校验的基础三餐时间,不增加 setting 表字段和数据库迁移。
|
|
static Future<void> setProductMealPeriodSchedule(
|
|
ProductMealPeriodSchedule schedule,
|
|
) =>
|
|
_updateSysJson((sysJson) {
|
|
sysJson[_productMealPeriodsKey] = schedule.toJson();
|
|
});
|
|
|
|
/// 按本机当前时间取得配置餐段;未覆盖时段返回空,调用方可保留人工选择。
|
|
static Future<ProductHourType?> getCurrentProductHourType({
|
|
DateTime? now,
|
|
}) async {
|
|
final schedule = await getProductMealPeriodSchedule();
|
|
return schedule.resolve(now ?? DateTime.now());
|
|
}
|
|
|
|
/// 读取顶部需要隐藏的设备类型;缺键、异常值和未知编码均忽略,因此升级后默认五项全部显示。
|
|
static Future<Set<TerminalDeviceType>>
|
|
getHiddenTerminalDeviceStatuses() async {
|
|
final setting = await _getSetting();
|
|
final sysJson = Map<String, dynamic>.from(json.decode(setting.sysJson));
|
|
final source = sysJson[_hiddenTerminalDeviceStatusesKey];
|
|
if (source is! List) return <TerminalDeviceType>{};
|
|
final deviceByCode = {
|
|
for (final device in TerminalDeviceType.values) device.code: device,
|
|
};
|
|
return source
|
|
.whereType<String>()
|
|
.map((code) => deviceByCode[code])
|
|
.whereType<TerminalDeviceType>()
|
|
.toSet();
|
|
}
|
|
|
|
/// 保存逐项隐藏设备编码并移除旧总开关;新设备因不在隐藏列表中会自动显示。
|
|
static Future<void> setHiddenTerminalDeviceStatuses(
|
|
Set<TerminalDeviceType> 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<SerialCardConfig> getSerialCardConfig() async {
|
|
final setting = await _getSetting();
|
|
final sysJson = Map<String, dynamic>.from(json.decode(setting.sysJson));
|
|
final source = sysJson[_serialCardConfigKey];
|
|
if (source is! Map) return const SerialCardConfig();
|
|
try {
|
|
return SerialCardConfig.fromJson(Map<String, dynamic>.from(source));
|
|
} on Object {
|
|
// 旧版本或人工修改的异常配置不能阻断收银读卡,无法解析时回退设备默认参数。
|
|
return const SerialCardConfig();
|
|
}
|
|
}
|
|
|
|
/// 密钥只写入本机设置和串口模块,不记录到日志,也不随普通业务接口上传。
|
|
static Future<void> setSerialCardConfig(SerialCardConfig config) =>
|
|
_updateSysJson((sysJson) {
|
|
sysJson[_serialCardConfigKey] = config.toJson();
|
|
});
|
|
|
|
/// 查询方式顺序和默认项属于页面交互配置,继续保存在 sysJson 中,不增加数据库字段。
|
|
static Future<ReservationQueryPagePreferences>
|
|
getReservationQueryPagePreferences() async {
|
|
final setting = await _getSetting();
|
|
final sysJson = Map<String, dynamic>.from(json.decode(setting.sysJson));
|
|
final byCode = {
|
|
for (final method in ReservationRecognitionMethod.values)
|
|
method.code: method,
|
|
};
|
|
final restored = <ReservationRecognitionMethod>[];
|
|
final storedOrder =
|
|
(sysJson[_reservationQueryMethodOrderKey] as List<dynamic>?)
|
|
?.whereType<String>() ??
|
|
const <String>[];
|
|
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,
|
|
selectedMealSlot: selectedMealSlot,
|
|
verificationDateScope: verificationDateScope,
|
|
);
|
|
}
|
|
|
|
/// 顺序与默认项一起保存,避免连续操作后出现“默认项不在当前列表中”的矛盾状态。
|
|
static Future<void> setReservationQueryPagePreferences({
|
|
required List<ReservationRecognitionMethod> order,
|
|
required ReservationRecognitionMethod defaultMethod,
|
|
required bool autoRecognitionEnabled,
|
|
required bool continuousRecognitionEnabled,
|
|
required bool nameMaskEnabled,
|
|
required bool verificationEnabled,
|
|
required bool quickVerificationEnabled,
|
|
required ReservationVerificationDateScope verificationDateScope,
|
|
}) {
|
|
final normalized = <ReservationRecognitionMethod>[];
|
|
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[_reservationQueryVerificationDateScopeKey] =
|
|
verificationDateScope.code;
|
|
});
|
|
}
|
|
|
|
/// 保存查询菜单最近选中的基础三餐,并刷新现有 setting 文件缓存供下次进入页面恢复。
|
|
static Future<void> setReservationQuerySelectedMealSlot(
|
|
ReservationMealSlot mealSlot,
|
|
) {
|
|
if (!ReservationMealSlot.querySelectableValues.contains(mealSlot)) {
|
|
throw ArgumentError.value(mealSlot, 'mealSlot', '仅允许缓存早餐、午餐或晚餐');
|
|
}
|
|
return _updateSysJson((sysJson) {
|
|
sysJson[_reservationQuerySelectedMealSlotKey] = mealSlot.code;
|
|
});
|
|
}
|
|
|
|
/// 读取指定“路由 + 功能键”场景最近使用的人工硬件输入,异常旧数据会被忽略。
|
|
static Future<List<HardwareInputHistoryEntry>> getHardwareInputHistory(
|
|
String sceneKey,
|
|
) async {
|
|
final store = await _getHardwareInputHistoryStore();
|
|
return List.unmodifiable(store.entriesFor(sceneKey));
|
|
}
|
|
|
|
/// 保存当前场景最近 5 条输入;相同输入只更新备注和时间并置顶,不产生重复记录。
|
|
static Future<void> 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<HardwareInputHistoryStore>
|
|
_getHardwareInputHistoryStore() async {
|
|
final setting = await _getSetting();
|
|
final sysJson = Map<String, dynamic>.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<String, dynamic>.from(source),
|
|
);
|
|
} on Object {
|
|
return HardwareInputHistoryStore.empty;
|
|
}
|
|
}
|
|
|
|
// 根据后台返回的数据更新到缓存
|
|
static Future<void> 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)) {
|
|
if (type == '12') {
|
|
sysSetting.terminalRole = TerminalRole.printer;
|
|
} else {
|
|
final t = SysType.values.fromCode(type);
|
|
if (ObjectUtil.isNotEmpty(t)) {
|
|
sysSetting.sysType = t!;
|
|
sysSetting.terminalRole = TerminalRole.cashier;
|
|
}
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
|
|
/// 将服务端校验过的 EY_PRINT 一对一绑定和打印参数持久化到本机。
|
|
static Future<SysSetting> updatePrinterBootstrap(
|
|
Map<String, dynamic> bootstrap,
|
|
) async {
|
|
final setting = await getSysSetting();
|
|
final sourceType = bootstrap['sourceHardType']?.toString();
|
|
final businessType = SysType.values.fromCode(sourceType ?? '');
|
|
if (businessType == null) {
|
|
throw const FormatException('打印终端绑定来源不是超市或食堂设备');
|
|
}
|
|
final config = bootstrap['printerConfig'] is Map
|
|
? Map<String, dynamic>.from(bootstrap['printerConfig'] as Map)
|
|
: <String, dynamic>{};
|
|
setting.terminalRole = TerminalRole.printer;
|
|
setting.sysType = businessType;
|
|
setting.printerSourceHardId = bootstrap['sourceHardId']?.toString();
|
|
setting.printerSourceHardName = bootstrap['sourceHardName']?.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<bool> 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;
|
|
}
|
|
}
|