Files
AI-Health/health_app/lib/providers/auth_provider.dart
sccsbc fe4c81c54f feat: 集成 Apple Sign-In + iOS 上线准备
- 后端新增 Apple 登录端点 /api/auth/apple-login
- 新增 AppleTokenValidator 验证 IdentityToken
- User 实体添加 AppleUserId 字段,Phone 改为可空
- 前端添加 sign_in_with_apple 依赖和 Apple 登录按钮
- 统一 Bundle ID 为 com.datalumina.YYA,显示名称为小脉健康
- 配置 DEVELOPMENT_TEAM 和 Sign in with Apple Entitlements
- 补充 NSCamera/NSPhotoLibrary 权限描述
- 生产 API 地址改为 https://erpapi.datalumina.cn/xiaomai
- Flutter 升级至 3.44.6 / Dart 3.12.2

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-10 13:24:03 +08:00

265 lines
7.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
UserInfo({
required this.id,
required this.phone,
this.role = 'User',
this.name,
this.avatarUrl,
});
}
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, AuthState>(
AuthNotifier.new,
);
final localDbProvider = Provider<LocalDatabase>(
(ref) => LocalDatabase.instance,
);
final authExpiredNotifierProvider = Provider<AuthExpiredNotifier>((ref) {
return AuthExpiredNotifier();
});
final apiClientProvider = Provider<ApiClient>((ref) {
return ApiClient(
db: ref.watch(localDbProvider),
authExpiredNotifier: ref.watch(authExpiredNotifierProvider),
);
});
class AuthNotifier extends Notifier<AuthState> {
@override
AuthState build() {
final removeListener = ref.read(authExpiredNotifierProvider).addListener(
() {
state = const AuthState(isLoggedIn: false, isLoading: false);
},
);
ref.onDispose(removeListener);
_checkAuth();
// 初始为"加载中",让启动闸门显示 Splash 盖住登录页/首页初始态
return const AuthState(isLoggedIn: false, isLoading: true);
}
Future<void> _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);
return;
}
state = const AuthState(isLoading: true);
try {
final response = await Dio(
BaseOptions(baseUrl: baseUrl),
).post('/api/auth/refresh', data: {'refreshToken': refresh});
final data = response.data['data'];
if (data != null) {
await db.write('access_token', data['accessToken']);
await db.write('refresh_token', data['refreshToken']);
final u = data['user'] as Map<String, dynamic>?;
state = AuthState(
isLoggedIn: true,
isLoading: false,
user: UserInfo(id: '', phone: '', role: u?['role'] ?? 'User'),
);
_loadProfile();
} else {
state = const AuthState(isLoggedIn: false, isLoading: false);
}
} catch (_) {
state = const AuthState(isLoggedIn: false, isLoading: false);
}
}
Future<void> _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'],
),
);
}
} catch (e) {
log('[Auth] loadProfile: $e');
}
}
Future<void> refreshProfile() => _loadProfile();
/// 发送验证码
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<String?> 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',
),
);
return null;
} catch (e) {
return '注册失败: $e';
}
}
/// 登录(已有账号)
Future<String?> 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'],
),
);
return null;
} catch (e) {
return '登录失败: $e';
}
}
/// Apple 登录
Future<String?> 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,
if (authorizationCode != null) 'authorizationCode': authorizationCode,
if (name != null && name.isNotEmpty) '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'],
),
);
return null;
} catch (e) {
return 'Apple 登录失败: $e';
}
}
/// 登出
Future<void> 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();
state = const AuthState(isLoggedIn: false, isLoading: false);
}
}
/// 便捷:当前用户角色
final userRoleProvider = Provider<String>((ref) {
return ref.watch(authProvider).user?.role ?? 'User';
});