Files
biz_xfj_cloud/lib/pages/root/login/controller.dart
T
2026-09-22 13:29:10 +08:00

347 lines
12 KiB
Dart

import 'dart:async';
import 'package:common_utils/common_utils.dart';
import 'package:flutter/material.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 LoginController extends GetxController {
/// 登录流程日志标签,仅用于定位当前控制器产生的异常。
static const String _tag = "登录页面 Controller";
/// 当前终端系统设置;初始化完成后由本地设置服务提供。
late SysSetting sysSetting;
/// 当前登录用户设置;租户、账号和令牌均通过本地设置服务持久化。
late UserSetting userSetting;
/// 当前终端业务类型,用于驱动登录页相关展示与登录参数。
final Rx<SysType> sysType = SysType.CANTEEN.obs;
/// 服务端或离线缓存提供的可选租户列表。
final RxList<ServiceTenant> tenantList = <ServiceTenant>[].obs;
/// Forui 单选控制器;其选中值统一使用租户 `code`,请求头持久化值使用租户 `id`。
final tenantC = FRadioSelectGroupController<String>();
/// 当前选中租户,登录成功时从该对象提取服务端数值 `id`。
ServiceTenant currentTenant = ServiceTenant();
/// 登录用户名输入控制器,由当前登录页 Widget 子树独占并在关闭时释放。
final usernameC = TextEditingController();
/// 登录密码输入控制器,由当前登录页 Widget 子树独占并在关闭时释放。
final passwordC = TextEditingController();
/// 用户是否已经主动编辑表单;为 true 时禁止延迟自动登录抢占焦点。
bool _manualInputStarted = false;
/// 初始化租户默认项时暂时屏蔽选择监听,避免把程序赋值误显示成用户切换操作。
bool _applyingInitialTenantSelection = false;
/// 通知登录页局部 GetBuilder 刷新初始化后的非响应式内容。
void _initData() {
update(["login"]);
}
/// 同步注册租户监听,并异步启动可在页面销毁时安全终止的初始化流程。
@override
void onInit() {
super.onInit();
tenantC.addValueListener(_handleTenantSelection);
unawaited(_initialize());
}
/// 顺序读取系统设置、用户设置和租户列表;每次异步等待后先确认页面仍然存活。
Future<void> _initialize() async {
try {
final loadedSysSetting = await AppSettingTool.getSysSetting();
if (isClosed) return;
sysSetting = loadedSysSetting;
final loadedUserSetting = await AppSettingTool.getUserSetting();
if (isClosed) return;
userSetting = loadedUserSetting;
sysType.value = sysSetting.sysType;
await _restoreCachedTenantSelection();
if (isClosed) return;
await getTenantList();
if (isClosed ||
_manualInputStarted ||
ObjectUtil.isEmpty(currentTenant.id)) {
return;
}
await autoLogin();
} catch (error) {
if (!isClosed) {
logger.e("|$_tag|初始化登录信息错误error:|$error|");
}
}
}
/// 在联网请求租户列表前,用本地缓存把旧版本误存的租户 `code` 迁移回数值 `id`。
Future<void> _restoreCachedTenantSelection() async {
final savedTenantId = userSetting.tenantId;
if (ObjectUtil.isEmpty(savedTenantId)) return;
final cachedTenants = sysSetting.tenantList ?? const <ServiceTenant>[];
final selectedTenant = cachedTenants.firstWhereOrNull(
(tenant) =>
ObjectUtil.isNotEmpty(tenant.id) &&
ObjectUtil.isNotEmpty(tenant.code) &&
(tenant.id == savedTenantId || tenant.code == savedTenantId),
);
if (selectedTenant == null) return;
currentTenant = selectedTenant;
userSetting.tenantId = selectedTenant.id;
_applyingInitialTenantSelection = true;
try {
tenantC.value = {selectedTenant.code!};
} finally {
_applyingInitialTenantSelection = false;
}
await AppSettingTool.setUserSetting(userSetting);
}
/// 首帧完成后刷新登录页局部 GetBuilder。
@override
void onReady() {
super.onReady();
_initData();
}
/// 登录页面的 Widget 子树完全卸载后释放表单与租户选择监听。
///
/// 调用时机由页面内的局部 GetBuilder 管理,不能由路由 Binding 提前触发,否则退场动画
/// 中尚未卸载的 FTextField 仍可能处理触摸选区并访问已经释放的输入控制器。
@override
void onClose() {
tenantC.removeValueListener(_handleTenantSelection);
tenantC.dispose();
usernameC.dispose();
passwordC.dispose();
super.onClose();
}
/// 校验表单并执行登录;自动登录会在用户开始人工输入后立即让出控制权。
Future<void> doLogin(bool visibleMsg, {bool automatic = false}) async {
if (automatic && _manualInputStarted) return;
if (!_validateForm()) return;
final username = usernameC.text;
final password = passwordC.text;
final latestSysSetting = await AppSettingTool.getSysSetting();
if (isClosed) return;
// 自动登录等待本地配置期间,用户可能已经点入输入框;此时人工输入优先,不能再用 Loading 抢走焦点。
if (automatic && _manualInputStarted) return;
final pa = <String, dynamic>{
'username': username,
'password': password,
'sysType': latestSysSetting.sysType.name,
"grant_type": 'password',
"scope": 'server',
};
if (visibleMsg) {
EasyLoading.show(
status: '登录中...',
maskType: EasyLoadingMaskType.black,
);
}
try {
await AppSettingTool.setUserSetting(UserSetting());
if (isClosed) return;
final data = await UserApi.login(pa);
if (isClosed) return;
if (data == null) {
return;
}
if (data is! Map) {
throw const FormatException('登录响应不是 JSON 对象');
}
userSetting.userId = data['user_id'];
userSetting.accessToken = data['access_token'];
userSetting.refreshToken = data['refresh_token'];
userSetting.expiresTime = int.tryParse("${data['exp']}");
userSetting.sysType = latestSysSetting.sysType;
userSetting.username = username;
userSetting.password = password;
// TENANT-ID 会被后端解析为 Long,必须持久化租户 id,不能写入界面选择使用的 code。
userSetting.tenantId = currentTenant.id;
await AppSettingTool.setUserSetting(userSetting);
if (isClosed) return;
await TtsManage.speak("登录成功");
if (isClosed) return;
if (visibleMsg) {
Toast.success(msg: "登录成功");
}
// 启动页已经完成数据库与设备初始化,登录后进入食堂或超市业务工作区。
_clearForm();
await _openWorkspace();
} catch (error) {
logger.e("|$_tag|登录错误error:|$error|");
} finally {
if (visibleMsg) EasyLoading.dismiss();
}
}
/// 使用已保存账号执行自动登录;离线路径仅在终端已绑定服务时直接进入业务页。
Future<void> autoLogin() async {
if (_manualInputStarted ||
ObjectUtil.isEmpty(userSetting.username) ||
ObjectUtil.isEmpty(userSetting.password) ||
ObjectUtil.isEmpty(userSetting.sysType?.name)) {
return;
}
final latestSysSetting = await AppSettingTool.getSysSetting();
if (isClosed || _manualInputStarted) return;
usernameC.text = userSetting.username!;
passwordC.text = userSetting.password!;
// 无网络并且绑定过直接使用
final isNetwork = AppNetworkTool.isAutoNetwork();
final serviceId = latestSysSetting.serviceId;
if (!isNetwork && serviceId != null) {
Toast.success(msg: "登录成功");
EasyLoading.dismiss();
_clearForm();
await _openWorkspace();
return;
}
if (!isNetwork && serviceId == null) {
Toast.error(msg: "未绑定机器,不能开启离线模式");
return;
}
await doLogin(true, automatic: true);
}
/// 未绑定实例先进入设备 ID 流程;已绑定实例统一进入业务终端工作区。
Future<void> _openWorkspace() async {
final setting = await AppSettingTool.getSysSetting();
if (ObjectUtil.isEmpty(setting.serviceId)) {
Get.offAllNamed(RouteNames.deviceSetup);
return;
}
await Get.find<AppControl>().resumeAuthenticatedServices();
// 正常重启不会经过设备绑定页,这里主动建连 MQTT,避免打印等推送
// 依赖每分钟一次的重连定时任务,冷启动后几分钟才恢复消息。
unawaited(AppMqTool.mqRefresh());
Get.offAllNamed(RouteNames.terminal);
}
/// 用户主动编辑账号或密码后,本轮初始化只保留人工登录,避免延迟的自动登录弹层关闭软键盘。
void markManualInputStarted() {
_manualInputStarted = true;
}
/// 校验租户、用户名和密码是否完整,并在失败时给出当前缺失项提示。
bool _validateForm() {
if (ObjectUtil.isEmpty(currentTenant.id)) {
Toast.error(msg: "请选择单位");
return false;
}
final username = usernameC.text;
final password = passwordC.text;
if (ObjectUtil.isEmpty(username)) {
Toast.error(msg: "请输入账号");
return false;
}
if (ObjectUtil.isEmpty(password)) {
Toast.error(msg: "请输入密码");
return false;
}
return true;
}
/// 获取在线或离线租户列表,并把历史 `id/code` 统一迁移为“控件选 code、请求存 id”。
Future<void> getTenantList() async {
try {
final isNetwork = AppNetworkTool.isAutoNetwork();
List<ServiceTenant>? list = [];
if (isNetwork) {
list = await UserApi.getTenantList();
} else {
sysSetting = await AppSettingTool.getSysSetting();
list = sysSetting.tenantList;
}
if (isClosed || list == null) return;
tenantList.assignAll(list);
final savedTenantId = userSetting.tenantId;
final selectableTenants = list
.where(
(tenant) =>
ObjectUtil.isNotEmpty(tenant.id) &&
ObjectUtil.isNotEmpty(tenant.code),
)
.toList(growable: false);
final selectedTenant = selectableTenants.firstWhereOrNull(
(tenant) =>
tenant.id == savedTenantId || tenant.code == savedTenantId,
) ??
selectableTenants.firstOrNull;
if (selectedTenant != null) {
currentTenant = selectedTenant;
userSetting.tenantId = selectedTenant.id;
_applyingInitialTenantSelection = true;
try {
tenantC.value = {selectedTenant.code!};
} finally {
_applyingInitialTenantSelection = false;
}
await AppSettingTool.setUserSetting(userSetting);
if (isClosed) return;
}
// 存储租户列表
sysSetting.tenantList = list;
await AppSettingTool.setSysSetting(sysSetting);
} catch (error) {
if (!isClosed) {
logger.e("|$_tag|获取租户列表错误error:|$error|");
}
}
}
/// 响应用户租户选择;监听同步触发,持久化工作通过独立异步方法完成。
void _handleTenantSelection(Set<String> values) {
if (isClosed || _applyingInitialTenantSelection) return;
final selectedCode = values.firstOrNull;
if (selectedCode == null) return;
final target = tenantList.firstWhereOrNull(
(tenant) => tenant.code == selectedCode,
);
if (target == null || ObjectUtil.isEmpty(target.id)) return;
currentTenant = target;
userSetting.tenantId = target.id;
unawaited(_persistTenantSelection());
}
/// 持久化用户主动选择的租户,并确保异常路径同样关闭全局 Loading。
Future<void> _persistTenantSelection() async {
if (isClosed) return;
EasyLoading.show(
status: '切换中...',
maskType: EasyLoadingMaskType.black,
);
try {
await AppSettingTool.setUserSetting(userSetting);
} catch (error) {
if (!isClosed) {
logger.e("|$_tag|保存租户选择错误error:|$error|");
Toast.error(msg: "切换单位失败,请重试");
}
} finally {
EasyLoading.dismiss();
}
}
/// 在路由移除前清空表单文本;调用方必须保证输入控制器尚未释放。
void _clearForm() {
usernameC.clear();
passwordC.clear();
}
}