import 'package:patbond_flutter/analytics/analytics_service.dart'; import 'package:patbond_flutter/core/network/api_client.dart'; import 'package:patbond_flutter/core/network/api_exception.dart'; import 'package:patbond_flutter/core/network/token_refresher.dart'; import 'package:patbond_flutter/features/auth/auth_models.dart'; import 'package:patbond_flutter/features/auth/session_manager.dart'; import 'package:uuid/uuid.dart'; /// Splash 会话恢复的裁决结果。状态切换由 Splash 在最短停留时间后执行。 enum SessionRestoreResult { authenticated, noSession } /// 认证仓库接口(页面依赖此抽象,widget 测试注入假实现)。 abstract class AuthRepository { Future login({required String username, required String password}); Future register({ required String username, required String phone, required String password, }); /// 撤销当前会话并清除本地凭证。服务端调用失败也保证本地清除。 Future logout(); /// 启动恢复:本地有 refresh token 则静默刷新。 /// 网络失败抛 [ApiNetworkException](token 保留); /// refresh 失效抛 [SessionExpiredException](会话已清)。 Future restoreSession(); Future me(); } class ApiAuthRepository implements AuthRepository { ApiAuthRepository({ required this._api, required this._session, required this._refresher, AnalyticsService? analytics, this._uuid = const Uuid(), }) : _analytics = analytics; final ApiClient _api; final SessionManager _session; final TokenRefresher _refresher; final AnalyticsService? _analytics; final Uuid _uuid; @override Future login({ required String username, required String password, }) async { final startTime = DateTime.now(); try { final data = await _api.request( '/api/v1/auth/login', body: {'username': username, 'password': password}, ); await _session.saveSession( AuthTokens.fromJson(data! as Map), ); _analytics?.identify(_session.userId!); _analytics?.trackEvent('auth_login_succeeded', { 'identifierType': 'username', 'durationMs': DateTime.now().difference(startTime).inMilliseconds, }); } catch (error) { _analytics?.trackEvent('auth_login_failed', { 'identifierType': 'username', 'failureReason': error.toString(), }); rethrow; } } @override Future register({ required String username, required String phone, required String password, }) async { final startTime = DateTime.now(); try { final data = await _api.request( '/api/v1/auth/register', body: {'username': username, 'phone': phone, 'password': password}, // 每次提交一个幂等键;token 刷新后的自动重放沿用同一个键。 headers: {'Idempotency-Key': _uuid.v4()}, ); await _session.saveSession( AuthTokens.fromJson(data! as Map), ); _analytics?.identify(_session.userId!); _analytics?.trackEvent('auth_register_succeeded', { 'durationMs': DateTime.now().difference(startTime).inMilliseconds, }); } catch (error) { _analytics?.trackEvent('auth_register_failed', { 'failureReason': error.toString(), }); rethrow; } } @override Future logout() async { final refreshToken = _session.refreshToken; try { await _api.request( '/api/v1/auth/logout', body: {'refreshToken': refreshToken}, requiresAuth: true, ); } on ApiException { // 服务端撤销失败不阻塞本地登出;refresh 侧最终会自然过期。 } finally { await _session.clearSession(); _analytics?.reset(); _analytics?.trackEvent('auth_logout', {'serverRevoked': true}); } } @override Future restoreSession() async { await _session.loadFromStorage(); if (_session.refreshToken == null) { return SessionRestoreResult.noSession; } await _refresher.refresh(); return SessionRestoreResult.authenticated; } @override Future me() async { final data = await _api.request( '/api/v1/me', method: 'GET', requiresAuth: true, ); return UserProfile.fromJson(data! as Map); } }