fix(app): 家长同意验证流程 — PIPL 第28条合规
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled

- 新增 ParentalConsentPage: 显示隐私政策要点 + 双重确认复选框
- 角色选择流程: 学生 → 家长同意确认 → 班级码输入
- Authenticated 状态: 添加 needsParentalConsent/parentalConsentAt/selectedRole
- ParentalConsentAccepted 事件: 记录同意时间戳
- 路由守卫: 注册 /parental-consent 路径和重定向逻辑
- 非学生角色(老师/家长/独立用户)不需要经过同意流程

审计 ID: S-03
This commit is contained in:
iven
2026-06-03 10:25:23 +08:00
parent a34c9fd176
commit 99db8e5cb0
5 changed files with 318 additions and 4 deletions

View File

@@ -34,6 +34,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
on<LoginRequested>(_onLoginRequested);
on<RegisterRequested>(_onRegisterRequested);
on<RoleSelected>(_onRoleSelected);
on<ParentalConsentAccepted>(_onParentalConsentAccepted);
on<ClassCodeSubmitted>(_onClassCodeSubmitted);
on<LogoutRequested>(_onLogoutRequested);
on<TokenRefreshed>(_onTokenRefreshed);
@@ -124,16 +125,38 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
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,
needsClassCode: needsClassCode,
needsParentalConsent: needsParentalConsent,
needsClassCode: needsClassCode && !needsParentalConsent,
selectedRole: event.role,
));
_logger.i('角色选择: ${event.role.name}, 需要班级码: $needsClassCode');
_logger.i('角色选择: ${event.role.name}, 需要家长同意: $needsParentalConsent');
}
/// 家长/监护人同意信息收集PIPL 合规)
Future<void> _onParentalConsentAccepted(
ParentalConsentAccepted event,
Emitter<AuthState> emit,
) async {
final currentState = state;
if (currentState is! Authenticated) return;
_logger.i('家长同意已确认: ${event.consentAt}');
emit(currentState.copyWith(
needsParentalConsent: false,
needsClassCode: true,
parentalConsentAt: event.consentAt,
));
}
/// 班级码加入

View File

@@ -43,6 +43,12 @@ final class RoleSelected extends AuthEvent {
const RoleSelected(this.role);
}
/// 家长/监护人同意 PIPL 信息收集(审计 S-03
final class ParentalConsentAccepted extends AuthEvent {
final DateTime consentAt;
const ParentalConsentAccepted(this.consentAt);
}
/// 班级码加入(学生/家长加入班级)
final class ClassCodeSubmitted extends AuthEvent {
final String classCode;

View File

@@ -37,6 +37,9 @@ final class Authenticated extends AuthState {
/// 是否需要角色选择(新注册用户还没有角色)
final bool needsRoleSelection;
/// 是否需要家长/监护人同意PIPL 第28条 — 学生角色)
final bool needsParentalConsent;
/// 是否需要班级码加入(学生/家长角色)
final bool needsClassCode;
@@ -46,27 +49,43 @@ final class Authenticated extends AuthState {
/// 班级码验证错误信息
final String? classCodeError;
/// 已选择的角色(角色选择后暂存)
final UserRoleType? selectedRole;
/// 家长同意时间戳
final DateTime? parentalConsentAt;
const Authenticated({
required this.user,
this.needsRoleSelection = false,
this.needsParentalConsent = false,
this.needsClassCode = false,
this.isLoading = false,
this.classCodeError,
this.selectedRole,
this.parentalConsentAt,
});
Authenticated copyWith({
User? user,
bool? needsRoleSelection,
bool? needsParentalConsent,
bool? needsClassCode,
bool? isLoading,
String? classCodeError,
UserRoleType? selectedRole,
DateTime? parentalConsentAt,
}) =>
Authenticated(
user: user ?? this.user,
needsRoleSelection: needsRoleSelection ?? this.needsRoleSelection,
needsParentalConsent:
needsParentalConsent ?? this.needsParentalConsent,
needsClassCode: needsClassCode ?? this.needsClassCode,
isLoading: isLoading ?? this.isLoading,
classCodeError: classCodeError,
selectedRole: selectedRole ?? this.selectedRole,
parentalConsentAt: parentalConsentAt ?? this.parentalConsentAt,
);
}