refactor(post): consolidate post sync to PostSyncService and remove ProcessPostUseCase
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 3m28s
Frontend CI / ota-android (push) Successful in 10m25s
Frontend CI / build-android-apk (push) Successful in 55m22s

- Replace ProcessPostUseCase with new PostSyncService in src/services/post/
- Add postListStore for state management in src/stores/post/
- Remove deprecated ProcessPostUseCase and ProcessMessageUseCase files
- Delete architecture documentation files (ARCHITECTURE_REFACTOR_PLAN.md, architecture-comparison-report.md)
- Update all consumers (HomeScreen, PostDetailScreen, SearchScreen, useUserProfile, useDifferentialPosts)
- Simplify postService to only contain API layer methods
- Remove unused type exports from message stores

BREAKING CHANGE: ProcessPostUseCase and ProcessMessageUseCase have been removed.
Use postSyncService for post operations instead.
This commit is contained in:
lafay
2026-04-13 00:26:05 +08:00
parent 6610d2f173
commit 2adc9360a5
21 changed files with 679 additions and 2896 deletions

View File

@@ -1,231 +0,0 @@
# 架构修复方案文档
## 1. 重构目标
### 1.1 核心原则
- **单一职责**:每个模块只负责一个明确的功能
- **依赖倒置**:高层模块不依赖低层模块,都依赖抽象
- **开闭原则**:对扩展开放,对修改关闭
### 1.2 目标架构
```
src/
├── core/ # 核心业务逻辑(框架无关)
│ ├── entities/ # 业务实体
│ ├── repositories/ # 仓库接口
│ └── usecases/ # 业务用例
├── data/ # 数据层
│ ├── datasources/ # 数据源实现API、DB、Cache
│ ├── repositories/ # 仓库实现
│ └── mappers/ # 数据映射
├── presentation/ # 表现层
│ ├── components/ # UI组件
│ ├── screens/ # 屏幕组件
│ ├── stores/ # 状态管理
│ └── hooks/ # 自定义hooks
└── infrastructure/ # 基础设施
├── navigation/ # 导航
├── theme/ # 主题
└── services/ # 外部服务
```
## 2. 具体修复方案
### 2.1 messageManager.ts 重构方案
#### 现状问题
- 1500+行,上帝对象
- 混合了 WebSocket、数据库、状态管理、UI通知
- 与SQLite深度耦合
#### 目标结构
```
src/data/
├── datasources/
│ ├── WebSocketClient.ts # WebSocket连接管理
│ └── MessageDataSource.ts # 消息数据源
├── repositories/
│ └── MessageRepository.ts # 消息仓库实现
└── mappers/
└── MessageMapper.ts # 消息数据映射
src/core/
├── entities/
│ └── Message.ts # 消息实体
├── repositories/
│ └── IMessageRepository.ts # 仓库接口
└── usecases/
├── ProcessMessageUseCase.ts
├── SyncMessagesUseCase.ts
└── UpdateReadReceiptUseCase.ts
src/presentation/stores/
└── messageStore.ts # 简化的消息状态管理
```
#### 重构步骤
1. 提取 WebSocketClient - 纯连接管理
2. 创建 MessageRepository - 数据访问抽象
3. 创建 UseCases - 业务逻辑
4. 重写 messageStore - 仅负责UI状态
5. 删除旧的 messageManager.ts
### 2.2 乐观更新工具函数方案
#### 现状问题
- userStore.ts 中重复6+次相同模式
- 代码冗余,难以维护
#### 目标API
```typescript
// utils/optimisticUpdate.ts
export interface OptimisticUpdateOptions<T, R> {
store: StoreApi<any>;
getCurrentState: () => T;
optimisticUpdate: (current: T) => T;
apiCall: () => Promise<R>;
onSuccess?: (result: R, current: T) => T;
onError?: (error: any, original: T) => void;
}
export async function optimisticUpdate<T, R>(
options: OptimisticUpdateOptions<T, R>
): Promise<R>
```
#### 使用示例
```typescript
// 重构前(重复代码)
set(state => ({ posts: updatedPosts }));
try {
const result = await postService.like(postId);
// ...
} catch (error) {
set(state => ({ posts: originalPosts }));
}
// 重构后(简洁)
await optimisticUpdate({
store,
getCurrentState: () => store.getState().posts,
optimisticUpdate: (posts) => updatePostLikes(posts, postId, true),
apiCall: () => postService.like(postId),
onSuccess: (result, posts) => updatePostWithServerData(posts, result),
});
```
### 2.3 useResponsive.ts 拆分方案
#### 现状问题
- 485行43个导出
- 功能混杂:断点检测、响应式值计算、平台检测
#### 目标结构
```
src/presentation/hooks/responsive/
├── useBreakpoint.ts # 断点检测
├── useResponsiveValue.ts # 响应式值映射
├── useOrientation.ts # 方向检测
├── usePlatform.ts # 平台检测
├── useScreenSize.ts # 屏幕尺寸
└── index.ts # 统一导出
```
#### 各Hook职责
- `useBreakpoint`: 返回当前断点 (mobile/tablet/desktop/wide)
- `useResponsiveValue`: 根据断点返回不同值
- `useOrientation`: 返回 portrait/landscape
- `usePlatform`: 返回 ios/android/web
- `useScreenSize`: 返回 width/height
### 2.4 MainNavigator 解耦方案
#### 现状问题
- 1118行直接依赖store
- 混合了导航配置和业务逻辑
#### 目标结构
```
src/infrastructure/navigation/
├── MainNavigator.tsx # 简化后的导航器
├── AuthNavigator.tsx # 认证导航
├── TabNavigator.tsx # 标签导航
├── navigationService.ts # 导航服务(解耦层)
├── navigationTypes.ts # 类型定义
└── hooks/
└── useNavigationState.ts # 导航状态hook
```
#### 解耦策略
1. 创建 NavigationService - 提供导航操作的抽象
2. Store通过NavigationService触发导航而非直接操作
3. MainNavigator只负责渲染不处理业务逻辑
### 2.5 Repository 层创建方案
#### 现状问题
- 服务层直接操作数据库
- 没有明确的数据访问边界
#### 目标结构
```
src/data/repositories/
├── MessageRepository.ts # 消息数据访问
├── PostRepository.ts # 帖子数据访问
├── UserRepository.ts # 用户数据访问
└── interfaces/
├── IMessageRepository.ts
├── IPostRepository.ts
└── IUserRepository.ts
```
#### 职责划分
- **Repository**: 数据访问抽象,定义接口
- **DataSource**: 具体数据源实现API、SQLite、Cache
- **Service**: 业务逻辑编排使用Repository
## 3. 实施计划
### 阶段1基础设施第1-2天
- [ ] 创建目录结构
- [ ] 实现乐观更新工具函数
- [ ] 拆分 useResponsive hooks
### 阶段2数据层重构第3-5天
- [ ] 创建 Repository 接口和实现
- [ ] 创建 DataSource 层
- [ ] 迁移数据库操作到 Repository
### 阶段3核心业务重构第6-10天
- [ ] 重构 messageManager → UseCases + Store
- [ ] 创建 WebSocketClient
- [ ] 实现消息同步逻辑
### 阶段4表现层重构第11-12天
- [ ] 解耦 MainNavigator
- [ ] 重构 userStore 使用乐观更新工具
- [ ] 更新所有屏幕组件
### 阶段5测试与优化第13-14天
- [ ] 单元测试
- [ ] 集成测试
- [ ] 性能优化
## 4. 风险与应对
| 风险 | 影响 | 应对措施 |
|-----|------|---------|
| 重构引入bug | 高 | 每个模块重构后必须测试保持API兼容 |
| 开发时间超期 | 中 | 分阶段交付,优先核心功能 |
| 团队成员不熟悉新架构 | 中 | 编写详细文档,代码审查 |
| 性能下降 | 低 | 添加性能监控,及时优化 |
## 5. 验收标准
- [ ] messageManager.ts 被删除,功能拆分到多个小模块
- [ ] userStore.ts 中无重复乐观更新代码
- [ ] useResponsive.ts 拆分为5+个专注hook
- [ ] MainNavigator.tsx 行数 < 300
- [ ] 所有单元测试通过
- [ ] 无TypeScript错误
- [ ] 性能不劣化(启动时间、内存占用)

View File

