// // AccountContextLoader.swift // suixinkan // // Created by Codex on 2026/6/22. // import Foundation @MainActor /// 用户资料服务协议,抽象用户信息读取能力以便测试替换。 protocol UserProfileServing { /// 获取当前登录账号的用户资料。 func userInfo() async throws -> UserInfoResponse } @MainActor /// 账号上下文加载器,统一装配用户资料、角色权限、景区和门店上下文。 struct AccountContextLoader { /// 刷新完整账号上下文;登录成功等场景仍需要同步拉齐全部数据。 func refresh( accountContext: AccountContext, permissionContext: PermissionContext, profileAPI: UserProfileServing, accountContextAPI: AccountContextServing, cachedCurrentRoleCode: String? = nil, cachedLegacyRoleId: Int? = nil, cachedCurrentScenicId: Int? = nil, cachedCurrentStoreId: Int? = nil ) async throws { try await restorePermissions( permissionContext: permissionContext, accountContext: accountContext, accountContextAPI: accountContextAPI, cachedCurrentRoleCode: cachedCurrentRoleCode, cachedLegacyRoleId: cachedLegacyRoleId, cachedCurrentScenicId: cachedCurrentScenicId, cachedCurrentStoreId: cachedCurrentStoreId ) try await refreshSupplementalContext( accountContext: accountContext, permissionContext: permissionContext, profileAPI: profileAPI, accountContextAPI: accountContextAPI, cachedCurrentScenicId: cachedCurrentScenicId, cachedCurrentStoreId: cachedCurrentStoreId ) } /// 冷启动阻塞阶段:仅恢复角色权限,确保首页菜单和权限控制可用。 func restorePermissions( permissionContext: PermissionContext, accountContext: AccountContext, accountContextAPI: AccountContextServing, cachedCurrentRoleCode: String? = nil, cachedLegacyRoleId: Int? = nil, cachedCurrentScenicId: Int? = nil, cachedCurrentStoreId: Int? = nil ) async throws { let rolePermissions = try await accountContextAPI.rolePermissions() permissionContext.replaceRolePermissions( rolePermissions, currentRoleCode: cachedCurrentRoleCode, cachedLegacyRoleId: cachedLegacyRoleId ) let roleScenicScopes = permissionContext.currentRoleScenicScopes() guard accountContext.scenicScopes.isEmpty, !roleScenicScopes.isEmpty else { return } accountContext.replaceScopes( scenic: roleScenicScopes, stores: accountContext.storeScopes, currentScenicId: cachedCurrentScenicId, currentStoreId: cachedCurrentStoreId ) } /// 冷启动后台阶段:补齐用户资料、景区和门店上下文。 func refreshSupplementalContext( accountContext: AccountContext, permissionContext: PermissionContext, profileAPI: UserProfileServing, accountContextAPI: AccountContextServing, cachedCurrentScenicId: Int? = nil, cachedCurrentStoreId: Int? = nil ) async throws { let userInfo = try await profileAPI.userInfo() let scenicResponse = await loadScenicList( accountContextAPI: accountContextAPI, rolePermissions: permissionContext.rolePermissions ) let storesResponse = await loadStores(accountContextAPI: accountContextAPI) let profile = makeProfile(from: userInfo, fallback: accountContext.profile) let scenicScopes = scopedScenics( permissionContext: permissionContext, scenicResponse: scenicResponse ) let storeScopes = storesResponse?.list.map { store in BusinessScope(id: store.id, name: store.name, kind: .store, parentScenicId: store.scenicId) } ?? [] accountContext.replaceProfile(profile) accountContext.replaceScopes( scenic: scenicScopes, stores: storeScopes, currentScenicId: cachedCurrentScenicId ?? accountContext.currentScenic?.id, currentStoreId: cachedCurrentStoreId ?? accountContext.currentStore?.id ) } /// 景区列表接口失败时,从角色权限中的景区数据去重构造兜底列表。 private func loadScenicList( accountContextAPI: AccountContextServing, rolePermissions: [RolePermissionResponse] ) async -> ScenicListAllResponse { do { return try await accountContextAPI.scenicListAll() } catch { let fallback = uniqueScenics(from: rolePermissions) return ScenicListAllResponse(total: fallback.count, list: fallback) } } /// 门店列表接口失败时返回 nil,不阻断登录和恢复流程。 private func loadStores(accountContextAPI: AccountContextServing) async -> ListPayload? { try? await accountContextAPI.storeAll() } /// 根据当前角色优先返回角色可访问景区,角色无景区时使用景区列表接口结果。 private func scopedScenics( permissionContext: PermissionContext, scenicResponse: ScenicListAllResponse ) -> [BusinessScope] { let roleScenics = permissionContext.currentRoleScenicScopes() if !roleScenics.isEmpty { return roleScenics } return scenicResponse.list.map { BusinessScope(id: $0.id, name: $0.name, kind: .scenic) } } /// 从角色权限中去重生成景区列表。 private func uniqueScenics(from rolePermissions: [RolePermissionResponse]) -> [ScenicListItem] { var seen = Set() var result: [ScenicListItem] = [] for permission in rolePermissions { for scenic in permission.scenic where seen.insert(scenic.id).inserted { result.append(ScenicListItem(id: scenic.id, name: scenic.name)) } } return result } /// 将用户资料接口响应转换为全局账号资料。 private func makeProfile(from userInfo: UserInfoResponse, fallback: AccountProfile?) -> AccountProfile { AccountProfile( userId: fallback?.userId ?? "", displayName: nonEmpty(userInfo.nickname) ?? nonEmpty(userInfo.realName) ?? fallback?.displayName ?? "未设置昵称", phone: nonEmpty(userInfo.phone) ?? fallback?.phone, avatarURL: nonEmpty(userInfo.avatar) ?? fallback?.avatarURL ) } /// 去除空白字符后返回非空字符串。 private func nonEmpty(_ value: String?) -> String? { let text = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return text.isEmpty ? nil : text } }