// 功能描述:装配用户端依赖,并统一管理登录、退出和失效会话。 // 版本:1.1.0 import 'package:flutter/foundation.dart'; import '../data/repositories/client_repository.dart'; import '../data/services/api_client.dart'; import '../data/services/secure_session_store.dart'; import '../data/services/repair_draft_store.dart'; import '../domain/models/primary_models.dart'; class AppDependencies { AppDependencies({ required this.session, required this.repository, this.repairDraftStore, }); final UserSession session; final ClientRepository repository; final RepairDraftStore? repairDraftStore; static Future create() async { final store = SecureSessionStore(); final session = UserSession(store); await session.restore(); final api = ApiClient( () => session.token, onUnauthorized: session.invalidate, ); return AppDependencies( session: session, repository: ClientRepository(api), repairDraftStore: SecureRepairDraftStore(), ); } } /// 管理内存令牌、持久化令牌以及会话失效通知。 class UserSession extends ChangeNotifier { UserSession(this._store); static const _root = '/heqi/client/v1/user'; final SessionStore _store; String _token = ''; bool _expired = false; /// 本次登录是否持久化;默认兼容旧调用方,登录页由用户显式选择。 bool rememberLogin = true; /// 只保存本次明确同意的内容版本,不在客户端推测当前发布版本。 List loginConsents = const []; DateTime? loginConsentShownAt; Future _pendingClear = Future.value(); String get token => _token; bool get isAuthenticated => _token.isNotEmpty; bool get hasExpired => _expired; /// 从安全存储恢复令牌;是否有效由后续受保护接口响应确认。 Future restore() async { _token = await _store.readToken() ?? ''; _expired = false; } Future login({ required String phone, required String password, String? verificationCode, String? requestIdentity, }) async { final api = ApiClient(() => ''); final verification = verificationCode != null && verificationCode.isNotEmpty; final details = jsonMap( await api.post( '$_root/auth/login', authenticated: false, body: { 'phone': phone, 'mode': verification ? 'verification_code' : 'password', 'password': verification ? '' : password, 'code': verificationCode ?? '', 'request_identity': requestIdentity ?? '', if (loginConsents.isNotEmpty) 'consents': [ for (final c in loginConsents) { 'identity': c.identity, 'version': c.version, 'shown_at': loginConsentShownAt?.toUtc().toIso8601String(), }, ], }, ), ); final token = details['access_token'] as String? ?? ''; if (token.isEmpty) throw const ApiException(500, '登录令牌缺失'); // 等待旧令牌清理结束,避免迟到的删除任务误删刚写入的新令牌。 await _pendingClear; if (rememberLogin) { await _store.writeToken(token); } else { // 清理历史令牌失败时不能声称已关闭持久会话。 await _store.clear(); } _token = token; _expired = false; loginConsents = const []; loginConsentShownAt = null; notifyListeners(); } Future sendCode(String phone, String purpose) async { final api = ApiClient(() => ''); final details = jsonMap( await api.post( '$_root/auth/verification-code', authenticated: false, body: {'phone': phone, 'purpose': purpose}, ), ); // Mock环境只生成服务端校验记录,并未向用户手机发送短信,不能启动已发送倒计时。 if (details['delivery_status'] == 'not_sent') { throw const ApiException(2410, '短信验证码暂未开放'); } return details['request_identity'] as String? ?? ''; } /// 使用重置专用验证码修改密码,服务端校验验证码用途和密码规则。 Future resetPassword({ required String phone, required String code, required String requestIdentity, required String password, }) async { await ApiClient(() => '').post( '$_root/auth/reset-password', authenticated: false, body: { 'phone': phone, 'code': code, 'request_identity': requestIdentity, 'new_password': password, }, ); } Future logout() async { _token = ''; _expired = false; notifyListeners(); _pendingClear = _clearStoredToken(); await _pendingClear; } /// 仅当服务端拒绝的仍是当前令牌时,使会话失效并通知路由。 void invalidate(String rejectedToken) { if (rejectedToken.isEmpty || rejectedToken != _token || _expired) return; _token = ''; _expired = true; notifyListeners(); _pendingClear = _clearStoredToken(); } /// 尽力删除持久化令牌;失败不得阻塞用户返回登录页。 Future _clearStoredToken() async { try { await _store.clear(); } catch (error, stackTrace) { debugPrint('清理失效登录令牌失败:$error'); debugPrintStack(stackTrace: stackTrace); } } }