删除多个文档和文件,包括 micButton 状态表格、QEmotionBoardView 相关问题分析报告、序列图、飘屏组件分析文档等,以清理项目中的冗余内容,提升代码整洁性和可维护性。

This commit is contained in:
edwinQQQ
2025-08-27 18:25:12 +08:00
parent eee967c2e1
commit dce3ea94ce
20 changed files with 0 additions and 4677 deletions

View File

@@ -1,26 +0,0 @@
-[XPRoomViewController dealloc] [Line 314]🔄 XPRoomViewController: 清理 RoomAnimationView
-[RoomAnimationView removeItSelf] [Line 193]<5D><> RoomAnimationView: 开始销毁
-[RoomAnimationView cleanupAllSubviews] [Line 239] 清理所有子视图
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: BravoGiftBannerView (从容器: XPRoomAnimationHitView)
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: BravoGiftWinningFlagView (从容器: XPRoomAnimationHitView)
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: BravoGiftWinningFlagView (从容器: XPRoomAnimationHitView)
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: BravoGiftWinningFlagView (从容器: XPRoomAnimationHitView)
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: BravoGiftWinningFlagView (从容器: XPRoomAnimationHitView)
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: BravoGiftWinningFlagView (从容器: XPRoomAnimationHitView)
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: NetImageView (从容器: XPRoomAnimationHitView)
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: NetImageView (从容器: XPRoomAnimationHitView)
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: NetImageView (从容器: XPRoomAnimationHitView)
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: NetImageView (从容器: XPRoomAnimationHitView)
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: NetImageView (从容器: XPRoomAnimationHitView)
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: NetImageView (从容器: XPRoomAnimationHitView)
-[RoomAnimationView cleanupAllSubviews] [Line 250]<5D><> 标记移除视图: NetImageView (从容器: XPRoomAnimationHitView)
-[RoomAnimationView cleanupAllSubviews] [Line 265]🔄 所有子视图清理完成
-[RoomAnimationView removeItSelf] [Line 220]<5D><> 清理 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]<5D><> RoomAnimationView: 销毁完成
-[RoomAnimationView playBroveBanner:]_block_invoke [Line 752]🔄 BravoGiftBannerView complete 回调被调用

View File

@@ -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 <UIKit/UIKit.h>
#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<YMBannerDelegate> 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 <Foundation/Foundation.h>
#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 <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@class YMBaseBannerView;
@protocol YMBannerDelegate <NSObject>
@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 <Foundation/Foundation.h>
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组件的开发效率和用户体验质量。

View File

@@ -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<br/>Custom_Message_Sub_General_Floating_Screen_All_Room | playGameBanner: | 游戏系统 | 展示游戏相关飘屏,支持跳转 |
| 7 | 塔罗飘屏 | XPRoomTarrowBannerView | Custom_Message_Sub_Tarot_Advanced<br/>Custom_Message_Sub_Tarot_Intermediate | createTarotBannerAnimation: | 塔罗活动 | 展示塔罗相关活动信息 |
| 8 | 星厨房飘屏 | XPRoomStarKitchenBannerView | Custom_Message_Sub_Star_Kitchen_FullScreen | createStarKitchenBannerAnimation: | 厨房活动 | 展示星厨房活动 |
| 9 | 夺宝精灵飘屏 | - | Custom_Message_Sub_Treasure_Fairy_Draw_Gift_L4<br/>Custom_Message_Sub_Treasure_Fairy_Draw_Gift_L5<br/>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<br/>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系列)

View File

@@ -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. **低优先级**:添加扩展功能和优化
通过这种抽象设计,能够在保持现有功能完整性的同时,为未来的功能扩展和维护奠定良好的架构基础。

View File

@@ -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。修复后表情应该能正常显示。如果仍有问题可以通过添加的调试日志进一步排查。

View File

@@ -1,175 +0,0 @@
# QEmotionBoardView emoji 不显示问题排查报告
## 问题现状
- 表情数组已成功加载125个表情
- 表情图片文件存在且正确
- 但表情面板仍然不显示 emoji
## 排查过程
### 1. 数据流验证
已添加调试代码验证数据流:
- AppDelegate 中表情数组加载完成
- QEmotionBoardView 设置表情数组
- UIEmotionVerticalScrollView 接收表情数据
- UIEmotionPageView 接收表情数据
### 2. 关键问题发现
#### 问题1UIEmotionVerticalScrollView 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 为 0contentSize 也会为 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<QEmotion *> *)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<QEmotion *> *)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 可能还没有正确设置,导致布局计算错误。通过延迟设置或使用有效的尺寸基准可以解决这个问题。

View File

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

View File

@@ -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 模式下为手势容器添加背景色,便于调试
该方案既满足了新的功能需求,又解决了潜在的手势冲突问题,保持了代码的可维护性和扩展性。

View File

@@ -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. **日志记录**: 添加详细的操作日志
## 总结
邮箱验证码登录流程是一个完整的用户认证系统,包含了界面展示、验证码获取、用户登录、数据存储等完整环节。该流程具有良好的安全性、用户体验和错误处理机制,符合现代移动应用的认证标准。
通过本文档,开发人员可以全面了解邮箱登录的实现细节,便于后续的功能扩展和维护工作。

View File

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

View File

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

View File

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

View File

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

View File

@@ -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` - 账户数据模型

View File

@@ -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

View File

@@ -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<NIMMessage *> *)messages {
for (NIMMessage *message in messages) {
if (message.session.sessionType == NIMSessionTypeChatroom) {
NSString *sessionId = message.session.sessionId;
if ([sessionId isEqualToString:self.currentPublicRoomId]) {
// 添加自定义消息处理逻辑
[self handlePublicRoomMessage:message];
}
}
}
}
```
### 2. 状态监听扩展
可以添加状态变化的通知:
```objc
// 在状态变化时发送通知
[[NSNotificationCenter defaultCenter] postNotificationName:@"PublicRoomManagerStateChanged"
object:nil
userInfo:@{@"isInPublicRoom": @(self.isInPublicRoom)}];
```
### 3. 重试机制扩展
可以添加更复杂的重试逻辑:
```objc
- (void)retryEnterPublicRoom {
if (self.retryCount < 3) {
self.retryCount++;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self tryEnterPublicRoom];
});
}
}
```

View File

@@ -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. 或者使用更通用的通知名称,在通知数据中携带消息类型信息

View File

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

View File

@@ -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时才能说话。

View File

@@ -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: