// // ScenicQueueAPIModels.swift // suixinkan // import Foundation /// GET /api/app/scenic-queue/queue-stats 响应。 struct ScenicQueueStatsData: Decodable, Equatable { let queueCount: Int let avgWaitMin: Double let time: String? enum CodingKeys: String, CodingKey { case queueCount = "queue_count" case avgWaitMin = "avg_wait_min" case time } } /// GET /api/app/scenic-queue/home 响应。 struct ScenicQueueHomeData: Decodable, Equatable { let stats: ScenicQueueHomeStats? let list: ScenicQueueHomeListBlock? let time: String? } /// 排队 home 接口 stats 节点。 struct ScenicQueueHomeStats: Decodable, Equatable { let type: Int } /// 排队 home 接口分页列表节点。 struct ScenicQueueHomeListBlock: Decodable, Equatable { let list: [ScenicQueueHomeItem] let total: Int let page: Int let pageSize: Int enum CodingKeys: String, CodingKey { case list case total case page case pageSize = "page_size" } } /// 排队 home 接口单条记录。 struct ScenicQueueHomeItem: Decodable, Equatable { let id: Int64 let queueCode: String let mobile: String let status: Int let statusText: String let waitMin: Int let aheadCount: Int let isCalled: Int let createdAt: String? let calledAt: String? let expiredAt: String? let finishedAt: String? let identityTag: String? let queueBanLabel: String? let queueBanDays: Int? let queueTime: String? let queueCountToday: Int? let uid: Int64 let markAsPhotog: Int let markAsFreelancePhotog: Int let isMissRequeue: Int let isMissRequeueText: String? enum CodingKeys: String, CodingKey { case id case queueCode = "queue_code" case mobile case status case statusText = "status_text" case waitMin = "wait_min" case aheadCount = "ahead_count" case isCalled = "is_called" case createdAt = "created_at" case calledAt = "called_at" case expiredAt = "expired_at" case finishedAt = "finished_at" case identityTag = "identity_tag" case queueBanLabel = "queue_ban_label" case queueBanDays = "queue_ban_days" case queueTime = "queue_time" case queueCountToday = "queue_count_today" case uid case markAsPhotog = "mark_as_photog" case markAsFreelancePhotog = "mark_as_freelance_photog" case isMissRequeue = "is_miss_requeue" case isMissRequeueText = "is_miss_requeue_text" } } extension ScenicQueueHomeItem { /// 转换为当前排队展示实体。 func toCurrentTicket() -> ScenicQueueTicket { ScenicQueueTicket( recordId: id, queueNo: queueCode, phoneMasked: Self.maskMobile(mobile), dialPhoneDigits: mobile.filter(\.isNumber), status: isCalled == 1 || statusText.contains("已叫") ? .called : .waiting, statusText: statusText.isEmpty ? (isCalled == 1 ? "已叫号" : "待叫号") : statusText, waitMinutes: max(0, waitMin), peopleAhead: max(0, aheadCount), showCompleteAction: isCalled == 1 || statusText.contains("已叫"), queueBanLabel: queueBanLabel ?? "", identityTag: identityTag ?? "", queueTimeDisplay: Self.formatQueueTime(queueTime ?? createdAt), queueCountToday: queueCountToday ?? 0, uid: uid, markAsPhotog: markAsPhotog, markAsFreelancePhotog: markAsFreelancePhotog, isMissRequeue: isMissRequeue == 1, missRequeueText: isMissRequeueText ?? "" ) } /// 转换为过号列表展示实体。 func toSkippedTicket() -> ScenicQueueSkippedTicket { ScenicQueueSkippedTicket( recordId: id, queueNo: queueCode, phoneMasked: Self.maskMobile(mobile), dialPhoneDigits: mobile.filter(\.isNumber), statusText: statusText.isEmpty ? "已过号" : statusText, skippedTimeDisplay: Self.formatQueueTime(expiredAt ?? finishedAt ?? calledAt), queueBanLabel: queueBanLabel ?? "", isMissRequeue: isMissRequeue == 1, missRequeueText: isMissRequeueText ?? "" ) } private static func maskMobile(_ raw: String) -> String { let digits = raw.filter(\.isNumber) guard digits.count >= 7 else { return raw } let start = digits.prefix(3) let end = digits.suffix(4) return "\(start)****\(end)" } private static func formatQueueTime(_ raw: String?) -> String { let text = raw?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" guard !text.isEmpty else { return "--" } let normalized = text.replacingOccurrences(of: "T", with: " ").replacingOccurrences(of: "Z", with: "") let inputFormats = ["yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"] let formatter = DateFormatter() formatter.locale = Locale(identifier: "zh_CN") for format in inputFormats { formatter.dateFormat = format if let date = formatter.date(from: normalized) { formatter.dateFormat = "MM-dd HH:mm" return formatter.string(from: date) } } return String(text.prefix(16)) } } /// POST /api/app/scenic-queue/call 响应 data。 struct ScenicQueueCallData: Decodable, Equatable { let id: Int64 let status: Int let statusText: String let calledAt: String? enum CodingKeys: String, CodingKey { case id case status case statusText = "status_text" case calledAt = "called_at" } } /// POST /api/app/scenic-queue/pass 响应 data。 struct ScenicQueuePassData: Decodable, Equatable { let id: Int64 let status: Int let statusText: String enum CodingKeys: String, CodingKey { case id case status case statusText = "status_text" } } /// POST /api/app/scenic-queue/finish 响应 data。 struct ScenicQueueFinishData: Decodable, Equatable { let id: Int64 let status: Int let statusText: String enum CodingKeys: String, CodingKey { case id case status case statusText = "status_text" } } /// GET /api/app/scenic-queue/setting 响应。 struct ScenicQueueSettingData: Decodable, Equatable { let exists: Bool let setting: ScenicQueueSettingItem? let time: String? } /// 服务端排队设置条目。 struct ScenicQueueSettingItem: Codable, Equatable { let id: Int64? let scenicId: Int64? let scenicSpotId: Int64? 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 voiceBroadcasts: [ScenicQueueVoiceBroadcastItem]? let status: Int? let remark: String? 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 voiceBroadcasts = "voice_broadcasts" case status case remark case createdAt = "created_at" case updatedAt = "updated_at" } } extension ScenicQueueSettingItem { /// 转换为本地设置快照。 func toSnapshot(fallback: ScenicQueueSettingsSnapshot = ScenicQueueSettingsSnapshot()) -> ScenicQueueSettingsSnapshot { ScenicQueueSettingsSnapshot( shootMinute: (photoEstimateMin ?? 0).clamped(to: 0...30), shootSecond: (photoEstimateSec ?? 0).clamped(to: 0...59), firstAheadCount: (firstNoticeThresholdPos ?? 0).clamped(to: 0...50), firstSms: (firstNoticeSmsEnabled ?? 0) == 1, firstPhone: (firstNoticeCallEnabled ?? 0) == 1, secondAheadCount: (secondNoticeThresholdPos ?? 0).clamped(to: 0...50), secondSms: (secondNoticeSmsEnabled ?? 0) == 1, secondPhone: (secondNoticeCallEnabled ?? 0) == 1, broadcastIntervalSec: ScenicQueueSettingItem.normalizedBroadcastInterval(countdownBroadcastIntervalSec), countdownThresholdSec: ScenicQueueSettingItem.normalizedReadableThreshold(countdownReadableThresholdSec), queueDistanceMeter: (queueDistanceMeter ?? 0).clamped(to: 0...9_999_990), queueTakeLimit: (queueTakeLimit ?? 0).clamped(to: 0...999_999), missCallRequeueOffset: (missCallRequeueOffset ?? 1).clamped(to: 1...9_999), showStartShootingButton: showStartShootButton.map { $0 == 1 } ?? fallback.showStartShootingButton, autoCallAheadCount: (autoCallNextCount ?? fallback.autoCallAheadCount).clamped(to: 0...5), quickCallButtonEnabled: fallback.quickCallButtonEnabled, prepareCallButtonEnabled: fallback.prepareCallButtonEnabled, businessOpen: (status ?? 1) == 1, businessStartTime: businessStartTime?.nonEmptyTrimmed ?? "10:00", businessEndTime: businessEndTime?.nonEmptyTrimmed ?? "20:00" ) } private static func normalizedBroadcastInterval(_ value: Int?) -> Int { guard let value, 40...60 ~= value else { return 50 } return value } private static func normalizedReadableThreshold(_ value: Int?) -> Int { guard let value, 10...30 ~= value else { return 15 } return value } } /// POST /api/app/scenic-queue/save-setting 请求体。 struct ScenicQueueSaveSettingRequest: Encodable, Equatable { let scenicId: Int64 let scenicSpotId: Int64 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 ScenicQueueVoiceBroadcastItem: Codable, Equatable { let content: String? let sortOrder: Int? enum CodingKeys: String, CodingKey { case content case sortOrder = "sort_order" } } /// GET /api/app/scenic-queue/shoot-queue-qrcode 响应。 struct ScenicQueueShootQueueQrcodeData: Decodable, Equatable { let qrcodeUrl: String? let scene: String? let page: String? let time: String? enum CodingKeys: String, CodingKey { case qrcodeUrl = "qrcode_url" case scene case page case time } } /// GET /api/app/scenic-queue/setting-change-log 响应。 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 } } /// 配置变更日志条目。 struct ScenicQueueSettingChangeLogItem: Decodable, Equatable, Hashable { let id: Int64 let scenicId: Int? let scenicSpotId: Int? let scenicSpotName: String let operatorId: Int? let operatorName: String let operatorPhoneTail: String let summary: String let displayText: String let createdAt: String? enum CodingKeys: String, CodingKey { case id case scenicId = "scenic_id" case scenicSpotId = "scenic_spot_id" case scenicSpotName = "scenic_spot_name" case operatorId = "operator_id" case operatorName = "operator_name" case operatorPhoneTail = "operator_phone_tail" case summary case displayText = "display_text" case createdAt = "created_at" } } /// 打卡点列表响应。 struct ScenicSpotListResponse: Decodable, Equatable { let list: [ScenicSpotItem] let total: Int? enum CodingKeys: String, CodingKey { case list case total } } /// 打卡点列表条目。 struct ScenicSpotItem: Decodable, Equatable { let id: Int64 let name: String } /// Socket token 响应。 struct SocketTokenResponse: Decodable, Equatable { let socketToken: String enum CodingKeys: String, CodingKey { case socketToken = "socket_token" } } private extension Int { func clamped(to range: ClosedRange) -> Int { Swift.min(Swift.max(self, range.lowerBound), range.upperBound) } } private extension String { var nonEmptyTrimmed: String? { let text = trimmingCharacters(in: .whitespacesAndNewlines) return text.isEmpty ? nil : text } }