Files
suixinkan_uikit/suixinkan/DataStore/AppSessionStore.swift
汉秋 c083f1d4b3 升级 AppStore 缓存 Key 并在覆盖安装时强制重新登录。
统一账号作用域 Key 对齐 Android,新增缓存 schema 迁移清理旧数据,并同步更新相关模块与单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 09:35:55 +08:00

289 lines
10 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// AppSessionStore.swift
// suixinkan
//
import Foundation
///
enum AppAccountType: Equatable, Sendable {
case scenicUser
case storeUser
case unknown(String)
///
init(rawValue: String) {
switch rawValue {
case "scenic_user": self = .scenicUser
case "store_user": self = .storeUser
default: self = .unknown(rawValue)
}
}
///
var rawValue: String {
switch self {
case .scenicUser: "scenic_user"
case .storeUser: "store_user"
case let .unknown(value): value
}
}
}
///
final class AppSessionStore {
private enum Key: String {
case token = "key_in_token"
case lastLoginUsername = "key_last_login_username"
case privacyAgreementAccepted = "key_privacy_agreement_accepted"
case userId = "key_in_user_id"
case userName = "key_in_user_name"
case realName = "key_in_real_name"
case avatar = "key_in_avatar"
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"
}
private static let currentStoreIdSuffix = "scenic_store_id"
private let defaults: UserDefaults
/// 使 UserDefaults
init(defaults: UserDefaults) {
self.defaults = defaults
}
/// Token
var token: String {
get { string(for: .token) }
set { defaults.set(newValue, forKey: Key.token.rawValue) }
}
///
var lastLoginUsername: String? {
get { defaults.string(forKey: Key.lastLoginUsername.rawValue) }
set {
if let newValue {
defaults.set(newValue, forKey: Key.lastLoginUsername.rawValue)
} else {
defaults.removeObject(forKey: Key.lastLoginUsername.rawValue)
}
}
}
///
var privacyAgreementAccepted: Bool {
get { defaults.bool(forKey: Key.privacyAgreementAccepted.rawValue) }
set { defaults.set(newValue, forKey: Key.privacyAgreementAccepted.rawValue) }
}
/// ID
var userId: String {
get { string(for: .userId) }
set { defaults.set(newValue, forKey: Key.userId.rawValue) }
}
///
var userName: String {
get { string(for: .userName) }
set { defaults.set(newValue, forKey: Key.userName.rawValue) }
}
///
var realName: String {
get { string(for: .realName) }
set { defaults.set(newValue, forKey: Key.realName.rawValue) }
}
///
var avatar: String {
get { string(for: .avatar) }
set { defaults.set(newValue, forKey: Key.avatar.rawValue) }
}
///
var phone: String {
get { string(for: .phone) }
set { defaults.set(newValue, forKey: Key.phone.rawValue) }
}
///
var accountType: AppAccountType {
get { AppAccountType(rawValue: string(for: .accountType)) }
set { defaults.set(newValue.rawValue, forKey: Key.accountType.rawValue) }
}
///
var accountDisplayName: String {
get { string(for: .accountDisplayName) }
set { defaults.set(newValue, forKey: Key.accountDisplayName.rawValue) }
}
///
var roleCode: String {
get { accountString(for: .roleCode) }
set { setAccountString(newValue, for: .roleCode) }
}
///
var roleName: String {
get { accountString(for: .roleName) }
set { setAccountString(newValue, for: .roleName) }
}
/// ID0
var currentScenicId: Int {
get { Int(accountString(for: .currentScenicId)) ?? 0 }
set { setAccountString(newValue > 0 ? String(newValue) : "", for: .currentScenicId) }
}
///
var currentScenicName: String {
get { accountString(for: .currentScenicName) }
set { setAccountString(newValue, for: .currentScenicName) }
}
/// ID0
var currentStoreId: Int {
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)
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)"
}
///
var currentAppRole: AppRoleCode? {
AppRoleCode.fromCode(roleCode) ?? AppRoleCode.fromRoleName(roleName)
}
///
var isPhotographerRole: Bool {
if let role = currentAppRole {
return role.isPhotographer
}
return roleName.trimmingCharacters(in: .whitespacesAndNewlines) == "摄影师"
}
/// Token
var isLoggedIn: Bool {
!token.isEmpty
}
/// Token
func saveToken(_ token: String) {
self.token = token
}
///
func applyAccount(_ account: AccountSwitchAccount) {
userId = String(account.businessUserId)
accountType = AppAccountType(rawValue: account.accountType)
accountDisplayName = account.title
userName = account.subtitle
phone = account.phone
if !account.avatar.isEmpty {
avatar = account.avatar
}
currentScenicName = account.scenicName
currentScenicId = account.scenicId ?? 0
currentStoreId = account.storeId ?? 0
}
///
func applyUserInfo(_ info: UserInfoResponse) {
if !info.nickname.isEmpty { userName = info.nickname }
if !info.realName.isEmpty { realName = info.realName }
if !info.avatar.isEmpty { avatar = info.avatar }
if !info.phone.isEmpty { phone = info.phone }
if !info.roleName.isEmpty, roleName.isEmpty { roleName = info.roleName }
}
///
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)
}
}