切换账号时以用户选中账号为准写入展示标题与业务作用域,避免沿用旧门店名称;同时将首页举报摄影师入口 URI 对齐为 report_photographer。 Co-authored-by: Cursor <cursoragent@cursor.com>
214 lines
8.6 KiB
Swift
214 lines
8.6 KiB
Swift
//
|
||
// 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,
|
||
explicitScopeSelection: Bool = false
|
||
) async throws {
|
||
try await restorePermissions(
|
||
permissionContext: permissionContext,
|
||
accountContext: accountContext,
|
||
accountContextAPI: accountContextAPI,
|
||
cachedCurrentRoleCode: cachedCurrentRoleCode,
|
||
cachedLegacyRoleId: cachedLegacyRoleId,
|
||
cachedCurrentScenicId: cachedCurrentScenicId,
|
||
cachedCurrentStoreId: cachedCurrentStoreId,
|
||
explicitScopeSelection: explicitScopeSelection
|
||
)
|
||
try await refreshSupplementalContext(
|
||
accountContext: accountContext,
|
||
permissionContext: permissionContext,
|
||
profileAPI: profileAPI,
|
||
accountContextAPI: accountContextAPI,
|
||
cachedCurrentScenicId: cachedCurrentScenicId,
|
||
cachedCurrentStoreId: cachedCurrentStoreId,
|
||
explicitScopeSelection: explicitScopeSelection
|
||
)
|
||
}
|
||
|
||
/// 冷启动阻塞阶段:仅恢复角色权限,确保首页菜单和权限控制可用。
|
||
func restorePermissions(
|
||
permissionContext: PermissionContext,
|
||
accountContext: AccountContext,
|
||
accountContextAPI: AccountContextServing,
|
||
cachedCurrentRoleCode: String? = nil,
|
||
cachedLegacyRoleId: Int? = nil,
|
||
cachedCurrentScenicId: Int? = nil,
|
||
cachedCurrentStoreId: Int? = nil,
|
||
explicitScopeSelection: Bool = false
|
||
) 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,
|
||
explicitSelection: explicitScopeSelection
|
||
)
|
||
}
|
||
|
||
/// 冷启动后台阶段:补齐用户资料、景区和门店上下文。
|
||
func refreshSupplementalContext(
|
||
accountContext: AccountContext,
|
||
permissionContext: PermissionContext,
|
||
profileAPI: UserProfileServing,
|
||
accountContextAPI: AccountContextServing,
|
||
cachedCurrentScenicId: Int? = nil,
|
||
cachedCurrentStoreId: Int? = nil,
|
||
explicitScopeSelection: Bool = false
|
||
) 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 fetchedStoreScopes = storesResponse?.list.map { store in
|
||
BusinessScope(id: store.id, name: store.name, kind: .store, parentScenicId: store.scenicId)
|
||
} ?? []
|
||
// 门店列表接口失败或为空时,保留登录/切换时已写入的门店作用域,避免门店账号展示回退成景区名。
|
||
let storeScopes = fetchedStoreScopes.isEmpty ? accountContext.storeScopes : fetchedStoreScopes
|
||
|
||
accountContext.replaceProfile(profile)
|
||
accountContext.replaceScopes(
|
||
scenic: scenicScopes,
|
||
stores: storeScopes,
|
||
currentScenicId: resolvedScenicId(
|
||
cachedCurrentScenicId: cachedCurrentScenicId,
|
||
accountContext: accountContext,
|
||
explicitScopeSelection: explicitScopeSelection
|
||
),
|
||
currentStoreId: resolvedStoreId(
|
||
cachedCurrentStoreId: cachedCurrentStoreId,
|
||
accountContext: accountContext,
|
||
explicitScopeSelection: explicitScopeSelection
|
||
),
|
||
explicitSelection: explicitScopeSelection
|
||
)
|
||
}
|
||
|
||
/// 解析刷新时应使用的景区 ID。
|
||
private func resolvedScenicId(
|
||
cachedCurrentScenicId: Int?,
|
||
accountContext: AccountContext,
|
||
explicitScopeSelection: Bool
|
||
) -> Int? {
|
||
if explicitScopeSelection {
|
||
return cachedCurrentScenicId ?? accountContext.currentScenic?.id
|
||
}
|
||
return cachedCurrentScenicId ?? accountContext.currentScenic?.id
|
||
}
|
||
|
||
/// 解析刷新时应使用的门店 ID;景区账号在显式选择模式下必须清空门店。
|
||
private func resolvedStoreId(
|
||
cachedCurrentStoreId: Int?,
|
||
accountContext: AccountContext,
|
||
explicitScopeSelection: Bool
|
||
) -> Int? {
|
||
if explicitScopeSelection {
|
||
if accountContext.accountType == V9ScenicUser.accountTypeValue {
|
||
return nil
|
||
}
|
||
return cachedCurrentStoreId
|
||
}
|
||
return 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<StoreItem>? {
|
||
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<Int>()
|
||
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
|
||
}
|
||
}
|