347 lines
12 KiB
Dart
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();
|
|
}
|
|
}
|