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:
2026-06-25 13:39:02 +08:00
parent fb8430889b
commit 1a0d1c25f4
63 changed files with 9823 additions and 58 deletions

View 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
}
}
}
/// 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
}
}