refactor(post): consolidate post sync to PostSyncService and remove ProcessPostUseCase
- Replace ProcessPostUseCase with new PostSyncService in src/services/post/ - Add postListStore for state management in src/stores/post/ - Remove deprecated ProcessPostUseCase and ProcessMessageUseCase files - Delete architecture documentation files (ARCHITECTURE_REFACTOR_PLAN.md, architecture-comparison-report.md) - Update all consumers (HomeScreen, PostDetailScreen, SearchScreen, useUserProfile, useDifferentialPosts) - Simplify postService to only contain API layer methods - Remove unused type exports from message stores BREAKING CHANGE: ProcessPostUseCase and ProcessMessageUseCase have been removed. Use postSyncService for post operations instead.
This commit is contained in:
@@ -1,231 +0,0 @@
|
||||
# 架构修复方案文档
|
||||
|
||||
## 1. 重构目标
|
||||
|
||||
### 1.1 核心原则
|
||||
- **单一职责**:每个模块只负责一个明确的功能
|
||||
- **依赖倒置**:高层模块不依赖低层模块,都依赖抽象
|
||||
- **开闭原则**:对扩展开放,对修改关闭
|
||||
|
||||
### 1.2 目标架构
|
||||
```
|
||||
src/
|
||||
├── core/ # 核心业务逻辑(框架无关)
|
||||
│ ├── entities/ # 业务实体
|
||||
│ ├── repositories/ # 仓库接口
|
||||
│ └── usecases/ # 业务用例
|
||||
├── data/ # 数据层
|
||||
│ ├── datasources/ # 数据源实现(API、DB、Cache)
|
||||
│ ├── repositories/ # 仓库实现
|
||||
│ └── mappers/ # 数据映射
|
||||
├── presentation/ # 表现层
|
||||
│ ├── components/ # UI组件
|
||||
│ ├── screens/ # 屏幕组件
|
||||
│ ├── stores/ # 状态管理
|
||||
│ └── hooks/ # 自定义hooks
|
||||
└── infrastructure/ # 基础设施
|
||||
├── navigation/ # 导航
|
||||
├── theme/ # 主题
|
||||
└── services/ # 外部服务
|
||||
```
|
||||
|
||||
## 2. 具体修复方案
|
||||
|
||||
### 2.1 messageManager.ts 重构方案
|
||||
|
||||
#### 现状问题
|
||||
- 1500+行,上帝对象
|
||||
- 混合了 WebSocket、数据库、状态管理、UI通知
|
||||
- 与SQLite深度耦合
|
||||
|
||||
#### 目标结构
|
||||
```
|
||||
src/data/
|
||||
├── datasources/
|
||||
│ ├── WebSocketClient.ts # WebSocket连接管理
|
||||
│ └── MessageDataSource.ts # 消息数据源
|
||||
├── repositories/
|
||||
│ └── MessageRepository.ts # 消息仓库实现
|
||||
└── mappers/
|
||||
└── MessageMapper.ts # 消息数据映射
|
||||
|
||||
src/core/
|
||||
├── entities/
|
||||
│ └── Message.ts # 消息实体
|
||||
├── repositories/
|
||||
│ └── IMessageRepository.ts # 仓库接口
|
||||
└── usecases/
|
||||
├── ProcessMessageUseCase.ts
|
||||
├── SyncMessagesUseCase.ts
|
||||
└── UpdateReadReceiptUseCase.ts
|
||||
|
||||
src/presentation/stores/
|
||||
└── messageStore.ts # 简化的消息状态管理
|
||||
```
|
||||
|
||||
#### 重构步骤
|
||||
1. 提取 WebSocketClient - 纯连接管理
|
||||
2. 创建 MessageRepository - 数据访问抽象
|
||||
3. 创建 UseCases - 业务逻辑
|
||||
4. 重写 messageStore - 仅负责UI状态
|
||||
5. 删除旧的 messageManager.ts
|
||||
|
||||
### 2.2 乐观更新工具函数方案
|
||||
|
||||
#### 现状问题
|
||||
- userStore.ts 中重复6+次相同模式
|
||||
- 代码冗余,难以维护
|
||||
|
||||
#### 目标API
|
||||
```typescript
|
||||
// utils/optimisticUpdate.ts
|
||||
export interface OptimisticUpdateOptions<T, R> {
|
||||
store: StoreApi<any>;
|
||||
getCurrentState: () => T;
|
||||
optimisticUpdate: (current: T) => T;
|
||||
apiCall: () => Promise<R>;
|
||||
onSuccess?: (result: R, current: T) => T;
|
||||
onError?: (error: any, original: T) => void;
|
||||
}
|
||||
|
||||
export async function optimisticUpdate<T, R>(
|
||||
options: OptimisticUpdateOptions<T, R>
|
||||
): Promise<R>
|
||||
```
|
||||
|
||||
#### 使用示例
|
||||
```typescript
|
||||
// 重构前(重复代码)
|
||||
set(state => ({ posts: updatedPosts }));
|
||||
try {
|
||||
const result = await postService.like(postId);
|
||||
// ...
|
||||
} catch (error) {
|
||||
set(state => ({ posts: originalPosts }));
|
||||
}
|
||||
|
||||
// 重构后(简洁)
|
||||
await optimisticUpdate({
|
||||
store,
|
||||
getCurrentState: () => store.getState().posts,
|
||||
optimisticUpdate: (posts) => updatePostLikes(posts, postId, true),
|
||||
apiCall: () => postService.like(postId),
|
||||
onSuccess: (result, posts) => updatePostWithServerData(posts, result),
|
||||
});
|
||||
```
|
||||
|
||||
### 2.3 useResponsive.ts 拆分方案
|
||||
|
||||
#### 现状问题
|
||||
- 485行,43个导出
|
||||
- 功能混杂:断点检测、响应式值计算、平台检测
|
||||
|
||||
#### 目标结构
|
||||
```
|
||||
src/presentation/hooks/responsive/
|
||||
├── useBreakpoint.ts # 断点检测
|
||||
├── useResponsiveValue.ts # 响应式值映射
|
||||
├── useOrientation.ts # 方向检测
|
||||
├── usePlatform.ts # 平台检测
|
||||
├── useScreenSize.ts # 屏幕尺寸
|
||||
└── index.ts # 统一导出
|
||||
```
|
||||
|
||||
#### 各Hook职责
|
||||
- `useBreakpoint`: 返回当前断点 (mobile/tablet/desktop/wide)
|
||||
- `useResponsiveValue`: 根据断点返回不同值
|
||||
- `useOrientation`: 返回 portrait/landscape
|
||||
- `usePlatform`: 返回 ios/android/web
|
||||
- `useScreenSize`: 返回 width/height
|
||||
|
||||
### 2.4 MainNavigator 解耦方案
|
||||
|
||||
#### 现状问题
|
||||
- 1118行,直接依赖store
|
||||
- 混合了导航配置和业务逻辑
|
||||
|
||||
#### 目标结构
|
||||
```
|
||||
src/infrastructure/navigation/
|
||||
├── MainNavigator.tsx # 简化后的导航器
|
||||
├── AuthNavigator.tsx # 认证导航
|
||||
├── TabNavigator.tsx # 标签导航
|
||||
├── navigationService.ts # 导航服务(解耦层)
|
||||
├── navigationTypes.ts # 类型定义
|
||||
└── hooks/
|
||||
└── useNavigationState.ts # 导航状态hook
|
||||
```
|
||||
|
||||
#### 解耦策略
|
||||
1. 创建 NavigationService - 提供导航操作的抽象
|
||||
2. Store通过NavigationService触发导航,而非直接操作
|
||||
3. MainNavigator只负责渲染,不处理业务逻辑
|
||||
|
||||
### 2.5 Repository 层创建方案
|
||||
|
||||
#### 现状问题
|
||||
- 服务层直接操作数据库
|
||||
- 没有明确的数据访问边界
|
||||
|
||||
#### 目标结构
|
||||
```
|
||||
src/data/repositories/
|
||||
├── MessageRepository.ts # 消息数据访问
|
||||
├── PostRepository.ts # 帖子数据访问
|
||||
├── UserRepository.ts # 用户数据访问
|
||||
└── interfaces/
|
||||
├── IMessageRepository.ts
|
||||
├── IPostRepository.ts
|
||||
└── IUserRepository.ts
|
||||
```
|
||||
|
||||
#### 职责划分
|
||||
- **Repository**: 数据访问抽象,定义接口
|
||||
- **DataSource**: 具体数据源实现(API、SQLite、Cache)
|
||||
- **Service**: 业务逻辑编排,使用Repository
|
||||
|
||||
## 3. 实施计划
|
||||
|
||||
### 阶段1:基础设施(第1-2天)
|
||||
- [ ] 创建目录结构
|
||||
- [ ] 实现乐观更新工具函数
|
||||
- [ ] 拆分 useResponsive hooks
|
||||
|
||||
### 阶段2:数据层重构(第3-5天)
|
||||
- [ ] 创建 Repository 接口和实现
|
||||
- [ ] 创建 DataSource 层
|
||||
- [ ] 迁移数据库操作到 Repository
|
||||
|
||||
### 阶段3:核心业务重构(第6-10天)
|
||||
- [ ] 重构 messageManager → UseCases + Store
|
||||
- [ ] 创建 WebSocketClient
|
||||
- [ ] 实现消息同步逻辑
|
||||
|
||||
### 阶段4:表现层重构(第11-12天)
|
||||
- [ ] 解耦 MainNavigator
|
||||
- [ ] 重构 userStore 使用乐观更新工具
|
||||
- [ ] 更新所有屏幕组件
|
||||
|
||||
### 阶段5:测试与优化(第13-14天)
|
||||
- [ ] 单元测试
|
||||
- [ ] 集成测试
|
||||
- [ ] 性能优化
|
||||
|
||||
## 4. 风险与应对
|
||||
|
||||
| 风险 | 影响 | 应对措施 |
|
||||
|-----|------|---------|
|
||||
| 重构引入bug | 高 | 每个模块重构后必须测试,保持API兼容 |
|
||||
| 开发时间超期 | 中 | 分阶段交付,优先核心功能 |
|
||||
| 团队成员不熟悉新架构 | 中 | 编写详细文档,代码审查 |
|
||||
| 性能下降 | 低 | 添加性能监控,及时优化 |
|
||||
|
||||
## 5. 验收标准
|
||||
|
||||
- [ ] messageManager.ts 被删除,功能拆分到多个小模块
|
||||
- [ ] userStore.ts 中无重复乐观更新代码
|
||||
- [ ] useResponsive.ts 拆分为5+个专注hook
|
||||
- [ ] MainNavigator.tsx 行数 < 300
|
||||
- [ ] 所有单元测试通过
|
||||
- [ ] 无TypeScript错误
|
||||
- [ ] 性能不劣化(启动时间、内存占用)
|
||||
@@ -1,394 +0,0 @@
|
||||
# Messages模块与PostService/Manager架构对比分析报告
|
||||
|
||||
## 一、Messages模块架构分析
|
||||
|
||||
### 1.1 架构层次结构
|
||||
|
||||
Messages模块采用了**清晰的分层架构**,各层职责明确:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 表现层 (Screens) │
|
||||
│ ChatScreen, MessageListScreen, NotificationsScreen │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Hooks层 │
|
||||
│ useDifferentialMessages, useChatScreen │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 用例层 (UseCases) │
|
||||
│ ProcessMessageUseCase │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 领域层 (Entities) │
|
||||
│ Message, Conversation, GroupNotice │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 仓库层 (Repositories) │
|
||||
│ MessageRepository, IMessageRepository │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 数据源层 (DataSources) │
|
||||
│ SSEClient, LocalDataSource, ApiDataSource │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.2 核心组件详解
|
||||
|
||||
#### 1.2.1 领域实体层 (Core/Entities)
|
||||
- **Message.ts**: 定义消息、会话、群通知等核心领域模型
|
||||
- 包含工厂函数:`createMessage`, `createConversation`
|
||||
- 包含业务逻辑函数:`isMessageFromCurrentUser`, `shouldIncrementUnread`, `buildTextContent`
|
||||
|
||||
#### 1.2.2 用例层 (Core/UseCases)
|
||||
- **ProcessMessageUseCase**: 核心业务逻辑编排器
|
||||
- 订阅SSE事件(chat, group_message, read, recall, typing, group_notice等)
|
||||
- 消息去重处理(processedMessageIds Set)
|
||||
- 已读状态保护(pendingReadMap + 版本号机制)
|
||||
- 用户信息缓存与去重请求(pendingUserRequests Map)
|
||||
- 事件发布订阅模式(subscribers Set)
|
||||
|
||||
#### 1.2.3 仓库层 (Data/Repositories)
|
||||
- **IMessageRepository**: 定义数据访问接口
|
||||
- 消息操作:getMessages, saveMessage, updateMessageStatus, markAsRead
|
||||
- 会话操作:getConversations, saveConversation, updateUnreadCount
|
||||
- 同步操作:syncMessages, getServerLastSeq
|
||||
|
||||
- **MessageRepository**: 实现接口
|
||||
- 封装SQLite数据库操作
|
||||
- 消息与CachedMessage的转换
|
||||
|
||||
#### 1.2.4 映射器层 (Data/Mappers)
|
||||
- **MessageMapper**: 数据转换
|
||||
- `fromApiResponse`: API响应 → 应用模型
|
||||
- `fromDbRecord`: 数据库记录 → 应用模型
|
||||
- `toDbRecord`: 应用模型 → 数据库记录
|
||||
- `toApiRequest`: 应用模型 → API请求
|
||||
- 创建消息片段:`createTextSegment`, `createImageSegment`
|
||||
|
||||
#### 1.2.5 差异计算基础设施 (Infrastructure/Diff)
|
||||
- **MessageDiffCalculator**: 消息列表差异计算
|
||||
- 计算added, updated, deleted, moved, unchanged
|
||||
- 变化比例检测(changeRatio > 0.5 时触发重置)
|
||||
- 增量差异计算(基于缓存)
|
||||
|
||||
- **MessageUpdateBatcher**: 批量更新处理器
|
||||
- 16ms批量间隔(约60fps)
|
||||
- 去重和合并更新
|
||||
- 100ms最大等待时间
|
||||
- 统计信息:totalBatches, totalUpdates, averageBatchSize
|
||||
|
||||
- **types.ts**: 完整的类型定义
|
||||
- MessageUpdateType枚举:ADD, UPDATE, DELETE, MOVE, BATCH_*
|
||||
- DiffResult, DiffConfig等接口
|
||||
|
||||
#### 1.2.6 Hook层
|
||||
- **useDifferentialMessages**: 差异更新Hook
|
||||
- 集成DiffCalculator和Batcher
|
||||
- 自动处理批量更新
|
||||
- 提供flush, reset, forceUpdate方法
|
||||
- 支持配置:enableDiff, enableBatching, maxMessageCount, changeRatioThreshold
|
||||
|
||||
### 1.3 数据流
|
||||
|
||||
```
|
||||
SSE事件 → ProcessMessageUseCase → 事件发布 → useChatScreen → useDifferentialMessages
|
||||
↓ ↓
|
||||
MessageRepository 差异计算 + 批量处理
|
||||
↓ ↓
|
||||
SQLite数据库 优化后的消息列表
|
||||
```
|
||||
|
||||
### 1.4 状态管理方式
|
||||
|
||||
- **发布-订阅模式**:ProcessMessageUseCase作为事件中心
|
||||
- **本地SQLite缓存**:消息持久化存储
|
||||
- **内存状态**:通过Hook返回,组件直接消费
|
||||
- **差异更新**:通过MessageDiffCalculator + MessageUpdateBatcher减少重渲染
|
||||
|
||||
---
|
||||
|
||||
## 二、PostService/Manager架构分析
|
||||
|
||||
### 2.1 架构层次结构
|
||||
|
||||
Post模块采用了**扁平化架构**,Service和Manager层职责混合:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 表现层 (Screens) │
|
||||
│ HomeScreen, PostDetailScreen, CreatePostScreen │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Hooks层 │
|
||||
│ useCursorPagination (通用), usePrefetch │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Store层 (Zustand) │
|
||||
│ useUserStore (直接操作状态), postManager (缓存管理) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Service层 │
|
||||
│ postService (API调用 + 状态更新) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 数据源 │
|
||||
│ API (直接调用) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 核心组件详解
|
||||
|
||||
#### 2.2.1 Service层 (Services)
|
||||
- **postService.ts**: 帖子服务
|
||||
- CRUD操作:getPosts, getPost, createPost, updatePost, deletePost
|
||||
- 互动操作:likePost, unlikePost, favoritePost, unfavoritePost
|
||||
- 分页支持:传统分页 + 游标分页
|
||||
- **问题**:直接操作useUserStore(违反分层原则)
|
||||
|
||||
#### 2.2.2 Manager层 (Stores)
|
||||
- **postManager.ts**: 帖子缓存管理器
|
||||
- 继承CacheBus(发布订阅基类)
|
||||
- 内存缓存:listCache, detailCache
|
||||
- 请求去重:pendingRequests Map
|
||||
- TTL管理:LIST_TTL=30s, DETAIL_TTL=60s
|
||||
- 后台刷新机制
|
||||
|
||||
#### 2.2.3 Store层 (Zustand)
|
||||
- **useUserStore**: 用户状态存储
|
||||
- 直接存储posts数组
|
||||
- 帖子操作副作用直接修改状态
|
||||
- 混合了用户数据和帖子数据
|
||||
|
||||
#### 2.2.4 映射器层 (Data/Mappers)
|
||||
- **PostMapper**: 数据转换
|
||||
- `fromApiResponse`: API响应 → 应用模型
|
||||
- `fromApiResponseList`: 批量转换
|
||||
- `toApiRequest`: 应用模型 → API请求
|
||||
- 相比MessageMapper缺少数据库记录转换方法
|
||||
|
||||
### 2.3 数据流
|
||||
|
||||
```
|
||||
HomeScreen → useCursorPagination → postService.getPostsCursor
|
||||
↓
|
||||
useUserStore.setState(posts)
|
||||
↓
|
||||
PostCard组件渲染
|
||||
```
|
||||
|
||||
### 2.4 状态管理方式
|
||||
|
||||
- **Zustand Store**:集中式状态管理
|
||||
- **内存缓存**:postManager提供应用级缓存
|
||||
- **请求去重**:dedupe方法防止重复请求
|
||||
- **直接修改**:postService直接调用useUserStore.setState
|
||||
|
||||
---
|
||||
|
||||
## 三、主要架构差异
|
||||
|
||||
### 3.1 分层复杂度
|
||||
|
||||
| 维度 | Messages模块 | Post模块 |
|
||||
|------|-------------|---------|
|
||||
| 层次深度 | 6层(Entity→UseCase→Repository→Mapper→Hook→Screen) | 4层(Service→Store→Hook→Screen) |
|
||||
| 用例层 | 独立ProcessMessageUseCase | 无 |
|
||||
| 仓库接口 | IMessageRepository抽象接口 | 无 |
|
||||
| 基础设施 | Diff模块(Calculator + Batcher) | 无 |
|
||||
|
||||
### 3.2 数据持久化
|
||||
|
||||
| 维度 | Messages模块 | Post模块 |
|
||||
|------|-------------|---------|
|
||||
| 本地存储 | SQLite数据库 | 无 |
|
||||
| 缓存抽象 | MessageRepository封装 | postManager内存缓存 |
|
||||
| 离线支持 | 完整 | 不支持 |
|
||||
|
||||
### 3.3 实时更新
|
||||
|
||||
| 维度 | Messages模块 | Post模块 |
|
||||
|------|-------------|---------|
|
||||
| 推送机制 | SSE实时推送 | 轮询/下拉刷新 |
|
||||
| 事件订阅 | ProcessMessageUseCase发布订阅 | 无 |
|
||||
| 增量更新 | MessageDiffCalculator | 无 |
|
||||
|
||||
### 3.4 状态管理
|
||||
|
||||
| 维度 | Messages模块 | Post模块 |
|
||||
|------|-------------|---------|
|
||||
| 管理方式 | 发布订阅 + Hook局部状态 | Zustand全局Store |
|
||||
| 状态来源 | useChatScreen, useDifferentialMessages | useUserStore |
|
||||
| 批量更新 | MessageUpdateBatcher | 无 |
|
||||
| 差异检测 | MessageDiffCalculator | 无 |
|
||||
|
||||
### 3.5 依赖方向
|
||||
|
||||
**Messages模块(正确)**:
|
||||
```
|
||||
Hook → UseCase → Repository → DataSource
|
||||
↓
|
||||
Entity(不依赖外部)
|
||||
```
|
||||
|
||||
**Post模块(问题)**:
|
||||
```
|
||||
Service → Store(循环依赖风险)
|
||||
↓
|
||||
API直接调用
|
||||
```
|
||||
|
||||
### 3.6 核心问题总结
|
||||
|
||||
| # | 问题 | Messages | Post |
|
||||
|---|------|---------|------|
|
||||
| 1 | 违反分层原则 | 无 | postService直接操作useUserStore |
|
||||
| 2 | 缺少UseCase层 | ProcessMessageUseCase | 无业务逻辑编排 |
|
||||
| 3 | 缺少Repository抽象 | IMessageRepository | 无数据访问接口 |
|
||||
| 4 | 无差异更新 | useDifferentialMessages | 无 |
|
||||
| 5 | 无批量处理 | MessageUpdateBatcher | 无 |
|
||||
| 6 | 无本地持久化 | SQLite | 无 |
|
||||
| 7 | 无实时推送 | SSE | 无 |
|
||||
|
||||
---
|
||||
|
||||
## 四、对齐建议
|
||||
|
||||
### 4.1 架构重构目标
|
||||
|
||||
将Post模块对齐到Messages模块的架构模式:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 表现层 (Screens) │
|
||||
│ HomeScreen, PostDetailScreen │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Hooks层 │
|
||||
│ useDifferentialPosts (新增) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 用例层 (UseCases) │
|
||||
│ ProcessPostUseCase (新增) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 领域层 (Entities) │
|
||||
│ Post, PostComment (新增) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 仓库层 (Repositories) │
|
||||
│ PostRepository, IPostRepository (新增) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Service层 │
|
||||
│ postService (仅保留API调用,移除状态操作) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.2 具体改造项
|
||||
|
||||
#### 4.2.1 创建Post领域实体
|
||||
```typescript
|
||||
// src/core/entities/Post.ts
|
||||
export interface Post {
|
||||
id: string;
|
||||
authorId: string;
|
||||
title: string;
|
||||
content: string;
|
||||
images: string[];
|
||||
likeCount: number;
|
||||
commentCount: number;
|
||||
// ... 其他字段
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2.2 创建ProcessPostUseCase
|
||||
```typescript
|
||||
// src/core/usecases/ProcessPostUseCase.ts
|
||||
class ProcessPostUseCase {
|
||||
// 帖子增删改查
|
||||
// 点赞/收藏逻辑
|
||||
// 事件发布订阅
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2.3 创建IPostRepository接口
|
||||
```typescript
|
||||
// src/data/repositories/interfaces/IPostRepository.ts
|
||||
interface IPostRepository {
|
||||
getPosts(type: string, page: number, pageSize: number): Promise<Post[]>;
|
||||
savePost(post: Post): Promise<void>;
|
||||
updatePost(postId: string, updates: Partial<Post>): Promise<void>;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2.4 创建PostRepository实现
|
||||
```typescript
|
||||
// src/data/repositories/PostRepository.ts
|
||||
// 封装SQLite操作,实现IPostRepository接口
|
||||
```
|
||||
|
||||
#### 4.2.5 创建useDifferentialPosts Hook
|
||||
```typescript
|
||||
// src/hooks/useDifferentialPosts.ts
|
||||
// 类似useDifferentialMessages,处理帖子列表差异更新
|
||||
```
|
||||
|
||||
#### 4.2.6 改造postService
|
||||
- 移除对useUserStore的直接依赖
|
||||
- 仅保留API调用职责
|
||||
|
||||
---
|
||||
|
||||
## 五、架构对比图
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Messages模块 [理想架构]"
|
||||
E1[Entity<br/>Message.ts]
|
||||
U1[UseCase<br/>ProcessMessageUseCase]
|
||||
R1[Repository<br/>MessageRepository]
|
||||
M1[Mapper<br/>MessageMapper]
|
||||
H1[Hook<br/>useDifferentialMessages]
|
||||
D1[Diff基础设施<br/>DiffCalculator + Batcher]
|
||||
S1[(SQLite)]
|
||||
|
||||
E1 --> U1
|
||||
U1 --> R1
|
||||
U1 --> D1
|
||||
R1 --> S1
|
||||
M1 --> R1
|
||||
D1 --> H1
|
||||
H1 --> S1
|
||||
end
|
||||
|
||||
subgraph "Post模块 [当前架构]"
|
||||
E2[PostMapper]
|
||||
SV2[postService<br/>直接操作Store]
|
||||
ST2[useUserStore<br/>Zustand]
|
||||
H2[Hook<br/>useCursorPagination]
|
||||
PM2[postManager<br/>内存缓存]
|
||||
|
||||
E2 --> SV2
|
||||
SV2 --> ST2
|
||||
H2 --> SV2
|
||||
PM2 --> ST2
|
||||
end
|
||||
|
||||
style E1 fill:#90EE90
|
||||
style U1 fill:#90EE90
|
||||
style R1 fill:#90EE90
|
||||
style D1 fill:#90EE90
|
||||
style H1 fill:#90EE90
|
||||
style S1 fill:#90EE90
|
||||
|
||||
style SV2 fill:#FFB6C1
|
||||
style ST2 fill:#FFB6C1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、结论
|
||||
|
||||
Messages模块是一个**架构完善**的模块,具备:
|
||||
1. 清晰的分层架构
|
||||
2. 独立的业务逻辑编排层(UseCase)
|
||||
3. 完整的数据持久化(SQLite)
|
||||
4. 高效的差异更新机制
|
||||
5. 实时事件推送能力
|
||||
|
||||
Post模块当前存在以下问题:
|
||||
1. **违反分层原则**:Service直接操作Store
|
||||
2. **缺少UseCase层**:业务逻辑分散
|
||||
3. **无数据抽象**:缺少Repository接口
|
||||
4. **无差异更新**:每次全量更新导致性能问题
|
||||
5. **无本地持久化**:无法离线使用
|
||||
|
||||
建议按照对齐建议逐步重构Post模块,使其架构与Messages模块对齐。
|
||||
@@ -1,689 +0,0 @@
|
||||
/**
|
||||
* ProcessMessageUseCase - 处理消息用例
|
||||
* 处理消息的业务逻辑,协调 Repository 和 SSEClient
|
||||
* 纯业务逻辑,无UI依赖
|
||||
*/
|
||||
|
||||
import { messageRepository, userCacheRepository, CachedMessage } from '@/database';
|
||||
import { wsClient, WSEventType } from '../../data/datasources/WSClient';
|
||||
import { MessageMapper } from '@/data/mappers';
|
||||
import {
|
||||
Message,
|
||||
Conversation,
|
||||
GroupNotice,
|
||||
createMessage,
|
||||
isMessageFromCurrentUser,
|
||||
shouldIncrementUnread,
|
||||
buildGroupNoticeText,
|
||||
} from '../entities/Message';
|
||||
import { messageService } from '../../services/messageService';
|
||||
import { api } from '../../services/api';
|
||||
|
||||
const cachedMessageToMessage = (cached: CachedMessage): Message => ({
|
||||
id: cached.id,
|
||||
conversationId: cached.conversationId,
|
||||
senderId: cached.senderId,
|
||||
seq: cached.seq,
|
||||
segments: cached.segments || [],
|
||||
createdAt: cached.createdAt,
|
||||
status: cached.status as Message['status'],
|
||||
});
|
||||
|
||||
// 事件类型定义
|
||||
export type MessageUseCaseEventType =
|
||||
| 'message_received'
|
||||
| 'message_recalled'
|
||||
| 'typing_status'
|
||||
| 'group_notice'
|
||||
| 'connection_changed'
|
||||
| 'error';
|
||||
|
||||
export interface MessageUseCaseEvent {
|
||||
type: MessageUseCaseEventType;
|
||||
payload: any;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export type MessageUseCaseSubscriber = (event: MessageUseCaseEvent) => void;
|
||||
|
||||
// 已读状态保护记录
|
||||
interface ReadStateRecord {
|
||||
timestamp: number;
|
||||
version: number;
|
||||
lastReadSeq: number;
|
||||
clearTimer?: ReturnType<typeof setTimeout>;
|
||||
}
|
||||
|
||||
class ProcessMessageUseCase {
|
||||
private subscribers: Set<MessageUseCaseSubscriber> = new Set();
|
||||
private unsubscribeFns: Array<() => void> = [];
|
||||
private processedMessageIds: Set<string> = new Set();
|
||||
private processedMessageIdsExpiry: Map<string, number> = new Map();
|
||||
private pendingReadMap: Map<string, ReadStateRecord> = new Map();
|
||||
private readStateVersion = 0;
|
||||
private pendingUserRequests: Map<string, Promise<any>> = new Map();
|
||||
private currentUserId: string | null = null;
|
||||
|
||||
// 常量
|
||||
private readonly READ_STATE_PROTECTION_DELAY = 5000; // 5秒
|
||||
private readonly MESSAGE_ID_EXPIRY = 5 * 60 * 1000; // 5分钟
|
||||
|
||||
/**
|
||||
* 初始化用例
|
||||
*/
|
||||
initialize(currentUserId: string | null): void {
|
||||
this.currentUserId = currentUserId;
|
||||
this.setupWSListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁用例
|
||||
*/
|
||||
destroy(): void {
|
||||
this.unsubscribeFns.forEach((fn) => fn());
|
||||
this.unsubscribeFns = [];
|
||||
this.subscribers.clear();
|
||||
this.processedMessageIds.clear();
|
||||
this.processedMessageIdsExpiry.clear();
|
||||
this.pendingReadMap.clear();
|
||||
this.pendingUserRequests.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置SSE监听器
|
||||
*/
|
||||
private setupWSListeners(): void {
|
||||
// 监听私聊消息
|
||||
const unsubChat = wsClient.on('chat', (message) => {
|
||||
this.handleNewMessage(message);
|
||||
});
|
||||
|
||||
// 监听群聊消息
|
||||
const unsubGroupMessage = wsClient.on('group_message', (message) => {
|
||||
this.handleNewMessage(message);
|
||||
});
|
||||
|
||||
// 监听私聊已读回执
|
||||
const unsubRead = wsClient.on('read', (message) => {
|
||||
this.handleReadReceipt(message);
|
||||
});
|
||||
|
||||
// 监听群聊已读回执
|
||||
const unsubGroupRead = wsClient.on('group_read', (message) => {
|
||||
this.handleGroupReadReceipt(message);
|
||||
});
|
||||
|
||||
// 监听私聊消息撤回
|
||||
const unsubRecall = wsClient.on('recall', (message) => {
|
||||
this.handleRecallMessage(message);
|
||||
});
|
||||
|
||||
// 监听群聊消息撤回
|
||||
const unsubGroupRecall = wsClient.on('group_recall', (message) => {
|
||||
this.handleGroupRecallMessage(message);
|
||||
});
|
||||
|
||||
// 监听群聊输入状态
|
||||
const unsubGroupTyping = wsClient.on('group_typing', (message) => {
|
||||
this.handleGroupTyping(message);
|
||||
});
|
||||
|
||||
// 监听群通知
|
||||
const unsubGroupNotice = wsClient.on('group_notice', (message) => {
|
||||
this.handleGroupNotice(message);
|
||||
});
|
||||
|
||||
// 监听连接状态
|
||||
const unsubConnected = wsClient.on('connected', () => {
|
||||
this.notifySubscribers({
|
||||
type: 'connection_changed',
|
||||
payload: { connected: true },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
});
|
||||
|
||||
const unsubDisconnected = wsClient.on('disconnected', () => {
|
||||
this.notifySubscribers({
|
||||
type: 'connection_changed',
|
||||
payload: { connected: false },
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
});
|
||||
|
||||
this.unsubscribeFns = [
|
||||
unsubChat,
|
||||
unsubGroupMessage,
|
||||
unsubRead,
|
||||
unsubGroupRead,
|
||||
unsubRecall,
|
||||
unsubGroupRecall,
|
||||
unsubGroupTyping,
|
||||
unsubGroupNotice,
|
||||
unsubConnected,
|
||||
unsubDisconnected,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理新消息
|
||||
*/
|
||||
private async handleNewMessage(message: any): Promise<void> {
|
||||
const { conversation_id, id, sender_id, seq, segments, created_at, _isAck } = message;
|
||||
|
||||
// 消息去重检查
|
||||
if (this.isMessageProcessed(id)) {
|
||||
return;
|
||||
}
|
||||
this.markMessageAsProcessed(id);
|
||||
|
||||
// 构造消息对象
|
||||
const newMessage = createMessage({
|
||||
id,
|
||||
conversationId: String(conversation_id),
|
||||
senderId: sender_id,
|
||||
seq,
|
||||
segments: segments || [],
|
||||
createdAt: created_at,
|
||||
});
|
||||
|
||||
// 异步获取发送者信息(如果是群聊且不是当前用户发送的)
|
||||
if (message.type === 'group_message' && sender_id && sender_id !== this.currentUserId) {
|
||||
this.enrichMessageWithSender(newMessage);
|
||||
}
|
||||
|
||||
// 异步保存到本地数据库
|
||||
const isRead = _isAck || sender_id === this.currentUserId;
|
||||
messageRepository.saveMessage({
|
||||
id: newMessage.id,
|
||||
conversationId: newMessage.conversationId,
|
||||
senderId: newMessage.senderId,
|
||||
seq: newMessage.seq,
|
||||
segments: newMessage.segments,
|
||||
createdAt: newMessage.createdAt,
|
||||
status: newMessage.status || 'normal',
|
||||
isRead,
|
||||
}).catch((error) => {
|
||||
console.error('[ProcessMessageUseCase] 保存消息失败:', error);
|
||||
});
|
||||
|
||||
// 通知订阅者
|
||||
this.notifySubscribers({
|
||||
type: 'message_received',
|
||||
payload: {
|
||||
message: newMessage,
|
||||
isCurrentUser: sender_id === this.currentUserId,
|
||||
isAck: _isAck,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 丰富消息的发送者信息
|
||||
*/
|
||||
private async enrichMessageWithSender(message: Message): Promise<void> {
|
||||
try {
|
||||
const user = await this.getSenderInfo(message.senderId);
|
||||
if (user) {
|
||||
message.sender = user;
|
||||
// 重新通知,包含发送者信息
|
||||
this.notifySubscribers({
|
||||
type: 'message_received',
|
||||
payload: {
|
||||
message,
|
||||
isCurrentUser: false,
|
||||
isUpdated: true,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ProcessMessageUseCase] 获取发送者信息失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取发送者信息(带缓存和去重)
|
||||
*/
|
||||
private async getSenderInfo(userId: string): Promise<any | null> {
|
||||
// 先检查本地缓存
|
||||
const cachedUser = await userCacheRepository.get(userId);
|
||||
if (cachedUser) {
|
||||
return cachedUser;
|
||||
}
|
||||
|
||||
// 检查是否已有正在进行的请求
|
||||
const pendingRequest = this.pendingUserRequests.get(userId);
|
||||
if (pendingRequest) {
|
||||
return pendingRequest;
|
||||
}
|
||||
|
||||
// 发起新请求
|
||||
const request = this.fetchUserInfo(userId);
|
||||
this.pendingUserRequests.set(userId, request);
|
||||
|
||||
try {
|
||||
const user = await request;
|
||||
return user;
|
||||
} finally {
|
||||
this.pendingUserRequests.delete(userId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从服务器获取用户信息
|
||||
*/
|
||||
private async fetchUserInfo(userId: string): Promise<any | null> {
|
||||
try {
|
||||
const response = await api.get<any>(`/users/${userId}`);
|
||||
if (response.code === 0 && response.data) {
|
||||
await userCacheRepository.save(response.data as any);
|
||||
return response.data;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error(`[ProcessMessageUseCase] 获取用户信息失败: ${userId}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查消息是否已处理
|
||||
*/
|
||||
private isMessageProcessed(messageId: string): boolean {
|
||||
return this.processedMessageIds.has(messageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记消息已处理
|
||||
*/
|
||||
private markMessageAsProcessed(messageId: string): void {
|
||||
this.processedMessageIds.add(messageId);
|
||||
this.processedMessageIdsExpiry.set(messageId, Date.now());
|
||||
|
||||
// 定期清理
|
||||
if (this.processedMessageIds.size > 1000) {
|
||||
this.cleanupProcessedMessageIds();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的消息ID
|
||||
*/
|
||||
private cleanupProcessedMessageIds(): void {
|
||||
const now = Date.now();
|
||||
for (const [id, timestamp] of this.processedMessageIdsExpiry.entries()) {
|
||||
if (now - timestamp > this.MESSAGE_ID_EXPIRY) {
|
||||
this.processedMessageIds.delete(id);
|
||||
this.processedMessageIdsExpiry.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理私聊已读回执
|
||||
*/
|
||||
private handleReadReceipt(message: any): void {
|
||||
// 可以在这里处理对方已读的状态更新
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理群聊已读回执
|
||||
*/
|
||||
private handleGroupReadReceipt(message: any): void {
|
||||
// 可以在这里处理群聊已读状态更新
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理私聊消息撤回
|
||||
*/
|
||||
private handleRecallMessage(message: any): Promise<void> {
|
||||
const { conversation_id, message_id } = message;
|
||||
|
||||
// 更新本地数据库状态
|
||||
messageRepository
|
||||
.updateStatus(message_id, 'recalled', true)
|
||||
.catch((error: unknown) => {
|
||||
console.error('[ProcessMessageUseCase] 更新撤回状态失败:', error);
|
||||
});
|
||||
|
||||
// 通知订阅者
|
||||
this.notifySubscribers({
|
||||
type: 'message_recalled',
|
||||
payload: {
|
||||
conversationId: String(conversation_id),
|
||||
messageId: message_id,
|
||||
isGroup: false,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理群聊消息撤回
|
||||
*/
|
||||
private handleGroupRecallMessage(message: any): Promise<void> {
|
||||
const { conversation_id, message_id } = message;
|
||||
|
||||
// 更新本地数据库状态
|
||||
messageRepository
|
||||
.updateStatus(message_id, 'recalled', true)
|
||||
.catch((error: unknown) => {
|
||||
console.error('[ProcessMessageUseCase] 更新撤回状态失败:', error);
|
||||
});
|
||||
|
||||
// 通知订阅者
|
||||
this.notifySubscribers({
|
||||
type: 'message_recalled',
|
||||
payload: {
|
||||
conversationId: String(conversation_id),
|
||||
messageId: message_id,
|
||||
isGroup: true,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理群聊输入状态
|
||||
*/
|
||||
private handleGroupTyping(message: any): void {
|
||||
const { group_id, user_id, is_typing } = message;
|
||||
|
||||
this.notifySubscribers({
|
||||
type: 'typing_status',
|
||||
payload: {
|
||||
groupId: String(group_id),
|
||||
userId: user_id,
|
||||
isTyping: is_typing,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理群通知
|
||||
*/
|
||||
private handleGroupNotice(message: any): void {
|
||||
const { notice_type, group_id, data, timestamp, message_id, seq } = message;
|
||||
const groupIdStr = String(group_id);
|
||||
|
||||
const notice: GroupNotice = {
|
||||
type: notice_type,
|
||||
groupId: groupIdStr,
|
||||
data: data || {},
|
||||
timestamp: timestamp || Date.now(),
|
||||
messageId: message_id,
|
||||
seq,
|
||||
};
|
||||
|
||||
// 通知订阅者
|
||||
this.notifySubscribers({
|
||||
type: 'group_notice',
|
||||
payload: {
|
||||
notice,
|
||||
text: buildGroupNoticeText(notice),
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记会话已读
|
||||
*/
|
||||
async markAsRead(conversationId: string, seq: number): Promise<void> {
|
||||
const existingRecord = this.pendingReadMap.get(conversationId);
|
||||
|
||||
// 使用seq去重
|
||||
if (existingRecord && seq <= existingRecord.lastReadSeq) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 清除可能存在的旧定时器
|
||||
if (existingRecord?.clearTimer) {
|
||||
clearTimeout(existingRecord.clearTimer);
|
||||
}
|
||||
|
||||
// 递增全局版本号
|
||||
this.readStateVersion++;
|
||||
const currentVersion = this.readStateVersion;
|
||||
|
||||
// 记录已读状态
|
||||
this.pendingReadMap.set(conversationId, {
|
||||
timestamp: Date.now(),
|
||||
version: currentVersion,
|
||||
lastReadSeq: seq,
|
||||
});
|
||||
|
||||
// 更新本地数据库
|
||||
await messageRepository.markConversationAsRead(conversationId);
|
||||
|
||||
// 调用API
|
||||
try {
|
||||
await messageService.markAsRead(conversationId, seq);
|
||||
} catch (error) {
|
||||
console.error('[ProcessMessageUseCase] 标记已读API失败:', error);
|
||||
this.pendingReadMap.delete(conversationId);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 设置延迟清除保护
|
||||
const clearTimer = setTimeout(() => {
|
||||
const record = this.pendingReadMap.get(conversationId);
|
||||
if (record && record.version === currentVersion) {
|
||||
this.pendingReadMap.delete(conversationId);
|
||||
}
|
||||
}, this.READ_STATE_PROTECTION_DELAY);
|
||||
|
||||
this.pendingReadMap.set(conversationId, {
|
||||
timestamp: Date.now(),
|
||||
version: currentVersion,
|
||||
lastReadSeq: seq,
|
||||
clearTimer,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查会话是否有进行中的已读请求
|
||||
*/
|
||||
hasPendingRead(conversationId: string): boolean {
|
||||
return this.pendingReadMap.has(conversationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取进行中的已读记录
|
||||
*/
|
||||
getPendingReadRecord(conversationId: string): ReadStateRecord | undefined {
|
||||
return this.pendingReadMap.get(conversationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
async sendMessage(
|
||||
conversationId: string,
|
||||
segments: any[],
|
||||
options?: { replyToId?: string }
|
||||
): Promise<Message | null> {
|
||||
try {
|
||||
const response = await messageService.sendMessage(conversationId, {
|
||||
segments,
|
||||
reply_to_id: options?.replyToId,
|
||||
});
|
||||
|
||||
if (response) {
|
||||
const message = createMessage({
|
||||
id: response.id,
|
||||
conversationId,
|
||||
senderId: this.currentUserId || '',
|
||||
seq: response.seq,
|
||||
segments,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// 保存到本地数据库
|
||||
await messageRepository.saveMessage({
|
||||
id: message.id,
|
||||
conversationId: message.conversationId,
|
||||
senderId: message.senderId,
|
||||
seq: message.seq,
|
||||
segments: message.segments,
|
||||
createdAt: message.createdAt,
|
||||
status: message.status || 'normal',
|
||||
isRead: true,
|
||||
});
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('[ProcessMessageUseCase] 发送消息失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本地消息
|
||||
*/
|
||||
async getLocalMessages(conversationId: string, limit: number = 20): Promise<Message[]> {
|
||||
const cached = await messageRepository.getByConversation(conversationId, limit);
|
||||
return cached.map(cachedMessageToMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取历史消息
|
||||
*/
|
||||
async getHistoryMessages(
|
||||
conversationId: string,
|
||||
beforeSeq: number,
|
||||
limit: number = 20
|
||||
): Promise<Message[]> {
|
||||
// 先从本地获取
|
||||
const localMessages = await messageRepository.getBeforeSeq(
|
||||
conversationId,
|
||||
beforeSeq,
|
||||
limit
|
||||
);
|
||||
|
||||
if (localMessages.length >= limit) {
|
||||
return localMessages.map(cachedMessageToMessage);
|
||||
}
|
||||
|
||||
// 本地数据不足,从服务端获取
|
||||
try {
|
||||
const response = await messageService.getMessages(
|
||||
conversationId,
|
||||
undefined,
|
||||
beforeSeq,
|
||||
limit
|
||||
);
|
||||
|
||||
if (response?.messages && response.messages.length > 0) {
|
||||
const serverMessages = response.messages.map((m: any) =>
|
||||
createMessage({
|
||||
id: m.id,
|
||||
conversationId: m.conversation_id || conversationId,
|
||||
senderId: m.sender_id,
|
||||
seq: m.seq,
|
||||
segments: m.segments || [],
|
||||
createdAt: m.created_at,
|
||||
status: m.status || 'normal',
|
||||
})
|
||||
);
|
||||
|
||||
// 保存到本地
|
||||
await messageRepository.saveMessagesBatch(
|
||||
MessageMapper.toCachedMessages(serverMessages, conversationId)
|
||||
);
|
||||
|
||||
return serverMessages;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ProcessMessageUseCase] 获取历史消息失败:', error);
|
||||
}
|
||||
|
||||
return localMessages.map(cachedMessageToMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从服务端同步消息
|
||||
*/
|
||||
async syncMessagesFromServer(
|
||||
conversationId: string,
|
||||
afterSeq?: number
|
||||
): Promise<Message[]> {
|
||||
try {
|
||||
const response = await messageService.getMessages(conversationId, afterSeq);
|
||||
|
||||
if (response?.messages && response.messages.length > 0) {
|
||||
const messages = response.messages.map((m: any) =>
|
||||
createMessage({
|
||||
id: m.id,
|
||||
conversationId: m.conversation_id || conversationId,
|
||||
senderId: m.sender_id,
|
||||
seq: m.seq,
|
||||
segments: m.segments || [],
|
||||
createdAt: m.created_at,
|
||||
status: m.status || 'normal',
|
||||
})
|
||||
);
|
||||
|
||||
await messageRepository.saveMessagesBatch(
|
||||
MessageMapper.toCachedMessages(response.messages, conversationId)
|
||||
);
|
||||
|
||||
return messages;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ProcessMessageUseCase] 同步消息失败:', error);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅事件
|
||||
*/
|
||||
subscribe(subscriber: MessageUseCaseSubscriber): () => void {
|
||||
this.subscribers.add(subscriber);
|
||||
|
||||
return () => {
|
||||
this.subscribers.delete(subscriber);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知订阅者
|
||||
*/
|
||||
private notifySubscribers(event: MessageUseCaseEvent): void {
|
||||
this.subscribers.forEach((subscriber) => {
|
||||
try {
|
||||
subscriber(event);
|
||||
} catch (error) {
|
||||
console.error('[ProcessMessageUseCase] 订阅者执行失败:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户ID
|
||||
*/
|
||||
getCurrentUserId(): string | null {
|
||||
return this.currentUserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前用户ID
|
||||
*/
|
||||
setCurrentUserId(userId: string | null): void {
|
||||
this.currentUserId = userId;
|
||||
}
|
||||
}
|
||||
|
||||
export const processMessageUseCase = new ProcessMessageUseCase();
|
||||
export default processMessageUseCase;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,3 @@
|
||||
/**
|
||||
* 帖子差异更新 Hook
|
||||
* 接收原始帖子列表,返回优化后的帖子列表,自动处理批量更新
|
||||
* 集成 ProcessPostUseCase 进行状态管理
|
||||
*/
|
||||
|
||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
PostIdentifier,
|
||||
@@ -20,98 +14,47 @@ import {
|
||||
PostBatcherOptions,
|
||||
createPostUpdateBatcher,
|
||||
} from '../infrastructure/diff/PostUpdateBatcher';
|
||||
import { processPostUseCase } from '../core/usecases/ProcessPostUseCase';
|
||||
import type { PostUseCaseEvent, PostsState } from '../core/usecases/ProcessPostUseCase';
|
||||
import { postSyncService } from '../services/post/PostSyncService';
|
||||
import type { PostsState } from '../services/post/PostSyncService';
|
||||
import { usePostListStore } from '../stores/post/postListStore';
|
||||
import type { Post } from '../core/entities/Post';
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
/**
|
||||
* 差异更新 Hook 配置选项
|
||||
*/
|
||||
export interface UseDifferentialPostsOptions<T extends PostIdentifier> {
|
||||
/** 差异计算配置 */
|
||||
diffConfig?: PostDiffConfig;
|
||||
/** 批量处理配置 */
|
||||
batcherOptions?: PostBatcherOptions;
|
||||
/** 是否启用差异计算,默认 true */
|
||||
enableDiff?: boolean;
|
||||
/** 是否启用批量处理,默认 true */
|
||||
enableBatching?: boolean;
|
||||
/** 最大帖子数量限制,超出时触发重置 */
|
||||
maxPostCount?: number;
|
||||
/** 变化比例阈值(0-1),超过此值触发重置 */
|
||||
changeRatioThreshold?: number;
|
||||
/** 列表键(用于区分不同的帖子列表) */
|
||||
listKey?: string;
|
||||
/** 是否自动订阅 UseCase 状态变化,默认 true */
|
||||
autoSubscribe?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 差异更新信息
|
||||
*/
|
||||
export interface DiffUpdatesInfo {
|
||||
/** 新增数量 */
|
||||
addedCount: number;
|
||||
/** 更新数量 */
|
||||
updatedCount: number;
|
||||
/** 删除数量 */
|
||||
deletedCount: number;
|
||||
/** 上次更新时间 */
|
||||
lastUpdateTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 差异更新 Hook 返回值
|
||||
*/
|
||||
export interface UseDifferentialPostsResult<T extends PostIdentifier> {
|
||||
/** 优化后的帖子列表 */
|
||||
posts: T[];
|
||||
/** 是否正在加载 */
|
||||
loading: boolean;
|
||||
/** 是否首屏加载 */
|
||||
isInitialLoading: boolean;
|
||||
/** 是否正在加载更多 */
|
||||
isLoadingMore: boolean;
|
||||
/** 是否正在刷新 */
|
||||
refreshing: boolean;
|
||||
/** 错误信息 */
|
||||
error: string | null;
|
||||
/** 是否有更多数据 */
|
||||
hasMore: boolean;
|
||||
/** 待处理更新数量 */
|
||||
pendingUpdateCount: number;
|
||||
/** 是否正在处理更新 */
|
||||
isProcessing: boolean;
|
||||
/** 刷新方法 */
|
||||
refresh: () => Promise<void>;
|
||||
/** 加载更多方法 */
|
||||
loadMore: () => Promise<void>;
|
||||
/** 手动刷新批量处理队列 */
|
||||
flush: () => void;
|
||||
/** 重置方法 */
|
||||
reset: () => void;
|
||||
/** 强制更新(跳过批量处理) */
|
||||
forceUpdate: (posts: T[]) => void;
|
||||
/** 获取差异统计信息 */
|
||||
getDiffStats: () => {
|
||||
totalBatches: number;
|
||||
totalUpdates: number;
|
||||
averageBatchSize: number;
|
||||
};
|
||||
/** 差异更新信息 */
|
||||
getDiffStats: () => { totalBatches: number; totalUpdates: number; averageBatchSize: number };
|
||||
diffUpdates: DiffUpdatesInfo;
|
||||
}
|
||||
|
||||
// ==================== Hook 实现 ====================
|
||||
|
||||
/**
|
||||
* 帖子差异更新 Hook
|
||||
* @param initialPosts 初始帖子列表(可选)
|
||||
* @param options 配置选项
|
||||
* @returns 优化后的帖子列表和相关方法
|
||||
*/
|
||||
export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
initialPosts: T[] = [],
|
||||
options: UseDifferentialPostsOptions<T> = {}
|
||||
@@ -126,8 +69,6 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
autoSubscribe = true,
|
||||
} = options;
|
||||
|
||||
// ==================== 状态 ====================
|
||||
|
||||
const [posts, setPosts] = useState<T[]>(initialPosts);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
@@ -142,23 +83,15 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
lastUpdateTime: 0,
|
||||
});
|
||||
|
||||
// ==================== Refs ====================
|
||||
|
||||
const calculatorRef = useRef<PostDiffCalculator<T> | null>(null);
|
||||
const batcherRef = useRef<PostUpdateBatcher | null>(null);
|
||||
const previousPostsRef = useRef<T[]>(initialPosts);
|
||||
const unsubscribeRef = useRef<(() => void) | null>(null);
|
||||
|
||||
// ==================== 批量更新处理(须在订阅 batcher 的 useEffect 之前定义)====================
|
||||
|
||||
/**
|
||||
* 处理批量更新
|
||||
*/
|
||||
const handleBatchUpdates = useCallback((updates: PostUpdate[]) => {
|
||||
setIsProcessing(true);
|
||||
|
||||
try {
|
||||
setPosts((currentPosts) => {
|
||||
setPosts(currentPosts => {
|
||||
let newPosts = [...currentPosts];
|
||||
let addedCount = 0;
|
||||
let updatedCount = 0;
|
||||
@@ -210,9 +143,7 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
const deleteUpdate = update as any;
|
||||
const prevLength = newPosts.length;
|
||||
newPosts = newPosts.filter(p => p.id !== deleteUpdate.postId);
|
||||
if (newPosts.length < prevLength) {
|
||||
deletedCount++;
|
||||
}
|
||||
if (newPosts.length < prevLength) deletedCount++;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -243,7 +174,6 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
}
|
||||
}
|
||||
|
||||
// 更新差异统计
|
||||
if (addedCount > 0 || updatedCount > 0 || deletedCount > 0) {
|
||||
setDiffUpdates(prev => ({
|
||||
addedCount: prev.addedCount + addedCount,
|
||||
@@ -261,28 +191,17 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
}
|
||||
}, []);
|
||||
|
||||
// ==================== 初始化 ====================
|
||||
|
||||
// 初始化差异计算器
|
||||
useEffect(() => {
|
||||
if (enableDiff) {
|
||||
calculatorRef.current = createPostDiffCalculator<T>(diffConfig);
|
||||
}
|
||||
return () => {
|
||||
calculatorRef.current = null;
|
||||
};
|
||||
return () => { calculatorRef.current = null; };
|
||||
}, [enableDiff, diffConfig]);
|
||||
|
||||
// 初始化批量处理器
|
||||
useEffect(() => {
|
||||
if (!enableBatching) return;
|
||||
|
||||
batcherRef.current = createPostUpdateBatcher(batcherOptions);
|
||||
|
||||
const unsubscribe = batcherRef.current.subscribe((updates) => {
|
||||
handleBatchUpdates(updates);
|
||||
});
|
||||
|
||||
const unsubscribe = batcherRef.current.subscribe(handleBatchUpdates);
|
||||
return () => {
|
||||
unsubscribe();
|
||||
batcherRef.current?.destroy();
|
||||
@@ -290,144 +209,46 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
};
|
||||
}, [enableBatching, batcherOptions, handleBatchUpdates]);
|
||||
|
||||
// ==================== UseCase 订阅 ====================
|
||||
const syncFromStore = useCallback((state: PostsState) => {
|
||||
setLoading(state.isLoading);
|
||||
setRefreshing(state.isRefreshing);
|
||||
setError(state.error);
|
||||
setHasMore(state.hasMore);
|
||||
|
||||
/**
|
||||
* 处理 UseCase 事件
|
||||
*/
|
||||
const handleUseCaseEvent = useCallback((event: PostUseCaseEvent) => {
|
||||
switch (event.type) {
|
||||
case 'state_changed': {
|
||||
const { key: eventKey, state } = event.payload as { key: string; state: PostsState };
|
||||
// 全局订阅:只应用当前列表 key,避免其他 Tab/列表的 state 覆盖本列表(如 createPost 会更新所有 key)
|
||||
if (eventKey !== listKey) {
|
||||
break;
|
||||
}
|
||||
if (state) {
|
||||
setLoading(state.isLoading);
|
||||
setRefreshing(state.isRefreshing);
|
||||
setError(state.error);
|
||||
setHasMore(state.hasMore);
|
||||
|
||||
// ProcessPostUseCase 的 state.posts 已是合并后的完整列表。
|
||||
// 不再走差异计算 + PostUpdateBatcher:批量应用晚于 previousPostsRef 更新时,
|
||||
// BATCH_ADD 会在过短的 currentPosts 上 splice,导致连续游标加载「请求成功但列表不增长」。
|
||||
batcherRef.current?.clearPending();
|
||||
calculatorRef.current?.reset();
|
||||
if (state.posts && state.posts.length > 0) {
|
||||
const next = state.posts as unknown as T[];
|
||||
if (next.length > maxPostCount) {
|
||||
console.warn(`[useDifferentialPosts] Post count (${next.length}) exceeds limit (${maxPostCount})`);
|
||||
}
|
||||
setPosts(next);
|
||||
previousPostsRef.current = next;
|
||||
} else {
|
||||
setPosts([]);
|
||||
previousPostsRef.current = [];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'posts_loaded': {
|
||||
// fetchPosts 在 updatePostsState 时已发出 state_changed(含合并后 posts),此处不再应用,避免与其它 key 竞态或重复计算
|
||||
break;
|
||||
}
|
||||
|
||||
case 'post_created': {
|
||||
const { post } = event.payload;
|
||||
if (post) {
|
||||
// 新帖子添加到列表开头
|
||||
setPosts(prev => [post as T, ...prev]);
|
||||
setDiffUpdates(prev => ({
|
||||
...prev,
|
||||
addedCount: prev.addedCount + 1,
|
||||
lastUpdateTime: Date.now(),
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'post_updated': {
|
||||
const { post } = event.payload;
|
||||
if (post) {
|
||||
setPosts(prev => prev.map(p => p.id === post.id ? { ...p, ...post } as T : p));
|
||||
setDiffUpdates(prev => ({
|
||||
...prev,
|
||||
updatedCount: prev.updatedCount + 1,
|
||||
lastUpdateTime: Date.now(),
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'post_deleted': {
|
||||
const { postId } = event.payload;
|
||||
if (postId) {
|
||||
setPosts(prev => prev.filter(p => p.id !== postId));
|
||||
setDiffUpdates(prev => ({
|
||||
...prev,
|
||||
deletedCount: prev.deletedCount + 1,
|
||||
lastUpdateTime: Date.now(),
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'loading_changed': {
|
||||
const { isLoading } = event.payload;
|
||||
setLoading(isLoading);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'error_occurred': {
|
||||
const payload = event.payload as { key?: string; error?: string };
|
||||
if (payload.key !== undefined && payload.key !== listKey) {
|
||||
break;
|
||||
}
|
||||
if (payload.error !== undefined) {
|
||||
setError(payload.error);
|
||||
}
|
||||
break;
|
||||
batcherRef.current?.clearPending();
|
||||
calculatorRef.current?.reset();
|
||||
if (state.posts && state.posts.length > 0) {
|
||||
const next = state.posts as unknown as T[];
|
||||
if (next.length > maxPostCount) {
|
||||
console.warn(`[useDifferentialPosts] Post count (${next.length}) exceeds limit (${maxPostCount})`);
|
||||
}
|
||||
setPosts(next);
|
||||
previousPostsRef.current = next;
|
||||
} else {
|
||||
setPosts([]);
|
||||
previousPostsRef.current = [];
|
||||
}
|
||||
}, [listKey, maxPostCount]);
|
||||
}, [maxPostCount]);
|
||||
|
||||
// 订阅 UseCase 状态变化
|
||||
useEffect(() => {
|
||||
if (autoSubscribe) {
|
||||
unsubscribeRef.current = processPostUseCase.subscribe(handleUseCaseEvent);
|
||||
if (!autoSubscribe) return;
|
||||
|
||||
// 初始化时获取当前状态
|
||||
const currentState = processPostUseCase.getPostsState(listKey);
|
||||
if (currentState.posts.length > 0) {
|
||||
setPosts(currentState.posts as unknown as T[]);
|
||||
previousPostsRef.current = currentState.posts as unknown as T[];
|
||||
}
|
||||
setLoading(currentState.isLoading);
|
||||
setRefreshing(currentState.isRefreshing);
|
||||
setError(currentState.error);
|
||||
setHasMore(currentState.hasMore);
|
||||
const currentState = usePostListStore.getState().getPostsState(listKey);
|
||||
syncFromStore(currentState);
|
||||
|
||||
return () => {
|
||||
if (unsubscribeRef.current) {
|
||||
unsubscribeRef.current();
|
||||
unsubscribeRef.current = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [autoSubscribe, listKey, handleUseCaseEvent]);
|
||||
const unsubscribe = usePostListStore.subscribe(state => {
|
||||
const postsState = state.postsStateMap.get(listKey);
|
||||
if (postsState) syncFromStore(postsState);
|
||||
});
|
||||
|
||||
// ==================== 操作方法 ====================
|
||||
return unsubscribe;
|
||||
}, [autoSubscribe, listKey, syncFromStore]);
|
||||
|
||||
/**
|
||||
* 刷新帖子列表
|
||||
*/
|
||||
const refresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
setError(null);
|
||||
try {
|
||||
await processPostUseCase.refreshPosts(listKey);
|
||||
await postSyncService.refreshPosts(listKey);
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : '刷新失败';
|
||||
setError(errorMsg);
|
||||
@@ -436,15 +257,12 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
}
|
||||
}, [listKey]);
|
||||
|
||||
/**
|
||||
* 加载更多帖子
|
||||
*/
|
||||
const loadMore = useCallback(async () => {
|
||||
if (hasMore && !loading) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await processPostUseCase.loadMorePosts(listKey);
|
||||
await postSyncService.loadMorePosts(listKey);
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : '加载更多失败';
|
||||
setError(errorMsg);
|
||||
@@ -454,16 +272,8 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
}
|
||||
}, [listKey, hasMore, loading]);
|
||||
|
||||
/**
|
||||
* 手动刷新批量处理队列
|
||||
*/
|
||||
const flush = useCallback(() => {
|
||||
batcherRef.current?.flush();
|
||||
}, []);
|
||||
const flush = useCallback(() => { batcherRef.current?.flush(); }, []);
|
||||
|
||||
/**
|
||||
* 重置方法
|
||||
*/
|
||||
const reset = useCallback(() => {
|
||||
calculatorRef.current?.reset();
|
||||
batcherRef.current?.clearPending();
|
||||
@@ -473,25 +283,14 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
setError(null);
|
||||
setHasMore(true);
|
||||
previousPostsRef.current = [];
|
||||
setDiffUpdates({
|
||||
addedCount: 0,
|
||||
updatedCount: 0,
|
||||
deletedCount: 0,
|
||||
lastUpdateTime: 0,
|
||||
});
|
||||
setDiffUpdates({ addedCount: 0, updatedCount: 0, deletedCount: 0, lastUpdateTime: 0 });
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 强制更新方法
|
||||
*/
|
||||
const forceUpdate = useCallback((newPosts: T[]) => {
|
||||
setPosts(newPosts);
|
||||
previousPostsRef.current = newPosts;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*/
|
||||
const getDiffStats = useCallback(() => {
|
||||
const batcherStats = batcherRef.current?.getStats();
|
||||
return {
|
||||
@@ -501,20 +300,14 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
};
|
||||
}, []);
|
||||
|
||||
// ==================== 定期更新待处理数量 ====================
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableBatching || !batcherRef.current) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setPendingUpdateCount(batcherRef.current?.getPendingCount() ?? 0);
|
||||
}, 50);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [enableBatching]);
|
||||
|
||||
// ==================== 返回值 ====================
|
||||
|
||||
return {
|
||||
posts,
|
||||
loading,
|
||||
|
||||
@@ -34,7 +34,7 @@ import type { PostCardAction } from '../../components/business/PostCard';
|
||||
import { Loading, EmptyState, Text, ImageGallery, ImageGridItem, ResponsiveGrid } from '../../components/common';
|
||||
import { useResponsive, useResponsiveSpacing } from '../../hooks/useResponsive';
|
||||
import { useDifferentialPosts } from '../../hooks/useDifferentialPosts';
|
||||
import { processPostUseCase } from '../../core/usecases/ProcessPostUseCase';
|
||||
import { postSyncService } from '../../services/post/PostSyncService';
|
||||
import { SearchScreen } from './SearchScreen';
|
||||
import { CreatePostScreen } from '../create/CreatePostScreen';
|
||||
import * as hrefs from '../../navigation/hrefs';
|
||||
@@ -375,7 +375,7 @@ export const HomeScreen: React.FC = () => {
|
||||
isLoadingMoreRef.current = true;
|
||||
const startedAt = Date.now();
|
||||
try {
|
||||
await processPostUseCase.loadMorePosts(listKey);
|
||||
await postSyncService.loadMorePosts(listKey);
|
||||
console.log('[PostPerf] home loadMore', {
|
||||
listKey,
|
||||
costMs: Date.now() - startedAt,
|
||||
@@ -409,7 +409,7 @@ export const HomeScreen: React.FC = () => {
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
// 先获取正确类型的帖子
|
||||
await processPostUseCase.fetchPosts(
|
||||
await postSyncService.fetchPosts(
|
||||
{
|
||||
page: 1,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
@@ -554,9 +554,9 @@ export const HomeScreen: React.FC = () => {
|
||||
const handleLike = async (post: Post) => {
|
||||
try {
|
||||
if (post.is_liked) {
|
||||
await processPostUseCase.unlikePost(post.id);
|
||||
await postSyncService.unlikePost(post.id);
|
||||
} else {
|
||||
await processPostUseCase.likePost(post.id);
|
||||
await postSyncService.likePost(post.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('点赞操作失败:', error);
|
||||
@@ -567,9 +567,9 @@ export const HomeScreen: React.FC = () => {
|
||||
const handleBookmark = async (post: Post) => {
|
||||
try {
|
||||
if (post.is_favorited) {
|
||||
await processPostUseCase.unfavoritePost(post.id);
|
||||
await postSyncService.unfavoritePost(post.id);
|
||||
} else {
|
||||
await processPostUseCase.favoritePost(post.id);
|
||||
await postSyncService.favoritePost(post.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('收藏操作失败:', error);
|
||||
@@ -582,7 +582,7 @@ export const HomeScreen: React.FC = () => {
|
||||
try {
|
||||
const res = await postService.sharePost(post.id, { channel: 'copy_link' });
|
||||
if (res.ok && res.shares_count != null) {
|
||||
processPostUseCase.applyShareCountUpdate(post.id, res.shares_count);
|
||||
postSyncService.applyShareCountUpdate(post.id, res.shares_count);
|
||||
}
|
||||
} catch (shareError) {
|
||||
console.error('上报分享次数失败:', shareError);
|
||||
|
||||
@@ -37,7 +37,7 @@ import { Post, Comment, VoteResultDTO, VoteOptionDTO } from '../../types';
|
||||
import { useUserStore } from '../../stores';
|
||||
import { useCurrentUser } from '../../stores/authStore';
|
||||
import { postService, commentService, uploadService, authService, showPrompt, voteService } from '../../services';
|
||||
import { processPostUseCase } from '../../core/usecases/ProcessPostUseCase';
|
||||
import { postSyncService } from '../../services/post/PostSyncService';
|
||||
import { useCursorPagination } from '../../hooks/useCursorPagination';
|
||||
import { CommentItem, VoteCard, ReportDialog } from '../../components/business';
|
||||
import { Avatar, Button, Loading, EmptyState, Text, ImageGallery, ImageGrid, ImageGridItem, AdaptiveLayout, AppBackButton } from '../../components/common';
|
||||
@@ -238,7 +238,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
|
||||
try {
|
||||
// 使用 ProcessPostUseCase 获取帖子详情
|
||||
const postData = await processPostUseCase.fetchPostById(postId);
|
||||
const postData = await postSyncService.fetchPostById(postId);
|
||||
if (postData) {
|
||||
// 类型转换:将 core/entities/Post 转换为 PostDTO 以保持兼容性
|
||||
setPost(postData as unknown as Post);
|
||||
@@ -487,9 +487,9 @@ export const PostDetailScreen: React.FC = () => {
|
||||
|
||||
try {
|
||||
if (originalPost.is_liked) {
|
||||
await processPostUseCase.unlikePost(post.id);
|
||||
await postSyncService.unlikePost(post.id);
|
||||
} else {
|
||||
await processPostUseCase.likePost(post.id);
|
||||
await postSyncService.likePost(post.id);
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, { context: '点赞' });
|
||||
@@ -505,9 +505,9 @@ export const PostDetailScreen: React.FC = () => {
|
||||
|
||||
try {
|
||||
if (originalPost.is_favorited) {
|
||||
await processPostUseCase.unfavoritePost(post.id);
|
||||
await postSyncService.unfavoritePost(post.id);
|
||||
} else {
|
||||
await processPostUseCase.favoritePost(post.id);
|
||||
await postSyncService.favoritePost(post.id);
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, { context: '收藏' });
|
||||
@@ -526,7 +526,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
? { ...prev, shares_count: res.shares_count!, sharesCount: res.shares_count! }
|
||||
: null
|
||||
);
|
||||
processPostUseCase.applyShareCountUpdate(post.id, res.shares_count);
|
||||
postSyncService.applyShareCountUpdate(post.id, res.shares_count);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上报分享次数失败:', error);
|
||||
@@ -635,7 +635,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
onPress: async () => {
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await processPostUseCase.deletePost(post.id);
|
||||
await postSyncService.deletePost(post.id);
|
||||
Alert.alert('删除成功', '帖子已删除', [
|
||||
{
|
||||
text: '确定',
|
||||
|
||||
@@ -20,6 +20,7 @@ import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '
|
||||
import { Post, User } from '../../types';
|
||||
import { useUserStore } from '../../stores';
|
||||
import { postService, authService } from '../../services';
|
||||
import { postSyncService } from '../../services/post/PostSyncService';
|
||||
import { PostCard, TabBar, SearchBar } from '../../components/business';
|
||||
import type { PostCardAction } from '../../components/business/PostCard';
|
||||
import { Avatar, EmptyState, Text, ResponsiveGrid, Loading } from '../../components/common';
|
||||
@@ -185,19 +186,19 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack }) => {
|
||||
}
|
||||
break;
|
||||
case 'like':
|
||||
postService.likePost(post.id);
|
||||
postSyncService.likePost(post.id);
|
||||
break;
|
||||
case 'unlike':
|
||||
postService.unlikePost(post.id);
|
||||
postSyncService.unlikePost(post.id);
|
||||
break;
|
||||
case 'comment':
|
||||
handlePostPress(post.id, true);
|
||||
break;
|
||||
case 'bookmark':
|
||||
postService.favoritePost(post.id);
|
||||
postSyncService.favoritePost(post.id);
|
||||
break;
|
||||
case 'unbookmark':
|
||||
postService.unfavoritePost(post.id);
|
||||
postSyncService.unfavoritePost(post.id);
|
||||
break;
|
||||
case 'share':
|
||||
// 搜索页暂不处理分享
|
||||
|
||||
@@ -862,11 +862,9 @@ function createSegmentStyles(colors: AppColors, fontSize: number = 16) {
|
||||
// 容器
|
||||
segmentsContainer: {
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
},
|
||||
segmentsColumn: {
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
},
|
||||
inlineChunkBase: {
|
||||
fontSize,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Post, User } from '../../types';
|
||||
import { useUserStore } from '../../stores';
|
||||
import { useCurrentUser } from '../../stores/authStore';
|
||||
import { postService, authService, messageService } from '../../services';
|
||||
import { postSyncService } from '../../services/post/PostSyncService';
|
||||
import { userManager } from '../../stores/userManager';
|
||||
import * as hrefs from '../../navigation/hrefs';
|
||||
import { PostCardAction } from '../../components/business/PostCard';
|
||||
@@ -310,16 +311,23 @@ export const useUserProfile = (options: UseUserProfileOptions): UseUserProfileRe
|
||||
handleUserPress(post.author.id);
|
||||
}
|
||||
break;
|
||||
case 'like':
|
||||
case 'like':
|
||||
case 'unlike':
|
||||
post.is_liked ? postService.unlikePost(post.id) : postService.likePost(post.id);
|
||||
post.is_liked ? postSyncService.unlikePost(post.id) : postSyncService.likePost(post.id);
|
||||
break;
|
||||
case 'comment':
|
||||
handlePostPress(post.id, true);
|
||||
break;
|
||||
case 'bookmark':
|
||||
case 'unbookmark':
|
||||
post.is_favorited ? postService.unfavoritePost(post.id) : postService.favoritePost(post.id);
|
||||
post.is_favorited ? postSyncService.unfavoritePost(post.id) : postSyncService.favoritePost(post.id);
|
||||
break;
|
||||
case 'comment':
|
||||
handlePostPress(post.id, true);
|
||||
break;
|
||||
case 'bookmark':
|
||||
case 'unbookmark':
|
||||
post.is_favorited ? postSyncService.unfavoritePost(post.id) : postSyncService.favoritePost(post.id);
|
||||
break;
|
||||
case 'share':
|
||||
break;
|
||||
|
||||
346
src/services/post/PostSyncService.ts
Normal file
346
src/services/post/PostSyncService.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
import { postRepository } from '../../data/repositories/PostRepository';
|
||||
import type { Post } from '../../core/entities/Post';
|
||||
import type {
|
||||
GetPostsParams,
|
||||
CreatePostData,
|
||||
UpdatePostData,
|
||||
PostsResult,
|
||||
SearchPostsParams,
|
||||
} from '../../data/repositories/interfaces/IPostRepository';
|
||||
import { PaginationStateManager } from '../../infrastructure/pagination/PaginationStateManager';
|
||||
import type { PaginationState } from '../../infrastructure/pagination/types';
|
||||
import { usePostListStore, type PostsState } from '../../stores/post/postListStore';
|
||||
import { mergePostListWithFastPath, mergeRefreshWindow } from './postMerge';
|
||||
|
||||
export type { PostsState } from '../../stores/post/postListStore';
|
||||
|
||||
export interface PostDetailState {
|
||||
post: Post | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 20;
|
||||
const MERGE_PERF_WARN_THRESHOLD_MS = 12;
|
||||
|
||||
class PostSyncService {
|
||||
private static instance: PostSyncService;
|
||||
private paginationManager: PaginationStateManager;
|
||||
private currentUserId: string | null = null;
|
||||
|
||||
private constructor() {
|
||||
this.paginationManager = new PaginationStateManager();
|
||||
}
|
||||
|
||||
static getInstance(): PostSyncService {
|
||||
if (!PostSyncService.instance) {
|
||||
PostSyncService.instance = new PostSyncService();
|
||||
}
|
||||
return PostSyncService.instance;
|
||||
}
|
||||
|
||||
initialize(currentUserId: string | null = null): void {
|
||||
this.currentUserId = currentUserId;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
usePostListStore.getState().clearAllStates();
|
||||
this.paginationManager.clear();
|
||||
this.currentUserId = null;
|
||||
}
|
||||
|
||||
getPostsState(key: string = 'default'): PostsState {
|
||||
return usePostListStore.getState().getPostsState(key);
|
||||
}
|
||||
|
||||
getPostDetailState(postId: string): PostDetailState {
|
||||
return usePostListStore.getState().getPostDetailState(postId);
|
||||
}
|
||||
|
||||
applyShareCountUpdate(postId: string, sharesCount: number): void {
|
||||
usePostListStore.getState().applyShareCountUpdate(postId, sharesCount);
|
||||
}
|
||||
|
||||
async fetchPosts(params?: GetPostsParams, key: string = 'default'): Promise<PostsResult> {
|
||||
const store = usePostListStore.getState();
|
||||
store.updatePostsState(key, { isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const queryParams: GetPostsParams = {
|
||||
pageSize: params?.pageSize || DEFAULT_PAGE_SIZE,
|
||||
...params,
|
||||
cursor: params?.cursor !== undefined ? params.cursor : '',
|
||||
};
|
||||
|
||||
const result = await postRepository.getPosts(queryParams);
|
||||
const isCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
|
||||
|
||||
const currentState = usePostListStore.getState().getPostsState(key);
|
||||
const incomingPosts = Array.isArray(result.posts) ? result.posts : [];
|
||||
const mergeStart = Date.now();
|
||||
const mergedPosts = mergeRefreshWindow(currentState.posts, incomingPosts);
|
||||
const mergeCost = Date.now() - mergeStart;
|
||||
if (mergeCost >= MERGE_PERF_WARN_THRESHOLD_MS) {
|
||||
console.log('[PostPerf] fetchPosts merge cost', { key, costMs: mergeCost, prev: currentState.posts.length, next: incomingPosts.length });
|
||||
}
|
||||
|
||||
usePostListStore.getState().updatePostsState(key, {
|
||||
posts: mergedPosts,
|
||||
hasMore: result.hasMore,
|
||||
cursor: isCursorMode ? result.nextCursor : null,
|
||||
currentPage: isCursorMode ? undefined : 1,
|
||||
isLoading: false,
|
||||
lastLoadTime: Date.now(),
|
||||
lastParams: params,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '获取帖子列表失败';
|
||||
usePostListStore.getState().updatePostsState(key, { isLoading: false, error: errorMessage });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async refreshPosts(key: string = 'default'): Promise<PostsResult> {
|
||||
const state = usePostListStore.getState().getPostsState(key);
|
||||
usePostListStore.getState().updatePostsState(key, { isRefreshing: true, error: null });
|
||||
|
||||
try {
|
||||
const result = await postRepository.getPosts({
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
...state.lastParams,
|
||||
cursor: state.lastParams?.cursor !== undefined ? state.lastParams.cursor : '',
|
||||
});
|
||||
|
||||
const isCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
|
||||
const incomingPosts = Array.isArray(result.posts) ? result.posts : [];
|
||||
const mergeStart = Date.now();
|
||||
const mergedPosts = mergeRefreshWindow(state.posts, incomingPosts);
|
||||
const mergeCost = Date.now() - mergeStart;
|
||||
if (mergeCost >= MERGE_PERF_WARN_THRESHOLD_MS) {
|
||||
console.log('[PostPerf] refreshPosts merge cost', { key, costMs: mergeCost, prev: state.posts.length, next: incomingPosts.length });
|
||||
}
|
||||
|
||||
usePostListStore.getState().updatePostsState(key, {
|
||||
posts: mergedPosts,
|
||||
hasMore: result.hasMore,
|
||||
cursor: isCursorMode ? result.nextCursor : null,
|
||||
currentPage: isCursorMode ? undefined : 1,
|
||||
isRefreshing: false,
|
||||
lastLoadTime: Date.now(),
|
||||
});
|
||||
|
||||
this.paginationManager.reset(key);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '刷新帖子列表失败';
|
||||
usePostListStore.getState().updatePostsState(key, { isRefreshing: false, error: errorMessage });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async loadMorePosts(key: string = 'default'): Promise<PostsResult> {
|
||||
const state = usePostListStore.getState().getPostsState(key);
|
||||
|
||||
if (!state.hasMore) return { posts: [], hasMore: false };
|
||||
if (state.isLoading) return { posts: [], hasMore: state.hasMore };
|
||||
|
||||
usePostListStore.getState().updatePostsState(key, { isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const useCursorMode = state.cursor !== null && state.cursor !== undefined;
|
||||
const base = { ...(state.lastParams ?? {}) } as GetPostsParams;
|
||||
let queryParams: GetPostsParams;
|
||||
|
||||
if (useCursorMode) {
|
||||
delete base.page;
|
||||
queryParams = { ...base, pageSize: DEFAULT_PAGE_SIZE, cursor: state.cursor! };
|
||||
} else {
|
||||
const nextPage = (state.currentPage || 1) + 1;
|
||||
delete base.cursor;
|
||||
queryParams = { ...base, pageSize: DEFAULT_PAGE_SIZE, page: nextPage };
|
||||
}
|
||||
|
||||
const result = await postRepository.getPosts(queryParams);
|
||||
const incomingPosts = Array.isArray(result.posts) ? result.posts : [];
|
||||
const mergeStart = Date.now();
|
||||
const newPosts = mergePostListWithFastPath(state.posts, incomingPosts);
|
||||
const mergeCost = Date.now() - mergeStart;
|
||||
if (mergeCost >= MERGE_PERF_WARN_THRESHOLD_MS) {
|
||||
console.log('[PostPerf] loadMore merge cost', { key, costMs: mergeCost, prev: state.posts.length, incoming: incomingPosts.length, merged: newPosts.length });
|
||||
}
|
||||
|
||||
const responseIsCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
|
||||
|
||||
usePostListStore.getState().updatePostsState(key, {
|
||||
posts: newPosts,
|
||||
hasMore: result.hasMore,
|
||||
cursor: responseIsCursorMode ? result.nextCursor : null,
|
||||
currentPage: useCursorMode ? state.currentPage : ((state.currentPage || 1) + 1),
|
||||
isLoading: false,
|
||||
lastLoadTime: Date.now(),
|
||||
});
|
||||
|
||||
return { posts: incomingPosts, hasMore: result.hasMore, nextCursor: result.nextCursor };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '加载更多帖子失败';
|
||||
console.error('[PostSyncService] Load more error:', errorMessage);
|
||||
usePostListStore.getState().updatePostsState(key, { isLoading: false, error: errorMessage });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchPostById(id: string): Promise<Post | null> {
|
||||
usePostListStore.getState().updatePostDetailState(id, { isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const post = await postRepository.getPostById(id);
|
||||
usePostListStore.getState().updatePostDetailState(id, { post, isLoading: false });
|
||||
return post;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '获取帖子详情失败';
|
||||
usePostListStore.getState().updatePostDetailState(id, { isLoading: false, error: errorMessage });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createPost(data: CreatePostData): Promise<Post> {
|
||||
const post = await postRepository.createPost(data);
|
||||
const store = usePostListStore.getState();
|
||||
const newMap = new Map(store.postsStateMap);
|
||||
for (const [key, postsState] of newMap) {
|
||||
newMap.set(key, { ...postsState, posts: [post, ...postsState.posts] });
|
||||
}
|
||||
usePostListStore.setState({ postsStateMap: newMap });
|
||||
return post;
|
||||
}
|
||||
|
||||
async updatePost(id: string, data: UpdatePostData): Promise<Post> {
|
||||
const post = await postRepository.updatePost(id, data);
|
||||
usePostListStore.getState().updatePostDetailState(id, { post });
|
||||
usePostListStore.getState().updatePostInLists(id, post);
|
||||
return post;
|
||||
}
|
||||
|
||||
async deletePost(id: string): Promise<void> {
|
||||
await postRepository.deletePost(id);
|
||||
usePostListStore.getState().clearPostDetailState(id);
|
||||
usePostListStore.getState().removePostFromLists(id);
|
||||
}
|
||||
|
||||
async likePost(id: string): Promise<Post> {
|
||||
const post = await postRepository.likePost(id);
|
||||
usePostListStore.getState().updatePostDetailState(id, { post });
|
||||
usePostListStore.getState().updatePostInLists(id, post);
|
||||
return post;
|
||||
}
|
||||
|
||||
async unlikePost(id: string): Promise<Post> {
|
||||
const post = await postRepository.unlikePost(id);
|
||||
usePostListStore.getState().updatePostDetailState(id, { post });
|
||||
usePostListStore.getState().updatePostInLists(id, post);
|
||||
return post;
|
||||
}
|
||||
|
||||
async favoritePost(id: string): Promise<Post> {
|
||||
const post = await postRepository.favoritePost(id);
|
||||
usePostListStore.getState().updatePostDetailState(id, { post });
|
||||
usePostListStore.getState().updatePostInLists(id, post);
|
||||
return post;
|
||||
}
|
||||
|
||||
async unfavoritePost(id: string): Promise<Post> {
|
||||
const post = await postRepository.unfavoritePost(id);
|
||||
usePostListStore.getState().updatePostDetailState(id, { post });
|
||||
usePostListStore.getState().updatePostInLists(id, post);
|
||||
return post;
|
||||
}
|
||||
|
||||
async searchPosts(params: SearchPostsParams, key: string = 'search'): Promise<PostsResult> {
|
||||
usePostListStore.getState().updatePostsState(key, { isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const result = await postRepository.searchPosts(params);
|
||||
usePostListStore.getState().updatePostsState(key, {
|
||||
posts: result.posts,
|
||||
hasMore: result.hasMore,
|
||||
cursor: result.nextCursor || null,
|
||||
isLoading: false,
|
||||
lastLoadTime: Date.now(),
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '搜索帖子失败';
|
||||
usePostListStore.getState().updatePostsState(key, { isLoading: false, error: errorMessage });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async fetchPostsByUser(userId: string, params?: GetPostsParams, key?: string): Promise<PostsResult> {
|
||||
const listKey = key || `user_${userId}`;
|
||||
usePostListStore.getState().updatePostsState(listKey, { isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
const result = await postRepository.getPostsByUser(userId, params);
|
||||
usePostListStore.getState().updatePostsState(listKey, {
|
||||
posts: result.posts,
|
||||
hasMore: result.hasMore,
|
||||
cursor: result.nextCursor || null,
|
||||
isLoading: false,
|
||||
lastLoadTime: Date.now(),
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '获取用户帖子失败';
|
||||
usePostListStore.getState().updatePostsState(listKey, { isLoading: false, error: errorMessage });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
clearPostsState(key: string = 'default'): void {
|
||||
usePostListStore.getState().clearPostsState(key);
|
||||
this.paginationManager.reset(key);
|
||||
}
|
||||
|
||||
clearPostDetailState(postId: string): void {
|
||||
usePostListStore.getState().clearPostDetailState(postId);
|
||||
}
|
||||
|
||||
clearAllStates(): void {
|
||||
usePostListStore.getState().clearAllStates();
|
||||
this.paginationManager.clear();
|
||||
}
|
||||
|
||||
getCurrentUserId(): string | null {
|
||||
return this.currentUserId;
|
||||
}
|
||||
|
||||
setCurrentUserId(userId: string | null): void {
|
||||
this.currentUserId = userId;
|
||||
}
|
||||
|
||||
getPaginationState(key: string): PaginationState {
|
||||
return this.paginationManager.getState(key);
|
||||
}
|
||||
|
||||
resetPagination(key: string): void {
|
||||
this.paginationManager.reset(key);
|
||||
}
|
||||
|
||||
isLoading(key: string = 'default'): boolean {
|
||||
return usePostListStore.getState().getPostsState(key).isLoading;
|
||||
}
|
||||
|
||||
hasError(key: string = 'default'): boolean {
|
||||
return usePostListStore.getState().getPostsState(key).error !== null;
|
||||
}
|
||||
|
||||
getError(key: string = 'default'): string | null {
|
||||
return usePostListStore.getState().getPostsState(key).error;
|
||||
}
|
||||
}
|
||||
|
||||
export const postSyncService = PostSyncService.getInstance();
|
||||
export { PostSyncService };
|
||||
export default postSyncService;
|
||||
66
src/services/post/postMerge.ts
Normal file
66
src/services/post/postMerge.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { Post } from '../../core/entities/Post';
|
||||
|
||||
export function getEntityId(item: unknown): string | null {
|
||||
if (!item || typeof item !== 'object') return null;
|
||||
const maybeId = (item as { id?: unknown }).id;
|
||||
if (typeof maybeId === 'string' || typeof maybeId === 'number') return String(maybeId);
|
||||
return null;
|
||||
}
|
||||
|
||||
export function mergePostListWithFastPath(
|
||||
previousInput: Post[] | null | undefined,
|
||||
incomingInput: Post[] | null | undefined
|
||||
): Post[] {
|
||||
const previous = Array.isArray(previousInput) ? previousInput : [];
|
||||
const incoming = Array.isArray(incomingInput) ? incomingInput : [];
|
||||
if (incoming.length === 0) return previous;
|
||||
if (previous.length === 0) return incoming;
|
||||
|
||||
const lastPrevId = getEntityId(previous[previous.length - 1]);
|
||||
const firstIncomingId = getEntityId(incoming[0]);
|
||||
if (lastPrevId && firstIncomingId && lastPrevId !== firstIncomingId) {
|
||||
const prevIdSet = new Set(
|
||||
previous.map(item => getEntityId(item)).filter(Boolean) as string[]
|
||||
);
|
||||
if (!prevIdSet.has(firstIncomingId)) return [...previous, ...incoming];
|
||||
}
|
||||
|
||||
const merged = [...previous];
|
||||
const indexById = new Map<string, number>();
|
||||
for (let i = 0; i < merged.length; i += 1) {
|
||||
const id = getEntityId(merged[i]);
|
||||
if (id) indexById.set(id, i);
|
||||
}
|
||||
|
||||
for (const post of incoming) {
|
||||
const id = getEntityId(post);
|
||||
if (!id) { merged.push(post); continue; }
|
||||
const existingIndex = indexById.get(id);
|
||||
if (existingIndex === undefined) {
|
||||
indexById.set(id, merged.length);
|
||||
merged.push(post);
|
||||
} else {
|
||||
merged[existingIndex] = post;
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
export function mergeRefreshWindow(
|
||||
previousInput: Post[] | null | undefined,
|
||||
latestInput: Post[] | null | undefined
|
||||
): Post[] {
|
||||
const previous = Array.isArray(previousInput) ? previousInput : [];
|
||||
const latest = Array.isArray(latestInput) ? latestInput : [];
|
||||
if (latest.length === 0) return previous;
|
||||
if (previous.length === 0) return latest;
|
||||
|
||||
const latestIdSet = new Set(
|
||||
latest.map(item => getEntityId(item)).filter(Boolean) as string[]
|
||||
);
|
||||
const remaining = previous.filter(item => {
|
||||
const id = getEntityId(item);
|
||||
return !id || !latestIdSet.has(id);
|
||||
});
|
||||
return [...latest, ...remaining];
|
||||
}
|
||||
@@ -1,16 +1,11 @@
|
||||
/**
|
||||
* 帖子服务
|
||||
* 处理帖子的增删改查、点赞、收藏等功能
|
||||
*
|
||||
* @deprecated 此服务已弃用,请使用 ProcessPostUseCase 代替
|
||||
* 互动操作(点赞、收藏)已委托给 processPostUseCase
|
||||
* 帖子服务 - API 层薄封装
|
||||
* 互动操作(点赞、收藏)请使用 postSyncService
|
||||
*/
|
||||
|
||||
import { api, PaginatedData } from './api';
|
||||
import { Post, CreatePostInput } from '../types';
|
||||
import { CursorPaginationRequest, CursorPaginationResponse } from '../types/dto';
|
||||
import { processPostUseCase } from '../core/usecases/ProcessPostUseCase';
|
||||
import type { Post as PostEntity } from '../core/entities/Post';
|
||||
|
||||
// 帖子列表响应
|
||||
interface PostListResponse {
|
||||
@@ -129,55 +124,6 @@ class PostService {
|
||||
}
|
||||
}
|
||||
|
||||
// 点赞帖子
|
||||
// @deprecated 请使用 processPostUseCase.likePost() 代替
|
||||
async likePost(postId: string): Promise<Post | null> {
|
||||
try {
|
||||
const post = await processPostUseCase.likePost(postId);
|
||||
// 转换为旧版 Post 类型以保持向后兼容
|
||||
return post as unknown as Post;
|
||||
} catch (error) {
|
||||
console.error('[postService] likePost error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 取消点赞帖子
|
||||
// @deprecated 请使用 processPostUseCase.unlikePost() 代替
|
||||
async unlikePost(postId: string): Promise<Post | null> {
|
||||
try {
|
||||
const post = await processPostUseCase.unlikePost(postId);
|
||||
return post as unknown as Post;
|
||||
} catch (error) {
|
||||
console.error('[postService] unlikePost error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 收藏帖子
|
||||
// @deprecated 请使用 processPostUseCase.favoritePost() 代替
|
||||
async favoritePost(postId: string): Promise<Post | null> {
|
||||
try {
|
||||
const post = await processPostUseCase.favoritePost(postId);
|
||||
return post as unknown as Post;
|
||||
} catch (error) {
|
||||
console.error('[postService] favoritePost error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 取消收藏帖子
|
||||
// @deprecated 请使用 processPostUseCase.unfavoritePost() 代替
|
||||
async unfavoritePost(postId: string): Promise<Post | null> {
|
||||
try {
|
||||
const post = await processPostUseCase.unfavoritePost(postId);
|
||||
return post as unknown as Post;
|
||||
} catch (error) {
|
||||
console.error('[postService] unfavoritePost error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户帖子列表
|
||||
async getUserPosts(
|
||||
userId: string,
|
||||
|
||||
@@ -28,9 +28,6 @@ export {
|
||||
// MessageManager 新架构导出(已替换 conversationStore)
|
||||
export { messageManager, MessageManager, useMessageStore } from './message';
|
||||
export type {
|
||||
MessageEvent,
|
||||
MessageEventType,
|
||||
MessageSubscriber,
|
||||
MessageManagerConversationListDeps,
|
||||
} from './message';
|
||||
export {
|
||||
|
||||
@@ -29,9 +29,6 @@ import { useMessageStore, normalizeConversationId } from './store';
|
||||
|
||||
// 重新导出类型,保持兼容性
|
||||
export type {
|
||||
MessageEventType,
|
||||
MessageEvent,
|
||||
MessageSubscriber,
|
||||
MessageManagerState,
|
||||
ReadStateRecord,
|
||||
MessageManagerConversationListDeps,
|
||||
|
||||
@@ -23,15 +23,10 @@ export type { MessageState, MessageActions, MessageStore } from './store';
|
||||
// ==================== 类型导出 ====================
|
||||
// 从 types.ts 导出所有类型
|
||||
export type {
|
||||
MessageEventType,
|
||||
MessageEvent,
|
||||
MessageSubscriber,
|
||||
MessageManagerState,
|
||||
ReadStateRecord,
|
||||
MessageManagerConversationListDeps,
|
||||
HandleNewMessageOptions,
|
||||
// 服务接口类型(保留向后兼容,但服务层已不再使用)
|
||||
IMessageStateManager,
|
||||
IMessageDeduplication,
|
||||
IUserCacheService,
|
||||
IReadReceiptManager,
|
||||
|
||||
@@ -1,193 +1,48 @@
|
||||
/**
|
||||
* 消息管理类型定义模块
|
||||
* 包含所有与消息管理相关的类型、接口和类型别名
|
||||
*/
|
||||
|
||||
import type { ConversationResponse, MessageResponse, UserDTO } from '../../types/dto';
|
||||
import type { IConversationListPagedSource } from '../conversationListSources';
|
||||
|
||||
// ==================== 事件类型 ====================
|
||||
|
||||
/**
|
||||
* 消息事件类型枚举
|
||||
* 定义了所有可能的消息事件类型
|
||||
*/
|
||||
export type MessageEventType =
|
||||
| 'conversations_updated'
|
||||
| 'conversations_loading'
|
||||
| 'messages_updated'
|
||||
| 'unread_count_updated'
|
||||
| 'connection_changed'
|
||||
| 'message_sent'
|
||||
| 'message_read'
|
||||
| 'message_received'
|
||||
| 'message_recalled'
|
||||
| 'typing_status'
|
||||
| 'group_notice'
|
||||
| 'error';
|
||||
|
||||
/**
|
||||
* 消息事件接口
|
||||
* 所有订阅者接收的事件对象结构
|
||||
*/
|
||||
export interface MessageEvent {
|
||||
type: MessageEventType;
|
||||
payload: any;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息订阅者类型
|
||||
*/
|
||||
export type MessageSubscriber = (event: MessageEvent) => void;
|
||||
|
||||
// ==================== 状态接口 ====================
|
||||
|
||||
/**
|
||||
* 消息管理器内部状态接口
|
||||
* 定义了所有需要管理的状态字段
|
||||
*/
|
||||
export interface MessageManagerState {
|
||||
// 会话相关
|
||||
conversations: Map<string, ConversationResponse>;
|
||||
conversationList: ConversationResponse[];
|
||||
|
||||
// 消息相关 - 按会话ID存储
|
||||
messagesMap: Map<string, MessageResponse[]>;
|
||||
|
||||
// 未读数
|
||||
totalUnreadCount: number;
|
||||
systemUnreadCount: number;
|
||||
|
||||
// 连接状态
|
||||
isWSConnected: boolean;
|
||||
|
||||
// 当前活动会话ID(用户正在查看的会话)
|
||||
currentConversationId: string | null;
|
||||
|
||||
// 加载状态
|
||||
isLoadingConversations: boolean;
|
||||
loadingMessagesSet: Set<string>; // 正在加载消息的会话ID集合
|
||||
|
||||
// 初始化状态
|
||||
loadingMessagesSet: Set<string>;
|
||||
isInitialized: boolean;
|
||||
|
||||
// 订阅者
|
||||
subscribers: Set<MessageSubscriber>;
|
||||
|
||||
// 输入状态 - 按群组ID存储正在输入的用户ID列表
|
||||
typingUsersMap: Map<string, string[]>;
|
||||
|
||||
// 当前用户的禁言状态 - 按群组ID存储
|
||||
mutedStatusMap: Map<string, boolean>;
|
||||
}
|
||||
|
||||
// ==================== 已读状态相关 ====================
|
||||
|
||||
/**
|
||||
* 已读状态记录接口
|
||||
* 用于跟踪已读操作的状态和保护机制
|
||||
*/
|
||||
export interface ReadStateRecord {
|
||||
/** 标记已读的时间戳 */
|
||||
timestamp: number;
|
||||
/** 状态版本号,用于防止旧数据覆盖新数据 */
|
||||
version: number;
|
||||
/** 已上报的已读序号(用于去重) */
|
||||
lastReadSeq: number;
|
||||
/** 清除保护的定时器ID */
|
||||
clearTimer?: ReturnType<typeof setTimeout>;
|
||||
}
|
||||
|
||||
// ==================== 依赖注入接口 ====================
|
||||
|
||||
/**
|
||||
* 可注入会话列表源,便于测试或替换实现
|
||||
*/
|
||||
export interface MessageManagerConversationListDeps {
|
||||
remoteConversationListSource?: IConversationListPagedSource;
|
||||
localConversationListSource?: IConversationListPagedSource;
|
||||
/**
|
||||
* 未传 remoteConversationListSource 时:默认 `cursor`(/conversations/cursor),
|
||||
* `offset` 为常规页码(/conversations?page=&page_size=)。
|
||||
*/
|
||||
remoteListKind?: 'cursor' | 'offset';
|
||||
/** 与 remoteListKind 搭配,默认与 CONVERSATION_LIST_PAGE_SIZE 一致 */
|
||||
remoteListPageSize?: number;
|
||||
}
|
||||
|
||||
// ==================== 消息处理选项 ====================
|
||||
|
||||
/**
|
||||
* 处理新消息的选项
|
||||
*/
|
||||
export interface HandleNewMessageOptions {
|
||||
/** 是否抑制未读数增加 */
|
||||
suppressUnreadIncrement?: boolean;
|
||||
/** 是否抑制震动 */
|
||||
suppressVibration?: boolean;
|
||||
/** 是否抑制会话更新 */
|
||||
suppressConversationUpdates?: boolean;
|
||||
/** 是否抑制自动标记已读 */
|
||||
suppressAutoMarkAsRead?: boolean;
|
||||
}
|
||||
|
||||
// ==================== 服务接口 ====================
|
||||
|
||||
/**
|
||||
* 状态管理器接口
|
||||
* 定义状态管理器需要实现的方法
|
||||
*/
|
||||
export interface IMessageStateManager {
|
||||
// 获取状态
|
||||
getState(): MessageManagerState;
|
||||
getConversations(): ConversationResponse[];
|
||||
getConversation(conversationId: string): ConversationResponse | null;
|
||||
getMessages(conversationId: string): MessageResponse[];
|
||||
getUnreadCount(): { total: number; system: number };
|
||||
isConnected(): boolean;
|
||||
isLoading(): boolean;
|
||||
isLoadingMessages(conversationId: string): boolean;
|
||||
getTypingUsers(groupId: string): string[];
|
||||
isMuted(groupId: string): boolean;
|
||||
getActiveConversation(): string | null;
|
||||
|
||||
// 设置状态
|
||||
setConversations(conversations: Map<string, ConversationResponse>): void;
|
||||
updateConversation(conversation: ConversationResponse): void;
|
||||
removeConversation(conversationId: string): void;
|
||||
setMessages(conversationId: string, messages: MessageResponse[]): void;
|
||||
addMessage(conversationId: string, message: MessageResponse): void;
|
||||
updateMessage(conversationId: string, messageId: string, updates: Partial<MessageResponse>): void;
|
||||
setUnreadCount(total: number, system: number): void;
|
||||
setSSEConnected(connected: boolean): void;
|
||||
setCurrentConversation(conversationId: string | null): void;
|
||||
setLoading(loading: boolean): void;
|
||||
setLoadingMessages(conversationId: string, loading: boolean): void;
|
||||
setTypingUsers(groupId: string, users: string[]): void;
|
||||
setMutedStatus(groupId: string, isMuted: boolean): void;
|
||||
setInitialized(initialized: boolean): void;
|
||||
|
||||
// 订阅机制
|
||||
subscribe(subscriber: MessageSubscriber): () => void;
|
||||
notifySubscribers(event: MessageEvent): void;
|
||||
|
||||
// 重置
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息去重服务接口
|
||||
*/
|
||||
export interface IMessageDeduplication {
|
||||
isMessageProcessed(messageId: string): boolean;
|
||||
markMessageAsProcessed(messageId: string): void;
|
||||
cleanupProcessedMessageIds(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户缓存服务接口
|
||||
*/
|
||||
export interface IUserCacheService {
|
||||
getSenderInfo(userId: string): Promise<UserDTO | null>;
|
||||
enrichMessagesWithSenderInfo(
|
||||
@@ -197,9 +52,6 @@ export interface IUserCacheService {
|
||||
): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 已读回执管理器接口
|
||||
*/
|
||||
export interface IReadReceiptManager {
|
||||
markAsRead(conversationId: string, seq: number): Promise<void>;
|
||||
markAllAsRead(): Promise<void>;
|
||||
@@ -210,9 +62,6 @@ export interface IReadReceiptManager {
|
||||
getPendingReadMap(): Map<string, ReadStateRecord>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话操作接口
|
||||
*/
|
||||
export interface IConversationOperations {
|
||||
createConversation(userId: string): Promise<ConversationResponse | null>;
|
||||
updateConversation(conversationId: string, updates: Partial<ConversationResponse>): void;
|
||||
@@ -225,9 +74,6 @@ export interface IConversationOperations {
|
||||
decrementSystemUnreadCount(count?: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息发送服务接口
|
||||
*/
|
||||
export interface IMessageSendService {
|
||||
sendMessage(
|
||||
conversationId: string,
|
||||
@@ -236,9 +82,6 @@ export interface IMessageSendService {
|
||||
): Promise<MessageResponse | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息同步服务接口
|
||||
*/
|
||||
export interface IMessageSyncService {
|
||||
fetchConversations(forceRefresh?: boolean, source?: string): Promise<void>;
|
||||
loadMoreConversations(): Promise<void>;
|
||||
@@ -249,9 +92,6 @@ export interface IMessageSyncService {
|
||||
canLoadMoreConversations(): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket 消息处理器接口
|
||||
*/
|
||||
export interface IWSMessageHandler {
|
||||
connect(): void;
|
||||
disconnect(): void;
|
||||
|
||||
@@ -50,18 +50,10 @@ export {
|
||||
|
||||
// 重导出类型
|
||||
export type {
|
||||
// 事件类型
|
||||
MessageEventType,
|
||||
MessageEvent,
|
||||
MessageSubscriber,
|
||||
|
||||
// 状态类型
|
||||
MessageManagerState,
|
||||
ReadStateRecord,
|
||||
MessageManagerConversationListDeps,
|
||||
HandleNewMessageOptions,
|
||||
|
||||
// 服务接口类型
|
||||
IMessageDeduplication,
|
||||
IUserCacheService,
|
||||
IReadReceiptManager,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* 帖子模块统一导出
|
||||
*/
|
||||
|
||||
// ==================== Store ====================
|
||||
// ==================== Store (PostManager) ====================
|
||||
export {
|
||||
usePostManagerStore,
|
||||
type PostManagerState,
|
||||
@@ -10,6 +10,14 @@ export {
|
||||
type PostManagerStore,
|
||||
} from './postStore';
|
||||
|
||||
// ==================== Store (ProcessPostUseCase) ====================
|
||||
export {
|
||||
usePostListStore,
|
||||
type PostsState,
|
||||
type PostDetailState,
|
||||
type PostListStore,
|
||||
} from './postListStore';
|
||||
|
||||
// ==================== Manager ====================
|
||||
export { postManager, PostManager } from './PostManager';
|
||||
|
||||
|
||||
185
src/stores/post/postListStore.ts
Normal file
185
src/stores/post/postListStore.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { create } from 'zustand';
|
||||
import type { Post } from '../../core/entities/Post';
|
||||
|
||||
export interface PostsState {
|
||||
posts: Post[];
|
||||
isLoading: boolean;
|
||||
isRefreshing: boolean;
|
||||
hasMore: boolean;
|
||||
error: string | null;
|
||||
cursor: string | null;
|
||||
currentPage: number;
|
||||
lastLoadTime: number | null;
|
||||
lastParams?: any;
|
||||
}
|
||||
|
||||
export interface PostDetailState {
|
||||
post: Post | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface PostListStoreState {
|
||||
postsStateMap: Map<string, PostsState>;
|
||||
postDetailStateMap: Map<string, PostDetailState>;
|
||||
}
|
||||
|
||||
export interface PostListStoreActions {
|
||||
getPostsState: (key: string) => PostsState;
|
||||
getPostDetailState: (postId: string) => PostDetailState;
|
||||
updatePostsState: (key: string, partial: Partial<PostsState>) => void;
|
||||
updatePostDetailState: (postId: string, partial: Partial<PostDetailState>) => void;
|
||||
updatePostInLists: (postId: string, updatedPost: Post) => void;
|
||||
removePostFromLists: (postId: string) => void;
|
||||
applyShareCountUpdate: (postId: string, sharesCount: number) => void;
|
||||
clearPostsState: (key: string) => void;
|
||||
clearPostDetailState: (postId: string) => void;
|
||||
clearAllStates: () => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_POSTS_STATE: PostsState = {
|
||||
posts: [],
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
hasMore: true,
|
||||
error: null,
|
||||
cursor: null,
|
||||
currentPage: 1,
|
||||
lastLoadTime: null,
|
||||
};
|
||||
|
||||
const DEFAULT_POST_DETAIL_STATE: PostDetailState = {
|
||||
post: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
export type PostListStore = PostListStoreState & PostListStoreActions;
|
||||
|
||||
export const usePostListStore = create<PostListStore>((set, get) => ({
|
||||
postsStateMap: new Map(),
|
||||
postDetailStateMap: new Map(),
|
||||
|
||||
getPostsState: (key: string) => {
|
||||
return get().postsStateMap.get(key) ?? { ...DEFAULT_POSTS_STATE };
|
||||
},
|
||||
|
||||
getPostDetailState: (postId: string) => {
|
||||
return get().postDetailStateMap.get(postId) ?? { ...DEFAULT_POST_DETAIL_STATE };
|
||||
},
|
||||
|
||||
updatePostsState: (key: string, partial: Partial<PostsState>) => {
|
||||
set(state => {
|
||||
const current = state.postsStateMap.get(key) ?? { ...DEFAULT_POSTS_STATE };
|
||||
const newMap = new Map(state.postsStateMap);
|
||||
newMap.set(key, { ...current, ...partial });
|
||||
return { postsStateMap: newMap };
|
||||
});
|
||||
},
|
||||
|
||||
updatePostDetailState: (postId: string, partial: Partial<PostDetailState>) => {
|
||||
set(state => {
|
||||
const current = state.postDetailStateMap.get(postId) ?? { ...DEFAULT_POST_DETAIL_STATE };
|
||||
const newMap = new Map(state.postDetailStateMap);
|
||||
newMap.set(postId, { ...current, ...partial });
|
||||
return { postDetailStateMap: newMap };
|
||||
});
|
||||
},
|
||||
|
||||
updatePostInLists: (postId: string, updatedPost: Post) => {
|
||||
set(state => {
|
||||
const newMap = new Map(state.postsStateMap);
|
||||
let changed = false;
|
||||
for (const [key, postsState] of newMap) {
|
||||
const index = postsState.posts.findIndex(p => p.id === postId);
|
||||
if (index !== -1) {
|
||||
const newPosts = [...postsState.posts];
|
||||
newPosts[index] = updatedPost;
|
||||
newMap.set(key, { ...postsState, posts: newPosts });
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed ? { postsStateMap: newMap } : state;
|
||||
});
|
||||
},
|
||||
|
||||
removePostFromLists: (postId: string) => {
|
||||
set(state => {
|
||||
const newMap = new Map(state.postsStateMap);
|
||||
let changed = false;
|
||||
for (const [key, postsState] of newMap) {
|
||||
const filtered = postsState.posts.filter(p => p.id !== postId);
|
||||
if (filtered.length !== postsState.posts.length) {
|
||||
newMap.set(key, { ...postsState, posts: filtered });
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed ? { postsStateMap: newMap } : state;
|
||||
});
|
||||
},
|
||||
|
||||
applyShareCountUpdate: (postId: string, sharesCount: number) => {
|
||||
set(state => {
|
||||
const newPostsMap = new Map(state.postsStateMap);
|
||||
let postsChanged = false;
|
||||
for (const [key, postsState] of newPostsMap) {
|
||||
const index = postsState.posts.findIndex(p => p.id === postId);
|
||||
if (index !== -1) {
|
||||
const newPosts = [...postsState.posts];
|
||||
const cur = newPosts[index];
|
||||
newPosts[index] = { ...cur, sharesCount, shares_count: sharesCount };
|
||||
newPostsMap.set(key, { ...postsState, posts: newPosts });
|
||||
postsChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
const newDetailMap = new Map(state.postDetailStateMap);
|
||||
let detailChanged = false;
|
||||
const detail = newDetailMap.get(postId);
|
||||
if (detail?.post) {
|
||||
newDetailMap.set(postId, {
|
||||
...detail,
|
||||
post: { ...detail.post, sharesCount, shares_count: sharesCount },
|
||||
});
|
||||
detailChanged = true;
|
||||
}
|
||||
|
||||
if (!postsChanged && !detailChanged) return state;
|
||||
return {
|
||||
...(postsChanged ? { postsStateMap: newPostsMap } : {}),
|
||||
...(detailChanged ? { postDetailStateMap: newDetailMap } : {}),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
clearPostsState: (key: string) => {
|
||||
set(state => {
|
||||
const newMap = new Map(state.postsStateMap);
|
||||
newMap.delete(key);
|
||||
return { postsStateMap: newMap };
|
||||
});
|
||||
},
|
||||
|
||||
clearPostDetailState: (postId: string) => {
|
||||
set(state => {
|
||||
const newMap = new Map(state.postDetailStateMap);
|
||||
newMap.delete(postId);
|
||||
return { postDetailStateMap: newMap };
|
||||
});
|
||||
},
|
||||
|
||||
clearAllStates: () => {
|
||||
set({
|
||||
postsStateMap: new Map(),
|
||||
postDetailStateMap: new Map(),
|
||||
});
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
set({
|
||||
postsStateMap: new Map(),
|
||||
postDetailStateMap: new Map(),
|
||||
});
|
||||
},
|
||||
}));
|
||||
@@ -1,66 +1,40 @@
|
||||
/**
|
||||
* 帖子状态 Zustand Store
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import type { Post } from '../../types';
|
||||
import { CacheEntry, createCacheEntry, isCacheExpired, DEFAULT_TTL } from '../utils/cache';
|
||||
|
||||
// ==================== 状态接口 ====================
|
||||
|
||||
export interface PostManagerState {
|
||||
// 帖子列表缓存 (key: list:type:page:pageSize)
|
||||
listsMap: Map<string, CacheEntry<Post[]>>;
|
||||
|
||||
// 帖子详情缓存
|
||||
detailsMap: Map<string, CacheEntry<Post | null>>;
|
||||
|
||||
// 加载状态
|
||||
loadingListKeys: Set<string>;
|
||||
loadingDetailIds: Set<string>;
|
||||
|
||||
// 待处理请求(去重)
|
||||
pendingRequests: Map<string, Promise<any>>;
|
||||
}
|
||||
|
||||
export interface PostManagerActions {
|
||||
// 获取状态
|
||||
getPostsList: (type: string, page: number, pageSize: number) => Post[] | null;
|
||||
getPostDetail: (postId: string) => Post | null;
|
||||
isListExpired: (type: string, page: number, pageSize: number) => boolean;
|
||||
isDetailExpired: (postId: string) => boolean;
|
||||
isLoadingList: (type: string, page: number, pageSize: number) => boolean;
|
||||
isLoadingDetail: (postId: string) => boolean;
|
||||
|
||||
// 设置状态
|
||||
setPostsList: (type: string, page: number, pageSize: number, posts: Post[]) => void;
|
||||
setPostDetail: (postId: string, post: Post | null) => void;
|
||||
setLoadingList: (type: string, page: number, pageSize: number, loading: boolean) => void;
|
||||
setLoadingDetail: (postId: string, loading: boolean) => void;
|
||||
|
||||
// 缓存管理
|
||||
invalidateList: (type?: string, page?: number, pageSize?: number) => void;
|
||||
invalidateDetail: (postId: string) => void;
|
||||
invalidateAll: () => void;
|
||||
|
||||
// 请求去重
|
||||
getPendingRequest: <T>(key: string) => Promise<T> | undefined;
|
||||
setPendingRequest: <T>(key: string, request: Promise<T>) => void;
|
||||
deletePendingRequest: (key: string) => void;
|
||||
clearPendingRequests: () => void;
|
||||
|
||||
// 重置
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
// ==================== 辅助函数 ====================
|
||||
|
||||
function buildListKey(type: string, page: number, pageSize: number): string {
|
||||
return `list:${type}:${page}:${pageSize}`;
|
||||
}
|
||||
|
||||
// ==================== 初始状态 ====================
|
||||
|
||||
const initialState: PostManagerState = {
|
||||
listsMap: new Map(),
|
||||
detailsMap: new Map(),
|
||||
@@ -69,16 +43,11 @@ const initialState: PostManagerState = {
|
||||
pendingRequests: new Map(),
|
||||
};
|
||||
|
||||
// ==================== Store 创建 ====================
|
||||
|
||||
export type PostManagerStore = PostManagerState & PostManagerActions;
|
||||
|
||||
export const usePostManagerStore = create<PostManagerStore>((set, get) => ({
|
||||
// ==================== 初始状态 ====================
|
||||
...initialState,
|
||||
|
||||
// ==================== 获取状态 ====================
|
||||
|
||||
getPostsList: (type: string, page: number, pageSize: number) => {
|
||||
const key = buildListKey(type, page, pageSize);
|
||||
const entry = get().listsMap.get(key);
|
||||
@@ -116,8 +85,6 @@ export const usePostManagerStore = create<PostManagerStore>((set, get) => ({
|
||||
return get().loadingDetailIds.has(postId);
|
||||
},
|
||||
|
||||
// ==================== 设置状态 ====================
|
||||
|
||||
setPostsList: (type: string, page: number, pageSize: number, posts: Post[]) => {
|
||||
const key = buildListKey(type, page, pageSize);
|
||||
set(state => {
|
||||
@@ -160,8 +127,6 @@ export const usePostManagerStore = create<PostManagerStore>((set, get) => ({
|
||||
});
|
||||
},
|
||||
|
||||
// ==================== 缓存管理 ====================
|
||||
|
||||
invalidateList: (type?: string, page?: number, pageSize?: number) => {
|
||||
if (type !== undefined && page !== undefined && pageSize !== undefined) {
|
||||
const key = buildListKey(type, page, pageSize);
|
||||
@@ -191,8 +156,6 @@ export const usePostManagerStore = create<PostManagerStore>((set, get) => ({
|
||||
});
|
||||
},
|
||||
|
||||
// ==================== 请求去重 ====================
|
||||
|
||||
getPendingRequest: <T>(key: string) => {
|
||||
return get().pendingRequests.get(key) as Promise<T> | undefined;
|
||||
},
|
||||
@@ -217,8 +180,6 @@ export const usePostManagerStore = create<PostManagerStore>((set, get) => ({
|
||||
set({ pendingRequests: new Map() });
|
||||
},
|
||||
|
||||
// ==================== 重置 ====================
|
||||
|
||||
reset: () => {
|
||||
set({
|
||||
...initialState,
|
||||
|
||||
Reference in New Issue
Block a user