// 功能描述:装配工作人员端依赖,并统一管理登录、退出和失效会话。 // 版本:1.1.0 import 'package:flutter/foundation.dart'; import 'package:uuid/uuid.dart'; import '../data/offline/encrypted_draft_store.dart'; import '../data/repositories/service_repository.dart'; import '../data/services/api_client.dart'; import '../data/services/location_service.dart'; import '../data/services/secure_session_store.dart'; class AppDependencies { AppDependencies({ required this.session, required this.repository, required this.drafts, }); final StaffSession session; final ServiceRepository repository; final EncryptedDraftStore drafts; static Future create() async { final store = SecureSessionStore(); final session = StaffSession(store); await session.restore(); final authenticatedApi = ApiClient( () => session.token, onUnauthorized: session.invalidate, ); return AppDependencies( session: session, repository: ServiceRepository(authenticatedApi, GeolocatorLocationService()), drafts: EncryptedDraftStore(), ); } } /// 管理工作人员令牌、身份、角色、设备标识及会话失效通知。 class StaffSession extends ChangeNotifier { StaffSession(this._store); static const _root = '/heqi/client/v1/staff'; static const _tokenKey = 'service_app_access_token'; static const _identityKey = 'service_app_identity'; static const _roleKey = 'service_app_role'; static const _deviceKey = 'service_app_device'; final SessionStore _store; String _token = ''; String _identity = ''; String _roleCode = ''; String _deviceIdentity = ''; bool _expired = false; Future _pendingClear = Future.value(); String get token => _token; String get identity => _identity; String get roleCode => _roleCode; String get deviceIdentity => _deviceIdentity; bool get isAuthenticated => _token.isNotEmpty; bool get hasExpired => _expired; /// 从安全存储恢复会话上下文;是否有效由后续受保护接口响应确认。 Future restore() async { _token = await _store.read(_tokenKey) ?? ''; _identity = await _store.read(_identityKey) ?? ''; _roleCode = await _store.read(_roleKey) ?? ''; _deviceIdentity = await _store.read(_deviceKey) ?? ''; _expired = false; if (_deviceIdentity.isEmpty) { _deviceIdentity = const Uuid().v7(); await _store.write(_deviceKey, _deviceIdentity); } } Future login(String phone, String password) async { final api = ApiClient(() => ''); final details = jsonMap( await api.post( '$_root/auth/login', authenticated: false, body: {'phone': phone, 'mode': 'password', 'password': password}, ), ); final token = details['access_token'] as String? ?? ''; final identity = details['identity'] as String? ?? ''; final roleCode = details['role_code'] as String? ?? ''; if (token.isEmpty || identity.isEmpty || roleCode.isEmpty) { throw const ApiException(500, '工作人员登录上下文缺失'); } // 等待旧会话清理结束,避免迟到的删除任务误删刚写入的新会话。 await _pendingClear; await _store.write(_tokenKey, token); await _store.write(_identityKey, identity); await _store.write(_roleKey, roleCode); _token = token; _identity = identity; _roleCode = roleCode; _expired = false; notifyListeners(); } Future logout() async { _token = ''; _identity = ''; _roleCode = ''; _expired = false; notifyListeners(); _pendingClear = _clearStoredSession(); await _pendingClear; } /// 仅当服务端拒绝的仍是当前令牌时,使会话失效并通知路由。 void invalidate(String rejectedToken) { if (rejectedToken.isEmpty || rejectedToken != _token || _expired) return; _token = ''; _identity = ''; _roleCode = ''; _expired = true; notifyListeners(); _pendingClear = _clearStoredSession(); } /// 尽力删除会话字段并保留设备标识;单项失败不得阻塞返回登录页。 Future _clearStoredSession() async { for (final key in const [_tokenKey, _identityKey, _roleKey]) { try { await _store.delete(key); } catch (error, stackTrace) { debugPrint('清理工作人员失效会话失败($key):$error'); debugPrintStack(stackTrace: stackTrace); } } } }