diff --git a/DOC/log.txt b/DOC/log.txt deleted file mode 100644 index b02d2a8a..00000000 --- a/DOC/log.txt +++ /dev/null @@ -1,26 +0,0 @@ --[XPRoomViewController dealloc] [Line 314]🔄 XPRoomViewController: 清理 RoomAnimationView --[RoomAnimationView removeItSelf] [Line 193]�� RoomAnimationView: 开始销毁 --[RoomAnimationView cleanupAllSubviews] [Line 239] 清理所有子视图 --[RoomAnimationView cleanupAllSubviews] [Line 250]�� 标记移除视图: BravoGiftBannerView (从容器: XPRoomAnimationHitView) --[RoomAnimationView cleanupAllSubviews] [Line 250]�� 标记移除视图: BravoGiftWinningFlagView (从容器: XPRoomAnimationHitView) --[RoomAnimationView cleanupAllSubviews] [Line 250]�� 标记移除视图: BravoGiftWinningFlagView (从容器: XPRoomAnimationHitView) --[RoomAnimationView cleanupAllSubviews] [Line 250]�� 标记移除视图: BravoGiftWinningFlagView (从容器: XPRoomAnimationHitView) --[RoomAnimationView cleanupAllSubviews] [Line 250]�� 标记移除视图: BravoGiftWinningFlagView (从容器: XPRoomAnimationHitView) --[RoomAnimationView cleanupAllSubviews] [Line 250]�� 标记移除视图: BravoGiftWinningFlagView (从容器: XPRoomAnimationHitView) --[RoomAnimationView cleanupAllSubviews] [Line 250]�� 标记移除视图: NetImageView (从容器: XPRoomAnimationHitView) --[RoomAnimationView cleanupAllSubviews] [Line 250]�� 标记移除视图: NetImageView (从容器: XPRoomAnimationHitView) --[RoomAnimationView cleanupAllSubviews] [Line 250]�� 标记移除视图: NetImageView (从容器: XPRoomAnimationHitView) --[RoomAnimationView cleanupAllSubviews] [Line 250]�� 标记移除视图: NetImageView (从容器: XPRoomAnimationHitView) --[RoomAnimationView cleanupAllSubviews] [Line 250]�� 标记移除视图: NetImageView (从容器: XPRoomAnimationHitView) --[RoomAnimationView cleanupAllSubviews] [Line 250]�� 标记移除视图: NetImageView (从容器: XPRoomAnimationHitView) --[RoomAnimationView cleanupAllSubviews] [Line 250]�� 标记移除视图: NetImageView (从容器: XPRoomAnimationHitView) --[RoomAnimationView cleanupAllSubviews] [Line 265]🔄 所有子视图清理完成 --[RoomAnimationView removeItSelf] [Line 220]�� 清理 BannerScheduler --[RoomAnimationView cleanupGestureRecognizers] [Line 3690]🔄 清理手势识别器 --[RoomAnimationView cleanupGestureRecognizers] [Line 3713]🔄 手势识别器清理完成 --[RoomAnimationView cleanupCacheManagers] [Line 3717]🔄 清理缓存管理器 --[RoomAnimationView cleanupCacheManagers] [Line 3730]🔄 缓存管理器清理完成 --[RoomAnimationView removeNotificationObservers] [Line 3743]🔄 移除通知监听 --[RoomAnimationView removeNotificationObservers] [Line 3753]🔄 通知监听移除完成 --[RoomAnimationView removeItSelf] [Line 235]�� RoomAnimationView: 销毁完成 --[RoomAnimationView playBroveBanner:]_block_invoke [Line 752]🔄 BravoGiftBannerView complete 回调被调用 \ No newline at end of file diff --git a/DOC/新Banner组件架构设计.md b/DOC/新Banner组件架构设计.md deleted file mode 100644 index 33c1afb6..00000000 --- a/DOC/新Banner组件架构设计.md +++ /dev/null @@ -1,471 +0,0 @@ -# 新Banner组件架构设计 - -## 设计概述 - -基于对现有7种Banner组件的深度分析,设计一套统一的Banner组件架构,包含父类、子类、数据模型和用户反馈机制。 - -## 现有组件分析总结 - -### 共同UI结构模式 - -| 组件 | 背景 | 头像 | 标题/内容 | 礼物图标 | 操作按钮 | 动画效果 | -|------|------|------|-----------|----------|----------|----------| -| RoomHighValueGiftBannerAnimation | ✓ | ✓ | ✓ | ✓ | ✓ | SVGA | -| CPGiftBanner | ✓ | ✓✓(双人) | ✓ | ✓ | - | POP | -| BravoGiftBannerView | ✓ | ✓ | ✓ | ✓ | - | SVGA | -| LuckyPackageBannerView | ✓ | ✓ | ✓ | - | ✓ | POP | -| LuckyGiftWinningBannerView | ✓ | ✓ | ✓ | - | ✓ | POP | -| GameUniversalBannerView | ✓ | ✓ | ✓ | ✓ | ✓ | SVGA | -| PIUniversalBannerView | ✓ | - | ✓ | - | ✓ | SVGA | - -### 共同数据模式 - -- AttachmentModel作为数据源 -- 专用ViewModel进行数据解析 -- 完成回调机制 -- 用户交互跳转 - -## 架构设计 - -### 1. 基础父类设计 - -```objc -// YMBaseBannerView.h -#import -#import "YMBannerDataProtocol.h" -#import "YMBannerDelegate.h" - -NS_ASSUME_NONNULL_BEGIN - -@class AttachmentModel, YMBaseBannerViewModel; - -typedef NS_ENUM(NSUInteger, YMBannerType) { - YMBannerTypeHighValueGift, // 高价值礼物 - YMBannerTypeCPGift, // CP礼物 - YMBannerTypeBravoGift, // Bravo超级礼物 - YMBannerTypeLuckyPackage, // 幸运红包 - YMBannerTypeLuckyWinning, // 幸运中奖 - YMBannerTypeGameUniversal, // 通用游戏 - YMBannerTypeUniversal // 通用飘屏 -}; - -typedef NS_ENUM(NSUInteger, YMBannerAnimationType) { - YMBannerAnimationTypeSlide, // 滑动动画 - YMBannerAnimationTypeFade, // 淡入淡出 - YMBannerAnimationTypeBounce, // 弹跳效果 - YMBannerAnimationTypeCustom // 自定义动画 -}; - -@interface YMBaseBannerView : UIView - -#pragma mark - 核心属性 -@property (nonatomic, assign, readonly) YMBannerType bannerType; -@property (nonatomic, strong) AttachmentModel *attachment; -@property (nonatomic, strong) YMBaseBannerViewModel *viewModel; -@property (nonatomic, weak) id delegate; - -#pragma mark - UI组件 (子类可选择使用) -@property (nonatomic, strong, readonly) UIView *containerView; -@property (nonatomic, strong, readonly) UIImageView *backgroundImageView; -@property (nonatomic, strong, readonly) NetImageView *avatarImageView; -@property (nonatomic, strong, readonly) NetImageView *secondAvatarImageView; // CP双头像 -@property (nonatomic, strong, readonly) UILabel *titleLabel; -@property (nonatomic, strong, readonly) UILabel *subtitleLabel; -@property (nonatomic, strong, readonly) UILabel *contentLabel; -@property (nonatomic, strong, readonly) NetImageView *iconImageView; -@property (nonatomic, strong, readonly) UIButton *actionButton; -@property (nonatomic, strong, readonly) SVGAImageView *svgaView; - -#pragma mark - 动画配置 -@property (nonatomic, assign) YMBannerAnimationType animationType; -@property (nonatomic, assign) CGFloat showDuration; -@property (nonatomic, assign) CGFloat stayDuration; -@property (nonatomic, assign) CGFloat hideDuration; -@property (nonatomic, assign) BOOL enableSwipeGesture; - -#pragma mark - 回调 -@property (nonatomic, copy) void(^onDisplayComplete)(void); -@property (nonatomic, copy) void(^onUserTap)(YMBaseBannerView *banner); -@property (nonatomic, copy) void(^onActionTap)(YMBaseBannerView *banner); -@property (nonatomic, copy) void(^onDismiss)(YMBaseBannerView *banner); - -#pragma mark - 状态 -@property (nonatomic, assign, readonly) BOOL isDisplaying; -@property (nonatomic, assign, readonly) BOOL isDismissed; - -#pragma mark - 类方法 -+ (instancetype)bannerWithAttachment:(AttachmentModel *)attachment; -+ (void)displayInView:(UIView *)superView - attachment:(AttachmentModel *)attachment - complete:(void(^)(void))complete; - -#pragma mark - 实例方法 -- (instancetype)initWithBannerType:(YMBannerType)type; -- (void)configureWithAttachment:(AttachmentModel *)attachment; -- (void)displayInView:(UIView *)superView; -- (void)dismissWithAnimation:(BOOL)animated; - -#pragma mark - 子类重写方法 -- (Class)viewModelClass; // 返回对应的ViewModel类 -- (void)setupUIComponents; // 设置UI组件 -- (void)layoutUIComponents; // 布局UI组件 -- (void)configureWithViewModel:(YMBaseBannerViewModel *)viewModel; // 配置数据 -- (void)performShowAnimation; // 执行显示动画 -- (void)performHideAnimation; // 执行隐藏动画 -- (void)handleUserTap; // 处理用户点击 -- (void)handleActionTap; // 处理操作按钮点击 - -#pragma mark - 用户反馈 -- (void)reportDisplayEvent; // 上报展示事件 -- (void)reportClickEvent:(NSString *)action; // 上报点击事件 -- (void)reportDismissEvent:(NSString *)reason; // 上报消失事件 - -@end - -NS_ASSUME_NONNULL_END -``` - -### 2. 基础数据模型 - -```objc -// YMBaseBannerViewModel.h -#import -#import "PIBaseModel.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface YMBaseBannerViewModel : PIBaseModel - -#pragma mark - 基础信息 -@property (nonatomic, assign) NSInteger roomUid; -@property (nonatomic, assign) NSInteger targetRoomUid; -@property (nonatomic, copy) NSString *title; -@property (nonatomic, copy) NSString *subtitle; -@property (nonatomic, copy) NSString *content; - -#pragma mark - 用户信息 -@property (nonatomic, copy) NSString *senderNick; -@property (nonatomic, copy) NSString *senderAvatar; -@property (nonatomic, assign) NSInteger senderUid; -@property (nonatomic, copy) NSString *receiverNick; -@property (nonatomic, copy) NSString *receiverAvatar; -@property (nonatomic, assign) NSInteger receiverUid; - -#pragma mark - 视觉资源 -@property (nonatomic, copy) NSString *backgroundImageUrl; -@property (nonatomic, copy) NSString *iconImageUrl; -@property (nonatomic, copy) NSString *svgaUrl; - -#pragma mark - 交互配置 -@property (nonatomic, copy) NSString *actionText; -@property (nonatomic, copy) NSString *skipUrl; -@property (nonatomic, assign) BOOL enableClick; -@property (nonatomic, assign) BOOL enableAction; - -#pragma mark - 埋点数据 -@property (nonatomic, copy) NSString *eventType; -@property (nonatomic, strong) NSDictionary *trackingData; - -@end - -NS_ASSUME_NONNULL_END -``` - -### 3. 代理协议设计 - -```objc -// YMBannerDelegate.h -#import - -NS_ASSUME_NONNULL_BEGIN - -@class YMBaseBannerView; - -@protocol YMBannerDelegate - -@optional - -#pragma mark - 生命周期回调 -- (void)bannerWillDisplay:(YMBaseBannerView *)banner; -- (void)bannerDidDisplay:(YMBaseBannerView *)banner; -- (void)bannerWillDismiss:(YMBaseBannerView *)banner; -- (void)bannerDidDismiss:(YMBaseBannerView *)banner; - -#pragma mark - 交互回调 -- (void)banner:(YMBaseBannerView *)banner didTapWithAction:(NSString *)action; -- (void)banner:(YMBaseBannerView *)banner willNavigateToRoom:(NSInteger)roomUid; -- (void)banner:(YMBaseBannerView *)banner willOpenURL:(NSString *)url; - -#pragma mark - 数据回调 -- (void)banner:(YMBaseBannerView *)banner didReportEvent:(NSString *)event data:(NSDictionary *)data; - -@end - -NS_ASSUME_NONNULL_END -``` - -## 子类设计 - -### 1. 高价值礼物Banner - -```objc -// YMHighValueGiftBannerView.h -#import "YMBaseBannerView.h" - -@interface YMHighValueGiftBannerView : YMBaseBannerView - -@property (nonatomic, strong, readonly) UILabel *giftNameLabel; -@property (nonatomic, strong, readonly) UILabel *giftCountLabel; -@property (nonatomic, strong, readonly) MarqueeLabel *senderScrollLabel; -@property (nonatomic, strong, readonly) MarqueeLabel *roomNameScrollLabel; - -@end - -// YMHighValueGiftBannerViewModel.h -@interface YMHighValueGiftBannerViewModel : YMBaseBannerViewModel - -@property (nonatomic, copy) NSString *giftName; -@property (nonatomic, assign) NSInteger giftCount; -@property (nonatomic, copy) NSString *giftImageUrl; -@property (nonatomic, assign) NSInteger bgLevel; -@property (nonatomic, copy) NSString *roomTitle; - -@end -``` - -### 2. CP礼物Banner - -```objc -// YMCPGiftBannerView.h -#import "YMBaseBannerView.h" - -@interface YMCPGiftBannerView : YMBaseBannerView - -@property (nonatomic, strong, readonly) UIStackView *cpStackView; -@property (nonatomic, strong, readonly) UILabel *relationLabel; - -@end - -// YMCPGiftBannerViewModel.h -@interface YMCPGiftBannerViewModel : YMBaseBannerViewModel - -@property (nonatomic, copy) NSString *giftImageUrl; -@property (nonatomic, copy) NSString *relationText; - -@end -``` - -### 3. 通用游戏Banner - -```objc -// YMGameUniversalBannerView.h -#import "YMBaseBannerView.h" - -@interface YMGameUniversalBannerView : YMBaseBannerView - -@property (nonatomic, strong, readonly) NetImageView *gameIconView; -@property (nonatomic, assign) NSInteger gameID; - -@end - -// YMGameUniversalBannerViewModel.h -@interface YMGameUniversalBannerViewModel : YMBaseBannerViewModel - -@property (nonatomic, copy) NSString *gameIconUrl; -@property (nonatomic, assign) NSInteger gameID; -@property (nonatomic, copy) NSString *gameTitle; - -@end -``` - -## 用户反馈机制设计 - -### 1. 反馈管理器 - -```objc -// YMBannerFeedbackManager.h -#import - -NS_ASSUME_NONNULL_BEGIN - -typedef NS_ENUM(NSUInteger, YMBannerFeedbackType) { - YMBannerFeedbackTypeDisplay, // 展示 - YMBannerFeedbackTypeClick, // 点击 - YMBannerFeedbackTypeAction, // 操作 - YMBannerFeedbackTypeDismiss, // 消失 - YMBannerFeedbackTypeError // 错误 -}; - -@interface YMBannerFeedbackManager : NSObject - -+ (instancetype)shared; - -#pragma mark - 事件上报 -- (void)reportBannerEvent:(YMBannerFeedbackType)type - banner:(YMBaseBannerView *)banner - data:(NSDictionary *)data; - -#pragma mark - 性能监控 -- (void)startPerformanceMonitoring:(YMBaseBannerView *)banner; -- (void)endPerformanceMonitoring:(YMBaseBannerView *)banner; - -#pragma mark - 错误收集 -- (void)reportError:(NSError *)error - banner:(YMBaseBannerView *)banner - context:(NSDictionary *)context; - -@end - -NS_ASSUME_NONNULL_END -``` - -### 2. 埋点事件定义 - -```objc -// YMBannerTrackingEvents.h -extern NSString * const kYMBannerEventDisplay; // banner_display -extern NSString * const kYMBannerEventClick; // banner_click -extern NSString * const kYMBannerEventAction; // banner_action -extern NSString * const kYMBannerEventDismiss; // banner_dismiss -extern NSString * const kYMBannerEventLoadStart; // banner_load_start -extern NSString * const kYMBannerEventLoadEnd; // banner_load_end -extern NSString * const kYMBannerEventError; // banner_error - -// 埋点参数Key -extern NSString * const kYMBannerTrackingKeyType; // banner_type -extern NSString * const kYMBannerTrackingKeyRoomUid; // room_uid -extern NSString * const kYMBannerTrackingKeyDuration; // duration -extern NSString * const kYMBannerTrackingKeyAction; // action -extern NSString * const kYMBannerTrackingKeyReason; // reason -``` - -## 上下文调整说明 - -### 1. RoomAnimationView.m 调整 - -#### 1.1 替换现有Banner创建逻辑 - -```objc -// 原代码 -- (void)playRoomGiftBanner:(AttachmentModel *)obj { - [RoomHighValueGiftBannerAnimation display:self.bannerContainer - with:obj - complete:^{ - self.isRoomBannerV2Displaying = NO; - [self processNextRoomEffectAttachment]; - }]; -} - -// 新代码 -- (void)playRoomGiftBanner:(AttachmentModel *)obj { - YMHighValueGiftBannerView *banner = [YMHighValueGiftBannerView bannerWithAttachment:obj]; - banner.delegate = self; - banner.onDisplayComplete = ^{ - self.isRoomBannerV2Displaying = NO; - [self processNextRoomEffectAttachment]; - }; - [banner displayInView:self.bannerContainer]; -} -``` - -#### 1.2 添加代理实现 - -```objc -#pragma mark - YMBannerDelegate -- (void)banner:(YMBaseBannerView *)banner didReportEvent:(NSString *)event data:(NSDictionary *)data { - [[YMBannerFeedbackManager shared] reportBannerEvent:YMBannerFeedbackTypeDisplay - banner:banner - data:data]; -} - -- (void)banner:(YMBaseBannerView *)banner willNavigateToRoom:(NSInteger)roomUid { - // 处理房间跳转逻辑 - RoomInfoModel *currentRoom = self.hostDelegate.getRoomInfo; - if (currentRoom.uid != roomUid) { - // 执行跨房间跳转 - } -} -``` - -### 2. 项目依赖调整 - -#### 2.1 新增文件结构 - -```YuMi/Modules/YMRoom/View/Banner/ -├── Base/ -│ ├── YMBaseBannerView.h/.m -│ ├── YMBaseBannerViewModel.h/.m -│ └── YMBannerDelegate.h -├── Subclasses/ -│ ├── YMHighValueGiftBannerView.h/.m -│ ├── YMCPGiftBannerView.h/.m -│ ├── YMBravoGiftBannerView.h/.m -│ ├── YMGameUniversalBannerView.h/.m -│ └── YMLuckyPackageBannerView.h/.m -├── Manager/ -│ └── YMBannerFeedbackManager.h/.m -└── Constants/ - └── YMBannerTrackingEvents.h/.m -``` - -#### 2.2 Podfile依赖更新 - -```ruby -# 确保动画库版本兼容 -pod 'pop', '~> 1.0' -pod 'SVGAPlayer', '~> 2.3' - -# 新增性能监控 -pod 'YMPerformanceMonitor' # 如果有自定义性能监控库 -``` - -### 3. 配置文件调整 - -#### 3.1 添加Banner配置 - -```objc -// YMBannerConfig.h -@interface YMBannerConfig : NSObject - -@property (nonatomic, assign) BOOL enablePerformanceMonitoring; -@property (nonatomic, assign) BOOL enableErrorReporting; -@property (nonatomic, assign) CGFloat defaultShowDuration; -@property (nonatomic, assign) CGFloat defaultStayDuration; -@property (nonatomic, assign) CGFloat defaultHideDuration; - -+ (instancetype)shared; - -@end -``` - -### 4. 迁移策略 - -#### 4.1 Phase 1: 基础架构 - -- 创建基类和协议 -- 实现反馈管理器 -- 设置基础配置 - -#### 4.2 Phase 2: 渐进迁移 - -- 优先迁移使用频率最高的Banner -- 保持向后兼容性 -- 添加单元测试 - -#### 4.3 Phase 3: 优化完善 - -- 移除旧代码 -- 性能优化 -- 文档完善 - -## 总结 - -这套新的Banner组件架构具备以下优势: - -1. **统一性**: 提供一致的API和行为模式 -2. **扩展性**: 易于添加新的Banner类型 -3. **可维护性**: 集中管理动画、埋点和反馈 -4. **性能优化**: 统一的资源管理和内存优化 -5. **用户体验**: 标准化的交互和反馈机制 - -通过这套架构,可以显著提升Banner组件的开发效率和用户体验质量。 diff --git a/DOC/飘屏组件分析文档.md b/DOC/飘屏组件分析文档.md deleted file mode 100644 index 33f32c42..00000000 --- a/DOC/飘屏组件分析文档.md +++ /dev/null @@ -1,80 +0,0 @@ -# 飘屏组件分析文档 - -## 概述 - -本文档详细分析了项目中在 `RoomAnimationView.m` 的 `bannerContainer` 中展示的所有飘屏类型及其触发逻辑。 - -## 飘屏展示机制 - -- **展示容器**: `bannerContainer` (XPRoomAnimationHitView) -- **位置**: 视图层级最顶层,距离顶部导航栏高度 -- **尺寸**: 屏幕宽度 × 180px 高度 -- **管理机制**: 统一队列管理,按优先级顺序播放 - -## 飘屏类型详细表格 - -| 序号 | 飘屏名称 | 类名 | 触发消息类型 | 处理方法 | 业务场景 | 功能描述 | -|-----|---------|------|-------------|---------|----------|---------| -| 1 | 高价值礼物飘屏 | RoomHighValueGiftBannerAnimation | Custom_Message_Sub_Gift_ChannelNotify | playRoomGiftBanner: | 礼物系统 | 展示高价值礼物的全服广播 | -| 2 | CP礼物飘屏 | CPGiftBanner | Custom_Message_Sub_CP_Gift | playCPGiftBanner: | CP系统 | 展示CP相关礼物特效 | -| 3 | Bravo超级礼物飘屏 | BravoGiftBannerView | Custom_Message_Sub_Super_Gift_Banner | playBroveBanner: | 超级礼物 | 展示Bravo超级礼物效果 | -| 4 | 幸运红包飘屏 | LuckyPackageBannerView | Custom_Message_Sub_LuckyPackage | playLuckyPackageBanner: | 红包系统 | 展示房间红包,支持跨房间跳转 | -| 5 | 幸运礼物中奖飘屏 | LuckyGiftWinningBannerView | Custom_Message_Sub_Super_Gift_Winning_Coins_ALL_Room | playLuckyWinningBanner: | 中奖系统 | 展示全服中奖信息 | -| 6 | 通用游戏飘屏 | GameUniversalBannerView | Custom_Message_Sub_General_Floating_Screen_One_Room
Custom_Message_Sub_General_Floating_Screen_All_Room | playGameBanner: | 游戏系统 | 展示游戏相关飘屏,支持跳转 | -| 7 | 塔罗飘屏 | XPRoomTarrowBannerView | Custom_Message_Sub_Tarot_Advanced
Custom_Message_Sub_Tarot_Intermediate | createTarotBannerAnimation: | 塔罗活动 | 展示塔罗相关活动信息 | -| 8 | 星厨房飘屏 | XPRoomStarKitchenBannerView | Custom_Message_Sub_Star_Kitchen_FullScreen | createStarKitchenBannerAnimation: | 厨房活动 | 展示星厨房活动 | -| 9 | 夺宝精灵飘屏 | - | Custom_Message_Sub_Treasure_Fairy_Draw_Gift_L4
Custom_Message_Sub_Treasure_Fairy_Draw_Gift_L5
Custom_Message_Sub_Treasure_Fairy_Convert_L1/L2/L3 | createTreasureFairyBannerAnimation: | 夺宝活动 | 展示夺宝精灵高等级奖励 | -| 10 | 主播小时榜飘屏 | XPRoomAnchorRankBannerView | Custom_Message_Sub_Anchor_Hour_Rank | 创建主播排行榜飘屏 | 排行榜 | 展示主播小时榜信息 | -| 11 | 通用H5飘屏 | XPRoomTarrowBannerView | Custom_Message_Sub_Common_H5_Novice
Custom_Message_Sub_Common_H5_Advanced | createCommonH5BannerAnimation: | H5活动 | 展示通用H5活动飘屏 | -| 12 | 通用飘屏 | PIUniversalBannerView | 多种消息类型 | createGeneralFloatingScreenAnimation: | 通用展示 | 提供通用飘屏展示能力 | - -## 队列管理机制 - -### 核心属性 - -- **队列数组**: `roomBannertModelsQueueV2` (NSMutableArray) -- **状态标记**: `isRoomBannerV2Displaying` (BOOL) - -### 处理流程 - -1. **消息接收**: 调用 `inserBannerModelToQueue:` 将消息加入队列 -2. **优先级排序**: 调用 `sortBannerQueue` 按消息类型的second值排序(数值越小优先级越高) -3. **顺序播放**: 调用 `processNextRoomEffectAttachment` 逐个播放 -4. **播放完成**: 每个飘屏播放完成后,设置 `isRoomBannerV2Displaying = NO` 并处理下一个 - -### 优先级规则 - -飘屏按 `AttachmentModel.second` 值进行排序,确保重要消息优先展示。 - -## 消息类型分类 - -### 礼物相关 - -- Custom_Message_Sub_Gift_ChannelNotify (32) -- Custom_Message_Sub_CP_Gift -- Custom_Message_Sub_Super_Gift_Banner (1066) - -### 红包/福袋相关 - -- Custom_Message_Sub_LuckyPackage (607) -- Custom_Message_Sub_Super_Gift_Winning_Coins_ALL_Room (1063) - -### 游戏相关 - -- Custom_Message_Sub_General_Floating_Screen_One_Room (1071) -- Custom_Message_Sub_General_Floating_Screen_All_Room (1072) - -### 活动相关 - -- Custom_Message_Sub_Tarot_Advanced (714) -- Custom_Message_Sub_Tarot_Intermediate (713) -- Custom_Message_Sub_Star_Kitchen_FullScreen (1042) -- Custom_Message_Sub_Treasure_Fairy_* (9700系列) - -### 排行榜相关 - -- Custom_Message_Sub_Anchor_Hour_Rank (891) - -### 通用相关 - -- Custom_Message_Sub_Common_H5_* (1100系列) diff --git a/DOC/飘屏组件抽象分析.md b/DOC/飘屏组件抽象分析.md deleted file mode 100644 index e054f4a0..00000000 --- a/DOC/飘屏组件抽象分析.md +++ /dev/null @@ -1,203 +0,0 @@ -# 飘屏组件抽象分析 - -## 分析概述 - -基于对项目中12种飘屏组件的深入分析,发现这些组件存在高度相似的设计模式和实现结构,具备抽象出统一父类的条件。 - -## 现状分析 - -### 共同特征 - -经过代码分析,发现所有飘屏组件都具有以下共同特征: - -#### 1. 统一的类方法签名模式 - -大部分Banner类都采用了相似的类方法签名: - -``` objc -+ (void)display:(UIView *)superView - with:(AttachmentModel *)attachment - complete:(void(^)(void))complete; - -// 或者带房间信息的版本 -+ (void)display:(UIView *)superView - inRoomUid:(NSInteger)roomUid - with:(AttachmentModel *)attachment - complete:(void(^)(void))complete -exitCurrentRoom:(void(^)(void))exit; -``` - -#### 2. 共同的内部属性结构 - -```objc -@property (nonatomic, strong) SomeViewModel *model; // 数据模型 -@property (nonatomic, strong) UIImageView *backgroundImageView; // 背景图片 -@property (nonatomic, copy) void(^completeDisplay)(void); // 完成回调 -@property (nonatomic, copy) void(^exitCurrentRoom)(void); // 退房回调 -@property (nonatomic, assign) NSInteger currentRoomUid; // 当前房间ID -``` - -#### 3. 相似的动画流程 - -- 从屏幕右侧滑入 (x = KScreenWidth) -- 短暂停留展示 -- 滑出屏幕或淡出 -- 执行完成回调 - -#### 4. 统一的数据处理模式 - -- 都依赖 `AttachmentModel` 作为数据源 -- 都创建对应的ViewModel进行数据解析 -- 都继承自 `PIBaseModel` - -#### 5. 相似的UI组件 - -- 背景图片视图 -- 用户头像 -- 文本标签 -- 可选的SVGA动画视图 -- 可选的交互按钮 - -## 抽象方案设计 - -### 建议的基类结构: `BaseRoomBannerView` - -```objc -@interface BaseRoomBannerView : UIView - -#pragma mark - 核心属性 -@property (nonatomic, strong) AttachmentModel *attachment; -@property (nonatomic, strong) PIBaseModel *viewModel; -@property (nonatomic, assign) NSInteger currentRoomUid; -@property (nonatomic, assign) NSInteger targetRoomUid; - -#pragma mark - UI组件 -@property (nonatomic, strong) UIView *containerView; -@property (nonatomic, strong) UIImageView *backgroundImageView; -@property (nonatomic, strong) NetImageView *avatarImageView; -@property (nonatomic, strong) UILabel *titleLabel; -@property (nonatomic, strong) UILabel *subTitleLabel; -@property (nonatomic, strong) UIButton *actionButton; -@property (nonatomic, strong) SVGAImageView *svgaView; - -#pragma mark - 动画配置 -@property (nonatomic, assign) CGFloat showDuration; -@property (nonatomic, assign) CGFloat stayDuration; -@property (nonatomic, assign) CGFloat hideDuration; -@property (nonatomic, assign) BOOL useSlideAnimation; -@property (nonatomic, assign) BOOL useFadeAnimation; - -#pragma mark - 回调 -@property (nonatomic, copy) void(^completeDisplay)(void); -@property (nonatomic, copy) void(^exitCurrentRoom)(void); -@property (nonatomic, copy) void(^didTapBanner)(NSInteger roomID); - -#pragma mark - 类方法 -+ (instancetype)createWithAttachment:(AttachmentModel *)attachment - inRoomUid:(NSInteger)roomUid - complete:(void(^)(void))complete - exitCurrentRoom:(void(^)(void))exit; - -+ (void)display:(UIView *)superView - with:(AttachmentModel *)attachment - complete:(void(^)(void))complete; - -+ (void)display:(UIView *)superView - inRoomUid:(NSInteger)roomUid - with:(AttachmentModel *)attachment - complete:(void(^)(void))complete -exitCurrentRoom:(void(^)(void))exit; - -#pragma mark - 实例方法 -- (void)setupWithAttachment:(AttachmentModel *)attachment; -- (void)showInSuperView:(UIView *)superView; -- (void)performShowAnimation; -- (void)performHideAnimation; - -#pragma mark - 子类重写方法 -- (Class)viewModelClass; // 返回对应的ViewModel类 -- (void)setupUI; // 设置UI布局 -- (void)configureWithModel:(PIBaseModel *)model; // 配置数据 -- (void)customizeAnimation; // 自定义动画 -- (void)handleTapAction; // 处理点击事件 - -@end -``` - -### 抽象优势分析 - -#### 1. 代码复用率提升 - -- 消除重复的动画逻辑 -- 统一的数据处理流程 -- 共享的UI组件管理 - -#### 2. 维护成本降低 - -- 集中管理动画参数 -- 统一的回调处理机制 -- 标准化的生命周期管理 - -#### 3. 扩展性增强 - -- 新增飘屏类型只需继承基类 -- 便于添加通用功能(如埋点、性能监控) -- 支持主题切换等全局功能 - -#### 4. 一致性保证 - -- 统一的动画效果和时长 -- 标准化的交互行为 -- 一致的视觉表现 - -### 实现策略 - -#### 阶段一:创建基类 - -1. 提取共同属性和方法 -2. 实现通用动画逻辑 -3. 定义子类接口规范 - -#### 阶段二:重构现有类 - -1. 逐步迁移现有Banner类继承基类 -2. 移除重复代码 -3. 保持向后兼容 - -#### 阶段三:优化和扩展 - -1. 添加性能监控 -2. 实现主题支持 -3. 统一埋点逻辑 - -### 潜在挑战 - -#### 1. 兼容性问题 - -- 现有代码的重构风险 -- 不同Banner的特殊需求差异 -- 第三方依赖的适配 - -#### 2. 性能考虑 - -- 基类功能过于复杂可能影响性能 -- 内存占用的优化 -- 动画性能的平衡 - -#### 3. 维护复杂度 - -- 基类变更可能影响所有子类 -- 需要详细的文档和测试 -- 团队学习成本 - -## 结论 - -**建议进行抽象**:项目中的飘屏组件具有高度的相似性,抽象出基类能够显著提升代码质量和开发效率。建议采用渐进式重构策略,先创建基类和接口规范,再逐步迁移现有实现,确保系统稳定性的同时实现架构优化。 - -## 优先级建议 - -1. **高优先级**:创建 `BaseRoomBannerView` 基类 -2. **中优先级**:重构使用频率最高的Banner类 -3. **低优先级**:添加扩展功能和优化 - -通过这种抽象设计,能够在保持现有功能完整性的同时,为未来的功能扩展和维护奠定良好的架构基础。 diff --git a/QEmotionBoardView_问题分析报告.md b/QEmotionBoardView_问题分析报告.md deleted file mode 100644 index 8e6ff823..00000000 --- a/QEmotionBoardView_问题分析报告.md +++ /dev/null @@ -1,143 +0,0 @@ -# QEmotionBoardView emoji 不显示问题分析报告 - -## 问题描述 -QEmotionBoardView 表情面板中的 emoji 表情无法正常显示,表情面板显示空白。 - -## 代码逻辑梳理 - -### 整体架构 -QEmotionBoardView 是一个表情面板组件,主要包含以下几个核心类: - -1. **QEmotionBoardView**: 主视图控制器,负责整体布局和事件处理 -2. **UIEmotionPageView**: 表情页面视图,负责单个页面的表情渲染和交互 -3. **UIEmotionVerticalScrollView**: 垂直滚动视图,包装页面视图 -4. **QEmotion**: 表情数据模型 -5. **QEmotionHelper**: 表情管理工具类 - -### 核心流程 - -#### 1. 表情数据加载流程 -``` -AppDelegate.initEmojiData() -→ 读取 emoji.plist -→ 创建 QEmotion 对象数组 -→ 设置到 QEmotionHelper.emotionArray -``` - -#### 2. 表情面板显示流程 -``` -QEmotionBoardView 初始化 -→ 设置 emotions 属性 -→ layoutSubviews 触发布局 -→ UIEmotionVerticalScrollView.setEmotions -→ UIEmotionPageView.layoutEmotionsIfNeeded -→ 创建 CALayer 显示表情图片 -``` - -#### 3. 表情渲染机制 -- 使用 `CALayer` 而不是 `UIImageView` 来渲染表情,提高性能 -- 通过 `emotionlayer.contents = (__bridge id)(self.emotions[i].image.CGImage)` 设置图片 -- 计算每个表情的位置和点击区域 - -## 问题根因分析 - -### 主要问题:图片加载错误 - -在 `AppDelegate+ThirdConfig.m` 的 `initEmojiData` 方法中存在一个严重的 bug: - -**错误代码:** -```objective-c -UIImage * image = [UIImage imageNamed:dic[@"file"]]; -``` - -**问题分析:** -1. `dic[@"file"]` 指向的是外层字典的 `file` 字段,但这个字段在 plist 中并不存在 -2. 所有表情都尝试加载同一个不存在的图片文件 -3. 导致所有 `QEmotion.image` 都为 `nil` -4. 在 `QEmotionBoardView` 中,`emotionlayer.contents` 被设置为 `nil`,所以表情不显示 - -**正确代码:** -```objective-c -NSDictionary * emotionDic = [emojiArray xpSafeObjectAtIndex:i]; -UIImage * image = [UIImage imageNamed:emotionDic[@"file"]]; -``` - -## 解决方案 - -### 1. 修复图片加载逻辑 - -已修复 `AppDelegate+ThirdConfig.m` 中的图片加载逻辑: - -```objective-c -for (int i = 0; i < emojiArray.count; i++) { - NSDictionary * emotionDic = [emojiArray xpSafeObjectAtIndex:i]; - if (!emotionDic) continue; - - UIImage * image = [UIImage imageNamed:emotionDic[@"file"]]; - QEmotion * info = [[QEmotion alloc] init]; - - info.displayName = emotionDic[@"tag"]; - info.identifier = emotionDic[@"id"]; - info.image = image; - - [array addObject:info]; -} -``` - -### 2. 添加调试代码 - -在关键位置添加了调试日志: - -**AppDelegate 中:** -```objective-c -// 添加调试日志 -NSLog(@"加载表情: %@, 图片: %@, 是否成功: %@", info.displayName, emotionDic[@"file"], image ? @"是" : @"否"); -``` - -**QEmotionBoardView 中:** -```objective-c -// 添加调试日志 -NSLog(@"QEmotionBoardView 设置表情数组,数量: %lu", (unsigned long)emotions.count); -for (int i = 0; i < MIN(emotions.count, 5); i++) { - QEmotion *emotion = emotions[i]; - NSLog(@"表情 %d: %@, 图片: %@", i, emotion.displayName, emotion.image ? @"存在" : @"不存在"); -} -``` - -**表情渲染中:** -```objective-c -// 添加调试日志 -if (i < 3) { // 只打印前3个表情的调试信息 - NSLog(@"渲染表情 %d: %@, 图片: %@, CGImage: %@", i, self.emotions[i].displayName, - self.emotions[i].image ? @"存在" : @"不存在", - self.emotions[i].image.CGImage ? @"存在" : @"不存在"); -} -``` - -### 3. 验证图片文件 - -确认表情图片文件已正确添加到 Xcode 项目中: -- 图片文件位于 `YuMi/CustomUI/InputView/Emoji/` 目录 -- 文件名格式为 `emoji_XX@2x.png` -- plist 中配置的文件名为 `emoji_XX.png`(iOS 会自动处理 @2x 后缀) -- 所有图片文件都已添加到项目的 Bundle Resources 中 - -## 测试验证 - -修复后,可以通过以下方式验证: - -1. **查看控制台日志**:运行应用后查看控制台输出,确认: - - 表情数组加载完成,数量正确 - - 每个表情的图片加载成功 - - QEmotionBoardView 正确设置表情数组 - - 表情渲染时图片和 CGImage 都存在 - -2. **功能测试**: - - 打开表情面板,确认表情正常显示 - - 点击表情,确认能正常插入到输入框 - - 测试删除和发送功能 - -## 总结 - -问题的根本原因是 AppDelegate 中图片加载逻辑的错误,导致所有表情的图片都为 nil。修复后,表情应该能正常显示。如果仍有问题,可以通过添加的调试日志进一步排查。 - diff --git a/QEmotionBoardView_问题排查报告.md b/QEmotionBoardView_问题排查报告.md deleted file mode 100644 index eb2a4219..00000000 --- a/QEmotionBoardView_问题排查报告.md +++ /dev/null @@ -1,175 +0,0 @@ -# QEmotionBoardView emoji 不显示问题排查报告 - -## 问题现状 - -- 表情数组已成功加载(125个表情) -- 表情图片文件存在且正确 -- 但表情面板仍然不显示 emoji - -## 排查过程 - -### 1. 数据流验证 - -已添加调试代码验证数据流: - -- AppDelegate 中表情数组加载完成 -- QEmotionBoardView 设置表情数组 -- UIEmotionVerticalScrollView 接收表情数据 -- UIEmotionPageView 接收表情数据 - -### 2. 关键问题发现 - -#### 问题1:UIEmotionVerticalScrollView bounds 可能为0 - -在 `UIEmotionVerticalScrollView.setEmotions` 方法中: - -```objective-c -CGSize contentSize = CGSizeMake(self.bounds.size.width - [self edgeInsetsGetHorizontalValue:paddingInPage], - self.bounds.size.height - [self edgeInsetsGetVerticalValue:paddingInPage]); -``` - -**问题分析:** - -- 当调用 `setEmotions` 时,UIEmotionVerticalScrollView 可能还没有正确的 bounds -- 如果 bounds 为 0,contentSize 也会为 0 -- 导致 `emotionCountPerRow` 计算错误 -- 最终影响表情的布局和显示 - -#### 问题2:布局时机问题 - -UIEmotionVerticalScrollView 的 frame 设置时机可能有问题: - -```objective-c -CGSize size = [pageView verticalSizeThatFits:self.bounds.size emotionVerticalSpacing:emotionVerticalSpacing]; -self.pageView.frame = CGRectMake(0, 0, size.width, size.height); -``` - -**问题分析:** - -- 如果 `self.bounds.size` 为 0,计算出的 size 也会有问题 -- pageView 的 frame 可能设置不正确 -- 导致表情无法正确显示 - -## 解决方案 - -### 方案1:延迟设置表情数据 - -在 QEmotionBoardView 的 layoutSubviews 中设置表情数据,确保 bounds 已经正确: - -```objective-c -- (void)layoutSubviews { - [super layoutSubviews]; - - // 确保 bounds 已经设置 - if (CGRectIsEmpty(self.bounds)) { - return; - } - - // 设置表情数据 - if (self.emotions && self.emotions.count > 0) { - [self.verticalScrollView setEmotions:self.emotions - emotionSize:self.emotionSize - minimumEmotionHorizontalSpacing:self.minimumEmotionHorizontalSpacing - emotionVerticalSpacing:self.emotionVerticalSpacing - emotionSelectedBackgroundExtension:self.emotionSelectedBackgroundExtension - paddingInPage:self.paddingInPage]; - } - - // 其他布局代码... -} -``` - -### 方案2:修复 UIEmotionVerticalScrollView 的 bounds 问题 - -在 setEmotions 方法中添加 bounds 检查: - -```objective-c -- (void)setEmotions:(NSArray *)emotions - emotionSize:(CGSize)emotionSize - minimumEmotionHorizontalSpacing:(CGFloat)minimumEmotionHorizontalSpacing - emotionVerticalSpacing:(CGFloat)emotionVerticalSpacing - emotionSelectedBackgroundExtension:(UIEdgeInsets)emotionSelectedBackgroundExtension - paddingInPage:(UIEdgeInsets)paddingInPage { - - // 检查 bounds 是否有效 - if (CGRectIsEmpty(self.bounds)) { - NSLog(@"UIEmotionVerticalScrollView bounds 为空,延迟设置表情"); - // 延迟到下一个运行循环 - dispatch_async(dispatch_get_main_queue(), ^{ - [self setEmotions:emotions - emotionSize:emotionSize - minimumEmotionHorizontalSpacing:minimumEmotionHorizontalSpacing - emotionVerticalSpacing:emotionVerticalSpacing -emotionSelectedBackgroundExtension:emotionSelectedBackgroundExtension - paddingInPage:paddingInPage]; - }); - return; - } - - // 原有的设置逻辑... -} -``` - -### 方案3:使用固定尺寸 - -如果 bounds 问题无法解决,可以使用固定的 contentSize: - -```objective-c -- (void)setEmotions:(NSArray *)emotions - emotionSize:(CGSize)emotionSize - minimumEmotionHorizontalSpacing:(CGFloat)minimumEmotionHorizontalSpacing - emotionVerticalSpacing:(CGFloat)emotionVerticalSpacing - emotionSelectedBackgroundExtension:(UIEdgeInsets)emotionSelectedBackgroundExtension - paddingInPage:(UIEdgeInsets)paddingInPage { - - UIEmotionPageView *pageView = self.pageView; - pageView.emotions = emotions; - pageView.padding = paddingInPage; - - // 使用父视图的宽度作为基准 - CGFloat availableWidth = self.superview ? self.superview.bounds.size.width : 320; - CGSize contentSize = CGSizeMake(availableWidth - [self edgeInsetsGetHorizontalValue:paddingInPage], - self.bounds.size.height - [self edgeInsetsGetVerticalValue:paddingInPage]); - - // 其他逻辑... -} -``` - -## 调试建议 - -### 1. 运行应用并查看日志 - -运行应用后,查看控制台输出,特别关注: - -- UIEmotionVerticalScrollView bounds 是否为 0 -- contentSize 计算是否正确 -- emotionCountPerRow 和 numberOfRows 的值 - -### 2. 检查布局时机 - -在 Xcode 中使用 View Debugger 检查: - -- QEmotionBoardView 的 frame 是否正确 -- UIEmotionVerticalScrollView 的 frame 是否正确 -- UIEmotionPageView 的 frame 是否正确 - -### 3. 验证表情渲染 - -检查 CALayer 是否正确创建: - -- emotionLayers 数组是否有内容 -- 每个 layer 的 frame 是否正确 -- layer 的 contents 是否设置 - -## 预期结果 - -修复后,应该能看到: - -1. 控制台日志显示正确的 bounds 和 contentSize -2. 表情面板正常显示 emoji -3. 表情可以正常点击和选择 - -## 总结 - -主要问题是 UIEmotionVerticalScrollView 在设置表情数据时,bounds 可能还没有正确设置,导致布局计算错误。通过延迟设置或使用有效的尺寸基准可以解决这个问题。 - diff --git a/docs/AttachmentModel_Analysis_Report.md b/docs/AttachmentModel_Analysis_Report.md deleted file mode 100644 index 412d1343..00000000 --- a/docs/AttachmentModel_Analysis_Report.md +++ /dev/null @@ -1,426 +0,0 @@ -# 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 - -@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)decodeAttachment:(NSString *)content { - id 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 *)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月 -**维护人员**: 开发团队 - - - - - diff --git a/docs/Banner手势优化实施总结.md b/docs/Banner手势优化实施总结.md deleted file mode 100644 index 793b5160..00000000 --- a/docs/Banner手势优化实施总结.md +++ /dev/null @@ -1,248 +0,0 @@ -# Banner手势优化实施总结 - -## 概述 -本文档记录了在 `RoomAnimationView.m` 中对 banner 手势系统的优化实施过程。 - -## 最新优化内容(2025年1月) - -### 需求描述 -1. **bannerContainer 手势范围调整**: - - 中央宽度 2/3 的位置:保留 swipe 手势 - - 左右两侧各 1/6 宽度:添加 tap 手势 - -2. **tap 手势处理逻辑**: - - 检查当前显示的 banner 是否在 tap 位置可以响应事件 - - 如果可以响应:不处理,让 banner 继续原有逻辑 - - 如果不能响应:保存 tap 位置点,供后续使用 - -### 实施方案 - -#### 1. 手势识别器重新设计 -```objc -- (void)addBnnerContainGesture { - // 创建独立的手势容器,避免与XPRoomAnimationHitView的hitTest冲突 - [self insertSubview:self.bannerSwipeGestureContainer aboveSubview:self.bannerContainer]; - [self insertSubview:self.bannerLeftTapGestureContainer aboveSubview:self.bannerContainer]; - [self insertSubview:self.bannerRightTapGestureContainer aboveSubview:self.bannerContainer]; - - // 设置手势容器的布局约束 - [self.bannerSwipeGestureContainer mas_makeConstraints:^(MASConstraintMaker *make) { - make.center.mas_equalTo(self.bannerContainer); - make.top.bottom.mas_equalTo(self.bannerContainer); - make.width.mas_equalTo(self.bannerContainer.mas_width).multipliedBy(2.0/3.0); - }]; - - [self.bannerLeftTapGestureContainer mas_makeConstraints:^(MASConstraintMaker *make) { - make.top.leading.bottom.mas_equalTo(self.bannerContainer); - make.trailing.mas_equalTo(self.bannerSwipeGestureContainer.mas_leading); - }]; - - [self.bannerRightTapGestureContainer mas_makeConstraints:^(MASConstraintMaker *make) { - make.top.trailing.bottom.mas_equalTo(self.bannerContainer); - make.leading.mas_equalTo(self.bannerSwipeGestureContainer.mas_trailing); - }]; - - // 创建中央区域的 swipe 手势(2/3 宽度) - UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self - action:@selector(handleSwipe)]; - if (isMSRTL()) { - swipe.direction = UISwipeGestureRecognizerDirectionRight; - } else { - swipe.direction = UISwipeGestureRecognizerDirectionLeft; - } - swipe.delegate = self; - - // 创建左侧区域的 tap 手势(1/6 宽度) - UITapGestureRecognizer *leftTap = [[UITapGestureRecognizer alloc] initWithTarget:self - action:@selector(handleBannerTap:)]; - leftTap.delegate = self; - - // 创建右侧区域的 tap 手势(1/6 宽度) - UITapGestureRecognizer *rightTap = [[UITapGestureRecognizer alloc] initWithTarget:self - action:@selector(handleBannerTap:)]; - rightTap.delegate = self; - - // 添加手势识别器到对应的手势容器 - [self.bannerSwipeGestureContainer addGestureRecognizer:swipe]; - [self.bannerLeftTapGestureContainer addGestureRecognizer:leftTap]; - [self.bannerRightTapGestureContainer addGestureRecognizer:rightTap]; -} -``` - -#### 2. 区域划分逻辑 -```objc -- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { - CGPoint touchPoint = [touch locationInView:self.bannerContainer]; - CGFloat containerWidth = self.bannerContainer.bounds.size.width; - - // 计算区域边界 - CGFloat leftBoundary = containerWidth / 6.0; // 1/6 宽度 - CGFloat rightBoundary = containerWidth * 5.0 / 6.0; // 5/6 宽度 - - if ([gestureRecognizer isKindOfClass:[UISwipeGestureRecognizer class]]) { - // Swipe 手势只在中央 2/3 区域生效 - return touchPoint.x >= leftBoundary && touchPoint.x <= rightBoundary; - } else if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) { - // Tap 手势只在左右两侧 1/6 区域生效 - return touchPoint.x < leftBoundary || touchPoint.x > rightBoundary; - } - - return YES; -} -``` - -#### 3. Tap 手势处理逻辑 -```objc -- (void)handleBannerTap:(UITapGestureRecognizer *)tapGesture { - CGPoint tapPoint = [tapGesture locationInView:self.bannerContainer]; - - // 检查当前显示的 banner 是否在 tap 位置可以响应事件 - if ([self isPointInBannerInteractiveArea:tapPoint]) { - // banner 可以响应,不处理,让 banner 继续原有逻辑 - NSLog(@"🎯 Banner tap 位置在可交互区域,banner 将处理此事件"); - return; - } else { - // banner 不能响应,保存 tap 位置 - self.savedTapPoint = tapPoint; - self.hasSavedTapPoint = YES; - NSLog(@"💾 Banner tap 位置不在可交互区域,已保存位置: %@", NSStringFromCGPoint(tapPoint)); - } -} -``` - -#### 4. Banner 交互区域检查 -```objc -- (BOOL)isPointInBannerInteractiveArea:(CGPoint)point { - // 检查当前显示的 banner 是否在指定位置可以响应事件 - for (UIView *subview in self.bannerContainer.subviews) { - if (subview.hidden || subview.alpha <= 0.01) { - continue; - } - - // 检查点是否在子视图范围内 - if (CGRectContainsPoint(subview.bounds, point)) { - // 检查子视图是否支持用户交互 - if (subview.userInteractionEnabled) { - // 进一步检查子视图是否有可点击的元素 - CGPoint subviewPoint = [subview convertPoint:point fromView:self.bannerContainer]; - UIView *hitView = [subview hitTest:subviewPoint withEvent:nil]; - if (hitView && hitView.userInteractionEnabled) { - return YES; - } - } - } - } - return NO; -} -``` - -#### 5. 公共接口方法 -```objc -// 获取保存的 tap 位置 -- (CGPoint)getSavedTapPoint; - -// 检查是否有保存的 tap 位置 -- (BOOL)hasSavedTapPointAvailable; - -// 清除保存的 tap 位置 -- (void)clearSavedTapPoint; -``` - -### 新增属性 -```objc -// Banner 手势相关属性 -@property(nonatomic, assign) CGPoint savedTapPoint; -@property(nonatomic, assign) BOOL hasSavedTapPoint; - -// 手势容器(使用普通UIView避免XPRoomAnimationHitView的hitTest冲突) -@property(nonatomic, strong) UIView *bannerSwipeGestureContainer; -@property(nonatomic, strong) UIView *bannerLeftTapGestureContainer; -@property(nonatomic, strong) UIView *bannerRightTapGestureContainer; -``` - -### 协议支持 -- 添加了 `UIGestureRecognizerDelegate` 协议支持 -- 实现了手势识别器的 delegate 方法 - -## 技术特点 - -### 1. 精确的区域控制 -- 使用独立的手势容器精确划分区域 -- 中央 2/3 区域:swipe 手势容器 -- 左右两侧各 1/6 区域:tap 手势容器 - -### 2. 避免手势冲突 -- 使用普通 `UIView` 作为手势容器,避免 `XPRoomAnimationHitView` 的 `hitTest` 冲突 -- 手势容器独立于 banner 内容,确保手势识别不受干扰 - -### 3. 智能的事件处理 -- 检查 banner 是否在 tap 位置可响应 -- 自动判断是否需要保存 tap 位置 -- 避免与 banner 原有交互逻辑冲突 - -### 4. 灵活的接口设计 -- 提供公共方法获取保存的 tap 位置 -- 支持清除保存的位置 -- 便于外部代码使用 - -### 5. 完善的日志记录 -- 详细记录手势处理过程 -- 便于调试和问题排查 - -## 使用示例 - -```objc -// 检查是否有保存的 tap 位置 -if ([roomAnimationView hasSavedTapPointAvailable]) { - CGPoint savedPoint = [roomAnimationView getSavedTapPoint]; - NSLog(@"保存的 tap 位置: %@", NSStringFromCGPoint(savedPoint)); - - // 使用保存的位置进行后续处理 - // ... - - // 清除保存的位置 - [roomAnimationView clearSavedTapPoint]; -} -``` - -## 注意事项 - -1. **手势容器设计**:使用普通 `UIView` 作为手势容器,避免 `XPRoomAnimationHitView` 的 `hitTest` 冲突 -2. **区域划分**:通过独立的视图容器精确划分手势区域,确保手势识别不受干扰 -3. **交互检查**:通过 `hitTest` 方法检查子视图的实际可交互性 -4. **内存管理**:及时清除不需要的 tap 位置数据 -5. **调试支持**:在 DEBUG 模式下为手势容器添加背景色,便于调试区域划分 - -## 测试建议 - -1. **区域划分测试**: - - 在中央区域测试 swipe 手势 - - 在左右两侧测试 tap 手势 - - 验证手势在错误区域不触发 - -2. **交互逻辑测试**: - - 在有可交互 banner 的区域 tap - - 在无可交互 banner 的区域 tap - - 验证 tap 位置的保存和清除 - -3. **边界条件测试**: - - 测试不同屏幕尺寸下的区域划分 - - 测试 RTL 语言环境下的手势方向 - - 测试多个 banner 同时显示的情况 - -## 总结 - -本次优化成功实现了: -- ✅ bannerContainer 手势范围的精确划分 -- ✅ 智能的 tap 手势处理逻辑 -- ✅ 灵活的 tap 位置保存机制 -- ✅ 完善的公共接口设计 -- ✅ 与现有代码的良好兼容性 -- ✅ 解决了 XPRoomAnimationHitView 的手势冲突问题 - -### 关键改进 -1. **避免手势冲突**:使用普通 `UIView` 作为手势容器,避免 `XPRoomAnimationHitView` 的 `hitTest` 方法干扰 -2. **精确区域控制**:通过独立的视图容器实现精确的手势区域划分 -3. **调试友好**:在 DEBUG 模式下为手势容器添加背景色,便于调试 - -该方案既满足了新的功能需求,又解决了潜在的手势冲突问题,保持了代码的可维护性和扩展性。 diff --git a/docs/Email-VerificationCode-Login-Flow.md b/docs/Email-VerificationCode-Login-Flow.md deleted file mode 100644 index 2c23fd91..00000000 --- a/docs/Email-VerificationCode-Login-Flow.md +++ /dev/null @@ -1,434 +0,0 @@ -# 邮箱验证码登录流程文档 - -## 概述 - -本文档详细描述了 YuMi iOS 应用中 `LoginTypesViewController` 在 `LoginDisplayType_email` 模式下的邮箱验证码登录流程。该流程实现了基于邮箱和验证码的用户认证机制。 - -## 系统架构 - -### 核心组件 -- **LoginTypesViewController**: 登录类型控制器,负责 UI 展示和用户交互 -- **LoginPresenter**: 登录业务逻辑处理器,负责与 API 交互 -- **LoginInputItemView**: 输入组件,提供邮箱和验证码输入界面 -- **Api+Login**: 登录相关 API 接口封装 -- **AccountInfoStorage**: 账户信息本地存储管理 - -### 数据模型 - -#### LoginDisplayType 枚举 -```objc -typedef NS_ENUM(NSUInteger, LoginDisplayType) { - LoginDisplayType_id, // ID 登录 - LoginDisplayType_email, // 邮箱登录 ✓ - LoginDisplayType_phoneNum, // 手机号登录 - LoginDisplayType_email_forgetPassword, // 邮箱忘记密码 - LoginDisplayType_phoneNum_forgetPassword, // 手机号忘记密码 -}; -``` - -#### LoginInputType 枚举 -```objc -typedef NS_ENUM(NSUInteger, LoginInputType) { - LoginInputType_email, // 邮箱输入 - LoginInputType_verificationCode, // 验证码输入 - LoginInputType_login, // 登录按钮 - // ... 其他类型 -}; -``` - -#### GetSmsType 验证码类型 -```objc -typedef NS_ENUM(NSUInteger, GetSmsType) { - GetSmsType_Regist = 1, // 注册(邮箱登录使用此类型) - GetSmsType_Login = 2, // 登录 - GetSmsType_Reset_Password = 3, // 重设密码 - // ... 其他类型 -}; -``` - -## 登录流程详解 - -### 1. 界面初始化流程 - -#### 1.1 控制器初始化 -```objc -// 在 LoginViewController 中点击邮箱登录按钮 -- (void)didTapEntrcyButton:(UIButton *)sender { - if (sender.tag == LoginType_Email) { - LoginTypesViewController *vc = [[LoginTypesViewController alloc] init]; - [self.navigationController pushViewController:vc animated:YES]; - [vc updateLoginType:LoginDisplayType_email]; // 设置为邮箱登录模式 - } -} -``` - -#### 1.2 输入区域设置 -```objc -- (void)setupEmailInputArea { - [self setupInpuArea:LoginInputType_email // 第一行:邮箱输入 - second:LoginInputType_verificationCode // 第二行:验证码输入 - third:LoginInputType_none // 第三行:无 - action:LoginInputType_login // 操作按钮:登录 - showForgetPassword:NO]; // 不显示忘记密码 -} -``` - -#### 1.3 UI 组件配置 -- **第一行输入框**: 邮箱地址输入 - - 占位符: "请输入邮箱地址" - - 键盘类型: `UIKeyboardTypeEmailAddress` - - 回调: `handleFirstInputContentUpdate` - -- **第二行输入框**: 验证码输入 - - 占位符: "请输入验证码" - - 键盘类型: `UIKeyboardTypeDefault` - - 附带"获取验证码"按钮 - - 回调: `handleSecondInputContentUpdate` - -### 2. 验证码获取流程 - -#### 2.1 用户交互触发 -```objc -// 用户点击"获取验证码"按钮 -[self.secondLineInputView setHandleItemAction:^(LoginInputType inputType) { - if (inputType == LoginInputType_verificationCode) { - if (self.type == LoginDisplayType_email) { - [self handleTapGetMailVerificationCode]; - } - } -}]; -``` - -#### 2.2 邮箱验证码获取处理 -```objc -- (void)handleTapGetMailVerificationCode { - NSString *email = [self.firstLineInputView inputContent]; - - // 邮箱地址验证 - if (email.length == 0) { - [self.secondLineInputView endVerificationCountDown]; - return; - } - - // 调用 Presenter 发送验证码 - [self.presenter sendMailVerificationCode:email type:GetSmsType_Regist]; -} -``` - -#### 2.3 Presenter 层处理 -```objc -- (void)sendMailVerificationCode:(NSString *)emailAddress type:(NSInteger)type { - // DES 加密邮箱地址 - NSString *desEmail = [DESEncrypt encryptUseDES:emailAddress - key:KeyWithType(KeyType_PasswordEncode)]; - - @kWeakify(self); - [Api emailGetCode:[self createHttpCompletion:^(BaseModel *data) { - @kStrongify(self); - if ([[self getView] respondsToSelector:@selector(emailCodeSucess:type:)]) { - [[self getView] emailCodeSucess:@"" type:type]; - } - } fail:^(NSInteger code, NSString *msg) { - @kStrongify(self); - if ([[self getView] respondsToSelector:@selector(emailCodeFailure)]) { - [[self getView] emailCodeFailure]; - } - } showLoading:YES errorToast:YES] - emailAddress:desEmail - type:@(type)]; -} -``` - -#### 2.4 API 接口调用 -```objc -+ (void)emailGetCode:(HttpRequestHelperCompletion)completion - emailAddress:(NSString *)emailAddress - type:(NSNumber *)type { - [self makeRequest:@"email/getCode" - method:HttpRequestHelperMethodPOST - completion:completion, __FUNCTION__, emailAddress, type, nil]; -} -``` - -**API 详情**: -- **接口路径**: `POST /email/getCode` -- **请求参数**: - - `emailAddress`: 邮箱地址(DES 加密) - - `type`: 验证码类型(1=注册) - -#### 2.5 获取验证码成功处理 -```objc -- (void)emailCodeSucess:(NSString *)message type:(GetSmsType)type { - [self showSuccessToast:YMLocalizedString(@"XPLoginPhoneViewController2")]; // "验证码已发送" - [self.secondLineInputView startVerificationCountDown]; // 开始倒计时 - [self.secondLineInputView displayKeyboard]; // 显示键盘 -} -``` - -#### 2.6 获取验证码失败处理 -```objc -- (void)emailCodeFailure { - [self.secondLineInputView endVerificationCountDown]; // 结束倒计时 -} -``` - -### 3. 邮箱登录流程 - -#### 3.1 登录按钮状态检查 -```objc -- (void)checkActionButtonStatus { - switch (self.type) { - case LoginDisplayType_email: { - NSString *accountString = [self.firstLineInputView inputContent]; // 邮箱 - NSString *codeString = [self.secondLineInputView inputContent]; // 验证码 - - // 只有当邮箱和验证码都不为空时才启用登录按钮 - if (![NSString isEmpty:accountString] && ![NSString isEmpty:codeString]) { - self.bottomActionButton.enabled = YES; - } else { - self.bottomActionButton.enabled = NO; - } - } - break; - } -} -``` - -#### 3.2 登录按钮点击处理 -```objc -- (void)didTapActionButton { - [self.view endEditing:true]; - - switch (self.type) { - case LoginDisplayType_email: { - // 调用 Presenter 进行邮箱登录 - [self.presenter loginWithEmail:[self.firstLineInputView inputContent] - code:[self.secondLineInputView inputContent]]; - } - break; - } -} -``` - -#### 3.3 Presenter 层登录处理 -```objc -- (void)loginWithEmail:(NSString *)email code:(NSString *)code { - // DES 加密邮箱地址 - NSString *desMail = [DESEncrypt encryptUseDES:email - key:KeyWithType(KeyType_PasswordEncode)]; - - @kWeakify(self); - [Api loginWithCode:[self createHttpCompletion:^(BaseModel *data) { - @kStrongify(self); - - // 解析账户模型 - AccountModel *accountModel = [AccountModel modelWithDictionary:data.data]; - - // 保存账户信息 - if (accountModel && accountModel.access_token.length > 0) { - [[AccountInfoStorage instance] saveAccountInfo:accountModel]; - } - - // 通知登录成功 - if ([[self getView] respondsToSelector:@selector(loginSuccess)]) { - [[self getView] loginSuccess]; - } - } fail:^(NSInteger code, NSString *msg) { - @kStrongify(self); - [[self getView] loginFailWithMsg:msg]; - } errorToast:NO] - email:desMail - code:code - client_secret:clinet_s // 客户端密钥 - version:@"1" - client_id:@"erban-client" - grant_type:@"email"]; // 邮箱登录类型 -} -``` - -#### 3.4 API 接口调用 -```objc -+ (void)loginWithCode:(HttpRequestHelperCompletion)completion - email:(NSString *)email - code:(NSString *)code - client_secret:(NSString *)client_secret - version:(NSString *)version - client_id:(NSString *)client_id - grant_type:(NSString *)grant_type { - - NSString *fang = [NSString stringFromBase64String:@"b2F1dGgvdG9rZW4="]; // oauth/token - [self makeRequest:fang - method:HttpRequestHelperMethodPOST - completion:completion, __FUNCTION__, email, code, client_secret, - version, client_id, grant_type, nil]; -} -``` - -**API 详情**: -- **接口路径**: `POST /oauth/token` -- **请求参数**: - - `email`: 邮箱地址(DES 加密) - - `code`: 验证码 - - `client_secret`: 客户端密钥 - - `version`: 版本号 "1" - - `client_id`: 客户端ID "erban-client" - - `grant_type`: 授权类型 "email" - -#### 3.5 登录成功处理 -```objc -- (void)loginSuccess { - [self showSuccessToast:YMLocalizedString(@"XPLoginPhoneViewController1")]; // "登录成功" - [PILoginManager loginWithVC:self isLoginPhone:NO]; // 执行登录后续处理 -} -``` - -#### 3.6 登录失败处理 -```objc -- (void)loginFailWithMsg:(NSString *)msg { - [self showSuccessToast:msg]; // 显示错误信息 -} -``` - -## 数据流时序图 - -```mermaid -sequenceDiagram - participant User as 用户 - participant VC as LoginTypesViewController - participant IV as LoginInputItemView - participant P as LoginPresenter - participant API as Api+Login - participant Storage as AccountInfoStorage - - Note over User,Storage: 1. 初始化邮箱登录界面 - User->>VC: 选择邮箱登录 - VC->>VC: updateLoginType(LoginDisplayType_email) - VC->>VC: setupEmailInputArea() - VC->>IV: 创建邮箱输入框 - VC->>IV: 创建验证码输入框 - - Note over User,Storage: 2. 获取邮箱验证码 - User->>IV: 输入邮箱地址 - User->>IV: 点击"获取验证码" - IV->>VC: handleTapGetMailVerificationCode - VC->>VC: 验证邮箱地址非空 - VC->>P: sendMailVerificationCode(email, GetSmsType_Regist) - P->>P: DES加密邮箱地址 - P->>API: emailGetCode(encryptedEmail, type=1) - API-->>P: 验证码发送结果 - P-->>VC: emailCodeSucess / emailCodeFailure - VC->>IV: startVerificationCountDown / endVerificationCountDown - VC->>User: 显示成功/失败提示 - - Note over User,Storage: 3. 邮箱验证码登录 - User->>IV: 输入验证码 - IV->>VC: 输入内容变化回调 - VC->>VC: checkActionButtonStatus() - VC->>User: 启用/禁用登录按钮 - User->>VC: 点击登录按钮 - VC->>VC: didTapActionButton() - VC->>P: loginWithEmail(email, code) - P->>P: DES加密邮箱地址 - P->>API: loginWithCode(email, code, ...) - API-->>P: OAuth Token 响应 - P->>P: 解析 AccountModel - P->>Storage: saveAccountInfo(accountModel) - P-->>VC: loginSuccess / loginFailWithMsg - VC->>User: 显示登录结果 - VC->>User: 跳转到主界面 -``` - -## 安全机制 - -### 1. 数据加密 -- **邮箱地址加密**: 使用 DES 算法加密邮箱地址后传输 -```objc -NSString *desEmail = [DESEncrypt encryptUseDES:email key:KeyWithType(KeyType_PasswordEncode)]; -``` - -### 2. 输入验证 -- **邮箱格式验证**: 通过 `UIKeyboardTypeEmailAddress` 键盘类型引导正确输入 -- **非空验证**: 邮箱和验证码都必须非空才能执行登录 - -### 3. 验证码安全 -- **时效性**: 验证码具有倒计时机制,防止重复获取 -- **类型标识**: 使用 `GetSmsType_Regist = 1` 标识登录验证码 - -### 4. 网络安全 -- **错误处理**: 完整的成功/失败回调机制 -- **加载状态**: `showLoading:YES` 防止重复请求 -- **错误提示**: `errorToast:YES` 显示网络错误 - -## 错误处理机制 - -### 1. 邮箱验证码获取错误 -```objc -- (void)emailCodeFailure { - [self.secondLineInputView endVerificationCountDown]; // 停止倒计时 - // 用户可以重新获取验证码 -} -``` - -### 2. 登录失败处理 -```objc -- (void)loginFailWithMsg:(NSString *)msg { - [self showSuccessToast:msg]; // 显示具体错误信息 - // 用户可以重新尝试登录 -} -``` - -### 3. 网络请求错误 -- **自动重试**: 用户可以手动重新点击获取验证码或登录 -- **错误提示**: 通过 Toast 显示具体错误信息 -- **状态恢复**: 失败后恢复按钮可点击状态 - -## 本地化支持 - -### 关键文本资源 -- `@"20.20.51_text_1"`: "邮箱登录" -- `@"20.20.51_text_4"`: "请输入邮箱地址" -- `@"20.20.51_text_7"`: "请输入验证码" -- `@"XPLoginPhoneViewController2"`: "验证码已发送" -- `@"XPLoginPhoneViewController1"`: "登录成功" - -### 多语言支持 -- 简体中文 (`zh-Hant.lproj`) -- 英文 (`en.lproj`) -- 阿拉伯语 (`ar.lproj`) -- 土耳其语 (`tr.lproj`) - -## 依赖组件 - -### 外部框架 -- **MASConstraintMaker**: 自动布局 -- **ReactiveObjC**: 响应式编程(部分组件使用) - -### 内部组件 -- **YMLocalizedString**: 本地化字符串管理 -- **DESEncrypt**: DES 加密工具 -- **AccountInfoStorage**: 账户信息存储 -- **HttpRequestHelper**: 网络请求管理 - -## 扩展和维护 - -### 新增功能建议 -1. **邮箱格式验证**: 添加正则表达式验证邮箱格式 -2. **验证码长度限制**: 限制验证码输入长度 -3. **自动填充**: 支持系统邮箱自动填充 -4. **记住邮箱**: 保存最近使用的邮箱地址 - -### 性能优化 -1. **请求去重**: 防止短时间内重复请求验证码 -2. **缓存机制**: 缓存验证码倒计时状态 -3. **网络优化**: 添加请求超时和重试机制 - -### 代码维护 -1. **常量管理**: 将硬编码字符串提取为常量 -2. **错误码统一**: 统一管理API错误码 -3. **日志记录**: 添加详细的操作日志 - -## 总结 - -邮箱验证码登录流程是一个完整的用户认证系统,包含了界面展示、验证码获取、用户登录、数据存储等完整环节。该流程具有良好的安全性、用户体验和错误处理机制,符合现代移动应用的认证标准。 - -通过本文档,开发人员可以全面了解邮箱登录的实现细节,便于后续的功能扩展和维护工作。 \ No newline at end of file diff --git a/docs/ImageUpload_API_Swift.md b/docs/ImageUpload_API_Swift.md deleted file mode 100644 index b28e6359..00000000 --- a/docs/ImageUpload_API_Swift.md +++ /dev/null @@ -1,63 +0,0 @@ -# 图片上传接口(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) -> 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(文件名)、body(NSData)。 -- 支持自定义域名、加速域名等高级配置。 - -## 6. 错误处理建议 - -- 网络异常、图片压缩失败、SDK 上传失败等需统一处理。 -- 建议统一封装 `UploadError` 类型。 - -## 7. 扩展建议 - -- 支持多图批量上传 -- 支持上传进度回调 -- 支持 async/await -- 可结合项目网络层统一封装 \ No newline at end of file diff --git a/docs/MomentsPublish_API_Swift.md b/docs/MomentsPublish_API_Swift.md deleted file mode 100644 index 14f214cc..00000000 --- a/docs/MomentsPublish_API_Swift.md +++ /dev/null @@ -1,69 +0,0 @@ -# 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 - ) { - // 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 -- 可扩展为支持更多动态类型 -- 可结合项目网络层统一封装 \ No newline at end of file diff --git a/docs/NIMSDKManager_Usage_Guide.md b/docs/NIMSDKManager_Usage_Guide.md deleted file mode 100644 index 59fc5257..00000000 --- a/docs/NIMSDKManager_Usage_Guide.md +++ /dev/null @@ -1,622 +0,0 @@ -# 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 () -@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 *)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 *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月 -**维护人员**: 开发团队 - - - - - diff --git a/docs/NIMSDK_Integration_Documentation.md b/docs/NIMSDK_Integration_Documentation.md deleted file mode 100644 index c3b74a98..00000000 --- a/docs/NIMSDK_Integration_Documentation.md +++ /dev/null @@ -1,559 +0,0 @@ -# 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架构模式。项目集成了网易云信SDK(NIMSDK)用于实现即时通讯功能。 - -### 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 -``` - -### 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 -@end -``` - -#### 5.1.2 解码器实现 - -```objc -// CustomAttachmentDecoder.m -- (id)decodeAttachment:(NSString *)content { - id 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 - -@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 *)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月 -**维护人员**: 开发团队 - - - - - diff --git a/docs/OAuth_Ticket_API_Documentation.md b/docs/OAuth_Ticket_API_Documentation.md deleted file mode 100644 index 081fe568..00000000 --- a/docs/OAuth_Ticket_API_Documentation.md +++ /dev/null @@ -1,262 +0,0 @@ -# OAuth/Ticket 认证系统 API 文档 - -## 概述 - -本文档描述了 YuMi 应用中 OAuth 认证和 Ticket 会话管理的完整流程。系统采用两阶段认证机制: -1. **OAuth 阶段**:用户登录获取 `access_token` -2. **Ticket 阶段**:使用 `access_token` 获取业务会话 `ticket` - -## 认证流程架构 - -### 核心组件 -- **AccountInfoStorage**: 负责账户信息和 ticket 的本地存储 -- **HttpRequestHelper**: 网络请求管理,自动添加认证头 -- **Api+Login**: 登录相关 API 接口 -- **Api+Main**: Ticket 获取相关 API 接口 - -### 认证数据模型 - -#### AccountModel -```objc -@interface AccountModel : PIBaseModel -@property (nonatomic, assign) NSString *uid; // 用户唯一标识 -@property (nonatomic, copy) NSString *jti; // JWT ID -@property (nonatomic, copy) NSString *token_type; // Token 类型 -@property (nonatomic, copy) NSString *refresh_token; // 刷新令牌 -@property (nonatomic, copy) NSString *netEaseToken; // 网易云信令牌 -@property (nonatomic, copy) NSString *access_token; // OAuth 访问令牌 -@property (nonatomic, assign) NSNumber *expires_in; // 过期时间 -@end -``` - -## API 接口详情 - -### 1. OAuth 登录接口 - -#### 1.1 手机验证码登录 -```objc -+ (void)loginWithCode:(HttpRequestHelperCompletion)completion - phone:(NSString *)phone - code:(NSString *)code - client_secret:(NSString *)client_secret - version:(NSString *)version - client_id:(NSString *)client_id - grant_type:(NSString *)grant_type - phoneAreaCode:(NSString *)phoneAreaCode; -``` - -**接口路径**: `POST /oauth/token` - -**请求参数**: -| 参数名 | 类型 | 必填 | 描述 | -|--------|------|------|------| -| phone | String | 是 | 手机号(DES加密) | -| code | String | 是 | 验证码 | -| client_secret | String | 是 | 客户端密钥,固定值:"uyzjdhds" | -| version | String | 是 | 版本号,固定值:"1" | -| client_id | String | 是 | 客户端ID,固定值:"erban-client" | -| grant_type | String | 是 | 授权类型,验证码登录为:"sms_code" | -| phoneAreaCode | String | 是 | 手机区号 | - -**返回数据**: AccountModel 对象 - -#### 1.2 手机密码登录 -```objc -+ (void)loginWithPassword:(HttpRequestHelperCompletion)completion - phone:(NSString *)phone - password:(NSString *)password - client_secret:(NSString *)client_secret - version:(NSString *)version - client_id:(NSString *)client_id - grant_type:(NSString *)grant_type; -``` - -**接口路径**: `POST /oauth/token` - -**请求参数**: -| 参数名 | 类型 | 必填 | 描述 | -|--------|------|------|------| -| phone | String | 是 | 手机号(DES加密) | -| password | String | 是 | 密码(DES加密) | -| client_secret | String | 是 | 客户端密钥 | -| version | String | 是 | 版本号 | -| client_id | String | 是 | 客户端ID | -| grant_type | String | 是 | 授权类型,密码登录为:"password" | - -#### 1.3 第三方登录 -```objc -+ (void)loginWithThirdPart:(HttpRequestHelperCompletion)completion - openid:(NSString *)openid - unionid:(NSString *)unionid - access_token:(NSString *)access_token - type:(NSString *)type; -``` - -**接口路径**: `POST /acc/third/login` - -**请求参数**: -| 参数名 | 类型 | 必填 | 描述 | -|--------|------|------|------| -| openid | String | 是 | 第三方平台用户唯一标识 | -| unionid | String | 是 | 第三方平台联合ID | -| access_token | String | 是 | 第三方平台访问令牌 | -| type | String | 是 | 第三方平台类型(1:Apple, 2:Facebook, 3:Google等) | - -### 2. Ticket 获取接口 - -#### 2.1 获取 Ticket -```objc -+ (void)requestTicket:(HttpRequestHelperCompletion)completion - access_token:(NSString *)accessToken - issue_type:(NSString *)issueType; -``` - -**接口路径**: `POST /oauth/ticket` - -**请求参数**: -| 参数名 | 类型 | 必填 | 描述 | -|--------|------|------|------| -| access_token | String | 是 | OAuth 登录获取的访问令牌 | -| issue_type | String | 是 | 签发类型,固定值:"multi" | - -**返回数据**: -```json -{ - "code": 200, - "data": { - "tickets": [ - { - "ticket": "eyJhbGciOiJIUzI1NiJ9..." - } - ] - } -} -``` - -### 3. HTTP 请求头配置 - -所有业务 API 请求都会自动添加以下请求头: - -```objc -// 在 HttpRequestHelper 中自动配置 -- (void)setupHeader { - AFHTTPSessionManager *client = [HttpRequestHelper requestManager]; - - // 用户ID头 - if ([[AccountInfoStorage instance] getUid].length > 0) { - [client.requestSerializer setValue:[[AccountInfoStorage instance] getUid] - forHTTPHeaderField:@"pub_uid"]; - } - - // Ticket 认证头 - if ([[AccountInfoStorage instance] getTicket].length > 0) { - [client.requestSerializer setValue:[[AccountInfoStorage instance] getTicket] - forHTTPHeaderField:@"pub_ticket"]; - } - - // 其他公共头 - [client.requestSerializer setValue:[NSBundle uploadLanguageText] - forHTTPHeaderField:@"Accept-Language"]; - [client.requestSerializer setValue:PI_App_Version - forHTTPHeaderField:@"App-Version"]; -} -``` - -## 使用流程 - -### 完整登录流程示例 - -```objc -// 1. 用户登录获取 access_token -[Api loginWithCode:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) { - if (code == 200) { - // 保存账户信息 - AccountModel *accountModel = [AccountModel modelWithDictionary:data.data]; - [[AccountInfoStorage instance] saveAccountInfo:accountModel]; - - // 2. 使用 access_token 获取 ticket - [Api requestTicket:^(BaseModel * _Nullable data, NSInteger code, NSString * _Nullable msg) { - if (code == 200) { - NSArray *tickets = [data.data valueForKey:@"tickets"]; - NSString *ticket = [tickets[0] valueForKey:@"ticket"]; - - // 保存 ticket - [[AccountInfoStorage instance] saveTicket:ticket]; - - // 3. 登录成功,可以进行业务操作 - [self navigateToMainPage]; - } - } access_token:accountModel.access_token issue_type:@"multi"]; - } -} phone:encryptedPhone - code:verificationCode - client_secret:@"uyzjdhds" - version:@"1" - client_id:@"erban-client" - grant_type:@"sms_code" - phoneAreaCode:areaCode]; -``` - -### 自动登录流程 - -```objc -- (void)autoLogin { - // 检查本地是否有账户信息 - AccountModel *accountModel = [[AccountInfoStorage instance] getCurrentAccountInfo]; - if (accountModel == nil || accountModel.access_token == nil) { - [self tokenInvalid]; // 跳转到登录页 - return; - } - - // 检查是否有有效的 ticket - if ([[AccountInfoStorage instance] getTicket].length > 0) { - [[self getView] autoLoginSuccess]; - return; - } - - // 使用 access_token 重新获取 ticket - [Api requestTicket:^(BaseModel * _Nonnull data) { - NSArray *tickets = [data.data valueForKey:@"tickets"]; - NSString *ticket = [tickets[0] valueForKey:@"ticket"]; - [[AccountInfoStorage instance] saveTicket:ticket]; - [[self getView] autoLoginSuccess]; - } fail:^(NSInteger code, NSString * _Nullable msg) { - [self logout]; // ticket 获取失败,重新登录 - } access_token:accountModel.access_token issue_type:@"multi"]; -} -``` - -## 错误处理 - -### 401 未授权错误 -当接收到 401 状态码时,系统会自动处理: - -```objc -// 在 HttpRequestHelper 中 -if (response && response.statusCode == 401) { - failure(response.statusCode, YMLocalizedString(@"HttpRequestHelper7")); - // 通常需要重新登录 -} -``` - -### Ticket 过期处理 -- Ticket 过期时服务器返回 401 错误 -- 客户端应该使用保存的 `access_token` 重新获取 ticket -- 如果 `access_token` 也过期,则需要用户重新登录 - -## 安全注意事项 - -1. **数据加密**: 敏感信息(手机号、密码)使用 DES 加密传输 -2. **本地存储**: - - `access_token` 存储在文件系统中 - - `ticket` 存储在内存中,应用重启需重新获取 -3. **请求头**: 所有业务请求自动携带 `pub_uid` 和 `pub_ticket` 头 -4. **错误处理**: 建立完善的 401 错误重试机制 - -## 相关文件 - -- `YuMi/Structure/MVP/Model/AccountInfoStorage.h/m` - 账户信息存储管理 -- `YuMi/Modules/YMLogin/Api/Api+Login.h/m` - 登录相关接口 -- `YuMi/Modules/YMTabbar/Api/Api+Main.h/m` - Ticket 获取接口 -- `YuMi/Network/HttpRequestHelper.h/m` - 网络请求管理 -- `YuMi/Structure/MVP/Model/AccountModel.h/m` - 账户数据模型 \ No newline at end of file diff --git a/docs/PIGiftBravoGiftBroadcastView_Animation_Fix.md b/docs/PIGiftBravoGiftBroadcastView_Animation_Fix.md deleted file mode 100644 index ac61e34d..00000000 --- a/docs/PIGiftBravoGiftBroadcastView_Animation_Fix.md +++ /dev/null @@ -1,130 +0,0 @@ -# PIGiftBravoGiftBroadcastView 动画速度修复 - -## 问题描述 -`PIGiftBravoGiftBroadcastView` 中内容切换的动画变得非常快,影响用户体验。 - -## 问题分析 - -### 1. 动画时序问题 -- **原有时序**: - - 入场动画:0.5秒 - - 停留时间:2.0秒延迟 - - 出场动画:0.5秒 - - 下一个动画间隔:0.1秒 -- **问题**:间隔时间过短,导致动画切换过快 - -### 2. 状态管理问题 -- 缺少严格的状态检查 -- 可能存在多个动画同时运行的情况 -- 状态同步不够完善 - -### 3. 数据源问题 -- 没有对数据源进行验证 -- 数据源不足时可能导致动画异常 - -## 解决方案 - -### 1. 调整动画时序 -- **停留时间**:从2秒增加到3秒 -- **间隔时间**:从0.1秒增加到0.5秒 -- **总周期**:从3.1秒增加到4.5秒 - -### 2. 优化状态管理 -- 添加更严格的状态检查 -- 确保只有一个动画在运行 -- 防止容器中同时存在多个视图 - -### 3. 添加数据源检查 -- 在setupUI中验证数据源数量 -- 添加详细的日志输出便于调试 - -## 修改内容 - -### 文件:`PIGiftBravoGiftBroadcastView.m` - -#### 1. setupUI方法 -```objc -// 检查数据源 -if (self.source.count < 2) { - NSLog(@"⚠️ PIGiftBravoGiftBroadcastView: 数据源不足 (%lu个),动画可能过快", (unsigned long)self.source.count); -} else { - NSLog(@"✅ PIGiftBravoGiftBroadcastView: 数据源正常 (%lu个)", (unsigned long)self.source.count); -} -``` - -#### 2. playCurrentAnimation方法 -```objc -// 更严格的状态检查 -if (self.shouldStopAnimation) { - self.isAnimating = NO; - NSLog(@"🚫 PIGiftBravoGiftBroadcastView: 动画被停止标志阻止"); - return; -} - -// 确保只有一个动画在运行 -if (self.container.subviews.count > 0) { - NSLog(@"⚠️ PIGiftBravoGiftBroadcastView: 容器中已有视图,跳过当前动画"); - return; -} - -// 停留时间:从2秒增加到3秒 -[UIView animateWithDuration:0.5 delay:3.0 options:UIViewAnimationOptionCurveEaseIn animations:^{ - item.frame = CGRectMake(-KScreenWidth, 2, KScreenWidth - 25 - 42, 26); -} completion:^(BOOL finished) { - // 使用定时器延迟下一个动画,从0.1秒增加到0.5秒 - self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(scheduleNextAnimation) userInfo:nil repeats:NO]; - NSLog(@"⏰ PIGiftBravoGiftBroadcastView: 安排下一个动画,延迟0.5秒"); -}]; -``` - -#### 3. 添加详细日志 -- 动画启动/停止状态 -- 数据源验证结果 -- 动画播放进度 -- 状态变化追踪 - -## 预期效果 - -### 1. 动画速度改善 -- 动画切换更加平滑自然 -- 用户有足够时间阅读内容 -- 减少视觉疲劳 - -### 2. 稳定性提升 -- 防止多个动画同时运行 -- 更好的状态管理 -- 减少异常情况 - -### 3. 调试便利性 -- 详细的日志输出 -- 便于问题定位 -- 性能监控 - -## 测试建议 - -### 1. 功能测试 -- 验证动画时序是否正确 -- 检查状态管理是否正常 -- 确认数据源检查是否有效 - -### 2. 性能测试 -- 监控内存使用情况 -- 检查CPU占用率 -- 验证动画流畅度 - -### 3. 边界测试 -- 数据源为空的情况 -- 快速切换场景的情况 -- 内存压力下的表现 - -## 注意事项 - -1. **向后兼容**:修改保持了原有的API接口不变 -2. **性能影响**:增加了日志输出,在Release版本中可以考虑移除 -3. **配置灵活**:动画时间可以通过常量定义,便于后续调整 - -## 修改时间 -2025年1月27日 - -## 修改人员 -AI Assistant diff --git a/docs/PublicRoomManager_Usage_Guide.md b/docs/PublicRoomManager_Usage_Guide.md deleted file mode 100644 index 98908cd1..00000000 --- a/docs/PublicRoomManager_Usage_Guide.md +++ /dev/null @@ -1,247 +0,0 @@ -# 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 *)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]; - }); - } -} -``` diff --git a/docs/PublicRoomMessageForward_Implementation.md b/docs/PublicRoomMessageForward_Implementation.md deleted file mode 100644 index 7669f1bb..00000000 --- a/docs/PublicRoomMessageForward_Implementation.md +++ /dev/null @@ -1,92 +0,0 @@ -# 公共房间消息转发功能实现 - -## 功能概述 - -实现了从 PublicRoomManager 转发特定消息到房间中的功能。当 PublicRoomManager 接收到 attachment.first 为 106 的消息时,会自动转发到当前活跃的房间中。 - -## 实现方案 - -### 1. 通知机制 - -- 使用 NSNotificationCenter 进行消息转发 -- 通知名称:`@"MessageFromPublicRoomWithAttachmentNotification"` -- 通知对象:NIMMessage 对象 - -### 2. 修改的文件 - -#### PublicRoomManager.m - -- 在 `onRecvMessages:` 方法中添加转发逻辑 -- 当检测到 `attachment.first == 106` 时发送通知 - -#### XPRoomViewController.m - -- 在 `setupNotifications` 方法中注册通知监听 -- 添加 `handlePublicRoomMessageForward:` 方法处理转发的消息 -- 在 `dealloc` 中自动移除通知监听 - -#### YUMIConstant.m - -- 添加常量定义:`kMessageFromPublicRoomWithAttachmentNotification`(已添加但当前使用字符串字面量) - -#### XPRoomViewController.h - -- 添加常量声明(已添加但当前使用字符串字面量) - -## 使用流程 - -1. **消息接收**:PublicRoomManager 接收到公共房间消息 -2. **类型检查**:检查 attachment.first 是否为 106 -3. **发送通知**:如果是 106 类型,发送转发通知 -4. **接收处理**:XPRoomViewController 接收通知并处理 -5. **消息显示**:通过现有的消息处理流程显示在房间中 - -## 代码示例 - -### 发送通知(PublicRoomManager.m) -```objective-c -if (attachment && attachment.first == 106) { - [[NSNotificationCenter defaultCenter] postNotificationName:@"MessageFromPublicRoomWithAttachmentNotification" - object:message]; - NSLog(@"PublicRoomManager: 转发106类型消息到房间"); -} -``` - -### 接收处理(XPRoomViewController.m) -```objective-c -- (void)handlePublicRoomMessageForward:(NSNotification *)notification { - NIMMessage *message = notification.object; - if (![message isKindOfClass:[NIMMessage class]]) { - return; - } - - // 检查房间是否处于活跃状态 - if (!self.roomInfo || !self.messageContainerView) { - return; - } - - // 使用现有的消息处理流程 - [self.messageContainerView handleNIMCustomMessage:message]; -} -``` - -## 测试场景 - -1. **正常转发**:公共房间收到106类型消息时正确转发 -2. **房间状态**:房间最小化、关闭等状态下的处理 -3. **消息过滤**:确保转发的消息经过正确的过滤流程 -4. **性能影响**:确保不影响现有消息处理性能 - -## 注意事项 - -1. 消息会经过现有的 `isCanDisplayMessage` 过滤 -2. 支持最小化房间的特殊处理 -3. 自动处理内存管理(在 dealloc 中移除监听) -4. 包含完整的错误检查和日志记录 - -## 扩展性 - -如果将来需要转发其他类型的消息,可以: - -1. 修改条件判断(如 `attachment.first == 107`) -2. 或者使用更通用的通知名称,在通知数据中携带消息类型信息 diff --git a/docs/getUserInfo_API_Documentation.md b/docs/getUserInfo_API_Documentation.md deleted file mode 100644 index a0e6e934..00000000 --- a/docs/getUserInfo_API_Documentation.md +++ /dev/null @@ -1,293 +0,0 @@ -# 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`: NSInteger,HTTP 状态码或业务状态码 -- `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 -- **最后更新**: 当前版本 \ No newline at end of file diff --git a/micButton状态表格.md b/micButton状态表格.md deleted file mode 100644 index 64ebc247..00000000 --- a/micButton状态表格.md +++ /dev/null @@ -1,71 +0,0 @@ -# micButton 状态表格 - -## micButton 可用状态总览 - -| 场景 | 用户状态 | micButton显示 | micButton可用性 | micState值 | 音频状态 | 备注 | -|------|----------|---------------|----------------|------------|----------|------| -| **用户上麦前** | 未在麦位 | 隐藏 | 不可用 | MICState_None | 无音频 | isOnMic = NO | -| **用户刚上麦** | 刚上麦位 | 显示 | 可用 | MICState_Close | 静音 | 默认静音状态,localMuted = YES | -| **用户开麦** | 在麦位 | 显示开麦状态 | 可用 | MICState_Open | 开启音频 | 用户可以说话 | -| **用户关麦** | 在麦位 | 显示关麦状态 | 可用 | MICState_Close | 静音 | 用户无法说话 | -| **用户下麦** | 离开麦位 | 隐藏 | 不可用 | MICState_None | 无音频 | isOnMic = NO | - -## 不同场景下的状态变化 - -### 1. 用户加入/离开舞台 - -| 操作 | micButton状态变化 | 音频状态变化 | UI更新 | -|------|------------------|--------------|--------| -| 用户上麦 | 隐藏 → 显示(关麦状态) | 无音频 → 静音 | isOnMic: NO → YES | -| 用户下麦 | 显示 → 隐藏 | 当前状态 → 无音频 | isOnMic: YES → NO | - -### 2. 其他用户加入/离开舞台 - -| 操作 | 当前用户micButton | 影响范围 | 说明 | -|------|------------------|----------|------| -| 他人上麦 | 无变化 | 仅更新麦位显示 | micButton状态不受影响 | -| 他人下麦 | 无变化 | 仅更新麦位显示 | micButton状态不受影响 | - -### 3. 房间最小化场景 - -| 状态 | micButton处理 | 音频处理 | 数据同步 | -|------|---------------|----------|----------| -| 最小化时 | 监听队列变化 | 继续广播音频 | selfNeedBroadcast基于MicroMicStateType_Open | -| 恢复显示 | recheckMicState同步 | 保持当前状态 | 从XPSkillCardPlayerManager.micState同步 | - -## micButton 状态枚举详解 - -| MICState枚举 | 数值 | 含义 | UI表现 | 用户能否说话 | -|-------------|------|------|--------|-------------| -| MICState_None | 0 | 无麦克风状态 | micButton隐藏 | ❌ 否 | -| MICState_Close | 1 | 麦克风关闭 | 显示关麦图标 | ❌ 否 | -| MICState_Open | 2 | 麦克风开启 | 显示开麦图标 | ✅ 是 | - -## 关键时序和同步机制 - -### 状态更新流程 -``` -用户操作 → StageView处理 → 麦位队列更新 → onMicroQueueUpdate回调 -→ XPRoomViewController分发 → XPRoomMenuContainerView更新 -→ micButton状态/显示更新 → recheckMicState同步检查 -``` - -### 重要同步点 -| 时机 | 同步操作 | 目的 | -|------|----------|------| -| viewWillAppear | recheckMicState | 确保UI与全局状态一致 | -| 房间退出 | micState = MICState_None | 重置状态 | -| 麦位变化 | onMicroQueueUpdate | 实时更新UI | - -## 特殊情况处理 - -| 特殊情况 | micButton行为 | 处理逻辑 | -|----------|---------------|----------| -| 网络断线重连 | 重新同步状态 | recheckMicState确保一致性 | -| 被踢出麦位 | 立即隐藏 | NIMChatroomEventTypeKicked触发 | -| 房间模式切换 | 根据新模式调整 | 不同RoomModeType有不同处理 | -| 禁麦状态 | 显示但可能限制功能 | isNoProhibitMic控制 | - ---- - -**总结**: micButton的可用状态主要取决于用户是否在麦位(isOnMic),在麦位时根据MICState显示不同状态,用户只有在MICState_Open时才能说话。 \ No newline at end of file diff --git a/sequenceDiagram.cs b/sequenceDiagram.cs deleted file mode 100644 index 029a8683..00000000 --- a/sequenceDiagram.cs +++ /dev/null @@ -1,63 +0,0 @@ -sequenceDiagram - participant User as 用户 - participant StageView as StageView - participant RoomVC as XPRoomViewController - participant NIM as NIM SDK - participant API as Api服务 - participant MenuView as XPRoomMenuContainerView - - Note over User,MenuView: 用户上mic完整流程 - - %% 触发阶段 - User->>StageView: 点击麦位 - StageView->>StageView: microViewTapped(调用didSelectAtIndex) - - %% 权限检查阶段 - StageView->>StageView: 检查麦位状态(空闲/占用/锁定) - alt 麦位被占用 - StageView->>StageView: displayUserCard(显示用户信息) - else 麦位空闲且未锁定 - StageView->>RoomVC: 获取聊天室成员信息 - - %% 用户身份判断 - alt 超级管理员/管理员 - StageView->>StageView: 显示操作菜单(上麦/锁麦/静音/邀请) - User->>StageView: 选择"上麦"操作 - else 普通用户 - alt 用户已在其他麦位 - StageView->>NIM: nimDownQueue(下麦) - NIM->>NIM: 更新聊天室队列 - end - end - - %% 特殊房间类型处理 - alt 特定房间类型(RoomType_19Mic)且位置为6 - StageView->>API: requestBossMicUp(老板上麦) - API-->>StageView: 返回结果 - end - - %% 上麦核心流程 - StageView->>NIM: nimUpQueue(上麦请求) - NIM->>NIM: 构建队列扩展信息(userInfoToQueueExt) - NIM->>API: 更新聊天室队列 - API-->>NIM: 确认队列更新 - - %% 状态同步阶段 - NIM->>RoomVC: onMicroQueueUpdate回调 - RoomVC->>MenuView: 分发队列更新事件 - MenuView->>MenuView: 更新micButton状态 - MenuView->>MenuView: recheckMicState(状态同步检查) - - %% UI更新阶段 - StageView->>StageView: 更新麦位UI显示 - StageView->>StageView: 显示用户头像和信息 - - %% 消息广播 - NIM->>NIM: 发送自定义消息(CustomMessageType_Hall_Super_Admin) - NIM->>RoomVC: 广播给房间内所有用户 - - else 麦位被锁定 - StageView->>User: 显示"麦位已锁定"提示 - end - - Note over User,MenuView: 流程完成,用户成功上麦 \ No newline at end of file