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 通过
This commit is contained in:
iven
2026-06-01 01:22:53 +08:00
parent 232a53dbed
commit 0fe3bc705c
15 changed files with 1805 additions and 113 deletions

View File

@@ -0,0 +1,180 @@
// 认证 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());
}
}