From 2adc9360a5189b274081d92f556453c76073986e Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Mon, 13 Apr 2026 00:26:05 +0800 Subject: [PATCH] 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. --- ARCHITECTURE_REFACTOR_PLAN.md | 231 ---- plans/architecture-comparison-report.md | 394 ------- src/core/usecases/ProcessMessageUseCase.ts | 689 ----------- src/core/usecases/ProcessPostUseCase.ts | 1036 ----------------- src/hooks/useDifferentialPosts.ts | 283 +---- src/screens/home/HomeScreen.tsx | 16 +- src/screens/home/PostDetailScreen.tsx | 16 +- src/screens/home/SearchScreen.tsx | 9 +- .../components/ChatScreen/SegmentRenderer.tsx | 2 - src/screens/profile/useUserProfile.ts | 14 +- src/services/post/PostSyncService.ts | 346 ++++++ src/services/post/postMerge.ts | 66 ++ src/services/postService.ts | 58 +- src/stores/index.ts | 3 - src/stores/message/MessageManager.ts | 3 - src/stores/message/index.ts | 5 - src/stores/message/types.ts | 162 +-- src/stores/messageManager.ts | 8 - src/stores/post/index.ts | 10 +- src/stores/post/postListStore.ts | 185 +++ src/stores/post/postStore.ts | 39 - 21 files changed, 679 insertions(+), 2896 deletions(-) delete mode 100644 ARCHITECTURE_REFACTOR_PLAN.md delete mode 100644 plans/architecture-comparison-report.md delete mode 100644 src/core/usecases/ProcessMessageUseCase.ts delete mode 100644 src/core/usecases/ProcessPostUseCase.ts create mode 100644 src/services/post/PostSyncService.ts create mode 100644 src/services/post/postMerge.ts create mode 100644 src/stores/post/postListStore.ts diff --git a/ARCHITECTURE_REFACTOR_PLAN.md b/ARCHITECTURE_REFACTOR_PLAN.md deleted file mode 100644 index 1daaf2f..0000000 --- a/ARCHITECTURE_REFACTOR_PLAN.md +++ /dev/null @@ -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 { - store: StoreApi; - getCurrentState: () => T; - optimisticUpdate: (current: T) => T; - apiCall: () => Promise; - onSuccess?: (result: R, current: T) => T; - onError?: (error: any, original: T) => void; -} - -export async function optimisticUpdate( - options: OptimisticUpdateOptions -): Promise -``` - -#### 使用示例 -```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错误 -- [ ] 性能不劣化(启动时间、内存占用) diff --git a/plans/architecture-comparison-report.md b/plans/architecture-comparison-report.md deleted file mode 100644 index 16db566..0000000 --- a/plans/architecture-comparison-report.md +++ /dev/null @@ -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; - savePost(post: Post): Promise; - updatePost(postId: string, updates: Partial): Promise; - // ... -} -``` - -#### 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
Message.ts] - U1[UseCase
ProcessMessageUseCase] - R1[Repository
MessageRepository] - M1[Mapper
MessageMapper] - H1[Hook
useDifferentialMessages] - D1[Diff基础设施
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
直接操作Store] - ST2[useUserStore
Zustand] - H2[Hook
useCursorPagination] - PM2[postManager
内存缓存] - - 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模块对齐。 diff --git a/src/core/usecases/ProcessMessageUseCase.ts b/src/core/usecases/ProcessMessageUseCase.ts deleted file mode 100644 index 2dd7b9c..0000000 --- a/src/core/usecases/ProcessMessageUseCase.ts +++ /dev/null @@ -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; -} - -class ProcessMessageUseCase { - private subscribers: Set = new Set(); - private unsubscribeFns: Array<() => void> = []; - private processedMessageIds: Set = new Set(); - private processedMessageIdsExpiry: Map = new Map(); - private pendingReadMap: Map = new Map(); - private readStateVersion = 0; - private pendingUserRequests: Map> = 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 { - 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 { - 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 { - // 先检查本地缓存 - 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 { - try { - const response = await api.get(`/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 { - 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 { - 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 { - 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 { - 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 { - const cached = await messageRepository.getByConversation(conversationId, limit); - return cached.map(cachedMessageToMessage); - } - - /** - * 获取历史消息 - */ - async getHistoryMessages( - conversationId: string, - beforeSeq: number, - limit: number = 20 - ): Promise { - // 先从本地获取 - 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 { - 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; diff --git a/src/core/usecases/ProcessPostUseCase.ts b/src/core/usecases/ProcessPostUseCase.ts deleted file mode 100644 index bb42fa4..0000000 --- a/src/core/usecases/ProcessPostUseCase.ts +++ /dev/null @@ -1,1036 +0,0 @@ -/** - * ProcessPostUseCase - 处理帖子用例 - * 处理帖子的业务逻辑,协调 Repository 和状态管理 - * 纯业务逻辑,无UI依赖 - */ - -import { postRepository } from '../../data/repositories/PostRepository'; -import type { Post } from '../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'; - -// ==================== 事件类型定义 ==================== - -/** - * 帖子用例事件类型 - */ -export type PostUseCaseEventType = - | 'posts_loaded' - | 'post_created' - | 'post_updated' - | 'post_deleted' - | 'post_liked' - | 'post_unliked' - | 'post_favorited' - | 'post_unfavorited' - | 'loading_changed' - | 'error_occurred' - | 'state_changed'; - -/** - * 帖子用例事件 - */ -export interface PostUseCaseEvent { - type: PostUseCaseEventType; - payload: any; - timestamp: number; -} - -/** - * 事件订阅者类型 - */ -export type PostUseCaseSubscriber = (event: PostUseCaseEvent) => void; - -// ==================== 状态类型定义 ==================== - -/** - * 帖子列表状态 - */ -export interface PostsState { - /** 帖子列表 */ - posts: Post[]; - /** 是否正在加载 */ - isLoading: boolean; - /** 是否正在刷新 */ - isRefreshing: boolean; - /** 是否有更多数据 */ - hasMore: boolean; - /** 错误信息 */ - error: string | null; - /** 当前游标(cursor 模式) */ - cursor: string | null; - /** 当前页码(page 模式) */ - currentPage: number; - /** 最后加载时间 */ - lastLoadTime: number | null; - /** 上次请求的参数(用于加载更多和刷新) */ - lastParams?: GetPostsParams; -} - -/** - * 帖子详情状态 - */ -export interface PostDetailState { - /** 帖子详情 */ - post: Post | null; - /** 是否正在加载 */ - isLoading: boolean; - /** 错误信息 */ - error: string | null; -} - -// ==================== 默认配置 ==================== - -const DEFAULT_PAGE_SIZE = 20; - -// ==================== ProcessPostUseCase 类 ==================== - -class ProcessPostUseCase { - // 单例实例 - private static instance: ProcessPostUseCase; - - // 订阅者管理 - private subscribers: Set = new Set(); - - // 状态管理 - private postsState: Map = new Map(); - private postDetailState: Map = new Map(); - - // 分页状态管理器 - private paginationManager: PaginationStateManager; - - // 当前用户ID - private currentUserId: string | null = null; - - private readonly mergePerfWarnThresholdMs = 12; - - // 私有构造函数 - private constructor() { - this.paginationManager = new PaginationStateManager(); - } - - /** - * 获取单例实例 - */ - static getInstance(): ProcessPostUseCase { - if (!ProcessPostUseCase.instance) { - ProcessPostUseCase.instance = new ProcessPostUseCase(); - } - return ProcessPostUseCase.instance; - } - - // ==================== 初始化和销毁 ==================== - - /** - * 初始化用例 - * @param currentUserId 当前用户ID - */ - initialize(currentUserId: string | null = null): void { - this.currentUserId = currentUserId; - } - - /** - * 销毁用例 - */ - destroy(): void { - this.subscribers.clear(); - this.postsState.clear(); - this.postDetailState.clear(); - this.paginationManager.clear(); - this.currentUserId = null; - } - - // ==================== 订阅机制 ==================== - - /** - * 订阅事件 - * @param subscriber 订阅者函数 - * @returns 取消订阅函数 - */ - subscribe(subscriber: PostUseCaseSubscriber): () => void { - this.subscribers.add(subscriber); - return () => { - this.subscribers.delete(subscriber); - }; - } - - /** - * 通知所有订阅者 - * @param event 事件对象 - */ - private notifySubscribers(event: PostUseCaseEvent): void { - this.subscribers.forEach((subscriber) => { - try { - subscriber(event); - } catch (error) { - console.error('[ProcessPostUseCase] 订阅者执行失败:', error); - } - }); - } - - /** - * 通知状态变化 - * @param key 列表键 - */ - private notifyStateChanged(key: string): void { - const state = this.getPostsState(key); - this.notifySubscribers({ - type: 'state_changed', - payload: { key, state }, - timestamp: Date.now(), - }); - } - - // ==================== 状态管理 ==================== - - /** - * 获取帖子列表状态 - * @param key 列表键(默认为 'default') - */ - getPostsState(key: string = 'default'): PostsState { - let state = this.postsState.get(key); - if (!state) { - state = { - posts: [], - isLoading: false, - isRefreshing: false, - hasMore: true, - error: null, - cursor: null, - currentPage: 1, - lastLoadTime: null, - }; - this.postsState.set(key, state); - } - return state; - } - - /** - * 获取帖子详情状态 - * @param postId 帖子ID - */ - getPostDetailState(postId: string): PostDetailState { - let state = this.postDetailState.get(postId); - if (!state) { - state = { - post: null, - isLoading: false, - error: null, - }; - this.postDetailState.set(postId, state); - } - return state; - } - - /** - * 更新帖子列表状态 - * @param key 列表键 - * @param partialState 部分状态 - */ - private updatePostsState(key: string, partialState: Partial): void { - const currentState = this.getPostsState(key); - this.postsState.set(key, { - ...currentState, - ...partialState, - }); - this.notifyStateChanged(key); - } - - private 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; - } - - private 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 = this.getEntityId(previous[previous.length - 1]); - const firstIncomingId = this.getEntityId(incoming[0]); - if (lastPrevId && firstIncomingId && lastPrevId !== firstIncomingId) { - const prevIdSet = new Set(previous.map((item) => this.getEntityId(item)).filter(Boolean) as string[]); - if (!prevIdSet.has(firstIncomingId)) { - return [...previous, ...incoming]; - } - } - - const merged = [...previous]; - const indexById = new Map(); - for (let i = 0; i < merged.length; i += 1) { - const id = this.getEntityId(merged[i]); - if (id) { - indexById.set(id, i); - } - } - - for (const post of incoming) { - const id = this.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; - } - - private 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) => this.getEntityId(item)).filter(Boolean) as string[] - ); - const remaining = previous.filter((item) => { - const id = this.getEntityId(item); - return !id || !latestIdSet.has(id); - }); - return [...latest, ...remaining]; - } - - /** - * 更新帖子详情状态 - * @param postId 帖子ID - * @param partialState 部分状态 - */ - private updatePostDetailState(postId: string, partialState: Partial): void { - const currentState = this.getPostDetailState(postId); - this.postDetailState.set(postId, { - ...currentState, - ...partialState, - }); - } - - /** - * 分享计数上报成功后,同步各列表与详情中的分享数 - */ - applyShareCountUpdate(postId: string, sharesCount: number): void { - this.postsState.forEach((state, key) => { - const index = state.posts.findIndex((p) => p.id === postId); - if (index !== -1) { - const newPosts = [...state.posts]; - const cur = newPosts[index]; - newPosts[index] = { - ...cur, - sharesCount, - shares_count: sharesCount, - }; - this.updatePostsState(key, { posts: newPosts }); - } - }); - const detail = this.getPostDetailState(postId); - if (detail.post) { - this.updatePostDetailState(postId, { - post: { - ...detail.post, - sharesCount, - shares_count: sharesCount, - }, - }); - } - } - - // ==================== 帖子列表操作 ==================== - - /** - * 获取帖子列表 - * @param params 查询参数 - * @param key 列表键(用于区分不同的列表) - */ - async fetchPosts(params?: GetPostsParams, key: string = 'default'): Promise { - // 更新加载状态 - this.updatePostsState(key, { isLoading: true, error: null }); - - try { - // 默认传递空字符串 cursor 来启动 cursor 模式 - // 注意:cursor 要放在 ...params 后面,这样只有在 params 没有传 cursor 时才使用默认值 - const queryParams: GetPostsParams = { - pageSize: params?.pageSize || DEFAULT_PAGE_SIZE, - ...params, - cursor: params?.cursor !== undefined ? params.cursor : '', // 默认使用空字符串启动 cursor 模式 - }; - - const result = await postRepository.getPosts(queryParams); - - // 判断是否为 cursor 模式(有 next_cursor 返回) - const isCursorMode = result.nextCursor !== undefined && result.nextCursor !== null; - - // 更新状态(保存请求参数以便加载更多时使用) - const mergeStart = Date.now(); - const currentState = this.getPostsState(key); - const incomingPosts = Array.isArray(result.posts) ? result.posts : []; - const mergedPosts = this.mergeRefreshWindow(currentState.posts, incomingPosts); - const mergeCost = Date.now() - mergeStart; - if (mergeCost >= this.mergePerfWarnThresholdMs) { - console.log('[PostPerf] fetchPosts merge cost', { key, costMs: mergeCost, prev: currentState.posts.length, next: incomingPosts.length }); - } - - this.updatePostsState(key, { - posts: mergedPosts, - hasMore: result.hasMore, - cursor: isCursorMode ? result.nextCursor : null, // cursor 模式保存 cursor,否则设为 null 表示 page 模式 - currentPage: isCursorMode ? undefined : 1, // page 模式从第 1 页开始 - isLoading: false, - lastLoadTime: Date.now(), - lastParams: params, // 保存请求参数 - }); - - // 通知订阅者 - this.notifySubscribers({ - type: 'posts_loaded', - payload: { key, posts: incomingPosts, hasMore: result.hasMore }, - timestamp: Date.now(), - }); - - return result; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : '获取帖子列表失败'; - this.updatePostsState(key, { - isLoading: false, - error: errorMessage, - }); - - this.notifySubscribers({ - type: 'error_occurred', - payload: { key, error: errorMessage }, - timestamp: Date.now(), - }); - - throw error; - } - } - - /** - * 刷新帖子列表 - * @param key 列表键 - */ - async refreshPosts(key: string = 'default'): Promise { - const state = this.getPostsState(key); - - // 更新刷新状态 - this.updatePostsState(key, { isRefreshing: true, error: null }); - - try { - // 使用保存的请求参数(如 post_type)进行刷新 - // 默认传递空字符串 cursor 来启动 cursor 模式 - const result = await postRepository.getPosts({ - pageSize: DEFAULT_PAGE_SIZE, - ...state.lastParams, - cursor: state.lastParams?.cursor !== undefined ? state.lastParams.cursor : '', // 默认使用空字符串启动 cursor 模式 - }); - - // 判断是否为 cursor 模式(有 next_cursor 返回) - const isCursorMode = result.nextCursor !== undefined && result.nextCursor !== null; - - // 更新状态 - const mergeStart = Date.now(); - const incomingPosts = Array.isArray(result.posts) ? result.posts : []; - const mergedPosts = this.mergeRefreshWindow(state.posts, incomingPosts); - const mergeCost = Date.now() - mergeStart; - if (mergeCost >= this.mergePerfWarnThresholdMs) { - console.log('[PostPerf] refreshPosts merge cost', { key, costMs: mergeCost, prev: state.posts.length, next: incomingPosts.length }); - } - - this.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 : '刷新帖子列表失败'; - this.updatePostsState(key, { - isRefreshing: false, - error: errorMessage, - }); - - throw error; - } - } - - /** - * 加载更多帖子 - * @param key 列表键 - */ - async loadMorePosts(key: string = 'default'): Promise { - const state = this.getPostsState(key); - - // 检查是否还有更多数据 - if (!state.hasMore) { - return { - posts: [], - hasMore: false, - }; - } - - // 检查是否正在加载 - if (state.isLoading) { - return { - posts: [], - hasMore: state.hasMore, - }; - } - - // 更新加载状态 - this.updatePostsState(key, { isLoading: true, error: null }); - - try { - // 判断使用哪种分页模式:cursor 不为 null 表示使用 cursor 模式 - const useCursorMode = state.cursor !== null && state.cursor !== undefined; - - // 必须先展开 lastParams,再由本次分页字段覆盖。 - // lastParams 来自首次 fetch(通常含 page:1),若写在展开之前会把 nextPage/cursor 覆盖掉,导致永远请求第 1 页、无法加载第 3 页及以后。 - 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 mergeStart = Date.now(); - const incomingPosts = Array.isArray(result.posts) ? result.posts : []; - const newPosts = this.mergePostListWithFastPath(state.posts, incomingPosts); - const mergeCost = Date.now() - mergeStart; - if (mergeCost >= this.mergePerfWarnThresholdMs) { - console.log('[PostPerf] loadMore merge cost', { key, costMs: mergeCost, prev: state.posts.length, incoming: incomingPosts.length, merged: newPosts.length }); - } - - // 判断响应是否为 cursor 模式(有 next_cursor 返回) - const responseIsCursorMode = result.nextCursor !== undefined && result.nextCursor !== null; - - // 更新状态 - this.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('[ProcessPostUseCase] Load more error:', errorMessage); - this.updatePostsState(key, { - isLoading: false, - error: errorMessage, - }); - - throw error; - } - } - - // ==================== 帖子详情操作 ==================== - - /** - * 获取单个帖子详情 - * @param id 帖子ID - */ - async fetchPostById(id: string): Promise { - // 更新加载状态 - this.updatePostDetailState(id, { isLoading: true, error: null }); - - try { - const post = await postRepository.getPostById(id); - - // 更新状态 - this.updatePostDetailState(id, { - post, - isLoading: false, - }); - - return post; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : '获取帖子详情失败'; - this.updatePostDetailState(id, { - isLoading: false, - error: errorMessage, - }); - - throw error; - } - } - - // ==================== 帖子CRUD操作 ==================== - - /** - * 创建帖子 - * @param data 创建数据 - */ - async createPost(data: CreatePostData): Promise { - try { - const post = await postRepository.createPost(data); - - // 通知订阅者 - this.notifySubscribers({ - type: 'post_created', - payload: { post }, - timestamp: Date.now(), - }); - - // 更新所有列表(新帖子应该出现在列表顶部) - this.postsState.forEach((state, key) => { - this.updatePostsState(key, { - posts: [post, ...state.posts], - }); - }); - - return post; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : '创建帖子失败'; - this.notifySubscribers({ - type: 'error_occurred', - payload: { operation: 'create', error: errorMessage }, - timestamp: Date.now(), - }); - throw error; - } - } - - /** - * 更新帖子 - * @param id 帖子ID - * @param data 更新数据 - */ - async updatePost(id: string, data: UpdatePostData): Promise { - try { - const post = await postRepository.updatePost(id, data); - - // 更新详情状态 - this.updatePostDetailState(id, { post }); - - // 更新列表中的帖子 - this.updatePostInLists(id, post); - - // 通知订阅者 - this.notifySubscribers({ - type: 'post_updated', - payload: { postId: id, post }, - timestamp: Date.now(), - }); - - return post; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : '更新帖子失败'; - this.notifySubscribers({ - type: 'error_occurred', - payload: { operation: 'update', postId: id, error: errorMessage }, - timestamp: Date.now(), - }); - throw error; - } - } - - /** - * 删除帖子 - * @param id 帖子ID - */ - async deletePost(id: string): Promise { - try { - await postRepository.deletePost(id); - - // 从详情状态中移除 - this.postDetailState.delete(id); - - // 从所有列表中移除 - this.postsState.forEach((state, key) => { - const filteredPosts = state.posts.filter((post) => post.id !== id); - this.updatePostsState(key, { - posts: filteredPosts, - }); - }); - - // 通知订阅者 - this.notifySubscribers({ - type: 'post_deleted', - payload: { postId: id }, - timestamp: Date.now(), - }); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : '删除帖子失败'; - this.notifySubscribers({ - type: 'error_occurred', - payload: { operation: 'delete', postId: id, error: errorMessage }, - timestamp: Date.now(), - }); - throw error; - } - } - - // ==================== 互动操作 ==================== - - /** - * 点赞帖子 - * @param id 帖子ID - */ - async likePost(id: string): Promise { - try { - const post = await postRepository.likePost(id); - - // 更新详情状态 - this.updatePostDetailState(id, { post }); - - // 更新列表中的帖子 - this.updatePostInLists(id, post); - - // 通知订阅者 - this.notifySubscribers({ - type: 'post_liked', - payload: { postId: id, post }, - timestamp: Date.now(), - }); - - return post; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : '点赞失败'; - this.notifySubscribers({ - type: 'error_occurred', - payload: { operation: 'like', postId: id, error: errorMessage }, - timestamp: Date.now(), - }); - throw error; - } - } - - /** - * 取消点赞 - * @param id 帖子ID - */ - async unlikePost(id: string): Promise { - try { - const post = await postRepository.unlikePost(id); - - // 更新详情状态 - this.updatePostDetailState(id, { post }); - - // 更新列表中的帖子 - this.updatePostInLists(id, post); - - // 通知订阅者 - this.notifySubscribers({ - type: 'post_unliked', - payload: { postId: id, post }, - timestamp: Date.now(), - }); - - return post; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : '取消点赞失败'; - this.notifySubscribers({ - type: 'error_occurred', - payload: { operation: 'unlike', postId: id, error: errorMessage }, - timestamp: Date.now(), - }); - throw error; - } - } - - /** - * 收藏帖子 - * @param id 帖子ID - */ - async favoritePost(id: string): Promise { - try { - const post = await postRepository.favoritePost(id); - - // 更新详情状态 - this.updatePostDetailState(id, { post }); - - // 更新列表中的帖子 - this.updatePostInLists(id, post); - - // 通知订阅者 - this.notifySubscribers({ - type: 'post_favorited', - payload: { postId: id, post }, - timestamp: Date.now(), - }); - - return post; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : '收藏失败'; - this.notifySubscribers({ - type: 'error_occurred', - payload: { operation: 'favorite', postId: id, error: errorMessage }, - timestamp: Date.now(), - }); - throw error; - } - } - - /** - * 取消收藏 - * @param id 帖子ID - */ - async unfavoritePost(id: string): Promise { - try { - const post = await postRepository.unfavoritePost(id); - - // 更新详情状态 - this.updatePostDetailState(id, { post }); - - // 更新列表中的帖子 - this.updatePostInLists(id, post); - - // 通知订阅者 - this.notifySubscribers({ - type: 'post_unfavorited', - payload: { postId: id, post }, - timestamp: Date.now(), - }); - - return post; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : '取消收藏失败'; - this.notifySubscribers({ - type: 'error_occurred', - payload: { operation: 'unfavorite', postId: id, error: errorMessage }, - timestamp: Date.now(), - }); - throw error; - } - } - - // ==================== 搜索操作 ==================== - - /** - * 搜索帖子 - * @param params 搜索参数 - * @param key 列表键 - */ - async searchPosts(params: SearchPostsParams, key: string = 'search'): Promise { - // 更新加载状态 - this.updatePostsState(key, { isLoading: true, error: null }); - - try { - const result = await postRepository.searchPosts(params); - - // 更新状态 - this.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 : '搜索帖子失败'; - this.updatePostsState(key, { - isLoading: false, - error: errorMessage, - }); - - throw error; - } - } - - // ==================== 用户帖子操作 ==================== - - /** - * 获取用户发布的帖子 - * @param userId 用户ID - * @param params 查询参数 - * @param key 列表键 - */ - async fetchPostsByUser( - userId: string, - params?: GetPostsParams, - key?: string - ): Promise { - const listKey = key || `user_${userId}`; - - // 更新加载状态 - this.updatePostsState(listKey, { isLoading: true, error: null }); - - try { - const result = await postRepository.getPostsByUser(userId, params); - - // 更新状态 - this.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 : '获取用户帖子失败'; - this.updatePostsState(listKey, { - isLoading: false, - error: errorMessage, - }); - - throw error; - } - } - - // ==================== 辅助方法 ==================== - - /** - * 更新所有列表中的帖子 - * @param postId 帖子ID - * @param updatedPost 更新后的帖子 - */ - private updatePostInLists(postId: string, updatedPost: Post): void { - this.postsState.forEach((state, key) => { - const index = state.posts.findIndex((post) => post.id === postId); - if (index !== -1) { - const newPosts = [...state.posts]; - newPosts[index] = updatedPost; - this.updatePostsState(key, { posts: newPosts }); - } - }); - } - - /** - * 清除指定列表的状态 - * @param key 列表键 - */ - clearPostsState(key: string = 'default'): void { - this.postsState.delete(key); - this.paginationManager.reset(key); - } - - /** - * 清除帖子详情状态 - * @param postId 帖子ID - */ - clearPostDetailState(postId: string): void { - this.postDetailState.delete(postId); - } - - /** - * 清除所有状态 - */ - clearAllStates(): void { - this.postsState.clear(); - this.postDetailState.clear(); - this.paginationManager.clear(); - } - - /** - * 获取当前用户ID - */ - getCurrentUserId(): string | null { - return this.currentUserId; - } - - /** - * 设置当前用户ID - */ - setCurrentUserId(userId: string | null): void { - this.currentUserId = userId; - } - - // ==================== 分页状态管理集成 ==================== - - /** - * 获取分页状态 - * @param key 列表键 - */ - getPaginationState(key: string): PaginationState { - return this.paginationManager.getState(key); - } - - /** - * 重置分页状态 - * @param key 列表键 - */ - resetPagination(key: string): void { - this.paginationManager.reset(key); - } - - /** - * 检查是否正在加载 - * @param key 列表键 - */ - isLoading(key: string = 'default'): boolean { - return this.getPostsState(key).isLoading; - } - - /** - * 检查是否有错误 - * @param key 列表键 - */ - hasError(key: string = 'default'): boolean { - return this.getPostsState(key).error !== null; - } - - /** - * 获取错误信息 - * @param key 列表键 - */ - getError(key: string = 'default'): string | null { - return this.getPostsState(key).error; - } -} - -// ==================== 导出 ==================== - -// 导出单例实例 -export const processPostUseCase = ProcessPostUseCase.getInstance(); -export default processPostUseCase; \ No newline at end of file diff --git a/src/hooks/useDifferentialPosts.ts b/src/hooks/useDifferentialPosts.ts index 9a37618..6185406 100644 --- a/src/hooks/useDifferentialPosts.ts +++ b/src/hooks/useDifferentialPosts.ts @@ -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 { - /** 差异计算配置 */ 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 { - /** 优化后的帖子列表 */ posts: T[]; - /** 是否正在加载 */ loading: boolean; - /** 是否首屏加载 */ isInitialLoading: boolean; - /** 是否正在加载更多 */ isLoadingMore: boolean; - /** 是否正在刷新 */ refreshing: boolean; - /** 错误信息 */ error: string | null; - /** 是否有更多数据 */ hasMore: boolean; - /** 待处理更新数量 */ pendingUpdateCount: number; - /** 是否正在处理更新 */ isProcessing: boolean; - /** 刷新方法 */ refresh: () => Promise; - /** 加载更多方法 */ loadMore: () => Promise; - /** 手动刷新批量处理队列 */ 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( initialPosts: T[] = [], options: UseDifferentialPostsOptions = {} @@ -126,8 +69,6 @@ export function useDifferentialPosts( autoSubscribe = true, } = options; - // ==================== 状态 ==================== - const [posts, setPosts] = useState(initialPosts); const [loading, setLoading] = useState(false); const [refreshing, setRefreshing] = useState(false); @@ -142,23 +83,15 @@ export function useDifferentialPosts( lastUpdateTime: 0, }); - // ==================== Refs ==================== - const calculatorRef = useRef | null>(null); const batcherRef = useRef(null); const previousPostsRef = useRef(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( 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( } } - // 更新差异统计 if (addedCount > 0 || updatedCount > 0 || deletedCount > 0) { setDiffUpdates(prev => ({ addedCount: prev.addedCount + addedCount, @@ -261,28 +191,17 @@ export function useDifferentialPosts( } }, []); - // ==================== 初始化 ==================== - - // 初始化差异计算器 useEffect(() => { if (enableDiff) { calculatorRef.current = createPostDiffCalculator(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( }; }, [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( } }, [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( } }, [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( 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( }; }, []); - // ==================== 定期更新待处理数量 ==================== - useEffect(() => { if (!enableBatching || !batcherRef.current) return; - const interval = setInterval(() => { setPendingUpdateCount(batcherRef.current?.getPendingCount() ?? 0); }, 50); - return () => clearInterval(interval); }, [enableBatching]); - // ==================== 返回值 ==================== - return { posts, loading, diff --git a/src/screens/home/HomeScreen.tsx b/src/screens/home/HomeScreen.tsx index 24f0160..25e19aa 100644 --- a/src/screens/home/HomeScreen.tsx +++ b/src/screens/home/HomeScreen.tsx @@ -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); diff --git a/src/screens/home/PostDetailScreen.tsx b/src/screens/home/PostDetailScreen.tsx index c124336..ba78614 100644 --- a/src/screens/home/PostDetailScreen.tsx +++ b/src/screens/home/PostDetailScreen.tsx @@ -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: '确定', diff --git a/src/screens/home/SearchScreen.tsx b/src/screens/home/SearchScreen.tsx index 1ad58d0..9d0e968 100644 --- a/src/screens/home/SearchScreen.tsx +++ b/src/screens/home/SearchScreen.tsx @@ -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 = ({ 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': // 搜索页暂不处理分享 diff --git a/src/screens/message/components/ChatScreen/SegmentRenderer.tsx b/src/screens/message/components/ChatScreen/SegmentRenderer.tsx index 2ca2698..4075500 100644 --- a/src/screens/message/components/ChatScreen/SegmentRenderer.tsx +++ b/src/screens/message/components/ChatScreen/SegmentRenderer.tsx @@ -862,11 +862,9 @@ function createSegmentStyles(colors: AppColors, fontSize: number = 16) { // 容器 segmentsContainer: { flexDirection: 'column', - width: '100%', }, segmentsColumn: { flexDirection: 'column', - width: '100%', }, inlineChunkBase: { fontSize, diff --git a/src/screens/profile/useUserProfile.ts b/src/screens/profile/useUserProfile.ts index b078316..c2444da 100644 --- a/src/screens/profile/useUserProfile.ts +++ b/src/screens/profile/useUserProfile.ts @@ -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; diff --git a/src/services/post/PostSyncService.ts b/src/services/post/PostSyncService.ts new file mode 100644 index 0000000..3d6dd1c --- /dev/null +++ b/src/services/post/PostSyncService.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + const post = await postRepository.updatePost(id, data); + usePostListStore.getState().updatePostDetailState(id, { post }); + usePostListStore.getState().updatePostInLists(id, post); + return post; + } + + async deletePost(id: string): Promise { + await postRepository.deletePost(id); + usePostListStore.getState().clearPostDetailState(id); + usePostListStore.getState().removePostFromLists(id); + } + + async likePost(id: string): Promise { + const post = await postRepository.likePost(id); + usePostListStore.getState().updatePostDetailState(id, { post }); + usePostListStore.getState().updatePostInLists(id, post); + return post; + } + + async unlikePost(id: string): Promise { + const post = await postRepository.unlikePost(id); + usePostListStore.getState().updatePostDetailState(id, { post }); + usePostListStore.getState().updatePostInLists(id, post); + return post; + } + + async favoritePost(id: string): Promise { + const post = await postRepository.favoritePost(id); + usePostListStore.getState().updatePostDetailState(id, { post }); + usePostListStore.getState().updatePostInLists(id, post); + return post; + } + + async unfavoritePost(id: string): Promise { + 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 { + 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 { + 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; diff --git a/src/services/post/postMerge.ts b/src/services/post/postMerge.ts new file mode 100644 index 0000000..98b24a6 --- /dev/null +++ b/src/services/post/postMerge.ts @@ -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(); + 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]; +} diff --git a/src/services/postService.ts b/src/services/postService.ts index e50f58f..922956f 100644 --- a/src/services/postService.ts +++ b/src/services/postService.ts @@ -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 { - 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 { - 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 { - 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 { - 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, diff --git a/src/stores/index.ts b/src/stores/index.ts index c71cd69..ffa0652 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -28,9 +28,6 @@ export { // MessageManager 新架构导出(已替换 conversationStore) export { messageManager, MessageManager, useMessageStore } from './message'; export type { - MessageEvent, - MessageEventType, - MessageSubscriber, MessageManagerConversationListDeps, } from './message'; export { diff --git a/src/stores/message/MessageManager.ts b/src/stores/message/MessageManager.ts index a9753f6..5b35a0e 100644 --- a/src/stores/message/MessageManager.ts +++ b/src/stores/message/MessageManager.ts @@ -29,9 +29,6 @@ import { useMessageStore, normalizeConversationId } from './store'; // 重新导出类型,保持兼容性 export type { - MessageEventType, - MessageEvent, - MessageSubscriber, MessageManagerState, ReadStateRecord, MessageManagerConversationListDeps, diff --git a/src/stores/message/index.ts b/src/stores/message/index.ts index cb064a5..ba76428 100644 --- a/src/stores/message/index.ts +++ b/src/stores/message/index.ts @@ -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, diff --git a/src/stores/message/types.ts b/src/stores/message/types.ts index 39d5c39..2744f26 100644 --- a/src/stores/message/types.ts +++ b/src/stores/message/types.ts @@ -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; conversationList: ConversationResponse[]; - - // 消息相关 - 按会话ID存储 messagesMap: Map; - - // 未读数 totalUnreadCount: number; systemUnreadCount: number; - - // 连接状态 isWSConnected: boolean; - - // 当前活动会话ID(用户正在查看的会话) currentConversationId: string | null; - - // 加载状态 isLoadingConversations: boolean; - loadingMessagesSet: Set; // 正在加载消息的会话ID集合 - - // 初始化状态 + loadingMessagesSet: Set; isInitialized: boolean; - - // 订阅者 - subscribers: Set; - - // 输入状态 - 按群组ID存储正在输入的用户ID列表 typingUsersMap: Map; - - // 当前用户的禁言状态 - 按群组ID存储 mutedStatusMap: Map; } -// ==================== 已读状态相关 ==================== - -/** - * 已读状态记录接口 - * 用于跟踪已读操作的状态和保护机制 - */ export interface ReadStateRecord { - /** 标记已读的时间戳 */ timestamp: number; - /** 状态版本号,用于防止旧数据覆盖新数据 */ version: number; - /** 已上报的已读序号(用于去重) */ lastReadSeq: number; - /** 清除保护的定时器ID */ clearTimer?: ReturnType; } -// ==================== 依赖注入接口 ==================== - -/** - * 可注入会话列表源,便于测试或替换实现 - */ 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): 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): 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; enrichMessagesWithSenderInfo( @@ -197,9 +52,6 @@ export interface IUserCacheService { ): void; } -/** - * 已读回执管理器接口 - */ export interface IReadReceiptManager { markAsRead(conversationId: string, seq: number): Promise; markAllAsRead(): Promise; @@ -210,9 +62,6 @@ export interface IReadReceiptManager { getPendingReadMap(): Map; } -/** - * 会话操作接口 - */ export interface IConversationOperations { createConversation(userId: string): Promise; updateConversation(conversationId: string, updates: Partial): 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; } -/** - * 消息同步服务接口 - */ export interface IMessageSyncService { fetchConversations(forceRefresh?: boolean, source?: string): Promise; loadMoreConversations(): Promise; @@ -249,9 +92,6 @@ export interface IMessageSyncService { canLoadMoreConversations(): boolean; } -/** - * WebSocket 消息处理器接口 - */ export interface IWSMessageHandler { connect(): void; disconnect(): void; diff --git a/src/stores/messageManager.ts b/src/stores/messageManager.ts index e5771c3..6a963c0 100644 --- a/src/stores/messageManager.ts +++ b/src/stores/messageManager.ts @@ -50,18 +50,10 @@ export { // 重导出类型 export type { - // 事件类型 - MessageEventType, - MessageEvent, - MessageSubscriber, - - // 状态类型 MessageManagerState, ReadStateRecord, MessageManagerConversationListDeps, HandleNewMessageOptions, - - // 服务接口类型 IMessageDeduplication, IUserCacheService, IReadReceiptManager, diff --git a/src/stores/post/index.ts b/src/stores/post/index.ts index 3bc2e36..c54f703 100644 --- a/src/stores/post/index.ts +++ b/src/stores/post/index.ts @@ -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'; diff --git a/src/stores/post/postListStore.ts b/src/stores/post/postListStore.ts new file mode 100644 index 0000000..43a08b5 --- /dev/null +++ b/src/stores/post/postListStore.ts @@ -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; + postDetailStateMap: Map; +} + +export interface PostListStoreActions { + getPostsState: (key: string) => PostsState; + getPostDetailState: (postId: string) => PostDetailState; + updatePostsState: (key: string, partial: Partial) => void; + updatePostDetailState: (postId: string, partial: Partial) => 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((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) => { + 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) => { + 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(), + }); + }, +})); diff --git a/src/stores/post/postStore.ts b/src/stores/post/postStore.ts index 21eea0b..b23c871 100644 --- a/src/stores/post/postStore.ts +++ b/src/stores/post/postStore.ts @@ -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>; - - // 帖子详情缓存 detailsMap: Map>; - - // 加载状态 loadingListKeys: Set; loadingDetailIds: Set; - - // 待处理请求(去重) pendingRequests: Map>; } 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: (key: string) => Promise | undefined; setPendingRequest: (key: string, request: Promise) => 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((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((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((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((set, get) => ({ }); }, - // ==================== 请求去重 ==================== - getPendingRequest: (key: string) => { return get().pendingRequests.get(key) as Promise | undefined; }, @@ -217,8 +180,6 @@ export const usePostManagerStore = create((set, get) => ({ set({ pendingRequests: new Map() }); }, - // ==================== 重置 ==================== - reset: () => { set({ ...initialState,