升级 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

@ -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)
}
}