修复账号切换后「我的」页当前账号展示错误,并统一举报摄影师首页 URI。
切换账号时以用户选中账号为准写入展示标题与业务作用域,避免沿用旧门店名称;同时将首页举报摄影师入口 URI 对齐为 report_photographer。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -63,11 +63,22 @@ struct BusinessScope: Codable, Equatable, Identifiable {
|
||||
final class AccountContext: ObservableObject {
|
||||
@Published private(set) var profile: AccountProfile?
|
||||
@Published private(set) var accountType: String?
|
||||
@Published private(set) var businessUserId: Int?
|
||||
/// 当前账号在「我的」页面展示的标题,来源于登录/切换时选中的账号名称。
|
||||
@Published private(set) var currentAccountDisplayTitle: String?
|
||||
@Published private(set) var scenicScopes: [BusinessScope] = []
|
||||
@Published private(set) var storeScopes: [BusinessScope] = []
|
||||
@Published var currentScenic: BusinessScope?
|
||||
@Published var currentStore: BusinessScope?
|
||||
|
||||
/// 当前账号标识,与 AccountSwitchAccount.id 规则一致。
|
||||
var currentAccountIdentifier: String? {
|
||||
guard let businessUserId, businessUserId > 0 else { return nil }
|
||||
let type = accountType?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
guard !type.isEmpty else { return nil }
|
||||
return "\(type)_\(businessUserId)"
|
||||
}
|
||||
|
||||
/// 当前账号缓存前缀,与 Android 的账号级缓存作用域保持一致。
|
||||
var accountCachePrefix: String? {
|
||||
let userId = profile?.userId.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
@ -77,9 +88,20 @@ final class AccountContext: ObservableObject {
|
||||
}
|
||||
|
||||
/// 应用登录成功后写入账号资料。
|
||||
func applyLogin(profile: AccountProfile? = nil, accountType: String? = nil) {
|
||||
func applyLogin(
|
||||
profile: AccountProfile? = nil,
|
||||
accountType: String? = nil,
|
||||
businessUserId: Int? = nil,
|
||||
displayTitle: String? = nil
|
||||
) {
|
||||
self.profile = profile
|
||||
self.accountType = accountType?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let businessUserId, businessUserId > 0 {
|
||||
self.businessUserId = businessUserId
|
||||
}
|
||||
if let displayTitle = normalizedText(displayTitle) {
|
||||
currentAccountDisplayTitle = displayTitle
|
||||
}
|
||||
}
|
||||
|
||||
/// 替换当前账号资料,通常用于用户信息刷新后同步全局展示。
|
||||
@ -92,10 +114,29 @@ final class AccountContext: ObservableObject {
|
||||
scenic: [BusinessScope],
|
||||
stores: [BusinessScope],
|
||||
currentScenicId: Int? = nil,
|
||||
currentStoreId: Int? = nil
|
||||
currentStoreId: Int? = nil,
|
||||
explicitSelection: Bool = false
|
||||
) {
|
||||
scenicScopes = scenic
|
||||
storeScopes = stores
|
||||
|
||||
if explicitSelection {
|
||||
currentScenic = currentScenicId.flatMap { id in
|
||||
scenic.first { $0.id == id }
|
||||
} ?? scenic.first
|
||||
if let storeId = currentStoreId, storeId > 0 {
|
||||
currentStore = resolvedStore(
|
||||
in: stores,
|
||||
currentScenic: currentScenic,
|
||||
preferredStoreId: storeId,
|
||||
allowFallbackToFirst: false
|
||||
)
|
||||
} else {
|
||||
currentStore = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
currentScenic = currentScenicId.flatMap { id in
|
||||
scenic.first { $0.id == id }
|
||||
} ?? currentScenic.flatMap { current in
|
||||
@ -104,7 +145,8 @@ final class AccountContext: ObservableObject {
|
||||
currentStore = resolvedStore(
|
||||
in: stores,
|
||||
currentScenic: currentScenic,
|
||||
preferredStoreId: currentStoreId ?? currentStore?.id
|
||||
preferredStoreId: currentStoreId ?? currentStore?.id,
|
||||
allowFallbackToFirst: true
|
||||
)
|
||||
}
|
||||
|
||||
@ -115,7 +157,8 @@ final class AccountContext: ObservableObject {
|
||||
currentStore = resolvedStore(
|
||||
in: storeScopes,
|
||||
currentScenic: scenic,
|
||||
preferredStoreId: currentStore?.id
|
||||
preferredStoreId: currentStore?.id,
|
||||
allowFallbackToFirst: true
|
||||
)
|
||||
}
|
||||
|
||||
@ -134,6 +177,8 @@ final class AccountContext: ObservableObject {
|
||||
func reset() {
|
||||
profile = nil
|
||||
accountType = nil
|
||||
businessUserId = nil
|
||||
currentAccountDisplayTitle = nil
|
||||
scenicScopes = []
|
||||
storeScopes = []
|
||||
currentScenic = nil
|
||||
@ -144,7 +189,8 @@ final class AccountContext: ObservableObject {
|
||||
private func resolvedStore(
|
||||
in stores: [BusinessScope],
|
||||
currentScenic: BusinessScope?,
|
||||
preferredStoreId: Int?
|
||||
preferredStoreId: Int?,
|
||||
allowFallbackToFirst: Bool
|
||||
) -> BusinessScope? {
|
||||
let scenicId = currentScenic?.id
|
||||
let storesForScenic = stores.filter { store in
|
||||
@ -155,6 +201,13 @@ final class AccountContext: ObservableObject {
|
||||
let store = storesForScenic.first(where: { $0.id == preferredStoreId }) {
|
||||
return store
|
||||
}
|
||||
guard allowFallbackToFirst else { return nil }
|
||||
return storesForScenic.first
|
||||
}
|
||||
|
||||
/// 返回去空白后的非空文本。
|
||||
private func normalizedText(_ value: String?) -> String? {
|
||||
let text = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
return text.isEmpty ? nil : text
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,7 +26,8 @@ struct AccountContextLoader {
|
||||
cachedCurrentRoleCode: String? = nil,
|
||||
cachedLegacyRoleId: Int? = nil,
|
||||
cachedCurrentScenicId: Int? = nil,
|
||||
cachedCurrentStoreId: Int? = nil
|
||||
cachedCurrentStoreId: Int? = nil,
|
||||
explicitScopeSelection: Bool = false
|
||||
) async throws {
|
||||
try await restorePermissions(
|
||||
permissionContext: permissionContext,
|
||||
@ -35,7 +36,8 @@ struct AccountContextLoader {
|
||||
cachedCurrentRoleCode: cachedCurrentRoleCode,
|
||||
cachedLegacyRoleId: cachedLegacyRoleId,
|
||||
cachedCurrentScenicId: cachedCurrentScenicId,
|
||||
cachedCurrentStoreId: cachedCurrentStoreId
|
||||
cachedCurrentStoreId: cachedCurrentStoreId,
|
||||
explicitScopeSelection: explicitScopeSelection
|
||||
)
|
||||
try await refreshSupplementalContext(
|
||||
accountContext: accountContext,
|
||||
@ -43,7 +45,8 @@ struct AccountContextLoader {
|
||||
profileAPI: profileAPI,
|
||||
accountContextAPI: accountContextAPI,
|
||||
cachedCurrentScenicId: cachedCurrentScenicId,
|
||||
cachedCurrentStoreId: cachedCurrentStoreId
|
||||
cachedCurrentStoreId: cachedCurrentStoreId,
|
||||
explicitScopeSelection: explicitScopeSelection
|
||||
)
|
||||
}
|
||||
|
||||
@ -55,7 +58,8 @@ struct AccountContextLoader {
|
||||
cachedCurrentRoleCode: String? = nil,
|
||||
cachedLegacyRoleId: Int? = nil,
|
||||
cachedCurrentScenicId: Int? = nil,
|
||||
cachedCurrentStoreId: Int? = nil
|
||||
cachedCurrentStoreId: Int? = nil,
|
||||
explicitScopeSelection: Bool = false
|
||||
) async throws {
|
||||
let rolePermissions = try await accountContextAPI.rolePermissions()
|
||||
permissionContext.replaceRolePermissions(
|
||||
@ -70,7 +74,8 @@ struct AccountContextLoader {
|
||||
scenic: roleScenicScopes,
|
||||
stores: accountContext.storeScopes,
|
||||
currentScenicId: cachedCurrentScenicId,
|
||||
currentStoreId: cachedCurrentStoreId
|
||||
currentStoreId: cachedCurrentStoreId,
|
||||
explicitSelection: explicitScopeSelection
|
||||
)
|
||||
}
|
||||
|
||||
@ -81,7 +86,8 @@ struct AccountContextLoader {
|
||||
profileAPI: UserProfileServing,
|
||||
accountContextAPI: AccountContextServing,
|
||||
cachedCurrentScenicId: Int? = nil,
|
||||
cachedCurrentStoreId: Int? = nil
|
||||
cachedCurrentStoreId: Int? = nil,
|
||||
explicitScopeSelection: Bool = false
|
||||
) async throws {
|
||||
let userInfo = try await profileAPI.userInfo()
|
||||
let scenicResponse = await loadScenicList(
|
||||
@ -94,19 +100,57 @@ struct AccountContextLoader {
|
||||
permissionContext: permissionContext,
|
||||
scenicResponse: scenicResponse
|
||||
)
|
||||
let storeScopes = storesResponse?.list.map { store in
|
||||
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: cachedCurrentScenicId ?? accountContext.currentScenic?.id,
|
||||
currentStoreId: cachedCurrentStoreId ?? accountContext.currentStore?.id
|
||||
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,
|
||||
|
||||
@ -52,7 +52,8 @@ final class AuthSessionCoordinator {
|
||||
accountContext: AccountContext,
|
||||
permissionContext: PermissionContext,
|
||||
profileAPI: UserProfileServing,
|
||||
accountContextAPI: AccountContextServing
|
||||
accountContextAPI: AccountContextServing,
|
||||
selectedAccount: AccountSwitchAccount? = nil
|
||||
) async throws {
|
||||
let token = response.token.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
try tokenStore.save(token)
|
||||
@ -62,9 +63,22 @@ final class AuthSessionCoordinator {
|
||||
let profile = response.primaryProfile
|
||||
let scenicScopes = response.scenicScopes
|
||||
let storeScopes = response.storeScopes
|
||||
let identity = response.currentAccountIdentity
|
||||
accountContext.applyLogin(profile: profile, accountType: identity?.accountType)
|
||||
accountContext.replaceScopes(scenic: scenicScopes, stores: storeScopes)
|
||||
let identity = resolvedIdentity(from: response, selectedAccount: selectedAccount)
|
||||
let scopeSelection = selectedAccount?.preferredScopeSelection ?? response.preferredScopeSelection
|
||||
let displayTitle = nonEmpty(selectedAccount?.title) ?? response.currentAccountDisplayTitle
|
||||
accountContext.applyLogin(
|
||||
profile: profile,
|
||||
accountType: identity?.accountType,
|
||||
businessUserId: identity?.businessUserId,
|
||||
displayTitle: displayTitle
|
||||
)
|
||||
accountContext.replaceScopes(
|
||||
scenic: scenicScopes,
|
||||
stores: storeScopes,
|
||||
currentScenicId: scopeSelection.scenicId,
|
||||
currentStoreId: scopeSelection.storeId,
|
||||
explicitSelection: true
|
||||
)
|
||||
appSession.beginRestoring(token: token)
|
||||
|
||||
do {
|
||||
@ -74,8 +88,9 @@ final class AuthSessionCoordinator {
|
||||
profileAPI: profileAPI,
|
||||
accountContextAPI: accountContextAPI,
|
||||
cachedCurrentRoleCode: response.currentAccountRoleCode,
|
||||
cachedCurrentScenicId: accountContext.currentScenic?.id,
|
||||
cachedCurrentStoreId: accountContext.currentStore?.id
|
||||
cachedCurrentScenicId: scopeSelection.scenicId,
|
||||
cachedCurrentStoreId: scopeSelection.storeId,
|
||||
explicitScopeSelection: true
|
||||
)
|
||||
} catch {
|
||||
if APIError.isAuthenticationExpired(error) {
|
||||
@ -153,6 +168,7 @@ final class AuthSessionCoordinator {
|
||||
profile: profile,
|
||||
accountType: accountType,
|
||||
businessUserId: businessUserId,
|
||||
currentAccountDisplayTitle: accountContext.currentAccountDisplayTitle,
|
||||
currentRoleCode: permissionContext?.currentAppRole?.rawValue ?? currentRoleCode,
|
||||
scenicScopes: accountContext.scenicScopes,
|
||||
storeScopes: accountContext.storeScopes,
|
||||
@ -162,6 +178,17 @@ final class AuthSessionCoordinator {
|
||||
)
|
||||
}
|
||||
|
||||
/// 优先使用用户明确选择的账号身份,避免 set-user 响应仍携带旧 is_current 时解析错误。
|
||||
private func resolvedIdentity(
|
||||
from response: V9AuthResponse,
|
||||
selectedAccount: AccountSwitchAccount?
|
||||
) -> (accountType: String, businessUserId: Int)? {
|
||||
if let selectedAccount, selectedAccount.businessUserId > 0 {
|
||||
return (selectedAccount.accountType, selectedAccount.businessUserId)
|
||||
}
|
||||
return response.currentAccountIdentity.map { ($0.accountType, $0.businessUserId) }
|
||||
}
|
||||
|
||||
/// 将用户资料接口响应转换为全局账号资料。
|
||||
private func makeProfile(from userInfo: UserInfoResponse, fallback: AccountProfile?) -> AccountProfile {
|
||||
AccountProfile(
|
||||
@ -185,27 +212,3 @@ struct LoginPreferences: Equatable {
|
||||
let privacyAgreementAccepted: Bool
|
||||
}
|
||||
|
||||
private extension V9AuthResponse {
|
||||
/// 当前账号身份实体,表示登录后实际选中的业务账号。
|
||||
struct CurrentAccountIdentity {
|
||||
let accountType: String
|
||||
let businessUserId: Int
|
||||
}
|
||||
|
||||
/// 提取当前账号身份,优先使用后端标记的 isCurrent 账号。
|
||||
var currentAccountIdentity: CurrentAccountIdentity? {
|
||||
if let storeUser = storeUsers.first(where: \.isCurrent) ?? storeUsers.first {
|
||||
return CurrentAccountIdentity(
|
||||
accountType: storeUser.accountType,
|
||||
businessUserId: storeUser.businessUserId
|
||||
)
|
||||
}
|
||||
if let scenicUser = scenicUsers.first(where: \.isCurrent) ?? scenicUsers.first {
|
||||
return CurrentAccountIdentity(
|
||||
accountType: scenicUser.accountType,
|
||||
businessUserId: scenicUser.businessUserId
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,12 +89,18 @@ final class SessionBootstrapper {
|
||||
/// 将缓存快照恢复到账号上下文。
|
||||
private func restoreCachedSnapshot(to accountContext: AccountContext) -> AccountSnapshot? {
|
||||
guard let snapshot = snapshotStore.load() else { return nil }
|
||||
accountContext.applyLogin(profile: snapshot.profile, accountType: snapshot.accountType)
|
||||
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
|
||||
currentStoreId: snapshot.currentStoreId,
|
||||
explicitSelection: true
|
||||
)
|
||||
return snapshot
|
||||
}
|
||||
@ -198,6 +204,7 @@ final class SessionBootstrapper {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user