diff --git a/YuMi.xcodeproj/project.pbxproj b/YuMi.xcodeproj/project.pbxproj index 41e1f31..ab9692c 100644 --- a/YuMi.xcodeproj/project.pbxproj +++ b/YuMi.xcodeproj/project.pbxproj @@ -416,6 +416,12 @@ 4C1392962D6DA22B00A6DFB5 /* RechargerTransferHistoryViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C1392952D6DA22B00A6DFB5 /* RechargerTransferHistoryViewController.m */; }; 4C13929D2D70441500A6DFB5 /* giftgift.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 4C13929B2D70441500A6DFB5 /* giftgift.mp4 */; }; 4C1392A12D71675900A6DFB5 /* coincoin.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 4C1392A02D71675900A6DFB5 /* coincoin.mp4 */; }; + 4C14398D2EA62D3C0037B074 /* EPClientAPIBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C14398C2EA62D3C0037B074 /* EPClientAPIBridge.m */; }; + 4C1439912EA62DD30037B074 /* EPConfigManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C14398F2EA62DD30037B074 /* EPConfigManager.swift */; }; + 4C1439922EA62DD30037B074 /* EPConfigStorage.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C1439902EA62DD30037B074 /* EPConfigStorage.m */; }; + 4C1439942EA630490037B074 /* EPConfigAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1439932EA630490037B074 /* EPConfigAPI.swift */; }; + 4C14399C2EA635EB0037B074 /* EPNIMConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C1439972EA635EB0037B074 /* EPNIMConfig.m */; }; + 4C14399D2EA635EB0037B074 /* EPNIMManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C1439992EA635EB0037B074 /* EPNIMManager.m */; }; 4C1892992CF84349004D4426 /* RoomCahtCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C1892982CF84349004D4426 /* RoomCahtCell.m */; }; 4C1A141B2DCB4AB700B6D0CA /* ChatFaceVo.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A141A2DCB4AB700B6D0CA /* ChatFaceVo.m */; }; 4C1E98BF2E9A3A540031AE79 /* EPMineAPIHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4C1E98BD2E9A3A540031AE79 /* EPMineAPIHelper.m */; }; @@ -2478,6 +2484,16 @@ 4C1392952D6DA22B00A6DFB5 /* RechargerTransferHistoryViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RechargerTransferHistoryViewController.m; sourceTree = ""; }; 4C13929B2D70441500A6DFB5 /* giftgift.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = giftgift.mp4; sourceTree = ""; }; 4C1392A02D71675900A6DFB5 /* coincoin.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = coincoin.mp4; sourceTree = ""; }; + 4C14398B2EA62D3C0037B074 /* EPClientAPIBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EPClientAPIBridge.h; sourceTree = ""; }; + 4C14398C2EA62D3C0037B074 /* EPClientAPIBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EPClientAPIBridge.m; sourceTree = ""; }; + 4C14398E2EA62D8C0037B074 /* EPConfigStorage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EPConfigStorage.h; sourceTree = ""; }; + 4C14398F2EA62DD30037B074 /* EPConfigManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EPConfigManager.swift; sourceTree = ""; }; + 4C1439902EA62DD30037B074 /* EPConfigStorage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EPConfigStorage.m; sourceTree = ""; }; + 4C1439932EA630490037B074 /* EPConfigAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EPConfigAPI.swift; sourceTree = ""; }; + 4C1439962EA635EB0037B074 /* EPNIMConfig.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EPNIMConfig.h; sourceTree = ""; }; + 4C1439972EA635EB0037B074 /* EPNIMConfig.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EPNIMConfig.m; sourceTree = ""; }; + 4C1439982EA635EB0037B074 /* EPNIMManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EPNIMManager.h; sourceTree = ""; }; + 4C1439992EA635EB0037B074 /* EPNIMManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EPNIMManager.m; sourceTree = ""; }; 4C1892972CF84349004D4426 /* RoomCahtCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RoomCahtCell.h; sourceTree = ""; }; 4C1892982CF84349004D4426 /* RoomCahtCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RoomCahtCell.m; sourceTree = ""; }; 4C1A14192DCB4AB700B6D0CA /* ChatFaceVo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ChatFaceVo.h; sourceTree = ""; }; @@ -6465,6 +6481,7 @@ 4C0642922E98EF0A00BAF413 /* E-P */ = { isa = PBXGroup; children = ( + 4C14399B2EA635EB0037B074 /* SDKManager */, 4C59C0FC2EA2508F00D1F7BD /* NewMessage */, 4CD19C852E9CB31C0069DAA0 /* NewLogin */, 4C1E98C22E9A45160031AE79 /* Common */, @@ -6485,6 +6502,25 @@ path = Services; sourceTree = ""; }; + 4C14399A2EA635EB0037B074 /* NIMSDK */ = { + isa = PBXGroup; + children = ( + 4C1439962EA635EB0037B074 /* EPNIMConfig.h */, + 4C1439972EA635EB0037B074 /* EPNIMConfig.m */, + 4C1439982EA635EB0037B074 /* EPNIMManager.h */, + 4C1439992EA635EB0037B074 /* EPNIMManager.m */, + ); + path = NIMSDK; + sourceTree = ""; + }; + 4C14399B2EA635EB0037B074 /* SDKManager */ = { + isa = PBXGroup; + children = ( + 4C14399A2EA635EB0037B074 /* NIMSDK */, + ); + path = SDKManager; + sourceTree = ""; + }; 4C1E98BE2E9A3A540031AE79 /* Services */ = { isa = PBXGroup; children = ( @@ -6497,6 +6533,12 @@ 4C1E98C22E9A45160031AE79 /* Common */ = { isa = PBXGroup; children = ( + 4C1439932EA630490037B074 /* EPConfigAPI.swift */, + 4C14398F2EA62DD30037B074 /* EPConfigManager.swift */, + 4C1439902EA62DD30037B074 /* EPConfigStorage.m */, + 4C14398E2EA62D8C0037B074 /* EPConfigStorage.h */, + 4C14398B2EA62D3C0037B074 /* EPClientAPIBridge.h */, + 4C14398C2EA62D3C0037B074 /* EPClientAPIBridge.m */, 4C1E98C72E9A4DFD0031AE79 /* EPQCloudConfig.swift */, 4C1E98C82E9A4DFD0031AE79 /* EPSDKManager.swift */, 4C1E98C02E9A45160031AE79 /* EPImageUploader.swift */, @@ -12526,6 +12568,9 @@ 4C1A141B2DCB4AB700B6D0CA /* ChatFaceVo.m in Sources */, E8664ED627E434D5000171BA /* XPRoomPKRecordViewController.m in Sources */, E87E914E2796678D00A7B3F2 /* XPMineDressEmptyTableViewCell.m in Sources */, + 4C14398D2EA62D3C0037B074 /* EPClientAPIBridge.m in Sources */, + 4C14399C2EA635EB0037B074 /* EPNIMConfig.m in Sources */, + 4C14399D2EA635EB0037B074 /* EPNIMManager.m in Sources */, 232EBBFF2BD7A25500E8CEAD /* MSParamsDecode.m in Sources */, 9B7D804D27537950003DAC0C /* MessageCell.m in Sources */, 23E9EAA62A84C97C00B792F2 /* XPMineUserInfoTagVC.m in Sources */, @@ -12849,6 +12894,7 @@ E8751E5928A62A390056EF44 /* Api+Sailing.m in Sources */, E897ABFF28AF39B4003B3587 /* XPSailingAnimationView.m in Sources */, E885D5362977CE28004DC088 /* SessionSettingModel.m in Sources */, + 4C1439942EA630490037B074 /* EPConfigAPI.swift in Sources */, 9BE01AE128937DBC00B50299 /* XPDressUpShopCardViewController.m in Sources */, E896EFA22771AE9400AD2CC1 /* XPMineFriendViewController.m in Sources */, 1427218D29A75F6F00C7C423 /* HTTPRedirectResponse.m in Sources */, @@ -13108,6 +13154,8 @@ 2331C1742A5EB71000E1D940 /* XPNobleCenterResidueView.m in Sources */, 4C0A5B842E02675300955219 /* MedalsCollectionViewCell.m in Sources */, E80E2377299A47F60013FD40 /* AESUtils.m in Sources */, + 4C1439912EA62DD30037B074 /* EPConfigManager.swift in Sources */, + 4C1439922EA62DD30037B074 /* EPConfigStorage.m in Sources */, E81060E229876E9100B772F0 /* MessageImageModel.m in Sources */, E839533F276A0CDB00CF2F24 /* XPMineNameplateTableViewCell.m in Sources */, E80E09AE2A41336500CD2BE7 /* XPWebViewNavView.m in Sources */, diff --git a/YuMi/Appdelegate/AppDelegate.m b/YuMi/Appdelegate/AppDelegate.m index 6cb7b19..26ca932 100644 --- a/YuMi/Appdelegate/AppDelegate.m +++ b/YuMi/Appdelegate/AppDelegate.m @@ -7,21 +7,16 @@ #import "AppDelegate.h" -#import "TabbarViewController.h" #import "BaseNavigationController.h" -#import "AppDelegate+ThirdConfig.h" -#import #import #import "ClientConfig.h" -#import "LoginViewController.h" #import "AccountModel.h" #import "YuMi-swift.h" -#import "SessionViewController.h" -#import "LoginFullInfoViewController.h" #import "UIView+VAP.h" #import "SocialShareManager.h" #import "EPSignatureColorGuideView.h" #import "EPEmotionColorStorage.h" +#import "EPNIMManager.h" UIKIT_EXTERN NSString * const kOpenRoomNotification; @@ -61,18 +56,29 @@ void qg_VAP_Logger_handler(VAPLogLevel level, const char* file, int line, const [self.window makeKeyAndVisible]; [VAPView registerHWDLog:qg_VAP_Logger_handler]; - - ///初始化一些 sdk配置 - [self initThirdConfig]; - [self initUM:application launchOptions:launchOptions]; + // 冷启动配置:client/init → client/config,由 EPConfigManager 统一调度 @kWeakify(self); - [[ClientConfig shareConfig] clientConfig:^{ - @kStrongify(self); - dispatch_async(dispatch_get_main_queue(), ^{ + [[EPConfigManager shared] startColdBootWithOnSuccess:^{ + // 配置成功,初始化 NIMSDK 并进入主页面 + [[EPNIMManager sharedManager] initializeWithCompletion:^(NSError * _Nullable error) { + @kStrongify(self); + if (error) { + NSLog(@"[AppDelegate] NIMSDK 初始化失败: %@", error); + } else { + NSLog(@"[AppDelegate] NIMSDK 初始化成功"); + } + // 无论 NIMSDK 是否成功,都进入主页面 [self loadMainPage]; - [self setupLaunchADView]; - }); + }]; + } onFailure:^(NSString * _Nonnull errorMessage) { + @kStrongify(self); + // 配置失败,显示错误提示 + UIAlertController *alert = [UIAlertController alertControllerWithTitle:YMLocalizedString(@"提示") + message:errorMessage + preferredStyle:UIAlertControllerStyleAlert]; + [alert addAction:[UIAlertAction actionWithTitle:YMLocalizedString(@"确定") style:UIAlertActionStyleDefault handler:nil]]; + [self.window.rootViewController presentViewController:alert animated:YES completion:nil]; }]; if (@available(iOS 15, *)) { @@ -84,11 +90,6 @@ void qg_VAP_Logger_handler(VAPLogLevel level, const char* file, int line, const // MARK: - Helper Methods -- (void)initUM:(UIApplication *)application - launchOptions:(NSDictionary *)launchOptions { - // MobLink 已移除 -} - - (void)loadMainPage { AccountModel *accountModel = [[AccountInfoStorage instance] getCurrentAccountInfo]; if (accountModel == nil || @@ -103,10 +104,10 @@ void qg_VAP_Logger_handler(VAPLogLevel level, const char* file, int line, const [self checkAndShowSignatureColorGuide]; }); } - - [[ClientConfig shareConfig] clientInit]; } +// 移除 EPConfig 通知相关方法,改用 block 回调 + /// 检查并显示专属颜色引导页 - (void)checkAndShowSignatureColorGuide { UIWindow *keyWindow = kWindow; @@ -114,7 +115,7 @@ void qg_VAP_Logger_handler(VAPLogLevel level, const char* file, int line, const BOOL hasSignatureColor = [EPEmotionColorStorage hasUserSignatureColor]; -#if DEBUG +#if 0 // Debug 环境:总是显示引导页 NSLog(@"[AppDelegate] Debug 模式:显示专属颜色引导页(已有颜色: %@)", hasSignatureColor ? @"YES" : @"NO"); @@ -156,16 +157,9 @@ void qg_VAP_Logger_handler(VAPLogLevel level, const char* file, int line, const [[BaseNavigationController alloc] initWithRootViewController:lvc]; navigationController.modalPresentationStyle = UIModalPresentationFullScreen; self.window.rootViewController = navigationController; - - // 旧代码保留注释(便于回滚) - // LoginViewController *lvc = [[LoginViewController alloc] init]; - // BaseNavigationController * navigationController = [[BaseNavigationController alloc] initWithRootViewController:lvc]; - // navigationController.modalPresentationStyle = UIModalPresentationFullScreen; - // self.window.rootViewController = navigationController; } - (void)toHomeTabbarPage { - // ========== 白牌版本:使用新的 EPTabBarController ========== EPTabBarController *epTabBar = [EPTabBarController create]; [epTabBar refreshTabBarWithIsLogin:YES]; @@ -174,19 +168,10 @@ void qg_VAP_Logger_handler(VAPLogLevel level, const char* file, int line, const window.rootViewController = epTabBar; [window makeKeyAndVisible]; } - - NSLog(@"[AppDelegate] 自动登录后已切换到白牌 TabBar:EPTabBarController"); - - // ========== 原代码(已注释) ========== - /* - TabbarViewController *vc = [[TabbarViewController alloc] init]; - BaseNavigationController *navigationController = [[BaseNavigationController alloc] initWithRootViewController:vc]; - self.window.rootViewController = navigationController; - */ } - (void)applicationDidEnterBackground:(UIApplication *)application { - NSInteger count = [NIMSDK sharedSDK].conversationManager.allUnreadCount; + NSInteger count = [[EPNIMManager sharedManager] allUnreadCount]; [[UIApplication sharedApplication] setApplicationIconBadgeNumber:count]; } @@ -222,8 +207,8 @@ void qg_VAP_Logger_handler(VAPLogLevel level, const char* file, int line, const } - (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { - // 上传devicetoken至云信服务器。 - [[NIMSDK sharedSDK] updateApnsToken:deviceToken ]; + // 上传 deviceToken 至云信服务器(统一走 EPNIMManager) + [[EPNIMManager sharedManager] updateApnsToken:deviceToken]; } - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler{ @@ -255,45 +240,10 @@ void qg_VAP_Logger_handler(VAPLogLevel level, const char* file, int line, const ///URL Scheme跳转 -(BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options{ + // TODO: 在 EPTabbar 中补充 [SocialShareManager sharedManager] setHandleJumpToRoom [[SocialShareManager sharedManager] handleURL:url]; return YES; } -//- (void)__oldApplicationOpenURLMethod:(NSURL *)url { -// NSString *text = [url query]; -// if(text.length){ -// NSMutableDictionary *paramsDict = [NSMutableDictionary dictionary]; -// NSArray *paramArray = [text componentsSeparatedByString:@"&"]; -// for (NSString *param in paramArray) { -// if (param && param.length) { -// NSArray *parArr = [param componentsSeparatedByString:@"="]; -// if (parArr.count == 2) { -// [paramsDict setObject:parArr[1] forKey:parArr[0]]; -// } -// } -// } -// if(paramsDict[@"type"] != nil){ -// NSInteger type = [paramsDict[@"type"] integerValue]; -// if (type == 2) { -// NSString *uid = [NSString stringWithFormat:@"%@",paramsDict[@"uid"]]; -// [[NSNotificationCenter defaultCenter]postNotificationName:kOpenRoomNotification object:nil userInfo:@{@"uid":uid}]; -// ClientConfig *config = [ClientConfig shareConfig]; -// config.roomId = uid; -// }else if(type == 7){ -// NSString *uid = [NSString stringWithFormat:@"%@",paramsDict[@"uid"]]; -// [[NSNotificationCenter defaultCenter]postNotificationName:kOpenRoomNotification object:nil userInfo:@{@"type":@"kOpenChat",@"uid":uid}]; -// ClientConfig *config = [ClientConfig shareConfig]; -// config.chatId = uid; -// }else if (type == 8){ -// NSString *inviteCode = paramsDict[@"inviteCode"]; -// if (inviteCode != nil && [[AccountInfoStorage instance]getUid].length == 0){ -// ClientConfig *config = [ClientConfig shareConfig]; -// config.inviteCode = inviteCode; -// } -// } -//// return YES; -// } -// } -//} @end diff --git a/YuMi/E-P/Common/EPClientAPIBridge.h b/YuMi/E-P/Common/EPClientAPIBridge.h new file mode 100644 index 0000000..bc1f713 --- /dev/null +++ b/YuMi/E-P/Common/EPClientAPIBridge.h @@ -0,0 +1,26 @@ +// +// EPClientAPIBridge.h +// YuMi +// +// Deprecated: replaced by Swift EPConfigAPI +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// Bridge to wrap existing Objective-C APIs for Swift consumers +__attribute__((deprecated("Use EPConfigAPI (Swift) instead"))) +@interface EPClientAPIBridge : NSObject + +/// Call Api.clientInitConfig and forward raw dictionary and status ++ (void)clientInit:(void(^)(NSDictionary * _Nullable data, NSInteger code, NSString * _Nullable msg))completion; + +/// Call ClientConfig.clientConfig; returns code 200 on success (no payload) ++ (void)clientConfig:(void(^)(NSDictionary * _Nullable data, NSInteger code, NSString * _Nullable msg))completion; + +@end + +NS_ASSUME_NONNULL_END + + diff --git a/YuMi/E-P/Common/EPClientAPIBridge.m b/YuMi/E-P/Common/EPClientAPIBridge.m new file mode 100644 index 0000000..ec85252 --- /dev/null +++ b/YuMi/E-P/Common/EPClientAPIBridge.m @@ -0,0 +1,36 @@ +// +// EPClientAPIBridge.m +// YuMi +// +// Objective-C to Swift bridge for client init/config APIs +// + +#import "EPClientAPIBridge.h" +#import "Api+Main.h" +#import "ClientConfig.h" +#import "BaseModel.h" + +@implementation EPClientAPIBridge + ++ (void)clientInit:(void(^)(NSDictionary * _Nullable data, NSInteger code, NSString * _Nullable msg))completion { + if (!completion) { return; } + [Api clientInitConfig:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) { + NSDictionary *payload = nil; + if (code == 200 && data.data && [data.data isKindOfClass:[NSDictionary class]]) { + payload = (NSDictionary *)data.data; + } + completion(payload, code, msg); + }]; +} + ++ (void)clientConfig:(void(^)(NSDictionary * _Nullable data, NSInteger code, NSString * _Nullable msg))completion { + if (!completion) { return; } + // ClientConfig.clientConfig only has a finish block with no parameters; treat success as code 200 + [[ClientConfig shareConfig] clientConfig:^{ + completion(nil, 200, nil); + }]; +} + +@end + + diff --git a/YuMi/E-P/Common/EPConfigAPI.swift b/YuMi/E-P/Common/EPConfigAPI.swift new file mode 100644 index 0000000..f57c196 --- /dev/null +++ b/YuMi/E-P/Common/EPConfigAPI.swift @@ -0,0 +1,37 @@ +// +// EPConfigAPI.swift +// YuMi +// +// Thin Swift wrapper aligning EP module naming, for client/init & client/config +// + +import Foundation + +@objc final class EPConfigAPI: NSObject { + + /// GET client/init — returns payload dictionary when code == 200 + @objc static func clientInit( + completion: @escaping (_ data: [String: Any]?, _ code: Int, _ msg: String?) -> Void + ) { + Api.clientInitConfig { baseModel, code, msg in + var dict: [String: Any]? = nil + if code == 200, let payload = baseModel?.data as? [String: Any] { + dict = payload + } + completion(dict, Int(code), msg) + } + } + + /// GET client/config — treat success as code 200 (no payload) + @objc static func clientConfig( + completion: @escaping (_ data: [String: Any]?, _ code: Int, _ msg: String?) -> Void + ) { + // ClientConfig has + (instancetype)shareConfig; bridged to Swift as .share() + // If the symbol differs, adjust to your Swift name (e.g., shareConfig()). + ClientConfig.share().clientConfig { + completion(nil, 200, nil) + } + } +} + + diff --git a/YuMi/E-P/Common/EPConfigManager.swift b/YuMi/E-P/Common/EPConfigManager.swift new file mode 100644 index 0000000..4ca2e5a --- /dev/null +++ b/YuMi/E-P/Common/EPConfigManager.swift @@ -0,0 +1,133 @@ +// +// EPConfigManager.swift +// YuMi +// +// Cold boot configuration manager for client/init and client/config flows +// + +import Foundation + +@objc final class EPConfigManager: NSObject { + @objc static let shared = EPConfigManager() + + // MARK: - State + @objc private(set) var isInitReady: Bool = false + @objc private(set) var isConfigReady: Bool = false + @objc private(set) var isUsingPersistedInit: Bool = false + + // 原始数据(向后兼容) + @objc private(set) var initModelRaw: [String: Any]? = nil + @objc private(set) var configModelRaw: [String: Any]? = nil + + // 强类型模型 + @objc private(set) var clientDataModel: ClientDataModel? = nil + + private var hasStarted = false + + // 回调闭包 + private var successCallback: (() -> Void)? + private var failureCallback: ((String) -> Void)? + + // MARK: - Public API + @objc(startColdBootWithOnSuccess:onFailure:) + func startColdBoot( + onSuccess: @escaping () -> Void, + onFailure: @escaping (String) -> Void + ) { + guard !hasStarted else { + // 如果已经启动过,根据当前状态直接回调 + if isInitReady && isConfigReady { + onSuccess() + } else if !isInitReady { + onFailure("配置初始化失败") + } + return + } + hasStarted = true + + // 保存回调 + self.successCallback = onSuccess + self.failureCallback = onFailure + + runClientInitWithRetry(maxRetry: 5, interval: 1.0) + } + + // MARK: - Flow + private func runClientInitWithRetry(maxRetry: Int, interval: TimeInterval) { + attemptClientInit(remaining: maxRetry, interval: interval) + } + + private func attemptClientInit(remaining: Int, interval: TimeInterval) { + EPConfigAPI.clientInit { [weak self] data, code, msg in + guard let self = self else { return } + if code == 200, let dict = data { + self.onInitSuccess(dict) + self.runClientConfig() + } else if remaining > 0 { + DispatchQueue.main.asyncAfter(deadline: .now() + interval) { + self.attemptClientInit(remaining: remaining - 1, interval: interval) + } + } else { + self.onInitExhausted() + } + } + } + + private func onInitSuccess(_ dict: [String: Any]) { + // 1. 转换为强类型模型 + let model = ClientDataModel.model(withJSON: dict) + self.clientDataModel = model + + // 2. 更新 ClientConfig(保持现有代码兼容) + ClientConfig.share().configInfo = model + + // 3. 持久化原始字典(避免模型变更时数据失效) + _ = EPConfigStorage.saveInit(dict) + + // 4. 更新状态 + isUsingPersistedInit = false + initModelRaw = dict + isInitReady = true + + // 5. 继续执行 client/config + runClientConfig() + } + + private func onInitExhausted() { + if let persistedDict = EPConfigStorage.loadInit() as? [String: Any] { + // 使用持久化数据 + let model = ClientDataModel.model(withJSON: persistedDict) + self.clientDataModel = model + ClientConfig.share().configInfo = model + + isUsingPersistedInit = true + initModelRaw = persistedDict + isInitReady = true + + // 尝试获取最新配置 + runClientConfig() + } else { + // 无持久化数据,调用失败回调 + failureCallback?("网络异常,请稍后重新启动应用") + } + } + + private func runClientConfig() { + EPConfigAPI.clientConfig { [weak self] data, code, msg in + guard let self = self else { return } + if code == 200 { + // client/config 成功,标记完成并调用成功回调 + self.isConfigReady = true + self.successCallback?() + } else { + // client/config 失败,但 init 已成功,仍可继续 + self.isConfigReady = true + self.successCallback?() + } + } + } +} + +// 移除 Notification 扩展,不再需要 + + diff --git a/YuMi/E-P/Common/EPConfigStorage.h b/YuMi/E-P/Common/EPConfigStorage.h new file mode 100644 index 0000000..d00a343 --- /dev/null +++ b/YuMi/E-P/Common/EPConfigStorage.h @@ -0,0 +1,27 @@ +// +// EPConfigStorage.h +// YuMi +// +// Lightweight persistence for client/init data +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface EPConfigStorage : NSObject + +/// Save init payload dictionary to disk (Application Support) ++ (BOOL)saveInit:(NSDictionary *)dict; + +/// Load init payload dictionary from disk; returns nil if missing/invalid ++ (NSDictionary * _Nullable)loadInit; + +/// Remove persisted init payload ++ (BOOL)clearInit; + +@end + +NS_ASSUME_NONNULL_END + + diff --git a/YuMi/E-P/Common/EPConfigStorage.m b/YuMi/E-P/Common/EPConfigStorage.m new file mode 100644 index 0000000..77b95c1 --- /dev/null +++ b/YuMi/E-P/Common/EPConfigStorage.m @@ -0,0 +1,62 @@ +// +// EPConfigStorage.m +// YuMi +// +// Lightweight persistence for client/init data +// + +#import "EPConfigStorage.h" + +@implementation EPConfigStorage + +#pragma mark - Public + ++ (BOOL)saveInit:(NSDictionary *)dict { + if (![dict isKindOfClass:[NSDictionary class]]) { return NO; } + NSMutableDictionary *wrapped = [dict mutableCopy]; + wrapped[@"_version"] = @1; + wrapped[@"_timestamp"] = @((long long)([[NSDate date] timeIntervalSince1970])); + NSData *data = [NSJSONSerialization dataWithJSONObject:wrapped options:0 error:nil]; + if (!data) { return NO; } + NSString *path = [self initPath]; + NSError *error = nil; + NSFileManager *fm = [NSFileManager defaultManager]; + NSString *dir = [path stringByDeletingLastPathComponent]; + if (![fm fileExistsAtPath:dir]) { + [fm createDirectoryAtPath:dir withIntermediateDirectories:YES attributes:nil error:&error]; + if (error) { return NO; } + } + return [data writeToFile:path atomically:YES]; +} + ++ (NSDictionary * _Nullable)loadInit { + NSString *path = [self initPath]; + NSData *data = [NSData dataWithContentsOfFile:path]; + if (!data) { return nil; } + id obj = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + if (![obj isKindOfClass:[NSDictionary class]]) { return nil; } + return (NSDictionary *)obj; +} + ++ (BOOL)clearInit { + NSString *path = [self initPath]; + NSFileManager *fm = [NSFileManager defaultManager]; + if (![fm fileExistsAtPath:path]) { return YES; } + NSError *error = nil; + [fm removeItemAtPath:path error:&error]; + return (error == nil); +} + +#pragma mark - Helpers + ++ (NSString *)initPath { + NSArray *urls = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask]; + NSURL *dirURL = urls.firstObject; + if (!dirURL) { dirURL = [NSURL fileURLWithPath:[NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) firstObject]]; } + NSURL *fileURL = [dirURL URLByAppendingPathComponent:@"ep_config_init.json"]; + return fileURL.path; +} + +@end + + diff --git a/YuMi/E-P/Common/EPSDKManager+NIM.swift b/YuMi/E-P/Common/EPSDKManager+NIM.swift new file mode 100644 index 0000000..052c347 --- /dev/null +++ b/YuMi/E-P/Common/EPSDKManager+NIM.swift @@ -0,0 +1,27 @@ +// +// EPSDKManager+NIM.swift +// YuMi +// + +import Foundation + +@objc extension EPSDKManager { + /// 初始化 NIMSDK(从 ClientConfig 获取 nimKey) + @objc func initializeNIMSDK(completion: ((NSError?) -> Void)? = nil) { + EPNIMManager.shared().initialize { error in + completion?(error as NSError?) + } + } + + /// 上传 APNS token 到 NIM + @objc func updateNIMApnsToken(_ deviceToken: Data) { + EPNIMManager.shared().updateApnsToken(deviceToken) + } + + /// 获取 NIM 未读数 + @objc func nimUnreadCount() -> Int { + return Int(EPNIMManager.shared().allUnreadCount()) + } +} + + diff --git a/YuMi/E-P/SDKManager/NIMSDK/EPNIMConfig.h b/YuMi/E-P/SDKManager/NIMSDK/EPNIMConfig.h new file mode 100644 index 0000000..57dad32 --- /dev/null +++ b/YuMi/E-P/SDKManager/NIMSDK/EPNIMConfig.h @@ -0,0 +1,29 @@ +// +// EPNIMConfig.h +// YuMi +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// NIMSDK 配置模型(从 ClientConfig 派生) +@interface EPNIMConfig : NSObject + +@property (nonatomic, copy) NSString *appKey; +@property (nonatomic, copy) NSString *apnsCername; +@property (nonatomic, assign) BOOL shouldConsiderRevokedMessageUnreadCount; +@property (nonatomic, assign) BOOL shouldSyncStickTopSessionInfos; +@property (nonatomic, assign) BOOL enabledHttpsForInfo; +@property (nonatomic, assign) BOOL enabledHttpsForMessage; +@property (nonatomic, assign) NSInteger cdnTrackInterval; +@property (nonatomic, assign) NSInteger chatroomMessageReceiveMinInterval; + +/// 从 ClientConfig 创建配置;若缺失 nimKey 则返回 nil ++ (instancetype _Nullable)configFromClientConfig; + +@end + +NS_ASSUME_NONNULL_END + + diff --git a/YuMi/E-P/SDKManager/NIMSDK/EPNIMConfig.m b/YuMi/E-P/SDKManager/NIMSDK/EPNIMConfig.m new file mode 100644 index 0000000..df78a00 --- /dev/null +++ b/YuMi/E-P/SDKManager/NIMSDK/EPNIMConfig.m @@ -0,0 +1,39 @@ +// +// EPNIMConfig.m +// YuMi +// + +#import "EPNIMConfig.h" +#import "ClientConfig.h" +#import "YUMIConstant.h" + +@implementation EPNIMConfig + ++ (instancetype _Nullable)configFromClientConfig { + ClientConfig *client = [ClientConfig shareConfig]; + if (client.configInfo == nil) { + return nil; + } + NSString *nimKey = client.configInfo.nimKey; + if (nimKey.length == 0) { + return nil; + } + EPNIMConfig *cfg = [[EPNIMConfig alloc] init]; + cfg.appKey = nimKey; +#ifdef DEBUG + cfg.apnsCername = @"pikoDevelopPush"; +#else + cfg.apnsCername = @"newPiko"; +#endif + cfg.shouldConsiderRevokedMessageUnreadCount = YES; + cfg.shouldSyncStickTopSessionInfos = YES; + cfg.enabledHttpsForInfo = YES; + cfg.enabledHttpsForMessage = YES; + cfg.cdnTrackInterval = 0; + cfg.chatroomMessageReceiveMinInterval = 50; + return cfg; +} + +@end + + diff --git a/YuMi/E-P/SDKManager/NIMSDK/EPNIMManager.h b/YuMi/E-P/SDKManager/NIMSDK/EPNIMManager.h new file mode 100644 index 0000000..e37fc45 --- /dev/null +++ b/YuMi/E-P/SDKManager/NIMSDK/EPNIMManager.h @@ -0,0 +1,26 @@ +// +// EPNIMManager.h +// YuMi +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface EPNIMManager : NSObject + ++ (instancetype)sharedManager; + +- (void)initializeWithCompletion:(void(^ _Nullable)(NSError * _Nullable error))completion; + +- (void)updateApnsToken:(NSData *)deviceToken; + +- (NSInteger)allUnreadCount; + +- (BOOL)isInitialized; + +@end + +NS_ASSUME_NONNULL_END + + diff --git a/YuMi/E-P/SDKManager/NIMSDK/EPNIMManager.m b/YuMi/E-P/SDKManager/NIMSDK/EPNIMManager.m new file mode 100644 index 0000000..7788d9a --- /dev/null +++ b/YuMi/E-P/SDKManager/NIMSDK/EPNIMManager.m @@ -0,0 +1,61 @@ +// +// EPNIMManager.m +// YuMi +// + +#import "EPNIMManager.h" +#import "EPNIMConfig.h" +#import +#import "CustomAttachmentDecoder.h" + +@interface EPNIMManager () +@property (nonatomic, assign) BOOL initialized; +@end + +@implementation EPNIMManager + ++ (instancetype)sharedManager { + static EPNIMManager *s; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ s = [EPNIMManager new]; }); + return s; +} + +- (void)initializeWithCompletion:(void(^ _Nullable)(NSError * _Nullable error))completion { + if (self.initialized) { + if (completion) completion(nil); + return; + } + EPNIMConfig *cfg = [EPNIMConfig configFromClientConfig]; + if (!cfg) { + if (completion) { + completion([NSError errorWithDomain:@"EPNIM" code:-1001 userInfo:@{NSLocalizedDescriptionKey:@"ClientConfig not ready or nimKey missing"}]); + } + return; + } + NIMSDKOption *option = [NIMSDKOption optionWithAppKey:cfg.appKey]; + option.apnsCername = cfg.apnsCername; + [[NIMSDK sharedSDK] registerWithOption:option]; + [NIMCustomObject registerCustomDecoder:[[CustomAttachmentDecoder alloc] init]]; + [NIMSDKConfig sharedConfig].shouldConsiderRevokedMessageUnreadCount = cfg.shouldConsiderRevokedMessageUnreadCount; + [[NIMSDKConfig sharedConfig] setShouldSyncStickTopSessionInfos:cfg.shouldSyncStickTopSessionInfos]; + [NIMSDKConfig sharedConfig].enabledHttpsForInfo = cfg.enabledHttpsForInfo; + [NIMSDKConfig sharedConfig].enabledHttpsForMessage = cfg.enabledHttpsForMessage; + [NIMSDKConfig sharedConfig].cdnTrackInterval = cfg.cdnTrackInterval; + [NIMSDKConfig sharedConfig].chatroomMessageReceiveMinInterval = cfg.chatroomMessageReceiveMinInterval; + self.initialized = YES; + if (completion) completion(nil); +} + +- (void)updateApnsToken:(NSData *)deviceToken { + if (!deviceToken) return; + [[NIMSDK sharedSDK] updateApnsToken:deviceToken]; +} + +- (NSInteger)allUnreadCount { return [NIMSDK sharedSDK].conversationManager.allUnreadCount; } + +- (BOOL)isInitialized { return self.initialized; } + +@end + + diff --git a/YuMi/Modules/YMTabbar/Api/Api+Main.m b/YuMi/Modules/YMTabbar/Api/Api+Main.m index 5076a52..e962c0f 100644 --- a/YuMi/Modules/YMTabbar/Api/Api+Main.m +++ b/YuMi/Modules/YMTabbar/Api/Api+Main.m @@ -20,8 +20,7 @@ /// 初始化配置 /// @param complection 完成 + (void)clientInitConfig:(HttpRequestHelperCompletion)complection { - NSString * fang = [NSString stringFromBase64String:@"Y2xpZW50L2luaXQ="];///client/init - [HttpRequestHelper request:fang method:HttpRequestHelperMethodGET params:@{} completion:complection]; + [HttpRequestHelper request:@"client/init" method:HttpRequestHelperMethodGET params:@{} completion:complection]; } + (void)clientConfig:(HttpRequestHelperCompletion)completion { diff --git a/YuMi/YuMi-Bridging-Header.h b/YuMi/YuMi-Bridging-Header.h index c1faece..9f733c9 100644 --- a/YuMi/YuMi-Bridging-Header.h +++ b/YuMi/YuMi-Bridging-Header.h @@ -16,7 +16,8 @@ // MARK: - Foundation #import -// MARK: - New Modules (White Label) +// MARK: - New Modules +#import "EPConfigStorage.h" #import "EPMomentViewController.h" #import "EPMineViewController.h" #import "EPMomentCell.h" @@ -78,6 +79,7 @@ // MARK: - Login - Captcha & Config #import "ClientConfig.h" +#import "ClientDataModel.h" #import "TTPopup.h" // 注意: