fix: 推送点击仅按 type 路由到收款页或消息中心。

对齐后端消息类型约定,去掉 route/uri 等兼容字段,并补充联调日志与单测。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-22 18:20:36 +08:00
parent caeeb9a1cf
commit 1b2637f0ee
6 changed files with 544 additions and 133 deletions

View File

@ -2,129 +2,85 @@ import Foundation
///
enum PushDestination: Sendable, Equatable {
case payment
case orders
case verificationOrders
case task
case queue
case paymentRecord
case paymentDetails
case messageCenter
///
var requiresScenicContext: Bool {
switch self {
case .payment, .task, .queue:
true
case .orders, .verificationOrders, .messageCenter:
false
}
}
}
/// /APNs payload JSON
/// /APNs payload
struct PushPayload: Sendable, Equatable {
private let values: [String: String]
private let type: String
/// userInfo
/// userInfo
nonisolated init(userInfo: [AnyHashable: Any]) {
var flattened: [String: String] = [:]
Self.mergeDictionary(userInfo, into: &flattened)
values = flattened
type = Self.extractType(from: userInfo)
}
/// payload Actor 使
/// payload
nonisolated init(values: [String: String]) {
self.values = values
type = Self.normalizedType(values["type"])
}
///
nonisolated var normalizedValues: [String: String] {
values
type.isEmpty ? [:] : ["type": type]
}
///
///
nonisolated var destination: PushDestination {
let type = value(for: "type")
let route = value(for: "route")
let uri = value(for: "uri")
let action = value(for: "action")
let merged = [route, uri, action].joined(separator: " ").lowercased()
if type == "1" || type == "6"
|| merged.contains("payment") || merged.contains("payment_qr")
|| merged.contains("收款") {
return .payment
}
if merged.contains("queue") || merged.contains("scenic-queue")
|| merged.contains("排队") || merged.contains("叫号") {
return .queue
}
if merged.contains("verification") || merged.contains("writeoff")
|| merged.contains("write_off") || merged.contains("核销") {
return .verificationOrders
}
if merged.contains("order") || merged.contains("订单") {
return .orders
}
if merged.contains("task") || merged.contains("任务") {
return .task
}
return .messageCenter
}
private nonisolated func value(for key: String) -> String {
values[key]?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
}
private nonisolated static func mergeDictionary(
_ dictionary: [AnyHashable: Any],
into result: inout [String: String]
) {
for (rawKey, value) in dictionary {
guard let key = rawKey as? String else { continue }
merge(value, key: key, into: &result)
switch type {
case "1":
return .paymentRecord
case "6":
return .paymentDetails
default:
return .messageCenter
}
}
private nonisolated static func merge(
_ value: Any,
key: String,
into result: inout [String: String]
) {
if let dictionary = value as? [String: Any] {
result[key] = stringValue(value)
mergeDictionary(dictionary, into: &result)
return
}
if let dictionary = value as? [AnyHashable: Any] {
result[key] = stringValue(value)
mergeDictionary(dictionary, into: &result)
return
}
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 }
let text = stringValue(value)
result[key] = text
guard Self.nestedJSONKeys.contains(key),
let data = text.data(using: .utf8),
let object = try? JSONSerialization.jsonObject(with: data)
else { return }
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
}
if let dictionary = object as? [String: Any] {
mergeDictionary(dictionary, into: &result)
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 stringValue(_ value: Any) -> String {
private nonisolated static func normalizedType(_ value: Any?) -> String {
switch value {
case let string as String:
string
return string.trimmingCharacters(in: .whitespacesAndNewlines)
case let number as NSNumber:
number.stringValue
return number.stringValue
default:
String(describing: value)
return ""
}
}
private nonisolated static let nestedJSONKeys: Set<String> = [
"extras", "extra", "data", "JMessageExtra", "n_extras",
private nonisolated static let nestedTypeContainerKeys = [
"extras", "extra", "JMessageExtra", "n_extras",
]
}

View File

@ -229,6 +229,7 @@ final class PushNotificationManager: NSObject {
/// Scene
func handleNotificationResponse(_ response: UNNotificationResponse) {
let userInfo = response.notification.request.content.userInfo
Self.logReceivedPush(userInfo, source: "tap(cold-start)")
JPUSHService.handleRemoteNotification(userInfo)
handleNotificationTap(
payload: PushPayload(userInfo: userInfo),
@ -238,10 +239,35 @@ final class PushNotificationManager: NSObject {
///
func handleRemoteNotification(_ userInfo: [AnyHashable: Any]) {
Self.logReceivedPush(userInfo, source: "background")
JPUSHService.handleRemoteNotification(userInfo)
NotificationCenter.default.post(name: NotificationName.unreadMessageCountDidChange, object: nil)
}
/// payload 便
private nonisolated static func logReceivedPush(
_ userInfo: [AnyHashable: Any],
source: String
) {
#if DEBUG
let payloadText: String
let normalized = Dictionary(uniqueKeysWithValues: userInfo.compactMap { key, value in
(key as? String).map { ($0, value) }
})
if JSONSerialization.isValidJSONObject(normalized),
let data = try? JSONSerialization.data(
withJSONObject: normalized,
options: [.prettyPrinted, .sortedKeys]
),
let text = String(data: data, encoding: .utf8) {
payloadText = text
} else {
payloadText = String(describing: userInfo)
}
print("[Push] received (\(source)):\n\(payloadText)")
#endif
}
private func observeJPushNetworkLogin() {
NotificationCenter.default.addObserver(
self,
@ -352,7 +378,9 @@ extension PushNotificationManager: JPUSHRegisterDelegate {
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (Int) -> Void
) {
JPUSHService.handleRemoteNotification(notification.request.content.userInfo)
let userInfo = notification.request.content.userInfo
Self.logReceivedPush(userInfo, source: "foreground")
JPUSHService.handleRemoteNotification(userInfo)
Task { @MainActor in
NotificationCenter.default.post(name: NotificationName.unreadMessageCountDidChange, object: nil)
}
@ -367,6 +395,7 @@ extension PushNotificationManager: JPUSHRegisterDelegate {
withCompletionHandler completionHandler: @escaping () -> Void
) {
let userInfo = response.notification.request.content.userInfo
Self.logReceivedPush(userInfo, source: "tap")
JPUSHService.handleRemoteNotification(userInfo)
let payload = PushPayload(userInfo: userInfo)
let requestIdentifier = response.notification.request.identifier

View File

@ -84,16 +84,8 @@ final class PushRouteCoordinator: PushRouting {
return
}
let destination: PushDestination
if route.destination.requiresScenicContext,
appStore.session.currentScenicId <= 0 {
destination = .messageCenter
} else {
destination = route.destination
}
mainTabController.dismiss(animated: false)
mainTabController.openPushDestination(destination)
mainTabController.openPushDestination(route.destination)
}
private func markHandled(_ identifier: String) {

View File

@ -67,12 +67,7 @@ final class MainTabBarController: UITabBarController {
/// Tab
func openPushDestination(_ destination: PushDestination) {
let targetTab: AppTab = switch destination {
case .orders, .verificationOrders:
.orders
case .payment, .task, .queue, .messageCenter:
.home
}
let targetTab = AppTab.home
selectTab(targetTab)
guard let navigationController = tabNavigationControllers[targetTab] else { return }
@ -80,16 +75,12 @@ final class MainTabBarController: UITabBarController {
let controller: UIViewController?
switch destination {
case .payment:
case .paymentRecord:
controller = PaymentCollectionRecordViewController()
case .paymentDetails:
controller = PaymentCollectionDetailsViewController()
case .task:
controller = TaskAddViewController()
case .queue:
controller = ScenicQueueViewController()
case .messageCenter:
controller = MessageCenterViewController()
case .orders, .verificationOrders:
controller = nil
}
if let controller {