@@ -1,394 +0,0 @@
# Messages模块与PostService/Manager架构对比分析报告
## 一、Messages模块架构分析
### 1.1 架构层次结构
Messages模块采用了**清晰的分层架构**,各层职责明确:
```
┌─────────────────────────────────────────────────────────────┐
│ 表现层 (Screens) │
│ ChatScreen, MessageListScreen, NotificationsScreen │
├─────────────────────────────────────────────────────────────┤
│ Hooks层 │
│ useDifferentialMessages, useChatScreen │
├─────────────────────────────────────────────────────────────┤
│ 用例层 (UseCases) │
│ ProcessMessageUseCase │
├─────────────────────────────────────────────────────────────┤
│ 领域层 (Entities) │
│ Message, Conversation, GroupNotice │
├─────────────────────────────────────────────────────────────┤
│ 仓库层 (Repositories) │
│ MessageRepository, IMessageRepository │
├─────────────────────────────────────────────────────────────┤
│ 数据源层 (DataSources) │
│ SSEClient, LocalDataSource, ApiDataSource │
└─────────────────────────────────────────────────────────────┘
```
### 1.2 核心组件详解
#### 1.2.1 领域实体层 (Core/Entities)
- **Message.ts**: 定义消息、会话、群通知等核心领域模型
- 包含工厂函数:`createMessage`, `createConversation`
- 包含业务逻辑函数:`isMessageFromCurrentUser`, `shouldIncrementUnread`, `buildTextContent`
#### 1.2.2 用例层 (Core/UseCases)
- **ProcessMessageUseCase**: 核心业务逻辑编排器
- 订阅SSE事件chat, group_message, read, recall, typing, group_notice等
- 消息去重处理processedMessageIds Set
- 已读状态保护pendingReadMap + 版本号机制)
- 用户信息缓存与去重请求pendingUserRequests Map
- 事件发布订阅模式subscribers Set
#### 1.2.3 仓库层 (Data/Repositories)
- **IMessageRepository**: 定义数据访问接口
- 消息操作getMessages, saveMessage, updateMessageStatus, markAsRead
- 会话操作getConversations, saveConversation, updateUnreadCount
- 同步操作syncMessages, getServerLastSeq
- **MessageRepository**: 实现接口
- 封装SQLite数据库操作
- 消息与CachedMessage的转换
#### 1.2.4 映射器层 (Data/Mappers)
- **MessageMapper**: 数据转换
- `fromApiResponse`: API响应 → 应用模型
- `fromDbRecord`: 数据库记录 → 应用模型
- `toDbRecord`: 应用模型 → 数据库记录
- `toApiRequest`: 应用模型 → API请求
- 创建消息片段:`createTextSegment`, `createImageSegment`
#### 1.2.5 差异计算基础设施 (Infrastructure/Diff)
- **MessageDiffCalculator**: 消息列表差异计算
- 计算added, updated, deleted, moved, unchanged
- 变化比例检测changeRatio > 0.5 时触发重置)
- 增量差异计算(基于缓存)
- **MessageUpdateBatcher**: 批量更新处理器
- 16ms批量间隔约60fps
- 去重和合并更新
- 100ms最大等待时间
- 统计信息totalBatches, totalUpdates, averageBatchSize
- **types.ts**: 完整的类型定义
- MessageUpdateType枚举ADD, UPDATE, DELETE, MOVE, BATCH_*
- DiffResult, DiffConfig等接口
#### 1.2.6 Hook层
- **useDifferentialMessages**: 差异更新Hook
- 集成DiffCalculator和Batcher
- 自动处理批量更新
- 提供flush, reset, forceUpdate方法
- 支持配置enableDiff, enableBatching, maxMessageCount, changeRatioThreshold
### 1.3 数据流
```
SSE事件 → ProcessMessageUseCase → 事件发布 → useChatScreen → useDifferentialMessages
↓ ↓
MessageRepository 差异计算 + 批量处理
↓ ↓
SQLite数据库 优化后的消息列表
```
### 1.4 状态管理方式
- **发布-订阅模式**ProcessMessageUseCase作为事件中心
- **本地SQLite缓存**:消息持久化存储
- **内存状态**通过Hook返回组件直接消费
- **差异更新**通过MessageDiffCalculator + MessageUpdateBatcher减少重渲染
---
## 二、PostService/Manager架构分析
### 2.1 架构层次结构
Post模块采用了**扁平化架构**Service和Manager层职责混合
```
┌─────────────────────────────────────────────────────────────┐
│ 表现层 (Screens) │
│ HomeScreen, PostDetailScreen, CreatePostScreen │
├─────────────────────────────────────────────────────────────┤
│ Hooks层 │
│ useCursorPagination (通用), usePrefetch │
├─────────────────────────────────────────────────────────────┤
│ Store层 (Zustand) │
│ useUserStore (直接操作状态), postManager (缓存管理) │
├─────────────────────────────────────────────────────────────┤
│ Service层 │
│ postService (API调用 + 状态更新) │
├─────────────────────────────────────────────────────────────┤
│ 数据源 │
│ API (直接调用) │
└─────────────────────────────────────────────────────────────┘
```
### 2.2 核心组件详解
#### 2.2.1 Service层 (Services)
- **postService.ts**: 帖子服务
- CRUD操作getPosts, getPost, createPost, updatePost, deletePost
- 互动操作likePost, unlikePost, favoritePost, unfavoritePost
- 分页支持:传统分页 + 游标分页
- **问题**直接操作useUserStore违反分层原则
#### 2.2.2 Manager层 (Stores)
- **postManager.ts**: 帖子缓存管理器
- 继承CacheBus发布订阅基类
- 内存缓存listCache, detailCache
- 请求去重pendingRequests Map
- TTL管理LIST_TTL=30s, DETAIL_TTL=60s
- 后台刷新机制
#### 2.2.3 Store层 (Zustand)
- **useUserStore**: 用户状态存储
- 直接存储posts数组
- 帖子操作副作用直接修改状态
- 混合了用户数据和帖子数据
#### 2.2.4 映射器层 (Data/Mappers)
- **PostMapper**: 数据转换
- `fromApiResponse`: API响应 → 应用模型
- `fromApiResponseList`: 批量转换
- `toApiRequest`: 应用模型 → API请求
- 相比MessageMapper缺少数据库记录转换方法
### 2.3 数据流
```
HomeScreen → useCursorPagination → postService.getPostsCursor
useUserStore.setState(posts)
PostCard组件渲染
```
### 2.4 状态管理方式
- **Zustand Store**:集中式状态管理
- **内存缓存**postManager提供应用级缓存
- **请求去重**dedupe方法防止重复请求
- **直接修改**postService直接调用useUserStore.setState
---
## 三、主要架构差异
### 3.1 分层复杂度
| 维度 | Messages模块 | Post模块 |
|------|-------------|---------|
| 层次深度 | 6层Entity→UseCase→Repository→Mapper→Hook→Screen | 4层Service→Store→Hook→Screen |
| 用例层 | 独立ProcessMessageUseCase | 无 |
| 仓库接口 | IMessageRepository抽象接口 | 无 |
| 基础设施 | Diff模块Calculator + Batcher | 无 |
### 3.2 数据持久化
| 维度 | Messages模块 | Post模块 |
|------|-------------|---------|
| 本地存储 | SQLite数据库 | 无 |
| 缓存抽象 | MessageRepository封装 | postManager内存缓存 |
| 离线支持 | 完整 | 不支持 |
### 3.3 实时更新
| 维度 | Messages模块 | Post模块 |
|------|-------------|---------|
| 推送机制 | SSE实时推送 | 轮询/下拉刷新 |
| 事件订阅 | ProcessMessageUseCase发布订阅 | 无 |
| 增量更新 | MessageDiffCalculator | 无 |
### 3.4 状态管理
| 维度 | Messages模块 | Post模块 |
|------|-------------|---------|
| 管理方式 | 发布订阅 + Hook局部状态 | Zustand全局Store |
| 状态来源 | useChatScreen, useDifferentialMessages | useUserStore |
| 批量更新 | MessageUpdateBatcher | 无 |
| 差异检测 | MessageDiffCalculator | 无 |
### 3.5 依赖方向
**Messages模块正确**
```
Hook → UseCase → Repository → DataSource
Entity不依赖外部
```
**Post模块问题**
```
Service → Store循环依赖风险
API直接调用
```
### 3.6 核心问题总结
| # | 问题 | Messages | Post |
|---|------|---------|------|
| 1 | 违反分层原则 | 无 | postService直接操作useUserStore |
| 2 | 缺少UseCase层 | ProcessMessageUseCase | 无业务逻辑编排 |
| 3 | 缺少Repository抽象 | IMessageRepository | 无数据访问接口 |
| 4 | 无差异更新 | useDifferentialMessages | 无 |
| 5 | 无批量处理 | MessageUpdateBatcher | 无 |
| 6 | 无本地持久化 | SQLite | 无 |
| 7 | 无实时推送 | SSE | 无 |
---
## 四、对齐建议
### 4.1 架构重构目标
将Post模块对齐到Messages模块的架构模式
```
┌─────────────────────────────────────────────────────────────┐
│ 表现层 (Screens) │
│ HomeScreen, PostDetailScreen │
├─────────────────────────────────────────────────────────────┤
│ Hooks层 │
│ useDifferentialPosts (新增) │
├─────────────────────────────────────────────────────────────┤
│ 用例层 (UseCases) │
│ ProcessPostUseCase (新增) │
├─────────────────────────────────────────────────────────────┤
│ 领域层 (Entities) │
│ Post, PostComment (新增) │
├─────────────────────────────────────────────────────────────┤
│ 仓库层 (Repositories) │
│ PostRepository, IPostRepository (新增) │
├─────────────────────────────────────────────────────────────┤
│ Service层 │
│ postService (仅保留API调用移除状态操作) │
└─────────────────────────────────────────────────────────────┘
```
### 4.2 具体改造项
#### 4.2.1 创建Post领域实体
```typescript
// src/core/entities/Post.ts
export interface Post {
id: string;
authorId: string;
title: string;
content: string;
images: string[];
likeCount: number;
commentCount: number;
// ... 其他字段
}
```
#### 4.2.2 创建ProcessPostUseCase
```typescript
// src/core/usecases/ProcessPostUseCase.ts
class ProcessPostUseCase {
// 帖子增删改查
// 点赞/收藏逻辑
// 事件发布订阅
}
```
#### 4.2.3 创建IPostRepository接口
```typescript
// src/data/repositories/interfaces/IPostRepository.ts
interface IPostRepository {
getPosts(type: string, page: number, pageSize: number): Promise<Post[]>;
savePost(post: Post): Promise<void>;
updatePost(postId: string, updates: Partial<Post>): Promise<void>;
// ...
}
```
#### 4.2.4 创建PostRepository实现
```typescript
// src/data/repositories/PostRepository.ts
// 封装SQLite操作实现IPostRepository接口
```
#### 4.2.5 创建useDifferentialPosts Hook
```typescript
// src/hooks/useDifferentialPosts.ts
// 类似useDifferentialMessages处理帖子列表差异更新
```
#### 4.2.6 改造postService
- 移除对useUserStore的直接依赖
- 仅保留API调用职责
---
## 五、架构对比图
```mermaid
graph TB
subgraph "Messages模块 [理想架构]"
E1[Entity<br/>Message.ts]
U1[UseCase<br/>ProcessMessageUseCase]
R1[Repository<br/>MessageRepository]
M1[Mapper<br/>MessageMapper]
H1[Hook<br/>useDifferentialMessages]
D1[Diff基础设施<br/>DiffCalculator + Batcher]
S1[(SQLite)]
E1 --> U1
U1 --> R1
U1 --> D1
R1 --> S1
M1 --> R1
D1 --> H1
H1 --> S1
end
subgraph "Post模块 [当前架构]"
E2[PostMapper]
SV2[postService<br/>直接操作Store]
ST2[useUserStore<br/>Zustand]
H2[Hook<br/>useCursorPagination]
PM2[postManager<br/>内存缓存]
E2 --> SV2
SV2 --> ST2
H2 --> SV2
PM2 --> ST2
end
style E1 fill:#90EE90
style U1 fill:#90EE90
style R1 fill:#90EE90
style D1 fill:#90EE90
style H1 fill:#90EE90
style S1 fill:#90EE90
style SV2 fill:#FFB6C1
style ST2 fill:#FFB6C1
```
---
## 六、结论
Messages模块是一个**架构完善**的模块,具备:
1. 清晰的分层架构
2. 独立的业务逻辑编排层UseCase
3. 完整的数据持久化SQLite
4. 高效的差异更新机制
5. 实时事件推送能力
Post模块当前存在以下问题
1. **违反分层原则**Service直接操作Store
2. **缺少UseCase层**:业务逻辑分散
3. **无数据抽象**缺少Repository接口
4. **无差异更新**:每次全量更新导致性能问题
5. **无本地持久化**:无法离线使用
建议按照对齐建议逐步重构Post模块使其架构与Messages模块对齐。

