新增文件 (10): - data/models/user.dart — 用户+角色模型 (匹配后端 UserResp/RoleResp) - data/models/auth_token.dart — 认证令牌模型 (匹配后端 LoginResp) - data/repositories/auth_repository.dart — 认证仓库 (JWT 安全持久化 + PIPL 合规) - features/auth/bloc/auth_bloc.dart — 认证 BLoC (8 种事件, 6 种状态) - features/auth/bloc/auth_event.dart — 认证事件 (sealed class 穷尽匹配) - features/auth/bloc/auth_state.dart — 认证状态 (Authenticated 含角色/班级码流程) - features/auth/views/login_page.dart — 登录/注册页面 (重写占位页面) - features/auth/views/role_selection_page.dart — 角色选择页 (4 种角色卡片) - features/auth/views/class_code_join_page.dart — 班级码加入页 (6 位输入) 修改文件 (5): - pubspec.yaml — 添加 flutter_secure_storage 依赖 - app.dart — 注入 AuthBloc + RepositoryProvider - main.dart — 简化入口 (认证恢复在 BLoC 中处理) - core/routing/app_router.dart — 添加认证路由守卫 + 2 新路由 - erp-diary/service/class_service.rs — 移除未使用的 PaginatorTrait import 验证: flutter analyze (0 error) + cargo check 通过
181 lines
5.1 KiB
Dart
181 lines
5.1 KiB
Dart
// 认证 BLoC — 管理用户登录状态和认证流程
|
||
//
|
||
// 状态机: AuthInitial → AuthLoading → Unauthenticated/Authenticated
|
||
// ↕
|
||
// Authenticating → Authenticated/AuthError
|
||
|
||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
import 'package:logger/logger.dart';
|
||
|
||
import '../../../data/models/auth_token.dart';
|
||
import '../../../data/models/user.dart';
|
||
import '../../../data/remote/api_client.dart';
|
||
import '../../../data/repositories/auth_repository.dart';
|
||
|
||
part 'auth_event.dart';
|
||
part 'auth_state.dart';
|
||
|
||
/// 认证 BLoC — 处理所有认证相关的状态转换
|
||
class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||
final AuthRepository _authRepository;
|
||
final Logger _logger = Logger(printer: PrettyPrinter(methodCount: 0));
|
||
|
||
AuthBloc({required AuthRepository authRepository})
|
||
: _authRepository = authRepository,
|
||
super(const AuthInitial()) {
|
||
// 注册事件处理器
|
||
on<AppStarted>(_onAppStarted);
|
||
on<LoginRequested>(_onLoginRequested);
|
||
on<RegisterRequested>(_onRegisterRequested);
|
||
on<RoleSelected>(_onRoleSelected);
|
||
on<ClassCodeSubmitted>(_onClassCodeSubmitted);
|
||
on<LogoutRequested>(_onLogoutRequested);
|
||
on<TokenRefreshed>(_onTokenRefreshed);
|
||
on<AuthExpired>(_onAuthExpired);
|
||
}
|
||
|
||
/// App 启动 — 从本地存储恢复认证状态
|
||
Future<void> _onAppStarted(
|
||
AppStarted event,
|
||
Emitter<AuthState> emit,
|
||
) async {
|
||
emit(const AuthLoading());
|
||
try {
|
||
final user = await _authRepository.restoreAuth();
|
||
if (user != null) {
|
||
emit(Authenticated(
|
||
user: user,
|
||
needsRoleSelection: !user.hasRole,
|
||
));
|
||
} else {
|
||
emit(const Unauthenticated());
|
||
}
|
||
} catch (e) {
|
||
_logger.e('恢复认证状态失败: $e');
|
||
emit(const Unauthenticated());
|
||
}
|
||
}
|
||
|
||
/// 用户登录
|
||
Future<void> _onLoginRequested(
|
||
LoginRequested event,
|
||
Emitter<AuthState> emit,
|
||
) async {
|
||
emit(const Authenticating());
|
||
try {
|
||
final user = await _authRepository.login(
|
||
username: event.username,
|
||
password: event.password,
|
||
);
|
||
emit(Authenticated(
|
||
user: user,
|
||
needsRoleSelection: !user.hasRole,
|
||
));
|
||
} on AuthException catch (e) {
|
||
_logger.w('登录失败: ${e.message}');
|
||
emit(AuthError(e.message));
|
||
} on OfflineException {
|
||
emit(const AuthError('网络不可用,请检查网络连接', retryable: true));
|
||
} catch (e) {
|
||
_logger.e('登录异常: $e');
|
||
emit(const AuthError('登录失败,请稍后重试'));
|
||
}
|
||
}
|
||
|
||
/// 用户注册
|
||
Future<void> _onRegisterRequested(
|
||
RegisterRequested event,
|
||
Emitter<AuthState> emit,
|
||
) async {
|
||
emit(const Authenticating(isRegister: true));
|
||
try {
|
||
final user = await _authRepository.register(
|
||
username: event.username,
|
||
password: event.password,
|
||
displayName: event.displayName,
|
||
);
|
||
// 注册成功后需要选择角色
|
||
emit(Authenticated(
|
||
user: user,
|
||
needsRoleSelection: true,
|
||
));
|
||
} on AuthException catch (e) {
|
||
_logger.w('注册失败: ${e.message}');
|
||
emit(AuthError(e.message));
|
||
} on OfflineException {
|
||
emit(const AuthError('网络不可用,请检查网络连接', retryable: true));
|
||
} catch (e) {
|
||
_logger.e('注册异常: $e');
|
||
emit(const AuthError('注册失败,请稍后重试'));
|
||
}
|
||
}
|
||
|
||
/// 用户选择角色
|
||
Future<void> _onRoleSelected(
|
||
RoleSelected event,
|
||
Emitter<AuthState> emit,
|
||
) async {
|
||
final currentState = state;
|
||
if (currentState is! Authenticated) return;
|
||
|
||
// 根据角色决定下一步
|
||
final needsClassCode =
|
||
event.role == UserRoleType.student || event.role == UserRoleType.parent;
|
||
|
||
emit(currentState.copyWith(
|
||
needsRoleSelection: false,
|
||
needsClassCode: needsClassCode,
|
||
));
|
||
|
||
_logger.i('角色选择: ${event.role.name}, 需要班级码: $needsClassCode');
|
||
}
|
||
|
||
/// 班级码加入
|
||
Future<void> _onClassCodeSubmitted(
|
||
ClassCodeSubmitted event,
|
||
Emitter<AuthState> emit,
|
||
) async {
|
||
final currentState = state;
|
||
if (currentState is! Authenticated) return;
|
||
|
||
// TODO: 调用后端 API 验证班级码并加入班级
|
||
// 当前先标记为已完成,班级码验证在 F8 阶段完善
|
||
emit(currentState.copyWith(
|
||
needsClassCode: false,
|
||
));
|
||
|
||
_logger.i('班级码加入: ${event.classCode}');
|
||
}
|
||
|
||
/// 用户登出
|
||
Future<void> _onLogoutRequested(
|
||
LogoutRequested event,
|
||
Emitter<AuthState> emit,
|
||
) async {
|
||
try {
|
||
await _authRepository.logout();
|
||
} catch (e) {
|
||
_logger.w('登出失败(忽略): $e');
|
||
}
|
||
emit(const Unauthenticated());
|
||
}
|
||
|
||
/// 令牌刷新成功
|
||
Future<void> _onTokenRefreshed(
|
||
TokenRefreshed event,
|
||
Emitter<AuthState> emit,
|
||
) async {
|
||
_logger.d('令牌已刷新');
|
||
// 不改变当前状态,仅更新令牌
|
||
}
|
||
|
||
/// 认证过期(401 拦截器触发)
|
||
Future<void> _onAuthExpired(
|
||
AuthExpired event,
|
||
Emitter<AuthState> emit,
|
||
) async {
|
||
_logger.w('认证过期,需要重新登录');
|
||
emit(const Unauthenticated());
|
||
}
|
||
}
|