升级 AppStore 缓存 Key 并在覆盖安装时强制重新登录。
统一账号作用域 Key 对齐 Android,新增缓存 schema 迁移清理旧数据,并同步更新相关模块与单元测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -25,33 +25,53 @@ final class AppLocationStore {
|
||||
|
||||
/// 是否处于位置上报在线状态。
|
||||
var onlineStatus: Bool {
|
||||
get { defaults.bool(forKey: scopedKey(.onlineStatus)) }
|
||||
set { defaults.set(newValue, forKey: scopedKey(.onlineStatus)) }
|
||||
get {
|
||||
guard let key = scopedKey(.onlineStatus) else { return false }
|
||||
return defaults.bool(forKey: key)
|
||||
}
|
||||
set {
|
||||
guard let key = scopedKey(.onlineStatus) else { return }
|
||||
defaults.set(newValue, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
/// 上次位置上报时间戳,单位为毫秒。
|
||||
var lastReportTime: Int64 {
|
||||
get { Int64(defaults.integer(forKey: scopedKey(.lastReportTime))) }
|
||||
set { defaults.set(Int(newValue), forKey: scopedKey(.lastReportTime)) }
|
||||
get {
|
||||
guard let key = scopedKey(.lastReportTime) else { return 0 }
|
||||
return Int64(defaults.integer(forKey: key))
|
||||
}
|
||||
set {
|
||||
guard let key = scopedKey(.lastReportTime) else { return }
|
||||
defaults.set(Int(newValue), forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
/// 位置超时提前提醒分钟数,0 表示不提醒。
|
||||
var reminderMinutes: Int {
|
||||
get { defaults.integer(forKey: scopedKey(.reminderMinutes)) }
|
||||
set { defaults.set(newValue, forKey: scopedKey(.reminderMinutes)) }
|
||||
get {
|
||||
guard let key = scopedKey(.reminderMinutes) else { return 0 }
|
||||
return defaults.integer(forKey: key)
|
||||
}
|
||||
set {
|
||||
guard let key = scopedKey(.reminderMinutes) else { return }
|
||||
defaults.set(newValue, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
/// 清除上次位置上报时间。
|
||||
func clearLastReportTime() {
|
||||
defaults.removeObject(forKey: scopedKey(.lastReportTime))
|
||||
guard let key = scopedKey(.lastReportTime) else { return }
|
||||
defaults.removeObject(forKey: key)
|
||||
}
|
||||
|
||||
/// 清除当前账号的位置上报状态和偏好。
|
||||
func clear() {
|
||||
Key.allCases.forEach { defaults.removeObject(forKey: scopedKey($0)) }
|
||||
func clear(accountScope: String) {
|
||||
guard !accountScope.isEmpty else { return }
|
||||
Key.allCases.forEach { defaults.removeObject(forKey: "\(accountScope)_\($0.rawValue)") }
|
||||
}
|
||||
|
||||
private func scopedKey(_ key: Key) -> String {
|
||||
"\(session.accountCachePrefix)_\(key.rawValue)"
|
||||
private func scopedKey(_ key: Key) -> String? {
|
||||
session.accountScopedKey(key.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,16 +23,23 @@ final class AppPaymentStore {
|
||||
|
||||
/// 收款到账是否开启语音播报。
|
||||
var isOpenReceiveVoice: Bool {
|
||||
get { defaults.bool(forKey: scopedKey(.isOpenReceiveVoice)) }
|
||||
set { defaults.set(newValue, forKey: scopedKey(.isOpenReceiveVoice)) }
|
||||
get {
|
||||
guard let key = scopedKey(.isOpenReceiveVoice) else { return false }
|
||||
return defaults.bool(forKey: key)
|
||||
}
|
||||
set {
|
||||
guard let key = scopedKey(.isOpenReceiveVoice) else { return }
|
||||
defaults.set(newValue, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
/// 清除当前账号的收款偏好。
|
||||
func clear() {
|
||||
Key.allCases.forEach { defaults.removeObject(forKey: scopedKey($0)) }
|
||||
func clear(accountScope: String) {
|
||||
guard !accountScope.isEmpty else { return }
|
||||
Key.allCases.forEach { defaults.removeObject(forKey: "\(accountScope)_\($0.rawValue)") }
|
||||
}
|
||||
|
||||
private func scopedKey(_ key: Key) -> String {
|
||||
"\(session.accountCachePrefix)_\(key.rawValue)"
|
||||
private func scopedKey(_ key: Key) -> String? {
|
||||
session.accountScopedKey(key.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,8 +26,14 @@ final class AppPermissionStore {
|
||||
|
||||
/// 旧版 role_id,用于权限匹配兜底。
|
||||
var legacyRoleId: Int {
|
||||
get { defaults.integer(forKey: scopedKey(.legacyRoleId)) }
|
||||
set { defaults.set(newValue, forKey: scopedKey(.legacyRoleId)) }
|
||||
get {
|
||||
guard let key = scopedKey(.legacyRoleId) else { return 0 }
|
||||
return defaults.integer(forKey: key)
|
||||
}
|
||||
set {
|
||||
guard let key = scopedKey(.legacyRoleId) else { return }
|
||||
defaults.set(newValue, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存完整 role-permission 列表。
|
||||
@ -71,21 +77,24 @@ final class AppPermissionStore {
|
||||
}
|
||||
|
||||
/// 清除当前账号的全部权限缓存。
|
||||
func clear() {
|
||||
Key.allCases.forEach { defaults.removeObject(forKey: scopedKey($0)) }
|
||||
func clear(accountScope: String) {
|
||||
guard !accountScope.isEmpty else { return }
|
||||
Key.allCases.forEach { defaults.removeObject(forKey: "\(accountScope)_\($0.rawValue)") }
|
||||
}
|
||||
|
||||
private func save<Value: Encodable>(_ value: Value, for key: Key) {
|
||||
guard let data = try? JSONEncoder().encode(value) else { return }
|
||||
defaults.set(data, forKey: scopedKey(key))
|
||||
guard let scopedKey = scopedKey(key),
|
||||
let data = try? JSONEncoder().encode(value) else { return }
|
||||
defaults.set(data, forKey: scopedKey)
|
||||
}
|
||||
|
||||
private func load<Value: Decodable>(_ type: Value.Type, for key: Key) -> Value? {
|
||||
guard let data = defaults.data(forKey: scopedKey(key)) else { return nil }
|
||||
guard let scopedKey = scopedKey(key),
|
||||
let data = defaults.data(forKey: scopedKey) else { return nil }
|
||||
return try? JSONDecoder().decode(type, from: data)
|
||||
}
|
||||
|
||||
private func scopedKey(_ key: Key) -> String {
|
||||
"\(session.accountCachePrefix)_\(key.rawValue)"
|
||||
private func scopedKey(_ key: Key) -> String? {
|
||||
session.accountScopedKey(key.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,12 +9,12 @@ import Foundation
|
||||
final class AppScenicQueueStore {
|
||||
|
||||
private enum Key: String, CaseIterable {
|
||||
case punchSpotId = "_scenic_queue_punch_spot_id"
|
||||
case punchSpotName = "_scenic_queue_punch_spot_name"
|
||||
case remark = "_scenic_queue_remark"
|
||||
case settingsSnapshot = "_scenic_queue_settings_snapshot"
|
||||
case useOfflineTTS = "_scenic_queue_offline_tts_v1"
|
||||
case presetVoices = "_scenic_queue_preset_voices_v1"
|
||||
case punchSpotId = "scenic_queue_punch_spot_id"
|
||||
case punchSpotName = "scenic_queue_punch_spot_name"
|
||||
case remark = "scenic_queue_remark"
|
||||
case settingsSnapshot = "scenic_queue_settings_snapshot"
|
||||
case useOfflineTTS = "scenic_queue_offline_tts_v1"
|
||||
case presetVoices = "scenic_queue_preset_voices_v1"
|
||||
}
|
||||
|
||||
private let defaults: UserDefaults
|
||||
@ -28,26 +28,39 @@ final class AppScenicQueueStore {
|
||||
|
||||
/// 已保存打卡点 ID。
|
||||
var punchSpotId: String {
|
||||
get { string(for: .punchSpotId) }
|
||||
set { defaults.set(newValue, forKey: scopedKey(.punchSpotId)) }
|
||||
get {
|
||||
guard let key = scenicKey(.punchSpotId) else { return "" }
|
||||
return defaults.string(forKey: key) ?? ""
|
||||
}
|
||||
set {
|
||||
guard let key = scenicKey(.punchSpotId) else { return }
|
||||
defaults.set(newValue, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
/// 已保存打卡点名称。
|
||||
var punchSpotName: String {
|
||||
get { string(for: .punchSpotName) }
|
||||
set { defaults.set(newValue, forKey: scopedKey(.punchSpotName)) }
|
||||
get { spotString(for: .punchSpotName) }
|
||||
set { setSpotString(newValue, for: .punchSpotName) }
|
||||
}
|
||||
|
||||
/// 自定义播报文案。
|
||||
var remark: String {
|
||||
get { string(for: .remark) }
|
||||
set { defaults.set(newValue, forKey: scopedKey(.remark)) }
|
||||
get { spotString(for: .remark) }
|
||||
set { setSpotString(newValue, for: .remark) }
|
||||
}
|
||||
|
||||
/// 是否优先使用离线 TTS。
|
||||
var useOfflineTTS: Bool {
|
||||
get { defaults.bool(forKey: scopedKey(.useOfflineTTS)) }
|
||||
set { defaults.set(newValue, forKey: scopedKey(.useOfflineTTS)) }
|
||||
get {
|
||||
guard let key = scenicKey(.useOfflineTTS),
|
||||
defaults.object(forKey: key) != nil else { return true }
|
||||
return defaults.bool(forKey: key)
|
||||
}
|
||||
set {
|
||||
guard let key = scenicKey(.useOfflineTTS) else { return }
|
||||
defaults.set(newValue, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
/// 当前账号是否已经保存有效打卡点。
|
||||
@ -58,40 +71,61 @@ final class AppScenicQueueStore {
|
||||
|
||||
/// 保存排队设置快照。
|
||||
func saveSettingsSnapshot(_ snapshot: ScenicQueueSettingsSnapshot) {
|
||||
guard let data = try? JSONEncoder().encode(snapshot) else { return }
|
||||
defaults.set(data, forKey: scopedKey(.settingsSnapshot))
|
||||
guard let key = spotKey(.settingsSnapshot),
|
||||
let data = try? JSONEncoder().encode(snapshot) else { return }
|
||||
defaults.set(data, forKey: key)
|
||||
}
|
||||
|
||||
/// 读取排队设置快照。
|
||||
func settingsSnapshot() -> ScenicQueueSettingsSnapshot? {
|
||||
guard let data = defaults.data(forKey: scopedKey(.settingsSnapshot)) else { return nil }
|
||||
guard let key = spotKey(.settingsSnapshot),
|
||||
let data = defaults.data(forKey: key) else { return nil }
|
||||
return try? JSONDecoder().decode(ScenicQueueSettingsSnapshot.self, from: data)
|
||||
}
|
||||
|
||||
/// 保存排队预设播报文案,最多 5 条。
|
||||
func savePresetVoices(_ voices: [String]) {
|
||||
guard let key = spotKey(.presetVoices) else { return }
|
||||
let normalized = voices
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
.prefix(5)
|
||||
defaults.set(Array(normalized), forKey: scopedKey(.presetVoices))
|
||||
defaults.set(Array(normalized), forKey: key)
|
||||
}
|
||||
|
||||
/// 读取排队预设播报文案。
|
||||
func presetVoices() -> [String] {
|
||||
defaults.stringArray(forKey: scopedKey(.presetVoices)) ?? []
|
||||
guard let key = spotKey(.presetVoices) else { return [] }
|
||||
return defaults.stringArray(forKey: key) ?? []
|
||||
}
|
||||
|
||||
/// 清除当前账号的景区排队缓存。
|
||||
func clear() {
|
||||
Key.allCases.forEach { defaults.removeObject(forKey: scopedKey($0)) }
|
||||
func clear(accountScope: String) {
|
||||
guard !accountScope.isEmpty else { return }
|
||||
let scenicPrefix = "\(accountScope)_scenic_"
|
||||
let suffixes = Key.allCases.map { "_\($0.rawValue)" }
|
||||
defaults.dictionaryRepresentation().keys
|
||||
.filter { key in
|
||||
key.hasPrefix(scenicPrefix) && suffixes.contains(where: key.hasSuffix)
|
||||
}
|
||||
.forEach { defaults.removeObject(forKey: $0) }
|
||||
}
|
||||
|
||||
private func string(for key: Key) -> String {
|
||||
defaults.string(forKey: scopedKey(key)) ?? ""
|
||||
private func scenicKey(_ key: Key) -> String? {
|
||||
session.scenicScopedKey(key.rawValue)
|
||||
}
|
||||
|
||||
private func scopedKey(_ key: Key) -> String {
|
||||
"\(session.accountCachePrefix)_\(key.rawValue)"
|
||||
private func spotKey(_ key: Key) -> String? {
|
||||
session.scenicSpotScopedKey(key.rawValue, spotId: punchSpotId)
|
||||
}
|
||||
|
||||
private func spotString(for key: Key) -> String {
|
||||
guard let scopedKey = spotKey(key) else { return "" }
|
||||
return defaults.string(forKey: scopedKey) ?? ""
|
||||
}
|
||||
|
||||
private func setSpotString(_ value: String, for key: Key) {
|
||||
guard let scopedKey = spotKey(key) else { return }
|
||||
defaults.set(value, forKey: scopedKey)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) }
|
||||
}
|
||||
|
||||
/// 当前景区 ID,0 表示未选择。
|
||||
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) }
|
||||
}
|
||||
|
||||
/// 当前店铺 ID,0 表示未选择。
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@ final class AppStore {
|
||||
|
||||
/// 使用指定 UserDefaults 创建模块化应用存储。
|
||||
init(defaults: UserDefaults = .standard) {
|
||||
AppStoreCacheMigrator.migrateIfNeeded(defaults: defaults)
|
||||
let session = AppSessionStore(defaults: defaults)
|
||||
self.session = session
|
||||
permissions = AppPermissionStore(defaults: defaults, session: session)
|
||||
@ -38,10 +39,11 @@ final class AppStore {
|
||||
|
||||
/// 清除当前账号业务快照与登录态,保留隐私授权和上次登录用户名。
|
||||
func logout() {
|
||||
permissions.clear()
|
||||
location.clear()
|
||||
payment.clear()
|
||||
scenicQueue.clear()
|
||||
session.clearAuthenticatedSession()
|
||||
let accountScope = session.accountCachePrefix
|
||||
permissions.clear(accountScope: accountScope)
|
||||
location.clear(accountScope: accountScope)
|
||||
payment.clear(accountScope: accountScope)
|
||||
scenicQueue.clear(accountScope: accountScope)
|
||||
session.clearAuthenticatedSession(accountScope: accountScope)
|
||||
}
|
||||
}
|
||||
|
||||
116
suixinkan/DataStore/AppStoreCacheMigrator.swift
Normal file
116
suixinkan/DataStore/AppStoreCacheMigrator.swift
Normal file
@ -0,0 +1,116 @@
|
||||
//
|
||||
// AppStoreCacheMigrator.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 管理 AppStore 持久化结构版本,并在覆盖升级时清理不再兼容的旧缓存。
|
||||
enum AppStoreCacheMigrator {
|
||||
|
||||
/// 当前 AppStore 缓存结构版本。
|
||||
static let currentSchemaVersion = 1
|
||||
|
||||
/// AppStore 缓存结构版本标记。
|
||||
static let schemaVersionKey = "suixinkan.cache.schema.version"
|
||||
|
||||
private static let exactKeysToRemove: Set<String> = [
|
||||
// 旧线上会话、推送与首页缓存。
|
||||
"suixinkan.session.v1",
|
||||
"apns_device_token",
|
||||
"apns_uploaded_token",
|
||||
"home.common.menu.uris",
|
||||
"home.common.menu.android.baseline.v2",
|
||||
"home.routing.unknown.records",
|
||||
"payment.voice.broadcast.enabled",
|
||||
|
||||
// 重构版预发布阶段的全局会话字段。
|
||||
"key_in_token",
|
||||
"key_last_login_username",
|
||||
"key_privacy_agreement_accepted",
|
||||
"key_in_user_id",
|
||||
"key_in_user_name",
|
||||
"key_in_real_name",
|
||||
"key_in_avatar",
|
||||
"key_in_phone",
|
||||
"key_in_account_type",
|
||||
"key_in_account_display_name",
|
||||
"key_in_role_id",
|
||||
"key_in_role_code",
|
||||
"key_in_role_name",
|
||||
"key_in_current_scenic_id",
|
||||
"key_in_current_scenic_name",
|
||||
"key_in_current_store_id",
|
||||
|
||||
// 可能由早期实现写入的未分域业务字段。
|
||||
"key_role_permission_list",
|
||||
"key_in_permission",
|
||||
"key_current_role_scenic_list",
|
||||
"key_online_status",
|
||||
"key_last_location_report_time",
|
||||
"key_location_reminder_minutes",
|
||||
"key_is_open_receive_voice",
|
||||
|
||||
// 旧线上排队全局偏好与残留数据。
|
||||
"scenic_queue_tts_enabled",
|
||||
"scenic_queue_background_poll_enabled",
|
||||
"scenic_queue_selected_spot_id",
|
||||
"scenic_queue_selected_spot_name",
|
||||
"scenic_queue_custom_tts_text",
|
||||
"scenic_queue_photo_estimate_seconds",
|
||||
"scenic_queue_broadcast_interval_seconds",
|
||||
"scenic_queue_countdown_threshold_seconds",
|
||||
"scenic_queue_show_start_shooting_button",
|
||||
"scenic_queue_auto_call_ahead_count",
|
||||
"scenic_queue_quick_call_button_enabled",
|
||||
"scenic_queue_prepare_call_button_enabled",
|
||||
"scenic_queue_config_logs",
|
||||
"scenic_queue_settings_snapshot",
|
||||
"scenic_queue_preset_voices",
|
||||
]
|
||||
|
||||
private static let preReleaseAccountScopedSuffixes = [
|
||||
"key_in_role_id",
|
||||
"key_role_permission_list",
|
||||
"key_in_permission",
|
||||
"key_current_role_scenic_list",
|
||||
"key_online_status",
|
||||
"key_last_location_report_time",
|
||||
"key_location_reminder_minutes",
|
||||
"key_is_open_receive_voice",
|
||||
]
|
||||
|
||||
private static let dynamicKeyPatterns = [
|
||||
#"^account_.+_scenic_[^_]+_scenic_queue_selected_spot_(id|name)$"#,
|
||||
#"^account_.+_scenic_[^_]+_scenic_queue_custom_tts_text_spot_[^_]+$"#,
|
||||
#"^account_.+_scenic_[^_]+_scenic_queue_settings_snapshot_spot_[^_]+$"#,
|
||||
#"^account_.+_scenic_[^_]+_scenic_queue_preset_voices_spot_[^_]+$"#,
|
||||
#"^scenic_[^_]+_spot_[^_]+_scenic_queue_(settings_snapshot|preset_voices)$"#,
|
||||
#"^scenic_[^_]+_scenic_queue_settings_snapshot$"#,
|
||||
#"^.+_role_.*_common_uris$"#,
|
||||
#"^.+__scenic_queue_(punch_spot_id|punch_spot_name|punch_spot_remark|remark|settings_snapshot|offline_tts_v1|preset_voices_v1)$"#,
|
||||
]
|
||||
|
||||
/// 在缓存结构版本落后时执行一次旧数据清理,并在成功后写入当前版本。
|
||||
static func migrateIfNeeded(defaults: UserDefaults) {
|
||||
guard defaults.integer(forKey: schemaVersionKey) < currentSchemaVersion else { return }
|
||||
|
||||
let persistedKeys = Array(defaults.dictionaryRepresentation().keys)
|
||||
for key in persistedKeys where shouldRemove(key) {
|
||||
defaults.removeObject(forKey: key)
|
||||
}
|
||||
defaults.set(currentSchemaVersion, forKey: schemaVersionKey)
|
||||
}
|
||||
|
||||
private static func shouldRemove(_ key: String) -> Bool {
|
||||
if exactKeysToRemove.contains(key) {
|
||||
return true
|
||||
}
|
||||
if preReleaseAccountScopedSuffixes.contains(where: { key.hasSuffix("_\($0)") }) {
|
||||
return true
|
||||
}
|
||||
return dynamicKeyPatterns.contains { pattern in
|
||||
key.range(of: pattern, options: .regularExpression) != nil
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user