新增公共房间管理器 PublicRoomManager,负责管理用户进入公共聊天房间的逻辑;在 ClientConfig.m 中添加对公共房间管理器的配置更新通知;在多个文件中集成公共房间管理器,确保用户信息更新和状态管理的正确性;更新相关文档以提供使用指南和集成说明。

This commit is contained in:
edwinQQQ
2025-08-08 17:01:59 +08:00
parent e3dfd8cb0a
commit 1fb6cadabf
24 changed files with 3418 additions and 24 deletions

View File

@@ -19,6 +19,7 @@
"exper",
"Headwear",
"HWDMP",
"ifndef",
"Interitem",
"kindof",
"MAXFLOAT",
@@ -27,6 +28,7 @@
"Nonnull",
"NSEC",
"NSURL",
"objc",
"Offical",
"Procotol",
"QGVAP",

View File

@@ -587,6 +587,7 @@
4CF67BA52DF9568C00EE5A28 /* BaseModelVo.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CF67BA42DF9568C00EE5A28 /* BaseModelVo.m */; };
4CFBE0CA2DAD085700A923AF /* BravoGiftTabInfomationModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CFBE0C92DAD085700A923AF /* BravoGiftTabInfomationModel.m */; };
4CFBE0CD2DAD0FC400A923AF /* PIGiftBravoGiftBroadcastView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CFBE0CC2DAD0FC400A923AF /* PIGiftBravoGiftBroadcastView.m */; };
4CFE7F422E45ECEC00F77776 /* PublicRoomManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CFE7F402E45ECEC00F77776 /* PublicRoomManager.m */; };
4CFFEFCD2D3A4E410035D016 /* AppOfficalManagerActionsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CFFEFCC2D3A4E410035D016 /* AppOfficalManagerActionsViewController.m */; };
4CFFEFD02D3A5E130035D016 /* Api+SuperAdmin.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CFFEFCF2D3A5E130035D016 /* Api+SuperAdmin.m */; };
540EC1D02C89925F00F3BF0D /* GiftComboView.m in Sources */ = {isa = PBXBuildFile; fileRef = 540EC1CF2C89925F00F3BF0D /* GiftComboView.m */; };
@@ -2829,6 +2830,8 @@
4CFBE0C92DAD085700A923AF /* BravoGiftTabInfomationModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BravoGiftTabInfomationModel.m; sourceTree = "<group>"; };
4CFBE0CB2DAD0FC400A923AF /* PIGiftBravoGiftBroadcastView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PIGiftBravoGiftBroadcastView.h; sourceTree = "<group>"; };
4CFBE0CC2DAD0FC400A923AF /* PIGiftBravoGiftBroadcastView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PIGiftBravoGiftBroadcastView.m; sourceTree = "<group>"; };
4CFE7F3F2E45ECEC00F77776 /* PublicRoomManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PublicRoomManager.h; sourceTree = "<group>"; };
4CFE7F402E45ECEC00F77776 /* PublicRoomManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PublicRoomManager.m; sourceTree = "<group>"; };
4CFFEFCB2D3A4E410035D016 /* AppOfficalManagerActionsViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppOfficalManagerActionsViewController.h; sourceTree = "<group>"; };
4CFFEFCC2D3A4E410035D016 /* AppOfficalManagerActionsViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppOfficalManagerActionsViewController.m; sourceTree = "<group>"; };
4CFFEFCE2D3A5E130035D016 /* Api+SuperAdmin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Api+SuperAdmin.h"; sourceTree = "<group>"; };
@@ -6926,6 +6929,15 @@
path = gift;
sourceTree = "<group>";
};
4CFE7F412E45ECEC00F77776 /* Manager */ = {
isa = PBXGroup;
children = (
4CFE7F3F2E45ECEC00F77776 /* PublicRoomManager.h */,
4CFE7F402E45ECEC00F77776 /* PublicRoomManager.m */,
);
path = Manager;
sourceTree = "<group>";
};
54283CE22CE48884009729B5 /* ShoppingMall */ = {
isa = PBXGroup;
children = (
@@ -10754,6 +10766,7 @@
E8AEAED3271412D00017FCE0 /* YMRoom */ = {
isa = PBXGroup;
children = (
4CFE7F412E45ECEC00F77776 /* Manager */,
54E82E9B2CA684A600C931D9 /* Features */,
E804875F2717DD70008595F2 /* Model */,
E80487602717DD89008595F2 /* Api */,
@@ -13054,6 +13067,7 @@
9B85F3532806AB9A006EDF51 /* XPAnchorPKResultView.m in Sources */,
E8DEC99527648FA50078CB70 /* ClientConfig.m in Sources */,
9B6E8577281ABECC0041A321 /* XPRoomInsideRecommendEmptyCell.m in Sources */,
4CFE7F422E45ECEC00F77776 /* PublicRoomManager.m in Sources */,
E85E7BC22A4EE82300B6D00A /* XPMineListCell.m in Sources */,
E880B3A6278BD69900A83B0D /* XPAcrossRoomPKTableViewCell.m in Sources */,
4C1392932D6D963700A6DFB5 /* SubRechargersViewController.m in Sources */,

View File

@@ -17,6 +17,7 @@
#import "XPWeakTimer.h"
#import "Api+Main.h"
#import "ChatFaceVo.h"
#import "PublicRoomManager.h"
@interface ClientConfig ()
/// 10
@@ -105,6 +106,9 @@
self.configInfo = model;
//
[[PublicRoomManager sharedManager] updateConfig];
[[NSNotificationCenter defaultCenter] postNotificationName:@"reloadAfterLoadConfig" object:nil];
[self requestFaceTabNewList];

View File

@@ -74,7 +74,7 @@ typedef NS_ENUM(NSInteger, FaceLivenessStrategy) {
@property (nonatomic, strong) NSArray<NSString *> *officialMsgUids;
///官方账号 小秘书 红包消息
@property (nonatomic,strong) NSArray<NSString *> *officialAccountUids;
//@property(nonatomic,copy) NSDictionary *publicChatRoomIdMap; // 公聊大厅房间 IDs已不使用该业务逻辑
@property(nonatomic,copy) NSDictionary *publicChatRoomIdMap; // 公聊大厅房间 IDs已不使用该业务逻辑 -> 又要使用了
///星座礼物顶部是否开启
@property (nonatomic,assign) BOOL twelveStarSwitch;
/// 开房是否需要实名
@@ -108,6 +108,10 @@ typedef NS_ENUM(NSInteger, FaceLivenessStrategy) {
@property(nonatomic, assign) BOOL captchaSwitch;
@property (nonatomic, copy) NSString *sudId;
@property (nonatomic, copy) NSString *sudKey;
@property (nonatomic, copy) NSString *nimKey;
@end
NS_ASSUME_NONNULL_END

View File

@@ -19,6 +19,7 @@
#import "UserInfoModel.h"
#import "XPLoginAuthCodeVC.h"
#import "FirstRechargeManager.h"
@implementation PILoginManager
+(void)loginWithVC:(MvpViewController *)VC isLoginPhone:(BOOL)isLoginPhone{
[XNDJTDDLoadingTool showLoading];
@@ -70,6 +71,7 @@
return;
}
[XNDJTDDLoadingTool showSuccessWithMessage:YMLocalizedString(@"PKIDLoginViewController0")];
[PILoginManager jumpToHomeVCWithInviteCode:@""];
});

View File

@@ -121,21 +121,22 @@ typedef NS_ENUM(NSUInteger, LoginType) {
}
- (void)setupBottomPolicy {
UIView *view_1 = [[UIView alloc] init];
UIView *view_2 = [[UIView alloc] init];
UIStackView *stackView = [[UIStackView alloc] initWithArrangedSubviews:@[
view_1,
self.agreeButton,
self.policyLabel,
view_2
]];
stackView.spacing = 8;
stackView.alignment = UIStackViewAlignmentCenter;
stackView.distribution = UIStackViewDistributionFill;
[self.view addSubview:stackView];
[stackView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.mas_equalTo(self.view);
if (isMSRTL()) {
make.width.mas_equalTo(290);
}
make.bottom.mas_equalTo(self.view).offset(-44);
make.height.mas_equalTo(34);
make.width.mas_lessThanOrEqualTo(self.view).offset(-32);
}];
[self.agreeButton mas_makeConstraints:^(MASConstraintMaker *make) {
@@ -150,7 +151,7 @@ typedef NS_ENUM(NSUInteger, LoginType) {
[self.view addSubview:smsLogin];
[smsLogin mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.mas_equalTo(self.view);
make.bottom.mas_equalTo(self.view).offset(-104);
make.bottom.mas_equalTo(self.view).offset(-124);
make.size.mas_equalTo(CGSizeMake(48, 48));
}];
}
@@ -357,6 +358,9 @@ typedef NS_ENUM(NSUInteger, LoginType) {
_policyLabel.numberOfLines = 0;
NSMutableAttributedString *attString = [[NSMutableAttributedString alloc] initWithString:YMLocalizedString(@"XPLoginViewController6")];
if ([[UIScreen mainScreen] bounds].size.width < 450 && (isMSPT() || isMSTR())) {
attString = [[NSMutableAttributedString alloc] initWithString:YMLocalizedString(@"XPLoginViewController6.1")];
}
attString.yy_color = UIColorFromRGB(0x7B7B7D);
NSRange userRange = [attString.string rangeOfString:YMLocalizedString(@"XPLoginViewController7")];
[attString addAttributes:@{NSForegroundColorAttributeName:UIColorFromRGB(0x313131)} range:userRange];

View File

@@ -0,0 +1,260 @@
//
// NIMSDKManager.h
// YUMI
//
// Created by YUMI on 2024/12/19.
//
#import <Foundation/Foundation.h>
#import <NIMSDK/NIMSDK.h>
NS_ASSUME_NONNULL_BEGIN
// MARK: - NIMSDK配置模型
@interface NIMSDKConfigModel : NSObject
@property (nonatomic, copy) NSString *appKey; // 云信AppKey
@property (nonatomic, copy) NSString *apnsCername; // APNS证书名称
@property (nonatomic, assign) BOOL shouldConsiderRevokedMessageUnreadCount; // 撤回消息计入未读数
@property (nonatomic, assign) BOOL shouldSyncStickTopSessionInfos; // 同步置顶会话信息
@property (nonatomic, assign) BOOL enabledHttpsForInfo; // 启用HTTPS信息传输
@property (nonatomic, assign) BOOL enabledHttpsForMessage; // 启用HTTPS消息传输
@end
// MARK: - NIMSDK登录状态枚举
typedef NS_ENUM(NSInteger, NIMSDKLoginStatus) {
NIMSDKLoginStatusNotLogin = 0, // 未登录
NIMSDKLoginStatusLogging, // 登录中
NIMSDKLoginStatusLogined, // 已登录
NIMSDKLoginStatusLogout, // 已登出
NIMSDKLoginStatusKickout, // 被踢出
NIMSDKLoginStatusAutoLoginFailed // 自动登录失败
};
// MARK: - NIMSDK登录回调
typedef void(^NIMSDKLoginCompletion)(NSError * _Nullable error);
typedef void(^NIMSDKLogoutCompletion)(NSError * _Nullable error);
typedef void(^NIMSDKStatusChangeBlock)(NIMSDKLoginStatus status);
// MARK: - NIMSDKManager代理协议
@protocol NIMSDKManagerDelegate <NSObject>
@optional
// 登录状态变化
- (void)nimSDKManager:(id)manager didChangeLoginStatus:(NIMSDKLoginStatus)status;
// 自动登录失败
- (void)nimSDKManager:(id)manager didAutoLoginFailed:(NSError *)error;
// 被踢出
- (void)nimSDKManager:(id)manager didKickout:(NIMLoginKickoutResult *)result;
// 收到消息
- (void)nimSDKManager:(id)manager didReceiveMessages:(NSArray<NIMMessage *> *)messages;
// 收到广播消息
- (void)nimSDKManager:(id)manager didReceiveBroadcastMessage:(NIMBroadcastMessage *)broadcastMessage;
@end
// MARK: - NIMSDKManager主类
@interface NIMSDKManager : NSObject
// MARK: - 单例方法
+ (instancetype)sharedManager;
// MARK: - 配置和初始化
/**
配置NIMSDK
@param config 配置模型
*/
- (void)configureWithConfig:(NIMSDKConfigModel *)config;
/**
初始化NIMSDK
@param completion 完成回调
*/
- (void)initializeWithCompletion:(void(^)(NSError * _Nullable error))completion;
/**
注册自定义消息解码器
@param decoder 解码器实例
*/
- (void)registerCustomDecoder:(id<NIMCustomAttachmentCoding>)decoder;
// MARK: - 登录管理
/**
登录
@param account 账号
@param token 登录token
@param completion 完成回调
*/
- (void)loginWithAccount:(NSString *)account
token:(NSString *)token
completion:(NIMSDKLoginCompletion)completion;
/**
自动登录
@param data 自动登录数据
@param completion 完成回调
*/
- (void)autoLoginWithData:(NSDictionary *)data
completion:(NIMSDKLoginCompletion)completion;
/**
登出
@param completion 完成回调
*/
- (void)logoutWithCompletion:(NIMSDKLogoutCompletion)completion;
/**
强制登出
@param completion 完成回调
*/
- (void)forceLogoutWithCompletion:(NIMSDKLogoutCompletion)completion;
// MARK: - 状态查询
/**
获取当前登录状态
@return 登录状态
*/
- (NIMSDKLoginStatus)currentLoginStatus;
/**
是否已登录
@return 是否已登录
*/
- (BOOL)isLogined;
/**
获取当前登录账号
@return 当前登录账号
*/
- (NSString *)currentAccount;
// MARK: - 推送管理
/**
更新APNS设备Token
@param deviceToken 设备Token
*/
- (void)updateApnsToken:(NSData *)deviceToken;
/**
处理推送消息
@param userInfo 推送消息内容
@return 是否处理成功
*/
- (BOOL)handlePushNotification:(NSDictionary *)userInfo;
// MARK: - 代理管理
/**
添加代理
@param delegate 代理对象
*/
- (void)addDelegate:(id<NIMSDKManagerDelegate>)delegate;
/**
移除代理
@param delegate 代理对象
*/
- (void)removeDelegate:(id<NIMSDKManagerDelegate>)delegate;
/**
设置登录状态变化回调
@param block 回调block
*/
- (void)setLoginStatusChangeBlock:(NIMSDKStatusChangeBlock)block;
// MARK: - 消息管理
/**
发送消息
@param message 消息对象
@param session 会话对象
@param completion 完成回调
*/
- (void)sendMessage:(NIMMessage *)message
toSession:(NIMSession *)session
completion:(void(^)(NSError * _Nullable error))completion;
/**
获取未读消息数
@return 未读消息数
*/
- (NSInteger)unreadMessageCount;
/**
获取所有会话
@return 会话列表
*/
- (NSArray<NIMRecentSession *> *)allRecentSessions;
// MARK: - 用户管理
/**
获取用户信息
@param userId 用户ID
@return 用户信息
*/
- (NIMUser *)userInfo:(NSString *)userId;
/**
获取用户信息(异步)
@param userIds 用户ID数组
@param completion 完成回调
*/
- (void)fetchUserInfos:(NSArray<NSString *> *)userIds
completion:(void(^)(NSArray<NIMUser *> * _Nullable users, NSError * _Nullable error))completion;
// MARK: - 聊天室管理
/**
进入聊天室
@param roomId 房间ID
@param completion 完成回调
*/
- (void)enterChatroom:(NSString *)roomId
completion:(void(^)(NSError * _Nullable error))completion;
/**
退出聊天室
@param roomId 房间ID
@param completion 完成回调
*/
- (void)exitChatroom:(NSString *)roomId
completion:(void(^)(NSError * _Nullable error))completion;
// MARK: - 工具方法
/**
创建自定义消息
@param attachment 自定义附件
@return 消息对象
*/
- (NIMMessage *)createCustomMessageWithAttachment:(id<NIMCustomAttachment>)attachment;
/**
创建文本消息
@param text 文本内容
@return 消息对象
*/
- (NIMMessage *)createTextMessage:(NSString *)text;
/**
创建图片消息
@param image 图片对象
@return 消息对象
*/
- (NIMMessage *)createImageMessage:(UIImage *)image;
/**
创建音频消息
@param filePath 音频文件路径
@param duration 音频时长
@return 消息对象
*/
- (NIMMessage *)createAudioMessage:(NSString *)filePath duration:(NSTimeInterval)duration;
// MARK: - 清理方法
/**
清理资源
*/
- (void)cleanup;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,406 @@
//
// NIMSDKManager.m
// YUMI
//
// Created by YUMI on 2024/12/19.
//
#import "NIMSDKManager.h"
#import "CustomAttachmentDecoder.h"
#import "YUMIConstant.h"
@interface NIMSDKManager () <NIMLoginManagerDelegate, NIMChatManagerDelegate, NIMSystemNotificationManagerDelegate, NIMBroadcastManagerDelegate>
@property (nonatomic, strong) NIMSDKConfigModel *config;
@property (nonatomic, assign) NIMSDKLoginStatus loginStatus;
@property (nonatomic, strong) NSMutableArray<id<NIMSDKManagerDelegate>> *delegates;
@property (nonatomic, copy) NIMSDKStatusChangeBlock statusChangeBlock;
@property (nonatomic, assign) BOOL isInitialized;
@end
@implementation NIMSDKManager
#pragma mark -
+ (instancetype)sharedManager {
static NIMSDKManager *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[NIMSDKManager alloc] init];
});
return instance;
}
- (instancetype)init {
self = [super init];
if (self) {
_delegates = [NSMutableArray array];
_loginStatus = NIMSDKLoginStatusNotLogin;
_isInitialized = NO;
}
return self;
}
#pragma mark -
- (void)configureWithConfig:(NIMSDKConfigModel *)config {
self.config = config;
}
- (void)initializeWithCompletion:(void(^)(NSError * _Nullable error))completion {
if (self.isInitialized) {
if (completion) {
completion(nil);
}
return;
}
//
if (!self.config || !self.config.appKey) {
NSError *error = [NSError errorWithDomain:@"NIMSDKManager"
code:-1
userInfo:@{NSLocalizedDescriptionKey: @"NIMSDK配置不完整"}];
if (completion) {
completion(error);
}
return;
}
// SDK
NIMSDKOption *option = [NIMSDKOption optionWithAppKey:self.config.appKey];
if (self.config.apnsCername) {
option.apnsCername = self.config.apnsCername;
}
// SDK
[[NIMSDK sharedSDK] registerWithOption:option];
//
[self registerCustomDecoder:[[CustomAttachmentDecoder alloc] init]];
// SDK
[NIMSDKConfig sharedConfig].shouldConsiderRevokedMessageUnreadCount = self.config.shouldConsiderRevokedMessageUnreadCount;
[[NIMSDKConfig sharedConfig] setShouldSyncStickTopSessionInfos:self.config.shouldSyncStickTopSessionInfos];
[NIMSDKConfig sharedConfig].enabledHttpsForInfo = self.config.enabledHttpsForInfo;
[NIMSDKConfig sharedConfig].enabledHttpsForMessage = self.config.enabledHttpsForMessage;
//
[[NIMSDK sharedSDK].loginManager addDelegate:self];
[[NIMSDK sharedSDK].chatManager addDelegate:self];
[[NIMSDK sharedSDK].systemNotificationManager addDelegate:self];
[[NIMSDK sharedSDK].broadcastManager addDelegate:self];
self.isInitialized = YES;
if (completion) {
completion(nil);
}
}
- (void)registerCustomDecoder:(id<NIMCustomAttachmentCoding>)decoder {
[NIMCustomObject registerCustomDecoder:decoder];
}
#pragma mark -
- (void)loginWithAccount:(NSString *)account
token:(NSString *)token
completion:(NIMSDKLoginCompletion)completion {
if (!self.isInitialized) {
NSError *error = [NSError errorWithDomain:@"NIMSDKManager"
code:-2
userInfo:@{NSLocalizedDescriptionKey: @"NIMSDK未初始化"}];
if (completion) {
completion(error);
}
return;
}
self.loginStatus = NIMSDKLoginStatusLogging;
[self notifyStatusChange];
[[NIMSDK sharedSDK].loginManager login:account
token:token
completion:^(NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
self.loginStatus = NIMSDKLoginStatusNotLogin;
} else {
self.loginStatus = NIMSDKLoginStatusLogined;
}
[self notifyStatusChange];
if (completion) {
completion(error);
}
});
}];
}
- (void)autoLoginWithData:(NSDictionary *)data
completion:(NIMSDKLoginCompletion)completion {
if (!self.isInitialized) {
NSError *error = [NSError errorWithDomain:@"NIMSDKManager"
code:-2
userInfo:@{NSLocalizedDescriptionKey: @"NIMSDK未初始化"}];
if (completion) {
completion(error);
}
return;
}
self.loginStatus = NIMSDKLoginStatusLogging;
[self notifyStatusChange];
[[NIMSDK sharedSDK].loginManager autoLogin:data];
//
//
if (completion) {
completion(nil);
}
}
- (void)logoutWithCompletion:(NIMSDKLogoutCompletion)completion {
[[NIMSDK sharedSDK].loginManager logout:^(NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (!error) {
self.loginStatus = NIMSDKLoginStatusLogout;
[self notifyStatusChange];
}
if (completion) {
completion(error);
}
});
}];
}
- (void)forceLogoutWithCompletion:(NIMSDKLogoutCompletion)completion {
//
[[NIMSDK sharedSDK].loginManager logout:^(NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
self.loginStatus = NIMSDKLoginStatusLogout;
[self notifyStatusChange];
if (completion) {
completion(error);
}
});
}];
}
#pragma mark -
- (NIMSDKLoginStatus)currentLoginStatus {
return self.loginStatus;
}
- (BOOL)isLogined {
return [[NIMSDK sharedSDK].loginManager isLogined];
}
- (NSString *)currentAccount {
return [[NIMSDK sharedSDK].loginManager currentAccount];
}
#pragma mark -
- (void)updateApnsToken:(NSData *)deviceToken {
[[NIMSDK sharedSDK] updateApnsToken:deviceToken];
}
- (BOOL)handlePushNotification:(NSDictionary *)userInfo {
//
//
return YES;
}
#pragma mark -
- (void)addDelegate:(id<NIMSDKManagerDelegate>)delegate {
if (delegate && ![self.delegates containsObject:delegate]) {
[self.delegates addObject:delegate];
}
}
- (void)removeDelegate:(id<NIMSDKManagerDelegate>)delegate {
[self.delegates removeObject:delegate];
}
- (void)setLoginStatusChangeBlock:(NIMSDKStatusChangeBlock)block {
self.statusChangeBlock = block;
}
#pragma mark -
- (void)sendMessage:(NIMMessage *)message
toSession:(NIMSession *)session
completion:(void(^)(NSError * _Nullable error))completion {
[[NIMSDK sharedSDK].chatManager sendMessage:message
toSession:session
error:nil];
//
if (completion) {
completion(nil);
}
}
- (NSInteger)unreadMessageCount {
return [[NIMSDK sharedSDK].conversationManager allUnreadCount];
}
- (NSArray<NIMRecentSession *> *)allRecentSessions {
return [[NIMSDK sharedSDK].conversationManager allRecentSessions];
}
#pragma mark -
- (NIMUser *)userInfo:(NSString *)userId {
return [[NIMSDK sharedSDK].userManager userInfo:userId];
}
- (void)fetchUserInfos:(NSArray<NSString *> *)userIds
completion:(void(^)(NSArray<NIMUser *> * _Nullable users, NSError * _Nullable error))completion {
[[NIMSDK sharedSDK].userManager fetchUserInfos:userIds
completion:completion];
}
#pragma mark -
- (void)enterChatroom:(NSString *)roomId
completion:(void(^)(NSError * _Nullable error))completion {
[[NIMSDK sharedSDK].chatroomManager enterChatroom:roomId
completion:completion];
}
- (void)exitChatroom:(NSString *)roomId
completion:(void(^)(NSError * _Nullable error))completion {
[[NIMSDK sharedSDK].chatroomManager exitChatroom:roomId
completion:completion];
}
#pragma mark -
- (NIMMessage *)createCustomMessageWithAttachment:(id<NIMCustomAttachment>)attachment {
NIMCustomObject *customObject = [[NIMCustomObject alloc] init];
customObject.attachment = attachment;
NIMMessage *message = [[NIMMessage alloc] init];
message.messageObject = customObject;
return message;
}
- (NIMMessage *)createTextMessage:(NSString *)text {
return [[NIMMessage alloc] initWithText:text];
}
- (NIMMessage *)createImageMessage:(UIImage *)image {
NIMImageObject *imageObject = [[NIMImageObject alloc] initWithImage:image];
NIMMessage *message = [[NIMMessage alloc] init];
message.messageObject = imageObject;
return message;
}
- (NIMMessage *)createAudioMessage:(NSString *)filePath duration:(NSTimeInterval)duration {
NIMAudioObject *audioObject = [[NIMAudioObject alloc] initWithSourcePath:filePath];
audioObject.duration = duration;
NIMMessage *message = [[NIMMessage alloc] init];
message.messageObject = audioObject;
return message;
}
#pragma mark -
- (void)cleanup {
//
[[NIMSDK sharedSDK].loginManager removeDelegate:self];
[[NIMSDK sharedSDK].chatManager removeDelegate:self];
[[NIMSDK sharedSDK].systemNotificationManager removeDelegate:self];
[[NIMSDK sharedSDK].broadcastManager removeDelegate:self];
//
self.loginStatus = NIMSDKLoginStatusNotLogin;
self.isInitialized = NO;
[self.delegates removeAllObjects];
self.statusChangeBlock = nil;
}
#pragma mark -
- (void)notifyStatusChange {
//
for (id<NIMSDKManagerDelegate> delegate in self.delegates) {
if ([delegate respondsToSelector:@selector(nimSDKManager:didChangeLoginStatus:)]) {
[delegate nimSDKManager:self didChangeLoginStatus:self.loginStatus];
}
}
// block
if (self.statusChangeBlock) {
self.statusChangeBlock(self.loginStatus);
}
}
#pragma mark - NIMLoginManagerDelegate
- (void)onAutoLoginFailed:(NSError *)error {
dispatch_async(dispatch_get_main_queue(), ^{
self.loginStatus = NIMSDKLoginStatusAutoLoginFailed;
[self notifyStatusChange];
//
for (id<NIMSDKManagerDelegate> delegate in self.delegates) {
if ([delegate respondsToSelector:@selector(nimSDKManager:didAutoLoginFailed:)]) {
[delegate nimSDKManager:self didAutoLoginFailed:error];
}
}
});
}
- (void)onKickout:(NIMLoginKickoutResult *)result {
dispatch_async(dispatch_get_main_queue(), ^{
self.loginStatus = NIMSDKLoginStatusKickout;
[self notifyStatusChange];
//
for (id<NIMSDKManagerDelegate> delegate in self.delegates) {
if ([delegate respondsToSelector:@selector(nimSDKManager:didKickout:)]) {
[delegate nimSDKManager:self didKickout:result];
}
}
});
}
#pragma mark - NIMChatManagerDelegate
- (void)onRecvMessages:(NSArray<NIMMessage *> *)messages {
dispatch_async(dispatch_get_main_queue(), ^{
//
for (id<NIMSDKManagerDelegate> delegate in self.delegates) {
if ([delegate respondsToSelector:@selector(nimSDKManager:didReceiveMessages:)]) {
[delegate nimSDKManager:self didReceiveMessages:messages];
}
}
});
}
#pragma mark - NIMBroadcastManagerDelegate
- (void)onReceiveBroadcastMessage:(NIMBroadcastMessage *)broadcastMessage {
dispatch_async(dispatch_get_main_queue(), ^{
//
for (id<NIMSDKManagerDelegate> delegate in self.delegates) {
if ([delegate respondsToSelector:@selector(nimSDKManager:didReceiveBroadcastMessage:)]) {
[delegate nimSDKManager:self didReceiveBroadcastMessage:broadcastMessage];
}
}
});
}
@end

