// 认证 BLoC — 管理用户登录状态和认证流程 // // 状态机: AuthInitial → AuthLoading → Unauthenticated/Authenticated // ↕ // Authenticating → Authenticated/AuthError import 'package:dio/dio.dart'; 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'; import '../../../data/repositories/class_repository.dart'; part 'auth_event.dart'; part 'auth_state.dart'; /// 认证 BLoC — 处理所有认证相关的状态转换 class AuthBloc extends Bloc { final AuthRepository _authRepository; final ClassRepository? _classRepository; final Logger _logger = Logger(printer: PrettyPrinter(methodCount: 0)); AuthBloc({ required AuthRepository authRepository, ClassRepository? classRepository, }) : _authRepository = authRepository, _classRepository = classRepository, super(const AuthInitial()) { // 注册事件处理器 on(_onAppStarted); on(_onLoginRequested); on(_onRegisterRequested); on(_onRoleSelected); on(_onParentalConsentAccepted); on(_onClassCodeSubmitted); on(_onLogoutRequested); on(_onTokenRefreshed); on(_onAuthExpired); } /// App 启动 — 从本地存储恢复认证状态 Future _onAppStarted( AppStarted event, Emitter 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 _onLoginRequested( LoginRequested event, Emitter 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 _onRegisterRequested( RegisterRequested event, Emitter 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 _onRoleSelected( RoleSelected event, Emitter emit, ) async { final currentState = state; if (currentState is! Authenticated) return; // 学生角色需要先经过家长同意确认(PIPL 第28条) final needsParentalConsent = event.role == UserRoleType.student; // 根据角色决定下一步 final needsClassCode = event.role == UserRoleType.student || event.role == UserRoleType.parent; emit(currentState.copyWith( needsRoleSelection: false, needsParentalConsent: needsParentalConsent, needsClassCode: needsClassCode && !needsParentalConsent, selectedRole: event.role, )); _logger.i('角色选择: ${event.role.name}, 需要家长同意: $needsParentalConsent'); } /// 家长/监护人同意信息收集(PIPL 合规) Future _onParentalConsentAccepted( ParentalConsentAccepted event, Emitter emit, ) async { final currentState = state; if (currentState is! Authenticated) return; _logger.i('家长同意已确认: ${event.consentAt}'); emit(currentState.copyWith( needsParentalConsent: false, needsClassCode: true, parentalConsentAt: event.consentAt, )); } /// 班级码加入 Future _onClassCodeSubmitted( ClassCodeSubmitted event, Emitter emit, ) async { final currentState = state; if (currentState is! Authenticated) return; // 如果没有 ClassRepository(离线模式),直接跳过 final classRepo = _classRepository; if (classRepo == null) { _logger.w('ClassRepository 不可用,跳过班级码验证'); emit(currentState.copyWith(needsClassCode: false)); return; } emit(currentState.copyWith(isLoading: true)); try { // 调用后端 API 验证班级码并加入班级 await classRepo.joinClass( event.classCode, nickname: currentState.user.displayName, ); // 成功 — 清除班级码需求 emit(currentState.copyWith( needsClassCode: false, isLoading: false, )); _logger.i('班级码加入成功: ${event.classCode}'); } on DioException catch (e) { final statusCode = e.response?.statusCode; String errorMessage = '加入班级失败,请重试'; if (statusCode == 400) { // 班级码无效或已过期 final body = e.response?.data; if (body is Map && body['message'] is String) { errorMessage = body['message'] as String; } else { errorMessage = '班级码无效,请检查后重新输入'; } } else if (statusCode == 429) { // 尝试次数过多 — 锁定 errorMessage = '尝试次数过多,请等待 30 分钟后再试'; } _logger.w('班级码验证失败 ($statusCode): $errorMessage'); emit(currentState.copyWith( isLoading: false, classCodeError: errorMessage, )); } on OfflineException { emit(currentState.copyWith( isLoading: false, classCodeError: '网络不可用,请检查网络后重试', )); } catch (e) { _logger.e('班级码验证异常: $e'); emit(currentState.copyWith( isLoading: false, classCodeError: '加入班级失败,请稍后重试', )); } } /// 用户登出 Future _onLogoutRequested( LogoutRequested event, Emitter emit, ) async { try { await _authRepository.logout(); } catch (e) { _logger.w('登出失败(忽略): $e'); } emit(const Unauthenticated()); } /// 令牌刷新成功 Future _onTokenRefreshed( TokenRefreshed event, Emitter emit, ) async { _logger.d('令牌已刷新'); // 不改变当前状态,仅更新令牌 } /// 认证过期(401 拦截器触发) Future _onAuthExpired( AuthExpired event, Emitter emit, ) async { _logger.w('认证过期,需要重新登录'); emit(const Unauthenticated()); } }