升级 AppStore 缓存 Key 并在覆盖安装时强制重新登录。

统一账号作用域 Key 对齐 Android,新增缓存 schema 迁移清理旧数据,并同步更新相关模块与单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-14 09:35:55 +08:00
parent 05804ba7d6
commit c083f1d4b3
31 changed files with 852 additions and 123 deletions

View File

@ -44,13 +44,17 @@ final class AppSessionStore {
case phone = "key_in_phone"
case accountType = "key_in_account_type"
case accountDisplayName = "key_in_account_display_name"
}
private enum AccountKey: String, CaseIterable {
case roleCode = "key_in_role_code"
case roleName = "key_in_role_name"
case currentScenicId = "key_in_current_scenic_id"
case currentScenicName = "key_in_current_scenic_name"
case currentStoreId = "key_in_current_store_id"
}
private static let currentStoreIdSuffix = "scenic_store_id"
private let defaults: UserDefaults
/// 使 UserDefaults
@ -126,41 +130,74 @@ final class AppSessionStore {
///
var roleCode: String {
get { string(for: .roleCode) }
set { defaults.set(newValue, forKey: Key.roleCode.rawValue) }
get { accountString(for: .roleCode) }
set { setAccountString(newValue, for: .roleCode) }
}
///
var roleName: String {
get { string(for: .roleName) }
set { defaults.set(newValue, forKey: Key.roleName.rawValue) }
get { accountString(for: .roleName) }
set { setAccountString(newValue, for: .roleName) }
}
/// ID0
var currentScenicId: Int {
get { Int(string(for: .currentScenicId)) ?? 0 }
set { defaults.set(newValue > 0 ? String(newValue) : "", forKey: Key.currentScenicId.rawValue) }
get { Int(accountString(for: .currentScenicId)) ?? 0 }
set { setAccountString(newValue > 0 ? String(newValue) : "", for: .currentScenicId) }
}
///
var currentScenicName: String {
get { string(for: .currentScenicName) }
set { defaults.set(newValue, forKey: Key.currentScenicName.rawValue) }
get { accountString(for: .currentScenicName) }
set { setAccountString(newValue, for: .currentScenicName) }
}
/// ID0
var currentStoreId: Int {
get { defaults.integer(forKey: Key.currentStoreId.rawValue) }
set { defaults.set(newValue, forKey: Key.currentStoreId.rawValue) }
get {
guard let key = scenicScopedKey(Self.currentStoreIdSuffix) else { return 0 }
return defaults.integer(forKey: key)
}
set {
guard let key = scenicScopedKey(Self.currentStoreIdSuffix) else { return }
defaults.set(newValue, forKey: key)
}
}
/// Android `getAccountCachePrefix`
var accountCachePrefix: String {
let uid = userId.trimmingCharacters(in: .whitespacesAndNewlines)
let type = accountType.rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
if uid.isEmpty { return "guest" }
if type.isEmpty { return uid }
return "\(uid)_\(type)"
guard !uid.isEmpty, !type.isEmpty else { return "" }
return "\(type)_\(uid)"
}
/// Key nil
func accountScopedKey(_ suffix: String) -> String? {
let scope = accountCachePrefix
let normalizedSuffix = suffix.trimmingCharacters(in: CharacterSet(charactersIn: "_"))
guard !scope.isEmpty, !normalizedSuffix.isEmpty else { return nil }
return "\(scope)_\(normalizedSuffix)"
}
/// Key nil
func scenicScopedKey(_ suffix: String, scenicId: Int? = nil) -> String? {
let resolvedScenicId = scenicId ?? currentScenicId
let normalizedSuffix = suffix.trimmingCharacters(in: CharacterSet(charactersIn: "_"))
guard !accountCachePrefix.isEmpty, resolvedScenicId > 0, !normalizedSuffix.isEmpty else { return nil }
return "\(accountCachePrefix)_scenic_\(resolvedScenicId)_\(normalizedSuffix)"
}
/// Key nil
func scenicSpotScopedKey(_ suffix: String, spotId: String, scenicId: Int? = nil) -> String? {
let resolvedScenicId = scenicId ?? currentScenicId
let normalizedSpotId = spotId.trimmingCharacters(in: .whitespacesAndNewlines)
let normalizedSuffix = suffix.trimmingCharacters(in: CharacterSet(charactersIn: "_"))
guard !accountCachePrefix.isEmpty,
resolvedScenicId > 0,
!normalizedSpotId.isEmpty,
!normalizedSuffix.isEmpty else { return nil }
return "\(accountCachePrefix)_scenic_\(resolvedScenicId)_spot_\(normalizedSpotId)_\(normalizedSuffix)"
}
///
@ -211,23 +248,41 @@ final class AppSessionStore {
}
///
func clearAuthenticatedSession() {
token = ""
userId = ""
userName = ""
realName = ""
avatar = ""
phone = ""
accountType = .unknown("")
accountDisplayName = ""
roleCode = ""
roleName = ""
currentScenicId = 0
currentScenicName = ""
currentStoreId = 0
func clearAuthenticatedSession(accountScope: String) {
if !accountScope.isEmpty {
AccountKey.allCases.forEach {
defaults.removeObject(forKey: "\(accountScope)_\($0.rawValue)")
}
let storePrefix = "\(accountScope)_scenic_"
let storeSuffix = "_\(Self.currentStoreIdSuffix)"
defaults.dictionaryRepresentation().keys
.filter { $0.hasPrefix(storePrefix) && $0.hasSuffix(storeSuffix) }
.forEach(defaults.removeObject(forKey:))
}
[
Key.token,
.userId,
.userName,
.realName,
.avatar,
.phone,
.accountType,
.accountDisplayName,
].forEach { defaults.removeObject(forKey: $0.rawValue) }
}
private func string(for key: Key) -> String {
defaults.string(forKey: key.rawValue) ?? ""
}
private func accountString(for key: AccountKey) -> String {
guard let scopedKey = accountScopedKey(key.rawValue) else { return "" }
return defaults.string(forKey: scopedKey) ?? ""
}
private func setAccountString(_ value: String, for key: AccountKey) {
guard let scopedKey = accountScopedKey(key.rawValue) else { return }
defaults.set(value, forKey: scopedKey)
}
}