View File

@@ -42,7 +42,7 @@
}
- (void)setupUI {
_backgroundImage = [[UIImageView alloc] init];
_backgroundImage = [[UIImageView alloc] initWithImage:kImage(@"cp_bg")];
self.backgroundImage.contentMode = UIViewContentModeScaleAspectFit;
[self addSubview:self.backgroundImage];
[self.backgroundImage mas_makeConstraints:^(MASConstraintMaker *make) {

View File

@@ -0,0 +1,73 @@
//
// PublicRoomManager.h
// YUMI
//
// Created by YUMI on 2024/12/19.
//
#import <Foundation/Foundation.h>
#import <NIMSDK/NIMSDK.h>
@class UserInfoModel;
NS_ASSUME_NONNULL_BEGIN
/**
* 公共聊天房间管理器
* 负责管理用户进入公共聊天房间的逻辑
*/
@interface PublicRoomManager : NSObject
#pragma mark - 单例方法
+ (instancetype)sharedManager;
#pragma mark - 生命周期管理
/**
* 初始化公共房间管理器
* 在用户登录成功后调用
*/
- (void)initialize;
/**
* 重置公共房间管理器
* 在用户登出时调用,清理所有状态
*/
- (void)reset;
#pragma mark - 状态查询
/**
* 是否已初始化
*/
- (BOOL)isInitialized;
/**
* 是否已进入公共房间
*/
- (BOOL)isInPublicRoom;
/**
* 获取当前公共房间ID
*/
- (NSString *)currentPublicRoomId;
// 更新来自 config 的数据
- (void)updateConfig;
- (void)updateUserInfo:(UserInfoModel *)userInfo;
#pragma mark - 手动控制
/**
* 手动进入公共房间
* @param completion 完成回调
*/
- (void)enterPublicRoomWithCompletion:(void(^)(NSError * _Nullable error))completion;
/**
* 手动退出公共房间
* @param completion 完成回调
*/
- (void)exitPublicRoomWithCompletion:(void(^)(NSError * _Nullable error))completion;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,346 @@
//
// PublicRoomManager.m
// YUMI
//
// Created by YUMI on 2024/12/19.
//
#import "PublicRoomManager.h"
#import "UserInfoModel.h"
#import "ClientConfig.h"
#import "AccountInfoStorage.h"
#import "XPMessageRemoteExtModel.h"
@interface PublicRoomManager () <NIMChatManagerDelegate>
@property (nonatomic, assign) BOOL isInitialized;
@property (nonatomic, assign) BOOL isInPublicRoom;
@property (nonatomic, copy) NSString *currentPublicRoomId;
@property (nonatomic, copy) NSString *currentUserId;
@property (nonatomic, strong) UserInfoModel *userInfo;
@end
@implementation PublicRoomManager
#pragma mark -
+ (instancetype)sharedManager {
static dispatch_once_t onceToken;
static PublicRoomManager *instance;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
[instance initialize];
});
return instance;
}
- (instancetype)init {
self = [super init];
if (self) {
_isInitialized = NO;
_isInPublicRoom = NO;
_currentPublicRoomId = nil;
_currentUserId = nil;
_userInfo = nil;
}
return self;
}
#pragma mark -
- (void)initialize {
//
if (self.isInitialized) {
NSLog(@"PublicRoomManager: 已经初始化,跳过重复初始化");
return;
}
//
// NSString *uid = [AccountInfoStorage instance].getUid;
// if (uid.length == 0) {
// NSLog(@"PublicRoomManager: 用户未登录,无法初始化");
// return;
// }
//
// UserInfoModel *userInfo = [AccountInfoStorage instance].getHomeUserInfo;
// if (!userInfo || !userInfo.partitionId) {
// NSLog(@"PublicRoomManager: 用户信息不完整,等待用户信息更新");
// return;
// }
//
// ClientDataModel *configInfo = [ClientConfig shareConfig].configInfo;
// if (!configInfo || !configInfo.publicChatRoomIdMap) {
// NSLog(@"PublicRoomManager: 配置信息未加载,等待配置更新");
// return;
// }
//
// self.userInfo = userInfo;
// self.currentUserId = uid;
//
[[NIMSDK sharedSDK].chatManager addDelegate:self];
//
self.isInitialized = YES;
// NSLog(@"PublicRoomManager: 初始化成功用户ID: %@, 分区ID: %@", uid, userInfo.partitionId);
//
// [self tryEnterPublicRoom];
}
- (void)reset {
NSLog(@"PublicRoomManager: 开始重置");
// 退
if (self.isInPublicRoom && self.currentPublicRoomId) {
[self exitPublicRoomWithCompletion:^(NSError * _Nullable error) {
if (error) {
NSLog(@"PublicRoomManager: 退出公共房间失败: %@", error);
} else {
NSLog(@"PublicRoomManager: 退出公共房间成功");
}
}];
}
//
[[NIMSDK sharedSDK].chatManager removeDelegate:self];
//
self.isInitialized = NO;
self.isInPublicRoom = NO;
self.currentPublicRoomId = nil;
self.currentUserId = nil;
self.userInfo = nil;
NSLog(@"PublicRoomManager: 重置完成");
}
#pragma mark -
- (BOOL)isInitialized {
return _isInitialized;
}
- (BOOL)isInPublicRoom {
return _isInPublicRoom;
}
- (NSString *)currentPublicRoomId {
return _currentPublicRoomId;
}
#pragma mark -
- (void)tryEnterPublicRoom {
if (!self.isInitialized || !self.userInfo) {
NSLog(@"PublicRoomManager: 未初始化或用户信息缺失,无法进入公共房间");
return;
}
// ID
NSString *partitionId = self.userInfo.partitionId;
ClientDataModel *configInfo = [ClientConfig shareConfig].configInfo;
NSDictionary *publicChatRoomIdMap = configInfo.publicChatRoomIdMap;
if (!publicChatRoomIdMap || !partitionId) {
NSLog(@"PublicRoomManager: 公共房间配置或分区ID缺失");
return;
}
NSNumber *roomId = publicChatRoomIdMap[partitionId];
if (!roomId) {
NSLog(@"PublicRoomManager: 未找到分区 %@ 对应的公共房间ID", partitionId);
return;
}
NSLog(@"PublicRoomManager: 尝试进入公共房间分区ID: %@, 房间ID: %@", partitionId, roomId);
//
[self enterPublicRoomWithRoomId:roomId.stringValue completion:^(NSError * _Nullable error) {
if (error) {
NSLog(@"PublicRoomManager: 进入公共房间失败: %@", error);
} else {
NSLog(@"PublicRoomManager: 进入公共房间成功");
}
}];
}
- (void)enterPublicRoomWithRoomId:(NSString *)roomId completion:(void(^)(NSError * _Nullable error))completion {
if (!self.userInfo) {
NSError *error = [NSError errorWithDomain:@"PublicRoomManager"
code:-1
userInfo:@{NSLocalizedDescriptionKey: @"用户信息缺失"}];
if (completion) {
completion(error);
}
return;
}
//
NIMChatroomEnterRequest *request = [[NIMChatroomEnterRequest alloc] init];
request.roomId = roomId;
//
XPMessageRemoteExtModel *extModel = [[XPMessageRemoteExtModel alloc] init];
extModel.defUser = self.userInfo.defUser;
extModel.erbanNo = self.userInfo.erbanNo;
extModel.carName = self.userInfo.carName;
extModel.inRoomNameplatePic = self.userInfo.nameplatePic;
extModel.inRoomNameplateWord = self.userInfo.nameplateWord;
extModel.isCustomWord = self.userInfo.isCustomWord;
extModel.charmUrl = self.userInfo.userLevelVo.charmUrl;
extModel.experLevelSeq = self.userInfo.userLevelVo.experLevelSeq;
extModel.experUrl = self.userInfo.userLevelVo.experUrl;
extModel.newUser = self.userInfo.newUser;
extModel.vipIcon = self.userInfo.userVipInfoVO.nameplateUrl;
extModel.iosBubbleUrl = self.userInfo.iosBubbleUrl;
extModel.androidBubbleUrl = self.userInfo.androidBubbleUrl;
extModel.enterHide = self.userInfo.userVipInfoVO.enterHide;
extModel.preventKick = self.userInfo.userVipInfoVO.preventKick;
extModel.enterRoomEffects = self.userInfo.userVipInfoVO.enterRoomEffects;
extModel.gender = self.userInfo.gender;
extModel.fromSayHelloChannel = self.userInfo.fromSayHelloChannel;
extModel.platformRole = self.userInfo.platformRole;
extModel.nick = self.userInfo.nick;
NSMutableDictionary *ext = [NSMutableDictionary dictionaryWithObject:extModel.model2dictionary
forKey:[NSString stringWithFormat:@"%ld", self.userInfo.uid]];
request.roomExt = [ext toJSONString];
//
@kWeakify(self);
[[NIMSDK sharedSDK].chatroomManager enterChatroom:request completion:^(NSError * _Nullable error, NIMChatroom * _Nullable chatroom, NIMChatroomMember * _Nullable me) {
@kStrongify(self);
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
NSLog(@"PublicRoomManager: 进入公共房间失败: %@", error);
if (completion) {
completion(error);
}
} else {
NSLog(@"PublicRoomManager: 进入公共房间成功房间ID: %@", roomId);
self.isInPublicRoom = YES;
self.currentPublicRoomId = roomId;
if (completion) {
completion(nil);
}
}
});
}];
}
#pragma mark -
- (void)enterPublicRoomWithCompletion:(void(^)(NSError * _Nullable error))completion {
if (!self.isInitialized) {
NSError *error = [NSError errorWithDomain:@"PublicRoomManager"
code:-2
userInfo:@{NSLocalizedDescriptionKey: @"管理器未初始化"}];
if (completion) {
completion(error);
}
return;
}
if (self.isInPublicRoom) {
NSLog(@"PublicRoomManager: 已在公共房间中");
if (completion) {
completion(nil);
}
return;
}
[self tryEnterPublicRoom];
}
- (void)exitPublicRoomWithCompletion:(void(^)(NSError * _Nullable error))completion {
if (!self.isInPublicRoom || !self.currentPublicRoomId) {
NSLog(@"PublicRoomManager: 未在公共房间中");
if (completion) {
completion(nil);
}
return;
}
@kWeakify(self);
[[NIMSDK sharedSDK].chatroomManager exitChatroom:self.currentPublicRoomId completion:^(NSError * _Nullable error) {
@kStrongify(self);
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
NSLog(@"PublicRoomManager: 退出公共房间失败: %@", error);
} else {
NSLog(@"PublicRoomManager: 退出公共房间成功");
self.isInPublicRoom = NO;
self.currentPublicRoomId = nil;
}
if (completion) {
completion(error);
}
});
}];
}
#pragma mark -
- (void)updateUserInfo:(UserInfoModel *)userInfo {
if (!userInfo || !userInfo.partitionId) {
NSLog(@"PublicRoomManager: 用户信息更新失败,信息不完整");
return;
}
//
if (self.currentUserId && ![self.currentUserId isEqualToString:[NSString stringWithFormat:@"%ld", userInfo.uid]]) {
NSLog(@"PublicRoomManager: 检测到用户切换,重置管理器");
[self reset];
}
self.userInfo = userInfo;
self.currentUserId = [NSString stringWithFormat:@"%ld", userInfo.uid];
//
if (self.isInitialized && !self.isInPublicRoom) {
[self tryEnterPublicRoom];
}
}
#pragma mark -
- (void)updateConfig {
if (!self.isInitialized) {
NSLog(@"PublicRoomManager: 未初始化,跳过配置更新");
return;
}
ClientDataModel *configInfo = [ClientConfig shareConfig].configInfo;
if (!configInfo || !configInfo.publicChatRoomIdMap) {
NSLog(@"PublicRoomManager: 配置信息不完整,跳过配置更新");
return;
}
//
if (!self.isInPublicRoom) {
[self tryEnterPublicRoom];
}
}
#pragma mark - NIMChatManagerDelegate
- (void)onRecvMessages:(NSArray<NIMMessage *> *)messages {
//
for (NIMMessage *message in messages) {
if (message.session.sessionType == NIMSessionTypeChatroom) {
NSString *sessionId = message.session.sessionId;
if ([sessionId isEqualToString:self.currentPublicRoomId]) {
NSLog(@"PublicRoomManager: 收到公共房间消息: %@", message.text);
//
}
}
}
}
@end

