import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/network/api_exception.dart'; import '../../../core/network/error_message.dart'; import '../../../core/storage/token_storage.dart'; import '../data/auth_providers.dart'; import '../domain/auth_models.dart'; import '../domain/auth_state.dart'; /// 认证控制器。 /// /// 承载登录 / 注册 / 发送验证码 / 倒计时等全部业务逻辑,UI 仅触发动作并消费状态。 /// 使用 keepAlive,避免登录流程中页面切换导致状态丢失。 class AuthController extends Notifier { AuthController(); Timer? _countdownTimer; @override AuthState build() { ref.keepAlive(); ref.onDispose(() => _countdownTimer?.cancel()); return const AuthState(); } String get _deviceId => TokenStorage.deviceId; /// 密码登录。 Future loginByPassword({ required String phone, required String password, }) async { return _run(() => ref.read(authRepositoryProvider).loginByPassword( phone: phone, password: password, deviceId: _deviceId, )); } /// 短信登录。 Future loginBySms({ required String phone, required String code, }) async { return _run(() => ref.read(authRepositoryProvider).loginBySms( phone: phone, code: code, deviceId: _deviceId, )); } /// 注册。 Future register({ required String phone, required String code, required String password, }) async { return _run(() => ref.read(authRepositoryProvider).register( phone: phone, code: code, password: password, deviceId: _deviceId, )); } /// 发送短信验证码并启动 60s 倒计时。 /// /// 发送前先做本地校验:手机号为空或格式不正确时直接提示,不发起请求、 /// 不启动倒计时,避免出现「空号也能点但弹报错」的体验问题。 Future sendSmsCode({ required String phone, required SmsScene scene, }) async { if (state.isSendingCode || state.countdownSeconds > 0) return; final trimmed = phone.trim(); final phoneError = _validatePhone(trimmed); if (phoneError != null) { state = state.copyWith(alert: phoneError); return; } state = state.copyWith(isSendingCode: true, clearAlert: true); try { await ref.read(authRepositoryProvider).sendSmsCode( phone: trimmed, scene: scene, ); _startCountdown(); } catch (e) { state = state.copyWith( isSendingCode: false, alert: userMessageOf(e), ); } } /// 校验手机号:空或不符合 11 位中国大陆手机号规则时返回提示文案。 String? _validatePhone(String phone) { if (phone.isEmpty) return '请输入手机号'; // 1 开头的 11 位手机号。 if (!RegExp(r'^1\d{10}$').hasMatch(phone)) return '手机号格式不正确'; return null; } /// 重置密码(忘记密码流程)。 Future resetPassword({ required String phone, required String code, required String password, }) async { state = state.copyWith(isLoading: true, clearAlert: true); try { await ref.read(authRepositoryProvider).resetPassword( phone: phone, code: code, password: password, ); state = state.copyWith(isLoading: false); return true; } catch (e) { state = state.copyWith(isLoading: false, alert: userMessageOf(e)); return false; } } /// 消费一次性提示。 void consumeAlert() { if (state.alert != null) { state = state.copyWith(clearAlert: true); } } /// 登出。 void logout() { _countdownTimer?.cancel(); TokenStorage.clear(); state = const AuthState(); } Future _run(Future Function() action) async { state = state.copyWith(isLoading: true, clearAlert: true); try { await action(); state = state.copyWith(isLoading: false); return true; } catch (e) { state = state.copyWith(isLoading: false, alert: userMessageOf(e)); // 鉴权失效:本地令牌可能已作废,主动登出清理。 if (_isUnauthorized(e)) { logout(); } return false; } } /// 判断异常是否为鉴权失效(401)。 bool _isUnauthorized(Object e) { if (e is ApiException) return e.code == '401'; return false; } void _startCountdown() { state = state.copyWith(isSendingCode: false, countdownSeconds: 60); _countdownTimer?.cancel(); _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { final left = state.countdownSeconds - 1; if (left <= 0) { timer.cancel(); state = state.copyWith(countdownSeconds: 0); } else { state = state.copyWith(countdownSeconds: left); } }); } } /// 认证控制器 Provider(会话级 keepAlive 在 build 内通过 ref.keepAlive 实现)。 final authControllerProvider = NotifierProvider(AuthController.new);