统一收款详情布局、设置金额弹窗和收款记录样式,接入 type 6/1 推送驱动与轮询 fallback,使 iOS 收款反馈与 Android 一致。 Co-authored-by: Cursor <cursoragent@cursor.com>
128 lines
4.3 KiB
Swift
128 lines
4.3 KiB
Swift
//
|
||
// PushPayload.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/26.
|
||
//
|
||
|
||
import Foundation
|
||
|
||
/// APNs token 工具,把系统返回的 Data 转成后端可接收的小写十六进制字符串。
|
||
enum APNsDeviceToken {
|
||
static func hexString(from data: Data) -> String {
|
||
data.map { String(format: "%02x", $0) }.joined()
|
||
}
|
||
}
|
||
|
||
/// 推送 payload 解析器,兼容顶层字段和第三方通道常见 extras/data 嵌套结构。
|
||
struct PushPayload: Sendable {
|
||
enum Route: Sendable, Equatable {
|
||
case payment
|
||
case order
|
||
case verificationOrder
|
||
case task
|
||
case queue
|
||
case messageCenter
|
||
}
|
||
|
||
private let values: [String: String]
|
||
|
||
nonisolated init(userInfo: [AnyHashable: Any]) {
|
||
var result: [String: String] = [:]
|
||
for (key, value) in userInfo {
|
||
guard let key = key as? String else { continue }
|
||
result[key] = Self.stringValue(value)
|
||
if key == "extras" || key == "extra" || key == "data" || key == "JMessageExtra" {
|
||
Self.mergeJSON(value, into: &result)
|
||
}
|
||
}
|
||
values = result
|
||
}
|
||
|
||
nonisolated var route: Route {
|
||
let typeText = values["type"] ?? ""
|
||
let routeText = values["route"] ?? ""
|
||
let uriText = values["uri"] ?? ""
|
||
let actionText = values["action"] ?? ""
|
||
let merged = [typeText, routeText, uriText, actionText].joined(separator: " ").lowercased()
|
||
|
||
if typeText == "1" || typeText == "6" || merged.contains("payment") || merged.contains("pay") || merged.contains("收款") {
|
||
return .payment
|
||
}
|
||
if merged.contains("queue") || merged.contains("排队") || merged.contains("叫号") || uriText == "/scenic-queue" {
|
||
return .queue
|
||
}
|
||
if merged.contains("verification") || merged.contains("writeoff") || merged.contains("核销") {
|
||
return .verificationOrder
|
||
}
|
||
if merged.contains("order") || merged.contains("订单") {
|
||
return .order
|
||
}
|
||
if merged.contains("task") || merged.contains("任务") {
|
||
return .task
|
||
}
|
||
return .messageCenter
|
||
}
|
||
|
||
/// 推送 extras 中的 type 字段,供收款页区分扫码/付款事件。
|
||
nonisolated var paymentEventType: Int? {
|
||
guard let typeText = values["type"], !typeText.isEmpty else { return nil }
|
||
return Int(typeText)
|
||
}
|
||
|
||
/// 推送 extras 中的 data 字段,付款成功时通常为 JSON 字符串。
|
||
nonisolated var paymentEventData: String? {
|
||
let data = values["data"]?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||
return data.isEmpty ? nil : data
|
||
}
|
||
|
||
/// 从付款成功 data JSON 中解析到账金额,供全局语音播报使用。
|
||
nonisolated var paymentSuccessAmount: String? {
|
||
guard let dataJSON = paymentEventData,
|
||
let data = dataJSON.data(using: .utf8),
|
||
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
||
return nil
|
||
}
|
||
if let amount = json["order_amount"] as? String, !amount.isEmpty {
|
||
return amount
|
||
}
|
||
if let amount = json["order_amount"] as? NSNumber {
|
||
return amount.stringValue
|
||
}
|
||
return nil
|
||
}
|
||
|
||
nonisolated private static func mergeJSON(_ value: Any, into result: inout [String: String]) {
|
||
if let dict = value as? [String: Any] {
|
||
for (key, value) in dict {
|
||
result[key] = stringValue(value)
|
||
mergeJSON(value, into: &result)
|
||
}
|
||
return
|
||
}
|
||
|
||
guard let text = value as? String,
|
||
let data = text.data(using: .utf8),
|
||
let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
|
||
else { return }
|
||
|
||
for (key, value) in object {
|
||
result[key] = stringValue(value)
|
||
mergeJSON(value, into: &result)
|
||
}
|
||
}
|
||
|
||
nonisolated private static func stringValue(_ value: Any?) -> String {
|
||
switch value {
|
||
case let string as String:
|
||
return string
|
||
case let number as NSNumber:
|
||
return number.stringValue
|
||
case .some(let value):
|
||
return "\(value)"
|
||
case nil:
|
||
return ""
|
||
}
|
||
}
|
||
}
|