Add order tail flows and migrate queue, message, settlement, and audit modules.
Wire deposit orders, historical shooting, and multi-travel uploads into Orders, and replace home placeholders for queue management, message center, scenic settlement, and withdrawal audit. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
166
suixinkan/Core/Queue/ScenicQueueSocketClient.swift
Normal file
166
suixinkan/Core/Queue/ScenicQueueSocketClient.swift
Normal file
@ -0,0 +1,166 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 构造订阅 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user