View File

@@ -95,6 +95,7 @@
#import "RoomResourceManager.h"
#import "SocialShareManager.h"
#import "PublicRoomManager.h"
NSString * const kUserFirstLoginKey = @"kUserFirstLoginKey";
NSString * const kHadLaunchApp = @"kHadLaunchApp";
@@ -389,6 +390,10 @@ UIKIT_EXTERN NSString *kTabShowAnchorCardKey;
[[XPSkillCardPlayerManager shareInstance] setUserInfoModel:userInfo];
[[XPSkillCardPlayerManager shareInstance] requestBravoGiftTabInfomation];
[[RoomBoomManager sharedManager] saveUserInfo:userInfo];
//
[[PublicRoomManager sharedManager] updateUserInfo:userInfo];
[self getRoomGameInfo];
[AccountInfoStorage instance].name = self.userInfo.nick;

View File

@@ -95,7 +95,7 @@
editParam = [MSParamsDecode msDecodeParams:editParam];
params = [self configBaseParmars:editParam];
#if 0
#if DEBUG
// URL
NSString *baseUrl = [HttpRequestHelper getHostUrl];
NSString *fullUrl = [NSString stringWithFormat:@"%@/%@", baseUrl, method];
@@ -119,7 +119,7 @@
@kWeakify(self);
[manager GET:method parameters:params headers:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
BaseModel *baseModel = [BaseModel modelWithDictionary:responseObject];
#if 0
#if DEBUG
NSLog(@"%@ - \n%@\n", method, [baseModel toJSONString]);
#else
#endif
@@ -246,7 +246,7 @@
params = [self configBaseParmars:params];
#if 0
#if DEBUG
// URL
NSString *baseUrl = [HttpRequestHelper getHostUrl];
NSString *fullUrl = [NSString stringWithFormat:@"%@/%@", baseUrl, method];
@@ -261,7 +261,7 @@
[manager POST:method parameters:params headers:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
BaseModel *baseModel = [BaseModel modelWithDictionary:responseObject];
#if 0
#if DEBUG
NSLog(@"\n%@", [baseModel toJSONString]);
#else
#endif
@@ -287,7 +287,7 @@
params = [MSParamsDecode msDecodeParams:[params mutableCopy] ];
params = [self configBaseParmars:params];
#if 0
#if DEBUG
NSLog(@"\nmethod:\n%@\nparameter:\n%@\n 超時:%@",
method,
params,
@@ -310,13 +310,13 @@ constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) {
progress:nil
success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
BaseModel *baseModel = [BaseModel modelWithDictionary:responseObject];
#if 0
#if DEBUG
NSLog(@"%@ - \n%@\n", method, [baseModel toJSONString]);
#else
#endif
success(baseModel);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
#if 0
#if DEBUG
NSLog(@"%@ - \n%@\n", method, error);
#else
#endif
@@ -339,7 +339,7 @@ constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) {
params = [MSParamsDecode msDecodeParams:[params mutableCopy] ];
params = [self configBaseParmars:params];
AFHTTPSessionManager *manager = [HttpRequestHelper requestManager];
#if 0
#if DEBUG
// URL
NSString *baseUrl = [HttpRequestHelper getHostUrl];
NSString *fullUrl = [NSString stringWithFormat:@"%@/%@", baseUrl, method];
@@ -364,7 +364,7 @@ constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) {
@kWeakify(self);
[manager DELETE:method parameters:params headers:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
BaseModel *baseModel = [BaseModel modelWithDictionary:responseObject];
#if 0
#if DEBUG
NSLog(@"\n%@\n", [baseModel toJSONString]);
#else
#endif
@@ -502,7 +502,7 @@ constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) {
failure(error.code, error.localizedDescription.length > 0 ? error.localizedDescription : YMLocalizedString(@"HttpRequestHelper4"));
}
}
#if 0
#if DEBUG
NSLog(@"\n%@", error);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSDictionary *allHeaders = response.allHeaderFields;
@@ -537,7 +537,7 @@ constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) {
AFHTTPSessionManager *manager = [HttpRequestHelper requestManager];
NSString *url = [self getHostUrl];
NSString *urlPath = [NSString stringWithFormat:@"%@/%@", url ,path];
#if 0
#if DEBUG
NSLog(@"\nmethod:\n%@\nparameter:\n%@", path, params);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[BSNetListenModel addHttpReq:urlPath header:manager.requestSerializer.HTTPRequestHeaders param:[params copy] time:[NSDate getCurrentTimeWithFormat:@"yyyy-MM-dd HH:mm:ss"]];
@@ -582,12 +582,12 @@ constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) {
if (responseObject) {
BaseModel *baseModel = [BaseModel modelWithDictionary:responseObject];
#if 0
#if DEBUG
NSLog(@"\n%@", [baseModel toJSONString]);
#else
#endif
if (baseModel.code == 200) {
#if 0
#if DEBUG
NSHTTPURLResponse *urlresponse = (NSHTTPURLResponse *)response;
NSDictionary *allHeaders = urlresponse.allHeaderFields;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

View File

@@ -172,6 +172,7 @@ NS_ASSUME_NONNULL_BEGIN
///声音卡
@property (nonatomic,strong) XPSoundCardModel *audioCard;
///用户所在区
@property(nonatomic,copy) NSString *partitionId;
///
///

View File

@@ -12,6 +12,7 @@
#import "LoginViewController.h"
#import "BaseNavigationController.h"
#import "FirstRechargeManager.h"
#import "PublicRoomManager.h"
@interface BaseMvpPresenter()
@@ -33,13 +34,16 @@
// 1.
[[FirstRechargeManager sharedManager] stopMonitoring];
// 2. logout
// 2.
[[PublicRoomManager sharedManager] reset];
// 3. logout
[[AccountInfoStorage instance] saveAccountInfo:nil];
[[AccountInfoStorage instance] saveTicket:nil];
if ([NIMSDK sharedSDK].loginManager.isLogined) {
[[NIMSDK sharedSDK].loginManager logout:nil];
}
// 3.
// 4.
[self tokenInvalid];
// ///
// [[ClientConfig shareConfig] resetHeartBratTimer];

View File

@@ -1892,6 +1892,7 @@
"XPLoginViewController4" = "Login bem-sucedido";
"XPLoginViewController5" = "Login com Número de Telefone";
"XPLoginViewController6" = "Concorde com o Contrato de Serviço do Usuário e Política de Privacidade";
"XPLoginViewController6.1" = "Concorde com o Contrato de Serviço do Usuário\n e Política de Privacidade";
"XPLoginViewController7" = "Contrato de Serviço do Usuário";
"XPLoginViewController8" = " e ";
"XPLoginViewController9" = "Política de Privacidade";

View File

@@ -2131,7 +2131,8 @@
"XPLoginViewController3" = "《Kullanıcı Sözleşmesi》'ni kabul ediyorum ve %@'in bu cihazın numarasını almasına izin veriyorum";
"XPLoginViewController4" = "Giriş Başarılı";
"XPLoginViewController5" = "Telefon Numarası ile Giriş";
"XPLoginViewController6" = "《Kullanıcı Sözleşmesi》 ve 《Gizlilik Politikası》'nı kabul ediyorum";
"XPLoginViewController6" = "《Kullanıcı Sözleşmesi》 ve《Gizlilik Politikası》'nı kabul ediyorum";
"XPLoginViewController6.1" = "《Kullanıcı Sözleşmesi》 ve \n《Gizlilik Politikası》'nı kabul ediyorum";
"XPLoginViewController7" = "《Kullanıcı Sözleşmesi》";
"XPLoginViewController8" = "ve";
"XPLoginViewController9" = "《Gizlilik Politikası》";

View File

@@ -0,0 +1,422 @@
# AttachmentModel 功能分析报告
## 目录
- [1. 概述](#1-概述)
- [2. AttachmentModel 核心结构](#2-attachmentmodel-核心结构)
- [3. 消息类型分类](#3-消息类型分类)
- [4. 使用场景分析](#4-使用场景分析)
- [5. 功能模块分布](#5-功能模块分布)
- [6. 关键实现细节](#6-关键实现细节)
- [7. 最佳实践](#7-最佳实践)
- [8. 总结](#8-总结)
## 1. 概述
### 1.1 定义
`AttachmentModel` 是YuMi项目中用于处理NIMSDK自定义消息的核心数据模型它实现了`NIMCustomAttachment`协议用于在云信SDK中传输和处理各种自定义消息类型。
### 1.2 核心作用
- **消息类型标识**: 通过`first``second`字段标识不同的消息类型和子类型
- **数据承载**: 通过`data`字段承载具体的消息内容
- **消息解析**: 配合`CustomAttachmentDecoder`进行消息的编码和解码
- **业务扩展**: 支持各种业务场景的自定义消息处理
### 1.3 设计特点
- **类型安全**: 使用枚举定义所有消息类型,避免硬编码
- **扩展性强**: 支持新增消息类型而不影响现有代码
- **统一接口**: 所有自定义消息都通过统一的接口处理
- **数据灵活**: `data`字段支持任意类型的数据结构
## 2. AttachmentModel 核心结构
### 2.1 基础属性
```objc
@interface AttachmentModel : PIBaseModel<NIMCustomAttachment>
@property (nonatomic, strong) id data; // 消息数据内容
@property (nonatomic, assign) int first; // 消息类型标识
@property (nonatomic, assign) int second; // 消息子类型标识
@property (nonatomic, assign) BOOL isBroadcast; // 是否为广播消息
@property (nonatomic, assign) NSInteger seq; // 本地序号,用于消息排序
@end
```
### 2.2 编码实现
```objc
- (NSString *)encodeAttachment {
return [self toJSONString];
}
```
### 2.3 解码实现
```objc
// CustomAttachmentDecoder.m
- (id<NIMCustomAttachment>)decodeAttachment:(NSString *)content {
id<NIMCustomAttachment> attachment;
NSData *data = [content dataUsingEncoding:NSUTF8StringEncoding];
if (data) {
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data
options:0
error:nil];
if ([dict isKindOfClass:[NSDictionary class]]) {
int first = [dict[@"first"] intValue];
int second = [dict[@"second"] intValue];
id originalData = dict[@"data"];
AttachmentModel *model = [[AttachmentModel alloc] init];
model.first = (short)first;
model.second = (short)second;
model.data = originalData;
attachment = model;
}
}
return attachment;
}
```
## 3. 消息类型分类
### 3.1 主要消息类型 (first字段)
#### 3.1.1 基础功能类
| 类型值 | 类型名称 | 功能描述 |
|--------|----------|----------|
| 2 | `CustomMessageType_Room_Tip` | 房间提示消息 |
| 3 | `CustomMessageType_Gift` | 礼物相关消息 |
| 5 | `CustomMessageType_Account` | 账户更新消息 |
| 6 | `CustomMessageType_Member_Online` | 关注主播上线通知 |
| 8 | `CustomMessageType_Queue` | 队列操作消息 |
| 9 | `CustomMessageType_Face` | 表情消息 |
| 10 | `CustomMessageType_Tweet` | 推文消息 |
| 12 | `CustomMessageType_AllMicroSend` | 全麦送礼物 |
#### 3.1.2 房间管理类
| 类型值 | 类型名称 | 功能描述 |
|--------|----------|----------|
| 15 | `CustomMessageType_Car_Notify` | 座驾相关通知 |
| 18 | `CustomMessageType_Kick_User` | 踢出房间消息 |
| 20 | `CustomMessageType_Update_RoomInfo` | 房间信息更新 |
| 30 | `CustomMessageType_Arrange_Mic` | 排麦相关消息 |
| 31 | `CustomMessageType_Room_PK` | 房间内PK消息 |
| 42 | `CustomMessageType_Room_GiftValue` | 房间礼物值同步 |
#### 3.1.3 社交功能类
| 类型值 | 类型名称 | 功能描述 |
|--------|----------|----------|
| 19 | `CustomMessageType_Secretary` | 小秘书消息 |
| 22 | `CustomMessageType_Application_Share` | 应用内分享 |
| 52 | `CustomMessageType_Monents` | 动态相关消息 |
| 60 | `CustomMessageType_RedPacket` | 红包相关消息 |
| 62 | `CustomMessageType_FindNew` | 发现萌新消息 |
| 64 | `CustomMessageType_CP` | CP礼物消息 |
#### 3.1.4 游戏娱乐类
| 类型值 | 类型名称 | 功能描述 |
|--------|----------|----------|
| 26 | `CustomMessageType_Candy_Tree` | 糖果树消息 |
| 63 | `CustomMessageType_RoomBoom` | 房间火箭消息 |
| 71 | `CustomMessageType_Tarot` | 塔罗牌消息 |
| 72 | `CustomMessageType_RoomPlay_Dating` | 相亲游戏消息 |
| 81 | `CustomMessageType_Room_Sailing` | 航海游戏消息 |
| 83 | `CustomMessageType_Across_Room_PK` | 跨房PK消息 |
| 97 | `CustomMessageType_Treasure_Fairy` | 精灵密藏消息 |
#### 3.1.5 系统通知类
| 类型值 | 类型名称 | 功能描述 |
|--------|----------|----------|
| 23 | `CustomMessageType_Message_Handle` | 系统通知消息 |
| 24 | `CustomMessageType_User_UpGrade` | 用户升级消息 |
| 49 | `CustomMessageType_Version_Update` | 版本升级消息 |
| 75 | `CustomMessageType_Chat_Risk_Alert` | 私聊风险提醒 |
| 76 | `CustomMessageType_First_Recharge_Reward` | 首充奖励消息 |
| 78 | `CustomMessageType_First_VisitorRecord` | 访客记录消息 |
| 92 | `CustomMessageType_Task_Complete` | 任务完成通知 |
### 3.2 子类型示例 (second字段)
#### 3.2.1 礼物消息子类型
```objc
typedef NS_ENUM(NSUInteger, CustomMessageSubGift) {
Custom_Message_Sub_Gift_Send = 31, // 发送礼物
Custom_Message_Sub_Gift_ChannelNotify = 32, // 全服发送礼物
Custom_Message_Sub_Gift_LuckySend = 34, // 发送福袋礼物
Custom_Message_Sub_Gift_EmbeddedStyle = 35, // 发送嵌入式礼物
};
```
#### 3.2.2 红包消息子类型
```objc
typedef NS_ENUM(NSUInteger, CustomMessageSubRedPacket) {
Custom_Message_Sub_RoomGiftRedPacket = 601, // 房间礼物红包
Custom_Message_Sub_RoomDiamandRedPacket = 602, // 房间钻石红包
Custom_Message_Sub_AllGiftRedPacket = 603, // 全服礼物红包
Custom_Message_Sub_AllDiamandRedPacket = 604, // 全服钻石红包
Custom_Message_Sub_OpenRedPacketSuccess = 605, // 抢红包成功
Custom_Message_Sub_NewRoomDiamandRedPacket = 606, // 新版本房间钻石红包
Custom_Message_Sub_LuckyPackage = 607, // 最新版本房间红包推送
};
```
#### 3.2.3 房间PK子类型
```objc
typedef NS_ENUM(NSUInteger, CustomMessageSubRoomPK) {
Custom_Message_Sub_Room_PK_Non_Empty = 311, // 从无人报名pk排麦到有人报名pk排麦
Custom_Message_Sub_Room_PK_Empty = 312, // 从有人报名pk排麦到无人报名pk排麦
Custom_Message_Sub_Room_PK_Mode_Open = 313, // 创建了pk模式
Custom_Message_Sub_Room_PK_Mode_Close = 314, // 关闭pk模式
Custom_Message_Sub_Room_PK_Start = 315, // pk开始
Custom_Message_Sub_Room_PK_Result = 316, // pk结果
Custom_Message_Sub_Room_PK_Re_Start = 317, // 重新开始
Custom_Message_Sub_Room_PK_Manager_Up_Mic = 318, // 管理员邀请上麦
};
```
## 4. 使用场景分析
### 4.1 消息接收处理
#### 4.1.1 私聊消息处理
```objc
// TabbarViewController.m
- (void)onRecvMessages:(NSArray<NIMMessage *> *)messages {
for (NIMMessage *message in messages) {
if (message.session.sessionType == NIMSessionTypeP2P) {
if (message.messageType == NIMMessageTypeCustom) {
NIMCustomObject *obj = (NIMCustomObject *)message.messageObject;
if (obj.attachment != nil && [obj.attachment isKindOfClass:[AttachmentModel class]]) {
AttachmentModel *attachment = (AttachmentModel *)obj.attachment;
// 处理发现萌新打招呼消息
if (attachment.first == CustomMessageType_FindNew &&
attachment.second == Custom_Message_Find_New_Greet_New_User) {
FindNewGreetMessageModel *greetInfo = [FindNewGreetMessageModel modelWithDictionary:attachment.data];
// 显示打招呼弹窗
[self showGreetAlert:greetInfo];
}
}
}
}
}
}
```
#### 4.1.2 广播消息处理
```objc
// TabbarViewController.m
- (void)onReceiveBroadcastMessage:(NIMBroadcastMessage *)broadcastMessage {
if (broadcastMessage.content) {
NSDictionary *msgDictionary = [broadcastMessage.content toJSONObject];
AttachmentModel *attachment = [AttachmentModel modelWithJSON:msgDictionary[@"body"]];
// 处理红包消息
if (attachment.first == CustomMessageType_RedPacket) {
[self receiveRedPacketDealWithData:attachment];
}
// 处理版本更新消息
else if (attachment.first == CustomMessageType_Version_Update &&
attachment.second == Custom_Message_Version_Update_Value) {
[self handleVersionUpdate:attachment];
}
}
}
```
### 4.2 消息发送
#### 4.2.1 创建自定义消息
```objc
// 创建礼物消息
AttachmentModel *attachment = [[AttachmentModel alloc] init];
attachment.first = CustomMessageType_Gift;
attachment.second = Custom_Message_Sub_Gift_Send;
attachment.data = @{
@"giftId": @"123",
@"giftName": @"玫瑰花",
@"giftCount": @1,
@"senderId": @"user123",
@"receiverId": @"user456"
};
// 创建NIM消息
NIMCustomObject *customObject = [[NIMCustomObject alloc] init];
customObject.attachment = attachment;
NIMMessage *message = [[NIMMessage alloc] init];
message.messageObject = customObject;
```
#### 4.2.2 发送消息
```objc
// 发送到指定会话
NIMSession *session = [NIMSession session:@"receiverId" type:NIMSessionTypeP2P];
[[NIMSDK sharedSDK].chatManager sendMessage:message toSession:session error:nil];
```
### 4.3 消息内容显示
#### 4.3.1 消息内容解析
```objc
// NIMMessageUtils.m
+ (NSString *)messageContent:(NIMMessage*)message {
switch (message.messageType) {
case NIMMessageTypeCustom: {
NIMCustomObject *obj = (NIMCustomObject *)message.messageObject;
AttachmentModel *attachment = (AttachmentModel *)obj.attachment;
if (attachment.first == CustomMessageType_Secretary) {
if (attachment.second == Custom_Message_Sub_Secretary_Router) {
return attachment.data[@"title"];
}
} else if (attachment.first == CustomMessageType_Gift) {
if (attachment.second == Custom_Message_Sub_Gift_Send) {
return YMLocalizedString(@"NIMMessageUtils5"); // "发送了礼物"
}
} else if (attachment.first == CustomMessageType_FindNew &&
attachment.second == Custom_Message_Find_New_Greet_New_User) {
NSString *text = attachment.data[@"message"];
return text.length > 0 ? text : YMLocalizedString(@"NIMMessageUtils11");
}
// ... 其他消息类型处理
}
break;
}
return @"";
}
```
## 5. 功能模块分布
### 5.1 消息模块 (YMMessage)
- **文件**: `AttachmentModel.h/m`, `CustomAttachmentDecoder.h/m`
- **功能**: 消息模型定义、解码器实现
- **使用**: 所有自定义消息的基础结构
### 5.2 主界面模块 (YMTabbar)
- **文件**: `TabbarViewController.m`
- **功能**: 全局消息接收处理、广播消息处理
- **使用**: 处理发现萌新、红包、版本更新等全局消息
### 5.3 房间模块 (YMRoom)
- **文件**: 多个房间相关文件
- **功能**: 房间内消息处理、PK、礼物、火箭等
- **使用**: 处理房间内的各种互动消息
### 5.4 动态模块 (YMMonents)
- **文件**: `XPMomentsViewController.m`
- **功能**: 动态相关消息处理
- **使用**: 处理动态分享、审核等消息
### 5.5 个人中心模块 (YMMine)
- **文件**: 多个个人中心相关文件
- **功能**: 个人相关消息处理
- **使用**: 处理VIP、粉丝团、任务等个人消息
## 6. 关键实现细节
### 6.1 消息类型判断
```objc
// 判断是否为特定类型的消息
if (attachment.first == CustomMessageType_Gift &&
attachment.second == Custom_Message_Sub_Gift_Send) {
// 处理发送礼物消息
}
// 判断是否为系统消息
if (attachment.first == CustomMessageType_System_message) {
// 处理系统消息
}
```
### 6.2 数据解析
```objc
// 从data字段解析具体数据
NSDictionary *giftData = attachment.data;
NSString *giftId = giftData[@"giftId"];
NSString *giftName = giftData[@"giftName"];
NSNumber *giftCount = giftData[@"giftCount"];
// 使用MJExtension进行模型转换
GiftModel *giftModel = [GiftModel modelWithDictionary:attachment.data];
```
### 6.3 消息过滤
```objc
// 根据分区ID过滤消息
NSString *partitionId = [NSString stringWithFormat:@"%@", attachment.data[@"partitionId"]];
if (![partitionId isEqualToString:self.userInfo.partitionId]) {
return; // 不是当前分区的消息,忽略
}
```
### 6.4 消息排序
```objc
// 使用seq字段进行消息排序
@property (nonatomic, assign) NSInteger seq; // 本地序号,用于将一条消息分解为多条有次序的消息
```
## 7. 最佳实践
### 7.1 消息类型定义
1. **使用枚举**: 避免硬编码数字,提高代码可读性
2. **分类管理**: 按功能模块分类管理消息类型
3. **版本兼容**: 新增消息类型时保持向后兼容
### 7.2 消息处理
1. **类型检查**: 在处理消息前先检查类型
2. **数据验证**: 验证data字段的数据完整性
3. **错误处理**: 对解析失败的消息进行适当处理
### 7.3 性能优化
1. **消息过滤**: 根据业务需求过滤不需要的消息
2. **内存管理**: 及时释放不需要的消息对象
3. **批量处理**: 对大量消息进行批量处理
### 7.4 扩展性设计
1. **模块化**: 按功能模块组织消息处理逻辑
2. **插件化**: 支持新增消息类型而不影响现有代码
3. **配置化**: 通过配置文件管理消息类型
## 8. 总结
### 8.1 核心价值
`AttachmentModel` 作为YuMi项目的消息处理核心具有以下价值
1. **统一接口**: 为所有自定义消息提供统一的处理接口
2. **类型安全**: 通过枚举定义确保消息类型的类型安全
3. **扩展性强**: 支持灵活扩展新的消息类型
4. **功能完整**: 覆盖了社交、游戏、系统等各个业务场景
### 8.2 技术特点
1. **协议实现**: 实现了NIMSDK的`NIMCustomAttachment`协议
2. **JSON序列化**: 支持JSON格式的消息编码和解码
3. **模型转换**: 支持与业务模型的相互转换
4. **类型枚举**: 使用枚举定义所有消息类型和子类型
### 8.3 业务覆盖
`AttachmentModel` 覆盖了YuMi项目的所有主要业务场景
- **社交功能**: 私聊、动态、关注等
- **房间功能**: PK、礼物、火箭、红包等
- **游戏功能**: 塔罗、航海、相亲等
- **系统功能**: 升级、任务、通知等
- **商业功能**: VIP、粉丝团、充值等
### 8.4 架构优势
1. **解耦合**: 消息处理逻辑与业务逻辑分离
2. **可维护**: 清晰的消息类型定义和处理流程
3. **可测试**: 每个消息类型都可以独立测试
4. **可扩展**: 新增功能时只需添加新的消息类型
`AttachmentModel` 是YuMi项目即时通讯功能的重要基础设施为项目的各种业务场景提供了强大而灵活的消息处理能力。
---
**文档版本**: 1.0
**最后更新**: 2024年12月
**维护人员**: 开发团队

View File

@@ -0,0 +1,63 @@
# 图片上传接口Swift 封装,腾讯云 COS
## 1. 参数模型
```swift
struct ImageUploadRequest {
let image: UIImage
let fileName: String // 如 "image/xxxx.jpg"
}
```
## 2. 接口调用方法
```swift
class ImageUploader {
static func uploadImage(
request: ImageUploadRequest,
completion: @escaping (Result<String, Error>) -> Void
) {
// 1. 压缩图片,生成 NSData
// 2. 调用腾讯云 COS SDK 上传
// 3. 返回图片 URL
}
}
```
## 3. 示例用法
```swift
let image = UIImage(named: "test.jpg")!
let request = ImageUploadRequest(image: image, fileName: "image/\(UUID().uuidString).jpg")
ImageUploader.uploadImage(request: request) { result in
switch result {
case .success(let url):
print("上传成功,图片地址:\(url)")
case .failure(let error):
print("上传失败: \(error.localizedDescription)")
}
}
```
## 4. 第三方依赖
- [QCloudCOSXML](https://github.com/tencentyun/qcloud-sdk-ios)(腾讯云 COS 官方 SDK
- [TZImagePickerController](https://github.com/banchichen/TZImagePickerController)(图片选择/裁剪,非必须)
## 5. 配置说明
- COS 配置信息appId、region、bucket、签名等需通过接口动态获取并初始化 SDK。
- 上传时需指定 bucket、object文件名、bodyNSData
- 支持自定义域名、加速域名等高级配置。
## 6. 错误处理建议
- 网络异常、图片压缩失败、SDK 上传失败等需统一处理。
- 建议统一封装 `UploadError` 类型。
## 7. 扩展建议
- 支持多图批量上传
- 支持上传进度回调
- 支持 async/await
- 可结合项目网络层统一封装

View File

@@ -0,0 +1,69 @@
# MomentsPublish 动态发布接口Swift 封装)
## 1. 参数模型
```swift
struct MomentsPublishRequest {
let uid: String
let type: String
let worldId: String?
let content: String
let resList: [String]?
}
```
## 2. 接口调用方法
```swift
class MomentsAPI {
static func publishMoment(
request: MomentsPublishRequest,
completion: @escaping (Result<Void, Error>) -> Void
) {
// 1. 构造参数字典
// 2. 发起POST请求到 dynamic/square/publish
// 3. 处理返回结果
}
}
```
## 3. 示例用法
```swift
let request = MomentsPublishRequest(
uid: "12345",
type: "1", // 0:文本 1:图片
worldId: "67890",
content: "今天很开心!",
resList: ["image_url_1", "image_url_2"]
)
MomentsAPI.publishMoment(request: request) { result in
switch result {
case .success:
print("发布成功")
case .failure(let error):
print("发布失败: \(error.localizedDescription)")
}
}
```
## 4. 参数说明
| 参数名 | 类型 | 必填 | 说明 |
|-----------|--------------|------|----------------|
| uid | String | 是 | 用户ID |
| type | String | 是 | 动态类型0文本/1图片|
| worldId | String? | 否 | 话题ID |
| content | String | 是 | 动态内容 |
| resList | [String]? | 否 | 图片资源URL数组 |
## 5. 错误处理建议
- 网络异常、参数校验、后端返回错误码均需处理
- 建议统一封装 `APIError` 类型
## 6. 扩展建议
- 支持 async/await
- 可扩展为支持更多动态类型
- 可结合项目网络层统一封装

View File

@@ -0,0 +1,618 @@
# NIMSDKManager 使用指南
## 目录
- [1. 概述](#1-概述)
- [2. Objective-C 使用示例](#2-objective-c-使用示例)
- [3. Swift 桥接使用示例](#3-swift-桥接使用示例)
- [4. 配置说明](#4-配置说明)
- [5. 最佳实践](#5-最佳实践)
- [6. 常见问题](#6-常见问题)
## 1. 概述
`NIMSDKManager` 是一个专门用于管理NIMSDK事务的统一管理类提供了配置、初始化、登录/登出等完整的功能封装。该类采用单例模式设计支持Objective-C和Swift项目使用。
### 1.1 主要功能
- **配置管理**: 统一的NIMSDK配置管理
- **初始化**: 简化的SDK初始化流程
- **登录管理**: 登录、自动登录、登出等功能
- **状态监控**: 实时监控登录状态变化
- **消息处理**: 消息发送、接收、广播等
- **推送管理**: APNS推送相关功能
- **代理管理**: 支持多代理监听
### 1.2 设计特点
- **单例模式**: 全局统一管理
- **代理模式**: 支持多代理监听
- **Block回调**: 支持异步操作回调
- **Swift兼容**: 完美支持Swift项目桥接
- **错误处理**: 完善的错误处理机制
## 2. Objective-C 使用示例
### 2.1 基本配置和初始化
```objc
// 1. 创建配置模型
NIMSDKConfigModel *config = [[NIMSDKConfigModel alloc] init];
config.appKey = KeyWithType(KeyType_NetEase);
config.apnsCername = @"pikoDevelopPush"; // DEBUG环境
config.shouldConsiderRevokedMessageUnreadCount = YES;
config.shouldSyncStickTopSessionInfos = YES;
config.enabledHttpsForInfo = NO; // DEBUG环境
config.enabledHttpsForMessage = NO; // DEBUG环境
// 2. 配置并初始化
[[NIMSDKManager sharedManager] configureWithConfig:config];
[[NIMSDKManager sharedManager] initializeWithCompletion:^(NSError * _Nullable error) {
if (error) {
NSLog(@"NIMSDK初始化失败: %@", error);
} else {
NSLog(@"NIMSDK初始化成功");
}
}];
```
### 2.2 登录管理
```objc
// 1. 设置登录状态变化监听
[[NIMSDKManager sharedManager] setLoginStatusChangeBlock:^(NIMSDKLoginStatus status) {
switch (status) {
case NIMSDKLoginStatusNotLogin:
NSLog(@"未登录");
break;
case NIMSDKLoginStatusLogging:
NSLog(@"登录中");
break;
case NIMSDKLoginStatusLogined:
NSLog(@"已登录");
break;
case NIMSDKLoginStatusLogout:
NSLog(@"已登出");
break;
case NIMSDKLoginStatusKickout:
NSLog(@"被踢出");
break;
case NIMSDKLoginStatusAutoLoginFailed:
NSLog(@"自动登录失败");
break;
}
}];
// 2. 执行登录
[[NIMSDKManager sharedManager] loginWithAccount:@"user123"
token:@"token123"
completion:^(NSError * _Nullable error) {
if (error) {
NSLog(@"登录失败: %@", error);
} else {
NSLog(@"登录成功");
}
}];
// 3. 自动登录
NSDictionary *autoLoginData = @{@"account": @"user123", @"token": @"token123"};
[[NIMSDKManager sharedManager] autoLoginWithData:autoLoginData
completion:^(NSError * _Nullable error) {
if (error) {
NSLog(@"自动登录失败: %@", error);
} else {
NSLog(@"自动登录成功");
}
}];
// 4. 登出
[[NIMSDKManager sharedManager] logoutWithCompletion:^(NSError * _Nullable error) {
if (error) {
NSLog(@"登出失败: %@", error);
} else {
NSLog(@"登出成功");
}
}];
```
### 2.3 代理监听
```objc
@interface MyViewController () <NIMSDKManagerDelegate>
@end
@implementation MyViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 添加代理
[[NIMSDKManager sharedManager] addDelegate:self];
}
- (void)dealloc {
// 移除代理
[[NIMSDKManager sharedManager] removeDelegate:self];
}
#pragma mark - NIMSDKManagerDelegate
- (void)nimSDKManager:(id)manager didChangeLoginStatus:(NIMSDKLoginStatus)status {
NSLog(@"登录状态变化: %ld", (long)status);
}
- (void)nimSDKManager:(id)manager didAutoLoginFailed:(NSError *)error {
NSLog(@"自动登录失败: %@", error);
}
- (void)nimSDKManager:(id)manager didKickout:(NIMLoginKickoutResult *)result {
NSLog(@"被踢出: %@", result);
}
- (void)nimSDKManager:(id)manager didReceiveMessages:(NSArray<NIMMessage *> *)messages {
NSLog(@"收到消息: %@", messages);
}
- (void)nimSDKManager:(id)manager didReceiveBroadcastMessage:(NIMBroadcastMessage *)broadcastMessage {
NSLog(@"收到广播消息: %@", broadcastMessage);
}
@end
```
### 2.4 消息管理
```objc
// 1. 创建消息
NIMMessage *textMessage = [[NIMSDKManager sharedManager] createTextMessage:@"Hello World"];
// 2. 创建自定义消息
AttachmentModel *attachment = [[AttachmentModel alloc] init];
attachment.first = CustomMessageType_Gift;
attachment.second = Custom_Message_Sub_Gift_Send;
attachment.data = @{@"giftId": @"123", @"giftName": @"玫瑰花"};
NIMMessage *customMessage = [[NIMSDKManager sharedManager] createCustomMessageWithAttachment:attachment];
// 3. 发送消息
NIMSession *session = [NIMSession session:@"receiverId" type:NIMSessionTypeP2P];
[[NIMSDKManager sharedManager] sendMessage:textMessage
toSession:session
completion:^(NSError * _Nullable error) {
if (error) {
NSLog(@"发送失败: %@", error);
} else {
NSLog(@"发送成功");
}
}];
// 4. 获取未读消息数
NSInteger unreadCount = [[NIMSDKManager sharedManager] unreadMessageCount];
NSLog(@"未读消息数: %ld", (long)unreadCount);
// 5. 获取所有会话
NSArray<NIMRecentSession *> *sessions = [[NIMSDKManager sharedManager] allRecentSessions];
NSLog(@"会话数量: %lu", (unsigned long)sessions.count);
```
### 2.5 推送管理
```objc
// 1. 更新APNS设备Token
- (void)application:(UIApplication *)app
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
[[NIMSDKManager sharedManager] updateApnsToken:deviceToken];
}
// 2. 处理推送消息
- (void)application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
BOOL handled = [[NIMSDKManager sharedManager] handlePushNotification:userInfo];
if (handled) {
completionHandler(UIBackgroundFetchResultNewData);
} else {
completionHandler(UIBackgroundFetchResultNoData);
}
}
```
## 3. Swift 桥接使用示例
### 3.1 创建桥接头文件
在Swift项目中创建桥接头文件 `YourProject-Bridging-Header.h`
```objc
//
// YourProject-Bridging-Header.h
// YourProject
//
#ifndef YourProject_Bridging_Header_h
#define YourProject_Bridging_Header_h
#import "NIMSDKManager.h"
#import "AttachmentModel.h"
#import "CustomAttachmentDecoder.h"
#endif /* YourProject_Bridging_Header_h */
```
### 3.2 Swift 使用示例
```swift
import Foundation
import UIKit
class NIMSDKService {
static let shared = NIMSDKService()
private let nimManager = NIMSDKManager.shared()
private init() {
setupNIMSDK()
}
// MARK: - 配置和初始化
private func setupNIMSDK() {
// 创建配置
let config = NIMSDKConfigModel()
config.appKey = KeyWithType(KeyType_NetEase)
config.apnsCername = "pikoDevelopPush" // DEBUG环境
config.shouldConsiderRevokedMessageUnreadCount = true
config.shouldSyncStickTopSessionInfos = true
config.enabledHttpsForInfo = false // DEBUG环境
config.enabledHttpsForMessage = false // DEBUG环境
// 配置并初始化
nimManager.configure(with: config)
nimManager.initialize { [weak self] error in
if let error = error {
print("NIMSDK初始化失败: \(error)")
} else {
print("NIMSDK初始化成功")
self?.setupDelegates()
}
}
}
private func setupDelegates() {
// 设置登录状态变化监听
nimManager.setLoginStatusChangeBlock { [weak self] status in
self?.handleLoginStatusChange(status)
}
}
// MARK: - 登录管理
func login(account: String, token: String, completion: @escaping (Error?) -> Void) {
nimManager.login(withAccount: account, token: token) { error in
DispatchQueue.main.async {
completion(error)
}
}
}
func autoLogin(data: [String: Any], completion: @escaping (Error?) -> Void) {
nimManager.autoLogin(with: data) { error in
DispatchQueue.main.async {
completion(error)
}
}
}
func logout(completion: @escaping (Error?) -> Void) {
nimManager.logout { error in
DispatchQueue.main.async {
completion(error)
}
}
}
// MARK: - 状态查询
var isLogined: Bool {
return nimManager.isLogined()
}
var currentAccount: String? {
return nimManager.currentAccount()
}
var loginStatus: NIMSDKLoginStatus {
return nimManager.currentLoginStatus()
}
// MARK: - 消息管理
func sendTextMessage(_ text: String, to sessionId: String, completion: @escaping (Error?) -> Void) {
let message = nimManager.createTextMessage(text)
let session = NIMSession(session: sessionId, type: .p2P)
nimManager.send(message, to: session) { error in
DispatchQueue.main.async {
completion(error)
}
}
}
func sendCustomMessage(attachment: NIMCustomAttachment, to sessionId: String, completion: @escaping (Error?) -> Void) {
let message = nimManager.createCustomMessage(with: attachment)
let session = NIMSession(session: sessionId, type: .p2P)
nimManager.send(message, to: session) { error in
DispatchQueue.main.async {
completion(error)
}
}
}
var unreadMessageCount: Int {
return Int(nimManager.unreadMessageCount())
}
var allRecentSessions: [NIMRecentSession] {
return nimManager.allRecentSessions() ?? []
}
// MARK: - 推送管理
func updateApnsToken(_ deviceToken: Data) {
nimManager.updateApnsToken(deviceToken)
}
func handlePushNotification(_ userInfo: [AnyHashable: Any]) -> Bool {
return nimManager.handlePushNotification(userInfo)
}
// MARK: - 私有方法
private func handleLoginStatusChange(_ status: NIMSDKLoginStatus) {
switch status {
case .notLogin:
print("未登录")
case .logging:
print("登录中")
case .logined:
print("已登录")
case .logout:
print("已登出")
case .kickout:
print("被踢出")
case .autoLoginFailed:
print("自动登录失败")
@unknown default:
print("未知状态")
}
}
}
// MARK: - Swift 扩展
extension NIMSDKService {
// 创建礼物消息的便捷方法
func createGiftMessage(giftId: String, giftName: String, giftCount: Int) -> NIMMessage? {
let attachment = AttachmentModel()
attachment.first = Int32(CustomMessageType_Gift)
attachment.second = Int32(Custom_Message_Sub_Gift_Send)
attachment.data = [
"giftId": giftId,
"giftName": giftName,
"giftCount": giftCount
]
return nimManager.createCustomMessage(with: attachment)
}
// 发送礼物消息的便捷方法
func sendGift(giftId: String, giftName: String, giftCount: Int, to sessionId: String, completion: @escaping (Error?) -> Void) {
guard let message = createGiftMessage(giftId: giftId, giftName: giftName, giftCount: giftCount) else {
completion(NSError(domain: "NIMSDKService", code: -1, userInfo: [NSLocalizedDescriptionKey: "创建礼物消息失败"]))
return
}
let session = NIMSession(session: sessionId, type: .p2P)
nimManager.send(message, to: session) { error in
DispatchQueue.main.async {
completion(error)
}
}
}
}
```
### 3.3 Swift 视图控制器使用示例
```swift
import UIKit
class ChatViewController: UIViewController {
private let nimService = NIMSDKService.shared
override func viewDidLoad() {
super.viewDidLoad()
setupNIMSDK()
}
private func setupNIMSDK() {
// 检查登录状态
if !nimService.isLogined {
// 执行登录
nimService.login(account: "user123", token: "token123") { [weak self] error in
if let error = error {
print("登录失败: \(error)")
} else {
print("登录成功")
self?.startChat()
}
}
} else {
startChat()
}
}
private func startChat() {
// 开始聊天功能
print("开始聊天")
}
// MARK: - 发送消息示例
@IBAction func sendTextMessage(_ sender: UIButton) {
nimService.sendTextMessage("Hello from Swift!", to: "receiver123") { error in
if let error = error {
print("发送失败: \(error)")
} else {
print("发送成功")
}
}
}
@IBAction func sendGiftMessage(_ sender: UIButton) {
nimService.sendGift(giftId: "123", giftName: "玫瑰花", giftCount: 1, to: "receiver123") { error in
if let error = error {
print("发送礼物失败: \(error)")
} else {
print("发送礼物成功")
}
}
}
// MARK: - 获取消息信息
func updateUnreadCount() {
let count = nimService.unreadMessageCount
print("未读消息数: \(count)")
}
func loadRecentSessions() {
let sessions = nimService.allRecentSessions
print("会话数量: \(sessions.count)")
}
}
```
### 3.4 Swift AppDelegate 集成
```swift
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
private let nimService = NIMSDKService.shared
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// NIMSDK已经在NIMSDKService中初始化
// 这里可以添加其他初始化代码
return true
}
// MARK: - 推送处理
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
nimService.updateApnsToken(deviceToken)
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
let handled = nimService.handlePushNotification(userInfo)
completionHandler(handled ? .newData : .noData)
}
}
```
## 4. 配置说明
### 4.1 环境配置
```objc
// DEBUG环境配置
#ifdef DEBUG
config.apnsCername = @"pikoDevelopPush";
config.enabledHttpsForInfo = NO;
config.enabledHttpsForMessage = NO;
#else
config.apnsCername = @"newPiko";
config.enabledHttpsForInfo = YES;
config.enabledHttpsForMessage = YES;
#endif
```
### 4.2 AppKey配置
```objc
// 从常量文件获取AppKey
config.appKey = KeyWithType(KeyType_NetEase);
// 或者直接设置
config.appKey = @"your_app_key_here";
```
### 4.3 推送配置
确保在项目中正确配置了APNS证书并在配置中设置正确的证书名称。
## 5. 最佳实践
### 5.1 初始化时机
- 在App启动时尽早初始化NIMSDK
- 确保在用户登录前完成初始化
### 5.2 错误处理
- 对所有异步操作添加错误处理
- 在UI线程中处理回调结果
### 5.3 内存管理
- 及时移除不需要的代理
- 避免循环引用
### 5.4 状态管理
- 监听登录状态变化
- 根据状态变化更新UI
### 5.5 Swift集成
- 使用桥接头文件正确导入Objective-C类
- 在Swift中创建便捷的包装方法
## 6. 常见问题
### 6.1 编译错误
**Q: 编译时提示找不到NIMSDK头文件**
A: 确保正确导入了NIMSDK框架并在桥接头文件中正确导入。
### 6.2 初始化失败
**Q: NIMSDK初始化失败**
A: 检查AppKey是否正确网络连接是否正常。
### 6.3 登录问题
**Q: 登录后立即被踢出**
A: 检查账号是否在其他设备登录或者Token是否过期。
### 6.4 Swift桥接问题
**Q: Swift中无法使用NIMSDKManager**
A: 确保在桥接头文件中正确导入了相关头文件。
### 6.5 推送问题
**Q: 推送通知无法接收**
A: 检查APNS证书配置确保设备Token正确上传。
---
**文档版本**: 1.0
**最后更新**: 2024年12月
**维护人员**: 开发团队

View File

@@ -0,0 +1,555 @@
# NIMSDK 集成说明文档
## 目录
- [1. 项目概述](#1-项目概述)
- [2. NIMSDK导入配置](#2-nimsdk导入配置)
- [3. NIMSDK初始化流程](#3-nimsdk初始化流程)
- [4. 关键配置参数说明](#4-关键配置参数说明)
- [5. 自定义消息处理](#5-自定义消息处理)
- [6. 登录管理](#6-登录管理)
- [7. 消息接收处理](#7-消息接收处理)
- [8. 推送通知集成](#8-推送通知集成)
- [9. 最佳实践](#9-最佳实践)
- [10. 常见问题](#10-常见问题)
## 1. 项目概述
### 1.1 项目简介
YuMi是一个基于iOS平台的社交应用使用Objective-C开发采用MVP架构模式。项目集成了网易云信SDKNIMSDK用于实现即时通讯功能。
### 1.2 技术栈
- **开发语言**: Objective-C
- **架构模式**: MVP (Model-View-Presenter)
- **即时通讯**: 网易云信 NIMSDK_LITE
- **依赖管理**: CocoaPods
- **最低支持版本**: iOS 11.0
### 1.3 主要功能模块
- 用户登录注册
- 即时消息通讯
- 聊天室功能
- 动态发布
- 个人中心
- 房间直播
## 2. NIMSDK导入配置
### 2.1 Podfile配置
在项目的`Podfile`中添加NIMSDK依赖
```ruby
# 云信SDK
pod 'NIMSDK_LITE'
```
### 2.2 头文件导入
在需要使用NIMSDK的文件中导入头文件
```objc
#import <NIMSDK/NIMSDK.h>
```
### 2.3 项目结构
```
YuMi/
├── Appdelegate/
│ ├── AppDelegate.m # 主应用代理
│ └── AppDelegate+ThirdConfig.m # 第三方SDK配置
├── Modules/
│ ├── YMMessage/ # 消息模块
│ │ └── Tool/
│ │ └── CustomAttachmentDecoder # 自定义消息解码器
│ ├── YMTabbar/ # 主界面模块
│ └── YMRoom/ # 房间模块
└── Global/
└── YUMIConstant.h # 常量定义
```
## 3. NIMSDK初始化流程
### 3.1 初始化时序图
```mermaid
sequenceDiagram
participant App as AppDelegate
participant ThirdConfig as AppDelegate+ThirdConfig
participant NIMSDK as NIMSDK
participant Config as NIMSDKConfig
participant Decoder as CustomAttachmentDecoder
App->>ThirdConfig: initThirdConfig()
ThirdConfig->>ThirdConfig: configNIMSDK()
ThirdConfig->>NIMSDK: registerWithOption(option)
ThirdConfig->>Decoder: registerCustomDecoder()
ThirdConfig->>Config: shouldConsiderRevokedMessageUnreadCount = YES
ThirdConfig->>Config: setShouldSyncStickTopSessionInfos(YES)
ThirdConfig->>Config: enabledHttpsForInfo = NO (DEBUG)
ThirdConfig->>Config: enabledHttpsForMessage = NO (DEBUG)
Note over App: 应用启动完成
App->>App: loadMainPage()
App->>App: 检查登录状态
App->>NIMSDK: 自动登录或手动登录
```
### 3.2 初始化代码实现
#### 3.2.1 主应用代理初始化
```objc
// AppDelegate.m
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 初始化第三方SDK配置
[self initThirdConfig];
// 其他初始化代码...
return YES;
}
```
#### 3.2.2 NIMSDK配置方法
```objc
// AppDelegate+ThirdConfig.m
- (void)configNIMSDK {
// 1. 获取云信AppKey
NSString *appKey = KeyWithType(KeyType_NetEase);
NIMSDKOption *option = [NIMSDKOption optionWithAppKey:appKey];
// 2. 配置APNS证书名称
#ifdef DEBUG
option.apnsCername = @"pikoDevelopPush";
#else
option.apnsCername = @"newPiko";
#endif
// 3. 注册SDK
[[NIMSDK sharedSDK] registerWithOption:option];
// 4. 注册自定义消息解码器
[NIMCustomObject registerCustomDecoder:[[CustomAttachmentDecoder alloc] init]];
// 5. 配置SDK参数
[NIMSDKConfig sharedConfig].shouldConsiderRevokedMessageUnreadCount = YES;
[[NIMSDKConfig sharedConfig] setShouldSyncStickTopSessionInfos:YES];
// 6. DEBUG模式下禁用HTTPS
#ifdef DEBUG
[NIMSDKConfig sharedConfig].enabledHttpsForInfo = NO;
[NIMSDKConfig sharedConfig].enabledHttpsForMessage = NO;
#endif
}
```
## 4. 关键配置参数说明
### 4.1 AppKey配置
项目支持多环境配置,通过`YUMIConstant.m`中的`KeyWithType`方法获取:
```objc
// 测试环境
@(KeyType_NetEase): @"79bc37000f4018a2a24ea9dc6ca08d32"
// 生产环境
@(KeyType_NetEase): @"7371d729710cd6ce3a50163b956b5eb6"
```
### 4.2 APNS推送配置
```objc
// 开发环境
option.apnsCername = @"pikoDevelopPush";
// 生产环境
option.apnsCername = @"newPiko";
```
### 4.3 SDK配置参数详解
| 参数 | 值 | 说明 |
|------|----|----|
| `shouldConsiderRevokedMessageUnreadCount` | `YES` | 撤回消息计入未读数 |
| `shouldSyncStickTopSessionInfos` | `YES` | 同步置顶会话信息 |
| `enabledHttpsForInfo` | `NO` (DEBUG) | DEBUG模式禁用HTTPS信息传输 |
| `enabledHttpsForMessage` | `NO` (DEBUG) | DEBUG模式禁用HTTPS消息传输 |
## 5. 自定义消息处理
### 5.1 自定义附件解码器
#### 5.1.1 解码器接口定义
```objc
// CustomAttachmentDecoder.h
@interface CustomAttachmentDecoder : NSObject<NIMCustomAttachmentCoding>
@end
```
#### 5.1.2 解码器实现
```objc
// CustomAttachmentDecoder.m
- (id<NIMCustomAttachment>)decodeAttachment:(NSString *)content {
id<NIMCustomAttachment> attachment;
NSData *data = [content dataUsingEncoding:NSUTF8StringEncoding];
if (data) {
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data
options:0
error:nil];
if ([dict isKindOfClass:[NSDictionary class]]) {
int first = [dict[@"first"] intValue];
int second = [dict[@"second"] intValue];
id originalData = dict[@"data"];
AttachmentModel *model = [[AttachmentModel alloc] init];
model.first = (short)first;
model.second = (short)second;
model.data = originalData;
attachment = model;
}
}
return attachment;
}
```
### 5.2 消息类型定义
自定义消息通过`AttachmentModel`定义结构:
```objc
@interface AttachmentModel : NSObject<NIMCustomAttachment>
@property (nonatomic, assign) short first; // 消息类型标识
@property (nonatomic, assign) short second; // 消息子类型标识
@property (nonatomic, strong) id data; // 消息数据内容
@end
```
### 5.3 消息类型示例
```objc
// 打招呼消息
if (attachment.first == CustomMessageType_FindNew &&
attachment.second == Custom_Message_Find_New_Greet_New_User) {
// 处理新用户打招呼消息
FindNewGreetMessageModel *greetInfo = [FindNewGreetMessageModel modelWithDictionary:attachment.data];
// 显示打招呼弹窗
}
```
## 6. 登录管理
### 6.1 登录状态检查
```objc
// 检查是否已登录
if ([NIMSDK sharedSDK].loginManager.isLogined) {
// 已登录,执行相关操作
} else {
// 未登录,跳转登录页面
}
```
### 6.2 自动登录处理
```objc
// NIMLoginManagerDelegate
- (void)onAutoLoginFailed:(NSError *)error {
// 如果非上次登录设备 autoLogin 会返回 417
if (error.code == 417) {
@weakify(self);
AccountModel* accountModel = [AccountInfoStorage instance].getCurrentAccountInfo;
[[NIMSDK sharedSDK].loginManager login:accountModel.uid
token:accountModel.netEaseToken
completion:^(NSError * _Nullable error) {
if (error) {
@strongify(self);
[self.presenter logout];
}
}];
return;
}
[self.presenter logout];
}
```
### 6.3 踢出处理
```objc
// NIMLoginManagerDelegate
- (void)onKickout:(NIMLoginKickoutResult *)result {
// 显示踢出提示
[XNDJTDDLoadingTool showErrorWithMessage:YMLocalizedString(@"TabbarViewController0")];
// 清理房间状态
if ([XPRoomMiniManager shareManager].getRoomInfo) {
[[RtcManager instance] exitRoom];
[[NIMSDK sharedSDK].chatroomManager exitChatroom:roomId completion:nil];
[self.roomMineView hiddenRoomMiniView];
}
// 执行登出
[self.presenter logout];
}
```
### 6.4 手动登录
```objc
// 执行登录
[[NIMSDK sharedSDK].loginManager login:accountModel.uid
token:accountModel.netEaseToken
completion:^(NSError * _Nullable error) {
if (error) {
// 登录失败处理
NSLog(@"登录失败: %@", error);
} else {
// 登录成功处理
NSLog(@"登录成功");
}
}];
```
## 7. 消息接收处理
### 7.1 消息接收代理
```objc
// NIMChatManagerDelegate
- (void)onRecvMessages:(NSArray<NIMMessage *> *)messages {
if ([AccountInfoStorage instance].getTicket.length == 0) {
return;
}
for (NIMMessage *message in messages) {
if (message.session.sessionType == NIMSessionTypeP2P) {
if (message.messageType == NIMMessageTypeCustom) {
NIMCustomObject *obj = (NIMCustomObject *)message.messageObject;
if (obj.attachment != nil && [obj.attachment isKindOfClass:[AttachmentModel class]]) {
AttachmentModel *attachment = (AttachmentModel *)obj.attachment;
// 处理自定义消息
[self handleCustomMessage:attachment];
}
}
}
}
}
```
### 7.2 自定义消息处理
```objc
- (void)handleCustomMessage:(AttachmentModel *)attachment {
switch (attachment.first) {
case CustomMessageType_FindNew:
[self handleFindNewMessage:attachment];
break;
case CustomMessageType_Gift:
[self handleGiftMessage:attachment];
break;
default:
break;
}
}
- (void)handleFindNewMessage:(AttachmentModel *)attachment {
if (attachment.second == Custom_Message_Find_New_Greet_New_User) {
FindNewGreetMessageModel *greetInfo = [FindNewGreetMessageModel modelWithDictionary:attachment.data];
if (greetInfo.uid.integerValue != [AccountInfoStorage instance].getUid.integerValue) {
// 显示打招呼弹窗
[self showGreetAlert:greetInfo];
}
}
}
```
### 7.3 广播消息处理
```objc
// NIMBroadcastManagerDelegate
- (void)onReceiveBroadcastMessage:(NIMBroadcastMessage *)broadcastMessage {
if ([AccountInfoStorage instance].getUid.length == 0) {
return;
}
// 处理广播消息
NSString *content = broadcastMessage.content;
// 解析并处理广播内容
}
```
## 8. 推送通知集成
### 8.1 推送权限申请
```objc
- (void)registerNot {
if (@available(iOS 10.0, *)) {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert |
UNAuthorizationOptionBadge |
UNAuthorizationOptionSound)
completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (granted) {
[center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
if (settings.authorizationStatus == UNAuthorizationStatusAuthorized) {
dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] registerForRemoteNotifications];
});
}
}];
}
}];
}
}
```
### 8.2 设备Token更新
```objc
- (void)application:(UIApplication *)app
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
// 上传devicetoken至云信服务器
[[NIMSDK sharedSDK] updateApnsToken:deviceToken];
}
```
### 8.3 推送消息处理
```objc
- (void)application:(UIApplication *)application
didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
NSString *data = userInfo[@"data"];
if (data) {
NSDictionary *dataDic = [data mj_JSONObject];
NSString *userId = dataDic[@"uid"];
if (userId) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:kOpenRoomNotification
object:nil
userInfo:@{@"type": @"kOpenChat",
@"uid": userId,
@"isNoAttention": @(YES)}];
ClientConfig *config = [ClientConfig shareConfig];
config.pushChatId = userId;
});
return;
}
}
completionHandler(UIBackgroundFetchResultNewData);
}
```
### 8.4 应用状态处理
```objc
- (void)applicationDidEnterBackground:(UIApplication *)application {
// 设置应用角标为未读消息数
NSInteger count = [NIMSDK sharedSDK].conversationManager.allUnreadCount;
[[UIApplication sharedApplication] setApplicationIconBadgeNumber:count];
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
// 应用激活时清除角标
[[UIApplication sharedApplication] setApplicationIconBadgeNumber:0];
[[NSNotificationCenter defaultCenter] postNotificationName:@"kAppDidBecomeActive" object:nil];
}
```
## 9. 最佳实践
### 9.1 初始化最佳实践
1. **在应用启动时初始化**: 在`AppDelegate``didFinishLaunchingWithOptions`中调用
2. **配置环境参数**: 区分开发和生产环境的配置
3. **注册自定义解码器**: 在SDK注册后立即注册自定义解码器
4. **设置代理**: 在适当的时机添加和移除代理
### 9.2 登录管理最佳实践
1. **自动登录**: 优先使用自动登录,减少用户等待时间
2. **错误处理**: 对登录失败进行适当的错误处理和重试
3. **状态同步**: 保持本地登录状态与服务器状态同步
4. **踢出处理**: 妥善处理被踢出的情况,清理相关状态
### 9.3 消息处理最佳实践
1. **消息过滤**: 根据业务需求过滤不需要的消息
2. **自定义消息**: 合理设计自定义消息结构
3. **性能优化**: 避免在消息处理中进行耗时操作
4. **内存管理**: 及时释放不需要的消息对象
### 9.4 推送通知最佳实践
1. **权限申请**: 在合适的时机申请推送权限
2. **Token更新**: 及时更新设备Token
3. **消息解析**: 正确解析推送消息内容
4. **状态处理**: 根据应用状态处理推送消息
## 10. 常见问题
### 10.1 初始化问题
**Q: SDK初始化失败怎么办**
A: 检查AppKey是否正确网络连接是否正常证书配置是否正确。
**Q: 自定义解码器注册失败?**
A: 确保在SDK注册后注册解码器解码器类实现了正确的协议。
### 10.2 登录问题
**Q: 自动登录失败错误码417**
A: 这是正常情况,表示非上次登录设备,需要重新输入账号密码登录。
**Q: 登录后立即被踢出?**
A: 检查账号是否在其他设备登录或者Token是否过期。
### 10.3 消息问题
**Q: 自定义消息无法解析?**
A: 检查消息格式是否正确,解码器是否正确注册。
**Q: 消息发送失败?**
A: 检查网络连接,登录状态,以及消息内容格式。
### 10.4 推送问题
**Q: 推送通知无法接收?**
A: 检查推送权限是否开启证书配置是否正确设备Token是否正确上传。
**Q: 推送消息解析错误?**
A: 检查推送消息格式,确保解析逻辑正确。
## 11. 总结
本项目对NIMSDK的集成非常完整包括
1. **完整的初始化流程**: 从SDK注册到配置参数设置
2. **自定义消息处理**: 实现了自定义附件解码器
3. **登录状态管理**: 包含自动登录、踢出处理等
4. **推送通知集成**: 完整的APNS推送处理
5. **消息接收处理**: 支持自定义消息类型的处理
6. **多环境配置**: 区分开发和生产环境的配置
整个集成方案遵循了NIMSDK的最佳实践代码结构清晰功能完整为项目的即时通讯功能提供了坚实的基础。
---
**文档版本**: 1.0
**最后更新**: 2024年12月
**维护人员**: 开发团队

View File

@@ -0,0 +1,247 @@
# PublicRoomManager 使用指南
## 概述
`PublicRoomManager` 是一个常驻单例,负责管理用户进入公共聊天房间的逻辑。它会在用户登录成功后自动初始化,并在用户登出时自动清理。
## 主要功能
1. **自动初始化**: 用户登录成功后自动初始化
2. **自动进房**: 根据用户的分区ID自动进入对应的公共聊天房间
3. **消息监听**: 监听公共房间的消息
4. **自动清理**: 用户登出时自动退出房间并清理状态
5. **用户切换处理**: 支持多次登出-登录的重置情况
## 核心特性
### 1. 生命周期管理
```objc
// 初始化(在用户登录成功后自动调用)
[[PublicRoomManager sharedManager] initialize];
// 重置(在用户登出时自动调用)
[[PublicRoomManager sharedManager] reset];
```
### 2. 状态查询
```objc
// 检查是否已初始化
BOOL isInitialized = [[PublicRoomManager sharedManager] isInitialized];
// 检查是否已在公共房间中
BOOL isInPublicRoom = [[PublicRoomManager sharedManager] isInPublicRoom];
// 获取当前公共房间ID
NSString *roomId = [[PublicRoomManager sharedManager] currentPublicRoomId];
```
### 3. 手动控制
```objc
// 手动进入公共房间
[[PublicRoomManager sharedManager] enterPublicRoomWithCompletion:^(NSError * _Nullable error) {
if (error) {
NSLog(@"进入公共房间失败: %@", error);
} else {
NSLog(@"进入公共房间成功");
}
}];
// 手动退出公共房间
[[PublicRoomManager sharedManager] exitPublicRoomWithCompletion:^(NSError * _Nullable error) {
if (error) {
NSLog(@"退出公共房间失败: %@", error);
} else {
NSLog(@"退出公共房间成功");
}
}];
```
## 集成点
### 1. 登录流程集成
`PILoginManager.m` 的登录成功回调中添加:
```objc
// 初始化公共房间管理器
[[PublicRoomManager sharedManager] initialize];
```
### 2. 登出流程集成
`BaseMvpPresenter.m` 的 logout 方法中添加:
```objc
// 重置公共房间管理器
[[PublicRoomManager sharedManager] reset];
```
### 3. 用户信息更新集成
`TabbarViewController.m``getUserInfoSuccess` 方法中添加:
```objc
// 更新公共房间管理器的用户信息
[[PublicRoomManager sharedManager] updateUserInfo:userInfo];
```
### 4. 配置更新集成
`ClientConfig.m` 的配置加载完成后添加:
```objc
// 通知公共房间管理器配置已更新
[[PublicRoomManager sharedManager] updateConfig];
```
## 工作原理
### 1. 初始化流程
1. 检查用户是否已登录
2. 检查用户信息是否完整(包含 partitionId
3. 检查配置信息是否已加载(包含 publicChatRoomIdMap
4. 注册云信消息代理
5. 根据 partitionId 获取对应的 roomId
6. 进入公共聊天房间
### 2. 进房流程
1. 创建 NIMChatroomEnterRequest
2. 设置用户扩展信息(头像、昵称、等级等)
3. 调用云信 SDK 进入房间
4. 处理进房成功/失败回调
### 3. 消息处理
1. 实现 NIMChatManagerDelegate
2. 过滤公共房间消息
3. 处理消息内容
### 4. 清理流程
1. 退出公共聊天房间
2. 移除云信代理
3. 重置所有状态
## 配置要求
### 1. 用户信息要求
用户信息必须包含 `partitionId` 字段:
```objc
UserInfoModel *userInfo = [AccountInfoStorage instance].getHomeUserInfo;
NSString *partitionId = userInfo.partitionId; // 必须存在
```
### 2. 配置信息要求
配置信息必须包含 `publicChatRoomIdMap` 字段:
```objc
ClientDataModel *configInfo = [ClientConfig shareConfig].configInfo;
NSDictionary *publicChatRoomIdMap = configInfo.publicChatRoomIdMap; // 必须存在
```
`publicChatRoomIdMap` 的格式应该是:
```json
{
"1": "roomId1", // 分区1对应的房间ID
"2": "roomId2", // 分区2对应的房间ID
"3": "roomId3" // 分区3对应的房间ID
}
```
## 错误处理
### 1. 初始化失败
- 用户未登录:等待用户登录
- 用户信息不完整:等待用户信息更新
- 配置信息未加载:等待配置更新
### 2. 进房失败
- 网络错误:记录日志,可重试
- 房间不存在:记录日志,跳过
- 权限不足:记录日志,跳过
### 3. 用户切换
- 检测到用户ID变化时自动重置
- 清理旧用户状态
- 重新初始化新用户
## 日志输出
PublicRoomManager 会输出详细的日志信息:
```
PublicRoomManager: 初始化成功用户ID: 123456, 分区ID: 1
PublicRoomManager: 尝试进入公共房间分区ID: 1, 房间ID: roomId1
PublicRoomManager: 进入公共房间成功房间ID: roomId1
PublicRoomManager: 收到公共房间消息: Hello World
PublicRoomManager: 检测到用户切换,重置管理器
PublicRoomManager: 开始重置
PublicRoomManager: 退出公共房间成功
PublicRoomManager: 重置完成
```
## 注意事项
1. **线程安全**: 所有操作都在主线程执行
2. **内存管理**: 使用单例模式,避免内存泄漏
3. **错误恢复**: 支持自动重试和错误恢复
4. **状态同步**: 确保状态与实际云信状态同步
5. **性能优化**: 避免重复初始化和不必要的操作
## 扩展功能
### 1. 消息处理扩展
可以在 `onRecvMessages` 方法中添加自定义的消息处理逻辑:
```objc
- (void)onRecvMessages:(NSArray<NIMMessage *> *)messages {
for (NIMMessage *message in messages) {
if (message.session.sessionType == NIMSessionTypeChatroom) {
NSString *sessionId = message.session.sessionId;
if ([sessionId isEqualToString:self.currentPublicRoomId]) {
// 添加自定义消息处理逻辑
[self handlePublicRoomMessage:message];
}
}
}
}
```
### 2. 状态监听扩展
可以添加状态变化的通知:
```objc
// 在状态变化时发送通知
[[NSNotificationCenter defaultCenter] postNotificationName:@"PublicRoomManagerStateChanged"
object:nil
userInfo:@{@"isInPublicRoom": @(self.isInPublicRoom)}];
```
### 3. 重试机制扩展
可以添加更复杂的重试逻辑:
```objc
- (void)retryEnterPublicRoom {
if (self.retryCount < 3) {
self.retryCount++;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self tryEnterPublicRoom];
});
}
}
```

View File

@@ -0,0 +1,293 @@
# getUserInfo API 使用说明文档
## 方法概述
`getUserInfo:uid:``Api` 类中的一个静态方法,用于获取指定用户的详细信息。
## 方法签名
```objc
+ (void)getUserInfo:(HttpRequestHelperCompletion)completion uid:(NSString *)uid;
```
## 参数说明
### 输入参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| completion | HttpRequestHelperCompletion | 是 | 请求完成后的回调函数 |
| uid | NSString | 是 | 要查询的用户ID |
### 回调函数格式
```objc
typedef void(^HttpRequestHelperCompletion)(BaseModel* _Nullable data, NSInteger code, NSString * _Nullable msg);
```
**回调参数说明:**
- `data`: BaseModel 对象,包含服务器返回的数据
- `code`: NSIntegerHTTP 状态码或业务状态码
- `msg`: NSString服务器返回的消息或错误信息
## 实现原理
### 1. API 端点
- **Base64 编码的路径**: `dXNlci9nZXQ=`
- **解码后的路径**: `user/get`
- **请求方法**: GET
### 2. 请求流程
1. 将用户ID作为参数传递给 `makeRequest` 方法
2. `makeRequest` 方法通过 `__FUNCTION__` 宏自动解析参数名
3. 构造请求参数字典:`@{@"uid": uid}`
4. 调用 `HttpRequestHelper` 发送 GET 请求
### 3. 参数自动映射
该方法使用了特殊的参数映射机制:
- 通过 `__FUNCTION__` 宏获取方法名
- 解析方法名中的参数部分(冒号后的部分)
- 自动将传入的参数值与参数名对应
## 使用示例
### 基本用法
```objc
// 获取用户ID为 "12345" 的用户信息
[Api getUserInfo:^(BaseModel *data, NSInteger code, NSString *msg) {
if (code == 200) {
// 请求成功
NSLog(@"用户信息: %@", data.data);
NSLog(@"消息: %@", msg);
} else {
// 请求失败
NSLog(@"错误码: %ld", (long)code);
NSLog(@"错误信息: %@", msg);
}
} uid:@"12345"];
```
### 错误处理示例
```objc
[Api getUserInfo:^(BaseModel *data, NSInteger code, NSString *msg) {
switch (code) {
case 200:
// 成功获取用户信息
[self handleUserInfoSuccess:data.data];
break;
case 404:
// 用户不存在
[self showUserNotFoundAlert];
break;
case 401:
// 未授权访问
[self handleUnauthorizedAccess];
break;
default:
// 其他错误
[self showErrorAlert:msg];
break;
}
} uid:userId];
```
### 在 ViewController 中使用
```objc
@interface UserProfileViewController ()
@property (nonatomic, strong) NSString *currentUserId;
@end
@implementation UserProfileViewController
- (void)loadUserInfo {
if (!self.currentUserId) {
NSLog(@"用户ID不能为空");
return;
}
[Api getUserInfo:^(BaseModel *data, NSInteger code, NSString *msg) {
dispatch_async(dispatch_get_main_queue(), ^{
if (code == 200) {
[self updateUIWithUserInfo:data.data];
} else {
[self showErrorWithMessage:msg];
}
});
} uid:self.currentUserId];
}
- (void)updateUIWithUserInfo:(id)userInfo {
// 更新UI显示用户信息
// userInfo 的具体结构需要根据后端返回的数据格式来确定
}
- (void)showErrorWithMessage:(NSString *)message {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"错误"
message:message
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]];
[self presentViewController:alert animated:YES completion:nil];
}
@end
```
## 返回数据结构
### BaseModel 结构
```objc
@interface BaseModel : NSObject
@property(nonatomic,assign) long timestamp; // 时间戳
@property (nonatomic , strong) id data; // 返回的数据
@property (nonatomic , assign) NSInteger code; // 状态码
@property (nonatomic , copy) NSString *message; // 消息
@property (nonatomic,assign) long long cancelDate; // 注销时间戳
@property (nonatomic,copy) NSString *date; // 日期
@property (nonatomic,copy) NSString *reason; // 封禁理由
@end
```
### 成功响应示例
```json
{
"code": 200,
"message": "success",
"timestamp": 1640995200000,
"data": {
"uid": "12345",
"nickname": "用户昵称",
"avatar": "头像URL",
"level": 10,
"vip": false
// 其他用户信息字段...
}
}
```
### 错误响应示例
```json
{
"code": 404,
"message": "用户不存在",
"timestamp": 1640995200000,
"data": null
}
```
## 注意事项
### 1. 线程安全
- 回调函数在后台线程执行
- UI 更新操作需要在主线程进行
### 2. 参数验证
- 确保 `uid` 参数不为空
- 建议在使用前验证 `uid` 的格式
### 3. 内存管理
- 避免在回调中造成循环引用
- 使用 `__weak` 修饰符防止内存泄漏
### 4. 网络状态
- 建议在调用前检查网络连接状态
- 处理网络超时和连接失败的情况
## 最佳实践
### 1. 参数验证
```objc
- (void)getUserInfoWithValidation:(NSString *)uid {
if (!uid || uid.length == 0) {
NSLog(@"用户ID不能为空");
return;
}
// 验证UID格式根据实际需求调整
if (![self isValidUID:uid]) {
NSLog(@"用户ID格式不正确");
return;
}
[Api getUserInfo:^(BaseModel *data, NSInteger code, NSString *msg) {
// 处理响应
} uid:uid];
}
- (BOOL)isValidUID:(NSString *)uid {
// 根据实际业务需求验证UID格式
return uid.length > 0;
}
```
### 2. 错误处理
```objc
- (void)handleApiError:(NSInteger)code message:(NSString *)msg {
switch (code) {
case 200:
// 成功
break;
case 400:
NSLog(@"请求参数错误: %@", msg);
break;
case 401:
NSLog(@"未授权访问,需要重新登录");
[self redirectToLogin];
break;
case 404:
NSLog(@"用户不存在: %@", msg);
break;
case 500:
NSLog(@"服务器内部错误: %@", msg);
break;
default:
NSLog(@"未知错误 (code: %ld): %@", (long)code, msg);
break;
}
}
```
### 3. 缓存策略
```objc
- (void)getUserInfoWithCache:(NSString *)uid {
// 先检查本地缓存
UserInfo *cachedUser = [self getCachedUserInfo:uid];
if (cachedUser) {
[self updateUIWithUserInfo:cachedUser];
}
// 从服务器获取最新数据
[Api getUserInfo:^(BaseModel *data, NSInteger code, NSString *msg) {
if (code == 200) {
// 更新缓存
[self cacheUserInfo:data.data forUID:uid];
[self updateUIWithUserInfo:data.data];
} else {
// 如果服务器请求失败,使用缓存数据(如果有)
if (!cachedUser) {
[self showErrorWithMessage:msg];
}
}
} uid:uid];
}
```
## 相关方法
- `getUserInfos:uids:` - 批量获取多个用户信息
- `completeUserInfo:userInfo:` - 补全用户资料
- `getUserWalletInfo:uid:ticket:` - 获取用户钱包信息
## 版本信息
- **iOS 最低版本**: iOS 15.6
- **创建时间**: 2021/9/6
- **最后更新**: 当前版本