View File

@@ -1,689 +0,0 @@
/**
* ProcessMessageUseCase - 处理消息用例
* 处理消息的业务逻辑,协调 Repository 和 SSEClient
* 纯业务逻辑无UI依赖
*/
import { messageRepository, userCacheRepository, CachedMessage } from '@/database';
import { wsClient, WSEventType } from '../../data/datasources/WSClient';
import { MessageMapper } from '@/data/mappers';
import {
Message,
Conversation,
GroupNotice,
createMessage,
isMessageFromCurrentUser,
shouldIncrementUnread,
buildGroupNoticeText,
} from '../entities/Message';
import { messageService } from '../../services/messageService';
import { api } from '../../services/api';
const cachedMessageToMessage = (cached: CachedMessage): Message => ({
id: cached.id,
conversationId: cached.conversationId,
senderId: cached.senderId,
seq: cached.seq,
segments: cached.segments || [],
createdAt: cached.createdAt,
status: cached.status as Message['status'],
});
// 事件类型定义
export type MessageUseCaseEventType =
| 'message_received'
| 'message_recalled'
| 'typing_status'
| 'group_notice'
| 'connection_changed'
| 'error';
export interface MessageUseCaseEvent {
type: MessageUseCaseEventType;
payload: any;
timestamp: number;
}
export type MessageUseCaseSubscriber = (event: MessageUseCaseEvent) => void;
// 已读状态保护记录
interface ReadStateRecord {
timestamp: number;
version: number;
lastReadSeq: number;
clearTimer?: ReturnType<typeof setTimeout>;
}
class ProcessMessageUseCase {
private subscribers: Set<MessageUseCaseSubscriber> = new Set();
private unsubscribeFns: Array<() => void> = [];
private processedMessageIds: Set<string> = new Set();
private processedMessageIdsExpiry: Map<string, number> = new Map();
private pendingReadMap: Map<string, ReadStateRecord> = new Map();
private readStateVersion = 0;
private pendingUserRequests: Map<string, Promise<any>> = new Map();
private currentUserId: string | null = null;
// 常量
private readonly READ_STATE_PROTECTION_DELAY = 5000; // 5秒
private readonly MESSAGE_ID_EXPIRY = 5 * 60 * 1000; // 5分钟
/**
* 初始化用例
*/
initialize(currentUserId: string | null): void {
this.currentUserId = currentUserId;
this.setupWSListeners();
}
/**
* 销毁用例
*/
destroy(): void {
this.unsubscribeFns.forEach((fn) => fn());
this.unsubscribeFns = [];
this.subscribers.clear();
this.processedMessageIds.clear();
this.processedMessageIdsExpiry.clear();
this.pendingReadMap.clear();
this.pendingUserRequests.clear();
}
/**
* 设置SSE监听器
*/
private setupWSListeners(): void {
// 监听私聊消息
const unsubChat = wsClient.on('chat', (message) => {
this.handleNewMessage(message);
});
// 监听群聊消息
const unsubGroupMessage = wsClient.on('group_message', (message) => {
this.handleNewMessage(message);
});
// 监听私聊已读回执
const unsubRead = wsClient.on('read', (message) => {
this.handleReadReceipt(message);
});
// 监听群聊已读回执
const unsubGroupRead = wsClient.on('group_read', (message) => {
this.handleGroupReadReceipt(message);
});
// 监听私聊消息撤回
const unsubRecall = wsClient.on('recall', (message) => {
this.handleRecallMessage(message);
});
// 监听群聊消息撤回
const unsubGroupRecall = wsClient.on('group_recall', (message) => {
this.handleGroupRecallMessage(message);
});
// 监听群聊输入状态
const unsubGroupTyping = wsClient.on('group_typing', (message) => {
this.handleGroupTyping(message);
});
// 监听群通知
const unsubGroupNotice = wsClient.on('group_notice', (message) => {
this.handleGroupNotice(message);
});
// 监听连接状态
const unsubConnected = wsClient.on('connected', () => {
this.notifySubscribers({
type: 'connection_changed',
payload: { connected: true },
timestamp: Date.now(),
});
});
const unsubDisconnected = wsClient.on('disconnected', () => {
this.notifySubscribers({
type: 'connection_changed',
payload: { connected: false },
timestamp: Date.now(),
});
});
this.unsubscribeFns = [
unsubChat,
unsubGroupMessage,
unsubRead,
unsubGroupRead,
unsubRecall,
unsubGroupRecall,
unsubGroupTyping,
unsubGroupNotice,
unsubConnected,
unsubDisconnected,
];
}
/**
* 处理新消息
*/
private async handleNewMessage(message: any): Promise<void> {
const { conversation_id, id, sender_id, seq, segments, created_at, _isAck } = message;
// 消息去重检查
if (this.isMessageProcessed(id)) {
return;
}
this.markMessageAsProcessed(id);
// 构造消息对象
const newMessage = createMessage({
id,
conversationId: String(conversation_id),
senderId: sender_id,
seq,
segments: segments || [],
createdAt: created_at,
});
// 异步获取发送者信息(如果是群聊且不是当前用户发送的)
if (message.type === 'group_message' && sender_id && sender_id !== this.currentUserId) {
this.enrichMessageWithSender(newMessage);
}
// 异步保存到本地数据库
const isRead = _isAck || sender_id === this.currentUserId;
messageRepository.saveMessage({
id: newMessage.id,
conversationId: newMessage.conversationId,
senderId: newMessage.senderId,
seq: newMessage.seq,
segments: newMessage.segments,
createdAt: newMessage.createdAt,
status: newMessage.status || 'normal',
isRead,
}).catch((error) => {
console.error('[ProcessMessageUseCase] 保存消息失败:', error);
});
// 通知订阅者
this.notifySubscribers({
type: 'message_received',
payload: {
message: newMessage,
isCurrentUser: sender_id === this.currentUserId,
isAck: _isAck,
},
timestamp: Date.now(),
});
}
/**
* 丰富消息的发送者信息
*/
private async enrichMessageWithSender(message: Message): Promise<void> {
try {
const user = await this.getSenderInfo(message.senderId);
if (user) {
message.sender = user;
// 重新通知,包含发送者信息
this.notifySubscribers({
type: 'message_received',
payload: {
message,
isCurrentUser: false,
isUpdated: true,
},
timestamp: Date.now(),
});
}
} catch (error) {
console.error('[ProcessMessageUseCase] 获取发送者信息失败:', error);
}
}
/**
* 获取发送者信息(带缓存和去重)
*/
private async getSenderInfo(userId: string): Promise<any | null> {
// 先检查本地缓存
const cachedUser = await userCacheRepository.get(userId);
if (cachedUser) {
return cachedUser;
}
// 检查是否已有正在进行的请求
const pendingRequest = this.pendingUserRequests.get(userId);
if (pendingRequest) {
return pendingRequest;
}
// 发起新请求
const request = this.fetchUserInfo(userId);
this.pendingUserRequests.set(userId, request);
try {
const user = await request;
return user;
} finally {
this.pendingUserRequests.delete(userId);
}
}
/**
* 从服务器获取用户信息
*/
private async fetchUserInfo(userId: string): Promise<any | null> {
try {
const response = await api.get<any>(`/users/${userId}`);
if (response.code === 0 && response.data) {
await userCacheRepository.save(response.data as any);
return response.data;
}
return null;
} catch (error) {
console.error(`[ProcessMessageUseCase] 获取用户信息失败: ${userId}`, error);
return null;
}
}
/**
* 检查消息是否已处理
*/
private isMessageProcessed(messageId: string): boolean {
return this.processedMessageIds.has(messageId);
}
/**
* 标记消息已处理
*/
private markMessageAsProcessed(messageId: string): void {
this.processedMessageIds.add(messageId);
this.processedMessageIdsExpiry.set(messageId, Date.now());
// 定期清理
if (this.processedMessageIds.size > 1000) {
this.cleanupProcessedMessageIds();
}
}
/**
* 清理过期的消息ID
*/
private cleanupProcessedMessageIds(): void {
const now = Date.now();
for (const [id, timestamp] of this.processedMessageIdsExpiry.entries()) {
if (now - timestamp > this.MESSAGE_ID_EXPIRY) {
this.processedMessageIds.delete(id);
this.processedMessageIdsExpiry.delete(id);
}
}
}
/**
* 处理私聊已读回执
*/
private handleReadReceipt(message: any): void {
// 可以在这里处理对方已读的状态更新
}
/**
* 处理群聊已读回执
*/
private handleGroupReadReceipt(message: any): void {
// 可以在这里处理群聊已读状态更新
}
/**
* 处理私聊消息撤回
*/
private handleRecallMessage(message: any): Promise<void> {
const { conversation_id, message_id } = message;
// 更新本地数据库状态
messageRepository
.updateStatus(message_id, 'recalled', true)
.catch((error: unknown) => {
console.error('[ProcessMessageUseCase] 更新撤回状态失败:', error);
});
// 通知订阅者
this.notifySubscribers({
type: 'message_recalled',
payload: {
conversationId: String(conversation_id),
messageId: message_id,
isGroup: false,
},
timestamp: Date.now(),
});
return Promise.resolve();
}
/**
* 处理群聊消息撤回
*/
private handleGroupRecallMessage(message: any): Promise<void> {
const { conversation_id, message_id } = message;
// 更新本地数据库状态
messageRepository
.updateStatus(message_id, 'recalled', true)
.catch((error: unknown) => {
console.error('[ProcessMessageUseCase] 更新撤回状态失败:', error);
});
// 通知订阅者
this.notifySubscribers({
type: 'message_recalled',
payload: {
conversationId: String(conversation_id),
messageId: message_id,
isGroup: true,
},
timestamp: Date.now(),
});
return Promise.resolve();
}
/**
* 处理群聊输入状态
*/
private handleGroupTyping(message: any): void {
const { group_id, user_id, is_typing } = message;
this.notifySubscribers({
type: 'typing_status',
payload: {
groupId: String(group_id),
userId: user_id,
isTyping: is_typing,
},
timestamp: Date.now(),
});
}
/**
* 处理群通知
*/
private handleGroupNotice(message: any): void {
const { notice_type, group_id, data, timestamp, message_id, seq } = message;
const groupIdStr = String(group_id);
const notice: GroupNotice = {
type: notice_type,
groupId: groupIdStr,
data: data || {},
timestamp: timestamp || Date.now(),
messageId: message_id,
seq,
};
// 通知订阅者
this.notifySubscribers({
type: 'group_notice',
payload: {
notice,
text: buildGroupNoticeText(notice),
},
timestamp: Date.now(),
});
}
/**
* 标记会话已读
*/
async markAsRead(conversationId: string, seq: number): Promise<void> {
const existingRecord = this.pendingReadMap.get(conversationId);
// 使用seq去重
if (existingRecord && seq <= existingRecord.lastReadSeq) {
return;
}
// 清除可能存在的旧定时器
if (existingRecord?.clearTimer) {
clearTimeout(existingRecord.clearTimer);
}
// 递增全局版本号
this.readStateVersion++;
const currentVersion = this.readStateVersion;
// 记录已读状态
this.pendingReadMap.set(conversationId, {
timestamp: Date.now(),
version: currentVersion,
lastReadSeq: seq,
});
// 更新本地数据库
await messageRepository.markConversationAsRead(conversationId);
// 调用API
try {
await messageService.markAsRead(conversationId, seq);
} catch (error) {
console.error('[ProcessMessageUseCase] 标记已读API失败:', error);
this.pendingReadMap.delete(conversationId);
throw error;
}
// 设置延迟清除保护
const clearTimer = setTimeout(() => {
const record = this.pendingReadMap.get(conversationId);
if (record && record.version === currentVersion) {
this.pendingReadMap.delete(conversationId);
}
}, this.READ_STATE_PROTECTION_DELAY);
this.pendingReadMap.set(conversationId, {
timestamp: Date.now(),
version: currentVersion,
lastReadSeq: seq,
clearTimer,
});
}
/**
* 检查会话是否有进行中的已读请求
*/
hasPendingRead(conversationId: string): boolean {
return this.pendingReadMap.has(conversationId);
}
/**
* 获取进行中的已读记录
*/
getPendingReadRecord(conversationId: string): ReadStateRecord | undefined {
return this.pendingReadMap.get(conversationId);
}
/**
* 发送消息
*/
async sendMessage(
conversationId: string,
segments: any[],
options?: { replyToId?: string }
): Promise<Message | null> {
try {
const response = await messageService.sendMessage(conversationId, {
segments,
reply_to_id: options?.replyToId,
});
if (response) {
const message = createMessage({
id: response.id,
conversationId,
senderId: this.currentUserId || '',
seq: response.seq,
segments,
createdAt: new Date().toISOString(),
});
// 保存到本地数据库
await messageRepository.saveMessage({
id: message.id,
conversationId: message.conversationId,
senderId: message.senderId,
seq: message.seq,
segments: message.segments,
createdAt: message.createdAt,
status: message.status || 'normal',
isRead: true,
});
return message;
}
return null;
} catch (error) {
console.error('[ProcessMessageUseCase] 发送消息失败:', error);
throw error;
}
}
/**
* 获取本地消息
*/
async getLocalMessages(conversationId: string, limit: number = 20): Promise<Message[]> {
const cached = await messageRepository.getByConversation(conversationId, limit);
return cached.map(cachedMessageToMessage);
}
/**
* 获取历史消息
*/
async getHistoryMessages(
conversationId: string,
beforeSeq: number,
limit: number = 20
): Promise<Message[]> {
// 先从本地获取
const localMessages = await messageRepository.getBeforeSeq(
conversationId,
beforeSeq,
limit
);
if (localMessages.length >= limit) {
return localMessages.map(cachedMessageToMessage);
}
// 本地数据不足,从服务端获取
try {
const response = await messageService.getMessages(
conversationId,
undefined,
beforeSeq,
limit
);
if (response?.messages && response.messages.length > 0) {
const serverMessages = response.messages.map((m: any) =>
createMessage({
id: m.id,
conversationId: m.conversation_id || conversationId,
senderId: m.sender_id,
seq: m.seq,
segments: m.segments || [],
createdAt: m.created_at,
status: m.status || 'normal',
})
);
// 保存到本地
await messageRepository.saveMessagesBatch(
MessageMapper.toCachedMessages(serverMessages, conversationId)
);
return serverMessages;
}
} catch (error) {
console.error('[ProcessMessageUseCase] 获取历史消息失败:', error);
}
return localMessages.map(cachedMessageToMessage);
}
/**
* 从服务端同步消息
*/
async syncMessagesFromServer(
conversationId: string,
afterSeq?: number
): Promise<Message[]> {
try {
const response = await messageService.getMessages(conversationId, afterSeq);
if (response?.messages && response.messages.length > 0) {
const messages = response.messages.map((m: any) =>
createMessage({
id: m.id,
conversationId: m.conversation_id || conversationId,
senderId: m.sender_id,
seq: m.seq,
segments: m.segments || [],
createdAt: m.created_at,
status: m.status || 'normal',
})
);
await messageRepository.saveMessagesBatch(
MessageMapper.toCachedMessages(response.messages, conversationId)
);
return messages;
}
} catch (error) {
console.error('[ProcessMessageUseCase] 同步消息失败:', error);
}
return [];
}
/**
* 订阅事件
*/
subscribe(subscriber: MessageUseCaseSubscriber): () => void {
this.subscribers.add(subscriber);
return () => {
this.subscribers.delete(subscriber);
};
}
/**
* 通知订阅者
*/
private notifySubscribers(event: MessageUseCaseEvent): void {
this.subscribers.forEach((subscriber) => {
try {
subscriber(event);
} catch (error) {
console.error('[ProcessMessageUseCase] 订阅者执行失败:', error);
}
});
}
/**
* 获取当前用户ID
*/
getCurrentUserId(): string | null {
return this.currentUserId;
}
/**
* 设置当前用户ID
*/
setCurrentUserId(userId: string | null): void {
this.currentUserId = userId;
}
}
export const processMessageUseCase = new ProcessMessageUseCase();
export default processMessageUseCase;

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,3 @@
/**
* 帖子差异更新 Hook
* 接收原始帖子列表,返回优化后的帖子列表,自动处理批量更新
* 集成 ProcessPostUseCase 进行状态管理
*/
import { useState, useRef, useCallback, useEffect } from 'react';
import {
PostIdentifier,
@@ -20,98 +14,47 @@ import {
PostBatcherOptions,
createPostUpdateBatcher,
} from '../infrastructure/diff/PostUpdateBatcher';
import { processPostUseCase } from '../core/usecases/ProcessPostUseCase';
import type { PostUseCaseEvent, PostsState } from '../core/usecases/ProcessPostUseCase';
import { postSyncService } from '../services/post/PostSyncService';
import type { PostsState } from '../services/post/PostSyncService';
import { usePostListStore } from '../stores/post/postListStore';
import type { Post } from '../core/entities/Post';
// ==================== 类型定义 ====================
/**
* 差异更新 Hook 配置选项
*/
export interface UseDifferentialPostsOptions<T extends PostIdentifier> {
/** 差异计算配置 */
diffConfig?: PostDiffConfig;
/** 批量处理配置 */
batcherOptions?: PostBatcherOptions;
/** 是否启用差异计算,默认 true */
enableDiff?: boolean;
/** 是否启用批量处理,默认 true */
enableBatching?: boolean;
/** 最大帖子数量限制,超出时触发重置 */
maxPostCount?: number;
/** 变化比例阈值0-1超过此值触发重置 */
changeRatioThreshold?: number;
/** 列表键(用于区分不同的帖子列表) */
listKey?: string;
/** 是否自动订阅 UseCase 状态变化,默认 true */
autoSubscribe?: boolean;
}
/**
* 差异更新信息
*/
export interface DiffUpdatesInfo {
/** 新增数量 */
addedCount: number;
/** 更新数量 */
updatedCount: number;
/** 删除数量 */
deletedCount: number;
/** 上次更新时间 */
lastUpdateTime: number;
}
/**
* 差异更新 Hook 返回值
*/
export interface UseDifferentialPostsResult<T extends PostIdentifier> {
/** 优化后的帖子列表 */
posts: T[];
/** 是否正在加载 */
loading: boolean;
/** 是否首屏加载 */
isInitialLoading: boolean;
/** 是否正在加载更多 */
isLoadingMore: boolean;
/** 是否正在刷新 */
refreshing: boolean;
/** 错误信息 */
error: string | null;
/** 是否有更多数据 */
hasMore: boolean;
/** 待处理更新数量 */
pendingUpdateCount: number;
/** 是否正在处理更新 */
isProcessing: boolean;
/** 刷新方法 */
refresh: () => Promise<void>;
/** 加载更多方法 */
loadMore: () => Promise<void>;
/** 手动刷新批量处理队列 */
flush: () => void;
/** 重置方法 */
reset: () => void;
/** 强制更新(跳过批量处理) */
forceUpdate: (posts: T[]) => void;
/** 获取差异统计信息 */
getDiffStats: () => {
totalBatches: number;
totalUpdates: number;
averageBatchSize: number;
};
/** 差异更新信息 */
getDiffStats: () => { totalBatches: number; totalUpdates: number; averageBatchSize: number };
diffUpdates: DiffUpdatesInfo;
}
// ==================== Hook 实现 ====================
/**
* 帖子差异更新 Hook
* @param initialPosts 初始帖子列表(可选)
* @param options 配置选项
* @returns 优化后的帖子列表和相关方法
*/
export function useDifferentialPosts<T extends PostIdentifier = Post>(
initialPosts: T[] = [],
options: UseDifferentialPostsOptions<T> = {}
@@ -126,8 +69,6 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
autoSubscribe = true,
} = options;
// ==================== 状态 ====================
const [posts, setPosts] = useState<T[]>(initialPosts);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
@@ -142,23 +83,15 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
lastUpdateTime: 0,
});
// ==================== Refs ====================
const calculatorRef = useRef<PostDiffCalculator<T> | null>(null);
const batcherRef = useRef<PostUpdateBatcher | null>(null);
const previousPostsRef = useRef<T[]>(initialPosts);
const unsubscribeRef = useRef<(() => void) | null>(null);
// ==================== 批量更新处理(须在订阅 batcher 的 useEffect 之前定义)====================
/**
* 处理批量更新
*/
const handleBatchUpdates = useCallback((updates: PostUpdate[]) => {
setIsProcessing(true);
try {
setPosts((currentPosts) => {
setPosts(currentPosts => {
let newPosts = [...currentPosts];
let addedCount = 0;
let updatedCount = 0;
@@ -210,9 +143,7 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
const deleteUpdate = update as any;
const prevLength = newPosts.length;
newPosts = newPosts.filter(p => p.id !== deleteUpdate.postId);
if (newPosts.length < prevLength) {
deletedCount++;
}
if (newPosts.length < prevLength) deletedCount++;
break;
}
@@ -243,7 +174,6 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
}
}
// 更新差异统计
if (addedCount > 0 || updatedCount > 0 || deletedCount > 0) {
setDiffUpdates(prev => ({
addedCount: prev.addedCount + addedCount,
@@ -261,28 +191,17 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
}
}, []);
// ==================== 初始化 ====================
// 初始化差异计算器
useEffect(() => {
if (enableDiff) {
calculatorRef.current = createPostDiffCalculator<T>(diffConfig);
}
return () => {
calculatorRef.current = null;
};
return () => { calculatorRef.current = null; };
}, [enableDiff, diffConfig]);
// 初始化批量处理器
useEffect(() => {
if (!enableBatching) return;
batcherRef.current = createPostUpdateBatcher(batcherOptions);
const unsubscribe = batcherRef.current.subscribe((updates) => {
handleBatchUpdates(updates);
});
const unsubscribe = batcherRef.current.subscribe(handleBatchUpdates);
return () => {
unsubscribe();
batcherRef.current?.destroy();
@@ -290,144 +209,46 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
};
}, [enableBatching, batcherOptions, handleBatchUpdates]);
// ==================== UseCase 订阅 ====================
const syncFromStore = useCallback((state: PostsState) => {
setLoading(state.isLoading);
setRefreshing(state.isRefreshing);
setError(state.error);
setHasMore(state.hasMore);
/**
* 处理 UseCase 事件
*/
const handleUseCaseEvent = useCallback((event: PostUseCaseEvent) => {
switch (event.type) {
case 'state_changed': {
const { key: eventKey, state } = event.payload as { key: string; state: PostsState };
// 全局订阅:只应用当前列表 key避免其他 Tab/列表的 state 覆盖本列表(如 createPost 会更新所有 key
if (eventKey !== listKey) {
break;
}
if (state) {
setLoading(state.isLoading);
setRefreshing(state.isRefreshing);
setError(state.error);
setHasMore(state.hasMore);
// ProcessPostUseCase 的 state.posts 已是合并后的完整列表。
// 不再走差异计算 + PostUpdateBatcher批量应用晚于 previousPostsRef 更新时,
// BATCH_ADD 会在过短的 currentPosts 上 splice导致连续游标加载「请求成功但列表不增长」。
batcherRef.current?.clearPending();
calculatorRef.current?.reset();
if (state.posts && state.posts.length > 0) {
const next = state.posts as unknown as T[];
if (next.length > maxPostCount) {
console.warn(`[useDifferentialPosts] Post count (${next.length}) exceeds limit (${maxPostCount})`);
}
setPosts(next);
previousPostsRef.current = next;
} else {
setPosts([]);
previousPostsRef.current = [];
}
}
break;
}
case 'posts_loaded': {
// fetchPosts 在 updatePostsState 时已发出 state_changed含合并后 posts此处不再应用避免与其它 key 竞态或重复计算
break;
}
case 'post_created': {
const { post } = event.payload;
if (post) {
// 新帖子添加到列表开头
setPosts(prev => [post as T, ...prev]);
setDiffUpdates(prev => ({
...prev,
addedCount: prev.addedCount + 1,
lastUpdateTime: Date.now(),
}));
}
break;
}
case 'post_updated': {
const { post } = event.payload;
if (post) {
setPosts(prev => prev.map(p => p.id === post.id ? { ...p, ...post } as T : p));
setDiffUpdates(prev => ({
...prev,
updatedCount: prev.updatedCount + 1,
lastUpdateTime: Date.now(),
}));
}
break;
}
case 'post_deleted': {
const { postId } = event.payload;
if (postId) {
setPosts(prev => prev.filter(p => p.id !== postId));
setDiffUpdates(prev => ({
...prev,
deletedCount: prev.deletedCount + 1,
lastUpdateTime: Date.now(),
}));
}
break;
}
case 'loading_changed': {
const { isLoading } = event.payload;
setLoading(isLoading);
break;
}
case 'error_occurred': {
const payload = event.payload as { key?: string; error?: string };
if (payload.key !== undefined && payload.key !== listKey) {
break;
}
if (payload.error !== undefined) {
setError(payload.error);
}
break;
batcherRef.current?.clearPending();
calculatorRef.current?.reset();
if (state.posts && state.posts.length > 0) {
const next = state.posts as unknown as T[];
if (next.length > maxPostCount) {
console.warn(`[useDifferentialPosts] Post count (${next.length}) exceeds limit (${maxPostCount})`);
}
setPosts(next);
previousPostsRef.current = next;
} else {
setPosts([]);
previousPostsRef.current = [];
}
}, [listKey, maxPostCount]);
}, [maxPostCount]);
// 订阅 UseCase 状态变化
useEffect(() => {
if (autoSubscribe) {
unsubscribeRef.current = processPostUseCase.subscribe(handleUseCaseEvent);
if (!autoSubscribe) return;
// 初始化时获取当前状态
const currentState = processPostUseCase.getPostsState(listKey);
if (currentState.posts.length > 0) {
setPosts(currentState.posts as unknown as T[]);
previousPostsRef.current = currentState.posts as unknown as T[];
}
setLoading(currentState.isLoading);
setRefreshing(currentState.isRefreshing);
setError(currentState.error);
setHasMore(currentState.hasMore);
const currentState = usePostListStore.getState().getPostsState(listKey);
syncFromStore(currentState);
return () => {
if (unsubscribeRef.current) {
unsubscribeRef.current();
unsubscribeRef.current = null;
}
};
}
}, [autoSubscribe, listKey, handleUseCaseEvent]);
const unsubscribe = usePostListStore.subscribe(state => {
const postsState = state.postsStateMap.get(listKey);
if (postsState) syncFromStore(postsState);
});
// ==================== 操作方法 ====================
return unsubscribe;
}, [autoSubscribe, listKey, syncFromStore]);
/**
* 刷新帖子列表
*/
const refresh = useCallback(async () => {
setRefreshing(true);
setError(null);
try {
await processPostUseCase.refreshPosts(listKey);
await postSyncService.refreshPosts(listKey);
} catch (err) {
const errorMsg = err instanceof Error ? err.message : '刷新失败';
setError(errorMsg);
@@ -436,15 +257,12 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
}
}, [listKey]);
/**
* 加载更多帖子
*/
const loadMore = useCallback(async () => {
if (hasMore && !loading) {
setLoading(true);
setError(null);
try {
await processPostUseCase.loadMorePosts(listKey);
await postSyncService.loadMorePosts(listKey);
} catch (err) {
const errorMsg = err instanceof Error ? err.message : '加载更多失败';
setError(errorMsg);
@@ -454,16 +272,8 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
}
}, [listKey, hasMore, loading]);
/**
* 手动刷新批量处理队列
*/
const flush = useCallback(() => {
batcherRef.current?.flush();
}, []);
const flush = useCallback(() => { batcherRef.current?.flush(); }, []);
/**
* 重置方法
*/
const reset = useCallback(() => {
calculatorRef.current?.reset();
batcherRef.current?.clearPending();
@@ -473,25 +283,14 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
setError(null);
setHasMore(true);
previousPostsRef.current = [];
setDiffUpdates({
addedCount: 0,
updatedCount: 0,
deletedCount: 0,
lastUpdateTime: 0,
});
setDiffUpdates({ addedCount: 0, updatedCount: 0, deletedCount: 0, lastUpdateTime: 0 });
}, []);
/**
* 强制更新方法
*/
const forceUpdate = useCallback((newPosts: T[]) => {
setPosts(newPosts);
previousPostsRef.current = newPosts;
}, []);
/**
* 获取统计信息
*/
const getDiffStats = useCallback(() => {
const batcherStats = batcherRef.current?.getStats();
return {
@@ -501,20 +300,14 @@ export function useDifferentialPosts<T extends PostIdentifier = Post>(
};
}, []);
// ==================== 定期更新待处理数量 ====================
useEffect(() => {
if (!enableBatching || !batcherRef.current) return;
const interval = setInterval(() => {
setPendingUpdateCount(batcherRef.current?.getPendingCount() ?? 0);
}, 50);
return () => clearInterval(interval);
}, [enableBatching]);
// ==================== 返回值 ====================
return {
posts,
loading,

View File

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

View File

@@ -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: '确定',

View File

@@ -20,6 +20,7 @@ import { spacing, fontSizes, borderRadius, useAppColors, type AppColors } from '
import { Post, User } from '../../types';
import { useUserStore } from '../../stores';
import { postService, authService } from '../../services';
import { postSyncService } from '../../services/post/PostSyncService';
import { PostCard, TabBar, SearchBar } from '../../components/business';
import type { PostCardAction } from '../../components/business/PostCard';
import { Avatar, EmptyState, Text, ResponsiveGrid, Loading } from '../../components/common';
@@ -185,19 +186,19 @@ export const SearchScreen: React.FC<SearchScreenProps> = ({ onBack }) => {
}
break;
case 'like':
postService.likePost(post.id);
postSyncService.likePost(post.id);
break;
case 'unlike':
postService.unlikePost(post.id);
postSyncService.unlikePost(post.id);
break;
case 'comment':
handlePostPress(post.id, true);
break;
case 'bookmark':
postService.favoritePost(post.id);
postSyncService.favoritePost(post.id);
break;
case 'unbookmark':
postService.unfavoritePost(post.id);
postSyncService.unfavoritePost(post.id);
break;
case 'share':
// 搜索页暂不处理分享

View File

@@ -862,11 +862,9 @@ function createSegmentStyles(colors: AppColors, fontSize: number = 16) {
// 容器
segmentsContainer: {
flexDirection: 'column',
width: '100%',
},
segmentsColumn: {
flexDirection: 'column',
width: '100%',
},
inlineChunkBase: {
fontSize,

View File

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

View File

@@ -0,0 +1,346 @@
import { postRepository } from '../../data/repositories/PostRepository';
import type { Post } from '../../core/entities/Post';
import type {
GetPostsParams,
CreatePostData,
UpdatePostData,
PostsResult,
SearchPostsParams,
} from '../../data/repositories/interfaces/IPostRepository';
import { PaginationStateManager } from '../../infrastructure/pagination/PaginationStateManager';
import type { PaginationState } from '../../infrastructure/pagination/types';
import { usePostListStore, type PostsState } from '../../stores/post/postListStore';
import { mergePostListWithFastPath, mergeRefreshWindow } from './postMerge';
export type { PostsState } from '../../stores/post/postListStore';
export interface PostDetailState {
post: Post | null;
isLoading: boolean;
error: string | null;
}
const DEFAULT_PAGE_SIZE = 20;
const MERGE_PERF_WARN_THRESHOLD_MS = 12;
class PostSyncService {
private static instance: PostSyncService;
private paginationManager: PaginationStateManager;
private currentUserId: string | null = null;
private constructor() {
this.paginationManager = new PaginationStateManager();
}
static getInstance(): PostSyncService {
if (!PostSyncService.instance) {
PostSyncService.instance = new PostSyncService();
}
return PostSyncService.instance;
}
initialize(currentUserId: string | null = null): void {
this.currentUserId = currentUserId;
}
destroy(): void {
usePostListStore.getState().clearAllStates();
this.paginationManager.clear();
this.currentUserId = null;
}
getPostsState(key: string = 'default'): PostsState {
return usePostListStore.getState().getPostsState(key);
}
getPostDetailState(postId: string): PostDetailState {
return usePostListStore.getState().getPostDetailState(postId);
}
applyShareCountUpdate(postId: string, sharesCount: number): void {
usePostListStore.getState().applyShareCountUpdate(postId, sharesCount);
}
async fetchPosts(params?: GetPostsParams, key: string = 'default'): Promise<PostsResult> {
const store = usePostListStore.getState();
store.updatePostsState(key, { isLoading: true, error: null });
try {
const queryParams: GetPostsParams = {
pageSize: params?.pageSize || DEFAULT_PAGE_SIZE,
...params,
cursor: params?.cursor !== undefined ? params.cursor : '',
};
const result = await postRepository.getPosts(queryParams);
const isCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
const currentState = usePostListStore.getState().getPostsState(key);
const incomingPosts = Array.isArray(result.posts) ? result.posts : [];
const mergeStart = Date.now();
const mergedPosts = mergeRefreshWindow(currentState.posts, incomingPosts);
const mergeCost = Date.now() - mergeStart;
if (mergeCost >= MERGE_PERF_WARN_THRESHOLD_MS) {
console.log('[PostPerf] fetchPosts merge cost', { key, costMs: mergeCost, prev: currentState.posts.length, next: incomingPosts.length });
}
usePostListStore.getState().updatePostsState(key, {
posts: mergedPosts,
hasMore: result.hasMore,
cursor: isCursorMode ? result.nextCursor : null,
currentPage: isCursorMode ? undefined : 1,
isLoading: false,
lastLoadTime: Date.now(),
lastParams: params,
});
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '获取帖子列表失败';
usePostListStore.getState().updatePostsState(key, { isLoading: false, error: errorMessage });
throw error;
}
}
async refreshPosts(key: string = 'default'): Promise<PostsResult> {
const state = usePostListStore.getState().getPostsState(key);
usePostListStore.getState().updatePostsState(key, { isRefreshing: true, error: null });
try {
const result = await postRepository.getPosts({
pageSize: DEFAULT_PAGE_SIZE,
...state.lastParams,
cursor: state.lastParams?.cursor !== undefined ? state.lastParams.cursor : '',
});
const isCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
const incomingPosts = Array.isArray(result.posts) ? result.posts : [];
const mergeStart = Date.now();
const mergedPosts = mergeRefreshWindow(state.posts, incomingPosts);
const mergeCost = Date.now() - mergeStart;
if (mergeCost >= MERGE_PERF_WARN_THRESHOLD_MS) {
console.log('[PostPerf] refreshPosts merge cost', { key, costMs: mergeCost, prev: state.posts.length, next: incomingPosts.length });
}
usePostListStore.getState().updatePostsState(key, {
posts: mergedPosts,
hasMore: result.hasMore,
cursor: isCursorMode ? result.nextCursor : null,
currentPage: isCursorMode ? undefined : 1,
isRefreshing: false,
lastLoadTime: Date.now(),
});
this.paginationManager.reset(key);
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '刷新帖子列表失败';
usePostListStore.getState().updatePostsState(key, { isRefreshing: false, error: errorMessage });
throw error;
}
}
async loadMorePosts(key: string = 'default'): Promise<PostsResult> {
const state = usePostListStore.getState().getPostsState(key);
if (!state.hasMore) return { posts: [], hasMore: false };
if (state.isLoading) return { posts: [], hasMore: state.hasMore };
usePostListStore.getState().updatePostsState(key, { isLoading: true, error: null });
try {
const useCursorMode = state.cursor !== null && state.cursor !== undefined;
const base = { ...(state.lastParams ?? {}) } as GetPostsParams;
let queryParams: GetPostsParams;
if (useCursorMode) {
delete base.page;
queryParams = { ...base, pageSize: DEFAULT_PAGE_SIZE, cursor: state.cursor! };
} else {
const nextPage = (state.currentPage || 1) + 1;
delete base.cursor;
queryParams = { ...base, pageSize: DEFAULT_PAGE_SIZE, page: nextPage };
}
const result = await postRepository.getPosts(queryParams);
const incomingPosts = Array.isArray(result.posts) ? result.posts : [];
const mergeStart = Date.now();
const newPosts = mergePostListWithFastPath(state.posts, incomingPosts);
const mergeCost = Date.now() - mergeStart;
if (mergeCost >= MERGE_PERF_WARN_THRESHOLD_MS) {
console.log('[PostPerf] loadMore merge cost', { key, costMs: mergeCost, prev: state.posts.length, incoming: incomingPosts.length, merged: newPosts.length });
}
const responseIsCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
usePostListStore.getState().updatePostsState(key, {
posts: newPosts,
hasMore: result.hasMore,
cursor: responseIsCursorMode ? result.nextCursor : null,
currentPage: useCursorMode ? state.currentPage : ((state.currentPage || 1) + 1),
isLoading: false,
lastLoadTime: Date.now(),
});
return { posts: incomingPosts, hasMore: result.hasMore, nextCursor: result.nextCursor };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '加载更多帖子失败';
console.error('[PostSyncService] Load more error:', errorMessage);
usePostListStore.getState().updatePostsState(key, { isLoading: false, error: errorMessage });
throw error;
}
}
async fetchPostById(id: string): Promise<Post | null> {
usePostListStore.getState().updatePostDetailState(id, { isLoading: true, error: null });
try {
const post = await postRepository.getPostById(id);
usePostListStore.getState().updatePostDetailState(id, { post, isLoading: false });
return post;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '获取帖子详情失败';
usePostListStore.getState().updatePostDetailState(id, { isLoading: false, error: errorMessage });
throw error;
}
}
async createPost(data: CreatePostData): Promise<Post> {
const post = await postRepository.createPost(data);
const store = usePostListStore.getState();
const newMap = new Map(store.postsStateMap);
for (const [key, postsState] of newMap) {
newMap.set(key, { ...postsState, posts: [post, ...postsState.posts] });
}
usePostListStore.setState({ postsStateMap: newMap });
return post;
}
async updatePost(id: string, data: UpdatePostData): Promise<Post> {
const post = await postRepository.updatePost(id, data);
usePostListStore.getState().updatePostDetailState(id, { post });
usePostListStore.getState().updatePostInLists(id, post);
return post;
}
async deletePost(id: string): Promise<void> {
await postRepository.deletePost(id);
usePostListStore.getState().clearPostDetailState(id);
usePostListStore.getState().removePostFromLists(id);
}
async likePost(id: string): Promise<Post> {
const post = await postRepository.likePost(id);
usePostListStore.getState().updatePostDetailState(id, { post });
usePostListStore.getState().updatePostInLists(id, post);
return post;
}
async unlikePost(id: string): Promise<Post> {
const post = await postRepository.unlikePost(id);
usePostListStore.getState().updatePostDetailState(id, { post });
usePostListStore.getState().updatePostInLists(id, post);
return post;
}
async favoritePost(id: string): Promise<Post> {
const post = await postRepository.favoritePost(id);
usePostListStore.getState().updatePostDetailState(id, { post });
usePostListStore.getState().updatePostInLists(id, post);
return post;
}
async unfavoritePost(id: string): Promise<Post> {
const post = await postRepository.unfavoritePost(id);
usePostListStore.getState().updatePostDetailState(id, { post });
usePostListStore.getState().updatePostInLists(id, post);
return post;
}
async searchPosts(params: SearchPostsParams, key: string = 'search'): Promise<PostsResult> {
usePostListStore.getState().updatePostsState(key, { isLoading: true, error: null });
try {
const result = await postRepository.searchPosts(params);
usePostListStore.getState().updatePostsState(key, {
posts: result.posts,
hasMore: result.hasMore,
cursor: result.nextCursor || null,
isLoading: false,
lastLoadTime: Date.now(),
});
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '搜索帖子失败';
usePostListStore.getState().updatePostsState(key, { isLoading: false, error: errorMessage });
throw error;
}
}
async fetchPostsByUser(userId: string, params?: GetPostsParams, key?: string): Promise<PostsResult> {
const listKey = key || `user_${userId}`;
usePostListStore.getState().updatePostsState(listKey, { isLoading: true, error: null });
try {
const result = await postRepository.getPostsByUser(userId, params);
usePostListStore.getState().updatePostsState(listKey, {
posts: result.posts,
hasMore: result.hasMore,
cursor: result.nextCursor || null,
isLoading: false,
lastLoadTime: Date.now(),
});
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '获取用户帖子失败';
usePostListStore.getState().updatePostsState(listKey, { isLoading: false, error: errorMessage });
throw error;
}
}
clearPostsState(key: string = 'default'): void {
usePostListStore.getState().clearPostsState(key);
this.paginationManager.reset(key);
}
clearPostDetailState(postId: string): void {
usePostListStore.getState().clearPostDetailState(postId);
}
clearAllStates(): void {
usePostListStore.getState().clearAllStates();
this.paginationManager.clear();
}
getCurrentUserId(): string | null {
return this.currentUserId;
}
setCurrentUserId(userId: string | null): void {
this.currentUserId = userId;
}
getPaginationState(key: string): PaginationState {
return this.paginationManager.getState(key);
}
resetPagination(key: string): void {
this.paginationManager.reset(key);
}
isLoading(key: string = 'default'): boolean {
return usePostListStore.getState().getPostsState(key).isLoading;
}
hasError(key: string = 'default'): boolean {
return usePostListStore.getState().getPostsState(key).error !== null;
}
getError(key: string = 'default'): string | null {
return usePostListStore.getState().getPostsState(key).error;
}
}
export const postSyncService = PostSyncService.getInstance();
export { PostSyncService };
export default postSyncService;

View File

@@ -0,0 +1,66 @@
import type { Post } from '../../core/entities/Post';
export function getEntityId(item: unknown): string | null {
if (!item || typeof item !== 'object') return null;
const maybeId = (item as { id?: unknown }).id;
if (typeof maybeId === 'string' || typeof maybeId === 'number') return String(maybeId);
return null;
}
export function mergePostListWithFastPath(
previousInput: Post[] | null | undefined,
incomingInput: Post[] | null | undefined
): Post[] {
const previous = Array.isArray(previousInput) ? previousInput : [];
const incoming = Array.isArray(incomingInput) ? incomingInput : [];
if (incoming.length === 0) return previous;
if (previous.length === 0) return incoming;
const lastPrevId = getEntityId(previous[previous.length - 1]);
const firstIncomingId = getEntityId(incoming[0]);
if (lastPrevId && firstIncomingId && lastPrevId !== firstIncomingId) {
const prevIdSet = new Set(
previous.map(item => getEntityId(item)).filter(Boolean) as string[]
);
if (!prevIdSet.has(firstIncomingId)) return [...previous, ...incoming];
}
const merged = [...previous];
const indexById = new Map<string, number>();
for (let i = 0; i < merged.length; i += 1) {
const id = getEntityId(merged[i]);
if (id) indexById.set(id, i);
}
for (const post of incoming) {
const id = getEntityId(post);
if (!id) { merged.push(post); continue; }
const existingIndex = indexById.get(id);
if (existingIndex === undefined) {
indexById.set(id, merged.length);
merged.push(post);
} else {
merged[existingIndex] = post;
}
}
return merged;
}
export function mergeRefreshWindow(
previousInput: Post[] | null | undefined,
latestInput: Post[] | null | undefined
): Post[] {
const previous = Array.isArray(previousInput) ? previousInput : [];
const latest = Array.isArray(latestInput) ? latestInput : [];
if (latest.length === 0) return previous;
if (previous.length === 0) return latest;
const latestIdSet = new Set(
latest.map(item => getEntityId(item)).filter(Boolean) as string[]
);
const remaining = previous.filter(item => {
const id = getEntityId(item);
return !id || !latestIdSet.has(id);
});
return [...latest, ...remaining];
}

View File

@@ -1,16 +1,11 @@
/**
* 帖子服务
* 处理帖子的增删改查、点赞、收藏等功能
*
* @deprecated 此服务已弃用,请使用 ProcessPostUseCase 代替
* 互动操作(点赞、收藏)已委托给 processPostUseCase
* 帖子服务 - API 层薄封装
* 互动操作(点赞、收藏)请使用 postSyncService
*/
import { api, PaginatedData } from './api';
import { Post, CreatePostInput } from '../types';
import { CursorPaginationRequest, CursorPaginationResponse } from '../types/dto';
import { processPostUseCase } from '../core/usecases/ProcessPostUseCase';
import type { Post as PostEntity } from '../core/entities/Post';
// 帖子列表响应
interface PostListResponse {
@@ -129,55 +124,6 @@ class PostService {
}
}
// 点赞帖子
// @deprecated 请使用 processPostUseCase.likePost() 代替
async likePost(postId: string): Promise<Post | null> {
try {
const post = await processPostUseCase.likePost(postId);
// 转换为旧版 Post 类型以保持向后兼容
return post as unknown as Post;
} catch (error) {
console.error('[postService] likePost error:', error);
return null;
}
}
// 取消点赞帖子
// @deprecated 请使用 processPostUseCase.unlikePost() 代替
async unlikePost(postId: string): Promise<Post | null> {
try {
const post = await processPostUseCase.unlikePost(postId);
return post as unknown as Post;
} catch (error) {
console.error('[postService] unlikePost error:', error);
return null;
}
}
// 收藏帖子
// @deprecated 请使用 processPostUseCase.favoritePost() 代替
async favoritePost(postId: string): Promise<Post | null> {
try {
const post = await processPostUseCase.favoritePost(postId);
return post as unknown as Post;
} catch (error) {
console.error('[postService] favoritePost error:', error);
return null;
}
}
// 取消收藏帖子
// @deprecated 请使用 processPostUseCase.unfavoritePost() 代替
async unfavoritePost(postId: string): Promise<Post | null> {
try {
const post = await processPostUseCase.unfavoritePost(postId);
return post as unknown as Post;
} catch (error) {
console.error('[postService] unfavoritePost error:', error);
return null;
}
}
// 获取用户帖子列表
async getUserPosts(
userId: string,

View File

@@ -28,9 +28,6 @@ export {
// MessageManager 新架构导出(已替换 conversationStore
export { messageManager, MessageManager, useMessageStore } from './message';
export type {
MessageEvent,
MessageEventType,
MessageSubscriber,
MessageManagerConversationListDeps,
} from './message';
export {

View File

@@ -29,9 +29,6 @@ import { useMessageStore, normalizeConversationId } from './store';
// 重新导出类型,保持兼容性
export type {
MessageEventType,
MessageEvent,
MessageSubscriber,
MessageManagerState,
ReadStateRecord,
MessageManagerConversationListDeps,

View File

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

View File

@@ -1,193 +1,48 @@
/**
* 消息管理类型定义模块
* 包含所有与消息管理相关的类型、接口和类型别名
*/
import type { ConversationResponse, MessageResponse, UserDTO } from '../../types/dto';
import type { IConversationListPagedSource } from '../conversationListSources';
// ==================== 事件类型 ====================
/**
* 消息事件类型枚举
* 定义了所有可能的消息事件类型
*/
export type MessageEventType =
| 'conversations_updated'
| 'conversations_loading'
| 'messages_updated'
| 'unread_count_updated'
| 'connection_changed'
| 'message_sent'
| 'message_read'
| 'message_received'
| 'message_recalled'
| 'typing_status'
| 'group_notice'
| 'error';
/**
* 消息事件接口
* 所有订阅者接收的事件对象结构
*/
export interface MessageEvent {
type: MessageEventType;
payload: any;
timestamp: number;
}
/**
* 消息订阅者类型
*/
export type MessageSubscriber = (event: MessageEvent) => void;
// ==================== 状态接口 ====================
/**
* 消息管理器内部状态接口
* 定义了所有需要管理的状态字段
*/
export interface MessageManagerState {
// 会话相关
conversations: Map<string, ConversationResponse>;
conversationList: ConversationResponse[];
// 消息相关 - 按会话ID存储
messagesMap: Map<string, MessageResponse[]>;
// 未读数
totalUnreadCount: number;
systemUnreadCount: number;
// 连接状态
isWSConnected: boolean;
// 当前活动会话ID用户正在查看的会话
currentConversationId: string | null;
// 加载状态
isLoadingConversations: boolean;
loadingMessagesSet: Set<string>; // 正在加载消息的会话ID集合
// 初始化状态
loadingMessagesSet: Set<string>;
isInitialized: boolean;
// 订阅者
subscribers: Set<MessageSubscriber>;
// 输入状态 - 按群组ID存储正在输入的用户ID列表
typingUsersMap: Map<string, string[]>;
// 当前用户的禁言状态 - 按群组ID存储
mutedStatusMap: Map<string, boolean>;
}
// ==================== 已读状态相关 ====================
/**
* 已读状态记录接口
* 用于跟踪已读操作的状态和保护机制
*/
export interface ReadStateRecord {
/** 标记已读的时间戳 */
timestamp: number;
/** 状态版本号,用于防止旧数据覆盖新数据 */
version: number;
/** 已上报的已读序号(用于去重) */
lastReadSeq: number;
/** 清除保护的定时器ID */
clearTimer?: ReturnType<typeof setTimeout>;
}
// ==================== 依赖注入接口 ====================
/**
* 可注入会话列表源,便于测试或替换实现
*/
export interface MessageManagerConversationListDeps {
remoteConversationListSource?: IConversationListPagedSource;
localConversationListSource?: IConversationListPagedSource;
/**
* 未传 remoteConversationListSource 时:默认 `cursor`/conversations/cursor
* `offset` 为常规页码(/conversations?page=&page_size=)。
*/
remoteListKind?: 'cursor' | 'offset';
/** 与 remoteListKind 搭配,默认与 CONVERSATION_LIST_PAGE_SIZE 一致 */
remoteListPageSize?: number;
}
// ==================== 消息处理选项 ====================
/**
* 处理新消息的选项
*/
export interface HandleNewMessageOptions {
/** 是否抑制未读数增加 */
suppressUnreadIncrement?: boolean;
/** 是否抑制震动 */
suppressVibration?: boolean;
/** 是否抑制会话更新 */
suppressConversationUpdates?: boolean;
/** 是否抑制自动标记已读 */
suppressAutoMarkAsRead?: boolean;
}
// ==================== 服务接口 ====================
/**
* 状态管理器接口
* 定义状态管理器需要实现的方法
*/
export interface IMessageStateManager {
// 获取状态
getState(): MessageManagerState;
getConversations(): ConversationResponse[];
getConversation(conversationId: string): ConversationResponse | null;
getMessages(conversationId: string): MessageResponse[];
getUnreadCount(): { total: number; system: number };
isConnected(): boolean;
isLoading(): boolean;
isLoadingMessages(conversationId: string): boolean;
getTypingUsers(groupId: string): string[];
isMuted(groupId: string): boolean;
getActiveConversation(): string | null;
// 设置状态
setConversations(conversations: Map<string, ConversationResponse>): void;
updateConversation(conversation: ConversationResponse): void;
removeConversation(conversationId: string): void;
setMessages(conversationId: string, messages: MessageResponse[]): void;
addMessage(conversationId: string, message: MessageResponse): void;
updateMessage(conversationId: string, messageId: string, updates: Partial<MessageResponse>): void;
setUnreadCount(total: number, system: number): void;
setSSEConnected(connected: boolean): void;
setCurrentConversation(conversationId: string | null): void;
setLoading(loading: boolean): void;
setLoadingMessages(conversationId: string, loading: boolean): void;
setTypingUsers(groupId: string, users: string[]): void;
setMutedStatus(groupId: string, isMuted: boolean): void;
setInitialized(initialized: boolean): void;
// 订阅机制
subscribe(subscriber: MessageSubscriber): () => void;
notifySubscribers(event: MessageEvent): void;
// 重置
reset(): void;
}
/**
* 消息去重服务接口
*/
export interface IMessageDeduplication {
isMessageProcessed(messageId: string): boolean;
markMessageAsProcessed(messageId: string): void;
cleanupProcessedMessageIds(): void;
}
/**
* 用户缓存服务接口
*/
export interface IUserCacheService {
getSenderInfo(userId: string): Promise<UserDTO | null>;
enrichMessagesWithSenderInfo(
@@ -197,9 +52,6 @@ export interface IUserCacheService {
): void;
}
/**
* 已读回执管理器接口
*/
export interface IReadReceiptManager {
markAsRead(conversationId: string, seq: number): Promise<void>;
markAllAsRead(): Promise<void>;
@@ -210,9 +62,6 @@ export interface IReadReceiptManager {
getPendingReadMap(): Map<string, ReadStateRecord>;
}
/**
* 会话操作接口
*/
export interface IConversationOperations {
createConversation(userId: string): Promise<ConversationResponse | null>;
updateConversation(conversationId: string, updates: Partial<ConversationResponse>): void;
@@ -225,9 +74,6 @@ export interface IConversationOperations {
decrementSystemUnreadCount(count?: number): void;
}
/**
* 消息发送服务接口
*/
export interface IMessageSendService {
sendMessage(
conversationId: string,
@@ -236,9 +82,6 @@ export interface IMessageSendService {
): Promise<MessageResponse | null>;
}
/**
* 消息同步服务接口
*/
export interface IMessageSyncService {
fetchConversations(forceRefresh?: boolean, source?: string): Promise<void>;
loadMoreConversations(): Promise<void>;
@@ -249,9 +92,6 @@ export interface IMessageSyncService {
canLoadMoreConversations(): boolean;
}
/**
* WebSocket 消息处理器接口
*/
export interface IWSMessageHandler {
connect(): void;
disconnect(): void;

View File

@@ -50,18 +50,10 @@ export {
// 重导出类型
export type {
// 事件类型
MessageEventType,
MessageEvent,
MessageSubscriber,
// 状态类型
MessageManagerState,
ReadStateRecord,
MessageManagerConversationListDeps,
HandleNewMessageOptions,
// 服务接口类型
IMessageDeduplication,
IUserCacheService,
IReadReceiptManager,

View File

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

View File

@@ -0,0 +1,185 @@
import { create } from 'zustand';
import type { Post } from '../../core/entities/Post';
export interface PostsState {
posts: Post[];
isLoading: boolean;
isRefreshing: boolean;
hasMore: boolean;
error: string | null;
cursor: string | null;
currentPage: number;
lastLoadTime: number | null;
lastParams?: any;
}
export interface PostDetailState {
post: Post | null;
isLoading: boolean;
error: string | null;
}
export interface PostListStoreState {
postsStateMap: Map<string, PostsState>;
postDetailStateMap: Map<string, PostDetailState>;
}
export interface PostListStoreActions {
getPostsState: (key: string) => PostsState;
getPostDetailState: (postId: string) => PostDetailState;
updatePostsState: (key: string, partial: Partial<PostsState>) => void;
updatePostDetailState: (postId: string, partial: Partial<PostDetailState>) => void;
updatePostInLists: (postId: string, updatedPost: Post) => void;
removePostFromLists: (postId: string) => void;
applyShareCountUpdate: (postId: string, sharesCount: number) => void;
clearPostsState: (key: string) => void;
clearPostDetailState: (postId: string) => void;
clearAllStates: () => void;
reset: () => void;
}
const DEFAULT_POSTS_STATE: PostsState = {
posts: [],
isLoading: false,
isRefreshing: false,
hasMore: true,
error: null,
cursor: null,
currentPage: 1,
lastLoadTime: null,
};
const DEFAULT_POST_DETAIL_STATE: PostDetailState = {
post: null,
isLoading: false,
error: null,
};
export type PostListStore = PostListStoreState & PostListStoreActions;
export const usePostListStore = create<PostListStore>((set, get) => ({
postsStateMap: new Map(),
postDetailStateMap: new Map(),
getPostsState: (key: string) => {
return get().postsStateMap.get(key) ?? { ...DEFAULT_POSTS_STATE };
},
getPostDetailState: (postId: string) => {
return get().postDetailStateMap.get(postId) ?? { ...DEFAULT_POST_DETAIL_STATE };
},
updatePostsState: (key: string, partial: Partial<PostsState>) => {
set(state => {
const current = state.postsStateMap.get(key) ?? { ...DEFAULT_POSTS_STATE };
const newMap = new Map(state.postsStateMap);
newMap.set(key, { ...current, ...partial });
return { postsStateMap: newMap };
});
},
updatePostDetailState: (postId: string, partial: Partial<PostDetailState>) => {
set(state => {
const current = state.postDetailStateMap.get(postId) ?? { ...DEFAULT_POST_DETAIL_STATE };
const newMap = new Map(state.postDetailStateMap);
newMap.set(postId, { ...current, ...partial });
return { postDetailStateMap: newMap };
});
},
updatePostInLists: (postId: string, updatedPost: Post) => {
set(state => {
const newMap = new Map(state.postsStateMap);
let changed = false;
for (const [key, postsState] of newMap) {
const index = postsState.posts.findIndex(p => p.id === postId);
if (index !== -1) {
const newPosts = [...postsState.posts];
newPosts[index] = updatedPost;
newMap.set(key, { ...postsState, posts: newPosts });
changed = true;
}
}
return changed ? { postsStateMap: newMap } : state;
});
},
removePostFromLists: (postId: string) => {
set(state => {
const newMap = new Map(state.postsStateMap);
let changed = false;
for (const [key, postsState] of newMap) {
const filtered = postsState.posts.filter(p => p.id !== postId);
if (filtered.length !== postsState.posts.length) {
newMap.set(key, { ...postsState, posts: filtered });
changed = true;
}
}
return changed ? { postsStateMap: newMap } : state;
});
},
applyShareCountUpdate: (postId: string, sharesCount: number) => {
set(state => {
const newPostsMap = new Map(state.postsStateMap);
let postsChanged = false;
for (const [key, postsState] of newPostsMap) {
const index = postsState.posts.findIndex(p => p.id === postId);
if (index !== -1) {
const newPosts = [...postsState.posts];
const cur = newPosts[index];
newPosts[index] = { ...cur, sharesCount, shares_count: sharesCount };
newPostsMap.set(key, { ...postsState, posts: newPosts });
postsChanged = true;
}
}
const newDetailMap = new Map(state.postDetailStateMap);
let detailChanged = false;
const detail = newDetailMap.get(postId);
if (detail?.post) {
newDetailMap.set(postId, {
...detail,
post: { ...detail.post, sharesCount, shares_count: sharesCount },
});
detailChanged = true;
}
if (!postsChanged && !detailChanged) return state;
return {
...(postsChanged ? { postsStateMap: newPostsMap } : {}),
...(detailChanged ? { postDetailStateMap: newDetailMap } : {}),
};
});
},
clearPostsState: (key: string) => {
set(state => {
const newMap = new Map(state.postsStateMap);
newMap.delete(key);
return { postsStateMap: newMap };
});
},
clearPostDetailState: (postId: string) => {
set(state => {
const newMap = new Map(state.postDetailStateMap);
newMap.delete(postId);
return { postDetailStateMap: newMap };
});
},
clearAllStates: () => {
set({
postsStateMap: new Map(),
postDetailStateMap: new Map(),
});
},
reset: () => {
set({
postsStateMap: new Map(),
postDetailStateMap: new Map(),
});
},
}));

View File

@@ -1,66 +1,40 @@
/**
* 帖子状态 Zustand Store
*/
import { create } from 'zustand';
import type { Post } from '../../types';
import { CacheEntry, createCacheEntry, isCacheExpired, DEFAULT_TTL } from '../utils/cache';
// ==================== 状态接口 ====================
export interface PostManagerState {
// 帖子列表缓存 (key: list:type:page:pageSize)
listsMap: Map<string, CacheEntry<Post[]>>;
// 帖子详情缓存
detailsMap: Map<string, CacheEntry<Post | null>>;
// 加载状态
loadingListKeys: Set<string>;
loadingDetailIds: Set<string>;
// 待处理请求(去重)
pendingRequests: Map<string, Promise<any>>;
}
export interface PostManagerActions {
// 获取状态
getPostsList: (type: string, page: number, pageSize: number) => Post[] | null;
getPostDetail: (postId: string) => Post | null;
isListExpired: (type: string, page: number, pageSize: number) => boolean;
isDetailExpired: (postId: string) => boolean;
isLoadingList: (type: string, page: number, pageSize: number) => boolean;
isLoadingDetail: (postId: string) => boolean;
// 设置状态
setPostsList: (type: string, page: number, pageSize: number, posts: Post[]) => void;
setPostDetail: (postId: string, post: Post | null) => void;
setLoadingList: (type: string, page: number, pageSize: number, loading: boolean) => void;
setLoadingDetail: (postId: string, loading: boolean) => void;
// 缓存管理
invalidateList: (type?: string, page?: number, pageSize?: number) => void;
invalidateDetail: (postId: string) => void;
invalidateAll: () => void;
// 请求去重
getPendingRequest: <T>(key: string) => Promise<T> | undefined;
setPendingRequest: <T>(key: string, request: Promise<T>) => void;
deletePendingRequest: (key: string) => void;
clearPendingRequests: () => void;
// 重置
reset: () => void;
}
// ==================== 辅助函数 ====================
function buildListKey(type: string, page: number, pageSize: number): string {
return `list:${type}:${page}:${pageSize}`;
}
// ==================== 初始状态 ====================
const initialState: PostManagerState = {
listsMap: new Map(),
detailsMap: new Map(),
@@ -69,16 +43,11 @@ const initialState: PostManagerState = {
pendingRequests: new Map(),
};
// ==================== Store 创建 ====================
export type PostManagerStore = PostManagerState & PostManagerActions;
export const usePostManagerStore = create<PostManagerStore>((set, get) => ({
// ==================== 初始状态 ====================
...initialState,
// ==================== 获取状态 ====================
getPostsList: (type: string, page: number, pageSize: number) => {
const key = buildListKey(type, page, pageSize);
const entry = get().listsMap.get(key);
@@ -116,8 +85,6 @@ export const usePostManagerStore = create<PostManagerStore>((set, get) => ({
return get().loadingDetailIds.has(postId);
},
// ==================== 设置状态 ====================
setPostsList: (type: string, page: number, pageSize: number, posts: Post[]) => {
const key = buildListKey(type, page, pageSize);
set(state => {
@@ -160,8 +127,6 @@ export const usePostManagerStore = create<PostManagerStore>((set, get) => ({
});
},
// ==================== 缓存管理 ====================
invalidateList: (type?: string, page?: number, pageSize?: number) => {
if (type !== undefined && page !== undefined && pageSize !== undefined) {
const key = buildListKey(type, page, pageSize);
@@ -191,8 +156,6 @@ export const usePostManagerStore = create<PostManagerStore>((set, get) => ({
});
},
// ==================== 请求去重 ====================
getPendingRequest: <T>(key: string) => {
return get().pendingRequests.get(key) as Promise<T> | undefined;
},
@@ -217,8 +180,6 @@ export const usePostManagerStore = create<PostManagerStore>((set, get) => ({
set({ pendingRequests: new Map() });
},
// ==================== 重置 ====================
reset: () => {
set({
...initialState,