// // ScenicQueueSettingsModels.swift // suixinkan // // Created by Codex on 2026/6/25. // import Foundation /// 排队设置响应。 struct ScenicQueueSettingData: Decodable, Equatable { let exists: Bool let setting: ScenicQueueSettingItem? let time: String? enum CodingKeys: String, CodingKey { case exists case setting case time } /// 创建排队设置响应。 init(exists: Bool = false, setting: ScenicQueueSettingItem? = nil, time: String? = nil) { self.exists = exists self.setting = setting self.time = time } /// 宽松解码排队设置响应。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) exists = try container.decodeLossyBool(forKey: .exists) ?? false setting = try container.decodeIfPresent(ScenicQueueSettingItem.self, forKey: .setting) time = try container.decodeLossyString(forKey: .time) } } /// 排队设置实体。 struct ScenicQueueSettingItem: Decodable, Equatable { let id: Int64? let scenicId: Int? let scenicSpotId: Int? let scenicSpotName: String let photoEstimateMin: Int let photoEstimateSec: Int let firstNoticeThresholdPos: Int let firstNoticeSmsEnabled: Int let firstNoticeCallEnabled: Int let secondNoticeThresholdPos: Int let secondNoticeSmsEnabled: Int let secondNoticeCallEnabled: Int let countdownBroadcastIntervalSec: Int let countdownReadableThresholdSec: Int let queueDistanceMeter: Int let queueTakeLimit: Int let missCallRequeueOffset: Int let showStartShootButton: Int? let autoCallNextCount: Int? let businessStartTime: String let businessEndTime: String let status: Int let remark: String let voiceBroadcasts: [ScenicQueueVoiceBroadcastItem] let createdAt: String let updatedAt: String enum CodingKeys: String, CodingKey { case id case scenicId = "scenic_id" case scenicSpotId = "scenic_spot_id" case scenicSpotName = "scenic_spot_name" case photoEstimateMin = "photo_estimate_min" case photoEstimateSec = "photo_estimate_sec" case firstNoticeThresholdPos = "first_notice_threshold_pos" case firstNoticeSmsEnabled = "first_notice_sms_enabled" case firstNoticeCallEnabled = "first_notice_call_enabled" case secondNoticeThresholdPos = "second_notice_threshold_pos" case secondNoticeSmsEnabled = "second_notice_sms_enabled" case secondNoticeCallEnabled = "second_notice_call_enabled" case countdownBroadcastIntervalSec = "countdown_broadcast_interval_sec" case countdownReadableThresholdSec = "countdown_readable_threshold_sec" case queueDistanceMeter = "queue_distance_meter" case queueTakeLimit = "queue_take_limit" case missCallRequeueOffset = "miss_call_requeue_offset" case showStartShootButton = "show_start_shoot_button" case autoCallNextCount = "auto_call_next_count" case businessStartTime = "business_start_time" case businessEndTime = "business_end_time" case status case remark case voiceBroadcasts = "voice_broadcasts" case createdAt = "created_at" case updatedAt = "updated_at" } /// 宽松解码排队设置实体。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = (try container.decodeLossyInt(forKey: .id)).map(Int64.init) scenicId = try container.decodeLossyInt(forKey: .scenicId) scenicSpotId = try container.decodeLossyInt(forKey: .scenicSpotId) scenicSpotName = try container.decodeLossyString(forKey: .scenicSpotName) photoEstimateMin = try container.decodeLossyInt(forKey: .photoEstimateMin) ?? 0 photoEstimateSec = try container.decodeLossyInt(forKey: .photoEstimateSec) ?? 0 firstNoticeThresholdPos = try container.decodeLossyInt(forKey: .firstNoticeThresholdPos) ?? 0 firstNoticeSmsEnabled = try container.decodeLossyInt(forKey: .firstNoticeSmsEnabled) ?? 0 firstNoticeCallEnabled = try container.decodeLossyInt(forKey: .firstNoticeCallEnabled) ?? 0 secondNoticeThresholdPos = try container.decodeLossyInt(forKey: .secondNoticeThresholdPos) ?? 0 secondNoticeSmsEnabled = try container.decodeLossyInt(forKey: .secondNoticeSmsEnabled) ?? 0 secondNoticeCallEnabled = try container.decodeLossyInt(forKey: .secondNoticeCallEnabled) ?? 0 countdownBroadcastIntervalSec = try container.decodeLossyInt(forKey: .countdownBroadcastIntervalSec) ?? 50 countdownReadableThresholdSec = try container.decodeLossyInt(forKey: .countdownReadableThresholdSec) ?? 15 queueDistanceMeter = try container.decodeLossyInt(forKey: .queueDistanceMeter) ?? 0 queueTakeLimit = try container.decodeLossyInt(forKey: .queueTakeLimit) ?? 0 missCallRequeueOffset = try container.decodeLossyInt(forKey: .missCallRequeueOffset) ?? 1 showStartShootButton = try container.decodeLossyInt(forKey: .showStartShootButton) autoCallNextCount = try container.decodeLossyInt(forKey: .autoCallNextCount) businessStartTime = try container.decodeLossyString(forKey: .businessStartTime) businessEndTime = try container.decodeLossyString(forKey: .businessEndTime) status = try container.decodeLossyInt(forKey: .status) ?? 1 remark = try container.decodeLossyString(forKey: .remark) voiceBroadcasts = (try? container.decodeIfPresent([ScenicQueueVoiceBroadcastItem].self, forKey: .voiceBroadcasts)) ?? [] createdAt = try container.decodeLossyString(forKey: .createdAt) updatedAt = try container.decodeLossyString(forKey: .updatedAt) } } /// 排队语音播报预设。 struct ScenicQueueVoiceBroadcastItem: Codable, Identifiable, Equatable { var id: String { "\(sortOrder)-\(content)" } let content: String let sortOrder: Int enum CodingKeys: String, CodingKey { case content case sortOrder = "sort_order" } /// 创建语音播报预设。 init(content: String, sortOrder: Int) { self.content = content self.sortOrder = sortOrder } /// 宽松解码语音播报预设。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) content = try container.decodeLossyString(forKey: .content).trimmingCharacters(in: .whitespacesAndNewlines) sortOrder = try container.decodeLossyInt(forKey: .sortOrder) ?? 0 } } /// 保存排队设置请求体。 struct ScenicQueueSaveSettingRequest: Encodable, Equatable { let scenicId: Int let scenicSpotId: Int let photoEstimateMin: Int let photoEstimateSec: Int let firstNoticeThresholdPos: Int let firstNoticeSmsEnabled: Int let firstNoticeCallEnabled: Int let secondNoticeThresholdPos: Int let secondNoticeSmsEnabled: Int let secondNoticeCallEnabled: Int let countdownBroadcastIntervalSec: Int let countdownReadableThresholdSec: Int let queueDistanceMeter: Int? let queueTakeLimit: Int? let missCallRequeueOffset: Int? let showStartShootButton: Int? let autoCallNextCount: Int? let businessStartTime: String let businessEndTime: String let voiceBroadcasts: [ScenicQueueVoiceBroadcastItem] let status: Int let remark: String? enum CodingKeys: String, CodingKey { case scenicId = "scenic_id" case scenicSpotId = "scenic_spot_id" case photoEstimateMin = "photo_estimate_min" case photoEstimateSec = "photo_estimate_sec" case firstNoticeThresholdPos = "first_notice_threshold_pos" case firstNoticeSmsEnabled = "first_notice_sms_enabled" case firstNoticeCallEnabled = "first_notice_call_enabled" case secondNoticeThresholdPos = "second_notice_threshold_pos" case secondNoticeSmsEnabled = "second_notice_sms_enabled" case secondNoticeCallEnabled = "second_notice_call_enabled" case countdownBroadcastIntervalSec = "countdown_broadcast_interval_sec" case countdownReadableThresholdSec = "countdown_readable_threshold_sec" case queueDistanceMeter = "queue_distance_meter" case queueTakeLimit = "queue_take_limit" case missCallRequeueOffset = "miss_call_requeue_offset" case showStartShootButton = "show_start_shoot_button" case autoCallNextCount = "auto_call_next_count" case businessStartTime = "business_start_time" case businessEndTime = "business_end_time" case voiceBroadcasts = "voice_broadcasts" case status case remark } } /// 排队设置变更日志响应。 struct ScenicQueueSettingChangeLogData: Decodable, Equatable { let total: Int let page: Int let pageSize: Int let list: [ScenicQueueSettingChangeLogItem] enum CodingKeys: String, CodingKey { case total case page case pageSize = "page_size" case list } /// 创建变更日志响应。 init(total: Int = 0, page: Int = 1, pageSize: Int = 20, list: [ScenicQueueSettingChangeLogItem] = []) { self.total = total self.page = page self.pageSize = pageSize self.list = list } /// 宽松解码变更日志响应。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) total = try container.decodeLossyInt(forKey: .total) ?? 0 page = try container.decodeLossyInt(forKey: .page) ?? 1 pageSize = try container.decodeLossyInt(forKey: .pageSize) ?? 20 list = (try? container.decodeIfPresent([ScenicQueueSettingChangeLogItem].self, forKey: .list)) ?? [] } } /// 排队设置变更日志项。 struct ScenicQueueSettingChangeLogItem: Decodable, Equatable, Identifiable { let id: Int64 let scenicSpotName: String let operatorName: String let operatorPhoneTail: String let summary: String let displayText: String let createdAt: String enum CodingKeys: String, CodingKey { case id case scenicSpotName = "scenic_spot_name" case operatorName = "operator_name" case operatorPhoneTail = "operator_phone_tail" case summary case displayText = "display_text" case createdAt = "created_at" } /// 宽松解码变更日志项。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = Int64(try container.decodeLossyInt(forKey: .id) ?? 0) scenicSpotName = try container.decodeLossyString(forKey: .scenicSpotName) operatorName = try container.decodeLossyString(forKey: .operatorName) operatorPhoneTail = try container.decodeLossyString(forKey: .operatorPhoneTail) summary = try container.decodeLossyString(forKey: .summary) displayText = try container.decodeLossyString(forKey: .displayText) createdAt = try container.decodeLossyString(forKey: .createdAt) } } /// 排队取号二维码响应。 struct ScenicQueueShootQueueQRCodeData: Decodable, Equatable { let qrcodeUrl: String enum CodingKeys: String, CodingKey { case qrcodeUrl = "qrcode_url" } /// 创建二维码响应。 init(qrcodeUrl: String = "") { self.qrcodeUrl = qrcodeUrl } /// 宽松解码二维码响应。 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) qrcodeUrl = try container.decodeLossyString(forKey: .qrcodeUrl) } } /// 本地排队设置快照,用于服务器无配置或离线时保留关键配置。 struct ScenicQueueSettingsSnapshot: Codable, Equatable { var shootMinute: Int = 0 var shootSecond: Int = 30 var firstAheadCount: Int = 0 var firstSms: Bool = false var firstPhone: Bool = false var secondAheadCount: Int = 0 var secondSms: Bool = false var secondPhone: Bool = false var broadcastIntervalSec: Int = 50 var countdownThresholdSec: Int = 15 var queueDistanceMeter: Int = 0 var queueTakeLimit: Int = 0 var missCallRequeueOffset: Int = 1 var showStartShootingButton: Bool = true var autoCallAheadCount: Int = 0 var quickCallButtonEnabled: Bool = false var prepareCallButtonEnabled: Bool = false var businessOpen: Bool = true var businessStartTime: String = "10:00" var businessEndTime: String = "20:00" enum CodingKeys: String, CodingKey { case shootMinute = "shoot_minute" case shootSecond = "shoot_second" case firstAheadCount = "first_ahead_count" case firstSms = "first_sms" case firstPhone = "first_phone" case secondAheadCount = "second_ahead_count" case secondSms = "second_sms" case secondPhone = "second_phone" case broadcastIntervalSec = "broadcast_interval_sec" case countdownThresholdSec = "countdown_threshold_sec" case queueDistanceMeter = "queue_distance_meter" case queueTakeLimit = "queue_take_limit" case missCallRequeueOffset = "miss_call_requeue_offset" case showStartShootingButton = "show_start_shooting_button" case autoCallAheadCount = "auto_call_ahead_count" case quickCallButtonEnabled = "quick_call_button_enabled" case prepareCallButtonEnabled = "prepare_call_button_enabled" case businessOpen = "business_open" case businessStartTime = "business_start_time" case businessEndTime = "business_end_time" } } private extension KeyedDecodingContainer { func decodeLossyString(forKey key: Key) throws -> String { if let value = try? decodeIfPresent(String.self, forKey: key) { return value } if let value = try? decodeIfPresent(Int.self, forKey: key) { return String(value) } if let value = try? decodeIfPresent(Double.self, forKey: key) { return String(value) } if let value = try? decodeIfPresent(Bool.self, forKey: key) { return value ? "1" : "0" } return "" } func decodeLossyInt(forKey key: Key) throws -> Int? { if let value = try? decodeIfPresent(Int.self, forKey: key) { return value } if let value = try? decodeIfPresent(Double.self, forKey: key) { return Int(value) } if let value = try? decodeIfPresent(String.self, forKey: key), case let text = value.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty { return Int(text) ?? Int(Double(text) ?? 0) } if let value = try? decodeIfPresent(Bool.self, forKey: key) { return value ? 1 : 0 } return nil } func decodeLossyBool(forKey key: Key) throws -> Bool? { if let value = try? decodeIfPresent(Bool.self, forKey: key) { return value } if let value = try? decodeIfPresent(Int.self, forKey: key) { return value != 0 } if let value = try? decodeIfPresent(String.self, forKey: key), case let text = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(), !text.isEmpty { if ["1", "true", "yes"].contains(text) { return true } if ["0", "false", "no"].contains(text) { return false } } return nil } }