From 741e701750e55ac376cc50c5ca38f3ba484f0086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=89=E7=A7=8B?= <497055328@qq.com> Date: Tue, 7 Jul 2026 14:20:04 +0800 Subject: [PATCH] =?UTF-8?q?=E9=9B=86=E6=88=90=E5=8F=8B=E7=9B=9F=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E5=88=86=E6=9E=90=E5=92=8C=E5=BA=94=E7=94=A8=E6=80=A7?= =?UTF-8?q?=E8=83=BD=E7=9B=91=E6=8E=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Podfile | 3 ++ Podfile.lock | 16 +++++- suixinkan.xcodeproj/project.pbxproj | 8 +++ suixinkan/AppDelegate.swift | 2 +- suixinkan/Config/UmengBootstrap.swift | 51 ++++++++++++++++++ suixinkan/Config/UmengConfig.swift | 15 ++++++ .../Features/Auth/AuthSessionHelper.swift | 1 + suixinkan/suixinkan-Bridging-Header.h | 7 +++ suixinkanTests/UmengBootstrapTests.swift | 54 +++++++++++++++++++ 9 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 suixinkan/Config/UmengBootstrap.swift create mode 100644 suixinkan/Config/UmengConfig.swift create mode 100644 suixinkanTests/UmengBootstrapTests.swift diff --git a/Podfile b/Podfile index 2fbecab..6c7886f 100644 --- a/Podfile +++ b/Podfile @@ -5,6 +5,9 @@ target 'suixinkan' do pod 'AMapLocation-NO-IDFA' pod 'AMap3DMap-NO-IDFA' pod 'AMapSearch-NO-IDFA' + pod 'UMCommon' + pod 'UMDevice' + pod 'UMAPM' end target 'suixinkanTests' do diff --git a/Podfile.lock b/Podfile.lock index 1fc4a8a..b0d0a9d 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -6,11 +6,19 @@ PODS: - AMapFoundation-NO-IDFA (>= 1.9.0) - AMapSearch-NO-IDFA (9.8.0): - AMapFoundation-NO-IDFA (>= 1.9.0) + - UMAPM (2.0.7): + - UMCommon + - UMCommon (7.5.11): + - UMDevice + - UMDevice (3.6.0) DEPENDENCIES: - AMap3DMap-NO-IDFA - AMapLocation-NO-IDFA - AMapSearch-NO-IDFA + - UMAPM + - UMCommon + - UMDevice SPEC REPOS: trunk: @@ -18,13 +26,19 @@ SPEC REPOS: - AMapFoundation-NO-IDFA - AMapLocation-NO-IDFA - AMapSearch-NO-IDFA + - UMAPM + - UMCommon + - UMDevice SPEC CHECKSUMS: AMap3DMap-NO-IDFA: f53ee0cb33db83c1a08856b5995e53a0c2096158 AMapFoundation-NO-IDFA: a2e3c895398d7ee757278e1a0a8f9359da4b146e AMapLocation-NO-IDFA: 7cd8fc837ea41edfbf4d937cd20572e277b77d18 AMapSearch-NO-IDFA: c0afd2a69a076d4228becda4401dbe4a279a03ef + UMAPM: ba9fbebe8dd2048b251c332ab0afc86e1ba5ea63 + UMCommon: d652b3b372a801b36db203f5b546ab3e15676898 + UMDevice: 20b7b3c37a36b2c5c7ca8b5a54386ff9ed2b3b19 -PODFILE CHECKSUM: bf22a001ae02e51eb203f1eae5556a33b831ada7 +PODFILE CHECKSUM: b2f49c71df3a1aca1cf08d192e467a8be772283c COCOAPODS: 1.16.2 diff --git a/suixinkan.xcodeproj/project.pbxproj b/suixinkan.xcodeproj/project.pbxproj index 9427845..fdf103e 100644 --- a/suixinkan.xcodeproj/project.pbxproj +++ b/suixinkan.xcodeproj/project.pbxproj @@ -364,6 +364,10 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + ); PRODUCT_BUNDLE_IDENTIFIER = com.yuanzhixiang.suixinkan; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -403,6 +407,10 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + ); PRODUCT_BUNDLE_IDENTIFIER = com.yuanzhixiang.suixinkan; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; diff --git a/suixinkan/AppDelegate.swift b/suixinkan/AppDelegate.swift index b3d5b3f..87fa38c 100644 --- a/suixinkan/AppDelegate.swift +++ b/suixinkan/AppDelegate.swift @@ -13,6 +13,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + UmengBootstrap.configureIfNeeded() if AppStore.shared.privacyAgreementAccepted, !AppStore.shared.token.isEmpty { AMapBootstrap.configureIfNeeded() } @@ -35,4 +36,3 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } - diff --git a/suixinkan/Config/UmengBootstrap.swift b/suixinkan/Config/UmengBootstrap.swift new file mode 100644 index 0000000..6f72d1d --- /dev/null +++ b/suixinkan/Config/UmengBootstrap.swift @@ -0,0 +1,51 @@ +// +// UmengBootstrap.swift +// suixinkan +// + +import Foundation + +/// 友盟 SDK 隐私合规初始化入口,必须在用户同意隐私政策后调用。 +enum UmengBootstrap { + + /// 当前进程内是否已经完成友盟 SDK 初始化。 + private(set) static var isConfigured = false + + private static let lock = NSLock() + private static var configureHandler: (_ appKey: String, _ channel: String) -> Void = { appKey, channel in + UMConfigure.setLogEnabled(false) + UMConfigure.initWithAppkey(appKey, channel: channel) + } + + /// 在用户已同意 App 隐私政策后配置友盟 SDK,幂等。 + @discardableResult + static func configureIfNeeded(appStore: AppStore = .shared) -> Bool { + guard appStore.privacyAgreementAccepted else { return false } + + lock.lock() + defer { lock.unlock() } + guard !isConfigured else { return true } + + configureHandler(UmengConfig.appKey, UmengConfig.channel) + isConfigured = true + return true + } + + /// 测试专用:替换真实 SDK 初始化实现。 + static func setConfigureHandlerForTesting(_ handler: @escaping (_ appKey: String, _ channel: String) -> Void) { + lock.lock() + defer { lock.unlock() } + configureHandler = handler + } + + /// 测试专用:重置初始化状态与默认初始化实现。 + static func resetForTesting() { + lock.lock() + defer { lock.unlock() } + isConfigured = false + configureHandler = { appKey, channel in + UMConfigure.setLogEnabled(false) + UMConfigure.initWithAppkey(appKey, channel: channel) + } + } +} diff --git a/suixinkan/Config/UmengConfig.swift b/suixinkan/Config/UmengConfig.swift new file mode 100644 index 0000000..5c372fd --- /dev/null +++ b/suixinkan/Config/UmengConfig.swift @@ -0,0 +1,15 @@ +// +// UmengConfig.swift +// suixinkan +// + +import Foundation + +/// 友盟统计与性能监控 SDK 配置。 +enum UmengConfig { + /// 友盟后台申请的 iOS AppKey。 + nonisolated static let appKey = "6a470b81cbfa6959516c34d0" + + /// 友盟渠道标识,默认使用官方 App Store 渠道。 + nonisolated static let channel = "App Store" +} diff --git a/suixinkan/Features/Auth/AuthSessionHelper.swift b/suixinkan/Features/Auth/AuthSessionHelper.swift index 3df6df5..12522f6 100644 --- a/suixinkan/Features/Auth/AuthSessionHelper.swift +++ b/suixinkan/Features/Auth/AuthSessionHelper.swift @@ -25,6 +25,7 @@ enum AuthSessionHelper { if privacyAgreementAccepted { AMapBootstrap.configureIfNeeded() + UmengBootstrap.configureIfNeeded() } NotificationCenter.default.post(name: NotificationName.userDidLogin, object: nil) diff --git a/suixinkan/suixinkan-Bridging-Header.h b/suixinkan/suixinkan-Bridging-Header.h index a6b0052..bd5a286 100644 --- a/suixinkan/suixinkan-Bridging-Header.h +++ b/suixinkan/suixinkan-Bridging-Header.h @@ -7,3 +7,10 @@ #import #import #import +#import + +#if __has_include() +#import +#elif __has_include() +#import +#endif diff --git a/suixinkanTests/UmengBootstrapTests.swift b/suixinkanTests/UmengBootstrapTests.swift new file mode 100644 index 0000000..f2411a5 --- /dev/null +++ b/suixinkanTests/UmengBootstrapTests.swift @@ -0,0 +1,54 @@ +// +// UmengBootstrapTests.swift +// suixinkanTests +// + +import XCTest +@testable import suixinkan + +/// 友盟 SDK 初始化 gate 测试。 +final class UmengBootstrapTests: XCTestCase { + + private var defaults: UserDefaults! + private var appStore: AppStore! + + override func setUp() { + super.setUp() + UmengBootstrap.resetForTesting() + defaults = UserDefaults(suiteName: "UmengBootstrapTests")! + defaults.removePersistentDomain(forName: "UmengBootstrapTests") + appStore = AppStore(defaults: defaults) + } + + override func tearDown() { + UmengBootstrap.resetForTesting() + super.tearDown() + } + + func testConfigureReturnsFalseWhenPrivacyNotAccepted() { + var callCount = 0 + UmengBootstrap.setConfigureHandlerForTesting { _, _ in + callCount += 1 + } + appStore.privacyAgreementAccepted = false + + XCTAssertFalse(UmengBootstrap.configureIfNeeded(appStore: appStore)) + XCTAssertFalse(UmengBootstrap.isConfigured) + XCTAssertEqual(callCount, 0) + } + + func testConfigureIsIdempotentWhenPrivacyAccepted() { + var calls: [(appKey: String, channel: String)] = [] + UmengBootstrap.setConfigureHandlerForTesting { appKey, channel in + calls.append((appKey, channel)) + } + appStore.privacyAgreementAccepted = true + + XCTAssertTrue(UmengBootstrap.configureIfNeeded(appStore: appStore)) + XCTAssertTrue(UmengBootstrap.isConfigured) + XCTAssertTrue(UmengBootstrap.configureIfNeeded(appStore: appStore)) + XCTAssertEqual(calls.count, 1) + XCTAssertEqual(calls.first?.appKey, UmengConfig.appKey) + XCTAssertEqual(calls.first?.channel, UmengConfig.channel) + } +}