import Foundation /// 推送点击后可到达的业务页面。 enum PushDestination: Sendable, Equatable { case paymentRecord case paymentDetails case messageCenter case aiRetouchTaskList case aiRetouchTaskDetail(batchId: Int) } /// 极光/APNs payload 解析结果,仅提取业务消息类型用于点击路由。 struct PushPayload: Sendable, Equatable { private let type: String private let aiRetouchBatchId: Int? /// 从系统通知 userInfo 提取顶层或推送包装层中的业务消息类型。 nonisolated init(userInfo: [AnyHashable: Any]) { type = Self.extractType(from: userInfo) aiRetouchBatchId = Self.extractBatchId(from: userInfo) } /// 从可发送的字符串字典创建 payload,仅保留业务消息类型。 nonisolated init(values: [String: String]) { type = Self.normalizedType(values["type"]) aiRetouchBatchId = Int(values["ai_retouch_batch_id"] ?? "").flatMap { $0 > 0 ? $0 : nil } } /// 可发送的规范化字段快照。 nonisolated var normalizedValues: [String: String] { var values = type.isEmpty ? [:] : ["type": type] if let aiRetouchBatchId { values["ai_retouch_batch_id"] = String(aiRetouchBatchId) } return values } /// 仅根据后端业务消息类型解析目标页面。 nonisolated var destination: PushDestination { switch type { case "1": return .paymentRecord case "6": return .paymentDetails case "14": if let aiRetouchBatchId, aiRetouchBatchId > 0 { return .aiRetouchTaskDetail(batchId: aiRetouchBatchId) } return .aiRetouchTaskList default: return .messageCenter } } private nonisolated static func extractType( from dictionary: [AnyHashable: Any], depth: Int = 0 ) -> String { guard depth <= 4 else { return "" } let directType = normalizedType(dictionary["type"]) guard directType.isEmpty else { return directType } for key in nestedTypeContainerKeys { guard let value = dictionary[key] else { continue } if let nested = value as? [AnyHashable: Any] { let nestedType = extractType(from: nested, depth: depth + 1) if !nestedType.isEmpty { return nestedType } continue } guard let text = value as? String, let data = text.data(using: .utf8), let object = try? JSONSerialization.jsonObject(with: data), let nested = object as? [String: Any] else { continue } let bridged = Dictionary(uniqueKeysWithValues: nested.map { (AnyHashable($0.key), $0.value) }) let nestedType = extractType(from: bridged, depth: depth + 1) if !nestedType.isEmpty { return nestedType } } return "" } private nonisolated static func normalizedType(_ value: Any?) -> String { switch value { case let string as String: return string.trimmingCharacters(in: .whitespacesAndNewlines) case let number as NSNumber: return number.stringValue default: return "" } } private nonisolated static func extractBatchId( from dictionary: [AnyHashable: Any], depth: Int = 0 ) -> Int? { guard depth <= 5 else { return nil } if let value = normalizedPositiveInt(dictionary["ai_retouch_batch_id"]) { return value } if let data = dictionary["data"] { if let nested = data as? [AnyHashable: Any], let value = extractBatchId(from: nested, depth: depth + 1) { return value } if let text = data as? String, let nested = jsonDictionary(text), let value = extractBatchId(from: nested, depth: depth + 1) { return value } } for key in nestedTypeContainerKeys { guard let value = dictionary[key] else { continue } if let nested = value as? [AnyHashable: Any], let batchId = extractBatchId(from: nested, depth: depth + 1) { return batchId } if let text = value as? String, let nested = jsonDictionary(text), let batchId = extractBatchId(from: nested, depth: depth + 1) { return batchId } } return nil } private nonisolated static func normalizedPositiveInt(_ value: Any?) -> Int? { let parsed: Int? switch value { case let number as NSNumber: parsed = number.intValue case let string as String: parsed = Int(string.trimmingCharacters(in: .whitespacesAndNewlines)) default: parsed = nil } return parsed.flatMap { $0 > 0 ? $0 : nil } } private nonisolated static func jsonDictionary(_ text: String) -> [AnyHashable: Any]? { guard let data = text.data(using: .utf8), let object = try? JSONSerialization.jsonObject(with: data), let dictionary = object as? [String: Any] else { return nil } return Dictionary(uniqueKeysWithValues: dictionary.map { (AnyHashable($0.key), $0.value) }) } private nonisolated static let nestedTypeContainerKeys = [ "extras", "extra", "JMessageExtra", "n_extras", ] }