Files
patbond-flutter/lib/core/network/token_refresher.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

67 lines
2.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:dio/dio.dart';
import 'package:patbond_flutter/core/network/api_envelope.dart';
import 'package:patbond_flutter/core/network/api_exception.dart';
import 'package:patbond_flutter/features/auth/auth_models.dart';
import 'package:patbond_flutter/features/auth/session_manager.dart';
/// 单飞(single-flighttoken 刷新器。
///
/// 并发触发的多个 401/40101 只发出一次 `POST /auth/refresh`
/// 其余调用共享同一个进行中的刷新结果。
///
/// 清会话纪律(12 号组装稿 §7):只有服务端明确判定 refresh 失效
/// 40102 或 HTTP 401)才清除本地会话;网络失败、5xx 一律保留 token。
class TokenRefresher {
TokenRefresher({required this._dio, required this._session});
final Dio _dio;
final SessionManager _session;
Future<void>? _inflight;
/// 刷新并轮换 token。成功后新 token 已写入 [SessionManager]。
///
/// 抛出 [SessionExpiredException](会话已被清除)或
/// [ApiNetworkException]token 保留,可重试)。
Future<void> refresh() {
return _inflight ??= _doRefresh().whenComplete(() => _inflight = null);
}
Future<void> _doRefresh() async {
await _session.loadFromStorage();
final refreshToken = _session.refreshToken;
if (refreshToken == null) {
await _session.clearSession();
throw const SessionExpiredException('本地无 refresh token');
}
final Response<Object?> response;
try {
response = await _dio.post<Object?>(
'/api/v1/auth/refresh',
data: {'refreshToken': refreshToken},
);
} on DioException catch (error) {
throw ApiNetworkException('刷新请求失败:${error.type.name}');
}
final envelope = ApiEnvelope.tryParse(response.data);
if (response.statusCode == 401 ||
envelope?.code == ApiCodes.refreshTokenInvalid) {
await _session.clearSession();
throw const SessionExpiredException('refresh token 已失效');
}
if (envelope != null &&
envelope.code == ApiCodes.ok &&
envelope.data is Map<String, dynamic>) {
await _session.updateTokens(
AuthTokens.fromJson(envelope.data! as Map<String, dynamic>),
);
return;
}
throw ApiNetworkException(
'刷新响应异常:HTTP ${response.statusCode}code ${envelope?.code}',
);
}
}