1. 移除NFC和蓝牙相关依赖、权限及功能代码,精简传输链路 2. 重构设备在线统计逻辑,使用后端7天活跃字段替代本地计算 3. 更新应用名称、权限说明和协议文档 4. 新增消息转发、缓存管理、医疗免责提示功能 5. 优化运势模块和字体管理文案,修复构建日志问题
378 lines
12 KiB
Dart
378 lines
12 KiB
Dart
/// ============================================================
|
||
/// 文件: 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: '登录失败');
|
||
}
|
||
}
|
||
}
|