移除 SplashCoordinator,bootstrap 期间用 SplashView 覆盖避免空白闪屏,其余账号数据改为后台补充刷新。 Co-authored-by: Cursor <cursoragent@cursor.com>
210 lines
7.5 KiB
Swift
210 lines
7.5 KiB
Swift
//
|
||
// 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)
|
||
accountContext.replaceScopes(
|
||
scenic: snapshot.scenicScopes,
|
||
stores: snapshot.storeScopes,
|
||
currentScenicId: snapshot.currentScenicId,
|
||
currentStoreId: snapshot.currentStoreId
|
||
)
|
||
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,
|
||
currentRoleCode: permissionContext.currentAppRole?.rawValue ?? existing?.currentRoleCode,
|
||
scenicScopes: accountContext.scenicScopes,
|
||
storeScopes: accountContext.storeScopes,
|
||
currentScenicId: accountContext.currentScenic?.id,
|
||
currentStoreId: accountContext.currentStore?.id
|
||
)
|
||
)
|
||
}
|
||
}
|