新增 CooperationOrder 模块(双 Tab 列表、获客员绑定、主 Tab 扫码)、订单来源/带客单绑定及配套 API 测试;同步引入 roleCode 权限匹配与相关单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
121 lines
4.7 KiB
Swift
121 lines
4.7 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
|
||
) async throws {
|
||
async let userData = profileAPI.userInfo()
|
||
async let roleData = accountContextAPI.rolePermissions()
|
||
let (userInfo, rolePermissions) = try await (userData, roleData)
|
||
|
||
permissionContext.replaceRolePermissions(
|
||
rolePermissions,
|
||
currentRoleCode: cachedCurrentRoleCode,
|
||
cachedLegacyRoleId: cachedLegacyRoleId
|
||
)
|
||
|
||
let scenicResponse = await loadScenicList(accountContextAPI: accountContextAPI, rolePermissions: 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,
|
||
currentStoreId: cachedCurrentStoreId
|
||
)
|
||
}
|
||
|
||
/// 景区列表接口失败时,从角色权限中的景区数据去重构造兜底列表。
|
||
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
|
||
}
|
||
}
|