Files
nj/app/lib/features/auth/bloc/auth_bloc.dart
iven 0fe3bc705c feat(app): 实现认证模块 — F2 Auth BLoC + 登录/注册/角色选择/班级码加入
新增文件 (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 通过
2026-06-01 01:22:53 +08:00

181 lines
5.1 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.
// 认证 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());
}
}