import 'dart:convert'; import 'dart:developer'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:dio/dio.dart'; import '../core/api_client.dart'; import '../core/local_database.dart'; class UserInfo { final String id; final String phone; final String role; final String? name; final String? avatarUrl; final String? gender; final String? birthDate; UserInfo({ required this.id, required this.phone, this.role = 'User', this.name, this.avatarUrl, this.gender, this.birthDate, }); } class AuthState { final UserInfo? user; final bool isLoggedIn; final bool isLoading; const AuthState({this.user, this.isLoggedIn = false, this.isLoading = true}); } final authProvider = NotifierProvider( AuthNotifier.new, ); final localDbProvider = Provider( (ref) => LocalDatabase.instance, ); final authExpiredNotifierProvider = Provider((ref) { return AuthExpiredNotifier(); }); final apiClientProvider = Provider((ref) { return ApiClient( db: ref.watch(localDbProvider), authExpiredNotifier: ref.watch(authExpiredNotifierProvider), ); }); class AuthNotifier extends Notifier { static const _cachedUserKey = 'session_user'; @override AuthState build() { final removeListener = ref.read(authExpiredNotifierProvider).addListener( () { ref.read(localDbProvider).delete(_cachedUserKey); state = const AuthState(isLoggedIn: false, isLoading: false); _publishSessionIdentity(null); }, ); ref.onDispose(removeListener); _checkAuth(); // 初始为"加载中",让启动闸门显示 Splash 盖住登录页/首页初始态 return const AuthState(isLoggedIn: false, isLoading: true); } Future _checkAuth() async { final db = ref.read(localDbProvider); final refresh = await db.read('refresh_token'); if (refresh == null) { // 无 token:判定完成、未登录 state = const AuthState(isLoggedIn: false, isLoading: false); _publishSessionIdentity(null); return; } state = const AuthState(isLoading: true); try { final response = await Dio( BaseOptions( baseUrl: baseUrl, connectTimeout: const Duration(seconds: 5), receiveTimeout: const Duration(seconds: 8), ), ).post('/api/auth/refresh', data: {'refreshToken': refresh}); final body = response.data; final data = body is Map ? body['data'] : null; final code = body is Map ? body['code'] : null; if (data is Map) { await db.write('access_token', data['accessToken']); await db.write('refresh_token', data['refreshToken']); final u = data['user'] as Map?; final cachedUser = await _readCachedUser(); final user = _userFromMap(u, fallback: cachedUser); state = AuthState(isLoggedIn: true, isLoading: false, user: user); _publishSessionIdentity(user); await _cacheUser(user); _loadProfile(); } else if (code != null && code != 0) { await _clearSessionStorage(); state = const AuthState(isLoggedIn: false, isLoading: false); _publishSessionIdentity(null); } else { await _restoreOfflineSession(); } } on DioException catch (error) { if (error.response?.statusCode == 400 || error.response?.statusCode == 401) { await _clearSessionStorage(); state = const AuthState(isLoggedIn: false, isLoading: false); _publishSessionIdentity(null); } else { await _restoreOfflineSession(); } } catch (error) { log('[Auth] startup refresh: $error'); await _restoreOfflineSession(); } } Future _loadProfile() async { // Admin 不查 profile(无 User 记录) if (state.user?.role == 'Admin') return; try { final api = ref.read(apiClientProvider); final response = await api.get('/api/user/profile'); final user = response.data['data']; if (user != null) { state = AuthState( isLoggedIn: true, isLoading: false, user: UserInfo( id: user['id'] ?? '', phone: user['phone'] ?? '', role: user['role'] ?? state.user?.role ?? 'User', name: user['name'], avatarUrl: user['avatarUrl'], gender: user['gender'], birthDate: user['birthDate']?.toString(), ), ); await _cacheUser(state.user!); _publishSessionIdentity(state.user); } } catch (e) { log('[Auth] loadProfile: $e'); } } Future refreshProfile() => _loadProfile(); Future applyProfile(Map profile) async { final user = _userFromMap(profile, fallback: state.user); state = AuthState(isLoggedIn: true, isLoading: false, user: user); await _cacheUser(user); _publishSessionIdentity(user); } /// 发送验证码 Future<({String? error, String? devCode})> sendSms(String phone) async { try { final api = ref.read(apiClientProvider); final response = await api.post( '/api/auth/send-sms', data: {'phone': phone}, ); return ( error: null, devCode: response.data['data']?['devCode'] as String?, ); } catch (e) { return (error: '发送失败: $e', devCode: null); } } /// 注册(新用户,需选身份) Future register( String phone, String code, String name, String doctorId, ) async { try { final api = ref.read(apiClientProvider); final response = await api.post( '/api/auth/register', data: { 'phone': phone, 'smsCode': code, 'name': name, 'doctorId': doctorId, }, ); final data = response.data['data']; if (data == null) return response.data['message'] ?? '注册失败'; await api.saveTokens(data['accessToken'], data['refreshToken']); final user = data['user']; state = AuthState( isLoggedIn: true, isLoading: false, user: UserInfo( id: user['id'] ?? '', phone: user['phone'] ?? '', role: user['role'] ?? 'User', ), ); await _cacheUser(state.user!); _publishSessionIdentity(state.user); return null; } catch (e) { return '注册失败: $e'; } } /// 登录(已有账号) Future login(String phone, String code) async { try { final api = ref.read(apiClientProvider); final response = await api.post( '/api/auth/login', data: {'phone': phone, 'smsCode': code}, ); final data = response.data['data']; if (data == null) return response.data['message'] ?? '登录失败'; await api.saveTokens(data['accessToken'], data['refreshToken']); final user = data['user']; state = AuthState( isLoggedIn: true, isLoading: false, user: UserInfo( id: user['id'] ?? '', phone: user['phone'] ?? '', role: user['role'] ?? 'User', name: user['name'], avatarUrl: user['avatarUrl'], ), ); await _cacheUser(state.user!); _publishSessionIdentity(state.user); return null; } catch (e) { return '登录失败: $e'; } } /// Apple 登录 Future appleLogin( String identityToken, String? authorizationCode, String? name, ) async { try { final api = ref.read(apiClientProvider); final response = await api.post( '/api/auth/apple-login', data: { 'identityToken': identityToken, 'authorizationCode': authorizationCode, 'name': name, }, ); final data = response.data['data']; if (data == null) return response.data['message'] ?? 'Apple 登录失败'; await api.saveTokens(data['accessToken'], data['refreshToken']); final user = data['user']; state = AuthState( isLoggedIn: true, isLoading: false, user: UserInfo( id: user['id'] ?? '', phone: user['phone'] ?? '', role: user['role'] ?? 'User', name: user['name'], avatarUrl: user['avatarUrl'], ), ); await _cacheUser(state.user!); _publishSessionIdentity(state.user); return null; } catch (_) { return 'Apple 登录失败,请稍后重试'; } } /// 登出 Future logout() async { final api = ref.read(apiClientProvider); final db = ref.read(localDbProvider); final refresh = await db.read('refresh_token'); if (refresh != null) { try { await api.post('/api/auth/logout', data: {'refreshToken': refresh}); } catch (e) { log('[Auth] logout: $e'); } } await api.clearTokens(); await db.delete(_cachedUserKey); state = const AuthState(isLoggedIn: false, isLoading: false); _publishSessionIdentity(null); } Future _restoreOfflineSession() async { final cached = await _readCachedUser() ?? await _userFromStoredToken(); if (cached == null) { state = const AuthState(isLoggedIn: false, isLoading: false); _publishSessionIdentity(null); return; } state = AuthState(user: cached, isLoggedIn: true, isLoading: false); _publishSessionIdentity(cached); } UserInfo _userFromMap(Map? value, {UserInfo? fallback}) { return UserInfo( id: value?['id']?.toString() ?? fallback?.id ?? '', phone: value?['phone']?.toString() ?? fallback?.phone ?? '', role: value?['role']?.toString() ?? fallback?.role ?? 'User', name: value?['name']?.toString() ?? fallback?.name, avatarUrl: value?['avatarUrl']?.toString() ?? fallback?.avatarUrl, gender: value?['gender']?.toString() ?? fallback?.gender, birthDate: value?['birthDate']?.toString() ?? fallback?.birthDate, ); } Future _cacheUser(UserInfo user) { return ref .read(localDbProvider) .write( _cachedUserKey, jsonEncode({ 'id': user.id, 'phone': user.phone, 'role': user.role, 'name': user.name, 'avatarUrl': user.avatarUrl, 'gender': user.gender, 'birthDate': user.birthDate, }), ); } Future _readCachedUser() async { final raw = await ref.read(localDbProvider).read(_cachedUserKey); if (raw == null || raw.isEmpty) return null; try { final decoded = jsonDecode(raw); if (decoded is! Map) return null; return _userFromMap(Map.from(decoded)); } catch (_) { return null; } } Future _userFromStoredToken() async { final token = await ref.read(localDbProvider).read('access_token'); if (token == null) return null; try { final parts = token.split('.'); if (parts.length != 3) return null; final payload = jsonDecode( utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))), ); if (payload is! Map) return null; final map = Map.from(payload); String? claim(String shortName, String longName) => map[shortName]?.toString() ?? map[longName]?.toString(); return UserInfo( id: claim( 'sub', 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier', ) ?? '', phone: claim( 'phone', 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone', ) ?? '', role: claim( 'role', 'http://schemas.microsoft.com/ws/2008/06/identity/claims/role', ) ?? 'User', ); } catch (_) { return null; } } Future _clearSessionStorage() async { await ref.read(apiClientProvider).clearTokens(); await ref.read(localDbProvider).delete(_cachedUserKey); } void _publishSessionIdentity(UserInfo? user) { ref.read(userSessionIdentityProvider.notifier).setUser(user); } } /// 便捷:当前用户角色 final userRoleProvider = Provider((ref) { return ref.watch(authProvider).user?.role ?? 'User'; }); final userSessionIdentityProvider = NotifierProvider( UserSessionIdentityNotifier.new, ); class UserSessionIdentityNotifier extends Notifier { @override String? build() => null; void setUser(UserInfo? user) { if (user == null || user.id.isEmpty) { state = null; return; } state = user.id; } }