159 lines
4.7 KiB
Dart
159 lines
4.7 KiB
Dart
|
|
import 'dart:async';
|
|||
|
|
import 'dart:convert';
|
|||
|
|
|
|||
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
|
|
|
|||
|
|
import '../models/user.dart';
|
|||
|
|
import '../services/api_client.dart';
|
|||
|
|
import '../services/auth_service.dart';
|
|||
|
|
import '../utils/credential_storage.dart';
|
|||
|
|
import '../utils/session_org_scope.dart';
|
|||
|
|
|
|||
|
|
/// AuthService 提供者。
|
|||
|
|
final authServiceProvider = Provider<AuthService>((ref) => AuthService());
|
|||
|
|
|
|||
|
|
/// 登录状态。
|
|||
|
|
class AuthState {
|
|||
|
|
const AuthState({
|
|||
|
|
this.user,
|
|||
|
|
this.isLoading = false,
|
|||
|
|
this.errorMessage,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/// 当前登录用户;为 null 表示未登录。
|
|||
|
|
final User? user;
|
|||
|
|
|
|||
|
|
/// 是否正在执行登录请求。
|
|||
|
|
final bool isLoading;
|
|||
|
|
|
|||
|
|
/// 最近一次登录失败的错误信息。
|
|||
|
|
final String? errorMessage;
|
|||
|
|
|
|||
|
|
/// 是否已登录。
|
|||
|
|
bool get isLoggedIn => user != null;
|
|||
|
|
|
|||
|
|
AuthState copyWith({
|
|||
|
|
User? user,
|
|||
|
|
bool? isLoading,
|
|||
|
|
String? errorMessage,
|
|||
|
|
bool clearUser = false,
|
|||
|
|
bool clearError = false,
|
|||
|
|
}) {
|
|||
|
|
return AuthState(
|
|||
|
|
user: clearUser ? null : (user ?? this.user),
|
|||
|
|
isLoading: isLoading ?? this.isLoading,
|
|||
|
|
errorMessage: clearError ? null : (errorMessage ?? this.errorMessage),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 登录状态管理。
|
|||
|
|
class AuthNotifier extends Notifier<AuthState> {
|
|||
|
|
@override
|
|||
|
|
AuthState build() {
|
|||
|
|
// 注册会话失效回调:401 刷新失败 → 清空登录态(含本地持久化),
|
|||
|
|
// 路由 redirect 监听本状态,自动跳转登录页。
|
|||
|
|
ApiClient.instance.onUnauthorized = () {
|
|||
|
|
SessionOrgScope.instance.clear();
|
|||
|
|
unawaited(_clearPersistedSession());
|
|||
|
|
state = const AuthState();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 尝试从本地存储恢复登录态(免登录)。
|
|||
|
|
return _tryRestoreSession();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 从本地存储恢复登录态。
|
|||
|
|
AuthState _tryRestoreSession() {
|
|||
|
|
final cs = CredentialStorage.instance;
|
|||
|
|
final token = cs.token;
|
|||
|
|
final userMap = cs.savedUserMap;
|
|||
|
|
|
|||
|
|
if (token != null && token.isNotEmpty && userMap != null) {
|
|||
|
|
// 恢复 TokenStore(供 ApiInterceptor 使用)。
|
|||
|
|
TokenStore.instance.save(
|
|||
|
|
token: token,
|
|||
|
|
refreshToken: cs.refreshToken,
|
|||
|
|
);
|
|||
|
|
try {
|
|||
|
|
final user = User.fromJson(userMap);
|
|||
|
|
// 恢复数据权限范围:仅当登录用户 orgId 非空时收敛。
|
|||
|
|
SessionOrgScope.instance.setCurrentOrg(user.orgId);
|
|||
|
|
return AuthState(user: user);
|
|||
|
|
} catch (_) {
|
|||
|
|
// JSON 解析失败,清除残留数据。
|
|||
|
|
_clearPersistedSession();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return const AuthState();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 执行登录;成功返回 true,失败返回 false 并写入错误信息。
|
|||
|
|
///
|
|||
|
|
/// [captcha]/[uuid] 仅在后端要求验证码时传值;当前各环境已关闭登录验证码,
|
|||
|
|
/// 调用方(登录页)可不传,由 [AuthService.login] 决定是否写入请求体。
|
|||
|
|
Future<bool> login({
|
|||
|
|
required String account,
|
|||
|
|
required String password,
|
|||
|
|
String captcha = '',
|
|||
|
|
String uuid = '',
|
|||
|
|
bool rememberPassword = false,
|
|||
|
|
}) async {
|
|||
|
|
state = state.copyWith(isLoading: true, clearError: true);
|
|||
|
|
try {
|
|||
|
|
final user = await ref.read(authServiceProvider).login(
|
|||
|
|
account: account,
|
|||
|
|
password: password,
|
|||
|
|
captcha: captcha,
|
|||
|
|
uuid: uuid,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 持久化登录态(token + 用户信息),用于免登录恢复。
|
|||
|
|
await CredentialStorage.instance.saveSession(
|
|||
|
|
token: user.token,
|
|||
|
|
refreshToken: TokenStore.instance.refreshToken,
|
|||
|
|
userJson: jsonEncode(user.toJson()),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 记住密码。
|
|||
|
|
if (rememberPassword) {
|
|||
|
|
await CredentialStorage.instance.saveCredentials(
|
|||
|
|
account: account,
|
|||
|
|
password: password,
|
|||
|
|
);
|
|||
|
|
} else {
|
|||
|
|
await CredentialStorage.instance.clearCredentials();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
state = AuthState(user: user, isLoading: false);
|
|||
|
|
// 登录成功后写入数据权限范围,供 OrgService 客户端收敛组织/门店数据。
|
|||
|
|
SessionOrgScope.instance.setCurrentOrg(user.orgId);
|
|||
|
|
return true;
|
|||
|
|
} on AuthException catch (e) {
|
|||
|
|
state = state.copyWith(isLoading: false, errorMessage: e.message);
|
|||
|
|
return false;
|
|||
|
|
} catch (_) {
|
|||
|
|
state = state.copyWith(isLoading: false, errorMessage: '登录失败,请稍后重试');
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 登出并清空登录态。
|
|||
|
|
Future<void> logout() async {
|
|||
|
|
await ref.read(authServiceProvider).logout();
|
|||
|
|
SessionOrgScope.instance.clear();
|
|||
|
|
await _clearPersistedSession();
|
|||
|
|
state = const AuthState();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 清除本地持久化的登录态。
|
|||
|
|
Future<void> _clearPersistedSession() async {
|
|||
|
|
await CredentialStorage.instance.clearSession();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 登录状态提供者。
|
|||
|
|
final authProvider = NotifierProvider<AuthNotifier, AuthState>(
|
|||
|
|
AuthNotifier.new,
|
|||
|
|
);
|