Compare commits
4 Commits
v1.1.4
...
74f7370aab
| Author | SHA1 | Date | |
|---|---|---|---|
| 74f7370aab | |||
| 1b2637f0ee | |||
| caeeb9a1cf | |||
| e7f1d777dd |
3
Podfile
3
Podfile
@ -8,6 +8,9 @@ target 'suixinkan' do
|
||||
pod 'UMCommon'
|
||||
pod 'UMDevice'
|
||||
pod 'UMAPM'
|
||||
# 极光推送:固定无 IDFA 的 JCore,避免引入 ATT 授权。
|
||||
pod 'JCore', '5.4.2-noidfa'
|
||||
pod 'JPush', '6.1.0'
|
||||
pod 'IQKeyboardManagerSwift'
|
||||
# 微信 Open SDK:仅接入分享能力,不调用支付 API。
|
||||
pod 'WechatOpenSDK-XCFramework', '~> 2.0.4'
|
||||
|
||||
11
Podfile.lock
11
Podfile.lock
@ -46,6 +46,9 @@ PODS:
|
||||
- IQKeyboardCore
|
||||
- IQTextView (1.0.5):
|
||||
- IQKeyboardToolbar/Placeholderable
|
||||
- JCore (5.4.2-noidfa)
|
||||
- JPush (6.1.0):
|
||||
- JCore (> 5.1.1)
|
||||
- UMAPM (2.0.7):
|
||||
- UMCommon
|
||||
- UMCommon (7.5.11):
|
||||
@ -58,6 +61,8 @@ DEPENDENCIES:
|
||||
- AMapLocation-NO-IDFA
|
||||
- AMapSearch-NO-IDFA
|
||||
- IQKeyboardManagerSwift
|
||||
- JCore (= 5.4.2-noidfa)
|
||||
- JPush (= 6.1.0)
|
||||
- UMAPM
|
||||
- UMCommon
|
||||
- UMDevice
|
||||
@ -77,6 +82,8 @@ SPEC REPOS:
|
||||
- IQKeyboardToolbarManager
|
||||
- IQTextInputViewNotification
|
||||
- IQTextView
|
||||
- JCore
|
||||
- JPush
|
||||
- UMAPM
|
||||
- UMCommon
|
||||
- UMDevice
|
||||
@ -95,11 +102,13 @@ SPEC CHECKSUMS:
|
||||
IQKeyboardToolbarManager: 242e858cc74e06e00aa989bea1355b24d4397854
|
||||
IQTextInputViewNotification: 3b9fb27a16e7ee8958cc9092cfb07a1a9e1fd559
|
||||
IQTextView: ae13b4922f22e6f027f62c557d9f4f236b19d5c7
|
||||
JCore: 696e0ac22f417bb0ac4729fc41bc2941aef14e2b
|
||||
JPush: 891c497687be788b094bde368d51289355ee1112
|
||||
UMAPM: ba9fbebe8dd2048b251c332ab0afc86e1ba5ea63
|
||||
UMCommon: d652b3b372a801b36db203f5b546ab3e15676898
|
||||
UMDevice: 20b7b3c37a36b2c5c7ca8b5a54386ff9ed2b3b19
|
||||
WechatOpenSDK-XCFramework: ff342ae616bb86df3d236aca38059dfd4bc4a949
|
||||
|
||||
PODFILE CHECKSUM: 0fd38f741cbaae39c8e7a70fa233f651d3b34d0f
|
||||
PODFILE CHECKSUM: e7971a58b5d46ffc03f36ef716b922b6a75a6412
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
1
Pods/JCore/README.md
generated
Normal file
1
Pods/JCore/README.md
generated
Normal file
@ -0,0 +1 @@
|
||||
# jcore-sdk
|
||||
44
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/Info.plist
generated
Normal file
44
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/Info.plist
generated
Normal file
@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>AvailableLibraries</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>HeadersPath</key>
|
||||
<string>Headers</string>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>libJCore.a</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>SupportedPlatform</key>
|
||||
<string>ios</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>HeadersPath</key>
|
||||
<string>Headers</string>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64_x86_64-simulator</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>libJCore.a</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
<string>x86_64</string>
|
||||
</array>
|
||||
<key>SupportedPlatform</key>
|
||||
<string>ios</string>
|
||||
<key>SupportedPlatformVariant</key>
|
||||
<string>simulator</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XFWK</string>
|
||||
<key>XCFrameworkFormatVersion</key>
|
||||
<string>1.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
19
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/Headers/JGCOREAPI.h
generated
Normal file
19
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/Headers/JGCOREAPI.h
generated
Normal file
@ -0,0 +1,19 @@
|
||||
//
|
||||
// JGCOREService.h
|
||||
// JCore
|
||||
//
|
||||
// Created by Shuni Huang on 2024/3/19.
|
||||
// Copyright © 2024 jiguang. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface JGCOREAPI : NSObject
|
||||
|
||||
+ (void)ci:(BOOL)enable;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
27
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/Headers/JGInforCollectionAuth.h
generated
Normal file
27
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/Headers/JGInforCollectionAuth.h
generated
Normal file
@ -0,0 +1,27 @@
|
||||
//
|
||||
// JGInforCollectionAuth.h
|
||||
// JCore
|
||||
//
|
||||
// Created by 豆瓣 on 2021/10/27.
|
||||
// Copyright © 2021 jiguang. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// 合规接口,是否授权极光采集一定的设备信息
|
||||
@interface JGInforCollectionAuthItems : NSObject
|
||||
/// 是否授权,默认YES
|
||||
@property(nonatomic,assign)BOOL isAuth;
|
||||
@end
|
||||
|
||||
@interface JGInforCollectionAuth : NSObject
|
||||
|
||||
/// 设备信息采集授权接口(合规接口)
|
||||
/// 请务必在调用初始化、功能性接口前调用此接口进行合规授权
|
||||
/// @param authBlock auth:YES 则极光认为您同意极光采集一定的设备信息
|
||||
+(void)JCollectionAuth:(void(^_Nullable)(JGInforCollectionAuthItems *authInfo))authBlock;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
64
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/PrivacyInfo.xcprivacy
generated
Normal file
64
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/PrivacyInfo.xcprivacy
generated
Normal file
@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyTrackingDomains</key>
|
||||
<array/>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyCollectedDataType</key>
|
||||
<string>NSPrivacyCollectedDataTypeCrashData</string>
|
||||
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||
<array>
|
||||
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyCollectedDataType</key>
|
||||
<string>NSPrivacyCollectedDataTypePreciseLocation</string>
|
||||
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||
<array>
|
||||
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>E174.1</string>
|
||||
</array>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
</array>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/libJCore.a
generated
Normal file
BIN
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64/libJCore.a
generated
Normal file
Binary file not shown.
19
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator/Headers/JGCOREAPI.h
generated
Normal file
19
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator/Headers/JGCOREAPI.h
generated
Normal file
@ -0,0 +1,19 @@
|
||||
//
|
||||
// JGCOREService.h
|
||||
// JCore
|
||||
//
|
||||
// Created by Shuni Huang on 2024/3/19.
|
||||
// Copyright © 2024 jiguang. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface JGCOREAPI : NSObject
|
||||
|
||||
+ (void)ci:(BOOL)enable;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@ -0,0 +1,27 @@
|
||||
//
|
||||
// JGInforCollectionAuth.h
|
||||
// JCore
|
||||
//
|
||||
// Created by 豆瓣 on 2021/10/27.
|
||||
// Copyright © 2021 jiguang. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// 合规接口,是否授权极光采集一定的设备信息
|
||||
@interface JGInforCollectionAuthItems : NSObject
|
||||
/// 是否授权,默认YES
|
||||
@property(nonatomic,assign)BOOL isAuth;
|
||||
@end
|
||||
|
||||
@interface JGInforCollectionAuth : NSObject
|
||||
|
||||
/// 设备信息采集授权接口(合规接口)
|
||||
/// 请务必在调用初始化、功能性接口前调用此接口进行合规授权
|
||||
/// @param authBlock auth:YES 则极光认为您同意极光采集一定的设备信息
|
||||
+(void)JCollectionAuth:(void(^_Nullable)(JGInforCollectionAuthItems *authInfo))authBlock;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
64
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator/PrivacyInfo.xcprivacy
generated
Normal file
64
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator/PrivacyInfo.xcprivacy
generated
Normal file
@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyTrackingDomains</key>
|
||||
<array/>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyCollectedDataType</key>
|
||||
<string>NSPrivacyCollectedDataTypeCrashData</string>
|
||||
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||
<array>
|
||||
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyCollectedDataType</key>
|
||||
<string>NSPrivacyCollectedDataTypePreciseLocation</string>
|
||||
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||
<array>
|
||||
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>E174.1</string>
|
||||
</array>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
</array>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator/libJCore.a
generated
Normal file
BIN
Pods/JCore/jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator/libJCore.a
generated
Normal file
Binary file not shown.
44
Pods/JPush/jpush-ios-6.1.0.xcframework/Info.plist
generated
Normal file
44
Pods/JPush/jpush-ios-6.1.0.xcframework/Info.plist
generated
Normal file
@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>AvailableLibraries</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>HeadersPath</key>
|
||||
<string>Headers</string>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64_x86_64-simulator</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>libJPush.a</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
<string>x86_64</string>
|
||||
</array>
|
||||
<key>SupportedPlatform</key>
|
||||
<string>ios</string>
|
||||
<key>SupportedPlatformVariant</key>
|
||||
<string>simulator</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>HeadersPath</key>
|
||||
<string>Headers</string>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64</string>
|
||||
<key>LibraryPath</key>
|
||||
<string>libJPush.a</string>
|
||||
<key>SupportedArchitectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>SupportedPlatform</key>
|
||||
<string>ios</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XFWK</string>
|
||||
<key>XCFrameworkFormatVersion</key>
|
||||
<string>1.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
1023
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64/Headers/JPUSHService.h
generated
Normal file
1023
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64/Headers/JPUSHService.h
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64/PrivacyInfo.xcprivacy
generated
Normal file
23
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64/PrivacyInfo.xcprivacy
generated
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyTrackingDomains</key>
|
||||
<array/>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64/libJPush.a
generated
Normal file
BIN
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64/libJPush.a
generated
Normal file
Binary file not shown.
1023
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator/Headers/JPUSHService.h
generated
Normal file
1023
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator/Headers/JPUSHService.h
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator/PrivacyInfo.xcprivacy
generated
Normal file
23
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator/PrivacyInfo.xcprivacy
generated
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyTrackingDomains</key>
|
||||
<array/>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator/libJPush.a
generated
Normal file
BIN
Pods/JPush/jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator/libJPush.a
generated
Normal file
Binary file not shown.
11
Pods/Manifest.lock
generated
11
Pods/Manifest.lock
generated
@ -46,6 +46,9 @@ PODS:
|
||||
- IQKeyboardCore
|
||||
- IQTextView (1.0.5):
|
||||
- IQKeyboardToolbar/Placeholderable
|
||||
- JCore (5.4.2-noidfa)
|
||||
- JPush (6.1.0):
|
||||
- JCore (> 5.1.1)
|
||||
- UMAPM (2.0.7):
|
||||
- UMCommon
|
||||
- UMCommon (7.5.11):
|
||||
@ -58,6 +61,8 @@ DEPENDENCIES:
|
||||
- AMapLocation-NO-IDFA
|
||||
- AMapSearch-NO-IDFA
|
||||
- IQKeyboardManagerSwift
|
||||
- JCore (= 5.4.2-noidfa)
|
||||
- JPush (= 6.1.0)
|
||||
- UMAPM
|
||||
- UMCommon
|
||||
- UMDevice
|
||||
@ -77,6 +82,8 @@ SPEC REPOS:
|
||||
- IQKeyboardToolbarManager
|
||||
- IQTextInputViewNotification
|
||||
- IQTextView
|
||||
- JCore
|
||||
- JPush
|
||||
- UMAPM
|
||||
- UMCommon
|
||||
- UMDevice
|
||||
@ -95,11 +102,13 @@ SPEC CHECKSUMS:
|
||||
IQKeyboardToolbarManager: 242e858cc74e06e00aa989bea1355b24d4397854
|
||||
IQTextInputViewNotification: 3b9fb27a16e7ee8958cc9092cfb07a1a9e1fd559
|
||||
IQTextView: ae13b4922f22e6f027f62c557d9f4f236b19d5c7
|
||||
JCore: 696e0ac22f417bb0ac4729fc41bc2941aef14e2b
|
||||
JPush: 891c497687be788b094bde368d51289355ee1112
|
||||
UMAPM: ba9fbebe8dd2048b251c332ab0afc86e1ba5ea63
|
||||
UMCommon: d652b3b372a801b36db203f5b546ab3e15676898
|
||||
UMDevice: 20b7b3c37a36b2c5c7ca8b5a54386ff9ed2b3b19
|
||||
WechatOpenSDK-XCFramework: ff342ae616bb86df3d236aca38059dfd4bc4a949
|
||||
|
||||
PODFILE CHECKSUM: 0fd38f741cbaae39c8e7a70fa233f651d3b34d0f
|
||||
PODFILE CHECKSUM: e7971a58b5d46ffc03f36ef716b922b6a75a6412
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
5216
Pods/Pods.xcodeproj/project.pbxproj
generated
5216
Pods/Pods.xcodeproj/project.pbxproj
generated
File diff suppressed because it is too large
Load Diff
2
Pods/Target Support Files/JCore/JCore-xcframeworks-input-files.xcfilelist
generated
Normal file
2
Pods/Target Support Files/JCore/JCore-xcframeworks-input-files.xcfilelist
generated
Normal file
@ -0,0 +1,2 @@
|
||||
${PODS_ROOT}/Target Support Files/JCore/JCore-xcframeworks.sh
|
||||
${PODS_ROOT}/JCore/jcore-noidfa-ios-5.4.2.xcframework
|
||||
1
Pods/Target Support Files/JCore/JCore-xcframeworks-output-files.xcfilelist
generated
Normal file
1
Pods/Target Support Files/JCore/JCore-xcframeworks-output-files.xcfilelist
generated
Normal file
@ -0,0 +1 @@
|
||||
${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/jcore-noidfa-ios-5.4.2.framework
|
||||
121
Pods/Target Support Files/JCore/JCore-xcframeworks.sh
generated
Executable file
121
Pods/Target Support Files/JCore/JCore-xcframeworks.sh
generated
Executable file
@ -0,0 +1,121 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
set -u
|
||||
set -o pipefail
|
||||
|
||||
function on_error {
|
||||
echo "$(realpath -mq "${0}"):$1: error: Unexpected failure"
|
||||
}
|
||||
trap 'on_error $LINENO' ERR
|
||||
|
||||
|
||||
# This protects against multiple targets copying the same framework dependency at the same time. The solution
|
||||
# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html
|
||||
RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????")
|
||||
|
||||
|
||||
variant_for_slice()
|
||||
{
|
||||
case "$1" in
|
||||
"jcore-noidfa-ios-5.4.2.xcframework/ios-arm64")
|
||||
echo ""
|
||||
;;
|
||||
"jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator")
|
||||
echo "simulator"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
archs_for_slice()
|
||||
{
|
||||
case "$1" in
|
||||
"jcore-noidfa-ios-5.4.2.xcframework/ios-arm64")
|
||||
echo "arm64"
|
||||
;;
|
||||
"jcore-noidfa-ios-5.4.2.xcframework/ios-arm64_x86_64-simulator")
|
||||
echo "arm64 x86_64"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
copy_dir()
|
||||
{
|
||||
local source="$1"
|
||||
local destination="$2"
|
||||
|
||||
# Use filter instead of exclude so missing patterns don't throw errors.
|
||||
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" \"${source}*\" \"${destination}\""
|
||||
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" "${source}"/* "${destination}"
|
||||
}
|
||||
|
||||
SELECT_SLICE_RETVAL=""
|
||||
|
||||
select_slice() {
|
||||
local xcframework_name="$1"
|
||||
xcframework_name="${xcframework_name##*/}"
|
||||
local paths=("${@:2}")
|
||||
# Locate the correct slice of the .xcframework for the current architectures
|
||||
local target_path=""
|
||||
|
||||
# Split archs on space so we can find a slice that has all the needed archs
|
||||
local target_archs=$(echo $ARCHS | tr " " "\n")
|
||||
|
||||
local target_variant=""
|
||||
if [[ "$PLATFORM_NAME" == *"simulator" ]]; then
|
||||
target_variant="simulator"
|
||||
fi
|
||||
if [[ ! -z ${EFFECTIVE_PLATFORM_NAME+x} && "$EFFECTIVE_PLATFORM_NAME" == *"maccatalyst" ]]; then
|
||||
target_variant="maccatalyst"
|
||||
fi
|
||||
for i in ${!paths[@]}; do
|
||||
local matched_all_archs="1"
|
||||
local slice_archs="$(archs_for_slice "${xcframework_name}/${paths[$i]}")"
|
||||
local slice_variant="$(variant_for_slice "${xcframework_name}/${paths[$i]}")"
|
||||
for target_arch in $target_archs; do
|
||||
if ! [[ "${slice_variant}" == "$target_variant" ]]; then
|
||||
matched_all_archs="0"
|
||||
break
|
||||
fi
|
||||
|
||||
if ! echo "${slice_archs}" | tr " " "\n" | grep -F -q -x "$target_arch"; then
|
||||
matched_all_archs="0"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$matched_all_archs" == "1" ]]; then
|
||||
# Found a matching slice
|
||||
echo "Selected xcframework slice ${paths[$i]}"
|
||||
SELECT_SLICE_RETVAL=${paths[$i]}
|
||||
break
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
install_xcframework() {
|
||||
local basepath="$1"
|
||||
local name="$2"
|
||||
local package_type="$3"
|
||||
local paths=("${@:4}")
|
||||
|
||||
# Locate the correct slice of the .xcframework for the current architectures
|
||||
select_slice "${basepath}" "${paths[@]}"
|
||||
local target_path="$SELECT_SLICE_RETVAL"
|
||||
if [[ -z "$target_path" ]]; then
|
||||
echo "warning: [CP] $(basename ${basepath}): Unable to find matching slice in '${paths[@]}' for the current build architectures ($ARCHS) and platform (${EFFECTIVE_PLATFORM_NAME-${PLATFORM_NAME}})."
|
||||
return
|
||||
fi
|
||||
local source="$basepath/$target_path"
|
||||
|
||||
local destination="${PODS_XCFRAMEWORKS_BUILD_DIR}/${name}"
|
||||
|
||||
if [ ! -d "$destination" ]; then
|
||||
mkdir -p "$destination"
|
||||
fi
|
||||
|
||||
copy_dir "$source/" "$destination"
|
||||
echo "Copied $source to $destination"
|
||||
}
|
||||
|
||||
install_xcframework "${PODS_ROOT}/JCore/jcore-noidfa-ios-5.4.2.xcframework" "JCore" "library" "ios-arm64" "ios-arm64_x86_64-simulator"
|
||||
|
||||
17
Pods/Target Support Files/JCore/JCore.debug.xcconfig
generated
Normal file
17
Pods/Target Support Files/JCore/JCore.debug.xcconfig
generated
Normal file
@ -0,0 +1,17 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/JCore
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/JCore"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers"
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore"
|
||||
OTHER_LDFLAGS = $(inherited) -l"JCore" -l"resolv" -l"z" -framework "CFNetwork" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreTelephony" -framework "Foundation" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "WebKit" -weak_framework "Network" -weak_framework "UserNotifications"
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/JCore
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
VALID_ARCHS = x86_64 arm64
|
||||
17
Pods/Target Support Files/JCore/JCore.release.xcconfig
generated
Normal file
17
Pods/Target Support Files/JCore/JCore.release.xcconfig
generated
Normal file
@ -0,0 +1,17 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/JCore
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/JCore"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers"
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore"
|
||||
OTHER_LDFLAGS = $(inherited) -l"JCore" -l"resolv" -l"z" -framework "CFNetwork" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreTelephony" -framework "Foundation" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "WebKit" -weak_framework "Network" -weak_framework "UserNotifications"
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/JCore
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
VALID_ARCHS = x86_64 arm64
|
||||
2
Pods/Target Support Files/JPush/JPush-xcframeworks-input-files.xcfilelist
generated
Normal file
2
Pods/Target Support Files/JPush/JPush-xcframeworks-input-files.xcfilelist
generated
Normal file
@ -0,0 +1,2 @@
|
||||
${PODS_ROOT}/Target Support Files/JPush/JPush-xcframeworks.sh
|
||||
${PODS_ROOT}/JPush/jpush-ios-6.1.0.xcframework
|
||||
1
Pods/Target Support Files/JPush/JPush-xcframeworks-output-files.xcfilelist
generated
Normal file
1
Pods/Target Support Files/JPush/JPush-xcframeworks-output-files.xcfilelist
generated
Normal file
@ -0,0 +1 @@
|
||||
${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/jpush-ios-6.1.0.framework
|
||||
121
Pods/Target Support Files/JPush/JPush-xcframeworks.sh
generated
Executable file
121
Pods/Target Support Files/JPush/JPush-xcframeworks.sh
generated
Executable file
@ -0,0 +1,121 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
set -u
|
||||
set -o pipefail
|
||||
|
||||
function on_error {
|
||||
echo "$(realpath -mq "${0}"):$1: error: Unexpected failure"
|
||||
}
|
||||
trap 'on_error $LINENO' ERR
|
||||
|
||||
|
||||
# This protects against multiple targets copying the same framework dependency at the same time. The solution
|
||||
# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html
|
||||
RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????")
|
||||
|
||||
|
||||
variant_for_slice()
|
||||
{
|
||||
case "$1" in
|
||||
"jpush-ios-6.1.0.xcframework/ios-arm64")
|
||||
echo ""
|
||||
;;
|
||||
"jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator")
|
||||
echo "simulator"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
archs_for_slice()
|
||||
{
|
||||
case "$1" in
|
||||
"jpush-ios-6.1.0.xcframework/ios-arm64")
|
||||
echo "arm64"
|
||||
;;
|
||||
"jpush-ios-6.1.0.xcframework/ios-arm64_x86_64-simulator")
|
||||
echo "arm64 x86_64"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
copy_dir()
|
||||
{
|
||||
local source="$1"
|
||||
local destination="$2"
|
||||
|
||||
# Use filter instead of exclude so missing patterns don't throw errors.
|
||||
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" \"${source}*\" \"${destination}\""
|
||||
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" "${source}"/* "${destination}"
|
||||
}
|
||||
|
||||
SELECT_SLICE_RETVAL=""
|
||||
|
||||
select_slice() {
|
||||
local xcframework_name="$1"
|
||||
xcframework_name="${xcframework_name##*/}"
|
||||
local paths=("${@:2}")
|
||||
# Locate the correct slice of the .xcframework for the current architectures
|
||||
local target_path=""
|
||||
|
||||
# Split archs on space so we can find a slice that has all the needed archs
|
||||
local target_archs=$(echo $ARCHS | tr " " "\n")
|
||||
|
||||
local target_variant=""
|
||||
if [[ "$PLATFORM_NAME" == *"simulator" ]]; then
|
||||
target_variant="simulator"
|
||||
fi
|
||||
if [[ ! -z ${EFFECTIVE_PLATFORM_NAME+x} && "$EFFECTIVE_PLATFORM_NAME" == *"maccatalyst" ]]; then
|
||||
target_variant="maccatalyst"
|
||||
fi
|
||||
for i in ${!paths[@]}; do
|
||||
local matched_all_archs="1"
|
||||
local slice_archs="$(archs_for_slice "${xcframework_name}/${paths[$i]}")"
|
||||
local slice_variant="$(variant_for_slice "${xcframework_name}/${paths[$i]}")"
|
||||
for target_arch in $target_archs; do
|
||||
if ! [[ "${slice_variant}" == "$target_variant" ]]; then
|
||||
matched_all_archs="0"
|
||||
break
|
||||
fi
|
||||
|
||||
if ! echo "${slice_archs}" | tr " " "\n" | grep -F -q -x "$target_arch"; then
|
||||
matched_all_archs="0"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$matched_all_archs" == "1" ]]; then
|
||||
# Found a matching slice
|
||||
echo "Selected xcframework slice ${paths[$i]}"
|
||||
SELECT_SLICE_RETVAL=${paths[$i]}
|
||||
break
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
install_xcframework() {
|
||||
local basepath="$1"
|
||||
local name="$2"
|
||||
local package_type="$3"
|
||||
local paths=("${@:4}")
|
||||
|
||||
# Locate the correct slice of the .xcframework for the current architectures
|
||||
select_slice "${basepath}" "${paths[@]}"
|
||||
local target_path="$SELECT_SLICE_RETVAL"
|
||||
if [[ -z "$target_path" ]]; then
|
||||
echo "warning: [CP] $(basename ${basepath}): Unable to find matching slice in '${paths[@]}' for the current build architectures ($ARCHS) and platform (${EFFECTIVE_PLATFORM_NAME-${PLATFORM_NAME}})."
|
||||
return
|
||||
fi
|
||||
local source="$basepath/$target_path"
|
||||
|
||||
local destination="${PODS_XCFRAMEWORKS_BUILD_DIR}/${name}"
|
||||
|
||||
if [ ! -d "$destination" ]; then
|
||||
mkdir -p "$destination"
|
||||
fi
|
||||
|
||||
copy_dir "$source/" "$destination"
|
||||
echo "Copied $source to $destination"
|
||||
}
|
||||
|
||||
install_xcframework "${PODS_ROOT}/JPush/jpush-ios-6.1.0.xcframework" "JPush" "library" "ios-arm64" "ios-arm64_x86_64-simulator"
|
||||
|
||||
15
Pods/Target Support Files/JPush/JPush.debug.xcconfig
generated
Normal file
15
Pods/Target Support Files/JPush/JPush.debug.xcconfig
generated
Normal file
@ -0,0 +1,15 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/JPush
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/JCore" "${PODS_ROOT}/JPush"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/Headers"
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/JPush
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
SWIFT_INCLUDE_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore"
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
15
Pods/Target Support Files/JPush/JPush.release.xcconfig
generated
Normal file
15
Pods/Target Support Files/JPush/JPush.release.xcconfig
generated
Normal file
@ -0,0 +1,15 @@
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/JPush
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/JCore" "${PODS_ROOT}/JPush"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/Headers"
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE}
|
||||
PODS_ROOT = ${SRCROOT}
|
||||
PODS_TARGET_SRCROOT = ${PODS_ROOT}/JPush
|
||||
PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier}
|
||||
SKIP_INSTALL = YES
|
||||
SWIFT_INCLUDE_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore"
|
||||
USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES
|
||||
@ -219,6 +219,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
## JCore
|
||||
|
||||
Copyright jpush.cn
|
||||
|
||||
## JPush
|
||||
|
||||
Copyright jiguang.cn
|
||||
|
||||
## UMAPM
|
||||
|
||||
Copyright 2011 - 2020 umeng.com. All rights reserved.
|
||||
|
||||
@ -302,6 +302,26 @@ SOFTWARE.
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Copyright jpush.cn</string>
|
||||
<key>License</key>
|
||||
<string>Copyright</string>
|
||||
<key>Title</key>
|
||||
<string>JCore</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Copyright jiguang.cn</string>
|
||||
<key>License</key>
|
||||
<string>Copyright</string>
|
||||
<key>Title</key>
|
||||
<string>JPush</string>
|
||||
<key>Type</key>
|
||||
<string>PSGroupSpecifier</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>FooterText</key>
|
||||
<string>Copyright 2011 - 2020 umeng.com. All rights reserved.
|
||||
|
||||
@ -3,13 +3,13 @@ ARCHS = $(ARCHS_STANDARD)
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
||||
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/JCore" "${PODS_ROOT}/JPush" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers"
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/Headers"
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||
OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "AMapFoundationKit" -framework "AMapLocationKit" -framework "AMapSearchKit" -framework "Combine" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "MAMapKit" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "UMAPM" -framework "UMCommon" -framework "UMDevice" -framework "WebKit" -framework "WechatOpenSDK"
|
||||
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/AMap3DMap-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapFoundation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapLocation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapSearch-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMAPM" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMCommon" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMDevice" "-F${PODS_CONFIGURATION_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush" "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||
OTHER_LDFLAGS = $(inherited) -ObjC -l"JCore" -l"JPush" -l"c++" -l"resolv" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "AMapFoundationKit" -framework "AMapLocationKit" -framework "AMapSearchKit" -framework "CFNetwork" -framework "Combine" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "Foundation" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "MAMapKit" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "UMAPM" -framework "UMCommon" -framework "UMDevice" -framework "WebKit" -framework "WechatOpenSDK" -weak_framework "Network" -weak_framework "UserNotifications"
|
||||
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/AMap3DMap-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapFoundation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapLocation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapSearch-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "-F${PODS_CONFIGURATION_BUILD_DIR}/JCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/JPush" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMAPM" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMCommon" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMDevice" "-F${PODS_CONFIGURATION_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
|
||||
@ -3,13 +3,13 @@ ARCHS = $(ARCHS_STANDARD)
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
||||
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/JCore" "${PODS_ROOT}/JPush" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers"
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/Headers"
|
||||
LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks'
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||
OTHER_LDFLAGS = $(inherited) -ObjC -l"c++" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "AMapFoundationKit" -framework "AMapLocationKit" -framework "AMapSearchKit" -framework "Combine" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "MAMapKit" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "UMAPM" -framework "UMCommon" -framework "UMDevice" -framework "WebKit" -framework "WechatOpenSDK"
|
||||
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/AMap3DMap-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapFoundation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapLocation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapSearch-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMAPM" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMCommon" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMDevice" "-F${PODS_CONFIGURATION_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||
LIBRARY_SEARCH_PATHS = $(inherited) "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush" "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift
|
||||
OTHER_LDFLAGS = $(inherited) -ObjC -l"JCore" -l"JPush" -l"c++" -l"resolv" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "AMapFoundationKit" -framework "AMapLocationKit" -framework "AMapSearchKit" -framework "CFNetwork" -framework "Combine" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "Foundation" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "MAMapKit" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "UMAPM" -framework "UMCommon" -framework "UMDevice" -framework "WebKit" -framework "WechatOpenSDK" -weak_framework "Network" -weak_framework "UserNotifications"
|
||||
OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/AMap3DMap-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapFoundation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapLocation-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/AMapSearch-NO-IDFA" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "-F${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "-F${PODS_CONFIGURATION_BUILD_DIR}/JCore" "-F${PODS_CONFIGURATION_BUILD_DIR}/JPush" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMAPM" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMCommon" "-F${PODS_CONFIGURATION_BUILD_DIR}/UMDevice" "-F${PODS_CONFIGURATION_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||
OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
|
||||
@ -2,10 +2,10 @@ ARCHS = $(ARCHS_STANDARD)
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
||||
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/JCore" "${PODS_ROOT}/JPush" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers"
|
||||
OTHER_LDFLAGS = $(inherited) -l"c++" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "Combine" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "WebKit"
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/Headers"
|
||||
OTHER_LDFLAGS = $(inherited) -l"c++" -l"resolv" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "CFNetwork" -framework "Combine" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "Foundation" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "WebKit" -weak_framework "Network" -weak_framework "UserNotifications"
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
|
||||
|
||||
@ -2,10 +2,10 @@ ARCHS = $(ARCHS_STANDARD)
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO
|
||||
EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||
FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView" "${PODS_ROOT}/AMap3DMap-NO-IDFA" "${PODS_ROOT}/AMapFoundation-NO-IDFA" "${PODS_ROOT}/AMapLocation-NO-IDFA" "${PODS_ROOT}/AMapSearch-NO-IDFA" "${PODS_ROOT}/JCore" "${PODS_ROOT}/JPush" "${PODS_ROOT}/UMAPM/UMAPM_2.0.7" "${PODS_ROOT}/UMCommon/UMCommon_7.5.11" "${PODS_ROOT}/UMDevice/UMDevice_3.6.0" "${PODS_ROOT}/WechatOpenSDK-XCFramework" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMAPM" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMCommon" "${PODS_XCFRAMEWORKS_BUILD_DIR}/UMDevice" "${PODS_XCFRAMEWORKS_BUILD_DIR}/WechatOpenSDK-XCFramework"
|
||||
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers"
|
||||
OTHER_LDFLAGS = $(inherited) -l"c++" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "Combine" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "WebKit"
|
||||
HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardCore/IQKeyboardCore.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IQTextView/IQTextView.framework/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JCore/Headers" "${PODS_XCFRAMEWORKS_BUILD_DIR}/JPush/Headers"
|
||||
OTHER_LDFLAGS = $(inherited) -l"c++" -l"resolv" -l"sqlite3" -l"sqlite3.0" -l"z" -framework "CFNetwork" -framework "Combine" -framework "CoreFoundation" -framework "CoreGraphics" -framework "CoreLocation" -framework "CoreTelephony" -framework "CoreText" -framework "ExternalAccessory" -framework "Foundation" -framework "GLKit" -framework "IQKeyboardCore" -framework "IQKeyboardManagerSwift" -framework "IQKeyboardNotification" -framework "IQKeyboardReturnManager" -framework "IQKeyboardToolbar" -framework "IQKeyboardToolbarManager" -framework "IQTextInputViewNotification" -framework "IQTextView" -framework "OpenGLES" -framework "QuartzCore" -framework "Security" -framework "SystemConfiguration" -framework "UIKit" -framework "WebKit" -weak_framework "Network" -weak_framework "UserNotifications"
|
||||
PODS_BUILD_DIR = ${BUILD_DIR}
|
||||
PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)
|
||||
PODS_PODFILE_DIR_PATH = ${SRCROOT}/.
|
||||
|
||||
484
docs/后端消息推送格式.md
Normal file
484
docs/后端消息推送格式.md
Normal file
@ -0,0 +1,484 @@
|
||||
# 后端消息推送格式
|
||||
|
||||
## 1. 文档说明
|
||||
|
||||
- 本文整理后端经极光推送发送到 Android 和 iOS 的通知消息格式。
|
||||
- 当前内容基于截至 2026-07-23 的 Android、iOS 实际收包日志、两端客户端现有解析逻辑,以及后端开发人员提供的 `PushMsg` 类型定义。
|
||||
- 后端当前定义了 `1`~`13`、`100`、`999` 共 15 个业务消息类型;目前已取得 `type = 1`(付款成功)、`type = 2`(退款成功)、`type = 6`(扫码成功)和 `type = 13`(新获客线索)的收包样本。
|
||||
- 本文描述的是推送协议,不是普通 HTTP 接口响应格式。
|
||||
|
||||
## 2. 统一业务结构
|
||||
|
||||
极光在 Android 和 iOS 上交付的原始结构不同,但当前消息都可以归一化为以下业务结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "通知标题",
|
||||
"content": "通知正文",
|
||||
"msg_id": 1858,
|
||||
"type": 6,
|
||||
"data": {
|
||||
"order_type": 14
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.1 公共业务字段
|
||||
|
||||
| 字段 | 当前类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `title` | string | 通知标题。Android 为 `notificationTitle`,iOS 为 `aps.alert.title`。 |
|
||||
| `content` | string | 通知正文。Android 为 `notificationContent`,iOS 为 `aps.alert.body`。 |
|
||||
| `msg_id` | int | 后端业务消息 ID。它与极光消息 ID 不是同一个字段。 |
|
||||
| `type` | int | 业务消息类型,是客户端业务分发的主要依据。 |
|
||||
| `data` | object | 随消息类型变化的业务数据。 |
|
||||
|
||||
> “当前类型”表示现有样本中观察到的 JSON 类型。后端应保持字段类型稳定,避免同一个字段在不同推送中交替使用数字、字符串或 `null`。
|
||||
|
||||
## 3. Android 收包格式
|
||||
|
||||
Android 通过极光 `JPushMessageReceiver.onNotifyMessageArrived` 收到通知。标题、正文和附加字段分别由 SDK 提供,其中 `extras` 是 JSON 字符串:
|
||||
|
||||
```text
|
||||
[onNotifyMessageArrived] msgId=<极光消息ID>, title=<标题>, content=<正文>, extras=<业务JSON字符串>
|
||||
```
|
||||
|
||||
`extras` 的当前结构为:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {},
|
||||
"msg_id": 0,
|
||||
"type": 0
|
||||
}
|
||||
```
|
||||
|
||||
### 3.1 扫码成功原始日志
|
||||
|
||||
```text
|
||||
[onNotifyMessageArrived] msgId=18103321643292031, title=扫码成功通知, content=扫码成功, extras={"data":{"order_type":14},"msg_id":1852,"type":6}
|
||||
```
|
||||
|
||||
对应的 `extras`:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"order_type": 14
|
||||
},
|
||||
"msg_id": 1852,
|
||||
"type": 6
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 付款成功原始日志
|
||||
|
||||
```text
|
||||
[onNotifyMessageArrived] msgId=18103321644290274, title=付款成功通知, content=您的订单 #260722128005 已支付成功,支付金额 ¥0.01, extras={"data":{"order_amount":"0.01","order_number":"260722128005","order_type":14,"pay_time":1784711036,"remark":""},"msg_id":1853,"type":1}
|
||||
```
|
||||
|
||||
对应的 `extras`:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"order_amount": "0.01",
|
||||
"order_number": "260722128005",
|
||||
"order_type": 14,
|
||||
"pay_time": 1784711036,
|
||||
"remark": ""
|
||||
},
|
||||
"msg_id": 1853,
|
||||
"type": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 退款成功原始日志
|
||||
|
||||
```text
|
||||
[onNotifyMessageArrived] msgId=18103321800527159, title=退款成功通知, content=您的订单 #260722128002 已成功退款,退款金额 ¥0.01, extras={"data":{},"msg_id":1867,"type":2}
|
||||
```
|
||||
|
||||
对应的 `extras`:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {},
|
||||
"msg_id": 1867,
|
||||
"type": 2
|
||||
}
|
||||
```
|
||||
|
||||
## 4. iOS 收包格式
|
||||
|
||||
iOS 通过 APNs/极光收到 `userInfo` 字典。与 Android 相比:
|
||||
|
||||
- 标题和正文位于 `aps.alert`。
|
||||
- Android `extras` 中的 `msg_id`、`type`、`data` 在 iOS 中位于 payload 顶层。
|
||||
- `_j_*` 为极光传输层内部字段,不应作为业务分发依据。
|
||||
|
||||
通用结构如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"_j_business": 1,
|
||||
"_j_data": "{\"data_msgtype\":1,\"push_type\":8,\"is_vip\":0}",
|
||||
"_j_msgid": 18103321726881995,
|
||||
"_j_uid": 86399144805,
|
||||
"aps": {
|
||||
"alert": {
|
||||
"body": "通知正文",
|
||||
"title": "通知标题"
|
||||
},
|
||||
"sound": "default"
|
||||
},
|
||||
"data": {},
|
||||
"msg_id": 0,
|
||||
"type": 0
|
||||
}
|
||||
```
|
||||
|
||||
> 原始日志中该内部字段显示为 `*j\_data*`。本文按极光其他内部字段的命名方式记为 `_j_data`;后续联调时应以设备收到的真实字典 key 为准。该字段目前不参与业务解析。
|
||||
|
||||
### 4.1 扫码成功 payload
|
||||
|
||||
```json
|
||||
{
|
||||
"_j_business": 1,
|
||||
"_j_data": "{\"data_msgtype\":1,\"push_type\":8,\"is_vip\":0}",
|
||||
"_j_msgid": 18103321726881995,
|
||||
"_j_uid": 86399144805,
|
||||
"aps": {
|
||||
"alert": {
|
||||
"body": "扫码成功",
|
||||
"title": "扫码成功通知"
|
||||
},
|
||||
"sound": "default"
|
||||
},
|
||||
"data": {
|
||||
"order_type": 14
|
||||
},
|
||||
"msg_id": 1858,
|
||||
"type": 6
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 付款成功 payload
|
||||
|
||||
```json
|
||||
{
|
||||
"_j_business": 1,
|
||||
"_j_data": "{\"data_msgtype\":1,\"push_type\":8,\"is_vip\":0}",
|
||||
"_j_msgid": 18103321720587147,
|
||||
"_j_uid": 86399144805,
|
||||
"aps": {
|
||||
"alert": {
|
||||
"body": "您的订单 #260722128006 已支付成功,支付金额 ¥0.01",
|
||||
"title": "付款成功通知"
|
||||
},
|
||||
"sound": "default"
|
||||
},
|
||||
"data": {
|
||||
"order_amount": "0.01",
|
||||
"order_number": "260722128006",
|
||||
"order_type": 14,
|
||||
"pay_time": 1784712671,
|
||||
"remark": ""
|
||||
},
|
||||
"msg_id": 1856,
|
||||
"type": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 退款成功 payload
|
||||
|
||||
```json
|
||||
{
|
||||
"_j_business": 1,
|
||||
"_j_data": "{\"data_msgtype\":1,\"push_type\":8,\"is_vip\":0}",
|
||||
"_j_msgid": 18103321792603316,
|
||||
"_j_uid": 86399144805,
|
||||
"aps": {
|
||||
"alert": {
|
||||
"body": "您的订单 #260722128005 已成功退款,退款金额 ¥0.01",
|
||||
"title": "退款成功通知"
|
||||
},
|
||||
"sound": "default"
|
||||
},
|
||||
"data": {},
|
||||
"msg_id": 1862,
|
||||
"type": 2
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 新获客线索 payload
|
||||
|
||||
```json
|
||||
{
|
||||
"_j_business": 1,
|
||||
"_j_data": "{\"data_msgtype\":1,\"push_type\":8,\"is_vip\":0}",
|
||||
"_j_msgid": 18103323696468714,
|
||||
"_j_uid": 86399144805,
|
||||
"aps": {
|
||||
"alert": {
|
||||
"body": "获客员哦啦啦提交了一条新线索,请及时查看并跟进",
|
||||
"title": "新获客线索通知"
|
||||
},
|
||||
"sound": "default"
|
||||
},
|
||||
"data": {
|
||||
"lead_id": 75,
|
||||
"sale_user_id": 6,
|
||||
"saler_name": "哦啦啦"
|
||||
},
|
||||
"msg_id": 1869,
|
||||
"type": 13
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Android 与 iOS 字段对应关系
|
||||
|
||||
| 语义 | Android | iOS | 是否用于业务 |
|
||||
| --- | --- | --- | --- |
|
||||
| 极光消息 ID | SDK `message.msgId` | `_j_msgid` | 否,仅用于推送链路日志与排查 |
|
||||
| 通知标题 | SDK `message.notificationTitle` | `aps.alert.title` | 是,用于展示 |
|
||||
| 通知正文 | SDK `message.notificationContent` | `aps.alert.body` | 是,用于展示 |
|
||||
| 后端业务消息 ID | `extras.msg_id` | 顶层 `msg_id` | 是 |
|
||||
| 业务消息类型 | `extras.type` | 顶层 `type` | 是,作为业务分发主键 |
|
||||
| 业务数据 | `extras.data` | 顶层 `data` | 是,结构由 `type` 决定 |
|
||||
| 极光业务标记 | SDK 内部处理 | `_j_business` | 否 |
|
||||
| 极光附加元数据 | SDK 内部处理 | `_j_data` | 否 |
|
||||
| 极光用户 ID | SDK 内部处理 | `_j_uid` | 否 |
|
||||
| APNs 展示配置 | 不适用 | `aps` | 仅 iOS 系统使用 |
|
||||
|
||||
### 5.1 两种消息 ID 不可混用
|
||||
|
||||
- `msgId` / `_j_msgid`:极光生成的传输层消息 ID,当前样本为 17 位数字。
|
||||
- `msg_id`:后端生成的业务消息 ID,当前样本为 `1852`、`1853`、`1856`、`1858`。
|
||||
- 客户端日志、去重或上报时必须明确需要哪一种 ID,不能仅用名称相近而互相替代。
|
||||
- 极光消息 ID 已超过 JavaScript 可安全表示的整数范围;若需要经过 JavaScript、WebView 或其他只能安全处理 53 位整数的链路,应按字符串传递。
|
||||
|
||||
## 6. 消息类型清单
|
||||
|
||||
### 6.1 后端 `PushMsg` 完整类型定义
|
||||
|
||||
定义位置:后端 `PushMsg.php` 第 49 行附近。
|
||||
|
||||
| `type` | 消息类型 | 后端备注 | 当前收包样本 |
|
||||
| ---: | --- | --- | --- |
|
||||
| `1` | 付款成功通知 | 上文实际示例即为此类型 | Android、iOS 均已取得 |
|
||||
| `2` | 退款成功通知 | — | Android、iOS 均已取得 |
|
||||
| `3` | 下单成功通知 | App 无需推送,当前未使用 | 暂无 |
|
||||
| `4` | 签到成功通知 | — | 暂无 |
|
||||
| `5` | 付尾款成功通知 | — | 暂无 |
|
||||
| `6` | 扫码成功通知 | — | Android、iOS 均已取得 |
|
||||
| `7` | 付定金成功通知 | — | 暂无 |
|
||||
| `8` | 订单核销成功通知 | — | 暂无 |
|
||||
| `9` | 剪辑完成通知 | — | 暂无 |
|
||||
| `10` | 修图完成通知 | — | 暂无 |
|
||||
| `11` | 跟拍签到通知 | — | 暂无 |
|
||||
| `12` | 剪辑师新任务通知 | 类型名称映射中漏配 | 暂无 |
|
||||
| `13` | 新获客线索通知 | 获客员提交新线索 | iOS 已取得,Android 暂无 |
|
||||
| `100` | 用户登出通知 | 账号在其他设备登录 | 暂无 |
|
||||
| `999` | 系统消息 | 当前未使用 | 暂无 |
|
||||
|
||||
后端特别说明:
|
||||
|
||||
- `type` 表示业务消息类型,不代表推送渠道。
|
||||
- 推送渠道的取值另有定义:`1` 为极光推送,`2` 为微信小程序,`999` 为不推送。
|
||||
- `type = 12` 虽然已经定义,但尚未加入后端 `MAP_TYPE_NAMES`。后端调用 `getTypeName(12)` 时会返回“未知消息类型”,需要后端补充映射。
|
||||
- “暂无收包样本”只表示当前文档还没有该类型的完整 payload,不能据此认为该类型未启用。收到后应继续补充其标题、正文、`data` 字段和点击行为。
|
||||
|
||||
### 6.2 `type = 1`:付款成功
|
||||
|
||||
| 项目 | 当前值 |
|
||||
| --- | --- |
|
||||
| 标题示例 | `付款成功通知` |
|
||||
| 正文示例 | `您的订单 #260722128006 已支付成功,支付金额 ¥0.01` |
|
||||
| 客户端点击行为 | Android、iOS 均进入收款记录页 |
|
||||
|
||||
`data` 字段:
|
||||
|
||||
| 字段 | 类型 | 当前样本 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `order_amount` | string | `"0.01"` | 支付金额。使用十进制字符串,避免浮点精度问题。 |
|
||||
| `order_number` | string | `"260722128006"` | 业务订单号。 |
|
||||
| `order_type` | int | `14` | 订单类型;当前值表示“摄影师跟拍线下扫码”。 |
|
||||
| `pay_time` | int | `1784712671` | 支付时间。当前样本为 Unix 秒级时间戳;Android 现有代码同时兼容秒和毫秒。 |
|
||||
| `remark` | string | `""` | 付款备注;没有备注时当前返回空字符串。 |
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": 1,
|
||||
"msg_id": 1856,
|
||||
"data": {
|
||||
"order_amount": "0.01",
|
||||
"order_number": "260722128006",
|
||||
"order_type": 14,
|
||||
"pay_time": 1784712671,
|
||||
"remark": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 `type = 2`:退款成功
|
||||
|
||||
| 项目 | 当前值 |
|
||||
| --- | --- |
|
||||
| 标题示例 | `退款成功通知` |
|
||||
| 正文示例 | `您的订单 #260722128005 已成功退款,退款金额 ¥0.01` |
|
||||
| 客户端点击行为 | Android、iOS 均进入消息中心 |
|
||||
|
||||
`data` 字段:
|
||||
|
||||
| 字段 | 类型 | 当前样本 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| — | object | `{}` | 当前 Android、iOS 样本均为空对象。 |
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": 2,
|
||||
"msg_id": 1862,
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
> 当前订单号和退款金额只存在于展示正文中,没有以结构化字段放入 `data`。客户端如果需要可靠地刷新指定订单、展示退款详情或执行金额计算,应由后端在 `data` 中提供字段,不应解析自然语言正文。
|
||||
|
||||
### 6.4 `type = 6`:扫码成功
|
||||
|
||||
| 项目 | 当前值 |
|
||||
| --- | --- |
|
||||
| 标题示例 | `扫码成功通知` |
|
||||
| 正文示例 | `扫码成功` |
|
||||
| 客户端点击行为 | Android、iOS 均进入收款详情页 |
|
||||
|
||||
`data` 字段:
|
||||
|
||||
| 字段 | 类型 | 当前样本 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `order_type` | int | `14` | 订单类型;`14` 在 Android 现有代码中表示“摄影师跟拍线下扫码”。 |
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": 6,
|
||||
"msg_id": 1858,
|
||||
"data": {
|
||||
"order_type": 14
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.5 `type = 13`:新获客线索
|
||||
|
||||
| 项目 | 当前值 |
|
||||
| --- | --- |
|
||||
| 标题示例 | `新获客线索通知` |
|
||||
| 正文示例 | `获客员哦啦啦提交了一条新线索,请及时查看并跟进` |
|
||||
| 客户端点击行为 | Android、iOS 均进入消息中心 |
|
||||
|
||||
`data` 字段:
|
||||
|
||||
| 字段 | 类型 | 当前样本 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `lead_id` | int | `75` | 新增获客线索的唯一 ID。 |
|
||||
| `sale_user_id` | int | `6` | 提交线索的获客员 ID。 |
|
||||
| `saler_name` | string | `"哦啦啦"` | 获客员名称,与通知正文中的名称一致。 |
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": 13,
|
||||
"msg_id": 1869,
|
||||
"data": {
|
||||
"lead_id": 75,
|
||||
"sale_user_id": 6,
|
||||
"saler_name": "哦啦啦"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 当前点击路由只根据 `type` 判断,`type = 13` 属于默认分支,因此进入消息中心;`data` 中的线索字段不会改变跳转页面。
|
||||
|
||||
## 7. 客户端解析约定
|
||||
|
||||
1. Android、iOS 的通知点击路由只根据业务字段 `type` 判断,不读取标题、正文、`data`、`route`、`uri`、`action` 或极光内部字段决定页面。
|
||||
2. `type = 1`:进入收款记录页。
|
||||
3. `type = 6`:进入收款详情页。
|
||||
4. 其他任意 `type`、缺少 `type`、类型无法解析或 payload 损坏:进入消息中心。
|
||||
5. Android 从 `notificationExtras` 或厂商通道包装字段中提取 `type`;iOS 优先读取 payload 顶层 `type`,并兼容常见推送包装层中的 `type`。
|
||||
6. `data` 仍可按消息类型用于页面内容刷新,但不参与通知点击路由。新增字段应保持向后兼容,客户端应忽略当前版本不认识的额外字段。
|
||||
7. 金额继续使用字符串;时间戳建议统一为 Unix 秒级整数,并在协议中固定单位。
|
||||
8. `remark` 等可空业务字段建议固定返回空字符串或明确约定 `null`,不要在多种表示之间切换。
|
||||
|
||||
## 8. 新增消息类型记录模板
|
||||
|
||||
后续收到新类型时,复制以下内容追加到“消息类型清单”:
|
||||
|
||||
````markdown
|
||||
### 6.x `type = <类型值>`:<消息名称>
|
||||
|
||||
| 项目 | 当前值 |
|
||||
| --- | --- |
|
||||
| 首次发现日期 | `YYYY-MM-DD` |
|
||||
| 标题示例 | `<title>` |
|
||||
| 正文示例 | `<content>` |
|
||||
| 客户端业务行为 | `<展示、刷新或跳转行为>` |
|
||||
|
||||
`data` 字段:
|
||||
|
||||
| 字段 | 类型 | 必有 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `<field>` | `<type>` | `<是/否/待确认>` | `<说明>` |
|
||||
|
||||
Android `extras` 示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {},
|
||||
"msg_id": 0,
|
||||
"type": 0
|
||||
}
|
||||
```
|
||||
|
||||
iOS 业务字段示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {},
|
||||
"msg_id": 0,
|
||||
"type": 0
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
记录新类型时至少保留以下信息:
|
||||
|
||||
- Android 和 iOS 各一份原始收包日志。
|
||||
- `type`、`msg_id` 和完整 `data`。
|
||||
- 标题、正文以及消息点击后的期望页面。
|
||||
- 每个字段的 JSON 类型、是否必有、空值表示方式和时间/金额单位。
|
||||
- 同一业务事件在两端的字段是否一致。
|
||||
|
||||
## 9. 待后端确认项
|
||||
|
||||
| 项目 | 当前观察 | 需要确认 |
|
||||
| --- | --- | --- |
|
||||
| `msg_id` | 与极光消息 ID 不同 | 是否对应消息中心记录 ID,以及是否用于已读/送达状态上报 |
|
||||
| `data` | `type = 1`、`2`、`6`、`13` 的样本中都存在;`type = 2` 为 `{}` | 所有业务推送是否保证存在;无业务字段时是否统一返回 `{}` |
|
||||
| 退款成功的 `data` | 当前为空对象,订单号和退款金额只在正文中 | 是否补充 `order_number`、`refund_amount`、`refund_time`、`order_type` 等结构化字段 |
|
||||
| 其他类型的 `data` | 后端已定义消息名称,但尚未提供 payload | 分别确认 `type = 3`~`5`、`7`~`12`、`100`、`999` 的字段结构、必填项及点击行为 |
|
||||
| `pay_time` | 当前为秒级时间戳 | 后端是否统一保证使用 Unix 秒,且时区语义固定为 UTC 时间戳 |
|
||||
| `remark` | 无备注时为空字符串 | 是否可能返回 `null` 或省略 |
|
||||
| `order_type` | 当前为 `14` | 完整枚举和值含义由哪份接口文档维护 |
|
||||
| 标题与正文 | 当前均存在 | 是否为所有通知消息的必有字段 |
|
||||
| 推送渠道字段 | 后端已给出渠道取值 `1`、`2`、`999` | payload 或发送接口中对应字段的准确名称 |
|
||||
| `type = 12` | `MAP_TYPE_NAMES` 中漏配 | 后端补充映射,避免 `getTypeName(12)` 返回“未知消息类型” |
|
||||
| 新增 `type` | 当前定义维护在 `PushMsg.php` | 后端新增类型前是否可同步类型值、字段结构和客户端行为 |
|
||||
204
docs/消息中心未读数量与全部已读接口需求.md
Normal file
204
docs/消息中心未读数量与全部已读接口需求.md
Normal file
@ -0,0 +1,204 @@
|
||||
# 消息中心未读数量与全部已读接口需求
|
||||
|
||||
## 1. 需求背景
|
||||
|
||||
iOS App 需要实现以下功能:
|
||||
|
||||
1. 首页展示消息中心入口,有未读消息时显示红点。
|
||||
2. 获取最新未读消息数量后,同步更新 App 桌面图标右上角的角标数量。
|
||||
3. 用户点击某条未读消息并成功标记已读后,立即更新最新未读数量和桌面角标。
|
||||
4. 消息中心导航栏右上角提供“全部已读”按钮。
|
||||
5. 全部标记已读成功后,更新首页红点,并将桌面角标更新为服务端返回的最新未读数量。
|
||||
|
||||
## 2. 当前已有接口
|
||||
|
||||
### 2.1 获取消息列表
|
||||
|
||||
```http
|
||||
GET /api/app/msg/list
|
||||
```
|
||||
|
||||
当前参数:
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `last_id` | int | 否 | 分页游标,首次请求传 `0` |
|
||||
| `limit` | int | 否 | 每页数量 |
|
||||
| `unread` | int | 否 | `0` 获取全部消息,`1` 只获取未读消息 |
|
||||
|
||||
该接口仅返回分页消息,没有返回未读消息总数。客户端若通过分页遍历全部未读消息来统计数量,请求次数和数据流量都会随着未读消息数量增长,因此不适合作为桌面角标的常规数据来源。
|
||||
|
||||
### 2.2 标记单条消息已读
|
||||
|
||||
```http
|
||||
POST /api/app/msg/read?id={message_id}
|
||||
```
|
||||
|
||||
当前接口可以标记单条消息已读,但成功响应没有返回操作后的最新未读数量。
|
||||
|
||||
## 3. 需要新增或调整的接口
|
||||
|
||||
### 3.1 获取未读消息总数
|
||||
|
||||
建议新增:
|
||||
|
||||
```http
|
||||
GET /api/app/msg/unread-count
|
||||
```
|
||||
|
||||
#### 请求说明
|
||||
|
||||
- 不需要业务请求参数。
|
||||
- 根据当前登录 Token 识别用户及业务账号。
|
||||
- 返回当前账号下尚未读取的消息总数。
|
||||
|
||||
#### 成功响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 100000,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"unread_count": 12
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 字段说明
|
||||
|
||||
| 字段 | 类型 | 必有 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `unread_count` | int | 是 | 当前账号的最新未读消息数量,最小值为 `0` |
|
||||
|
||||
### 3.2 全部标记为已读
|
||||
|
||||
建议新增:
|
||||
|
||||
```http
|
||||
POST /api/app/msg/read-all
|
||||
```
|
||||
|
||||
#### 请求说明
|
||||
|
||||
- 不需要业务请求参数和请求体。
|
||||
- 根据当前登录 Token 识别用户及业务账号。
|
||||
- 将当前账号下请求执行时已经存在的所有未读消息标记为已读。
|
||||
- 接口需要支持幂等调用;没有未读消息时仍返回成功。
|
||||
|
||||
#### 成功响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 100000,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"updated_count": 12,
|
||||
"unread_count": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 字段说明
|
||||
|
||||
| 字段 | 类型 | 必有 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `updated_count` | int | 是 | 本次实际被更新为已读的消息数量 |
|
||||
| `unread_count` | int | 是 | 操作完成后的最新未读消息数量 |
|
||||
|
||||
> 客户端应以响应中的 `unread_count` 为准,不应直接假定其一定为 `0`。例如“全部已读”执行期间如果有新消息到达,操作完成后仍可能存在新的未读消息。
|
||||
|
||||
### 3.3 调整单条已读接口响应
|
||||
|
||||
保留现有接口:
|
||||
|
||||
```http
|
||||
POST /api/app/msg/read?id={message_id}
|
||||
```
|
||||
|
||||
建议成功响应增加最新未读数量:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 100000,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"unread_count": 11
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 字段说明
|
||||
|
||||
| 字段 | 类型 | 必有 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `unread_count` | int | 是 | 单条消息标记已读后的最新未读消息数量 |
|
||||
|
||||
#### 幂等要求
|
||||
|
||||
- 对已经处于已读状态的消息重复调用时,接口仍返回成功和最新 `unread_count`。
|
||||
- 消息不存在或不属于当前账号时,返回明确的业务错误,不能修改其他账号的消息。
|
||||
|
||||
## 4. 统一约定
|
||||
|
||||
### 4.1 账号范围
|
||||
|
||||
- 所有接口必须根据登录 Token 限定当前用户及当前业务账号的数据范围。
|
||||
- 景区账号、门店账号等不同业务账号之间的未读数量不能互相污染。
|
||||
- 切换账号后,客户端会重新调用未读数量接口。
|
||||
|
||||
### 4.2 数值约束
|
||||
|
||||
- `unread_count` 和 `updated_count` 必须返回 JSON 整数。
|
||||
- 数值不得为负数。
|
||||
- 即使数量为 `0`,字段也必须存在,不能返回 `null`、空字符串或省略字段。
|
||||
|
||||
### 4.3 一致性与并发
|
||||
|
||||
- 单条已读或全部已读操作及其响应中的 `unread_count` 应基于同一账号范围。
|
||||
- `read-all` 应尽量在事务内完成批量更新和剩余未读数统计。
|
||||
- 如果操作期间有新消息写入,响应应返回操作完成时数据库中的真实 `unread_count`。
|
||||
- 客户端始终以最近一次成功接口响应中的 `unread_count` 更新首页红点和桌面角标。
|
||||
|
||||
### 4.4 错误响应
|
||||
|
||||
沿用现有统一业务响应结构,例如:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 100001,
|
||||
"msg": "消息不存在或无权操作",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
建议至少区分以下情况:
|
||||
|
||||
- Token 无效或登录已过期。
|
||||
- 单条消息不存在。
|
||||
- 单条消息不属于当前账号。
|
||||
- 数据库更新失败。
|
||||
|
||||
## 5. 客户端调用时机
|
||||
|
||||
| 场景 | 调用接口 | 客户端处理 |
|
||||
| --- | --- | --- |
|
||||
| 登录后首次进入首页 | `GET /msg/unread-count` | 更新首页红点和桌面角标 |
|
||||
| App 回到前台 | `GET /msg/unread-count` | 校准最新未读数量 |
|
||||
| 前台收到新消息推送 | `GET /msg/unread-count` | 更新首页红点和桌面角标 |
|
||||
| 点击一条未读消息 | `POST /msg/read` | 使用返回的 `unread_count` 更新红点和桌面角标 |
|
||||
| 点击“全部已读” | `POST /msg/read-all` | 刷新消息列表,使用返回的 `unread_count` 更新红点和桌面角标 |
|
||||
| 切换业务账号 | `GET /msg/unread-count` | 使用新账号的数量覆盖旧账号角标 |
|
||||
| 退出登录 | 无 | 客户端将桌面角标清零 |
|
||||
|
||||
## 6. 验收标准
|
||||
|
||||
1. 未读数量为 `0` 时,接口稳定返回 `{"unread_count": 0}`。
|
||||
2. 存在多页未读消息时,未读数量接口返回完整总数,而不是当前页数量。
|
||||
3. 单条未读消息标记成功后,返回的 `unread_count` 与数据库实际剩余未读数一致。
|
||||
4. 对同一消息重复调用单条已读接口不会重复扣减未读数量。
|
||||
5. 全部已读接口可一次处理当前账号的所有未读消息,并返回实际更新数量。
|
||||
6. 重复调用全部已读接口仍成功,返回 `updated_count = 0`。
|
||||
7. 不同业务账号的未读数量相互隔离。
|
||||
8. 并发到达新消息时,响应中的 `unread_count` 能反映操作完成时的真实剩余数量。
|
||||
9. 所有接口继续使用项目现有统一鉴权、业务状态码和响应信封格式。
|
||||
|
||||
@ -208,6 +208,11 @@
|
||||
TargetAttributes = {
|
||||
93FF17C42FFB84950033C304 = {
|
||||
CreatedOnToolsVersion = 26.6;
|
||||
SystemCapabilities = {
|
||||
com.apple.Push = {
|
||||
enabled = 1;
|
||||
};
|
||||
};
|
||||
};
|
||||
A1B2C3D32FFB84950033C305 = {
|
||||
CreatedOnToolsVersion = 26.6;
|
||||
@ -422,6 +427,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 5BBB56C2281879245220A9F3 /* Pods-suixinkan.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
APS_ENVIRONMENT = development;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements;
|
||||
@ -487,6 +493,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 1060D5E389506965D83EA1CC /* Pods-suixinkan.release.xcconfig */;
|
||||
buildSettings = {
|
||||
APS_ENVIRONMENT = production;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = suixinkan/suixinkan.entitlements;
|
||||
|
||||
@ -29,6 +29,7 @@ final class NetworkServices {
|
||||
let liveAPI: LiveAPI
|
||||
let cloudDriveAPI: CloudDriveAPI
|
||||
let messageCenterAPI: MessageCenterAPI
|
||||
let pushAPI: PushAPI
|
||||
let wildPhotographerReportAPI: WildPhotographerReportAPI
|
||||
let uploadAPI: UploadAPI
|
||||
let ossUploadService: OSSUploadService
|
||||
@ -54,6 +55,7 @@ final class NetworkServices {
|
||||
liveAPI = LiveAPI(client: client)
|
||||
cloudDriveAPI = CloudDriveAPI(client: client)
|
||||
messageCenterAPI = MessageCenterAPI(client: client)
|
||||
pushAPI = PushAPI(client: client)
|
||||
wildPhotographerReportAPI = WildPhotographerReportAPI(client: client)
|
||||
uploadAPI = UploadAPI(client: client)
|
||||
ossUploadService = OSSUploadService(configService: uploadAPI)
|
||||
@ -84,6 +86,7 @@ final class NetworkServices {
|
||||
liveAPI = LiveAPI(client: apiClient)
|
||||
cloudDriveAPI = CloudDriveAPI(client: apiClient)
|
||||
messageCenterAPI = MessageCenterAPI(client: apiClient)
|
||||
pushAPI = PushAPI(client: apiClient)
|
||||
wildPhotographerReportAPI = WildPhotographerReportAPI(client: apiClient)
|
||||
uploadAPI = UploadAPI(client: apiClient)
|
||||
ossUploadService = OSSUploadService(configService: uploadAPI)
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
import IQKeyboardManagerSwift
|
||||
import IQKeyboardToolbarManager
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
|
||||
@main
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
@ -18,9 +19,36 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
if AppStore.shared.session.privacyAgreementAccepted, !AppStore.shared.session.token.isEmpty {
|
||||
AMapBootstrap.configureIfNeeded()
|
||||
}
|
||||
PushNotificationManager.shared.initializeIfPrivacyAccepted(launchOptions: launchOptions)
|
||||
return true
|
||||
}
|
||||
|
||||
/// APNs 注册成功后将 Device Token 交给极光。
|
||||
func application(
|
||||
_ application: UIApplication,
|
||||
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
|
||||
) {
|
||||
PushNotificationManager.shared.handleDeviceToken(deviceToken)
|
||||
}
|
||||
|
||||
/// APNs 注册失败不阻断应用启动,仅交由推送管理器记录。
|
||||
func application(
|
||||
_ application: UIApplication,
|
||||
didFailToRegisterForRemoteNotificationsWithError error: Error
|
||||
) {
|
||||
PushNotificationManager.shared.handleRegistrationError(error)
|
||||
}
|
||||
|
||||
/// 处理后台静默通知并完成极光到达统计。
|
||||
func application(
|
||||
_ application: UIApplication,
|
||||
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
|
||||
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
|
||||
) {
|
||||
PushNotificationManager.shared.handleRemoteNotification(userInfo)
|
||||
completionHandler(.newData)
|
||||
}
|
||||
|
||||
// MARK: UISceneSession Lifecycle
|
||||
|
||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
|
||||
30
suixinkan/Core/Push/PushAPI.swift
Normal file
30
suixinkan/Core/Push/PushAPI.swift
Normal file
@ -0,0 +1,30 @@
|
||||
import Foundation
|
||||
|
||||
/// 极光 Registration ID 上报能力,允许推送管理器注入测试替身。
|
||||
@MainActor
|
||||
protocol PushRegistrationServing: AnyObject {
|
||||
/// 将当前设备的极光 Registration ID 绑定到已登录业务账号。
|
||||
func registerJPushID(_ registrationID: String) async throws
|
||||
}
|
||||
|
||||
/// 推送 API,复用 Android 当前使用的 Registration ID 上报接口。
|
||||
@MainActor
|
||||
final class PushAPI: PushRegistrationServing {
|
||||
private let client: APIClient
|
||||
|
||||
/// 使用共享网络客户端创建推送 API。
|
||||
init(client: APIClient) {
|
||||
self.client = client
|
||||
}
|
||||
|
||||
/// 上报极光 Registration ID;后端字段名沿用 `jpush_reg_id`。
|
||||
func registerJPushID(_ registrationID: String) async throws {
|
||||
let _: EmptyPayload = try await client.send(
|
||||
APIRequest(
|
||||
method: .post,
|
||||
path: "/api/app/user/register-jpush-id",
|
||||
queryItems: [URLQueryItem(name: "jpush_reg_id", value: registrationID)]
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
86
suixinkan/Core/Push/PushModels.swift
Normal file
86
suixinkan/Core/Push/PushModels.swift
Normal file
@ -0,0 +1,86 @@
|
||||
import Foundation
|
||||
|
||||
/// 推送点击后可到达的业务页面。
|
||||
enum PushDestination: Sendable, Equatable {
|
||||
case paymentRecord
|
||||
case paymentDetails
|
||||
case messageCenter
|
||||
}
|
||||
|
||||
/// 极光/APNs payload 解析结果,仅提取业务消息类型用于点击路由。
|
||||
struct PushPayload: Sendable, Equatable {
|
||||
private let type: String
|
||||
|
||||
/// 从系统通知 userInfo 提取顶层或推送包装层中的业务消息类型。
|
||||
nonisolated init(userInfo: [AnyHashable: Any]) {
|
||||
type = Self.extractType(from: userInfo)
|
||||
}
|
||||
|
||||
/// 从可发送的字符串字典创建 payload,仅保留业务消息类型。
|
||||
nonisolated init(values: [String: String]) {
|
||||
type = Self.normalizedType(values["type"])
|
||||
}
|
||||
|
||||
/// 可发送的规范化字段快照。
|
||||
nonisolated var normalizedValues: [String: String] {
|
||||
type.isEmpty ? [:] : ["type": type]
|
||||
}
|
||||
|
||||
/// 仅根据后端业务消息类型解析目标页面。
|
||||
nonisolated var destination: PushDestination {
|
||||
switch type {
|
||||
case "1":
|
||||
return .paymentRecord
|
||||
case "6":
|
||||
return .paymentDetails
|
||||
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 let nestedTypeContainerKeys = [
|
||||
"extras", "extra", "JMessageExtra", "n_extras",
|
||||
]
|
||||
}
|
||||
422
suixinkan/Core/Push/PushNotificationManager.swift
Normal file
422
suixinkan/Core/Push/PushNotificationManager.swift
Normal file
@ -0,0 +1,422 @@
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
|
||||
/// App 桌面图标角标设置能力,便于业务同步未读消息数量并进行测试替换。
|
||||
@MainActor
|
||||
protocol ApplicationIconBadgeSetting: AnyObject {
|
||||
/// 将桌面图标角标更新为指定非负数量。
|
||||
func setBadgeCount(_ count: Int) async
|
||||
}
|
||||
|
||||
/// 使用 UserNotifications 更新 App 桌面图标角标的系统实现。
|
||||
@MainActor
|
||||
final class SystemApplicationIconBadgeSetter: ApplicationIconBadgeSetting {
|
||||
/// 全局角标设置器。
|
||||
static let shared = SystemApplicationIconBadgeSetter()
|
||||
|
||||
private init() {}
|
||||
|
||||
func setBadgeCount(_ count: Int) async {
|
||||
do {
|
||||
try await UNUserNotificationCenter.current().setBadgeCount(max(count, 0))
|
||||
} catch {
|
||||
#if DEBUG
|
||||
print("Update application icon badge failed: \(error.localizedDescription)")
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 极光 SDK 的最小能力边界,便于推送生命周期逻辑使用测试替身。
|
||||
@MainActor
|
||||
protocol PushSDKProviding: AnyObject {
|
||||
/// 初始化极光长连接与 APNs 代理服务。
|
||||
func initialize(launchOptions: [UIApplication.LaunchOptionsKey: Any]?, isProduction: Bool)
|
||||
|
||||
/// 请求系统通知权限并注册极光通知代理。
|
||||
func requestAuthorization(delegate: JPUSHRegisterDelegate)
|
||||
|
||||
/// 将 APNs Device Token 注册给极光。
|
||||
func registerDeviceToken(_ deviceToken: Data)
|
||||
|
||||
/// 获取极光 Registration ID。
|
||||
func fetchRegistrationID(completion: @escaping (Int32, String?) -> Void)
|
||||
}
|
||||
|
||||
/// 跨越极光 `@Sendable` 回调边界的只读闭包容器。
|
||||
private final class RegistrationIDCompletionBox: @unchecked Sendable {
|
||||
let completion: (Int32, String?) -> Void
|
||||
|
||||
init(completion: @escaping (Int32, String?) -> Void) {
|
||||
self.completion = completion
|
||||
}
|
||||
}
|
||||
|
||||
/// 生产环境极光 SDK 适配器,集中保存 AppKey 和渠道配置。
|
||||
@MainActor
|
||||
final class JPushSDKAdapter: PushSDKProviding {
|
||||
private static let appKey = "194a633c2a6b79c2f1033c40"
|
||||
private static let channel = "App Store"
|
||||
|
||||
/// 初始化极光 SDK;Debug 输出详细日志,Release 只保留告警和错误。
|
||||
func initialize(launchOptions: [UIApplication.LaunchOptionsKey: Any]?, isProduction: Bool) {
|
||||
JPUSHService.setup(
|
||||
withOption: launchOptions,
|
||||
appKey: Self.appKey,
|
||||
channel: Self.channel,
|
||||
apsForProduction: isProduction
|
||||
)
|
||||
#if DEBUG
|
||||
JPUSHService.setDebugMode()
|
||||
#else
|
||||
JPUSHService.setLogOFF()
|
||||
#endif
|
||||
}
|
||||
|
||||
/// 请求 alert、badge、sound 三类通知权限。
|
||||
func requestAuthorization(delegate: JPUSHRegisterDelegate) {
|
||||
let entity = JPUSHRegisterEntity()
|
||||
entity.types = 1 | 2 | 4
|
||||
JPUSHService.register(forRemoteNotificationConfig: entity, delegate: delegate)
|
||||
}
|
||||
|
||||
/// 将 APNs Device Token 交给极光。
|
||||
func registerDeviceToken(_ deviceToken: Data) {
|
||||
JPUSHService.registerDeviceToken(deviceToken)
|
||||
}
|
||||
|
||||
/// 异步获取当前设备 Registration ID。
|
||||
func fetchRegistrationID(completion: @escaping (Int32, String?) -> Void) {
|
||||
let box = RegistrationIDCompletionBox(completion: completion)
|
||||
JPUSHService.registrationIDCompletionHandler { resultCode, registrationID in
|
||||
box.completion(resultCode, registrationID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 推送生命周期管理器,负责隐私门禁、权限、Registration ID 绑定及通知点击分发。
|
||||
@MainActor
|
||||
final class PushNotificationManager: NSObject {
|
||||
/// 全局推送管理器。
|
||||
static let shared = PushNotificationManager()
|
||||
|
||||
/// 推送相关的本地持久化键。
|
||||
private enum Key {
|
||||
static let registrationID = "key_jpush_registration_id"
|
||||
static let uploadedRegistrationIDSuffix = "jpush_uploaded_registration_id"
|
||||
}
|
||||
|
||||
private let sdk: any PushSDKProviding
|
||||
private let api: any PushRegistrationServing
|
||||
private let appStore: AppStore
|
||||
private let defaults: UserDefaults
|
||||
private let router: any PushRouting
|
||||
private let applicationIconBadgeSetter: any ApplicationIconBadgeSetting
|
||||
|
||||
private var isInitialized = false
|
||||
private var didRequestAuthorization = false
|
||||
private var uploadTask: Task<Void, Never>?
|
||||
private var queuedForcedUpload = false
|
||||
private var isFetchingRegistrationID = false
|
||||
private var queuedForcedFetch = false
|
||||
|
||||
/// 创建推送管理器;生产环境使用共享依赖,测试可注入替身。
|
||||
init(
|
||||
sdk: (any PushSDKProviding)? = nil,
|
||||
api: (any PushRegistrationServing)? = nil,
|
||||
appStore: AppStore = .shared,
|
||||
defaults: UserDefaults = .standard,
|
||||
router: (any PushRouting)? = nil,
|
||||
applicationIconBadgeSetter: (any ApplicationIconBadgeSetting)? = nil
|
||||
) {
|
||||
self.sdk = sdk ?? JPushSDKAdapter()
|
||||
self.api = api ?? NetworkServices.shared.pushAPI
|
||||
self.appStore = appStore
|
||||
self.defaults = defaults
|
||||
self.router = router ?? PushRouteCoordinator.shared
|
||||
self.applicationIconBadgeSetter = applicationIconBadgeSetter ?? SystemApplicationIconBadgeSetter.shared
|
||||
super.init()
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
/// 绑定当前 Scene 的主窗口。
|
||||
func attach(window: UIWindow) {
|
||||
router.attach(window: window)
|
||||
}
|
||||
|
||||
/// 仅在用户已同意隐私协议后初始化极光 SDK。
|
||||
func initializeIfPrivacyAccepted(
|
||||
launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||
) {
|
||||
guard appStore.session.privacyAgreementAccepted, !isInitialized else { return }
|
||||
isInitialized = true
|
||||
sdk.initialize(launchOptions: launchOptions, isProduction: Self.isProductionBuild)
|
||||
observeJPushNetworkLogin()
|
||||
refreshRegistrationID(forceUpload: false)
|
||||
}
|
||||
|
||||
/// 登录成功后请求通知权限,并强制绑定当前账号。
|
||||
func handleLoginCompleted() {
|
||||
initializeIfPrivacyAccepted()
|
||||
guard isInitialized, appStore.session.isLoggedIn else { return }
|
||||
if !didRequestAuthorization {
|
||||
didRequestAuthorization = true
|
||||
sdk.requestAuthorization(delegate: self)
|
||||
}
|
||||
bindCurrentAccount()
|
||||
}
|
||||
|
||||
/// 账号切换后把同一设备重新绑定到新的业务账号。
|
||||
func handleAccountSwitched() {
|
||||
guard appStore.session.isLoggedIn else { return }
|
||||
bindCurrentAccount()
|
||||
}
|
||||
|
||||
/// 退出登录时停止账号上报并清除未完成的业务路由。
|
||||
func handleLogout() {
|
||||
uploadTask?.cancel()
|
||||
uploadTask = nil
|
||||
queuedForcedUpload = false
|
||||
router.resetPendingRoute()
|
||||
Task { await updateApplicationIconBadgeCount(0) }
|
||||
}
|
||||
|
||||
/// 将桌面 App Icon 角标更新为最新未读消息数量。
|
||||
func updateApplicationIconBadgeCount(_ count: Int) async {
|
||||
await applicationIconBadgeSetter.setBadgeCount(max(count, 0))
|
||||
}
|
||||
|
||||
/// App 回到前台时补偿失败或尚未完成的 Registration ID 上报。
|
||||
func retryPendingRegistrationUpload() {
|
||||
guard appStore.session.isLoggedIn else { return }
|
||||
uploadCachedRegistrationID(force: false)
|
||||
refreshRegistrationID(forceUpload: false)
|
||||
}
|
||||
|
||||
/// 登录根页面建立后继续执行通知点击暂存的路由。
|
||||
func routePendingNotificationIfPossible() {
|
||||
router.routePendingIfPossible()
|
||||
}
|
||||
|
||||
/// APNs 注册成功后把 Device Token 注册给极光。
|
||||
func handleDeviceToken(_ deviceToken: Data) {
|
||||
guard isInitialized else { return }
|
||||
sdk.registerDeviceToken(deviceToken)
|
||||
refreshRegistrationID(forceUpload: false)
|
||||
}
|
||||
|
||||
/// APNs 注册失败只记录调试信息,不阻断登录和主业务。
|
||||
func handleRegistrationError(_ error: Error) {
|
||||
#if DEBUG
|
||||
print("JPush APNs registration failed: \(error.localizedDescription)")
|
||||
#endif
|
||||
}
|
||||
|
||||
/// 处理冷启动或运行中通知点击。
|
||||
func handleNotificationTap(
|
||||
payload: PushPayload,
|
||||
requestIdentifier: String
|
||||
) {
|
||||
router.handle(
|
||||
destination: payload.destination,
|
||||
requestIdentifier: requestIdentifier
|
||||
)
|
||||
}
|
||||
|
||||
/// 处理 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),
|
||||
requestIdentifier: response.notification.request.identifier
|
||||
)
|
||||
}
|
||||
|
||||
/// 处理后台静默通知并向极光上报到达,不触发页面跳转。
|
||||
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,
|
||||
selector: #selector(handleJPushNetworkLogin),
|
||||
name: .jpfNetworkDidLogin,
|
||||
object: nil,
|
||||
)
|
||||
}
|
||||
|
||||
@objc private func handleJPushNetworkLogin() {
|
||||
refreshRegistrationID(forceUpload: false)
|
||||
}
|
||||
|
||||
private func refreshRegistrationID(forceUpload: Bool) {
|
||||
guard isInitialized else { return }
|
||||
queuedForcedFetch = queuedForcedFetch || forceUpload
|
||||
guard !isFetchingRegistrationID else { return }
|
||||
isFetchingRegistrationID = true
|
||||
sdk.fetchRegistrationID { [weak self] resultCode, registrationID in
|
||||
Task { @MainActor in
|
||||
guard let self else { return }
|
||||
let shouldForceUpload = self.queuedForcedFetch
|
||||
self.queuedForcedFetch = false
|
||||
self.isFetchingRegistrationID = false
|
||||
guard resultCode == 0 else { return }
|
||||
let normalized = registrationID?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
guard !normalized.isEmpty else { return }
|
||||
self.defaults.set(normalized, forKey: Key.registrationID)
|
||||
self.upload(registrationID: normalized, force: shouldForceUpload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func uploadCachedRegistrationID(force: Bool) {
|
||||
let registrationID = cachedRegistrationID
|
||||
guard !registrationID.isEmpty else { return }
|
||||
upload(registrationID: registrationID, force: force)
|
||||
}
|
||||
|
||||
private func bindCurrentAccount() {
|
||||
if cachedRegistrationID.isEmpty {
|
||||
refreshRegistrationID(forceUpload: true)
|
||||
} else {
|
||||
uploadCachedRegistrationID(force: true)
|
||||
refreshRegistrationID(forceUpload: false)
|
||||
}
|
||||
}
|
||||
|
||||
private var cachedRegistrationID: String {
|
||||
defaults.string(forKey: Key.registrationID)?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
}
|
||||
|
||||
private func upload(registrationID: String, force: Bool) {
|
||||
guard appStore.session.isLoggedIn,
|
||||
let uploadedKey = appStore.session.accountScopedKey(Key.uploadedRegistrationIDSuffix)
|
||||
else { return }
|
||||
|
||||
if !force, defaults.string(forKey: uploadedKey) == registrationID {
|
||||
return
|
||||
}
|
||||
if uploadTask != nil {
|
||||
queuedForcedUpload = queuedForcedUpload || force
|
||||
return
|
||||
}
|
||||
|
||||
let accountScope = appStore.session.accountCachePrefix
|
||||
uploadTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
var succeeded = false
|
||||
do {
|
||||
try await self.api.registerJPushID(registrationID)
|
||||
if self.appStore.session.accountCachePrefix == accountScope {
|
||||
self.defaults.set(registrationID, forKey: uploadedKey)
|
||||
}
|
||||
succeeded = true
|
||||
} catch is CancellationError {
|
||||
// 退出登录时取消,不视为失败重试。
|
||||
} catch {
|
||||
#if DEBUG
|
||||
print("JPush Registration ID upload failed: \(error.localizedDescription)")
|
||||
#endif
|
||||
}
|
||||
|
||||
self.uploadTask = nil
|
||||
let shouldForceAgain = self.queuedForcedUpload
|
||||
self.queuedForcedUpload = false
|
||||
if succeeded, shouldForceAgain {
|
||||
self.uploadCachedRegistrationID(force: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated static var isProductionBuild: Bool {
|
||||
#if DEBUG
|
||||
false
|
||||
#else
|
||||
true
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
extension PushNotificationManager: JPUSHRegisterDelegate {
|
||||
/// 前台通知只展示系统 UI,不自动执行业务路由。
|
||||
nonisolated func jpushNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
willPresent notification: UNNotification,
|
||||
withCompletionHandler completionHandler: @escaping (Int) -> Void
|
||||
) {
|
||||
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)
|
||||
}
|
||||
let options: UNNotificationPresentationOptions = [.banner, .list, .sound, .badge]
|
||||
completionHandler(Int(options.rawValue))
|
||||
}
|
||||
|
||||
/// 用户点击通知后上报极光统计,并在主 Actor 执行业务路由。
|
||||
nonisolated func jpushNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
didReceive response: UNNotificationResponse,
|
||||
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
|
||||
Task { @MainActor [weak self] in
|
||||
self?.handleNotificationTap(
|
||||
payload: payload,
|
||||
requestIdentifier: requestIdentifier
|
||||
)
|
||||
}
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
/// 系统通知设置入口回调,首期无需额外应用内页面。
|
||||
nonisolated func jpushNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
openSettingsFor notification: UNNotification
|
||||
) {}
|
||||
|
||||
/// 极光通知授权状态回调,权限变化由系统设置管理。
|
||||
nonisolated func jpushNotificationAuthorization(
|
||||
_ status: JPAuthorizationStatus,
|
||||
withInfo info: [AnyHashable: Any]?
|
||||
) {}
|
||||
}
|
||||
98
suixinkan/Core/Push/PushRouteCoordinator.swift
Normal file
98
suixinkan/Core/Push/PushRouteCoordinator.swift
Normal file
@ -0,0 +1,98 @@
|
||||
import UIKit
|
||||
|
||||
/// 推送点击路由能力,允许生命周期管理器与 UIKit 导航解耦并支持测试替身。
|
||||
@MainActor
|
||||
protocol PushRouting: AnyObject {
|
||||
/// 绑定当前 Scene 的主窗口。
|
||||
func attach(window: UIWindow)
|
||||
|
||||
/// 处理一次通知点击;未登录时暂存到登录成功后继续。
|
||||
func handle(destination: PushDestination, requestIdentifier: String)
|
||||
|
||||
/// 主界面建立后执行尚未完成的通知路由。
|
||||
func routePendingIfPossible()
|
||||
|
||||
/// 清除退出登录前遗留的待处理路由。
|
||||
func resetPendingRoute()
|
||||
}
|
||||
|
||||
/// 集中式推送路由器,负责冷启动去重、登录等待和跨 Tab 导航。
|
||||
@MainActor
|
||||
final class PushRouteCoordinator: PushRouting {
|
||||
/// 全局推送路由器。
|
||||
static let shared = PushRouteCoordinator()
|
||||
|
||||
/// 等待登录或主界面建立后执行的一次性推送路由。
|
||||
private struct PendingRoute {
|
||||
let destination: PushDestination
|
||||
let requestIdentifier: String
|
||||
}
|
||||
|
||||
private weak var window: UIWindow?
|
||||
private var pendingRoute: PendingRoute?
|
||||
private var handledRequestIdentifiers: [String] = []
|
||||
private let appStore: AppStore
|
||||
|
||||
/// 创建路由器并注入应用状态。
|
||||
init(appStore: AppStore = .shared) {
|
||||
self.appStore = appStore
|
||||
}
|
||||
|
||||
/// 绑定当前 Scene 的主窗口。
|
||||
func attach(window: UIWindow) {
|
||||
self.window = window
|
||||
}
|
||||
|
||||
/// 处理通知点击;相同 request identifier 只执行一次。
|
||||
func handle(destination: PushDestination, requestIdentifier: String) {
|
||||
let identifier = requestIdentifier.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !identifier.isEmpty, handledRequestIdentifiers.contains(identifier) {
|
||||
return
|
||||
}
|
||||
markHandled(identifier)
|
||||
|
||||
let route = PendingRoute(destination: destination, requestIdentifier: identifier)
|
||||
guard appStore.session.isLoggedIn, mainTabController != nil else {
|
||||
pendingRoute = route
|
||||
return
|
||||
}
|
||||
perform(route)
|
||||
}
|
||||
|
||||
/// 登录完成、根页面切换后继续执行最后一次待处理路由。
|
||||
func routePendingIfPossible() {
|
||||
guard appStore.session.isLoggedIn,
|
||||
mainTabController != nil,
|
||||
let route = pendingRoute
|
||||
else { return }
|
||||
pendingRoute = nil
|
||||
perform(route)
|
||||
}
|
||||
|
||||
/// 用户退出登录时丢弃之前账号尚未执行的跳转。
|
||||
func resetPendingRoute() {
|
||||
pendingRoute = nil
|
||||
}
|
||||
|
||||
private var mainTabController: MainTabBarController? {
|
||||
window?.rootViewController as? MainTabBarController
|
||||
}
|
||||
|
||||
private func perform(_ route: PendingRoute) {
|
||||
guard let mainTabController else {
|
||||
pendingRoute = route
|
||||
return
|
||||
}
|
||||
|
||||
mainTabController.dismiss(animated: false)
|
||||
mainTabController.openPushDestination(route.destination)
|
||||
}
|
||||
|
||||
private func markHandled(_ identifier: String) {
|
||||
guard !identifier.isEmpty else { return }
|
||||
handledRequestIdentifiers.append(identifier)
|
||||
if handledRequestIdentifiers.count > 50 {
|
||||
handledRequestIdentifiers.removeFirst(handledRequestIdentifiers.count - 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -17,18 +17,23 @@ final class HomeCommonMenuStore {
|
||||
/// 读取已保存的常用 URI;无记录时返回空数组。
|
||||
func savedCommonURIs(accountScope: String, roleCode: String) -> [String] {
|
||||
guard let key = storageKey(accountScope: accountScope, roleCode: roleCode) else { return [] }
|
||||
return defaults.stringArray(forKey: key) ?? []
|
||||
let saved = defaults.stringArray(forKey: key) ?? []
|
||||
let sanitized = saved.filter(HomeMenuCatalog.isCustomizable(uri:))
|
||||
if sanitized != saved {
|
||||
defaults.set(sanitized, forKey: key)
|
||||
}
|
||||
return sanitized
|
||||
}
|
||||
|
||||
/// 保存常用 URI 列表。
|
||||
func saveCommonURIs(_ uris: [String], accountScope: String, roleCode: String) {
|
||||
guard let key = storageKey(accountScope: accountScope, roleCode: roleCode) else { return }
|
||||
defaults.set(uris, forKey: key)
|
||||
defaults.set(uris.filter(HomeMenuCatalog.isCustomizable(uri:)), forKey: key)
|
||||
}
|
||||
|
||||
/// 根据权限生成默认常用 URI(最多 4 个)。
|
||||
static func defaultCommonURIs(from permissions: [HomePermissionItem]) -> [String] {
|
||||
let uris = permissions.map(\.uri)
|
||||
let uris = permissions.map(\.uri).filter(HomeMenuCatalog.isCustomizable(uri:))
|
||||
return uris.count > 4 ? Array(uris.prefix(4)) : uris
|
||||
}
|
||||
|
||||
@ -51,8 +56,9 @@ final class HomeCommonMenuStore {
|
||||
commonURIs: [String]
|
||||
) -> (common: [HomeMenuItem], more: [HomeMenuItem]) {
|
||||
let commonSet = Set(commonURIs)
|
||||
let common = allMenus.filter { commonSet.contains($0.uri) }
|
||||
let more = allMenus.filter { !commonSet.contains($0.uri) }
|
||||
let customizableMenus = allMenus.filter { HomeMenuCatalog.isCustomizable(uri: $0.uri) }
|
||||
let common = customizableMenus.filter { commonSet.contains($0.uri) }
|
||||
let more = customizableMenus.filter { !commonSet.contains($0.uri) }
|
||||
return (common, more)
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,8 @@ import Foundation
|
||||
/// 首页菜单静态 catalog,与服务端 permission URI 取交集后展示。
|
||||
enum HomeMenuCatalog {
|
||||
|
||||
private static let fixedHomeEntryURIs: Set<String> = ["message_center"]
|
||||
|
||||
/// 全部已知菜单项,对齐 Android `Constants.menuList`。
|
||||
static let allItems: [HomeMenuItem] = [
|
||||
HomeMenuItem(uri: "space_settings", title: "空间设置", iconName: "home_menu_space"),
|
||||
@ -45,6 +47,7 @@ enum HomeMenuCatalog {
|
||||
/// 将服务端权限映射为可展示菜单,保持服务端顺序。
|
||||
static func visibleMenus(from permissions: [HomePermissionItem]) -> [HomeMenuItem] {
|
||||
permissions.compactMap { permission in
|
||||
guard isCustomizable(uri: permission.uri) else { return nil }
|
||||
guard let catalog = allItems.first(where: { $0.uri == permission.uri }) else { return nil }
|
||||
return HomeMenuItem(
|
||||
uri: catalog.uri,
|
||||
@ -59,4 +62,9 @@ enum HomeMenuCatalog {
|
||||
static func item(for uri: String) -> HomeMenuItem? {
|
||||
allItems.first { $0.uri == uri }
|
||||
}
|
||||
|
||||
/// 判断入口是否允许出现在首页常用应用及其自定义页面中。
|
||||
static func isCustomizable(uri: String) -> Bool {
|
||||
!fixedHomeEntryURIs.contains(uri)
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,6 +48,7 @@ final class AllFunctionsViewModel {
|
||||
|
||||
/// 将菜单加入常用应用。
|
||||
func addToCommon(_ menu: HomeMenuItem) {
|
||||
guard HomeMenuCatalog.isCustomizable(uri: menu.uri) else { return }
|
||||
guard !commonMenus.contains(where: { $0.uri == menu.uri }) else { return }
|
||||
commonMenus.append(menu)
|
||||
moreMenus.removeAll { $0.uri == menu.uri }
|
||||
|
||||
@ -25,6 +25,7 @@ final class HomeViewModel {
|
||||
private(set) var reportTimeText = ""
|
||||
private(set) var isLocationReporting = false
|
||||
private(set) var needsPermissionReload = false
|
||||
private(set) var unreadMessageCount = 0
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
@ -65,6 +66,7 @@ final class HomeViewModel {
|
||||
var isOnline: Bool { locationStateStore.isOnline }
|
||||
var countdownDisplayText: String { locationStateStore.countdownDisplayText }
|
||||
var reminderMinutes: Int { locationStateStore.reminderMinutes }
|
||||
var hasUnreadMessages: Bool { unreadMessageCount > 0 }
|
||||
|
||||
/// 首次进入首页时加载权限并恢复在线状态。
|
||||
func initialize(api: HomeAPI) async {
|
||||
@ -76,6 +78,28 @@ final class HomeViewModel {
|
||||
/// 账号切换后标记需要重新拉取权限。
|
||||
func markNeedsPermissionReload() {
|
||||
needsPermissionReload = true
|
||||
unreadMessageCount = 0
|
||||
}
|
||||
|
||||
/// 查询未读消息总数,成功时更新首页状态并返回 `true`。
|
||||
func refreshUnreadMessageStatus(api: any MessageCenterServing) async -> Bool {
|
||||
do {
|
||||
let response = try await api.unreadCount()
|
||||
unreadMessageCount = response.unreadCount
|
||||
notifyStateChange()
|
||||
return true
|
||||
} catch is CancellationError {
|
||||
return false
|
||||
} catch {
|
||||
// 查询失败时保留上一次状态,避免网络抖动导致红点错误消失。
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// 使用已知的最新未读消息数量立即刷新首页状态。
|
||||
func updateUnreadMessageCount(_ count: Int) {
|
||||
unreadMessageCount = max(count, 0)
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
/// 查询待审核记录并决定进入申请页或审核状态页。
|
||||
|
||||
@ -11,8 +11,14 @@ protocol MessageCenterServing {
|
||||
/// 拉取消息列表。
|
||||
func list(lastId: Int, limit: Int, unread: Int) async throws -> MessageListResponse
|
||||
|
||||
/// 标记消息已读。
|
||||
func markAsRead(messageId: Int) async throws
|
||||
/// 获取当前账号未读消息总数。
|
||||
func unreadCount() async throws -> MessageUnreadCountResponse
|
||||
|
||||
/// 标记消息已读并返回最新未读数量。
|
||||
func markAsRead(messageId: Int) async throws -> MessageUnreadCountResponse
|
||||
|
||||
/// 将当前账号全部消息标记已读。
|
||||
func markAllAsRead() async throws -> MessageReadAllResponse
|
||||
|
||||
/// 删除消息。
|
||||
func delete(messageId: Int) async throws
|
||||
@ -43,9 +49,16 @@ final class MessageCenterAPI: MessageCenterServing {
|
||||
)
|
||||
}
|
||||
|
||||
/// GET /api/app/msg/unread-count
|
||||
func unreadCount() async throws -> MessageUnreadCountResponse {
|
||||
try await client.send(
|
||||
APIRequest(method: .get, path: "/api/app/msg/unread-count")
|
||||
)
|
||||
}
|
||||
|
||||
/// POST /api/app/msg/read
|
||||
func markAsRead(messageId: Int) async throws {
|
||||
let _: EmptyPayload = try await client.send(
|
||||
func markAsRead(messageId: Int) async throws -> MessageUnreadCountResponse {
|
||||
try await client.send(
|
||||
APIRequest(
|
||||
method: .post,
|
||||
path: "/api/app/msg/read",
|
||||
@ -54,6 +67,13 @@ final class MessageCenterAPI: MessageCenterServing {
|
||||
)
|
||||
}
|
||||
|
||||
/// POST /api/app/msg/read-all
|
||||
func markAllAsRead() async throws -> MessageReadAllResponse {
|
||||
try await client.send(
|
||||
APIRequest(method: .post, path: "/api/app/msg/read-all")
|
||||
)
|
||||
}
|
||||
|
||||
/// POST /api/app/msg/delete
|
||||
func delete(messageId: Int) async throws {
|
||||
let _: EmptyPayload = try await client.send(
|
||||
|
||||
@ -32,6 +32,46 @@ enum MessageJSONValue: Decodable, Hashable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
/// 消息未读数量响应,用于同步首页红点和桌面图标角标。
|
||||
struct MessageUnreadCountResponse: Decodable, Equatable, Sendable {
|
||||
let unreadCount: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case unreadCount = "unread_count"
|
||||
}
|
||||
|
||||
init(unreadCount: Int = 0) {
|
||||
self.unreadCount = max(unreadCount, 0)
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
unreadCount = max(try container.decodeLossyInt(forKey: .unreadCount) ?? 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// 全部标记已读响应,包含本次更新数量和操作后的未读数量。
|
||||
struct MessageReadAllResponse: Decodable, Equatable, Sendable {
|
||||
let updatedCount: Int
|
||||
let unreadCount: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case updatedCount = "updated_count"
|
||||
case unreadCount = "unread_count"
|
||||
}
|
||||
|
||||
init(updatedCount: Int = 0, unreadCount: Int = 0) {
|
||||
self.updatedCount = max(updatedCount, 0)
|
||||
self.unreadCount = max(unreadCount, 0)
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
updatedCount = max(try container.decodeLossyInt(forKey: .updatedCount) ?? 0, 0)
|
||||
unreadCount = max(try container.decodeLossyInt(forKey: .unreadCount) ?? 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// 消息中心列表响应,对齐 Android `MsgResponse`。
|
||||
struct MessageListResponse: Decodable, Equatable, Sendable {
|
||||
let hasMore: Bool
|
||||
|
||||
@ -11,11 +11,13 @@ final class MessageCenterViewModel {
|
||||
private(set) var isLoading = false
|
||||
private(set) var isRefreshing = false
|
||||
private(set) var isLoadingMore = false
|
||||
private(set) var isMarkingAllAsRead = false
|
||||
private(set) var canLoadMore = false
|
||||
|
||||
var onStateChange: (() -> Void)?
|
||||
var onShowMessage: ((String) -> Void)?
|
||||
var onOpenDetail: ((MessageItem) -> Void)?
|
||||
var onUnreadMessageCountChange: ((Int) -> Void)?
|
||||
|
||||
private let pageSize = 20
|
||||
private var lastId = 0
|
||||
@ -55,9 +57,10 @@ final class MessageCenterViewModel {
|
||||
}
|
||||
|
||||
do {
|
||||
try await api.markAsRead(messageId: id)
|
||||
let response = try await api.markAsRead(messageId: id)
|
||||
let readItem = item.markedRead()
|
||||
items = items.map { $0.id == id ? readItem : $0 }
|
||||
notifyUnreadMessageCountChanged(response.unreadCount)
|
||||
onOpenDetail?(readItem)
|
||||
} catch is CancellationError {
|
||||
return
|
||||
@ -66,6 +69,30 @@ final class MessageCenterViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// 将当前账号的全部消息标记已读,成功时返回服务端最新统计。
|
||||
func markAllAsRead(api: any MessageCenterServing) async -> MessageReadAllResponse? {
|
||||
guard !isLoading, !isMarkingAllAsRead else { return nil }
|
||||
isMarkingAllAsRead = true
|
||||
notifyStateChange()
|
||||
defer {
|
||||
isMarkingAllAsRead = false
|
||||
notifyStateChange()
|
||||
}
|
||||
|
||||
do {
|
||||
let response = try await api.markAllAsRead()
|
||||
items = items.map { $0.isRead ? $0 : $0.markedRead() }
|
||||
notifyUnreadMessageCountChanged(response.unreadCount)
|
||||
onShowMessage?(response.updatedCount > 0 ? "已全部标记为已读" : "暂无未读消息")
|
||||
return response
|
||||
} catch is CancellationError {
|
||||
return nil
|
||||
} catch {
|
||||
onShowMessage?("全部标记已读失败,请稍后重试")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// 删除后从本地列表移除消息。
|
||||
func removeMessage(id: Int) {
|
||||
items.removeAll { $0.id == id }
|
||||
@ -110,6 +137,16 @@ final class MessageCenterViewModel {
|
||||
private func notifyStateChange() {
|
||||
onStateChange?()
|
||||
}
|
||||
|
||||
private func notifyUnreadMessageCountChanged(_ count: Int) {
|
||||
let normalizedCount = max(count, 0)
|
||||
onUnreadMessageCountChange?(normalizedCount)
|
||||
NotificationCenter.default.post(
|
||||
name: NotificationName.unreadMessageCountDidChange,
|
||||
object: nil,
|
||||
userInfo: [NotificationUserInfoKey.unreadCount: normalizedCount]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 消息详情页 ViewModel,负责删除当前消息。
|
||||
|
||||
@ -42,5 +42,9 @@
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@ -24,7 +24,18 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
self.window = window
|
||||
(UIApplication.shared.delegate as? AppDelegate)?.window = window
|
||||
|
||||
PushNotificationManager.shared.attach(window: window)
|
||||
|
||||
registerNotifications()
|
||||
|
||||
if AppStore.shared.session.isLoggedIn {
|
||||
DispatchQueue.main.async {
|
||||
PushNotificationManager.shared.handleLoginCompleted()
|
||||
}
|
||||
}
|
||||
if let response = connectionOptions.notificationResponse {
|
||||
PushNotificationManager.shared.handleNotificationResponse(response)
|
||||
}
|
||||
}
|
||||
|
||||
func sceneDidDisconnect(_ scene: UIScene) {
|
||||
@ -43,6 +54,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
_ = WeChatManager.shared.handleContinueUserActivity(userActivity)
|
||||
}
|
||||
|
||||
func sceneDidBecomeActive(_ scene: UIScene) {
|
||||
PushNotificationManager.shared.retryPendingRegistrationUpload()
|
||||
}
|
||||
|
||||
private func registerNotifications() {
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
@ -62,19 +77,35 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
name: NotificationName.userDidLogin,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleAccountDidSwitch),
|
||||
name: NotificationName.accountDidSwitch,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
@objc private func handleSessionDidExpire() {
|
||||
PushNotificationManager.shared.handleLogout()
|
||||
AppStore.shared.logout()
|
||||
AppRouter.setRoot(.login, on: window)
|
||||
}
|
||||
|
||||
@objc private func handleUserDidLogout() {
|
||||
PushNotificationManager.shared.handleLogout()
|
||||
AppStore.shared.logout()
|
||||
AppRouter.setRoot(.login, on: window)
|
||||
}
|
||||
|
||||
@objc private func handleUserDidLogin() {
|
||||
AppRouter.setRoot(.mainTab, on: window)
|
||||
DispatchQueue.main.async {
|
||||
PushNotificationManager.shared.handleLoginCompleted()
|
||||
PushNotificationManager.shared.routePendingNotificationIfPossible()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleAccountDidSwitch() {
|
||||
PushNotificationManager.shared.handleAccountSwitched()
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ final class HomeViewController: BaseViewController {
|
||||
|
||||
private let viewModel = HomeViewModel()
|
||||
private let homeAPI = NetworkServices.shared.homeAPI
|
||||
private let messageCenterAPI = NetworkServices.shared.messageCenterAPI
|
||||
|
||||
private let scenicHeaderView = HomeScenicHeaderView()
|
||||
private var collectionView: UICollectionView!
|
||||
@ -74,6 +75,7 @@ final class HomeViewController: BaseViewController {
|
||||
Task { @MainActor in self?.showToast(message) }
|
||||
}
|
||||
scenicHeaderView.onTap = { [weak self] in self?.presentScenicSelection() }
|
||||
scenicHeaderView.onMessageTap = { [weak self] in self?.openMessageCenter() }
|
||||
collectionView.delegate = self
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
@ -88,11 +90,26 @@ final class HomeViewController: BaseViewController {
|
||||
name: NotificationName.scenicDidChange,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleUnreadMessageCountDidChange(_:)),
|
||||
name: NotificationName.unreadMessageCountDidChange,
|
||||
object: nil
|
||||
)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleUnreadMessageCountDidChange(_:)),
|
||||
name: UIApplication.didBecomeActiveNotification,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
navigationController?.setNavigationBarHidden(true, animated: animated)
|
||||
if hasInitialized {
|
||||
Task { await refreshUnreadMessageStatus() }
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
@ -272,6 +289,7 @@ final class HomeViewController: BaseViewController {
|
||||
private func initializeHome() async {
|
||||
showLoading()
|
||||
await viewModel.initialize(api: homeAPI)
|
||||
await refreshUnreadMessageStatus()
|
||||
hideLoading()
|
||||
|
||||
applyViewModel()
|
||||
@ -287,7 +305,10 @@ final class HomeViewController: BaseViewController {
|
||||
|
||||
@MainActor
|
||||
private func applyViewModel() {
|
||||
scenicHeaderView.apply(scenicName: viewModel.currentScenicName)
|
||||
scenicHeaderView.apply(
|
||||
scenicName: viewModel.currentScenicName,
|
||||
hasUnreadMessages: viewModel.hasUnreadMessages
|
||||
)
|
||||
|
||||
let showWorkBlock = !viewModel.isMinimalTopRole
|
||||
let showStore = viewModel.currentAppRole == .storeAdmin && viewModel.storeItem != nil
|
||||
@ -499,6 +520,10 @@ final class HomeViewController: BaseViewController {
|
||||
pushScenicSelection()
|
||||
}
|
||||
|
||||
private func openMessageCenter() {
|
||||
navigationController?.pushViewController(MessageCenterViewController(), animated: true)
|
||||
}
|
||||
|
||||
private func pushScenicSelection() {
|
||||
navigationController?.setNavigationBarHidden(false, animated: true)
|
||||
let controller = ScenicSelectionViewController()
|
||||
@ -529,6 +554,7 @@ final class HomeViewController: BaseViewController {
|
||||
viewModel.markNeedsPermissionReload()
|
||||
Task {
|
||||
await viewModel.reloadIfNeeded(api: homeAPI)
|
||||
await refreshUnreadMessageStatus()
|
||||
applyViewModel()
|
||||
await evaluateDialogsWithDelay()
|
||||
}
|
||||
@ -541,6 +567,21 @@ final class HomeViewController: BaseViewController {
|
||||
applyViewModel()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleUnreadMessageCountDidChange(_ notification: Notification) {
|
||||
guard hasInitialized else { return }
|
||||
if let count = notification.userInfo?[NotificationUserInfoKey.unreadCount] as? Int {
|
||||
viewModel.updateUnreadMessageCount(count)
|
||||
Task { await PushNotificationManager.shared.updateApplicationIconBadgeCount(count) }
|
||||
} else {
|
||||
Task { await refreshUnreadMessageStatus() }
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshUnreadMessageStatus() async {
|
||||
guard await viewModel.refreshUnreadMessageStatus(api: messageCenterAPI) else { return }
|
||||
await PushNotificationManager.shared.updateApplicationIconBadgeCount(viewModel.unreadMessageCount)
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeViewController: UICollectionViewDelegate {
|
||||
|
||||
@ -9,10 +9,16 @@ import UIKit
|
||||
/// 首页顶部景区选择条。
|
||||
final class HomeScenicHeaderView: UIView {
|
||||
|
||||
/// 点击景区名称时触发。
|
||||
var onTap: (() -> Void)?
|
||||
|
||||
/// 点击消息入口时触发。
|
||||
var onMessageTap: (() -> Void)?
|
||||
|
||||
private let titleLabel = UILabel()
|
||||
private let arrowView = UIImageView(image: UIImage(systemName: "chevron.down"))
|
||||
private let messageButton = UIButton(type: .system)
|
||||
private let unreadDotView = UIView()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
@ -25,8 +31,24 @@ final class HomeScenicHeaderView: UIView {
|
||||
arrowView.contentMode = .scaleAspectFit
|
||||
arrowView.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
|
||||
var messageConfiguration = UIButton.Configuration.plain()
|
||||
messageConfiguration.image = UIImage(systemName: "bell")
|
||||
messageConfiguration.baseForegroundColor = AppColor.textPrimary
|
||||
messageConfiguration.contentInsets = .zero
|
||||
messageButton.configuration = messageConfiguration
|
||||
messageButton.accessibilityIdentifier = "home_message_button"
|
||||
messageButton.addTarget(self, action: #selector(handleMessageTap), for: .touchUpInside)
|
||||
|
||||
unreadDotView.backgroundColor = AppColor.danger
|
||||
unreadDotView.layer.cornerRadius = 4
|
||||
unreadDotView.isUserInteractionEnabled = false
|
||||
unreadDotView.isHidden = true
|
||||
unreadDotView.accessibilityIdentifier = "home_message_unread_dot"
|
||||
|
||||
addSubview(titleLabel)
|
||||
addSubview(arrowView)
|
||||
addSubview(messageButton)
|
||||
messageButton.addSubview(unreadDotView)
|
||||
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.leading.equalTo(safeAreaLayoutGuide).offset(AppSpacing.screenHorizontalInset)
|
||||
@ -34,12 +56,23 @@ final class HomeScenicHeaderView: UIView {
|
||||
}
|
||||
arrowView.snp.makeConstraints { make in
|
||||
make.leading.equalTo(titleLabel.snp.trailing).offset(AppSpacing.xs)
|
||||
make.trailing.lessThanOrEqualTo(safeAreaLayoutGuide).offset(-AppSpacing.screenHorizontalInset)
|
||||
make.trailing.lessThanOrEqualTo(messageButton.snp.leading).offset(-AppSpacing.xs)
|
||||
make.centerY.equalTo(safeAreaLayoutGuide)
|
||||
make.width.height.equalTo(16)
|
||||
}
|
||||
messageButton.snp.makeConstraints { make in
|
||||
make.trailing.equalTo(safeAreaLayoutGuide).offset(-AppSpacing.screenHorizontalInset)
|
||||
make.centerY.equalTo(safeAreaLayoutGuide)
|
||||
make.width.height.equalTo(44)
|
||||
}
|
||||
unreadDotView.snp.makeConstraints { make in
|
||||
make.centerX.equalTo(messageButton.snp.centerX).offset(9)
|
||||
make.centerY.equalTo(messageButton.snp.centerY).offset(-9)
|
||||
make.width.height.equalTo(8)
|
||||
}
|
||||
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
||||
tap.delegate = self
|
||||
addGestureRecognizer(tap)
|
||||
}
|
||||
|
||||
@ -48,12 +81,26 @@ final class HomeScenicHeaderView: UIView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func apply(scenicName: String) {
|
||||
/// 更新景区名称与未读消息状态。
|
||||
func apply(scenicName: String, hasUnreadMessages: Bool) {
|
||||
let trimmed = scenicName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
titleLabel.text = trimmed.isEmpty ? "请选择景区" : trimmed
|
||||
unreadDotView.isHidden = !hasUnreadMessages
|
||||
messageButton.accessibilityLabel = hasUnreadMessages ? "消息中心,有未读消息" : "消息中心"
|
||||
}
|
||||
|
||||
@objc private func handleTap() {
|
||||
onTap?()
|
||||
}
|
||||
|
||||
@objc private func handleMessageTap() {
|
||||
onMessageTap?()
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeScenicHeaderView: UIGestureRecognizerDelegate {
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
||||
guard let touchedView = touch.view else { return true }
|
||||
return touchedView !== messageButton && !touchedView.isDescendant(of: messageButton)
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,6 +65,29 @@ final class MainTabBarController: UITabBarController {
|
||||
}
|
||||
}
|
||||
|
||||
/// 响应推送点击,切换到目标 Tab 并打开对应业务页面。
|
||||
func openPushDestination(_ destination: PushDestination) {
|
||||
let targetTab = AppTab.home
|
||||
|
||||
selectTab(targetTab)
|
||||
guard let navigationController = tabNavigationControllers[targetTab] else { return }
|
||||
_ = navigationController.popToRootViewController(animated: false)
|
||||
|
||||
let controller: UIViewController?
|
||||
switch destination {
|
||||
case .paymentRecord:
|
||||
controller = PaymentCollectionRecordViewController()
|
||||
case .paymentDetails:
|
||||
controller = PaymentCollectionDetailsViewController()
|
||||
case .messageCenter:
|
||||
controller = MessageCenterViewController()
|
||||
}
|
||||
|
||||
if let controller {
|
||||
navigationController.pushViewController(controller, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
private func configureTabBarAppearance() {
|
||||
tabBar.tintColor = AppColor.primary
|
||||
tabBar.unselectedItemTintColor = AppColor.textTabInactive
|
||||
|
||||
@ -19,6 +19,12 @@ final class MessageCenterViewController: BaseViewController, UITableViewDelegate
|
||||
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
|
||||
private let footerSpinner = UIActivityIndicatorView(style: .medium)
|
||||
private let emptyLabel = UILabel()
|
||||
private lazy var markAllReadButton = UIBarButtonItem(
|
||||
title: "全部已读",
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(markAllReadTapped)
|
||||
)
|
||||
|
||||
private var dataSource: UITableViewDiffableDataSource<Section, MessageItem>!
|
||||
|
||||
@ -46,6 +52,7 @@ final class MessageCenterViewController: BaseViewController, UITableViewDelegate
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = "消息中心"
|
||||
navigationItem.rightBarButtonItem = markAllReadButton
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
@ -79,7 +86,8 @@ final class MessageCenterViewController: BaseViewController, UITableViewDelegate
|
||||
|
||||
override func setupConstraints() {
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.edges.equalTo(view.safeAreaLayoutGuide)
|
||||
make.top.equalTo(view.safeAreaLayoutGuide)
|
||||
make.leading.trailing.bottom.equalToSuperview()
|
||||
}
|
||||
loadingIndicator.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
@ -103,6 +111,11 @@ final class MessageCenterViewController: BaseViewController, UITableViewDelegate
|
||||
self?.openDetail(message)
|
||||
}
|
||||
}
|
||||
viewModel.onUnreadMessageCountChange = { count in
|
||||
Task { @MainActor in
|
||||
await PushNotificationManager.shared.updateApplicationIconBadgeCount(count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
@ -125,6 +138,12 @@ final class MessageCenterViewController: BaseViewController, UITableViewDelegate
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func markAllReadTapped() {
|
||||
Task { @MainActor in
|
||||
await viewModel.markAllAsRead(api: api)
|
||||
}
|
||||
}
|
||||
|
||||
private func configureDataSource() {
|
||||
dataSource = UITableViewDiffableDataSource<Section, MessageItem>(tableView: tableView) { tableView, indexPath, item in
|
||||
let cell = tableView.dequeueReusableCell(
|
||||
@ -154,6 +173,7 @@ final class MessageCenterViewController: BaseViewController, UITableViewDelegate
|
||||
}
|
||||
|
||||
tableView.tableFooterView = footerView()
|
||||
markAllReadButton.isEnabled = !viewModel.isLoading && !viewModel.isMarkingAllAsRead
|
||||
}
|
||||
|
||||
private func applySnapshot(animated: Bool) {
|
||||
|
||||
@ -22,6 +22,10 @@
|
||||
#if __has_include(<UMCommon/UMCommon.h>)
|
||||
#import <UMCommon/UMCommon.h>
|
||||
#endif
|
||||
|
||||
#if __has_include(<JPUSHService.h>)
|
||||
#import <JPUSHService.h>
|
||||
#endif
|
||||
#import "Features/ScenicQueue/Services/AliyunNuiTTSBridge.h"
|
||||
|
||||
#if __has_include(<UMAPM/UMAPM.h>)
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>$(APS_ENVIRONMENT)</string>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:www.zhifly.cn</string>
|
||||
|
||||
@ -29,7 +29,7 @@ final class AllFunctionsViewModelTests: XCTestCase {
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testLoadFunctionsUsesDefaultFirstFourWhenNoSavedURIs() {
|
||||
func testLoadFunctionsUsesDefaultFirstFourAndExcludesMessageCenter() {
|
||||
appStore.permissions.savePermissionItems([
|
||||
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
|
||||
HomePermissionItem(id: "2", name: "云盘", uri: "cloud_management"),
|
||||
@ -44,7 +44,7 @@ final class AllFunctionsViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(viewModel.commonMenus.map(\.uri), [
|
||||
"wallet", "cloud_management", "task_management", "system_settings",
|
||||
])
|
||||
XCTAssertEqual(viewModel.moreMenus.map(\.uri), ["message_center"])
|
||||
XCTAssertTrue(viewModel.moreMenus.isEmpty)
|
||||
XCTAssertEqual(
|
||||
menuStore.savedCommonURIs(
|
||||
accountScope: appStore.session.accountCachePrefix,
|
||||
|
||||
@ -23,14 +23,26 @@ final class HomeCommonMenuStoreTests: XCTestCase {
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testDefaultCommonURIsTakeFirstFour() {
|
||||
let permissions = (1...6).map {
|
||||
func testDefaultCommonURIsTakeFirstFourAndExcludeMessageCenter() {
|
||||
let permissions = [
|
||||
HomePermissionItem(id: "0", name: "消息", uri: "message_center"),
|
||||
] + (1...5).map {
|
||||
HomePermissionItem(id: "\($0)", name: "菜单\($0)", uri: "uri_\($0)")
|
||||
}
|
||||
let uris = HomeCommonMenuStore.defaultCommonURIs(from: permissions)
|
||||
XCTAssertEqual(uris, ["uri_1", "uri_2", "uri_3", "uri_4"])
|
||||
}
|
||||
|
||||
func testSavedMessageCenterIsRemovedFromLegacyCommonURIs() {
|
||||
let key = "scenic_user_u1_role_photographer_common_uris"
|
||||
defaults.set(["message_center", "wallet"], forKey: key)
|
||||
|
||||
let uris = store.savedCommonURIs(accountScope: "scenic_user_u1", roleCode: "photographer")
|
||||
|
||||
XCTAssertEqual(uris, ["wallet"])
|
||||
XCTAssertEqual(defaults.stringArray(forKey: key), ["wallet"])
|
||||
}
|
||||
|
||||
func testSavedCommonURIsOverrideDefault() {
|
||||
let permissions = [
|
||||
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
|
||||
@ -65,8 +77,12 @@ final class HomeCommonMenuStoreTests: XCTestCase {
|
||||
HomeMenuItem(uri: "wallet", title: "钱包", iconName: "wallet.pass"),
|
||||
HomeMenuItem(uri: "cloud_management", title: "云盘", iconName: "icloud"),
|
||||
HomeMenuItem(uri: "task_management", title: "任务", iconName: "checklist"),
|
||||
HomeMenuItem(uri: "message_center", title: "消息中心", iconName: "bell"),
|
||||
]
|
||||
let split = HomeCommonMenuStore.splitMenus(from: menus, commonURIs: ["wallet", "task_management"])
|
||||
let split = HomeCommonMenuStore.splitMenus(
|
||||
from: menus,
|
||||
commonURIs: ["wallet", "task_management", "message_center"]
|
||||
)
|
||||
XCTAssertEqual(split.common.map(\.uri), ["wallet", "task_management"])
|
||||
XCTAssertEqual(split.more.map(\.uri), ["cloud_management"])
|
||||
}
|
||||
@ -75,7 +91,7 @@ final class HomeCommonMenuStoreTests: XCTestCase {
|
||||
let item = HomeMenuCatalog.item(for: "report_photographer")
|
||||
|
||||
XCTAssertEqual(item?.title, "举报摄影师")
|
||||
XCTAssertEqual(item?.iconName, "shield.fill")
|
||||
XCTAssertEqual(item?.iconName, "shield.lefthalf.filled")
|
||||
}
|
||||
|
||||
func testHomeMenuCatalogUsesIOS16CompatibleIcons() {
|
||||
|
||||
@ -90,10 +90,10 @@ final class HomeViewModelTests: XCTestCase {
|
||||
let menuStore = HomeCommonMenuStore(defaults: defaults)
|
||||
appStore.permissions.savePermissionItems([
|
||||
HomePermissionItem(id: "1", name: "钱包", uri: "wallet"),
|
||||
HomePermissionItem(id: "2", name: "云盘", uri: "cloud_management"),
|
||||
HomePermissionItem(id: "3", name: "任务", uri: "task_management"),
|
||||
HomePermissionItem(id: "4", name: "设置", uri: "system_settings"),
|
||||
HomePermissionItem(id: "5", name: "消息", uri: "message_center"),
|
||||
HomePermissionItem(id: "2", name: "消息", uri: "message_center"),
|
||||
HomePermissionItem(id: "3", name: "云盘", uri: "cloud_management"),
|
||||
HomePermissionItem(id: "4", name: "任务", uri: "task_management"),
|
||||
HomePermissionItem(id: "5", name: "设置", uri: "system_settings"),
|
||||
])
|
||||
let viewModel = HomeViewModel(appStore: appStore, commonMenuStore: menuStore)
|
||||
|
||||
@ -231,6 +231,50 @@ final class HomeViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(session.requests.count, 1)
|
||||
}
|
||||
|
||||
func testUnreadMessageStatusUsesCountEndpointAndUpdatesBadgeState() async {
|
||||
let api = HomeMessageCenterAPIMock()
|
||||
api.unreadCountResponses = [
|
||||
MessageUnreadCountResponse(unreadCount: 9),
|
||||
MessageUnreadCountResponse(unreadCount: 0),
|
||||
]
|
||||
let viewModel = HomeViewModel(appStore: appStore)
|
||||
|
||||
await viewModel.refreshUnreadMessageStatus(api: api)
|
||||
XCTAssertTrue(viewModel.hasUnreadMessages)
|
||||
XCTAssertEqual(viewModel.unreadMessageCount, 9)
|
||||
|
||||
await viewModel.refreshUnreadMessageStatus(api: api)
|
||||
XCTAssertFalse(viewModel.hasUnreadMessages)
|
||||
XCTAssertEqual(viewModel.unreadMessageCount, 0)
|
||||
XCTAssertEqual(api.unreadCountCallCount, 2)
|
||||
}
|
||||
|
||||
func testScenicHeaderMessageButtonShowsUnreadStateAndHandlesTap() throws {
|
||||
let headerView = HomeScenicHeaderView(frame: CGRect(x: 0, y: 0, width: 390, height: 100))
|
||||
var tapCount = 0
|
||||
headerView.onMessageTap = { tapCount += 1 }
|
||||
|
||||
headerView.apply(scenicName: "测试景区", hasUnreadMessages: true)
|
||||
headerView.layoutIfNeeded()
|
||||
|
||||
let button = try XCTUnwrap(
|
||||
findSubview(in: headerView) { $0.accessibilityIdentifier == "home_message_button" } as? UIButton
|
||||
)
|
||||
let unreadDot = try XCTUnwrap(
|
||||
findSubview(in: headerView) { $0.accessibilityIdentifier == "home_message_unread_dot" }
|
||||
)
|
||||
XCTAssertEqual(button.bounds.size, CGSize(width: 44, height: 44))
|
||||
XCTAssertEqual(button.accessibilityLabel, "消息中心,有未读消息")
|
||||
XCTAssertFalse(unreadDot.isHidden)
|
||||
|
||||
button.sendActions(for: .touchUpInside)
|
||||
XCTAssertEqual(tapCount, 1)
|
||||
|
||||
headerView.apply(scenicName: "测试景区", hasUnreadMessages: false)
|
||||
XCTAssertEqual(button.accessibilityLabel, "消息中心")
|
||||
XCTAssertTrue(unreadDot.isHidden)
|
||||
}
|
||||
|
||||
func testRefreshLocalDisplayStateClearsWhenNameMissing() {
|
||||
appStore.session.currentScenicId = 10
|
||||
appStore.session.currentScenicName = ""
|
||||
@ -283,6 +327,45 @@ final class HomeViewModelTests: XCTestCase {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func findSubview(in view: UIView, matching predicate: (UIView) -> Bool) -> UIView? {
|
||||
if predicate(view) {
|
||||
return view
|
||||
}
|
||||
for subview in view.subviews {
|
||||
if let match = findSubview(in: subview, matching: predicate) {
|
||||
return match
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// 首页未读消息查询替身。
|
||||
@MainActor
|
||||
private final class HomeMessageCenterAPIMock: MessageCenterServing {
|
||||
var unreadCountResponses: [MessageUnreadCountResponse] = []
|
||||
var unreadCountCallCount = 0
|
||||
|
||||
func list(lastId: Int, limit: Int, unread: Int) async throws -> MessageListResponse {
|
||||
MessageListResponse()
|
||||
}
|
||||
|
||||
func unreadCount() async throws -> MessageUnreadCountResponse {
|
||||
unreadCountCallCount += 1
|
||||
guard !unreadCountResponses.isEmpty else { return MessageUnreadCountResponse() }
|
||||
return unreadCountResponses.removeFirst()
|
||||
}
|
||||
|
||||
func markAsRead(messageId: Int) async throws -> MessageUnreadCountResponse {
|
||||
MessageUnreadCountResponse()
|
||||
}
|
||||
|
||||
func markAllAsRead() async throws -> MessageReadAllResponse {
|
||||
MessageReadAllResponse()
|
||||
}
|
||||
|
||||
func delete(messageId: Int) async throws {}
|
||||
}
|
||||
|
||||
private extension UIColor {
|
||||
|
||||
@ -53,21 +53,37 @@ final class MessageCenterAPITests: XCTestCase {
|
||||
XCTAssertEqual(response.items.first?.extraData?["order_no"], .string("NO123"))
|
||||
}
|
||||
|
||||
func testReadAndDeleteUseAndroidPathsAndBody() async throws {
|
||||
let session = MockURLSession(responses: [envelopeJSON("{}"), envelopeJSON("{}")])
|
||||
func testUnreadCountReadAllReadAndDeleteUseExpectedContracts() async throws {
|
||||
let session = MockURLSession(responses: [
|
||||
envelopeJSON(#"{"unread_count":"12"}"#),
|
||||
envelopeJSON(#"{"unread_count":11}"#),
|
||||
envelopeJSON(#"{"updated_count":11,"unread_count":0}"#),
|
||||
envelopeJSON("{}"),
|
||||
])
|
||||
let api = MessageCenterAPI(client: APIClient(environment: .testing, session: session))
|
||||
|
||||
try await api.markAsRead(messageId: 21)
|
||||
let unread = try await api.unreadCount()
|
||||
let read = try await api.markAsRead(messageId: 21)
|
||||
let readAll = try await api.markAllAsRead()
|
||||
try await api.delete(messageId: 21)
|
||||
|
||||
XCTAssertEqual(session.requests[0].httpMethod, "POST")
|
||||
XCTAssertEqual(session.requests[0].url?.path, "/api/app/msg/read")
|
||||
let readQuery = URLComponents(url: try XCTUnwrap(session.requests[0].url), resolvingAgainstBaseURL: false)?.queryItems
|
||||
XCTAssertEqual(unread.unreadCount, 12)
|
||||
XCTAssertEqual(session.requests[0].httpMethod, "GET")
|
||||
XCTAssertEqual(session.requests[0].url?.path, "/api/app/msg/unread-count")
|
||||
|
||||
XCTAssertEqual(read.unreadCount, 11)
|
||||
XCTAssertEqual(session.requests[1].httpMethod, "POST")
|
||||
XCTAssertEqual(session.requests[1].url?.path, "/api/app/msg/read")
|
||||
let readQuery = URLComponents(url: try XCTUnwrap(session.requests[1].url), resolvingAgainstBaseURL: false)?.queryItems
|
||||
XCTAssertEqual(readQuery?.first { $0.name == "id" }?.value, "21")
|
||||
|
||||
XCTAssertEqual(session.requests[1].httpMethod, "POST")
|
||||
XCTAssertEqual(session.requests[1].url?.path, "/api/app/msg/delete")
|
||||
let deleteBody = try bodyJSON(session.requests[1])
|
||||
XCTAssertEqual(readAll, MessageReadAllResponse(updatedCount: 11, unreadCount: 0))
|
||||
XCTAssertEqual(session.requests[2].httpMethod, "POST")
|
||||
XCTAssertEqual(session.requests[2].url?.path, "/api/app/msg/read-all")
|
||||
|
||||
XCTAssertEqual(session.requests[3].httpMethod, "POST")
|
||||
XCTAssertEqual(session.requests[3].url?.path, "/api/app/msg/delete")
|
||||
let deleteBody = try bodyJSON(session.requests[3])
|
||||
XCTAssertEqual(deleteBody["id"] as? Int, 21)
|
||||
}
|
||||
|
||||
|
||||
@ -32,17 +32,77 @@ final class MessageCenterViewModelTests: XCTestCase {
|
||||
func testUnreadSelectionMarksReadAndOpensDetail() async {
|
||||
let api = MockMessageCenterAPI()
|
||||
api.listResponses = [MessageListResponse(hasMore: false, lastId: 0, items: [makeMessage(id: 3, isRead: false)])]
|
||||
api.readResponse = MessageUnreadCountResponse(unreadCount: 4)
|
||||
let viewModel = MessageCenterViewModel()
|
||||
var opened: MessageItem?
|
||||
var callbackCount: Int?
|
||||
viewModel.onOpenDetail = { opened = $0 }
|
||||
viewModel.onUnreadMessageCountChange = { callbackCount = $0 }
|
||||
let unreadChanged = expectation(
|
||||
forNotification: NotificationName.unreadMessageCountDidChange,
|
||||
object: nil,
|
||||
handler: { notification in
|
||||
notification.userInfo?[NotificationUserInfoKey.unreadCount] as? Int == 4
|
||||
}
|
||||
)
|
||||
|
||||
await viewModel.loadInitial(api: api)
|
||||
await viewModel.selectMessage(id: 3, api: api)
|
||||
await fulfillment(of: [unreadChanged], timeout: 1)
|
||||
|
||||
XCTAssertEqual(api.readCalls, [3])
|
||||
XCTAssertEqual(opened?.id, 3)
|
||||
XCTAssertEqual(opened?.isRead, true)
|
||||
XCTAssertEqual(viewModel.items.first?.isRead, true)
|
||||
XCTAssertEqual(callbackCount, 4)
|
||||
}
|
||||
|
||||
func testMarkAllAsReadUpdatesItemsAndPublishesLatestCount() async {
|
||||
let api = MockMessageCenterAPI()
|
||||
api.listResponses = [MessageListResponse(items: [
|
||||
makeMessage(id: 1, isRead: false),
|
||||
makeMessage(id: 2, isRead: true),
|
||||
])]
|
||||
api.readAllResponse = MessageReadAllResponse(updatedCount: 1, unreadCount: 0)
|
||||
let viewModel = MessageCenterViewModel()
|
||||
var callbackCount: Int?
|
||||
var message: String?
|
||||
viewModel.onUnreadMessageCountChange = { callbackCount = $0 }
|
||||
viewModel.onShowMessage = { message = $0 }
|
||||
let unreadChanged = expectation(
|
||||
forNotification: NotificationName.unreadMessageCountDidChange,
|
||||
object: nil,
|
||||
handler: { notification in
|
||||
notification.userInfo?[NotificationUserInfoKey.unreadCount] as? Int == 0
|
||||
}
|
||||
)
|
||||
|
||||
await viewModel.loadInitial(api: api)
|
||||
let response = await viewModel.markAllAsRead(api: api)
|
||||
await fulfillment(of: [unreadChanged], timeout: 1)
|
||||
|
||||
XCTAssertEqual(api.readAllCallCount, 1)
|
||||
XCTAssertEqual(response, MessageReadAllResponse(updatedCount: 1, unreadCount: 0))
|
||||
XCTAssertTrue(viewModel.items.allSatisfy(\.isRead))
|
||||
XCTAssertEqual(callbackCount, 0)
|
||||
XCTAssertEqual(message, "已全部标记为已读")
|
||||
}
|
||||
|
||||
func testMarkAllAsReadFailureKeepsUnreadState() async {
|
||||
let api = MockMessageCenterAPI()
|
||||
api.listResponses = [MessageListResponse(items: [makeMessage(id: 6, isRead: false)])]
|
||||
api.readAllError = TestError(message: "fail")
|
||||
let viewModel = MessageCenterViewModel()
|
||||
var message: String?
|
||||
viewModel.onShowMessage = { message = $0 }
|
||||
|
||||
await viewModel.loadInitial(api: api)
|
||||
let response = await viewModel.markAllAsRead(api: api)
|
||||
|
||||
XCTAssertNil(response)
|
||||
XCTAssertEqual(api.readAllCallCount, 1)
|
||||
XCTAssertEqual(viewModel.items.first?.isRead, false)
|
||||
XCTAssertEqual(message, "全部标记已读失败,请稍后重试")
|
||||
}
|
||||
|
||||
func testReadSelectionOpensDetailWithoutCallingReadAPI() async {
|
||||
@ -125,8 +185,12 @@ private final class MockMessageCenterAPI: MessageCenterServing {
|
||||
var listResponses: [MessageListResponse] = []
|
||||
var listCalls: [(lastId: Int, limit: Int, unread: Int)] = []
|
||||
var readCalls: [Int] = []
|
||||
var readResponse = MessageUnreadCountResponse()
|
||||
var readAllResponse = MessageReadAllResponse()
|
||||
var readAllCallCount = 0
|
||||
var deleteCalls: [Int] = []
|
||||
var readError: Error?
|
||||
var readAllError: Error?
|
||||
var deleteError: Error?
|
||||
|
||||
func list(lastId: Int, limit: Int, unread: Int) async throws -> MessageListResponse {
|
||||
@ -135,9 +199,20 @@ private final class MockMessageCenterAPI: MessageCenterServing {
|
||||
return listResponses.removeFirst()
|
||||
}
|
||||
|
||||
func markAsRead(messageId: Int) async throws {
|
||||
func unreadCount() async throws -> MessageUnreadCountResponse {
|
||||
MessageUnreadCountResponse()
|
||||
}
|
||||
|
||||
func markAsRead(messageId: Int) async throws -> MessageUnreadCountResponse {
|
||||
readCalls.append(messageId)
|
||||
if let readError { throw readError }
|
||||
return readResponse
|
||||
}
|
||||
|
||||
func markAllAsRead() async throws -> MessageReadAllResponse {
|
||||
readAllCallCount += 1
|
||||
if let readAllError { throw readAllError }
|
||||
return readAllResponse
|
||||
}
|
||||
|
||||
func delete(messageId: Int) async throws {
|
||||
|
||||
357
suixinkanTests/PushNotificationTests.swift
Normal file
357
suixinkanTests/PushNotificationTests.swift
Normal file
@ -0,0 +1,357 @@
|
||||
import UIKit
|
||||
import XCTest
|
||||
@testable import suixinkan
|
||||
|
||||
/// 极光推送测试,覆盖 payload、Registration ID 上报、生命周期和冷启动路由。
|
||||
@MainActor
|
||||
final class PushNotificationTests: XCTestCase {
|
||||
private var defaults: UserDefaults!
|
||||
private var appStore: AppStore!
|
||||
private var suiteName: String!
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
suiteName = "PushNotificationTests.\(UUID().uuidString)"
|
||||
defaults = UserDefaults(suiteName: suiteName)!
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
appStore = AppStore(defaults: defaults)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
if let suiteName {
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
defaults = nil
|
||||
appStore = nil
|
||||
suiteName = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testPayloadRoutesPaymentTypesToTheirPages() {
|
||||
XCTAssertEqual(
|
||||
PushPayload(userInfo: ["type": 1, "route": "message_center"]).destination,
|
||||
.paymentRecord
|
||||
)
|
||||
XCTAssertEqual(
|
||||
PushPayload(userInfo: ["type": "6", "action": "task_management"]).destination,
|
||||
.paymentDetails
|
||||
)
|
||||
}
|
||||
|
||||
func testPayloadIgnoresNonTypeRoutingFieldsAndOtherTypesFallBackToMessageCenter() {
|
||||
let conflictingFields: [AnyHashable: Any] = [
|
||||
"type": 2,
|
||||
"route": "payment",
|
||||
"uri": "/payment_qr",
|
||||
"action": "收款",
|
||||
]
|
||||
|
||||
[0, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 100, 999].forEach { type in
|
||||
XCTAssertEqual(PushPayload(userInfo: ["type": type]).destination, .messageCenter)
|
||||
}
|
||||
XCTAssertEqual(PushPayload(userInfo: conflictingFields).destination, .messageCenter)
|
||||
XCTAssertEqual(PushPayload(userInfo: ["route": "payment"]).destination, .messageCenter)
|
||||
XCTAssertEqual(PushPayload(userInfo: ["data": ["type": 6]]).destination, .messageCenter)
|
||||
XCTAssertEqual(PushPayload(userInfo: ["type": 999]).destination, .messageCenter)
|
||||
}
|
||||
|
||||
func testPayloadReadsTypeFromNestedJSONStringWithoutUsingOtherFields() {
|
||||
let nested = PushPayload(userInfo: [
|
||||
"extras": #"{"type":6,"route":"message_center"}"#,
|
||||
])
|
||||
|
||||
XCTAssertEqual(nested.destination, .paymentDetails)
|
||||
}
|
||||
|
||||
func testPushAPIUsesAndroidCompatibleEndpointAndQuery() async throws {
|
||||
let session = MockURLSession(responses: [try TestJSON.envelope(data: EmptyPayload())])
|
||||
let api = PushAPI(client: APIClient(environment: .testing, session: session))
|
||||
|
||||
try await api.registerJPushID("registration-id-123")
|
||||
|
||||
let request = try XCTUnwrap(session.requests.first)
|
||||
XCTAssertEqual(request.httpMethod, "POST")
|
||||
XCTAssertEqual(request.url?.path, "/api/app/user/register-jpush-id")
|
||||
let query = URLComponents(url: try XCTUnwrap(request.url), resolvingAgainstBaseURL: false)?.queryItems
|
||||
XCTAssertEqual(query?.first { $0.name == "jpush_reg_id" }?.value, "registration-id-123")
|
||||
}
|
||||
|
||||
func testPrivacyGatePreventsSDKInitialization() {
|
||||
let sdk = PushSDKMock(registrationID: "reg-id")
|
||||
let manager = makeManager(sdk: sdk)
|
||||
|
||||
manager.initializeIfPrivacyAccepted()
|
||||
XCTAssertEqual(sdk.initializeCount, 0)
|
||||
|
||||
appStore.session.privacyAgreementAccepted = true
|
||||
manager.initializeIfPrivacyAccepted()
|
||||
manager.initializeIfPrivacyAccepted()
|
||||
XCTAssertEqual(sdk.initializeCount, 1)
|
||||
}
|
||||
|
||||
func testEmptyRegistrationIDDoesNotUpload() async {
|
||||
authenticate(userID: "100")
|
||||
let sdk = PushSDKMock(registrationID: nil)
|
||||
let api = PushRegistrationAPIMock()
|
||||
let manager = makeManager(sdk: sdk, api: api)
|
||||
|
||||
manager.handleLoginCompleted()
|
||||
await Task.yield()
|
||||
await Task.yield()
|
||||
|
||||
XCTAssertTrue(api.registrationIDs.isEmpty)
|
||||
}
|
||||
|
||||
func testLoggedOutStateDoesNotUpload() async {
|
||||
appStore.session.privacyAgreementAccepted = true
|
||||
let sdk = PushSDKMock(registrationID: "reg-id")
|
||||
let api = PushRegistrationAPIMock()
|
||||
let manager = makeManager(sdk: sdk, api: api)
|
||||
|
||||
manager.initializeIfPrivacyAccepted()
|
||||
await Task.yield()
|
||||
await Task.yield()
|
||||
|
||||
XCTAssertTrue(api.registrationIDs.isEmpty)
|
||||
}
|
||||
|
||||
func testLoginRequestsAuthorizationAndUploadsRegistrationIDOnce() async {
|
||||
authenticate(userID: "100")
|
||||
let sdk = PushSDKMock(registrationID: "reg-id")
|
||||
let api = PushRegistrationAPIMock()
|
||||
let manager = makeManager(sdk: sdk, api: api)
|
||||
|
||||
manager.handleLoginCompleted()
|
||||
await waitUntil { api.registrationIDs.count == 1 }
|
||||
manager.retryPendingRegistrationUpload()
|
||||
await Task.yield()
|
||||
|
||||
XCTAssertEqual(sdk.initializeCount, 1)
|
||||
XCTAssertEqual(sdk.authorizationRequestCount, 1)
|
||||
XCTAssertEqual(api.registrationIDs, ["reg-id"])
|
||||
}
|
||||
|
||||
func testAccountSwitchForcesRegistrationIDRebind() async {
|
||||
authenticate(userID: "100")
|
||||
let sdk = PushSDKMock(registrationID: "reg-id")
|
||||
let api = PushRegistrationAPIMock()
|
||||
let manager = makeManager(sdk: sdk, api: api)
|
||||
|
||||
manager.handleLoginCompleted()
|
||||
await waitUntil { api.registrationIDs.count == 1 }
|
||||
appStore.session.userId = "200"
|
||||
manager.handleAccountSwitched()
|
||||
await waitUntil { api.registrationIDs.count == 2 }
|
||||
|
||||
XCTAssertEqual(api.registrationIDs, ["reg-id", "reg-id"])
|
||||
}
|
||||
|
||||
func testFailedRegistrationUploadRetriesOnNextActivation() async {
|
||||
authenticate(userID: "100")
|
||||
let sdk = PushSDKMock(registrationID: "reg-id")
|
||||
let api = PushRegistrationAPIMock(errors: [PushTestError.expectedFailure, nil])
|
||||
let manager = makeManager(sdk: sdk, api: api)
|
||||
|
||||
manager.handleLoginCompleted()
|
||||
await waitUntil { api.registrationIDs.count == 1 }
|
||||
await waitUntil { api.completedCallCount == 1 }
|
||||
manager.retryPendingRegistrationUpload()
|
||||
await waitUntil { api.registrationIDs.count == 2 }
|
||||
|
||||
XCTAssertEqual(api.registrationIDs, ["reg-id", "reg-id"])
|
||||
}
|
||||
|
||||
func testNotificationTapRoutesButOrdinaryLifecycleDoesNot() {
|
||||
authenticate(userID: "100")
|
||||
let router = PushRouterMock()
|
||||
let manager = makeManager(router: router)
|
||||
|
||||
manager.initializeIfPrivacyAccepted()
|
||||
XCTAssertTrue(router.destinations.isEmpty)
|
||||
|
||||
manager.handleNotificationTap(
|
||||
payload: PushPayload(userInfo: ["type": 1, "route": "task_management"]),
|
||||
requestIdentifier: "request-1"
|
||||
)
|
||||
XCTAssertEqual(router.destinations, [.paymentRecord])
|
||||
}
|
||||
|
||||
func testRemoteNotificationNotifiesUnreadMessageStateChange() async {
|
||||
let manager = makeManager()
|
||||
let unreadChanged = expectation(
|
||||
forNotification: NotificationName.unreadMessageCountDidChange,
|
||||
object: nil
|
||||
)
|
||||
|
||||
manager.handleRemoteNotification(["route": "message_center"])
|
||||
|
||||
await fulfillment(of: [unreadChanged], timeout: 1)
|
||||
}
|
||||
|
||||
func testApplicationIconBadgeUsesLatestNonnegativeCount() async {
|
||||
let badgeSetter = ApplicationIconBadgeSetterMock()
|
||||
let manager = makeManager(badgeSetter: badgeSetter)
|
||||
|
||||
await manager.updateApplicationIconBadgeCount(12)
|
||||
await manager.updateApplicationIconBadgeCount(-2)
|
||||
|
||||
XCTAssertEqual(badgeSetter.counts, [12, 0])
|
||||
}
|
||||
|
||||
func testRouteCoordinatorWaitsForLoginAndDeduplicatesColdStartResponse() {
|
||||
let coordinator = PushRouteCoordinator(appStore: appStore)
|
||||
let window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window.rootViewController = UINavigationController(rootViewController: UIViewController())
|
||||
coordinator.attach(window: window)
|
||||
|
||||
coordinator.handle(destination: .paymentDetails, requestIdentifier: "cold-request")
|
||||
authenticate(userID: "100")
|
||||
let mainTab = MainTabBarController()
|
||||
mainTab.loadViewIfNeeded()
|
||||
window.rootViewController = mainTab
|
||||
coordinator.routePendingIfPossible()
|
||||
|
||||
let navigationController = mainTab.selectedViewController as? UINavigationController
|
||||
XCTAssertTrue(navigationController?.topViewController is PaymentCollectionDetailsViewController)
|
||||
|
||||
coordinator.handle(destination: .messageCenter, requestIdentifier: "cold-request")
|
||||
XCTAssertTrue(navigationController?.topViewController is PaymentCollectionDetailsViewController)
|
||||
}
|
||||
|
||||
private func authenticate(userID: String) {
|
||||
appStore.session.privacyAgreementAccepted = true
|
||||
appStore.session.saveToken("token")
|
||||
appStore.session.userId = userID
|
||||
appStore.session.accountType = .storeUser
|
||||
}
|
||||
|
||||
private func makeManager(
|
||||
sdk: PushSDKMock? = nil,
|
||||
api: PushRegistrationAPIMock? = nil,
|
||||
router: PushRouterMock? = nil,
|
||||
badgeSetter: ApplicationIconBadgeSetterMock? = nil
|
||||
) -> PushNotificationManager {
|
||||
let resolvedSDK: PushSDKMock
|
||||
if let sdk {
|
||||
resolvedSDK = sdk
|
||||
} else {
|
||||
resolvedSDK = PushSDKMock(registrationID: nil)
|
||||
}
|
||||
|
||||
let resolvedAPI: PushRegistrationAPIMock
|
||||
if let api {
|
||||
resolvedAPI = api
|
||||
} else {
|
||||
resolvedAPI = PushRegistrationAPIMock()
|
||||
}
|
||||
|
||||
let resolvedRouter: PushRouterMock
|
||||
if let router {
|
||||
resolvedRouter = router
|
||||
} else {
|
||||
resolvedRouter = PushRouterMock()
|
||||
}
|
||||
|
||||
return PushNotificationManager(
|
||||
sdk: resolvedSDK,
|
||||
api: resolvedAPI,
|
||||
appStore: appStore,
|
||||
defaults: defaults,
|
||||
router: resolvedRouter,
|
||||
applicationIconBadgeSetter: badgeSetter ?? ApplicationIconBadgeSetterMock()
|
||||
)
|
||||
}
|
||||
|
||||
private func waitUntil(
|
||||
timeoutIterations: Int = 100,
|
||||
_ condition: () -> Bool
|
||||
) async {
|
||||
for _ in 0..<timeoutIterations {
|
||||
if condition() { return }
|
||||
await Task.yield()
|
||||
}
|
||||
XCTFail("等待异步条件超时")
|
||||
}
|
||||
}
|
||||
|
||||
/// 记录 App 桌面图标角标更新的测试替身。
|
||||
@MainActor
|
||||
private final class ApplicationIconBadgeSetterMock: ApplicationIconBadgeSetting {
|
||||
private(set) var counts: [Int] = []
|
||||
|
||||
func setBadgeCount(_ count: Int) async {
|
||||
counts.append(count)
|
||||
}
|
||||
}
|
||||
|
||||
/// 可控的极光 SDK 测试替身,记录初始化与通知授权行为。
|
||||
@MainActor
|
||||
private final class PushSDKMock: PushSDKProviding {
|
||||
private let registrationID: String?
|
||||
private(set) var initializeCount = 0
|
||||
private(set) var authorizationRequestCount = 0
|
||||
private(set) var deviceTokens: [Data] = []
|
||||
|
||||
init(registrationID: String?) {
|
||||
self.registrationID = registrationID
|
||||
}
|
||||
|
||||
func initialize(launchOptions: [UIApplication.LaunchOptionsKey: Any]?, isProduction: Bool) {
|
||||
initializeCount += 1
|
||||
}
|
||||
|
||||
func requestAuthorization(delegate: JPUSHRegisterDelegate) {
|
||||
authorizationRequestCount += 1
|
||||
}
|
||||
|
||||
func registerDeviceToken(_ deviceToken: Data) {
|
||||
deviceTokens.append(deviceToken)
|
||||
}
|
||||
|
||||
func fetchRegistrationID(completion: @escaping (Int32, String?) -> Void) {
|
||||
completion(0, registrationID)
|
||||
}
|
||||
}
|
||||
|
||||
/// 可控制成功或失败结果的 Registration ID 上报接口测试替身。
|
||||
@MainActor
|
||||
private final class PushRegistrationAPIMock: PushRegistrationServing {
|
||||
private var errors: [Error?]
|
||||
private(set) var registrationIDs: [String] = []
|
||||
private(set) var completedCallCount = 0
|
||||
|
||||
init(errors: [Error?] = []) {
|
||||
self.errors = errors
|
||||
}
|
||||
|
||||
func registerJPushID(_ registrationID: String) async throws {
|
||||
registrationIDs.append(registrationID)
|
||||
let error = errors.isEmpty ? nil : errors.removeFirst()
|
||||
completedCallCount += 1
|
||||
if let error { throw error }
|
||||
}
|
||||
}
|
||||
|
||||
/// 记录通知点击路由行为的测试替身。
|
||||
@MainActor
|
||||
private final class PushRouterMock: PushRouting {
|
||||
private(set) var destinations: [PushDestination] = []
|
||||
private(set) var resetCount = 0
|
||||
|
||||
func attach(window: UIWindow) {}
|
||||
|
||||
func handle(destination: PushDestination, requestIdentifier: String) {
|
||||
destinations.append(destination)
|
||||
}
|
||||
|
||||
func routePendingIfPossible() {}
|
||||
|
||||
func resetPendingRoute() {
|
||||
resetCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
/// 推送单元测试使用的可预期失败错误。
|
||||
private enum PushTestError: Error {
|
||||
case expectedFailure
|
||||
}
|
||||
Reference in New Issue
Block a user