diff --git a/lib/common/models/database/database_models.dart b/lib/common/models/database/database_models.dart index 7d06f91..608ed49 100644 --- a/lib/common/models/database/database_models.dart +++ b/lib/common/models/database/database_models.dart @@ -149,6 +149,14 @@ class SysSetting extends Equatable { @JsonKey(defaultValue: false) bool manualHardwareInput; + /// 手动退出登录时是否忘记已保存的登录密码;旧配置缺失时同样默认开启。 + @JsonKey(defaultValue: true) + bool forgetPasswordOnManualLogout; + + /// 机器突然断电或重启后,下次冷启动是否要求重新输入登录密码;默认开启。 + @JsonKey(defaultValue: true) + bool forgetPasswordOnPowerLoss; + /// Root 最近一次真实命令检查状态;旧配置缺失时按未检查处理。 @JsonKey(defaultValue: RootAccessStatus.unknown) RootAccessStatus rootAccessStatus; @@ -174,6 +182,8 @@ class SysSetting extends Equatable { this.printerOutputMode = 'IMAGE', this.tenantList, this.manualHardwareInput = false, + this.forgetPasswordOnManualLogout = true, + this.forgetPasswordOnPowerLoss = true, this.rootAccessStatus = RootAccessStatus.unknown, this.rootAccessCheckedAt, this.rootAccessDiagnostic, @@ -234,6 +244,8 @@ class SysSetting extends Equatable { defaultPayDisplayType, tenantList, manualHardwareInput, + forgetPasswordOnManualLogout, + forgetPasswordOnPowerLoss, rootAccessStatus, rootAccessCheckedAt?.millisecondsSinceEpoch, rootAccessDiagnostic, diff --git a/lib/common/models/database/database_models.g.dart b/lib/common/models/database/database_models.g.dart index a632bd8..49233ab 100644 --- a/lib/common/models/database/database_models.g.dart +++ b/lib/common/models/database/database_models.g.dart @@ -23,6 +23,10 @@ SysSetting _$SysSettingFromJson(Map json) => SysSetting( ?.map((e) => ServiceTenant.fromJson(e as Map)) .toList(), manualHardwareInput: json['manualHardwareInput'] as bool? ?? false, + forgetPasswordOnManualLogout: + json['forgetPasswordOnManualLogout'] as bool? ?? true, + forgetPasswordOnPowerLoss: + json['forgetPasswordOnPowerLoss'] as bool? ?? true, rootAccessStatus: $enumDecodeNullable( _$RootAccessStatusEnumMap, json['rootAccessStatus']) ?? RootAccessStatus.unknown, @@ -143,6 +147,8 @@ Map _$SysSettingToJson(SysSetting instance) => _$OrderPayDisplayTypeEnumMap[instance.defaultPayDisplayType], 'tenantList': instance.tenantList, 'manualHardwareInput': instance.manualHardwareInput, + 'forgetPasswordOnManualLogout': instance.forgetPasswordOnManualLogout, + 'forgetPasswordOnPowerLoss': instance.forgetPasswordOnPowerLoss, 'rootAccessStatus': _$RootAccessStatusEnumMap[instance.rootAccessStatus]!, 'rootAccessCheckedAt': instance.rootAccessCheckedAt?.toIso8601String(), 'rootAccessDiagnostic': instance.rootAccessDiagnostic, diff --git a/lib/common/utils/app/app_setting_tool.dart b/lib/common/utils/app/app_setting_tool.dart index 62ce100..5ce9b3f 100644 --- a/lib/common/utils/app/app_setting_tool.dart +++ b/lib/common/utils/app/app_setting_tool.dart @@ -404,6 +404,50 @@ abstract class AppSettingTool { await _setSetting(userJson: userInfo); } + /// 清除已保存的登录密码和会话令牌,保留用户名与租户便于下次登录快速回填。 + static Future forgetSavedPassword() async { + final userInfo = await getUserSetting(); + if (ObjectUtil.isEmpty(userInfo.password)) return; + userInfo + ..password = null + ..userId = null + ..accessToken = null + ..refreshToken = null + ..expiresTime = null; + await setUserSetting(userInfo); + } + + /// 手动退出登录时按设置决定是否忘记密码。 + /// + /// 关闭开关则完整保留凭据(下次启动自动登录);离线时不清除, + /// 保证断网机器仍可凭已保存凭据重新进入离线模式,与基线行为一致。 + static Future applyManualLogoutPasswordPolicy() async { + final sysInfo = await getSysSetting(); + if (!sysInfo.forgetPasswordOnManualLogout) return; + if (!AppNetworkTool.isAutoNetwork()) return; + await forgetSavedPassword(); + } + + /// 冷启动时执行“突然关机忘记密码”策略。 + /// + /// 终端常驻运行,除手动退出外的任何结束(断电、强制重启、进程被杀) + /// 都属于突然关机;开启开关且当前联网时清除保存的密码,要求重新登录。 + /// 返回是否清除了登录凭据,调用方据此回退到登录页。 + static Future applyStartupPowerLossPasswordPolicy() async { + final userInfo = await getUserSetting(); + if (ObjectUtil.isEmpty(userInfo.password)) return false; + final sysInfo = await getSysSetting(); + if (!sysInfo.forgetPasswordOnPowerLoss) return false; + // 冷启动监听可能尚未写入初始网络状态,这里直接探测当前连接。 + if (!await AppNetworkTool.isHandNetwork()) { + logger.w('|$_tag|启动时网络不可用,保留已保存密码以便离线进入'); + return false; + } + await forgetSavedPassword(); + logger.w('|$_tag|检测到上次非手动退出,已按设置清除保存的登录密码'); + return true; + } + static Future setCanteenExtraPriceConfig( CanteenExtraPriceSetting canteenExtraPriceConfig, ) async { diff --git a/lib/common/utils/app/control/app_control.dart b/lib/common/utils/app/control/app_control.dart index d3f8400..4ae55da 100644 --- a/lib/common/utils/app/control/app_control.dart +++ b/lib/common/utils/app/control/app_control.dart @@ -290,6 +290,14 @@ class AppControl extends GetxController { if (AppNetworkTool.subscription == null) { AppNetworkTool.listenNetwork(); } + // 突然关机密码策略必须在读取用户凭据前执行,否则自动登录会抢先使用旧密码。 + final clearedSavedPassword = + await AppSettingTool.applyStartupPowerLossPasswordPolicy(); + var resolvedDestination = destination; + if (clearedSavedPassword && destination == RouteNames.terminal) { + // 凭据已按策略清除,跳过更新恢复直达终端的预留路径,回到登录页。 + resolvedDestination = RouteNames.login; + } final userSetting = await AppSettingTool.getUserSetting(); _authenticatedServicesEnabled = ObjectUtil.isNotEmpty(userSetting.accessToken); @@ -306,7 +314,7 @@ class AppControl extends GetxController { : '启动完成'; await Future.delayed(const Duration(milliseconds: 650)); if (Get.currentRoute == RouteNames.load) { - Get.offAllNamed(destination); + Get.offAllNamed(resolvedDestination); } } catch (e, stack) { final currentIndex = startupSteps.indexWhere( diff --git a/lib/common/utils/app/terminal_navigation_tool.dart b/lib/common/utils/app/terminal_navigation_tool.dart index 7acc7ad..589e8ad 100644 --- a/lib/common/utils/app/terminal_navigation_tool.dart +++ b/lib/common/utils/app/terminal_navigation_tool.dart @@ -83,9 +83,8 @@ abstract final class TerminalNavigationTool { static Future logout() async { if (!await _verifyAdminSession(actionLabel: '退出登录')) return; Get.find().lock(); - if (AppNetworkTool.isAutoNetwork()) { - await AppSettingTool.setUserSetting(UserSetting()); - } + // 是否忘记已保存的登录密码由基本设置开关决定;离线时始终保留以便离线重进。 + await AppSettingTool.applyManualLogoutPasswordPolicy(); Get.offAllNamed(RouteNames.login); } diff --git a/lib/pages/root/device_setup/view.dart b/lib/pages/root/device_setup/view.dart index 4755b1f..dd597b4 100644 --- a/lib/pages/root/device_setup/view.dart +++ b/lib/pages/root/device_setup/view.dart @@ -64,7 +64,8 @@ class _DeviceSetupPageState extends State { /// 清理账号登录态并返回登录页,不保留未完成的设备绑定输入。 Future _logout() async { - await AppSettingTool.setUserSetting(UserSetting()); + // 登录密码是否清除遵循基本设置里的手动退出忘记密码开关。 + await AppSettingTool.applyManualLogoutPasswordPolicy(); await Get.find().suspendAuthenticatedServices(); Get.offAllNamed(RouteNames.login); } diff --git a/lib/pages/root/login/controller.dart b/lib/pages/root/login/controller.dart index 1e872bf..7932baf 100644 --- a/lib/pages/root/login/controller.dart +++ b/lib/pages/root/login/controller.dart @@ -68,6 +68,11 @@ class LoginController extends GetxController { if (isClosed) return; userSetting = loadedUserSetting; sysType.value = sysSetting.sysType; + // 密码被忘记后用户名仍保留,回填可让收银员只补输密码;已人工输入时不覆盖。 + if (usernameC.text.isEmpty && + ObjectUtil.isNotEmpty(userSetting.username)) { + usernameC.text = userSetting.username!; + } await _restoreCachedTenantSelection(); if (isClosed) return; diff --git a/lib/pages/terminal/setting/controller.dart b/lib/pages/terminal/setting/controller.dart index 19faeaf..d89c6ca 100644 --- a/lib/pages/terminal/setting/controller.dart +++ b/lib/pages/terminal/setting/controller.dart @@ -28,6 +28,12 @@ class SettingController extends GetxController with WidgetsBindingObserver { /// 是否使用人工输入代替扫码、读卡和人脸等真实硬件输入。 final manualHardwareInput = false.obs; + /// 手动退出登录时是否忘记已保存的登录密码。 + final forgetPasswordOnManualLogout = true.obs; + + /// 机器突然关机(断电、强制重启)后是否忘记已保存的登录密码。 + final forgetPasswordOnPowerLoss = true.obs; + /// Android Kiosk 当前能力与运行状态,应用恢复前台时刷新。 final kioskStatus = AppRuntimeTool.kioskStatus.obs; @@ -302,6 +308,8 @@ class SettingController extends GetxController with WidgetsBindingObserver { isBound.value = ObjectUtil.isNotEmpty(setting.serviceMerchantId); manualHardwareInput.value = AppRuntimeTool.isEmulator || setting.manualHardwareInput; + forgetPasswordOnManualLogout.value = setting.forgetPasswordOnManualLogout; + forgetPasswordOnPowerLoss.value = setting.forgetPasswordOnPowerLoss; AppRuntimeTool.applyManualHardwareInput(setting.manualHardwareInput); } @@ -1093,6 +1101,41 @@ class SettingController extends GetxController with WidgetsBindingObserver { Toast.success(msg: enabled ? '已设置为主控手动输入模式' : '已恢复真实硬件模式'); } + /// 切换手动退出登录时是否忘记已保存的登录密码。 + Future setForgetPasswordOnManualLogout(bool enabled) async { + final setting = sysSetting.value; + if (setting == null) return; + setting.forgetPasswordOnManualLogout = enabled; + await AppSettingTool.setSysSetting(setting); + forgetPasswordOnManualLogout.value = enabled; + sysSetting.refresh(); + Toast.success(msg: enabled ? '手动退出时将忘记登录密码' : '手动退出时将保留登录密码'); + } + + /// 切换机器突然关机后是否忘记已保存的登录密码。 + Future setForgetPasswordOnPowerLoss(bool enabled) async { + final setting = sysSetting.value; + if (setting == null) return; + setting.forgetPasswordOnPowerLoss = enabled; + await AppSettingTool.setSysSetting(setting); + forgetPasswordOnPowerLoss.value = enabled; + sysSetting.refresh(); + Toast.success(msg: enabled ? '突然关机后将忘记登录密码' : '突然关机后将保留登录密码'); + } + + /// 把两个忘记密码开关恢复为默认开启。 + Future resetForgetPasswordPolicies() async { + final setting = sysSetting.value; + if (setting == null) return; + setting.forgetPasswordOnManualLogout = true; + setting.forgetPasswordOnPowerLoss = true; + await AppSettingTool.setSysSetting(setting); + forgetPasswordOnManualLogout.value = true; + forgetPasswordOnPowerLoss.value = true; + sysSetting.refresh(); + Toast.success(msg: '忘记密码开关已恢复默认'); + } + /// 立即启停当前 Kiosk,并同步保存以后进入 App 时是否自动恢复 Lock Task。 Future setKioskAutoEnterEnabled(bool enabled) async { if (!kioskStatus.value.available || kioskSwitching.value) return; diff --git a/lib/pages/terminal/setting/widgets/setting_baseInfo.dart b/lib/pages/terminal/setting/widgets/setting_baseInfo.dart index 907eee3..5d95ead 100644 --- a/lib/pages/terminal/setting/widgets/setting_baseInfo.dart +++ b/lib/pages/terminal/setting/widgets/setting_baseInfo.dart @@ -267,6 +267,8 @@ Widget settingBaseInfo(BuildContext context) { ), ), SizedBox(height: XfjDesignTokens.cardGap.h), + _loginPasswordPolicySetting(), + SizedBox(height: XfjDesignTokens.cardGap.h), _terminalDeviceStatusVisibilitySetting(), SizedBox(height: XfjDesignTokens.cardGap.h), SettingGroup( @@ -323,6 +325,65 @@ Widget settingBaseInfo(BuildContext context) { }); } +/// 登录密码的两个忘记时机开关,均默认开启,并提供一键恢复默认。 +/// 忘记密码只清除已保存的密码,用户名仍会在登录页回填。 +Widget _loginPasswordPolicySetting() { + final controller = Get.find(); + return SettingGroup( + title: '登录密码', + child: Column( + children: [ + Obx(() { + final enabled = controller.forgetPasswordOnManualLogout.value; + return SettingInfoRow( + icon: FAssets.icons.logOut, + label: '手动退出时忘记密码', + subtitle: '开启后退出登录将清除已保存的密码,下次登录需重新输入;离线退出时仍保留以便断网重进。', + value: enabled ? '已开启' : '已关闭', + trailing: FSwitch( + key: const ValueKey('setting-forget-password-manual-logout'), + semanticLabel: '手动退出时忘记登录密码', + value: enabled, + onChange: (value) => unawaited( + controller.setForgetPasswordOnManualLogout(value), + ), + ), + ); + }), + Obx(() { + final enabled = controller.forgetPasswordOnPowerLoss.value; + return SettingInfoRow( + icon: FAssets.icons.powerOff, + label: '突然关机时忘记密码', + subtitle: '开启后断电或重启再次启动时需重新输入密码,防止他人开机后直接进入系统。', + value: enabled ? '已开启' : '已关闭', + trailing: FSwitch( + key: const ValueKey('setting-forget-password-power-loss'), + semanticLabel: '突然关机时忘记登录密码', + value: enabled, + onChange: (value) => unawaited( + controller.setForgetPasswordOnPowerLoss(value), + ), + ), + ); + }), + SettingInfoRow( + icon: FAssets.icons.rotateCcw, + label: '恢复默认', + subtitle: '把两个忘记密码开关都恢复为默认开启。', + showDivider: false, + trailing: SettingActionButton( + label: '恢复默认', + icon: FAssets.icons.rotateCcw, + primary: true, + onTap: () => unawaited(controller.resetForgetPasswordPolicies()), + ), + ), + ], + ), + ); +} + /// 以数据驱动的五行开关提供顶部设备状态逐项配置,并与详情弹框共享应用级响应状态。 Widget _terminalDeviceStatusVisibilitySetting() { final control = Get.find();