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(); /// 本人资料部分更新(昵称 / 头像,三态语义见 [UpdateMeRequest])。 /// 成功返回**更新后的全量 [UserProfile]**(服务端回显,含现签 avatarUrl)。 Future updateMe(UpdateMeRequest request); } /// 基于 [ApiClient] 的实现。 /// /// **端口线路**(ADR-002 无网关,分端口直连):注册/登录/登出/刷新走 auth /// 服务(:8081);`GET` 与 `PATCH /api/v1/me` 由 **user 服务**(:8082 /// `MeController`)提供,故经 [userApi] 直连——auth 上没有 `/api/v1/me` /// 路由,走主客户端会得到 404。未提供 [userApi] 时回落主客户端(既有测试 /// 桩场景)。同一线路分离手法与 `ApiCommunityRepository` 的 `mediaApi` 一致。 class ApiAuthRepository implements AuthRepository { ApiAuthRepository({ required ApiClient api, required this._session, required this._refresher, ApiClient? userApi, this._analytics, this._uuid = const Uuid(), }) : _api = api, // 缺省回落主客户端:既有测试桩只注入一个 ApiClient。 _userApi = userApi ?? api; final ApiClient _api; final ApiClient _userApi; 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 _userApi.request( '/api/v1/me', method: 'GET', requiresAuth: true, ); return UserProfile.fromJson(data! as Map); } @override Future updateMe(UpdateMeRequest request) async { // 无乐观锁、无幂等键(契约定型:唯一合法写者 + 列级选择性 UPDATE, // 同 body 重放天然幂等)。三态由 request.toJson 表达——「不改」的字段 // 根本不出现在 body 里,绝不发成 null。 final data = await _userApi.request( '/api/v1/me', method: 'PATCH', body: request.toJson(), requiresAuth: true, ); return UserProfile.fromJson(data! as Map); } }