// // 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) } } /// 当前景区 ID,0 表示未选择。 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) } } /// 当前店铺 ID,0 表示未选择。 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) } }