414 lines
13 KiB
Dart
414 lines
13 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:common_utils/common_utils.dart';
|
|
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
|
import 'package:forui/forui.dart';
|
|
import 'package:get/get.dart';
|
|
|
|
import 'package:pj_supermarket_cloud/common/index.dart';
|
|
|
|
class SettingController extends GetxController {
|
|
static const String _tag = "设置 Controller";
|
|
|
|
final appC = Get.find<AppControl>();
|
|
|
|
Rx<SysSetting?> sysSetting = Rx(null);
|
|
|
|
Rx<UserSetting?> userSetting = Rx(null);
|
|
|
|
// 全局文字大小
|
|
var globalFontSizeC = FRadioSelectGroupController<String>(
|
|
value: SettingGlobalFontSize.xl3.code,
|
|
);
|
|
|
|
// 付款方式
|
|
var globalPayTableC = FMultiSelectGroupController<String>(
|
|
values: {},
|
|
);
|
|
|
|
// 默认付款方式
|
|
var defaultGlobalPayTableC = FRadioSelectGroupController<String>();
|
|
|
|
/// 商品餐段时间配置;餐段校验弹框和自动切换按该时间范围判定。
|
|
final productMealPeriodSchedule = ProductMealPeriodSchedule.defaults.obs;
|
|
|
|
/// 餐段配置正在保存时禁用重复提交并展示按钮状态。
|
|
final productMealPeriodSaving = false.obs;
|
|
|
|
_initData() {
|
|
update(["setting"]);
|
|
}
|
|
|
|
RxList<SettingMenu> menuList = <SettingMenu>[
|
|
SettingMenu(
|
|
prefixIcon: FAssets.icons.bookCopy,
|
|
title: "基本信息",
|
|
type: SettingMenuType.baseInfo,
|
|
),
|
|
SettingMenu(
|
|
prefixIcon: FAssets.icons.databaseBackup,
|
|
title: "资源备份",
|
|
type: SettingMenuType.backup,
|
|
),
|
|
SettingMenu(
|
|
prefixIcon: FAssets.icons.refreshCw,
|
|
title: "定时任务",
|
|
type: SettingMenuType.syncSetting,
|
|
),
|
|
SettingMenu(
|
|
prefixIcon: FAssets.icons.japaneseYen,
|
|
title: "支付设置",
|
|
type: SettingMenuType.paySetting,
|
|
),
|
|
SettingMenu(
|
|
prefixIcon: FAssets.icons.utensils,
|
|
title: "餐段时间配置",
|
|
type: SettingMenuType.mealPeriodSetting,
|
|
),
|
|
SettingMenu(
|
|
prefixIcon: FAssets.icons.scanFace,
|
|
title: "人脸管理",
|
|
type: SettingMenuType.faceSetting,
|
|
),
|
|
SettingMenu(
|
|
prefixIcon: FAssets.icons.settings2,
|
|
title: "人脸设置",
|
|
type: SettingMenuType.facePluginSetting,
|
|
),
|
|
SettingMenu(
|
|
prefixIcon: FAssets.icons.shoppingCart,
|
|
title: "商品管理",
|
|
type: SettingMenuType.productSetting,
|
|
),
|
|
SettingMenu(
|
|
prefixIcon: FAssets.icons.logs,
|
|
title: "日志预览",
|
|
type: SettingMenuType.logPreview,
|
|
),
|
|
SettingMenu(
|
|
prefixIcon: FAssets.icons.rocket,
|
|
title: "版本管理",
|
|
type: SettingMenuType.versionSetting,
|
|
),
|
|
].obs;
|
|
|
|
Rx<SettingMenuType> cuurentMenuType = SettingMenuType.baseInfo.obs;
|
|
|
|
/// 各任务尚未保存的 Cron 输入草稿,以任务类型隔离编辑状态。
|
|
final scheduledTaskCronDrafts = <ScheduledTaskType, String>{}.obs;
|
|
|
|
/// 各任务 Cron 草稿的本地校验错误,保存成功或继续输入后移除。
|
|
final scheduledTaskCronErrors = <ScheduledTaskType, String>{}.obs;
|
|
|
|
/// 正在持久化配置的任务集合,用于禁止同任务重复提交并局部禁用控件。
|
|
final savingScheduledTasks = <ScheduledTaskType>{}.obs;
|
|
|
|
late StreamSubscription mqUpdateListen;
|
|
|
|
@override
|
|
void onInit() async {
|
|
super.onInit();
|
|
await getSetting();
|
|
await _initScheduledTasks();
|
|
globalFontSizeC.addValueListener((v) {
|
|
final fontSizeType = SettingGlobalFontSize.values.fromCode(
|
|
v.first,
|
|
);
|
|
if (fontSizeType == null) return;
|
|
final appTheme = Get.find<AppThemeControl>();
|
|
appTheme.setSizeScalar(fontSizeType);
|
|
});
|
|
globalPayTableC.addValueListener((v) async {
|
|
List<OrderPayDisplayType> usePayList = [];
|
|
for (final i in v) {
|
|
final payType = OrderPayDisplayType.values.fromCode(
|
|
i,
|
|
);
|
|
if (payType == null) continue;
|
|
usePayList.add(payType);
|
|
}
|
|
usePayList.sort((pren, next) => pren.code.compareTo(next.code));
|
|
sysSetting.value?.usePayDisplayType = usePayList;
|
|
await AppSettingTool.setSysSetting(sysSetting.value!);
|
|
});
|
|
defaultGlobalPayTableC.addValueListener((v) async {
|
|
final payType = OrderPayDisplayType.values.fromCode(
|
|
v.first,
|
|
);
|
|
if (payType == null) return;
|
|
sysSetting.value?.defaultPayDisplayType = payType;
|
|
await AppSettingTool.setSysSetting(sysSetting.value!);
|
|
});
|
|
mqUpdateListen = appC.mqUpdateEventBus.on<MqtActionEnum>().listen(
|
|
(event) async {
|
|
if (event == MqtActionEnum.CONFIG) {
|
|
sysSetting.value = await AppSettingTool.getSysSetting();
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
void onReady() {
|
|
super.onReady();
|
|
_initData();
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
super.onClose();
|
|
globalFontSizeC.dispose();
|
|
globalPayTableC.dispose();
|
|
defaultGlobalPayTableC.dispose();
|
|
mqUpdateListen.cancel();
|
|
// 隐藏键盘
|
|
// FocusManager.instance.primaryFocus?.unfocus();
|
|
// SystemChannels.textInput.invokeMethod('TextInput.hide');
|
|
}
|
|
|
|
void doMenu(SettingMenu v) {
|
|
if (v.type == cuurentMenuType.value) {
|
|
return;
|
|
}
|
|
cuurentMenuType.value = v.type;
|
|
menuList.refresh();
|
|
}
|
|
|
|
/// 按菜单类型直接切换(供人脸设置页跳转人脸管理等场景使用)。
|
|
void openMenu(SettingMenuType type) {
|
|
cuurentMenuType.value = type;
|
|
menuList.refresh();
|
|
}
|
|
|
|
/// 启动统一调度器并用已保存的 Cron 规则初始化草稿。
|
|
Future<void> _initScheduledTasks() async {
|
|
try {
|
|
await SyncCheckTool.instance.startAll();
|
|
scheduledTaskCronDrafts.assignAll({
|
|
for (final entry in SyncCheckTool.instance.configs.entries)
|
|
entry.key: entry.value.cron,
|
|
});
|
|
} catch (e, stack) {
|
|
logger.e("|$_tag|定时任务初始化失败error:|$e|stack:|$stack|");
|
|
}
|
|
}
|
|
|
|
/// 更新指定任务的 Cron 草稿,输入变化后清除该任务旧校验错误。
|
|
void setScheduledTaskCronDraft(ScheduledTaskType type, String value) {
|
|
scheduledTaskCronDrafts[type] = value;
|
|
scheduledTaskCronErrors.remove(type);
|
|
}
|
|
|
|
/// 切换任务自动调度开关;关闭运行保障任务前要求操作人员确认。
|
|
Future<void> setScheduledTaskEnabled(
|
|
ScheduledTaskType type,
|
|
bool enabled,
|
|
) async {
|
|
final config = SyncCheckTool.instance.configs[type];
|
|
if (config == null || config.enabled == enabled) return;
|
|
if (!enabled && _isRuntimeGuardTask(type)) {
|
|
final confirmed = await appMessageDialog(
|
|
Get.context!,
|
|
'关闭后终端将不再自动执行该运行保障任务,是否继续?',
|
|
);
|
|
if (!confirmed) return;
|
|
}
|
|
await _saveScheduledTaskConfig(config.copyWith(enabled: enabled));
|
|
}
|
|
|
|
/// 切换任务是否仅在完整空闲窗口自动执行,并持久化到结构化任务表。
|
|
Future<void> setScheduledTaskIdleOnly(
|
|
ScheduledTaskType type,
|
|
bool idleOnly,
|
|
) async {
|
|
final config = SyncCheckTool.instance.configs[type];
|
|
if (config == null || config.idleOnly == idleOnly) return;
|
|
await _saveScheduledTaskConfig(config.copyWith(idleOnly: idleOnly));
|
|
}
|
|
|
|
/// Cron 弹窗返回经过本地解析校验的表达式,Controller 仍在持久化边界再次校验。
|
|
Future<void> setScheduledTaskCron(
|
|
ScheduledTaskType type,
|
|
String expression,
|
|
) async {
|
|
final config = SyncCheckTool.instance.configs[type];
|
|
if (config == null) return;
|
|
final cron = expression.trim();
|
|
if (!SyncCheckTool.isValidCron(cron)) {
|
|
Toast.error(msg: '请输入有效的 6 段 Cron 表达式');
|
|
return;
|
|
}
|
|
if (cron == config.cron) return;
|
|
await _saveScheduledTaskConfig(config.copyWith(cron: cron));
|
|
}
|
|
|
|
/// 串联调度重建和 SQLite 保存,负责重复提交保护、草稿回退及用户反馈。
|
|
Future<void> _saveScheduledTaskConfig(ScheduledTaskConfig config) async {
|
|
if (savingScheduledTasks.contains(config.type)) return;
|
|
final previous = SyncCheckTool.instance.configs[config.type];
|
|
savingScheduledTasks.add(config.type);
|
|
try {
|
|
await SyncCheckTool.instance.updateConfig(config);
|
|
scheduledTaskCronDrafts[config.type] = config.cron;
|
|
scheduledTaskCronErrors.remove(config.type);
|
|
Toast.success(msg: '任务配置已保存');
|
|
} catch (e, stack) {
|
|
if (previous != null) {
|
|
scheduledTaskCronDrafts[config.type] = previous.cron;
|
|
}
|
|
logger.e(
|
|
"|$_tag|保存定时任务失败type:${config.type.name}|error:$e|stack:$stack|");
|
|
Toast.error(msg: '任务配置保存失败,请重试');
|
|
} finally {
|
|
savingScheduledTasks.remove(config.type);
|
|
}
|
|
}
|
|
|
|
/// 手动触发指定任务,并根据立即执行、排队或合并结果向操作人员反馈。
|
|
Future<void> runScheduledTaskNow(ScheduledTaskType type) async {
|
|
final scheduler = SyncCheckTool.instance;
|
|
if (scheduler.runningTasks.contains(type) ||
|
|
scheduler.pendingTasks.contains(type)) {
|
|
await scheduler.trigger(type);
|
|
Toast.success(msg: '已合并到待执行任务');
|
|
return;
|
|
}
|
|
await scheduler.trigger(type);
|
|
if (scheduler.pendingTasks.contains(type)) {
|
|
Toast.success(msg: '任务已排队,将在安全窗口执行');
|
|
return;
|
|
}
|
|
final record = scheduler.records[type];
|
|
if (record?.success == true) {
|
|
Toast.success(msg: record?.message ?? '任务执行完成');
|
|
} else {
|
|
Toast.error(msg: record?.message ?? '任务执行失败');
|
|
}
|
|
}
|
|
|
|
/// 返回关闭自动调度前需要二次确认的 MQTT、打印、广播和设备状态保障任务。
|
|
bool _isRuntimeGuardTask(ScheduledTaskType type) => const {
|
|
ScheduledTaskType.mqReconnect,
|
|
ScheduledTaskType.printCheck,
|
|
ScheduledTaskType.broadcastCheck,
|
|
ScheduledTaskType.deviceStatus,
|
|
}.contains(type);
|
|
|
|
// 同步设备信息(人脸设置页等入口使用)
|
|
void syncConfig() async {
|
|
final isNetwork = AppNetworkTool.isAutoNetworkMsg();
|
|
if (!isNetwork) return;
|
|
EasyLoading.show(
|
|
status: '同步中...',
|
|
maskType: EasyLoadingMaskType.black,
|
|
);
|
|
try {
|
|
final setting = await SyncConfigTool.autoSyncSysInfo();
|
|
if (setting != null) sysSetting.value = setting;
|
|
Toast.success(msg: "同步设备信息成功");
|
|
EasyLoading.dismiss();
|
|
} catch (e, stack) {
|
|
Toast.error(msg: "同步设备信息错误");
|
|
logger.e("|$_tag|同步设备信息error:|$e|stack:|$stack|");
|
|
EasyLoading.dismiss();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 备份sql
|
|
void backupsSql() async {
|
|
EasyLoading.show(
|
|
status: '备份中...',
|
|
maskType: EasyLoadingMaskType.black,
|
|
);
|
|
try {
|
|
final bool = await AppFileTool.backupSqlFile();
|
|
if (!bool) throw Error();
|
|
EasyLoading.dismiss();
|
|
Toast.success(msg: "备份中sql成功");
|
|
EasyLoading.dismiss();
|
|
} catch (e) {
|
|
Toast.error(msg: "备份中sql失败");
|
|
logger.e("|$_tag|备份中sqlerror:|$e|");
|
|
EasyLoading.dismiss();
|
|
}
|
|
}
|
|
|
|
// 备份日志
|
|
void backupsLog() async {
|
|
Toast.success(msg: "当前日志为实时备份");
|
|
}
|
|
|
|
// sql恢复
|
|
Future<void> recoverSql() async {
|
|
EasyLoading.show(
|
|
status: '恢复中...',
|
|
maskType: EasyLoadingMaskType.black,
|
|
);
|
|
try {
|
|
await AppFileTool.useLastBackDatabase();
|
|
EasyLoading.dismiss();
|
|
Toast.success(msg: "sql恢复成功");
|
|
EasyLoading.dismiss();
|
|
} catch (e) {
|
|
Toast.error(msg: "sql恢复失败");
|
|
logger.e("|$_tag|sql恢复失败error:|$e|");
|
|
EasyLoading.dismiss();
|
|
}
|
|
}
|
|
|
|
Future<void> getSetting() async {
|
|
sysSetting.value = await AppSettingTool.getSysSetting();
|
|
productMealPeriodSchedule.value =
|
|
await AppSettingTool.getProductMealPeriodSchedule();
|
|
if (sysSetting.value!.globalFontSize != null) {
|
|
globalFontSizeC.value = {sysSetting.value!.globalFontSize!.code};
|
|
}
|
|
if (!ObjectUtil.isEmptyList(sysSetting.value!.usePayDisplayType)) {
|
|
globalPayTableC.value =
|
|
sysSetting.value!.usePayDisplayType!.map((v) => v.code).toSet();
|
|
}
|
|
if (sysSetting.value!.defaultPayDisplayType != null) {
|
|
defaultGlobalPayTableC.value = {
|
|
sysSetting.value!.defaultPayDisplayType!.code
|
|
};
|
|
}
|
|
}
|
|
|
|
Future<bool> checkIsBind() async {
|
|
final sysSetting = await AppSettingTool.getSysSetting();
|
|
final id = sysSetting.serviceMerchantId;
|
|
if (ObjectUtil.isEmpty(id)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// 校验并保存餐段时间;空档允许保留,相邻或交叉范围由强类型模型统一处理。
|
|
Future<bool> saveProductMealPeriods(List<ProductMealPeriod> periods) async {
|
|
if (productMealPeriodSaving.value) return false;
|
|
final error = ProductMealPeriodSchedule.validate(periods);
|
|
if (error != null) {
|
|
Toast.error(msg: error);
|
|
return false;
|
|
}
|
|
productMealPeriodSaving.value = true;
|
|
try {
|
|
final schedule = ProductMealPeriodSchedule.checked(periods);
|
|
await AppSettingTool.setProductMealPeriodSchedule(schedule);
|
|
productMealPeriodSchedule.value = schedule;
|
|
Toast.success(msg: '餐段配置已保存');
|
|
return true;
|
|
} catch (e, stack) {
|
|
logger.e('|$_tag|保存餐段配置失败error:$e|stack:$stack|');
|
|
Toast.error(msg: '餐段配置保存失败,请重试');
|
|
return false;
|
|
} finally {
|
|
productMealPeriodSaving.value = false;
|
|
}
|
|
}
|
|
}
|