Files
patbond-flutter/lib/features/auth/auth_repository.dart
T
lixi 8d890c0424 feat: 落地登录纵切——网络层、认证会话与登录/注册/Splash 三页(ADR-003/ADR-004)
- 新增 lib/core/network/:ApiClient(dio 封装,错误信封→类型化异常,base URL 经 --dart-define=PATBOND_API_BASE_URL 注入)、AuthInterceptor(Bearer + 设备标识)、TokenRefresher(单飞刷新,401/40101 重放一次,仅 40102/再 401 清会话)
- 新增 lib/features/auth/:SessionManager(token 只进 flutter_secure_storage)、AuthRepository(login/register/refresh/logout/me,注册带 Idempotency-Key)、Splash(500ms 最短停留/5s 超时/错误态重试+改用账号登录)、登录页与注册页(照 12 号组装稿,错误三层映射,预留区不渲染)
- App 根组件改为认证状态机驱动(Splash↔登录↔主壳 300ms fade);个人中心退出登录接入真实 logout
- 顺带修复:FIX-1 促销卡渐变改 [primaryStrong, primary]、FIX-2 补 helperStyle: muted、README dart format 命令补 --output=none
- 新增依赖:dio ^5.11.1、flutter_secure_storage ^11.0.0、uuid ^4.6.0
- 门禁:dart format(0 changed)/ flutter analyze(No issues)/ flutter test(30 passed,其中新增 23:TokenRefresher 5 + AuthRepository 10 + 登录页 4 + 注册页 4)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-04 11:52:51 +08:00

112 lines
3.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 '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<void> login({required String username, required String password});
Future<void> register({
required String username,
required String phone,
required String password,
});
/// 撤销当前会话并清除本地凭证。服务端调用失败也保证本地清除。
Future<void> logout();
/// 启动恢复:本地有 refresh token 则静默刷新。
/// 网络失败抛 [ApiNetworkException]token 保留);
/// refresh 失效抛 [SessionExpiredException](会话已清)。
Future<SessionRestoreResult> restoreSession();
Future<UserProfile> me();
}
class ApiAuthRepository implements AuthRepository {
ApiAuthRepository({
required this._api,
required this._session,
required this._refresher,
this._uuid = const Uuid(),
});
final ApiClient _api;
final SessionManager _session;
final TokenRefresher _refresher;
final Uuid _uuid;
@override
Future<void> login({
required String username,
required String password,
}) async {
final data = await _api.request(
'/api/v1/auth/login',
body: {'username': username, 'password': password},
);
await _session.saveSession(
AuthTokens.fromJson(data! as Map<String, dynamic>),
);
}
@override
Future<void> register({
required String username,
required String phone,
required String password,
}) async {
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<String, dynamic>),
);
}
@override
Future<void> 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();
}
}
@override
Future<SessionRestoreResult> restoreSession() async {
await _session.loadFromStorage();
if (_session.refreshToken == null) {
return SessionRestoreResult.noSession;
}
await _refresher.refresh();
return SessionRestoreResult.authenticated;
}
@override
Future<UserProfile> me() async {
final data = await _api.request(
'/api/v1/me',
method: 'GET',
requiresAuth: true,
);
return UserProfile.fromJson(data! as Map<String, dynamic>);
}
}