// // AppStore.swift // suixinkan // import Foundation /// 本地会话存储,与 Android `AppStoreDataSource` 对齐。 final class AppStore { static let shared = AppStore() private enum Key { static let token = "key_in_token" static let lastLoginUsername = "key_last_login_username" static let privacyAgreementAccepted = "key_privacy_agreement_accepted" static let userId = "key_in_user_id" static let userName = "key_in_user_name" static let realName = "key_in_real_name" static let avatar = "key_in_avatar" static let phone = "key_in_phone" static let accountType = "key_in_account_type" static let accountDisplayName = "key_in_account_display_name" static let roleCode = "key_in_role_code" static let roleName = "key_in_role_name" static let currentScenicId = "key_in_current_scenic_id" static let currentScenicName = "key_in_current_scenic_name" static let currentStoreId = "key_in_current_store_id" static let legacyRoleId = "key_in_role_id" static let rolePermissionList = "key_role_permission_list" static let permissionItems = "key_in_permission" static let roleScenicList = "key_current_role_scenic_list" static let onlineStatus = "key_online_status" static let lastLocationReportTime = "key_last_location_report_time" static let locationReminderMinutes = "key_location_reminder_minutes" static let isOpenReceiveVoice = "key_is_open_receive_voice" static let scenicQueuePunchSpotId = "_scenic_queue_punch_spot_id" static let scenicQueuePunchSpotName = "_scenic_queue_punch_spot_name" static let scenicQueueRemark = "_scenic_queue_remark" static let scenicQueueSettingsSnapshot = "_scenic_queue_settings_snapshot" static let scenicQueueOfflineTTS = "_scenic_queue_offline_tts_v1" static let scenicQueuePresetVoices = "_scenic_queue_preset_voices_v1" } private let defaults: UserDefaults init(defaults: UserDefaults = .standard) { self.defaults = defaults } /// 登录 Token。 var token: String { get { defaults.string(forKey: Key.token) ?? "" } set { defaults.set(newValue, forKey: Key.token) } } /// 上次登录手机号,用于登录页恢复。 var lastLoginUsername: String? { get { defaults.string(forKey: Key.lastLoginUsername) } set { if let newValue { defaults.set(newValue, forKey: Key.lastLoginUsername) } else { defaults.removeObject(forKey: Key.lastLoginUsername) } } } /// 是否已同意隐私协议。 var privacyAgreementAccepted: Bool { get { defaults.bool(forKey: Key.privacyAgreementAccepted) } set { defaults.set(newValue, forKey: Key.privacyAgreementAccepted) } } var userId: String { get { defaults.string(forKey: Key.userId) ?? "" } set { defaults.set(newValue, forKey: Key.userId) } } var userName: String { get { defaults.string(forKey: Key.userName) ?? "" } set { defaults.set(newValue, forKey: Key.userName) } } var realName: String { get { defaults.string(forKey: Key.realName) ?? "" } set { defaults.set(newValue, forKey: Key.realName) } } var avatar: String { get { defaults.string(forKey: Key.avatar) ?? "" } set { defaults.set(newValue, forKey: Key.avatar) } } var phone: String { get { defaults.string(forKey: Key.phone) ?? "" } set { defaults.set(newValue, forKey: Key.phone) } } var accountType: String { get { defaults.string(forKey: Key.accountType) ?? "" } set { defaults.set(newValue, forKey: Key.accountType) } } var accountDisplayName: String { get { defaults.string(forKey: Key.accountDisplayName) ?? "" } set { defaults.set(newValue, forKey: Key.accountDisplayName) } } var roleCode: String { get { defaults.string(forKey: Key.roleCode) ?? "" } set { defaults.set(newValue, forKey: Key.roleCode) } } var roleName: String { get { defaults.string(forKey: Key.roleName) ?? "" } set { defaults.set(newValue, forKey: Key.roleName) } } var currentScenicId: Int { get { let value = defaults.string(forKey: Key.currentScenicId) ?? "" return Int(value) ?? 0 } set { defaults.set(newValue > 0 ? String(newValue) : "", forKey: Key.currentScenicId) } } var currentScenicName: String { get { defaults.string(forKey: Key.currentScenicName) ?? "" } set { defaults.set(newValue, forKey: Key.currentScenicName) } } var currentStoreId: Int { get { defaults.integer(forKey: Key.currentStoreId) } set { defaults.set(newValue, forKey: Key.currentStoreId) } } /// 旧版 role_id,用于权限匹配兜底。 var legacyRoleId: Int { get { defaults.integer(forKey: accountScopedKey(Key.legacyRoleId)) } set { defaults.set(newValue, forKey: accountScopedKey(Key.legacyRoleId)) } } /// 是否在线(位置上报会话)。 var onlineStatus: Bool { get { defaults.bool(forKey: accountScopedKey(Key.onlineStatus)) } set { defaults.set(newValue, forKey: accountScopedKey(Key.onlineStatus)) } } /// 上次位置上报时间戳(毫秒)。 var lastLocationReportTime: Int64 { get { Int64(defaults.integer(forKey: accountScopedKey(Key.lastLocationReportTime))) } set { defaults.set(Int(newValue), forKey: accountScopedKey(Key.lastLocationReportTime)) } } /// 位置超时提前提醒分钟数,0 表示不提醒。 var locationReminderMinutes: Int { get { defaults.integer(forKey: accountScopedKey(Key.locationReminderMinutes)) } set { defaults.set(newValue, forKey: accountScopedKey(Key.locationReminderMinutes)) } } /// 收款到账是否开启语音播报,对齐 Android `getIsOpenReceiveVoice`。 var isOpenReceiveVoice: Bool { get { defaults.bool(forKey: accountScopedKey(Key.isOpenReceiveVoice)) } set { defaults.set(newValue, forKey: accountScopedKey(Key.isOpenReceiveVoice)) } } /// 排队管理已保存打卡点 ID。 var scenicQueuePunchSpotId: String { get { defaults.string(forKey: accountScopedKey(Key.scenicQueuePunchSpotId)) ?? "" } set { defaults.set(newValue, forKey: accountScopedKey(Key.scenicQueuePunchSpotId)) } } /// 排队管理已保存打卡点名称。 var scenicQueuePunchSpotName: String { get { defaults.string(forKey: accountScopedKey(Key.scenicQueuePunchSpotName)) ?? "" } set { defaults.set(newValue, forKey: accountScopedKey(Key.scenicQueuePunchSpotName)) } } /// 排队管理自定义播报文案。 var scenicQueueRemark: String { get { defaults.string(forKey: accountScopedKey(Key.scenicQueueRemark)) ?? "" } set { defaults.set(newValue, forKey: accountScopedKey(Key.scenicQueueRemark)) } } /// 排队语音是否优先使用离线 TTS。 var scenicQueueUseOfflineTTS: Bool { get { defaults.bool(forKey: accountScopedKey(Key.scenicQueueOfflineTTS)) } set { defaults.set(newValue, forKey: accountScopedKey(Key.scenicQueueOfflineTTS)) } } /// 当前账号缓存前缀,对齐 Android `getAccountCachePrefix`。 var accountCachePrefix: String { let uid = userId.trimmingCharacters(in: .whitespacesAndNewlines) let type = accountType.trimmingCharacters(in: .whitespacesAndNewlines) if uid.isEmpty { return "guest" } if type.isEmpty { return uid } return "\(uid)_\(type)" } /// 当前解析后的业务角色。 var currentAppRole: AppRoleCode? { AppRoleCode.fromCode(roleCode) ?? AppRoleCode.fromRoleName(roleName) } /// 当前是否为摄影师角色。 var isPhotographerRole: Bool { if let role = currentAppRole { return role.isPhotographer } let name = roleName.trimmingCharacters(in: .whitespacesAndNewlines) return name == "摄影师" } /// Token 非空视为已登录。 var isLoggedIn: Bool { !token.isEmpty } /// 保存登录 Token。 func saveToken(_ token: String) { self.token = token } /// 从账号切换实体写入会话快照。 func applyAccount(_ account: AccountSwitchAccount) { userId = String(account.businessUserId) accountType = 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 logout() { clearPermissionSnapshot() token = "" userId = "" userName = "" realName = "" avatar = "" phone = "" accountType = "" accountDisplayName = "" roleCode = "" roleName = "" currentScenicId = 0 currentScenicName = "" currentStoreId = 0 } /// 保存完整 role-permission 列表。 func saveRolePermissionList(_ list: [RolePermissionResponse]) { guard let data = try? JSONEncoder().encode(list) else { return } defaults.set(data, forKey: accountScopedKey(Key.rolePermissionList)) } /// 读取完整 role-permission 列表。 func rolePermissionList() -> [RolePermissionResponse] { guard let data = defaults.data(forKey: accountScopedKey(Key.rolePermissionList)), let list = try? JSONDecoder().decode([RolePermissionResponse].self, from: data) else { return [] } return list } /// 保存当前角色扁平权限。 func savePermissionItems(_ items: [HomePermissionItem]) { guard let data = try? JSONEncoder().encode(items) else { return } defaults.set(data, forKey: accountScopedKey(Key.permissionItems)) } /// 读取当前角色扁平权限。 func permissionItems() -> [HomePermissionItem] { guard let data = defaults.data(forKey: accountScopedKey(Key.permissionItems)), let items = try? JSONDecoder().decode([HomePermissionItem].self, from: data) else { return [] } return items } /// 保存当前角色可选景区列表。 func saveRoleScenicList(_ scenicList: [ScenicInfo]) { guard let data = try? JSONEncoder().encode(scenicList) else { return } defaults.set(data, forKey: accountScopedKey(Key.roleScenicList)) } /// 读取当前角色可选景区列表。 func roleScenicList() -> [ScenicInfo] { guard let data = defaults.data(forKey: accountScopedKey(Key.roleScenicList)), let list = try? JSONDecoder().decode([ScenicInfo].self, from: data) else { return [] } return list } /// 在 role-permission 列表中匹配当前账号角色。 func matchRolePermissionItem(in list: [RolePermissionResponse]) -> RolePermissionResponse? { RolePermissionMatcher.match( in: list, roleCode: roleCode, roleName: roleName, legacyRoleId: legacyRoleId ) } /// 清除上次位置上报时间。 func clearLastLocationReportTime() { defaults.removeObject(forKey: accountScopedKey(Key.lastLocationReportTime)) } /// 是否已经保存排队打卡点。 func hasScenicQueuePunchSpotSaved() -> Bool { let id = scenicQueuePunchSpotId.trimmingCharacters(in: .whitespacesAndNewlines) return Int64(id).map { $0 > 0 } ?? false } /// 保存排队设置快照。 func saveScenicQueueSettingsSnapshot(_ snapshot: ScenicQueueSettingsSnapshot) { guard let data = try? JSONEncoder().encode(snapshot) else { return } defaults.set(data, forKey: accountScopedKey(Key.scenicQueueSettingsSnapshot)) } /// 读取排队设置快照。 func scenicQueueSettingsSnapshot() -> ScenicQueueSettingsSnapshot? { guard let data = defaults.data(forKey: accountScopedKey(Key.scenicQueueSettingsSnapshot)) else { return nil } return try? JSONDecoder().decode(ScenicQueueSettingsSnapshot.self, from: data) } /// 保存排队预设播报文案,最多 5 条。 func saveScenicQueuePresetVoices(_ voices: [String]) { let normalized = voices .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } .prefix(5) defaults.set(Array(normalized), forKey: accountScopedKey(Key.scenicQueuePresetVoices)) } /// 读取排队预设播报文案。 func scenicQueuePresetVoices() -> [String] { defaults.stringArray(forKey: accountScopedKey(Key.scenicQueuePresetVoices)) ?? [] } /// 清除排队管理本地缓存。 func clearScenicQueueSnapshot() { [ Key.scenicQueuePunchSpotId, Key.scenicQueuePunchSpotName, Key.scenicQueueRemark, Key.scenicQueueSettingsSnapshot, Key.scenicQueueOfflineTTS, Key.scenicQueuePresetVoices, ].forEach { defaults.removeObject(forKey: accountScopedKey($0)) } } /// 清除权限与位置会话快照。 func clearPermissionSnapshot() { let prefix = accountScopedKey("") let keys = [ Key.rolePermissionList, Key.permissionItems, Key.roleScenicList, Key.onlineStatus, Key.lastLocationReportTime, Key.locationReminderMinutes, Key.isOpenReceiveVoice, Key.scenicQueuePunchSpotId, Key.scenicQueuePunchSpotName, Key.scenicQueueRemark, Key.scenicQueueSettingsSnapshot, Key.scenicQueueOfflineTTS, Key.scenicQueuePresetVoices, ] keys.forEach { defaults.removeObject(forKey: prefix + $0) } } private func accountScopedKey(_ key: String) -> String { "\(accountCachePrefix)_\(key)" } }