Files
lixi 3f8388e5d4
CI / flutter-gates (push) Successful in 50s
fix: 清零 flutter analyze 问题并修复隐私红线正则缺陷(CI 门禁)
- 修复真实缺陷:隐私红线 RegExp 使用 Dart 不支持的 (?i) 内联标志,构造即抛异常
  导致带 props 的事件被静默丢弃;改为 caseSensitive: false
- 清理埋点模块未用 import/字段/变量(4 warning)
- 构造函数改用初始化形参(含 Dart 3.12 私有具名参数)
- E2E 手动脚本声明 ignore avoid_print + library 指令
- 门禁:format 0 changed / analyze No issues / flutter test 34 passed
2026-09-04 19:44:46 +08:00

143 lines
4.3 KiB
Dart
Raw Permalink 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/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<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._analytics,
this._uuid = const Uuid(),
});
final ApiClient _api;
final SessionManager _session;
final TokenRefresher _refresher;
final AnalyticsService? _analytics;
final Uuid _uuid;
@override
Future<void> 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<String, dynamic>),
);
_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<void> 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<String, dynamic>),
);
_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<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();
_analytics?.reset();
_analytics?.trackEvent('auth_logout', {'serverRevoked': true});
}
}
@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>);
}
}