Files
suixinkan_ios_new/suixinkan/Core/Queue/ScenicQueueSocketClient.swift
汉秋 c39c3d3c75 新增订单长尾流程,并迁移排队、消息、结算与审核模块
将定金订单、历史拍摄与多行程上传接入 Orders,并以真实页面替换首页排队管理、消息中心、景区结算与提现审核占位入口。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 13:39:02 +08:00

167 lines
5.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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<Void, Never>?
/// 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
}
}
}
/// payloadtype 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
}
}