Files
xianyan/lib/features/auth/services/oauth_service.dart
Developer 214a0684d0 chore: 移除NFC/蓝牙相关支持,更新设备在线统计,新增功能优化
1.  移除NFC和蓝牙相关依赖、权限及功能代码,精简传输链路
2.  重构设备在线统计逻辑,使用后端7天活跃字段替代本地计算
3.  更新应用名称、权限说明和协议文档
4.  新增消息转发、缓存管理、医疗免责提示功能
5.  优化运势模块和字体管理文案,修复构建日志问题
2026-06-06 06:12:09 +08:00

378 lines
12 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.
/// ============================================================
/// 文件: oauth_service.dart
/// 创建时间: 2026-06-05
/// 更新时间: 2026-06-05
/// 名称: OAuth社交登录服务
/// 作用: 处理Apple/Google/GitHub第三方登录
/// 上次更新: 新增OAuth社交登录服务
/// ============================================================
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_web_auth/flutter_web_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import '../../../core/network/api_client.dart';
import '../../../core/utils/logger.dart';
import '../../../core/utils/platform/platform_utils.dart' as pu;
/// OAuth平台枚举
enum OAuthPlatform {
apple,
google,
github;
/// 平台标识值
String get value => switch (this) {
OAuthPlatform.apple => 'apple',
OAuthPlatform.google => 'google',
OAuthPlatform.github => 'github',
};
/// 显示名称
String get displayName => switch (this) {
OAuthPlatform.apple => 'Apple',
OAuthPlatform.google => 'Google',
OAuthPlatform.github => 'GitHub',
};
/// 图标emoji
String get icon => switch (this) {
OAuthPlatform.apple => '',
OAuthPlatform.google => '',
OAuthPlatform.github => '',
};
}
/// OAuth登录结果
class OAuthLoginResult {
const OAuthLoginResult({
required this.success,
this.token,
this.userinfo,
this.isNewUser = false,
this.bindPlatform,
this.error,
});
final bool success;
final String? token;
final Map<String, dynamic>? userinfo;
final bool isNewUser;
final String? bindPlatform;
final String? error;
/// 从服务端响应JSON构造
factory OAuthLoginResult.fromJson(Map<String, dynamic> json) {
final data = json['data'];
final Map<String, dynamic>? dataMap =
data is Map<String, dynamic> ? data : null;
return OAuthLoginResult(
success: json['code'] == 1,
token: dataMap?['token'] as String?,
userinfo: dataMap?['userinfo'] != null
? Map<String, dynamic>.from(dataMap!['userinfo'] as Map)
: null,
isNewUser: dataMap?['is_new_user'] as bool? ?? false,
bindPlatform: dataMap?['bind_platform'] as String?,
error: json['code'] != 1 ? json['msg'] as String? : null,
);
}
}
/// OAuth社交登录服务
class OAuthService {
OAuthService._();
static const String _tag = 'OAuthService';
/// ApiClient单例的Dio实例
static final Dio _dio = ApiClient.instance.dio;
// ============================================================
// Apple登录
// ============================================================
/// Apple登录
///
/// 仅支持iOS和macOS平台通过Sign in with Apple获取id_token后发送到服务端验证。
static Future<OAuthLoginResult> loginWithApple({
String? deviceName,
String? deviceModel,
String? platformType,
String? deviceId,
}) async {
try {
if (!pu.isIOS && !pu.isMacOS) {
return const OAuthLoginResult(
success: false,
error: 'Apple登录仅支持iOS和macOS',
);
}
final credential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
webAuthenticationOptions: pu.isIOS
? null
: WebAuthenticationOptions(
clientId: 'apps.xy.xianyan.service',
redirectUri:
Uri.parse('https://tools.wktyl.com/oauth/callback'),
),
);
Log.i('$_tag: Apple登录获取id_token成功');
return _loginToServer(
platform: OAuthPlatform.apple,
idToken: credential.identityToken,
authorizationCode: credential.authorizationCode,
deviceName: deviceName,
deviceModel: deviceModel,
platformType: platformType,
deviceId: deviceId,
);
} on SignInWithAppleAuthorizationException catch (e) {
if (e.code == AuthorizationErrorCode.canceled) {
return const OAuthLoginResult(success: false, error: '用户取消');
}
Log.e('$_tag: Apple登录失败: $e');
return OAuthLoginResult(
success: false,
error: 'Apple登录失败: ${e.message}',
);
} on PlatformException catch (e) {
Log.e('$_tag: Apple登录平台异常: $e');
return const OAuthLoginResult(success: false, error: 'Apple登录失败');
} catch (e) {
Log.e('$_tag: Apple登录异常: $e');
return const OAuthLoginResult(success: false, error: 'Apple登录失败');
}
}
// ============================================================
// Google登录
// ============================================================
/// Google登录
///
/// 通过Google Sign In获取serverAuthCode后发送到服务端验证。
static Future<OAuthLoginResult> loginWithGoogle({
String? deviceName,
String? deviceModel,
String? platformType,
String? deviceId,
}) async {
try {
final googleSignIn = GoogleSignIn(scopes: ['email', 'profile']);
final account = await googleSignIn.signIn();
if (account == null) {
return const OAuthLoginResult(success: false, error: '用户取消');
}
final auth = await account.authentication;
final serverAuthCode = auth.serverAuthCode;
if (serverAuthCode == null) {
Log.w('$_tag: Google serverAuthCode为空尝试使用idToken');
return const OAuthLoginResult(
success: false,
error: 'Google授权码获取失败请重试',
);
}
Log.i('$_tag: Google登录获取serverAuthCode成功');
return _loginToServer(
platform: OAuthPlatform.google,
code: serverAuthCode,
deviceName: deviceName,
deviceModel: deviceModel,
platformType: platformType,
deviceId: deviceId,
);
} on PlatformException catch (e) {
if (e.code == 'sign_in_canceled') {
return const OAuthLoginResult(success: false, error: '用户取消');
}
Log.e('$_tag: Google登录平台异常: $e');
return const OAuthLoginResult(success: false, error: 'Google登录失败');
} catch (e) {
Log.e('$_tag: Google登录异常: $e');
return const OAuthLoginResult(success: false, error: 'Google登录失败');
}
}
// ============================================================
// GitHub登录
// ============================================================
/// GitHub登录通过浏览器OAuth
///
/// 先从服务端获取OAuth配置再通过浏览器完成授权流程最后将授权码发送到服务端。
static Future<OAuthLoginResult> loginWithGithub({
String? deviceName,
String? deviceModel,
String? platformType,
String? deviceId,
}) async {
try {
// 从服务端获取GitHub OAuth配置
final configResp = await _dio.get<Map<String, dynamic>>(
'/api/oauth/config',
queryParameters: {'platform': 'github'},
);
final configData = configResp.data;
if (configData == null ||
configData['code'] != 1 ||
(configData['data'] as Map<String, dynamic>?)?['configured'] !=
true) {
return const OAuthLoginResult(
success: false,
error: 'GitHub登录暂未配置',
);
}
final dataMap = configData['data'] as Map<String, dynamic>;
final authorizeUrl = dataMap['authorize_url'] as String;
// 通过浏览器完成OAuth
final result = await FlutterWebAuth.authenticate(
url: authorizeUrl,
callbackUrlScheme: 'xianyan',
);
// 从回调URL中提取code
final callbackUri = Uri.parse(result);
final code = callbackUri.queryParameters['code'];
if (code == null || code.isEmpty) {
return const OAuthLoginResult(
success: false,
error: 'GitHub授权码获取失败',
);
}
Log.i('$_tag: GitHub登录获取授权码成功');
return _loginToServer(
platform: OAuthPlatform.github,
code: code,
deviceName: deviceName,
deviceModel: deviceModel,
platformType: platformType,
deviceId: deviceId,
);
} on PlatformException catch (e) {
if (e.code == 'CANCELED') {
return const OAuthLoginResult(success: false, error: '用户取消');
}
Log.e('$_tag: GitHub登录平台异常: $e');
return const OAuthLoginResult(success: false, error: 'GitHub登录失败');
} catch (e) {
Log.e('$_tag: GitHub登录异常: $e');
return const OAuthLoginResult(success: false, error: 'GitHub登录失败');
}
}
// ============================================================
// 绑定/解绑社交账号
// ============================================================
/// 绑定社交账号
static Future<Map<String, dynamic>> bind({
required OAuthPlatform platform,
String? code,
String? idToken,
}) async {
final resp = await _dio.post<Map<String, dynamic>>(
'/api/oauth/bind',
data: {
'platform': platform.value,
if (code != null) 'code': code,
if (idToken != null) 'id_token': idToken,
},
);
return resp.data ?? {};
}
/// 解绑社交账号
static Future<Map<String, dynamic>> unbind({
required OAuthPlatform platform,
}) async {
final resp = await _dio.post<Map<String, dynamic>>(
'/api/oauth/unbind',
data: {'platform': platform.value},
);
return resp.data ?? {};
}
/// 获取已绑定列表
static Future<List<Map<String, dynamic>>> getBoundList() async {
final resp = await _dio.get<Map<String, dynamic>>('/api/oauth/bound');
final data = resp.data;
if (data != null && data['code'] == 1) {
final innerData = data['data'];
if (innerData is Map<String, dynamic> &&
innerData['bindings'] != null) {
return List<Map<String, dynamic>>.from(
innerData['bindings'] as List,
);
}
}
return [];
}
// ============================================================
// 内部方法
// ============================================================
/// 向服务端发送登录请求
static Future<OAuthLoginResult> _loginToServer({
required OAuthPlatform platform,
String? code,
String? idToken,
String? authorizationCode,
String? deviceName,
String? deviceModel,
String? platformType,
String? deviceId,
}) async {
try {
final resp = await _dio.post<Map<String, dynamic>>(
'/api/oauth/login',
data: {
'platform': platform.value,
if (code != null) 'code': code,
if (idToken != null) 'id_token': idToken,
if (authorizationCode != null) 'code': authorizationCode,
if (deviceName != null) 'device_name': deviceName,
if (deviceModel != null) 'device_model': deviceModel,
if (platformType != null) 'platform_type': platformType,
if (deviceId != null) 'device_id': deviceId,
},
);
final result = OAuthLoginResult.fromJson(resp.data ?? {});
Log.i(
'$_tag: ${platform.displayName}登录${result.success ? "成功" : "失败"}',
);
return result;
} on DioException catch (e) {
Log.e('$_tag: 服务端登录请求失败: $e');
return const OAuthLoginResult(
success: false,
error: '网络请求失败,请检查网络连接',
);
} catch (e) {
Log.e('$_tag: 服务端登录异常: $e');
return const OAuthLoginResult(success: false, error: '登录失败');
}
}
}