// // SessionBootstrapper.swift // suixinkan // // Created by Codex on 2026/6/20. // import Foundation import Combine @MainActor /// 登录态启动恢复器,负责冷启动时从缓存恢复并校验 token。 final class SessionBootstrapper { private let tokenStore: SessionTokenStore private let snapshotStore: AccountSnapshotStore private var didAttemptRestore = false /// 初始化启动恢复器,并注入 token 与账号快照存储。 init( tokenStore: SessionTokenStore, snapshotStore: AccountSnapshotStore ) { self.tokenStore = tokenStore self.snapshotStore = snapshotStore } /// 创建使用默认存储服务的启动恢复器。 convenience init() { self.init( tokenStore: SessionTokenStore(), snapshotStore: AccountSnapshotStore() ) } /// 从本地缓存恢复登录态;阻塞等待 rolePermissions,其余数据后台刷新。 func restore( appSession: AppSession, accountContext: AccountContext, permissionContext: PermissionContext, profileAPI: UserProfileServing, accountContextAPI: AccountContextServing, toastCenter: ToastCenter ) async { guard !didAttemptRestore, !appSession.isLoggedIn else { return } didAttemptRestore = true guard let token = tokenStore.load() else { appSession.logout() return } appSession.beginRestoring(token: token) let cachedSnapshot = restoreCachedSnapshot(to: accountContext) let loader = AccountContextLoader() do { try await loader.restorePermissions( permissionContext: permissionContext, accountContext: accountContext, accountContextAPI: accountContextAPI, cachedCurrentRoleCode: cachedSnapshot?.currentRoleCode, cachedLegacyRoleId: cachedSnapshot?.currentRoleId, cachedCurrentScenicId: cachedSnapshot?.currentScenicId, cachedCurrentStoreId: cachedSnapshot?.currentStoreId ) appSession.markLoggedIn(token: token) refreshSupplementalContextInBackground( loader: loader, appSession: appSession, accountContext: accountContext, permissionContext: permissionContext, profileAPI: profileAPI, accountContextAPI: accountContextAPI, toastCenter: toastCenter, cachedSnapshot: cachedSnapshot ) } catch { handleBlockingRestoreFailure( error, token: token, appSession: appSession, accountContext: accountContext, permissionContext: permissionContext, toastCenter: toastCenter ) } } /// 将缓存快照恢复到账号上下文。 private func restoreCachedSnapshot(to accountContext: AccountContext) -> AccountSnapshot? { guard let snapshot = snapshotStore.load() else { return nil } accountContext.applyLogin( profile: snapshot.profile, accountType: snapshot.accountType, businessUserId: snapshot.businessUserId, displayTitle: snapshot.currentAccountDisplayTitle ) accountContext.replaceScopes( scenic: snapshot.scenicScopes, stores: snapshot.storeScopes, currentScenicId: snapshot.currentScenicId, currentStoreId: snapshot.currentStoreId, explicitSelection: true ) return snapshot } /// 后台补齐用户资料、景区和门店,不阻塞进入主界面。 private func refreshSupplementalContextInBackground( loader: AccountContextLoader, appSession: AppSession, accountContext: AccountContext, permissionContext: PermissionContext, profileAPI: UserProfileServing, accountContextAPI: AccountContextServing, toastCenter: ToastCenter, cachedSnapshot: AccountSnapshot? ) { Task { do { try await loader.refreshSupplementalContext( accountContext: accountContext, permissionContext: permissionContext, profileAPI: profileAPI, accountContextAPI: accountContextAPI, cachedCurrentScenicId: cachedSnapshot?.currentScenicId, cachedCurrentStoreId: cachedSnapshot?.currentStoreId ) saveLatestSnapshot(from: accountContext, permissionContext: permissionContext) } catch { handleSupplementalRefreshFailure( error, appSession: appSession, accountContext: accountContext, permissionContext: permissionContext, toastCenter: toastCenter ) } } } /// 处理阻塞阶段恢复失败:鉴权失效登出,网络异常则降级为本地登录态。 private func handleBlockingRestoreFailure( _ error: Error, token: String, appSession: AppSession, accountContext: AccountContext, permissionContext: PermissionContext, toastCenter: ToastCenter ) { if APIError.isAuthenticationExpired(error) { clearPersistedSession( appSession: appSession, accountContext: accountContext, permissionContext: permissionContext ) toastCenter.show("登录状态已失效,请重新登录") return } appSession.markLoggedIn(token: token) toastCenter.show("网络异常,已使用本地登录状态") } /// 处理后台补充刷新失败:鉴权失效登出,网络异常保留当前登录态。 private func handleSupplementalRefreshFailure( _ error: Error, appSession: AppSession, accountContext: AccountContext, permissionContext: PermissionContext, toastCenter: ToastCenter ) { if APIError.isAuthenticationExpired(error) { clearPersistedSession( appSession: appSession, accountContext: accountContext, permissionContext: permissionContext ) toastCenter.show("登录状态已失效,请重新登录") return } toastCenter.show("网络异常,已使用本地登录状态") } /// 清空本地 token、快照和内存上下文。 private func clearPersistedSession( appSession: AppSession, accountContext: AccountContext, permissionContext: PermissionContext ) { try? tokenStore.clear() snapshotStore.clear() accountContext.reset() permissionContext.reset() appSession.logout() } /// 保存服务端校验后的最新账号上下文。 private func saveLatestSnapshot(from accountContext: AccountContext, permissionContext: PermissionContext) { let existing = snapshotStore.load() snapshotStore.save( AccountSnapshot( profile: accountContext.profile, accountType: accountContext.accountType ?? existing?.accountType, businessUserId: existing?.businessUserId, currentAccountDisplayTitle: accountContext.currentAccountDisplayTitle ?? existing?.currentAccountDisplayTitle, currentRoleCode: permissionContext.currentAppRole?.rawValue ?? existing?.currentRoleCode, scenicScopes: accountContext.scenicScopes, storeScopes: accountContext.storeScopes, currentScenicId: accountContext.currentScenic?.id, currentStoreId: accountContext.currentStore?.id ) ) } }