// // ScenicQueueSettingsStore.swift // suixinkan // // Created by Codex on 2026/6/25. // import Foundation /// 排队管理本地设置 key。 enum ScenicQueueLocalSettings { static let selectedSpotIdKey = "scenic_queue_selected_spot_id" static let selectedSpotNameKey = "scenic_queue_selected_spot_name" static let customTtsTextKey = "scenic_queue_custom_tts_text" static let settingsSnapshotKey = "scenic_queue_settings_snapshot" static let photoEstimateSecondsKey = "scenic_queue_photo_estimate_seconds" static let broadcastIntervalSecondsKey = "scenic_queue_broadcast_interval_seconds" static let countdownThresholdSecondsKey = "scenic_queue_countdown_threshold_seconds" static let showStartShootingButtonKey = "scenic_queue_show_start_shooting_button" static let autoCallAheadCountKey = "scenic_queue_auto_call_ahead_count" static let quickCallButtonEnabledKey = "scenic_queue_quick_call_button_enabled" static let prepareCallButtonEnabledKey = "scenic_queue_prepare_call_button_enabled" static let presetVoicesKey = "scenic_queue_preset_voices" static let ttsEnabledKey = "scenic_queue_tts_enabled" static let backgroundPollEnabledKey = "scenic_queue_background_poll_enabled" } /// 排队管理本地设置存储,按用户、景区、打卡点隔离并兼容旧 key。 enum ScenicQueueSettingsStore { /// 生成景区作用域 key。 static func scopedKey(base: String, userId: String?, scenicId: Int?) -> String { guard let scenicId else { return base } let user = normalizedUserId(userId) return "\(base)_user_\(user)_scenic_\(scenicId)" } /// 生成打卡点作用域 key。 static func scopedSpotKey(base: String, userId: String?, scenicId: Int?, spotId: Int?) -> String { guard let scenicId, let spotId else { return scopedKey(base: base, userId: userId, scenicId: scenicId) } let user = normalizedUserId(userId) return "\(base)_user_\(user)_scenic_\(scenicId)_spot_\(spotId)" } /// 读取已选打卡点 ID。 static func selectedSpotId(userId: String?, scenicId: Int?) -> Int? { guard let scenicId else { return legacyPositiveInt(forKey: ScenicQueueLocalSettings.selectedSpotIdKey) } let key = scopedKey(base: ScenicQueueLocalSettings.selectedSpotIdKey, userId: userId, scenicId: scenicId) if let value = positiveInt(forKey: key) { return value } if let legacy = legacyPositiveInt(forKey: ScenicQueueLocalSettings.selectedSpotIdKey) { UserDefaults.standard.set(legacy, forKey: key) return legacy } return nil } /// 读取已选打卡点名称。 static func selectedSpotName(userId: String?, scenicId: Int?) -> String { guard let scenicId else { return UserDefaults.standard.string(forKey: ScenicQueueLocalSettings.selectedSpotNameKey) ?? "" } let key = scopedKey(base: ScenicQueueLocalSettings.selectedSpotNameKey, userId: userId, scenicId: scenicId) if let value = UserDefaults.standard.string(forKey: key), !value.isEmpty { return value } let legacy = UserDefaults.standard.string(forKey: ScenicQueueLocalSettings.selectedSpotNameKey) ?? "" if !legacy.isEmpty { UserDefaults.standard.set(legacy, forKey: key) } return legacy } /// 保存已选打卡点。 static func saveSelectedSpot(id: Int, name: String, userId: String?, scenicId: Int) { let defaults = UserDefaults.standard defaults.set(id, forKey: ScenicQueueLocalSettings.selectedSpotIdKey) defaults.set(name, forKey: ScenicQueueLocalSettings.selectedSpotNameKey) defaults.set(id, forKey: scopedKey(base: ScenicQueueLocalSettings.selectedSpotIdKey, userId: userId, scenicId: scenicId)) defaults.set(name, forKey: scopedKey(base: ScenicQueueLocalSettings.selectedSpotNameKey, userId: userId, scenicId: scenicId)) } /// 读取自定义语音文本。 static func customTtsText(userId: String?, scenicId: Int?, spotId: Int?) -> String { guard let scenicId, let spotId else { return UserDefaults.standard.string(forKey: ScenicQueueLocalSettings.customTtsTextKey) ?? "" } let key = scopedSpotKey(base: ScenicQueueLocalSettings.customTtsTextKey, userId: userId, scenicId: scenicId, spotId: spotId) if let value = UserDefaults.standard.string(forKey: key) { return value } let legacy = UserDefaults.standard.string(forKey: ScenicQueueLocalSettings.customTtsTextKey) ?? "" if !legacy.isEmpty { UserDefaults.standard.set(legacy, forKey: key) } return legacy } /// 保存自定义语音文本。 static func saveCustomTtsText(_ text: String, userId: String?, scenicId: Int?, spotId: Int?) { let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) UserDefaults.standard.set(trimmed, forKey: ScenicQueueLocalSettings.customTtsTextKey) if let scenicId, let spotId { UserDefaults.standard.set( trimmed, forKey: scopedSpotKey(base: ScenicQueueLocalSettings.customTtsTextKey, userId: userId, scenicId: scenicId, spotId: spotId) ) } } /// 读取设置快照。 static func settingsSnapshot(userId: String?, scenicId: Int, spotId: Int) -> ScenicQueueSettingsSnapshot? { let keys = [ scopedSpotKey(base: ScenicQueueLocalSettings.settingsSnapshotKey, userId: userId, scenicId: scenicId, spotId: spotId), "scenic_\(scenicId)_spot_\(spotId)_\(ScenicQueueLocalSettings.settingsSnapshotKey)", "scenic_\(scenicId)_\(ScenicQueueLocalSettings.settingsSnapshotKey)" ] for key in keys { guard let data = UserDefaults.standard.data(forKey: key), let snapshot = try? JSONDecoder().decode(ScenicQueueSettingsSnapshot.self, from: data) else { continue } return snapshot } return nil } /// 保存设置快照。 static func saveSettingsSnapshot(_ snapshot: ScenicQueueSettingsSnapshot, userId: String?, scenicId: Int, spotId: Int) { guard let data = try? JSONEncoder().encode(snapshot) else { return } UserDefaults.standard.set( data, forKey: scopedSpotKey(base: ScenicQueueLocalSettings.settingsSnapshotKey, userId: userId, scenicId: scenicId, spotId: spotId) ) UserDefaults.standard.set(data, forKey: "scenic_\(scenicId)_spot_\(spotId)_\(ScenicQueueLocalSettings.settingsSnapshotKey)") } /// 读取语音预设。 static func presetVoices(userId: String?, scenicId: Int?, spotId: Int?) -> [String] { guard let scenicId, let spotId else { return [] } let keys = [ scopedSpotKey(base: ScenicQueueLocalSettings.presetVoicesKey, userId: userId, scenicId: scenicId, spotId: spotId), "scenic_\(scenicId)_spot_\(spotId)_\(ScenicQueueLocalSettings.presetVoicesKey)" ] for key in keys { guard let data = UserDefaults.standard.data(forKey: key), let voices = try? JSONDecoder().decode([String].self, from: data) else { continue } return voices } return [] } /// 保存语音预设。 static func savePresetVoices(_ voices: [String], userId: String?, scenicId: Int, spotId: Int) { let normalized = Array(voices.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty }.prefix(5)) guard let data = try? JSONEncoder().encode(normalized) else { return } UserDefaults.standard.set( data, forKey: scopedSpotKey(base: ScenicQueueLocalSettings.presetVoicesKey, userId: userId, scenicId: scenicId, spotId: spotId) ) UserDefaults.standard.set(data, forKey: "scenic_\(scenicId)_spot_\(spotId)_\(ScenicQueueLocalSettings.presetVoicesKey)") } /// 规范化dUserId格式。 private static func normalizedUserId(_ userId: String?) -> String { let text = userId?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return text.isEmpty ? "anonymous" : text } /// positive整数相关逻辑。 private static func positiveInt(forKey key: String) -> Int? { guard UserDefaults.standard.object(forKey: key) != nil else { return nil } let value = UserDefaults.standard.integer(forKey: key) return value > 0 ? value : nil } /// legacy正数整数相关逻辑。 private static func legacyPositiveInt(forKey key: String) -> Int? { positiveInt(forKey: key) } }