修复账号切换后「我的」页当前账号展示错误,并统一举报摄影师首页 URI。

切换账号时以用户选中账号为准写入展示标题与业务作用域,避免沿用旧门店名称;同时将首页举报摄影师入口 URI 对齐为 report_photographer。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-01 17:21:04 +08:00
parent fcb86d43f5
commit 5ab9b1b324
23 changed files with 419 additions and 92 deletions

View File

@ -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
}
}

View File

@ -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,

View File

@ -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
}
}

View File

@ -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,