Files
pj_supermarket_cloud/lib/pages/setting/controller.dart
T
2026-09-09 17:45:45 +08:00

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;
}
}
}