98 lines
3.5 KiB
Dart
98 lines
3.5 KiB
Dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../core/network/api_client.dart';
|
|
import '../core/network/api_exception.dart';
|
|
import '../core/storage/token_store.dart';
|
|
import '../models/user_model.dart';
|
|
import '../services/auth_service.dart';
|
|
import '../services/user_service.dart';
|
|
|
|
/// 登录态:未登录 / 已登录(含用户信息)
|
|
/// Token 本体在 TokenStore(安全存储 + 内存缓存),此处只持有用户模型与状态。
|
|
class AuthState {
|
|
const AuthState({this.user, this.restored = false});
|
|
|
|
final UserModel? user;
|
|
final bool restored; // 是否已完成启动恢复(避免启动期误跳登录)
|
|
|
|
bool get isLoggedIn => user != null && TokenStore.hasToken;
|
|
|
|
AuthState copyWith({UserModel? user, bool? restored}) => AuthState(
|
|
user: user ?? this.user,
|
|
restored: restored ?? this.restored,
|
|
);
|
|
}
|
|
|
|
class AuthNotifier extends StateNotifier<AuthState> {
|
|
AuthNotifier() : super(const AuthState(restored: false)) {
|
|
// 会话过期(401 刷新失败)→ 清空登录态
|
|
ApiClient.onSessionExpired = _onSessionExpired;
|
|
}
|
|
|
|
/// 启动恢复:读取安全存储中的 Token,有效则拉取用户信息
|
|
Future<void> restore() async {
|
|
await TokenStore.restore();
|
|
if (!TokenStore.hasToken) {
|
|
state = state.copyWith(restored: true);
|
|
return;
|
|
}
|
|
// 数据完整性防线:有 access 无 refresh 是不完整登录态(2h 后必然 401
|
|
// 且无法静默刷新),直接清理回游客态引导重新登录,避免僵尸登录态
|
|
if (TokenStore.refreshToken == null || TokenStore.refreshToken!.isEmpty) {
|
|
await TokenStore.clear();
|
|
state = state.copyWith(restored: true);
|
|
return;
|
|
}
|
|
try {
|
|
final user = await UserService.fetchMe();
|
|
state = AuthState(user: user, restored: true);
|
|
} on ApiException catch (e) {
|
|
// 仅 Token 确认失效(1002)才清理回游客态;
|
|
// 网络异常等情况保留 Token,下次启动/请求重试恢复
|
|
if (e.isUnauthorized) await TokenStore.clear();
|
|
state = AuthState(restored: true);
|
|
} catch (_) {
|
|
// 未知异常:保守保留 Token
|
|
state = AuthState(restored: true);
|
|
}
|
|
}
|
|
|
|
/// 登录/注册成功:Token 已由 AuthService 写入 TokenStore,此处刷新用户。
|
|
/// 登录响应里的 user 是简要信息(无 points/bio/gender),
|
|
/// 需补拉全量资料,否则积分显示为 0、编辑资料保存会把服务端字段清空。
|
|
Future<void> onLoginSuccess(UserModel user) async {
|
|
state = AuthState(user: user, restored: true);
|
|
try {
|
|
final full = await UserService.fetchMe();
|
|
state = state.copyWith(user: full);
|
|
} catch (_) {
|
|
// 补拉失败不影响登录态,保留简要信息
|
|
}
|
|
}
|
|
|
|
/// 修改资料/头像后同步当前用户(不改变登录态与 Token)
|
|
void updateUser(UserModel user) => state = state.copyWith(user: user);
|
|
|
|
/// 退出登录:先通知服务端注销(失败忽略,本地照常清理),再清空登录态
|
|
Future<void> logout() async {
|
|
try {
|
|
await AuthService.logout();
|
|
} catch (_) {
|
|
// 服务端已不可达或 Token 已失效:本地清理即可
|
|
}
|
|
await _clearLocal();
|
|
}
|
|
|
|
Future<void> _onSessionExpired() async => _clearLocal();
|
|
|
|
Future<void> _clearLocal() async {
|
|
await TokenStore.clear();
|
|
state = AuthState(restored: true);
|
|
}
|
|
}
|
|
|
|
/// 全局登录态 Provider
|
|
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
|
|
return AuthNotifier();
|
|
});
|