// // ScenicQueueSocketClient.swift // suixinkan // // Created by Codex on 2026/6/25. // import Foundation /// 排队 WebSocket 消息。 struct ScenicQueueSocketMessage: Equatable { static let queueUpdatedAction = "scenic_spot_queue_updated" static let ticketCalledAction = "scenic_queue_ticket_called" let code: Int let data: ScenicQueueSocketData? var isScenicQueueEvent: Bool { let action = data?.action ?? "" return action == Self.queueUpdatedAction || action == Self.ticketCalledAction } } /// 排队 WebSocket data 块。 struct ScenicQueueSocketData: Equatable { let action: String let params: ScenicQueueSocketParams? } /// 排队 WebSocket 参数。 struct ScenicQueueSocketParams: Equatable { let scenicSpotId: Int64? let recordId: Int64? let operatorUid: Int64? let eventId: String? } @MainActor /// 排队 WebSocket 客户端,负责订阅排队更新和叫号事件。 final class ScenicQueueSocketClient { private var webSocketTask: URLSessionWebSocketTask? private var receiveTask: Task? /// 连接 WebSocket 并订阅指定打卡点的排队事件。 func connect( socketToken: String, scenicSpotId: Int, onMessage: @escaping @MainActor (ScenicQueueSocketMessage) -> Void ) { let token = socketToken.trimmingCharacters(in: .whitespacesAndNewlines) guard !token.isEmpty else { return } disconnect(reason: "Reconnect before scenic queue socket connect") let task = URLSession.shared.webSocketTask(with: APIEnvironment.current.webSocketURL) webSocketTask = task task.resume() receiveTask = Task { [weak self] in await self?.receiveLoop(task: task, onMessage: onMessage) } Task { for payload in Self.subscriptionPayloads(scenicSpotId: scenicSpotId) { try? await task.send(.string(payload)) } } } /// 断开 WebSocket。 func disconnect(reason: String = "Scenic queue page stopped") { receiveTask?.cancel() receiveTask = nil webSocketTask?.cancel(with: .normalClosure, reason: reason.data(using: .utf8)) webSocketTask = nil } private func receiveLoop( task: URLSessionWebSocketTask, onMessage: @escaping @MainActor (ScenicQueueSocketMessage) -> Void ) async { while !Task.isCancelled { do { let message = try await task.receive() let text: String? switch message { case .string(let value): text = value case .data(let data): text = String(data: data, encoding: .utf8) @unknown default: text = nil } guard let text, let parsed = Self.parseMessage(text), parsed.isScenicQueueEvent else { continue } onMessage(parsed) } catch { return } } } /// 构造订阅 payload,type 与 Android 保持一致。 static func subscriptionPayloads(scenicSpotId: Int) -> [String] { [ #"{"type":306,"params":{"scenic_spot_id":\#(scenicSpotId)}}"#, #"{"type":307,"params":{"scenic_spot_id":\#(scenicSpotId)}}"# ] } /// 解析排队 WebSocket 原始消息。 static func parseMessage(_ raw: String) -> ScenicQueueSocketMessage? { let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) guard trimmed.hasPrefix("{"), let data = trimmed.data(using: .utf8) else { return nil } guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return nil } let code = intValue(root["code"]) ?? 0 var socketData: ScenicQueueSocketData? if let dataObject = root["data"] as? [String: Any] { let action = stringValue(dataObject["action"]) var params: ScenicQueueSocketParams? if let paramsObject = dataObject["params"] as? [String: Any] { params = ScenicQueueSocketParams( scenicSpotId: int64Value(paramsObject["scenic_spot_id"]), recordId: int64Value(paramsObject["record_id"]), operatorUid: int64Value(paramsObject["operator_uid"]), eventId: stringValue(paramsObject["event_id"]).trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty ) } socketData = ScenicQueueSocketData(action: action, params: params) } return ScenicQueueSocketMessage(code: code, data: socketData) } private static func stringValue(_ value: Any?) -> String { switch value { case let value as String: return value case let value as NSNumber: return value.stringValue case let value?: return "\(value)" case nil: return "" } } private static func intValue(_ value: Any?) -> Int? { if let value = value as? Int { return value } if let value = value as? NSNumber { return value.intValue } if let value = value as? String { return Int(value) } return nil } private static func int64Value(_ value: Any?) -> Int64? { if let value = value as? Int64 { return value } if let value = value as? Int { return Int64(value) } if let value = value as? NSNumber { return value.int64Value } if let value = value as? String { return Int64(value) } return nil } } private extension String { var nilIfEmpty: String? { isEmpty ? nil : self } }