dev #5
394
plans/architecture-comparison-report.md
Normal file
394
plans/architecture-comparison-report.md
Normal file
@@ -0,0 +1,394 @@
|
||||
# Messages模块与PostService/Manager架构对比分析报告
|
||||
|
||||
## 一、Messages模块架构分析
|
||||
|
||||
### 1.1 架构层次结构
|
||||
|
||||
Messages模块采用了**清晰的分层架构**,各层职责明确:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 表现层 (Screens) │
|
||||
│ ChatScreen, MessageListScreen, NotificationsScreen │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Hooks层 │
|
||||
│ useDifferentialMessages, useChatScreen │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 用例层 (UseCases) │
|
||||
│ ProcessMessageUseCase │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 领域层 (Entities) │
|
||||
│ Message, Conversation, GroupNotice │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 仓库层 (Repositories) │
|
||||
│ MessageRepository, IMessageRepository │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 数据源层 (DataSources) │
|
||||
│ SSEClient, LocalDataSource, ApiDataSource │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 1.2 核心组件详解
|
||||
|
||||
#### 1.2.1 领域实体层 (Core/Entities)
|
||||
- **Message.ts**: 定义消息、会话、群通知等核心领域模型
|
||||
- 包含工厂函数:`createMessage`, `createConversation`
|
||||
- 包含业务逻辑函数:`isMessageFromCurrentUser`, `shouldIncrementUnread`, `buildTextContent`
|
||||
|
||||
#### 1.2.2 用例层 (Core/UseCases)
|
||||
- **ProcessMessageUseCase**: 核心业务逻辑编排器
|
||||
- 订阅SSE事件(chat, group_message, read, recall, typing, group_notice等)
|
||||
- 消息去重处理(processedMessageIds Set)
|
||||
- 已读状态保护(pendingReadMap + 版本号机制)
|
||||
- 用户信息缓存与去重请求(pendingUserRequests Map)
|
||||
- 事件发布订阅模式(subscribers Set)
|
||||
|
||||
#### 1.2.3 仓库层 (Data/Repositories)
|
||||
- **IMessageRepository**: 定义数据访问接口
|
||||
- 消息操作:getMessages, saveMessage, updateMessageStatus, markAsRead
|
||||
- 会话操作:getConversations, saveConversation, updateUnreadCount
|
||||
- 同步操作:syncMessages, getServerLastSeq
|
||||
|
||||
- **MessageRepository**: 实现接口
|
||||
- 封装SQLite数据库操作
|
||||
- 消息与CachedMessage的转换
|
||||
|
||||
#### 1.2.4 映射器层 (Data/Mappers)
|
||||
- **MessageMapper**: 数据转换
|
||||
- `fromApiResponse`: API响应 → 应用模型
|
||||
- `fromDbRecord`: 数据库记录 → 应用模型
|
||||
- `toDbRecord`: 应用模型 → 数据库记录
|
||||
- `toApiRequest`: 应用模型 → API请求
|
||||
- 创建消息片段:`createTextSegment`, `createImageSegment`
|
||||
|
||||
#### 1.2.5 差异计算基础设施 (Infrastructure/Diff)
|
||||
- **MessageDiffCalculator**: 消息列表差异计算
|
||||
- 计算added, updated, deleted, moved, unchanged
|
||||
- 变化比例检测(changeRatio > 0.5 时触发重置)
|
||||
- 增量差异计算(基于缓存)
|
||||
|
||||
- **MessageUpdateBatcher**: 批量更新处理器
|
||||
- 16ms批量间隔(约60fps)
|
||||
- 去重和合并更新
|
||||
- 100ms最大等待时间
|
||||
- 统计信息:totalBatches, totalUpdates, averageBatchSize
|
||||
|
||||
- **types.ts**: 完整的类型定义
|
||||
- MessageUpdateType枚举:ADD, UPDATE, DELETE, MOVE, BATCH_*
|
||||
- DiffResult, DiffConfig等接口
|
||||
|
||||
#### 1.2.6 Hook层
|
||||
- **useDifferentialMessages**: 差异更新Hook
|
||||
- 集成DiffCalculator和Batcher
|
||||
- 自动处理批量更新
|
||||
- 提供flush, reset, forceUpdate方法
|
||||
- 支持配置:enableDiff, enableBatching, maxMessageCount, changeRatioThreshold
|
||||
|
||||
### 1.3 数据流
|
||||
|
||||
```
|
||||
SSE事件 → ProcessMessageUseCase → 事件发布 → useChatScreen → useDifferentialMessages
|
||||
↓ ↓
|
||||
MessageRepository 差异计算 + 批量处理
|
||||
↓ ↓
|
||||
SQLite数据库 优化后的消息列表
|
||||
```
|
||||
|
||||
### 1.4 状态管理方式
|
||||
|
||||
- **发布-订阅模式**:ProcessMessageUseCase作为事件中心
|
||||
- **本地SQLite缓存**:消息持久化存储
|
||||
- **内存状态**:通过Hook返回,组件直接消费
|
||||
- **差异更新**:通过MessageDiffCalculator + MessageUpdateBatcher减少重渲染
|
||||
|
||||
---
|
||||
|
||||
## 二、PostService/Manager架构分析
|
||||
|
||||
### 2.1 架构层次结构
|
||||
|
||||
Post模块采用了**扁平化架构**,Service和Manager层职责混合:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 表现层 (Screens) │
|
||||
│ HomeScreen, PostDetailScreen, CreatePostScreen │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Hooks层 │
|
||||
│ useCursorPagination (通用), usePrefetch │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Store层 (Zustand) │
|
||||
│ useUserStore (直接操作状态), postManager (缓存管理) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Service层 │
|
||||
│ postService (API调用 + 状态更新) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 数据源 │
|
||||
│ API (直接调用) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 核心组件详解
|
||||
|
||||
#### 2.2.1 Service层 (Services)
|
||||
- **postService.ts**: 帖子服务
|
||||
- CRUD操作:getPosts, getPost, createPost, updatePost, deletePost
|
||||
- 互动操作:likePost, unlikePost, favoritePost, unfavoritePost
|
||||
- 分页支持:传统分页 + 游标分页
|
||||
- **问题**:直接操作useUserStore(违反分层原则)
|
||||
|
||||
#### 2.2.2 Manager层 (Stores)
|
||||
- **postManager.ts**: 帖子缓存管理器
|
||||
- 继承CacheBus(发布订阅基类)
|
||||
- 内存缓存:listCache, detailCache
|
||||
- 请求去重:pendingRequests Map
|
||||
- TTL管理:LIST_TTL=30s, DETAIL_TTL=60s
|
||||
- 后台刷新机制
|
||||
|
||||
#### 2.2.3 Store层 (Zustand)
|
||||
- **useUserStore**: 用户状态存储
|
||||
- 直接存储posts数组
|
||||
- 帖子操作副作用直接修改状态
|
||||
- 混合了用户数据和帖子数据
|
||||
|
||||
#### 2.2.4 映射器层 (Data/Mappers)
|
||||
- **PostMapper**: 数据转换
|
||||
- `fromApiResponse`: API响应 → 应用模型
|
||||
- `fromApiResponseList`: 批量转换
|
||||
- `toApiRequest`: 应用模型 → API请求
|
||||
- 相比MessageMapper缺少数据库记录转换方法
|
||||
|
||||
### 2.3 数据流
|
||||
|
||||
```
|
||||
HomeScreen → useCursorPagination → postService.getPostsCursor
|
||||
↓
|
||||
useUserStore.setState(posts)
|
||||
↓
|
||||
PostCard组件渲染
|
||||
```
|
||||
|
||||
### 2.4 状态管理方式
|
||||
|
||||
- **Zustand Store**:集中式状态管理
|
||||
- **内存缓存**:postManager提供应用级缓存
|
||||
- **请求去重**:dedupe方法防止重复请求
|
||||
- **直接修改**:postService直接调用useUserStore.setState
|
||||
|
||||
---
|
||||
|
||||
## 三、主要架构差异
|
||||
|
||||
### 3.1 分层复杂度
|
||||
|
||||
| 维度 | Messages模块 | Post模块 |
|
||||
|------|-------------|---------|
|
||||
| 层次深度 | 6层(Entity→UseCase→Repository→Mapper→Hook→Screen) | 4层(Service→Store→Hook→Screen) |
|
||||
| 用例层 | 独立ProcessMessageUseCase | 无 |
|
||||
| 仓库接口 | IMessageRepository抽象接口 | 无 |
|
||||
| 基础设施 | Diff模块(Calculator + Batcher) | 无 |
|
||||
|
||||
### 3.2 数据持久化
|
||||
|
||||
| 维度 | Messages模块 | Post模块 |
|
||||
|------|-------------|---------|
|
||||
| 本地存储 | SQLite数据库 | 无 |
|
||||
| 缓存抽象 | MessageRepository封装 | postManager内存缓存 |
|
||||
| 离线支持 | 完整 | 不支持 |
|
||||
|
||||
### 3.3 实时更新
|
||||
|
||||
| 维度 | Messages模块 | Post模块 |
|
||||
|------|-------------|---------|
|
||||
| 推送机制 | SSE实时推送 | 轮询/下拉刷新 |
|
||||
| 事件订阅 | ProcessMessageUseCase发布订阅 | 无 |
|
||||
| 增量更新 | MessageDiffCalculator | 无 |
|
||||
|
||||
### 3.4 状态管理
|
||||
|
||||
| 维度 | Messages模块 | Post模块 |
|
||||
|------|-------------|---------|
|
||||
| 管理方式 | 发布订阅 + Hook局部状态 | Zustand全局Store |
|
||||
| 状态来源 | useChatScreen, useDifferentialMessages | useUserStore |
|
||||
| 批量更新 | MessageUpdateBatcher | 无 |
|
||||
| 差异检测 | MessageDiffCalculator | 无 |
|
||||
|
||||
### 3.5 依赖方向
|
||||
|
||||
**Messages模块(正确)**:
|
||||
```
|
||||
Hook → UseCase → Repository → DataSource
|
||||
↓
|
||||
Entity(不依赖外部)
|
||||
```
|
||||
|
||||
**Post模块(问题)**:
|
||||
```
|
||||
Service → Store(循环依赖风险)
|
||||
↓
|
||||
API直接调用
|
||||
```
|
||||
|
||||
### 3.6 核心问题总结
|
||||
|
||||
| # | 问题 | Messages | Post |
|
||||
|---|------|---------|------|
|
||||
| 1 | 违反分层原则 | 无 | postService直接操作useUserStore |
|
||||
| 2 | 缺少UseCase层 | ProcessMessageUseCase | 无业务逻辑编排 |
|
||||
| 3 | 缺少Repository抽象 | IMessageRepository | 无数据访问接口 |
|
||||
| 4 | 无差异更新 | useDifferentialMessages | 无 |
|
||||
| 5 | 无批量处理 | MessageUpdateBatcher | 无 |
|
||||
| 6 | 无本地持久化 | SQLite | 无 |
|
||||
| 7 | 无实时推送 | SSE | 无 |
|
||||
|
||||
---
|
||||
|
||||
## 四、对齐建议
|
||||
|
||||
### 4.1 架构重构目标
|
||||
|
||||
将Post模块对齐到Messages模块的架构模式:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 表现层 (Screens) │
|
||||
│ HomeScreen, PostDetailScreen │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Hooks层 │
|
||||
│ useDifferentialPosts (新增) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 用例层 (UseCases) │
|
||||
│ ProcessPostUseCase (新增) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 领域层 (Entities) │
|
||||
│ Post, PostComment (新增) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 仓库层 (Repositories) │
|
||||
│ PostRepository, IPostRepository (新增) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Service层 │
|
||||
│ postService (仅保留API调用,移除状态操作) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.2 具体改造项
|
||||
|
||||
#### 4.2.1 创建Post领域实体
|
||||
```typescript
|
||||
// src/core/entities/Post.ts
|
||||
export interface Post {
|
||||
id: string;
|
||||
authorId: string;
|
||||
title: string;
|
||||
content: string;
|
||||
images: string[];
|
||||
likeCount: number;
|
||||
commentCount: number;
|
||||
// ... 其他字段
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2.2 创建ProcessPostUseCase
|
||||
```typescript
|
||||
// src/core/usecases/ProcessPostUseCase.ts
|
||||
class ProcessPostUseCase {
|
||||
// 帖子增删改查
|
||||
// 点赞/收藏逻辑
|
||||
// 事件发布订阅
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2.3 创建IPostRepository接口
|
||||
```typescript
|
||||
// src/data/repositories/interfaces/IPostRepository.ts
|
||||
interface IPostRepository {
|
||||
getPosts(type: string, page: number, pageSize: number): Promise<Post[]>;
|
||||
savePost(post: Post): Promise<void>;
|
||||
updatePost(postId: string, updates: Partial<Post>): Promise<void>;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2.4 创建PostRepository实现
|
||||
```typescript
|
||||
// src/data/repositories/PostRepository.ts
|
||||
// 封装SQLite操作,实现IPostRepository接口
|
||||
```
|
||||
|
||||
#### 4.2.5 创建useDifferentialPosts Hook
|
||||
```typescript
|
||||
// src/hooks/useDifferentialPosts.ts
|
||||
// 类似useDifferentialMessages,处理帖子列表差异更新
|
||||
```
|
||||
|
||||
#### 4.2.6 改造postService
|
||||
- 移除对useUserStore的直接依赖
|
||||
- 仅保留API调用职责
|
||||
|
||||
---
|
||||
|
||||
## 五、架构对比图
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Messages模块 [理想架构]"
|
||||
E1[Entity<br/>Message.ts]
|
||||
U1[UseCase<br/>ProcessMessageUseCase]
|
||||
R1[Repository<br/>MessageRepository]
|
||||
M1[Mapper<br/>MessageMapper]
|
||||
H1[Hook<br/>useDifferentialMessages]
|
||||
D1[Diff基础设施<br/>DiffCalculator + Batcher]
|
||||
S1[(SQLite)]
|
||||
|
||||
E1 --> U1
|
||||
U1 --> R1
|
||||
U1 --> D1
|
||||
R1 --> S1
|
||||
M1 --> R1
|
||||
D1 --> H1
|
||||
H1 --> S1
|
||||
end
|
||||
|
||||
subgraph "Post模块 [当前架构]"
|
||||
E2[PostMapper]
|
||||
SV2[postService<br/>直接操作Store]
|
||||
ST2[useUserStore<br/>Zustand]
|
||||
H2[Hook<br/>useCursorPagination]
|
||||
PM2[postManager<br/>内存缓存]
|
||||
|
||||
E2 --> SV2
|
||||
SV2 --> ST2
|
||||
H2 --> SV2
|
||||
PM2 --> ST2
|
||||
end
|
||||
|
||||
style E1 fill:#90EE90
|
||||
style U1 fill:#90EE90
|
||||
style R1 fill:#90EE90
|
||||
style D1 fill:#90EE90
|
||||
style H1 fill:#90EE90
|
||||
style S1 fill:#90EE90
|
||||
|
||||
style SV2 fill:#FFB6C1
|
||||
style ST2 fill:#FFB6C1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、结论
|
||||
|
||||
Messages模块是一个**架构完善**的模块,具备:
|
||||
1. 清晰的分层架构
|
||||
2. 独立的业务逻辑编排层(UseCase)
|
||||
3. 完整的数据持久化(SQLite)
|
||||
4. 高效的差异更新机制
|
||||
5. 实时事件推送能力
|
||||
|
||||
Post模块当前存在以下问题:
|
||||
1. **违反分层原则**:Service直接操作Store
|
||||
2. **缺少UseCase层**:业务逻辑分散
|
||||
3. **无数据抽象**:缺少Repository接口
|
||||
4. **无差异更新**:每次全量更新导致性能问题
|
||||
5. **无本地持久化**:无法离线使用
|
||||
|
||||
建议按照对齐建议逐步重构Post模块,使其架构与Messages模块对齐。
|
||||
@@ -1,147 +0,0 @@
|
||||
/**
|
||||
* NotificationItem 通知项组件
|
||||
* 根据通知类型显示不同图标和内容
|
||||
*
|
||||
* @deprecated 请使用 SystemMessageItem 组件代替
|
||||
* 该组件使用旧的 Notification 类型,新功能请使用 SystemMessageItem
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { View, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { zhCN } from 'date-fns/locale';
|
||||
import { colors, spacing, borderRadius } from '../../theme';
|
||||
import { Notification, NotificationType } from '../../types';
|
||||
import Text from '../common/Text';
|
||||
import Avatar from '../common/Avatar';
|
||||
|
||||
interface NotificationItemProps {
|
||||
notification: Notification;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
// 通知类型到图标和颜色的映射
|
||||
const getNotificationIcon = (type: NotificationType): { icon: string; color: string } => {
|
||||
switch (type) {
|
||||
case 'like_post':
|
||||
return { icon: 'heart', color: colors.error.main };
|
||||
case 'like_comment':
|
||||
return { icon: 'heart', color: colors.error.main };
|
||||
case 'comment':
|
||||
return { icon: 'comment', color: colors.info.main };
|
||||
case 'reply':
|
||||
return { icon: 'reply', color: colors.info.main };
|
||||
case 'follow':
|
||||
return { icon: 'account-plus', color: colors.primary.main };
|
||||
case 'mention':
|
||||
return { icon: 'at', color: colors.warning.main };
|
||||
case 'system':
|
||||
return { icon: 'information', color: colors.text.secondary };
|
||||
default:
|
||||
return { icon: 'bell', color: colors.text.secondary };
|
||||
}
|
||||
};
|
||||
|
||||
const NotificationItem: React.FC<NotificationItemProps> = ({
|
||||
notification,
|
||||
onPress,
|
||||
}) => {
|
||||
// 格式化时间
|
||||
const formatTime = (dateString: string): string => {
|
||||
try {
|
||||
return formatDistanceToNow(new Date(dateString), {
|
||||
addSuffix: true,
|
||||
locale: zhCN,
|
||||
});
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const { icon, color } = getNotificationIcon(notification.type);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.container, !notification.isRead && styles.unread]}
|
||||
onPress={onPress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{/* 通知图标/头像 */}
|
||||
<View style={styles.iconContainer}>
|
||||
{notification.type === 'follow' && notification.data.userId ? (
|
||||
<Avatar
|
||||
source={null}
|
||||
size={40}
|
||||
name={notification.title}
|
||||
/>
|
||||
) : (
|
||||
<View style={[styles.iconWrapper, { backgroundColor: color + '20' }]}>
|
||||
<MaterialCommunityIcons name={icon as any} size={20} color={color} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 通知内容 */}
|
||||
<View style={styles.content}>
|
||||
<View style={styles.textContainer}>
|
||||
<Text
|
||||
variant="body"
|
||||
numberOfLines={2}
|
||||
style={!notification.isRead ? styles.unreadText : undefined}
|
||||
>
|
||||
{notification.content}
|
||||
</Text>
|
||||
</View>
|
||||
<Text variant="caption" color={colors.text.secondary}>
|
||||
{formatTime(notification.createdAt)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* 未读标记 */}
|
||||
{!notification.isRead && <View style={styles.unreadDot} />}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: spacing.lg,
|
||||
backgroundColor: colors.background.paper,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.divider,
|
||||
},
|
||||
unread: {
|
||||
backgroundColor: colors.primary.light + '10',
|
||||
},
|
||||
iconContainer: {
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
iconWrapper: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
textContainer: {
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
unreadText: {
|
||||
fontWeight: '600',
|
||||
},
|
||||
unreadDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: colors.primary.main,
|
||||
marginLeft: spacing.sm,
|
||||
},
|
||||
});
|
||||
|
||||
export default NotificationItem;
|
||||
@@ -5,7 +5,6 @@
|
||||
export { default as PostCard } from './PostCard';
|
||||
export { default as CommentItem } from './CommentItem';
|
||||
export { default as UserProfileHeader } from './UserProfileHeader';
|
||||
export { default as NotificationItem } from './NotificationItem';
|
||||
export { default as SystemMessageItem } from './SystemMessageItem';
|
||||
export { default as SearchBar } from './SearchBar';
|
||||
export { default as TabBar } from './TabBar';
|
||||
|
||||
267
src/core/entities/Post.ts
Normal file
267
src/core/entities/Post.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* Post Entity - 帖子领域实体
|
||||
* 定义帖子的核心属性和行为,不依赖任何外部框架
|
||||
*/
|
||||
|
||||
// ==================== 帖子状态枚举 ====================
|
||||
|
||||
export type PostStatus = 'published' | 'draft' | 'deleted';
|
||||
|
||||
// ==================== 值对象 ====================
|
||||
|
||||
/**
|
||||
* 帖子作者信息
|
||||
*/
|
||||
export interface PostAuthor {
|
||||
id: string;
|
||||
username: string;
|
||||
nickname?: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 帖子图片信息
|
||||
*/
|
||||
export interface PostImage {
|
||||
url: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
thumbnailUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 帖子标签
|
||||
*/
|
||||
export interface PostTag {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// ==================== 主实体 ====================
|
||||
|
||||
/**
|
||||
* 帖子实体
|
||||
*/
|
||||
export interface Post {
|
||||
/** 帖子唯一标识 */
|
||||
id: string;
|
||||
/** 作者ID */
|
||||
authorId: string;
|
||||
/** 作者信息 */
|
||||
author?: PostAuthor;
|
||||
/** 帖子标题 */
|
||||
title: string;
|
||||
/** 帖子内容 */
|
||||
content: string;
|
||||
/** 图片列表 */
|
||||
images: PostImage[];
|
||||
/** 点赞数 (camelCase) */
|
||||
likesCount: number;
|
||||
/** 评论数 (camelCase) */
|
||||
commentsCount: number;
|
||||
/** 分享数 (camelCase) */
|
||||
sharesCount: number;
|
||||
/** 浏览数 (camelCase) */
|
||||
viewsCount: number;
|
||||
/** 收藏数 (camelCase) */
|
||||
favoritesCount: number;
|
||||
/** 当前用户是否已点赞 (camelCase) */
|
||||
isLiked: boolean;
|
||||
/** 当前用户是否已收藏 (camelCase) */
|
||||
isFavorited: boolean;
|
||||
/** 是否置顶 (camelCase) */
|
||||
isPinned: boolean;
|
||||
/** 帖子状态 */
|
||||
status: PostStatus;
|
||||
/** 所属社区ID */
|
||||
communityId?: string;
|
||||
/** 标签列表 */
|
||||
tags: string[];
|
||||
/** 创建时间 */
|
||||
createdAt: string;
|
||||
/** 更新时间 */
|
||||
updatedAt: string;
|
||||
|
||||
// ==================== 向后兼容字段 (snake_case) ====================
|
||||
// 这些字段用于与旧代码兼容,新代码应使用 camelCase 版本
|
||||
|
||||
/** @deprecated 请使用 likesCount */
|
||||
likes_count?: number;
|
||||
/** @deprecated 请使用 commentsCount */
|
||||
comments_count?: number;
|
||||
/** @deprecated 请使用 favoritesCount */
|
||||
favorites_count?: number;
|
||||
/** @deprecated 请使用 sharesCount */
|
||||
shares_count?: number;
|
||||
/** @deprecated 请使用 viewsCount */
|
||||
views_count?: number;
|
||||
/** @deprecated 请使用 isLiked */
|
||||
is_liked?: boolean;
|
||||
/** @deprecated 请使用 isFavorited */
|
||||
is_favorited?: boolean;
|
||||
/** @deprecated 请使用 isPinned */
|
||||
is_pinned?: boolean;
|
||||
/** @deprecated 请使用 authorId */
|
||||
user_id?: string;
|
||||
/** @deprecated 请使用 createdAt */
|
||||
created_at?: string;
|
||||
/** @deprecated 请使用 updatedAt */
|
||||
updated_at?: string;
|
||||
/** @deprecated 请使用 communityId */
|
||||
community_id?: string;
|
||||
/** @deprecated 请使用 status */
|
||||
status_str?: string;
|
||||
/** 投票帖子标识 (部分旧代码使用) */
|
||||
is_vote?: boolean;
|
||||
/** 锁定状态 (部分旧代码使用) */
|
||||
is_locked?: boolean;
|
||||
/** 置顶评论 (部分旧代码使用) */
|
||||
top_comment?: any;
|
||||
}
|
||||
|
||||
// ==================== 工厂函数 ====================
|
||||
|
||||
/**
|
||||
* 创建帖子作者信息
|
||||
*/
|
||||
export const createPostAuthor = (data: Partial<PostAuthor>): PostAuthor => ({
|
||||
id: data.id || '',
|
||||
username: data.username || '',
|
||||
nickname: data.nickname,
|
||||
avatar: data.avatar,
|
||||
});
|
||||
|
||||
/**
|
||||
* 创建帖子图片信息
|
||||
*/
|
||||
export const createPostImage = (data: Partial<PostImage>): PostImage => ({
|
||||
url: data.url || '',
|
||||
width: data.width,
|
||||
height: data.height,
|
||||
thumbnailUrl: data.thumbnailUrl,
|
||||
});
|
||||
|
||||
/**
|
||||
* 创建帖子实体
|
||||
*/
|
||||
export const createPost = (data: Partial<Post>): Post => ({
|
||||
id: data.id || '',
|
||||
authorId: data.authorId || '',
|
||||
author: data.author,
|
||||
title: data.title || '',
|
||||
content: data.content || '',
|
||||
images: data.images || [],
|
||||
likesCount: data.likesCount || 0,
|
||||
commentsCount: data.commentsCount || 0,
|
||||
sharesCount: data.sharesCount || 0,
|
||||
viewsCount: data.viewsCount || 0,
|
||||
favoritesCount: data.favoritesCount || 0,
|
||||
isLiked: data.isLiked || false,
|
||||
isFavorited: data.isFavorited || false,
|
||||
isPinned: data.isPinned || false,
|
||||
status: data.status || 'published',
|
||||
communityId: data.communityId,
|
||||
tags: data.tags || [],
|
||||
createdAt: data.createdAt || new Date().toISOString(),
|
||||
updatedAt: data.updatedAt || new Date().toISOString(),
|
||||
});
|
||||
|
||||
// ==================== 类型守卫 ====================
|
||||
|
||||
/**
|
||||
* 检查是否为有效的帖子状态
|
||||
*/
|
||||
export const isValidPostStatus = (status: string): status is PostStatus => {
|
||||
return ['published', 'draft', 'deleted'].includes(status);
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查帖子是否已发布
|
||||
*/
|
||||
export const isPostPublished = (post: Post): boolean => {
|
||||
return post.status === 'published';
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查帖子是否已删除
|
||||
*/
|
||||
export const isPostDeleted = (post: Post): boolean => {
|
||||
return post.status === 'deleted';
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查帖子是否为草稿
|
||||
*/
|
||||
export const isPostDraft = (post: Post): boolean => {
|
||||
return post.status === 'draft';
|
||||
};
|
||||
|
||||
// ==================== 辅助函数 ====================
|
||||
|
||||
/**
|
||||
* 检查当前用户是否为帖子作者
|
||||
*/
|
||||
export const isPostAuthor = (post: Post, userId: string): boolean => {
|
||||
return post.authorId === userId;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取帖子的主要图片(第一张图片)
|
||||
*/
|
||||
export const getPostThumbnail = (post: Post): PostImage | undefined => {
|
||||
return post.images[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查帖子是否包含图片
|
||||
*/
|
||||
export const hasPostImages = (post: Post): boolean => {
|
||||
return post.images.length > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取帖子的显示标题(优先使用标题,否则使用内容摘要)
|
||||
*/
|
||||
export const getPostDisplayTitle = (post: Post, maxLength: number = 50): string => {
|
||||
if (post.title) {
|
||||
return post.title;
|
||||
}
|
||||
if (post.content.length > maxLength) {
|
||||
return post.content.substring(0, maxLength) + '...';
|
||||
}
|
||||
return post.content;
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算帖子的互动总数
|
||||
*/
|
||||
export const getPostTotalEngagement = (post: Post): number => {
|
||||
return post.likesCount + post.commentsCount + post.sharesCount + post.favoritesCount;
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化帖子创建时间为相对时间描述
|
||||
*/
|
||||
export const formatPostTime = (post: Post): string => {
|
||||
const createdAt = new Date(post.createdAt);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - createdAt.getTime();
|
||||
const diffSeconds = Math.floor(diffMs / 1000);
|
||||
const diffMinutes = Math.floor(diffSeconds / 60);
|
||||
const diffHours = Math.floor(diffMinutes / 60);
|
||||
const diffDays = Math.floor(diffHours / 24);
|
||||
|
||||
if (diffSeconds < 60) {
|
||||
return '刚刚';
|
||||
}
|
||||
if (diffMinutes < 60) {
|
||||
return `${diffMinutes}分钟前`;
|
||||
}
|
||||
if (diffHours < 24) {
|
||||
return `${diffHours}小时前`;
|
||||
}
|
||||
if (diffDays < 7) {
|
||||
return `${diffDays}天前`;
|
||||
}
|
||||
return createdAt.toLocaleDateString('zh-CN');
|
||||
};
|
||||
863
src/core/usecases/ProcessPostUseCase.ts
Normal file
863
src/core/usecases/ProcessPostUseCase.ts
Normal file
@@ -0,0 +1,863 @@
|
||||
/**
|
||||
* 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: string | null;
|
||||
/** 最后加载时间 */
|
||||
lastLoadTime: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 帖子详情状态
|
||||
*/
|
||||
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<PostUseCaseSubscriber> = new Set();
|
||||
|
||||
// 状态管理
|
||||
private postsState: Map<string, PostsState> = new Map();
|
||||
private postDetailState: Map<string, PostDetailState> = new Map();
|
||||
|
||||
// 分页状态管理器
|
||||
private paginationManager: PaginationStateManager;
|
||||
|
||||
// 当前用户ID
|
||||
private currentUserId: string | null = null;
|
||||
|
||||
// 私有构造函数
|
||||
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,
|
||||
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<PostsState>): void {
|
||||
const currentState = this.getPostsState(key);
|
||||
this.postsState.set(key, {
|
||||
...currentState,
|
||||
...partialState,
|
||||
});
|
||||
this.notifyStateChanged(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新帖子详情状态
|
||||
* @param postId 帖子ID
|
||||
* @param partialState 部分状态
|
||||
*/
|
||||
private updatePostDetailState(postId: string, partialState: Partial<PostDetailState>): void {
|
||||
const currentState = this.getPostDetailState(postId);
|
||||
this.postDetailState.set(postId, {
|
||||
...currentState,
|
||||
...partialState,
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 帖子列表操作 ====================
|
||||
|
||||
/**
|
||||
* 获取帖子列表
|
||||
* @param params 查询参数
|
||||
* @param key 列表键(用于区分不同的列表)
|
||||
*/
|
||||
async fetchPosts(params?: GetPostsParams, key: string = 'default'): Promise<PostsResult> {
|
||||
const state = this.getPostsState(key);
|
||||
|
||||
// 更新加载状态
|
||||
this.updatePostsState(key, { isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
// 合并分页参数
|
||||
const queryParams: GetPostsParams = {
|
||||
page: params?.page || 1,
|
||||
pageSize: params?.pageSize || DEFAULT_PAGE_SIZE,
|
||||
cursor: params?.cursor || state.cursor || undefined,
|
||||
...params,
|
||||
};
|
||||
|
||||
const result = await postRepository.getPosts(queryParams);
|
||||
|
||||
// 更新状态
|
||||
this.updatePostsState(key, {
|
||||
posts: result.posts,
|
||||
hasMore: result.hasMore,
|
||||
cursor: result.nextCursor || null,
|
||||
isLoading: false,
|
||||
lastLoadTime: Date.now(),
|
||||
});
|
||||
|
||||
// 通知订阅者
|
||||
this.notifySubscribers({
|
||||
type: 'posts_loaded',
|
||||
payload: { key, posts: result.posts, 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<PostsResult> {
|
||||
// 更新刷新状态
|
||||
this.updatePostsState(key, { isRefreshing: true, error: null });
|
||||
|
||||
try {
|
||||
const result = await postRepository.getPosts({
|
||||
page: 1,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
});
|
||||
|
||||
// 更新状态
|
||||
this.updatePostsState(key, {
|
||||
posts: result.posts,
|
||||
hasMore: result.hasMore,
|
||||
cursor: result.nextCursor || null,
|
||||
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<PostsResult> {
|
||||
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 {
|
||||
const queryParams: GetPostsParams = {
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
cursor: state.cursor || undefined,
|
||||
};
|
||||
|
||||
const result = await postRepository.getPosts(queryParams);
|
||||
|
||||
// 合并新数据
|
||||
const newPosts = [...state.posts, ...result.posts];
|
||||
|
||||
// 更新状态
|
||||
this.updatePostsState(key, {
|
||||
posts: newPosts,
|
||||
hasMore: result.hasMore,
|
||||
cursor: result.nextCursor || null,
|
||||
isLoading: false,
|
||||
lastLoadTime: Date.now(),
|
||||
});
|
||||
|
||||
return {
|
||||
posts: result.posts,
|
||||
hasMore: result.hasMore,
|
||||
nextCursor: result.nextCursor,
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '加载更多帖子失败';
|
||||
this.updatePostsState(key, {
|
||||
isLoading: false,
|
||||
error: errorMessage,
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 帖子详情操作 ====================
|
||||
|
||||
/**
|
||||
* 获取单个帖子详情
|
||||
* @param id 帖子ID
|
||||
*/
|
||||
async fetchPostById(id: string): Promise<Post | null> {
|
||||
// 更新加载状态
|
||||
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<Post> {
|
||||
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<Post> {
|
||||
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<void> {
|
||||
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<Post> {
|
||||
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<Post> {
|
||||
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<Post> {
|
||||
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<Post> {
|
||||
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<PostsResult> {
|
||||
// 更新加载状态
|
||||
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<PostsResult> {
|
||||
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;
|
||||
@@ -132,6 +132,12 @@ export class LocalDataSource implements ILocalDataSource {
|
||||
updatedAt TEXT NOT NULL,
|
||||
PRIMARY KEY (groupId, userId)
|
||||
)`,
|
||||
// 帖子缓存表
|
||||
`CREATE TABLE IF NOT EXISTS posts_cache (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL
|
||||
)`,
|
||||
];
|
||||
|
||||
for (const sql of tables) {
|
||||
|
||||
550
src/data/repositories/PostRepository.ts
Normal file
550
src/data/repositories/PostRepository.ts
Normal file
@@ -0,0 +1,550 @@
|
||||
/**
|
||||
* PostRepository - 帖子仓库实现
|
||||
* 实现IPostRepository接口,封装帖子相关的数据访问逻辑
|
||||
*/
|
||||
|
||||
import { ApiDataSource, apiDataSource } from '../datasources/ApiDataSource';
|
||||
import { LocalDataSource, localDataSource } from '../datasources/LocalDataSource';
|
||||
import { PostMapper } from '../mappers/PostMapper';
|
||||
import type { Post, PostImage, PostStatus } from '../../core/entities/Post';
|
||||
import { createPost } from '../../core/entities/Post';
|
||||
import type {
|
||||
IPostRepository,
|
||||
GetPostsParams,
|
||||
CreatePostData,
|
||||
UpdatePostData,
|
||||
PostsResult,
|
||||
SearchPostsParams,
|
||||
} from './interfaces/IPostRepository';
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
/**
|
||||
* API帖子响应类型
|
||||
*/
|
||||
interface PostApiResponse {
|
||||
id: number;
|
||||
user_id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
images?: Array<{ url: string; width?: number; height?: number; thumbnail_url?: string }>;
|
||||
likes_count: number;
|
||||
comments_count: number;
|
||||
shares_count: number;
|
||||
views_count: number;
|
||||
favorites_count: number;
|
||||
is_liked: boolean;
|
||||
is_favorited: boolean;
|
||||
is_pinned: boolean;
|
||||
status: string;
|
||||
community_id?: string;
|
||||
tags?: string[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
author?: {
|
||||
id: number;
|
||||
username: string;
|
||||
nickname?: string;
|
||||
avatar?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* API帖子列表响应类型
|
||||
*/
|
||||
interface PostsListApiResponse {
|
||||
posts: PostApiResponse[];
|
||||
has_more: boolean;
|
||||
next_cursor?: string;
|
||||
total?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存的帖子数据
|
||||
*/
|
||||
interface CachedPost {
|
||||
id: string;
|
||||
data: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// ==================== PostRepository 实现 ====================
|
||||
|
||||
export class PostRepository implements IPostRepository {
|
||||
private api: ApiDataSource;
|
||||
private localDb: LocalDataSource;
|
||||
private memoryCache: Map<string, { post: Post; timestamp: number }> = new Map();
|
||||
private readonly CACHE_TTL = 5 * 60 * 1000; // 5分钟缓存过期时间
|
||||
|
||||
constructor(api: ApiDataSource, localDb: LocalDataSource) {
|
||||
this.api = api;
|
||||
this.localDb = localDb;
|
||||
}
|
||||
|
||||
// ==================== 私有辅助方法 ====================
|
||||
|
||||
/**
|
||||
* 将API响应转换为领域实体
|
||||
* 同时设置 camelCase 和 snake_case 字段以保持向后兼容
|
||||
*/
|
||||
private mapToPost(response: PostApiResponse): Post {
|
||||
const model = PostMapper.fromApiResponse(response as any);
|
||||
return {
|
||||
// camelCase 字段(新标准)
|
||||
id: model.id,
|
||||
authorId: model.authorId,
|
||||
author: model.author ? {
|
||||
id: model.author.id,
|
||||
username: model.author.username,
|
||||
nickname: model.author.nickname,
|
||||
avatar: model.author.avatar,
|
||||
} : undefined,
|
||||
title: model.title,
|
||||
content: model.content,
|
||||
images: (model.images || []).map(url => ({
|
||||
url,
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
})) as PostImage[],
|
||||
likesCount: model.likeCount,
|
||||
commentsCount: model.commentCount,
|
||||
sharesCount: model.shareCount,
|
||||
viewsCount: model.viewCount,
|
||||
favoritesCount: model.favoriteCount,
|
||||
isLiked: model.isLiked,
|
||||
isFavorited: model.isFavorited,
|
||||
isPinned: model.isTop,
|
||||
status: model.status,
|
||||
communityId: model.communityId,
|
||||
tags: model.tags || [],
|
||||
createdAt: model.createdAt.toISOString(),
|
||||
updatedAt: model.updatedAt.toISOString(),
|
||||
|
||||
// snake_case 字段(向后兼容)
|
||||
likes_count: model.likeCount,
|
||||
comments_count: model.commentCount,
|
||||
favorites_count: model.favoriteCount,
|
||||
shares_count: model.shareCount,
|
||||
views_count: model.viewCount,
|
||||
is_liked: model.isLiked,
|
||||
is_favorited: model.isFavorited,
|
||||
is_pinned: model.isTop,
|
||||
user_id: model.authorId,
|
||||
created_at: model.createdAt.toISOString(),
|
||||
updated_at: model.updatedAt.toISOString(),
|
||||
community_id: model.communityId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 从内存缓存获取帖子
|
||||
*/
|
||||
private getFromMemoryCache(id: string): Post | null {
|
||||
const cached = this.memoryCache.get(id);
|
||||
if (!cached) return null;
|
||||
|
||||
// 检查缓存是否过期
|
||||
if (Date.now() - cached.timestamp > this.CACHE_TTL) {
|
||||
this.memoryCache.delete(id);
|
||||
return null;
|
||||
}
|
||||
|
||||
return cached.post;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存帖子到内存缓存
|
||||
*/
|
||||
private saveToMemoryCache(post: Post): void {
|
||||
this.memoryCache.set(post.id, {
|
||||
post,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地数据库获取缓存的帖子
|
||||
*/
|
||||
private async getFromLocalCache(id: string): Promise<Post | null> {
|
||||
try {
|
||||
await this.localDb.initialize();
|
||||
const result = await this.localDb.getFirst<CachedPost>(
|
||||
'SELECT * FROM posts_cache WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!result) return null;
|
||||
|
||||
const post = JSON.parse(result.data) as Post;
|
||||
return post;
|
||||
} catch (error) {
|
||||
console.error('[PostRepository] 获取本地缓存失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存帖子到本地缓存
|
||||
*/
|
||||
private async saveToLocalCache(post: Post): Promise<void> {
|
||||
try {
|
||||
await this.localDb.initialize();
|
||||
await this.localDb.run(
|
||||
`INSERT OR REPLACE INTO posts_cache (id, data, updatedAt) VALUES (?, ?, ?)`,
|
||||
[post.id, JSON.stringify(post), new Date().toISOString()]
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('[PostRepository] 保存本地缓存失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除帖子的本地缓存
|
||||
*/
|
||||
private async clearLocalCache(id: string): Promise<void> {
|
||||
try {
|
||||
await this.localDb.initialize();
|
||||
await this.localDb.run('DELETE FROM posts_cache WHERE id = ?', [id]);
|
||||
} catch (error) {
|
||||
console.error('[PostRepository] 清除本地缓存失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理错误
|
||||
*/
|
||||
private handleError(error: unknown, operation: string): never {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error(`[PostRepository] ${operation} 失败:`, error);
|
||||
throw new Error(`帖子${operation}失败: ${message}`);
|
||||
}
|
||||
|
||||
// ==================== 帖子查询 ====================
|
||||
|
||||
/**
|
||||
* 获取帖子列表
|
||||
*/
|
||||
async getPosts(params?: GetPostsParams): Promise<PostsResult> {
|
||||
try {
|
||||
const queryParams: Record<string, any> = {};
|
||||
|
||||
if (params?.page) queryParams.page = params.page;
|
||||
if (params?.pageSize) queryParams.page_size = params.pageSize;
|
||||
if (params?.cursor) queryParams.cursor = params.cursor;
|
||||
if (params?.post_type) queryParams.tab = params.post_type;
|
||||
if (params?.communityId) queryParams.community_id = params.communityId;
|
||||
if (params?.authorId) queryParams.author_id = params.authorId;
|
||||
if (params?.tags && params.tags.length > 0) queryParams.tags = params.tags.join(',');
|
||||
if (params?.status) queryParams.status = params.status;
|
||||
if (params?.sortBy) queryParams.sort_by = params.sortBy;
|
||||
if (params?.sortOrder) queryParams.sort_order = params.sortOrder;
|
||||
if (params?.pinnedOnly) queryParams.pinned_only = true;
|
||||
|
||||
const response = await this.api.get<PostsListApiResponse>('/posts', queryParams);
|
||||
|
||||
const posts = response.posts.map(p => this.mapToPost(p));
|
||||
|
||||
// 缓存帖子
|
||||
posts.forEach(post => {
|
||||
this.saveToMemoryCache(post);
|
||||
this.saveToLocalCache(post);
|
||||
});
|
||||
|
||||
return {
|
||||
posts,
|
||||
hasMore: response.has_more,
|
||||
nextCursor: response.next_cursor,
|
||||
total: response.total,
|
||||
};
|
||||
} catch (error) {
|
||||
this.handleError(error, '获取帖子列表');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个帖子详情
|
||||
*/
|
||||
async getPostById(id: string): Promise<Post | null> {
|
||||
try {
|
||||
// 1. 先查内存缓存
|
||||
const memoryCached = this.getFromMemoryCache(id);
|
||||
if (memoryCached) {
|
||||
return memoryCached;
|
||||
}
|
||||
|
||||
// 2. 查本地数据库缓存
|
||||
const localCached = await this.getFromLocalCache(id);
|
||||
if (localCached) {
|
||||
this.saveToMemoryCache(localCached);
|
||||
return localCached;
|
||||
}
|
||||
|
||||
// 3. 从API获取
|
||||
const response = await this.api.get<PostApiResponse>(`/posts/${id}`);
|
||||
const post = this.mapToPost(response);
|
||||
|
||||
// 更新缓存
|
||||
this.saveToMemoryCache(post);
|
||||
await this.saveToLocalCache(post);
|
||||
|
||||
return post;
|
||||
} catch (error) {
|
||||
// 如果API请求失败,尝试返回本地缓存
|
||||
const localCached = await this.getFromLocalCache(id);
|
||||
if (localCached) {
|
||||
console.warn('[PostRepository] API获取失败,使用本地缓存:', error);
|
||||
return localCached;
|
||||
}
|
||||
this.handleError(error, '获取帖子详情');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户发布的帖子列表
|
||||
*/
|
||||
async getPostsByUser(userId: string, params?: GetPostsParams): Promise<PostsResult> {
|
||||
try {
|
||||
const queryParams: Record<string, any> = { author_id: userId };
|
||||
|
||||
if (params?.page) queryParams.page = params.page;
|
||||
if (params?.pageSize) queryParams.page_size = params.pageSize;
|
||||
if (params?.cursor) queryParams.cursor = params.cursor;
|
||||
if (params?.status) queryParams.status = params.status;
|
||||
if (params?.sortBy) queryParams.sort_by = params.sortBy;
|
||||
if (params?.sortOrder) queryParams.sort_order = params.sortOrder;
|
||||
|
||||
const response = await this.api.get<PostsListApiResponse>(`/users/${userId}/posts`, queryParams);
|
||||
|
||||
const posts = response.posts.map(p => this.mapToPost(p));
|
||||
|
||||
// 缓存帖子
|
||||
posts.forEach(post => {
|
||||
this.saveToMemoryCache(post);
|
||||
this.saveToLocalCache(post);
|
||||
});
|
||||
|
||||
return {
|
||||
posts,
|
||||
hasMore: response.has_more,
|
||||
nextCursor: response.next_cursor,
|
||||
total: response.total,
|
||||
};
|
||||
} catch (error) {
|
||||
this.handleError(error, '获取用户帖子列表');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索帖子
|
||||
*/
|
||||
async searchPosts(params: SearchPostsParams): Promise<PostsResult> {
|
||||
try {
|
||||
const queryParams: Record<string, any> = {
|
||||
keyword: params.keyword,
|
||||
};
|
||||
|
||||
if (params.page) queryParams.page = params.page;
|
||||
if (params.pageSize) queryParams.page_size = params.pageSize;
|
||||
if (params.scope) queryParams.scope = params.scope;
|
||||
if (params.communityId) queryParams.community_id = params.communityId;
|
||||
|
||||
const response = await this.api.get<PostsListApiResponse>('/posts/search', queryParams);
|
||||
|
||||
const posts = response.posts.map(p => this.mapToPost(p));
|
||||
|
||||
// 缓存帖子
|
||||
posts.forEach(post => {
|
||||
this.saveToMemoryCache(post);
|
||||
this.saveToLocalCache(post);
|
||||
});
|
||||
|
||||
return {
|
||||
posts,
|
||||
hasMore: response.has_more,
|
||||
nextCursor: response.next_cursor,
|
||||
total: response.total,
|
||||
};
|
||||
} catch (error) {
|
||||
this.handleError(error, '搜索帖子');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 帖子操作 ====================
|
||||
|
||||
/**
|
||||
* 创建帖子
|
||||
*/
|
||||
async createPost(data: CreatePostData): Promise<Post> {
|
||||
try {
|
||||
const requestData = PostMapper.createPostRequest(
|
||||
data.title,
|
||||
data.content,
|
||||
data.images?.map(img => img.url),
|
||||
data.communityId
|
||||
);
|
||||
|
||||
if (data.tags && data.tags.length > 0) {
|
||||
requestData.tags = data.tags;
|
||||
}
|
||||
|
||||
if (data.status) {
|
||||
requestData.status = data.status;
|
||||
}
|
||||
|
||||
const response = await this.api.post<PostApiResponse>('/posts', requestData);
|
||||
const post = this.mapToPost(response);
|
||||
|
||||
// 缓存新创建的帖子
|
||||
this.saveToMemoryCache(post);
|
||||
await this.saveToLocalCache(post);
|
||||
|
||||
return post;
|
||||
} catch (error) {
|
||||
this.handleError(error, '创建帖子');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新帖子
|
||||
*/
|
||||
async updatePost(id: string, data: UpdatePostData): Promise<Post> {
|
||||
try {
|
||||
const requestData: Record<string, any> = {};
|
||||
|
||||
if (data.title !== undefined) requestData.title = data.title;
|
||||
if (data.content !== undefined) requestData.content = data.content;
|
||||
if (data.images !== undefined) requestData.images = data.images.map(img => img.url);
|
||||
if (data.tags !== undefined) requestData.tags = data.tags;
|
||||
if (data.status !== undefined) requestData.status = data.status;
|
||||
if (data.isPinned !== undefined) requestData.is_pinned = data.isPinned;
|
||||
|
||||
const response = await this.api.put<PostApiResponse>(`/posts/${id}`, requestData);
|
||||
const post = this.mapToPost(response);
|
||||
|
||||
// 更新缓存
|
||||
this.saveToMemoryCache(post);
|
||||
await this.saveToLocalCache(post);
|
||||
|
||||
return post;
|
||||
} catch (error) {
|
||||
this.handleError(error, '更新帖子');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除帖子
|
||||
*/
|
||||
async deletePost(id: string): Promise<void> {
|
||||
try {
|
||||
await this.api.delete(`/posts/${id}`);
|
||||
|
||||
// 清除缓存
|
||||
this.memoryCache.delete(id);
|
||||
await this.clearLocalCache(id);
|
||||
} catch (error) {
|
||||
this.handleError(error, '删除帖子');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 互动操作 ====================
|
||||
|
||||
/**
|
||||
* 点赞帖子
|
||||
*/
|
||||
async likePost(id: string): Promise<Post> {
|
||||
try {
|
||||
const response = await this.api.post<PostApiResponse>(`/posts/${id}/like`);
|
||||
const post = this.mapToPost(response);
|
||||
|
||||
// 更新缓存
|
||||
this.saveToMemoryCache(post);
|
||||
await this.saveToLocalCache(post);
|
||||
|
||||
return post;
|
||||
} catch (error) {
|
||||
this.handleError(error, '点赞帖子');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消点赞
|
||||
*/
|
||||
async unlikePost(id: string): Promise<Post> {
|
||||
try {
|
||||
const response = await this.api.delete<PostApiResponse>(`/posts/${id}/like`);
|
||||
const post = this.mapToPost(response);
|
||||
|
||||
// 更新缓存
|
||||
this.saveToMemoryCache(post);
|
||||
await this.saveToLocalCache(post);
|
||||
|
||||
return post;
|
||||
} catch (error) {
|
||||
this.handleError(error, '取消点赞');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收藏帖子
|
||||
*/
|
||||
async favoritePost(id: string): Promise<Post> {
|
||||
try {
|
||||
const response = await this.api.post<PostApiResponse>(`/posts/${id}/favorite`);
|
||||
const post = this.mapToPost(response);
|
||||
|
||||
// 更新缓存
|
||||
this.saveToMemoryCache(post);
|
||||
await this.saveToLocalCache(post);
|
||||
|
||||
return post;
|
||||
} catch (error) {
|
||||
this.handleError(error, '收藏帖子');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消收藏
|
||||
*/
|
||||
async unfavoritePost(id: string): Promise<Post> {
|
||||
try {
|
||||
const response = await this.api.delete<PostApiResponse>(`/posts/${id}/favorite`);
|
||||
const post = this.mapToPost(response);
|
||||
|
||||
// 更新缓存
|
||||
this.saveToMemoryCache(post);
|
||||
await this.saveToLocalCache(post);
|
||||
|
||||
return post;
|
||||
} catch (error) {
|
||||
this.handleError(error, '取消收藏');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 缓存管理 ====================
|
||||
|
||||
/**
|
||||
* 清除所有缓存
|
||||
*/
|
||||
async clearAllCache(): Promise<void> {
|
||||
this.memoryCache.clear();
|
||||
try {
|
||||
await this.localDb.initialize();
|
||||
await this.localDb.run('DELETE FROM posts_cache');
|
||||
} catch (error) {
|
||||
console.error('[PostRepository] 清除所有缓存失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使指定帖子的缓存失效
|
||||
*/
|
||||
async invalidateCache(id: string): Promise<void> {
|
||||
this.memoryCache.delete(id);
|
||||
await this.clearLocalCache(id);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 单例导出 ====================
|
||||
|
||||
export const postRepository = new PostRepository(apiDataSource, localDataSource);
|
||||
export default postRepository;
|
||||
190
src/data/repositories/interfaces/IPostRepository.ts
Normal file
190
src/data/repositories/interfaces/IPostRepository.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* 帖子 Repository 接口
|
||||
* 定义帖子数据访问的抽象
|
||||
*/
|
||||
|
||||
import type { Post, PostImage, PostStatus } from '../../../core/entities/Post';
|
||||
|
||||
// ==================== 请求/响应类型 ====================
|
||||
|
||||
/**
|
||||
* 获取帖子列表参数
|
||||
*/
|
||||
export interface GetPostsParams {
|
||||
/** 分页 - 页码(从1开始) */
|
||||
page?: number;
|
||||
/** 分页 - 每页数量 */
|
||||
pageSize?: number;
|
||||
/** 游标 - 用于无限滚动加载 */
|
||||
cursor?: string;
|
||||
/** 帖子类型筛选(可选):recommend, follow, hot, latest */
|
||||
post_type?: 'recommend' | 'follow' | 'hot' | 'latest';
|
||||
/** 社区ID过滤 */
|
||||
communityId?: string;
|
||||
/** 作者ID过滤 */
|
||||
authorId?: string;
|
||||
/** 标签过滤 */
|
||||
tags?: string[];
|
||||
/** 状态过滤 */
|
||||
status?: PostStatus;
|
||||
/** 排序字段 */
|
||||
sortBy?: 'createdAt' | 'likesCount' | 'commentsCount' | 'viewsCount';
|
||||
/** 排序方向 */
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
/** 是否只获取置顶帖子 */
|
||||
pinnedOnly?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建帖子数据
|
||||
*/
|
||||
export interface CreatePostData {
|
||||
/** 帖子标题 */
|
||||
title: string;
|
||||
/** 帖子内容 */
|
||||
content: string;
|
||||
/** 图片列表 */
|
||||
images?: PostImage[];
|
||||
/** 所属社区ID */
|
||||
communityId?: string;
|
||||
/** 标签列表 */
|
||||
tags?: string[];
|
||||
/** 帖子状态 */
|
||||
status?: PostStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新帖子数据
|
||||
*/
|
||||
export interface UpdatePostData {
|
||||
/** 帖子标题 */
|
||||
title?: string;
|
||||
/** 帖子内容 */
|
||||
content?: string;
|
||||
/** 图片列表 */
|
||||
images?: PostImage[];
|
||||
/** 标签列表 */
|
||||
tags?: string[];
|
||||
/** 帖子状态 */
|
||||
status?: PostStatus;
|
||||
/** 是否置顶 */
|
||||
isPinned?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 帖子列表结果
|
||||
*/
|
||||
export interface PostsResult {
|
||||
/** 帖子列表 */
|
||||
posts: Post[];
|
||||
/** 是否有更多数据 */
|
||||
hasMore: boolean;
|
||||
/** 下一页游标 */
|
||||
nextCursor?: string;
|
||||
/** 总数(如果支持) */
|
||||
total?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索帖子参数
|
||||
*/
|
||||
export interface SearchPostsParams {
|
||||
/** 搜索关键词 */
|
||||
keyword: string;
|
||||
/** 分页 - 页码 */
|
||||
page?: number;
|
||||
/** 分页 - 每页数量 */
|
||||
pageSize?: number;
|
||||
/** 搜索范围:标题、内容、或全部 */
|
||||
scope?: 'title' | 'content' | 'all';
|
||||
/** 社区ID过滤 */
|
||||
communityId?: string;
|
||||
}
|
||||
|
||||
// ==================== Repository 接口 ====================
|
||||
|
||||
export interface IPostRepository {
|
||||
// ==================== 帖子查询 ====================
|
||||
|
||||
/**
|
||||
* 获取帖子列表
|
||||
* @param params 查询参数
|
||||
* @returns 帖子列表结果
|
||||
*/
|
||||
getPosts(params?: GetPostsParams): Promise<PostsResult>;
|
||||
|
||||
/**
|
||||
* 获取单个帖子详情
|
||||
* @param id 帖子ID
|
||||
* @returns 帖子实体,不存在则返回null
|
||||
*/
|
||||
getPostById(id: string): Promise<Post | null>;
|
||||
|
||||
/**
|
||||
* 获取用户发布的帖子列表
|
||||
* @param userId 用户ID
|
||||
* @param params 查询参数
|
||||
* @returns 帖子列表结果
|
||||
*/
|
||||
getPostsByUser(userId: string, params?: GetPostsParams): Promise<PostsResult>;
|
||||
|
||||
/**
|
||||
* 搜索帖子
|
||||
* @param params 搜索参数
|
||||
* @returns 帖子列表结果
|
||||
*/
|
||||
searchPosts(params: SearchPostsParams): Promise<PostsResult>;
|
||||
|
||||
// ==================== 帖子操作 ====================
|
||||
|
||||
/**
|
||||
* 创建帖子
|
||||
* @param data 创建帖子数据
|
||||
* @returns 创建的帖子实体
|
||||
*/
|
||||
createPost(data: CreatePostData): Promise<Post>;
|
||||
|
||||
/**
|
||||
* 更新帖子
|
||||
* @param id 帖子ID
|
||||
* @param data 更新数据
|
||||
* @returns 更新后的帖子实体
|
||||
*/
|
||||
updatePost(id: string, data: UpdatePostData): Promise<Post>;
|
||||
|
||||
/**
|
||||
* 删除帖子
|
||||
* @param id 帖子ID
|
||||
*/
|
||||
deletePost(id: string): Promise<void>;
|
||||
|
||||
// ==================== 互动操作 ====================
|
||||
|
||||
/**
|
||||
* 点赞帖子
|
||||
* @param id 帖子ID
|
||||
* @returns 更新后的帖子实体
|
||||
*/
|
||||
likePost(id: string): Promise<Post>;
|
||||
|
||||
/**
|
||||
* 取消点赞
|
||||
* @param id 帖子ID
|
||||
* @returns 更新后的帖子实体
|
||||
*/
|
||||
unlikePost(id: string): Promise<Post>;
|
||||
|
||||
/**
|
||||
* 收藏帖子
|
||||
* @param id 帖子ID
|
||||
* @returns 更新后的帖子实体
|
||||
*/
|
||||
favoritePost(id: string): Promise<Post>;
|
||||
|
||||
/**
|
||||
* 取消收藏
|
||||
* @param id 帖子ID
|
||||
* @returns 更新后的帖子实体
|
||||
*/
|
||||
unfavoritePost(id: string): Promise<Post>;
|
||||
}
|
||||
@@ -95,3 +95,19 @@ export type { UseConnectionStateResult } from './useConnectionState';
|
||||
export { useMediaCache } from './useMediaCache';
|
||||
|
||||
export type { UseMediaCacheReturn } from './useMediaCache';
|
||||
|
||||
// ==================== 差异更新 Hooks ====================
|
||||
export { useDifferentialMessages } from './useDifferentialMessages';
|
||||
|
||||
export type {
|
||||
UseDifferentialMessagesOptions,
|
||||
UseDifferentialMessagesResult,
|
||||
} from './useDifferentialMessages';
|
||||
|
||||
export { useDifferentialPosts } from './useDifferentialPosts';
|
||||
|
||||
export type {
|
||||
UseDifferentialPostsOptions,
|
||||
UseDifferentialPostsResult,
|
||||
DiffUpdatesInfo,
|
||||
} from './useDifferentialPosts';
|
||||
|
||||
635
src/hooks/useDifferentialPosts.ts
Normal file
635
src/hooks/useDifferentialPosts.ts
Normal file
@@ -0,0 +1,635 @@
|
||||
/**
|
||||
* 帖子差异更新 Hook
|
||||
* 接收原始帖子列表,返回优化后的帖子列表,自动处理批量更新
|
||||
* 集成 ProcessPostUseCase 进行状态管理
|
||||
*/
|
||||
|
||||
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
PostIdentifier,
|
||||
PostUpdate,
|
||||
PostUpdateType,
|
||||
PostDiffConfig,
|
||||
PostDiffResult,
|
||||
} from '../infrastructure/diff/postTypes';
|
||||
import {
|
||||
PostDiffCalculator,
|
||||
createPostDiffCalculator,
|
||||
} from '../infrastructure/diff/PostDiffCalculator';
|
||||
import {
|
||||
PostUpdateBatcher,
|
||||
PostBatcherOptions,
|
||||
createPostUpdateBatcher,
|
||||
} from '../infrastructure/diff/PostUpdateBatcher';
|
||||
import { processPostUseCase } from '../core/usecases/ProcessPostUseCase';
|
||||
import type { PostUseCaseEvent, PostsState } from '../core/usecases/ProcessPostUseCase';
|
||||
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;
|
||||
/** 是否正在刷新 */
|
||||
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;
|
||||
};
|
||||
/** 差异更新信息 */
|
||||
diffUpdates: DiffUpdatesInfo;
|
||||
}
|
||||
|
||||
// ==================== Hook 实现 ====================
|
||||
|
||||
/**
|
||||
* 帖子差异更新 Hook
|
||||
* @param initialPosts 初始帖子列表(可选)
|
||||
* @param options 配置选项
|
||||
* @returns 优化后的帖子列表和相关方法
|
||||
*/
|
||||
export function useDifferentialPosts<T extends PostIdentifier = Post>(
|
||||
initialPosts: T[] = [],
|
||||
options: UseDifferentialPostsOptions<T> = {}
|
||||
): UseDifferentialPostsResult<T> {
|
||||
const {
|
||||
diffConfig,
|
||||
batcherOptions,
|
||||
enableDiff = true,
|
||||
enableBatching = true,
|
||||
maxPostCount = 10000,
|
||||
changeRatioThreshold = 0.5,
|
||||
listKey = 'default',
|
||||
autoSubscribe = true,
|
||||
} = options;
|
||||
|
||||
// ==================== 状态 ====================
|
||||
|
||||
const [posts, setPosts] = useState<T[]>(initialPosts);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [pendingUpdateCount, setPendingUpdateCount] = useState(0);
|
||||
const [diffUpdates, setDiffUpdates] = useState<DiffUpdatesInfo>({
|
||||
addedCount: 0,
|
||||
updatedCount: 0,
|
||||
deletedCount: 0,
|
||||
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);
|
||||
|
||||
// ==================== 初始化 ====================
|
||||
|
||||
// 初始化差异计算器
|
||||
useEffect(() => {
|
||||
if (enableDiff) {
|
||||
calculatorRef.current = createPostDiffCalculator<T>(diffConfig);
|
||||
}
|
||||
return () => {
|
||||
calculatorRef.current = null;
|
||||
};
|
||||
}, [enableDiff, diffConfig]);
|
||||
|
||||
// 初始化批量处理器
|
||||
useEffect(() => {
|
||||
if (enableBatching) {
|
||||
batcherRef.current = createPostUpdateBatcher(batcherOptions);
|
||||
|
||||
// 订阅批量更新
|
||||
const unsubscribe = batcherRef.current.subscribe((updates) => {
|
||||
handleBatchUpdates(updates);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
batcherRef.current?.destroy();
|
||||
batcherRef.current = null;
|
||||
};
|
||||
}
|
||||
}, [enableBatching, batcherOptions]);
|
||||
|
||||
// ==================== 批量更新处理 ====================
|
||||
|
||||
/**
|
||||
* 处理批量更新
|
||||
*/
|
||||
const handleBatchUpdates = useCallback((updates: PostUpdate[]) => {
|
||||
setIsProcessing(true);
|
||||
|
||||
try {
|
||||
setPosts((currentPosts) => {
|
||||
let newPosts = [...currentPosts];
|
||||
let addedCount = 0;
|
||||
let updatedCount = 0;
|
||||
let deletedCount = 0;
|
||||
|
||||
for (const update of updates) {
|
||||
switch (update.type) {
|
||||
case PostUpdateType.ADD: {
|
||||
const addUpdate = update as any;
|
||||
const index = addUpdate.index ?? newPosts.length;
|
||||
newPosts.splice(index, 0, addUpdate.post as T);
|
||||
addedCount++;
|
||||
break;
|
||||
}
|
||||
|
||||
case PostUpdateType.BATCH_ADD: {
|
||||
const batchAddUpdate = update as any;
|
||||
const postsToAdd = batchAddUpdate.posts || [];
|
||||
const startIndex = batchAddUpdate.startIndex ?? newPosts.length;
|
||||
newPosts.splice(startIndex, 0, ...postsToAdd as T[]);
|
||||
addedCount += postsToAdd.length;
|
||||
break;
|
||||
}
|
||||
|
||||
case PostUpdateType.UPDATE: {
|
||||
const updatePost = update as any;
|
||||
const index = newPosts.findIndex(p => p.id === updatePost.postId);
|
||||
if (index !== -1) {
|
||||
newPosts[index] = { ...newPosts[index], ...updatePost.updates };
|
||||
updatedCount++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case PostUpdateType.BATCH_UPDATE: {
|
||||
const batchUpdate = update as any;
|
||||
const updatesList = batchUpdate.updates || [];
|
||||
for (const { postId, changes } of updatesList) {
|
||||
const index = newPosts.findIndex(p => p.id === postId);
|
||||
if (index !== -1) {
|
||||
newPosts[index] = { ...newPosts[index], ...changes };
|
||||
updatedCount++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case PostUpdateType.DELETE: {
|
||||
const deleteUpdate = update as any;
|
||||
const prevLength = newPosts.length;
|
||||
newPosts = newPosts.filter(p => p.id !== deleteUpdate.postId);
|
||||
if (newPosts.length < prevLength) {
|
||||
deletedCount++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case PostUpdateType.BATCH_DELETE: {
|
||||
const batchDelete = update as any;
|
||||
const idsToDelete = new Set(batchDelete.postIds || []);
|
||||
const prevLength = newPosts.length;
|
||||
newPosts = newPosts.filter(p => !idsToDelete.has(p.id));
|
||||
deletedCount += prevLength - newPosts.length;
|
||||
break;
|
||||
}
|
||||
|
||||
case PostUpdateType.MOVE: {
|
||||
const moveUpdate = update as any;
|
||||
const fromIndex = newPosts.findIndex(p => p.id === moveUpdate.postId);
|
||||
if (fromIndex !== -1) {
|
||||
const [movedPost] = newPosts.splice(fromIndex, 1);
|
||||
newPosts.splice(moveUpdate.toIndex, 0, movedPost);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case PostUpdateType.RESET: {
|
||||
const resetUpdate = update as any;
|
||||
newPosts = resetUpdate.posts || [];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新差异统计
|
||||
if (addedCount > 0 || updatedCount > 0 || deletedCount > 0) {
|
||||
setDiffUpdates(prev => ({
|
||||
addedCount: prev.addedCount + addedCount,
|
||||
updatedCount: prev.updatedCount + updatedCount,
|
||||
deletedCount: prev.deletedCount + deletedCount,
|
||||
lastUpdateTime: Date.now(),
|
||||
}));
|
||||
}
|
||||
|
||||
return newPosts;
|
||||
});
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
setPendingUpdateCount(batcherRef.current?.getPendingCount() ?? 0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// ==================== 差异计算 ====================
|
||||
|
||||
/**
|
||||
* 计算并应用差异
|
||||
*/
|
||||
const calculateAndApplyDiff = useCallback((newPosts: T[]) => {
|
||||
const previousPosts = previousPostsRef.current;
|
||||
|
||||
// 如果帖子数量变化太大,直接重置
|
||||
const previousCount = previousPosts.length;
|
||||
const currentCount = newPosts.length;
|
||||
const changeRatio = previousCount === 0 ? 1 : Math.abs(currentCount - previousCount) / previousCount;
|
||||
|
||||
if (changeRatio > changeRatioThreshold) {
|
||||
setPosts(newPosts);
|
||||
previousPostsRef.current = newPosts;
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用差异计算
|
||||
if (enableDiff && calculatorRef.current) {
|
||||
const diff = calculatorRef.current.calculateDiff(previousPosts, newPosts);
|
||||
|
||||
if (diff.shouldReset) {
|
||||
setPosts(newPosts);
|
||||
} else {
|
||||
// 将差异转换为更新操作
|
||||
const updates: PostUpdate[] = [];
|
||||
|
||||
// 处理新增
|
||||
if (diff.added.length > 0) {
|
||||
if (diff.added.length === 1) {
|
||||
updates.push({
|
||||
type: PostUpdateType.ADD,
|
||||
post: diff.added[0],
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} else {
|
||||
updates.push({
|
||||
type: PostUpdateType.BATCH_ADD,
|
||||
posts: diff.added,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 处理更新
|
||||
if (diff.updated.length > 0) {
|
||||
if (diff.updated.length === 1) {
|
||||
updates.push({
|
||||
type: PostUpdateType.UPDATE,
|
||||
postId: diff.updated[0].post.id,
|
||||
updates: diff.updated[0].changes,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} else {
|
||||
updates.push({
|
||||
type: PostUpdateType.BATCH_UPDATE,
|
||||
updates: diff.updated.map(u => ({
|
||||
postId: u.post.id,
|
||||
changes: u.changes,
|
||||
})),
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 处理删除
|
||||
if (diff.deleted.length > 0) {
|
||||
if (diff.deleted.length === 1) {
|
||||
updates.push({
|
||||
type: PostUpdateType.DELETE,
|
||||
postId: diff.deleted[0].post.id,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} else {
|
||||
updates.push({
|
||||
type: PostUpdateType.BATCH_DELETE,
|
||||
postIds: diff.deleted.map(d => d.post.id),
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 处理移动
|
||||
if (diff.moved.length > 0) {
|
||||
for (const move of diff.moved) {
|
||||
updates.push({
|
||||
type: PostUpdateType.MOVE,
|
||||
postId: move.post.id,
|
||||
toIndex: move.toIndex,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 应用更新
|
||||
if (updates.length > 0) {
|
||||
if (enableBatching && batcherRef.current) {
|
||||
batcherRef.current.addUpdates(updates);
|
||||
} else {
|
||||
handleBatchUpdates(updates);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 不使用差异计算,直接设置
|
||||
setPosts(newPosts);
|
||||
}
|
||||
|
||||
// 更新帖子数量限制检查
|
||||
if (newPosts.length > maxPostCount) {
|
||||
console.warn(`[useDifferentialPosts] Post count (${newPosts.length}) exceeds limit (${maxPostCount})`);
|
||||
}
|
||||
|
||||
previousPostsRef.current = newPosts;
|
||||
}, [enableDiff, enableBatching, maxPostCount, changeRatioThreshold, handleBatchUpdates]);
|
||||
|
||||
// ==================== UseCase 订阅 ====================
|
||||
|
||||
/**
|
||||
* 处理 UseCase 事件
|
||||
*/
|
||||
const handleUseCaseEvent = useCallback((event: PostUseCaseEvent) => {
|
||||
switch (event.type) {
|
||||
case 'state_changed': {
|
||||
const { state } = event.payload as { key: string; state: PostsState };
|
||||
if (state) {
|
||||
setLoading(state.isLoading);
|
||||
setRefreshing(state.isRefreshing);
|
||||
setError(state.error);
|
||||
setHasMore(state.hasMore);
|
||||
|
||||
// 如果帖子列表有变化,进行差异计算
|
||||
if (state.posts && state.posts.length > 0) {
|
||||
calculateAndApplyDiff(state.posts as unknown as T[]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'posts_loaded': {
|
||||
const { posts: loadedPosts } = event.payload;
|
||||
if (loadedPosts) {
|
||||
calculateAndApplyDiff(loadedPosts as T[]);
|
||||
}
|
||||
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 { error: errorMsg } = event.payload;
|
||||
setError(errorMsg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [calculateAndApplyDiff]);
|
||||
|
||||
// 订阅 UseCase 状态变化
|
||||
useEffect(() => {
|
||||
if (autoSubscribe) {
|
||||
unsubscribeRef.current = processPostUseCase.subscribe(handleUseCaseEvent);
|
||||
|
||||
// 初始化时获取当前状态
|
||||
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);
|
||||
|
||||
return () => {
|
||||
if (unsubscribeRef.current) {
|
||||
unsubscribeRef.current();
|
||||
unsubscribeRef.current = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [autoSubscribe, listKey, handleUseCaseEvent]);
|
||||
|
||||
// ==================== 操作方法 ====================
|
||||
|
||||
/**
|
||||
* 刷新帖子列表
|
||||
*/
|
||||
const refresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
setError(null);
|
||||
try {
|
||||
await processPostUseCase.refreshPosts(listKey);
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : '刷新失败';
|
||||
setError(errorMsg);
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, [listKey]);
|
||||
|
||||
/**
|
||||
* 加载更多帖子
|
||||
*/
|
||||
const loadMore = useCallback(async () => {
|
||||
if (hasMore && !loading) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await processPostUseCase.loadMorePosts(listKey);
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : '加载更多失败';
|
||||
setError(errorMsg);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}, [listKey, hasMore, loading]);
|
||||
|
||||
/**
|
||||
* 手动刷新批量处理队列
|
||||
*/
|
||||
const flush = useCallback(() => {
|
||||
batcherRef.current?.flush();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 重置方法
|
||||
*/
|
||||
const reset = useCallback(() => {
|
||||
calculatorRef.current?.reset();
|
||||
batcherRef.current?.clearPending();
|
||||
setPosts([]);
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
setError(null);
|
||||
setHasMore(true);
|
||||
previousPostsRef.current = [];
|
||||
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 {
|
||||
totalBatches: batcherStats?.totalBatches ?? 0,
|
||||
totalUpdates: batcherStats?.totalUpdates ?? 0,
|
||||
averageBatchSize: batcherStats?.averageBatchSize ?? 0,
|
||||
};
|
||||
}, []);
|
||||
|
||||
// ==================== 定期更新待处理数量 ====================
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableBatching || !batcherRef.current) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setPendingUpdateCount(batcherRef.current?.getPendingCount() ?? 0);
|
||||
}, 50);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [enableBatching]);
|
||||
|
||||
// ==================== 返回值 ====================
|
||||
|
||||
return {
|
||||
posts,
|
||||
loading,
|
||||
refreshing,
|
||||
error,
|
||||
hasMore,
|
||||
pendingUpdateCount,
|
||||
isProcessing,
|
||||
refresh,
|
||||
loadMore,
|
||||
flush,
|
||||
reset,
|
||||
forceUpdate,
|
||||
getDiffStats,
|
||||
diffUpdates,
|
||||
};
|
||||
}
|
||||
|
||||
export default useDifferentialPosts;
|
||||
430
src/infrastructure/diff/PostDiffCalculator.ts
Normal file
430
src/infrastructure/diff/PostDiffCalculator.ts
Normal file
@@ -0,0 +1,430 @@
|
||||
/**
|
||||
* 帖子差异计算器
|
||||
* 计算两个帖子列表之间的差异,支持增量更新
|
||||
*/
|
||||
|
||||
import {
|
||||
PostIdentifier,
|
||||
PostDiffResult,
|
||||
PostDiffConfig,
|
||||
DEFAULT_POST_DIFF_CONFIG,
|
||||
PostComparator,
|
||||
PostSorter,
|
||||
PostDiffStats,
|
||||
} from './postTypes';
|
||||
|
||||
/**
|
||||
* 帖子差异计算器类
|
||||
* 用于高效计算帖子列表的变化
|
||||
*/
|
||||
export class PostDiffCalculator<T extends PostIdentifier> {
|
||||
private config: Required<PostDiffConfig>;
|
||||
private previousPosts: Map<string, T> = new Map();
|
||||
private previousOrder: string[] = [];
|
||||
private stats: PostDiffStats = {
|
||||
totalBatches: 0,
|
||||
totalUpdates: 0,
|
||||
averageBatchSize: 0,
|
||||
lastBatchTime: 0,
|
||||
addedCount: 0,
|
||||
updatedCount: 0,
|
||||
deletedCount: 0,
|
||||
};
|
||||
|
||||
constructor(config: PostDiffConfig = {}) {
|
||||
this.config = { ...DEFAULT_POST_DIFF_CONFIG, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算两个帖子列表的差异
|
||||
* @param oldList 旧帖子列表
|
||||
* @param newList 新帖子列表
|
||||
* @returns 差异结果
|
||||
*/
|
||||
calculateDiff(oldList: T[], newList: T[]): PostDiffResult<T> {
|
||||
const result: PostDiffResult<T> = {
|
||||
added: [],
|
||||
updated: [],
|
||||
deleted: [],
|
||||
moved: [],
|
||||
unchanged: [],
|
||||
shouldReset: false,
|
||||
};
|
||||
|
||||
// 如果旧列表为空,直接返回新列表作为新增
|
||||
if (oldList.length === 0) {
|
||||
result.added = [...newList];
|
||||
this.updateCache(newList);
|
||||
this.updateStats(0, newList.length, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 如果新列表为空,表示全部删除
|
||||
if (newList.length === 0) {
|
||||
result.deleted = oldList.map((post, index) => ({ post, index }));
|
||||
this.clearCache();
|
||||
this.updateStats(0, 0, oldList.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 检查是否需要重置(变化太大)
|
||||
const changeRatio = this.calculateChangeRatio(oldList, newList);
|
||||
if (changeRatio > this.config.resetThreshold) {
|
||||
result.shouldReset = true;
|
||||
result.added = [...newList];
|
||||
this.updateCache(newList);
|
||||
this.updateStats(0, newList.length, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 构建旧列表的索引映射
|
||||
const oldIndexMap = new Map<string, number>();
|
||||
oldList.forEach((post, index) => {
|
||||
oldIndexMap.set(post.id, index);
|
||||
});
|
||||
|
||||
// 构建新列表的索引映射
|
||||
const newIndexMap = new Map<string, number>();
|
||||
newList.forEach((post, index) => {
|
||||
newIndexMap.set(post.id, index);
|
||||
});
|
||||
|
||||
// 检测新增、更新和未变更的帖子
|
||||
const processedIds = new Set<string>();
|
||||
let addedCount = 0;
|
||||
let updatedCount = 0;
|
||||
|
||||
for (let newIndex = 0; newIndex < newList.length; newIndex++) {
|
||||
const newPost = newList[newIndex];
|
||||
const oldIndex = oldIndexMap.get(newPost.id);
|
||||
|
||||
if (oldIndex === undefined) {
|
||||
// 新增帖子
|
||||
result.added.push(newPost);
|
||||
addedCount++;
|
||||
} else {
|
||||
const oldPost = oldList[oldIndex];
|
||||
processedIds.add(newPost.id);
|
||||
|
||||
// 检测是否有更新
|
||||
const changes = this.detectChanges(oldPost, newPost);
|
||||
if (changes && Object.keys(changes).length > 0) {
|
||||
result.updated.push({
|
||||
post: newPost,
|
||||
index: oldIndex,
|
||||
changes,
|
||||
});
|
||||
updatedCount++;
|
||||
} else {
|
||||
result.unchanged.push(newPost);
|
||||
}
|
||||
|
||||
// 检测是否移动
|
||||
if (oldIndex !== newIndex) {
|
||||
result.moved.push({
|
||||
post: newPost,
|
||||
fromIndex: oldIndex,
|
||||
toIndex: newIndex,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检测删除的帖子
|
||||
let deletedCount = 0;
|
||||
for (let oldIndex = 0; oldIndex < oldList.length; oldIndex++) {
|
||||
const oldPost = oldList[oldIndex];
|
||||
if (!processedIds.has(oldPost.id)) {
|
||||
result.deleted.push({ post: oldPost, index: oldIndex });
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
this.updateCache(newList);
|
||||
this.updateStats(addedCount, updatedCount, deletedCount);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算增量差异(基于缓存的上次状态)
|
||||
* @param newList 新帖子列表
|
||||
* @returns 差异结果
|
||||
*/
|
||||
calculateIncrementalDiff(newList: T[]): PostDiffResult<T> {
|
||||
const oldList = Array.from(this.previousPosts.values());
|
||||
// 按照之前的顺序排序
|
||||
oldList.sort((a, b) => {
|
||||
const indexA = this.previousOrder.indexOf(a.id);
|
||||
const indexB = this.previousOrder.indexOf(b.id);
|
||||
return indexA - indexB;
|
||||
});
|
||||
return this.calculateDiff(oldList, newList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算单个帖子的变化
|
||||
* @param oldPost 旧帖子
|
||||
* @param newPost 新帖子
|
||||
* @returns 变化的字段
|
||||
*/
|
||||
detectChanges(oldPost: T, newPost: T): Partial<T> | null {
|
||||
const changes: Partial<T> = {};
|
||||
let hasChanges = false;
|
||||
|
||||
// 如果配置了追踪特定字段,只检测这些字段
|
||||
const fieldsToCheck = this.config.detectPartialUpdates
|
||||
? this.config.trackedFields
|
||||
: (Object.keys(newPost) as Array<keyof T>);
|
||||
|
||||
for (const key of fieldsToCheck) {
|
||||
if (key === 'id') continue; // ID 不变
|
||||
|
||||
const oldValue = (oldPost as any)[key];
|
||||
const newValue = (newPost as any)[key];
|
||||
|
||||
// 深度比较
|
||||
if (this.hasValueChanged(oldValue, newValue)) {
|
||||
(changes as any)[key] = newValue;
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanges ? changes : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测值是否变化
|
||||
* @param oldValue 旧值
|
||||
* @param newValue 新值
|
||||
* @returns 是否变化
|
||||
*/
|
||||
private hasValueChanged(oldValue: any, newValue: any): boolean {
|
||||
// 简单类型直接比较
|
||||
if (typeof oldValue !== 'object' || oldValue === null || newValue === null) {
|
||||
return oldValue !== newValue;
|
||||
}
|
||||
|
||||
// 数组比较
|
||||
if (Array.isArray(oldValue) && Array.isArray(newValue)) {
|
||||
if (oldValue.length !== newValue.length) return true;
|
||||
return JSON.stringify(oldValue) !== JSON.stringify(newValue);
|
||||
}
|
||||
|
||||
// 对象比较
|
||||
return JSON.stringify(oldValue) !== JSON.stringify(newValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算变化比例
|
||||
* @param oldList 旧列表
|
||||
* @param newList 新列表
|
||||
* @returns 变化比例(0-1)
|
||||
*/
|
||||
private calculateChangeRatio(oldList: T[], newList: T[]): number {
|
||||
if (oldList.length === 0 && newList.length === 0) return 0;
|
||||
if (oldList.length === 0 || newList.length === 0) return 1;
|
||||
|
||||
const oldIds = new Set(oldList.map(p => p.id));
|
||||
const newIds = new Set(newList.map(p => p.id));
|
||||
|
||||
let commonCount = 0;
|
||||
oldIds.forEach(id => {
|
||||
if (newIds.has(id)) {
|
||||
commonCount++;
|
||||
}
|
||||
});
|
||||
|
||||
const maxLength = Math.max(oldList.length, newList.length);
|
||||
return 1 - commonCount / maxLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新缓存
|
||||
* @param posts 帖子列表
|
||||
*/
|
||||
private updateCache(posts: T[]): void {
|
||||
this.previousPosts.clear();
|
||||
this.previousOrder = [];
|
||||
|
||||
for (const post of posts) {
|
||||
this.previousPosts.set(post.id, post);
|
||||
this.previousOrder.push(post.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
private clearCache(): void {
|
||||
this.previousPosts.clear();
|
||||
this.previousOrder = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新统计信息
|
||||
*/
|
||||
private updateStats(addedCount: number, updatedCount: number, deletedCount: number): void {
|
||||
this.stats.totalBatches++;
|
||||
const totalChanges = addedCount + updatedCount + deletedCount;
|
||||
this.stats.totalUpdates += totalChanges;
|
||||
this.stats.averageBatchSize = this.stats.totalUpdates / this.stats.totalBatches;
|
||||
this.stats.lastBatchTime = Date.now();
|
||||
this.stats.addedCount += addedCount;
|
||||
this.stats.updatedCount += updatedCount;
|
||||
this.stats.deletedCount += deletedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置配置
|
||||
* @param config 配置
|
||||
*/
|
||||
setConfig(config: Partial<PostDiffConfig>): void {
|
||||
this.config = { ...this.config, ...config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前配置
|
||||
* @returns 当前配置
|
||||
*/
|
||||
getConfig(): Required<PostDiffConfig> {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置计算器状态
|
||||
*/
|
||||
reset(): void {
|
||||
this.clearCache();
|
||||
this.stats = {
|
||||
totalBatches: 0,
|
||||
totalUpdates: 0,
|
||||
averageBatchSize: 0,
|
||||
lastBatchTime: 0,
|
||||
addedCount: 0,
|
||||
updatedCount: 0,
|
||||
deletedCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存的帖子数量
|
||||
* @returns 缓存数量
|
||||
*/
|
||||
getCachedCount(): number {
|
||||
return this.previousPosts.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查帖子是否在缓存中
|
||||
* @param postId 帖子 ID
|
||||
* @returns 是否存在
|
||||
*/
|
||||
hasPost(postId: string): boolean {
|
||||
return this.previousPosts.has(postId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存中的帖子
|
||||
* @param postId 帖子 ID
|
||||
* @returns 帖子或 undefined
|
||||
*/
|
||||
getCachedPost(postId: string): T | undefined {
|
||||
return this.previousPosts.get(postId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
* @returns 统计信息
|
||||
*/
|
||||
getStats(): PostDiffStats {
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量检测帖子变化
|
||||
* @param posts 要检测的帖子列表
|
||||
* @returns 变化的帖子列表
|
||||
*/
|
||||
batchDetectChanges(posts: T[]): Array<{ post: T; changes: Partial<T> }> {
|
||||
const results: Array<{ post: T; changes: Partial<T> }> = [];
|
||||
|
||||
for (const post of posts) {
|
||||
const cachedPost = this.previousPosts.get(post.id);
|
||||
if (cachedPost) {
|
||||
const changes = this.detectChanges(cachedPost, post);
|
||||
if (changes && Object.keys(changes).length > 0) {
|
||||
results.push({ post, changes });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较两个帖子列表是否相同
|
||||
* @param list1 列表1
|
||||
* @param list2 列表2
|
||||
* @returns 是否相同
|
||||
*/
|
||||
areEqual(list1: T[], list2: T[]): boolean {
|
||||
if (list1.length !== list2.length) return false;
|
||||
|
||||
for (let i = 0; i < list1.length; i++) {
|
||||
if (list1[i].id !== list2[i].id) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取两个帖子列表的交集ID
|
||||
* @param list1 列表1
|
||||
* @param list2 列表2
|
||||
* @returns 交集ID集合
|
||||
*/
|
||||
getIntersection(list1: T[], list2: T[]): Set<string> {
|
||||
const ids1 = new Set(list1.map(p => p.id));
|
||||
const ids2 = new Set(list2.map(p => p.id));
|
||||
const intersection = new Set<string>();
|
||||
|
||||
ids1.forEach(id => {
|
||||
if (ids2.has(id)) {
|
||||
intersection.add(id);
|
||||
}
|
||||
});
|
||||
|
||||
return intersection;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取两个帖子列表的差集ID
|
||||
* @param list1 列表1
|
||||
* @param list2 列表2
|
||||
* @returns 差集ID集合(在list1中但不在list2中)
|
||||
*/
|
||||
getDifference(list1: T[], list2: T[]): Set<string> {
|
||||
const ids1 = new Set(list1.map(p => p.id));
|
||||
const ids2 = new Set(list2.map(p => p.id));
|
||||
const difference = new Set<string>();
|
||||
|
||||
ids1.forEach(id => {
|
||||
if (!ids2.has(id)) {
|
||||
difference.add(id);
|
||||
}
|
||||
});
|
||||
|
||||
return difference;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建帖子差异计算器实例的工厂函数
|
||||
* @param config 配置
|
||||
* @returns PostDiffCalculator 实例
|
||||
*/
|
||||
export function createPostDiffCalculator<T extends PostIdentifier>(
|
||||
config?: PostDiffConfig
|
||||
): PostDiffCalculator<T> {
|
||||
return new PostDiffCalculator<T>(config);
|
||||
}
|
||||
505
src/infrastructure/diff/PostUpdateBatcher.ts
Normal file
505
src/infrastructure/diff/PostUpdateBatcher.ts
Normal file
@@ -0,0 +1,505 @@
|
||||
/**
|
||||
* 帖子更新批量处理器
|
||||
* 收集更新操作并按批处理,减少 UI 更新次数
|
||||
*/
|
||||
|
||||
import {
|
||||
PostUpdate,
|
||||
PostUpdateType,
|
||||
PostBatchUpdateListener,
|
||||
PostDiffConfig,
|
||||
DEFAULT_POST_DIFF_CONFIG,
|
||||
PostIdentifier,
|
||||
PostUpdateBatch,
|
||||
} from './postTypes';
|
||||
|
||||
/**
|
||||
* 批量处理器配置选项
|
||||
*/
|
||||
export interface PostBatcherOptions {
|
||||
/** 批量处理间隔(毫秒),默认 16ms(约 60fps) */
|
||||
batchInterval?: number;
|
||||
/** 最大批量大小,默认 50 */
|
||||
maxBatchSize?: number;
|
||||
/** 是否启用去重,默认 true */
|
||||
enableDeduplication?: boolean;
|
||||
/** 是否合并相同帖子的多次更新,默认 true */
|
||||
enableMerge?: boolean;
|
||||
/** 最大等待时间(毫秒),超过此时间强制刷新,默认 100ms */
|
||||
maxWaitTime?: number;
|
||||
/** 是否自动启动批量处理,默认 true */
|
||||
autoStart?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认批量处理器配置
|
||||
*/
|
||||
export const DEFAULT_POST_BATCHER_OPTIONS: Required<PostBatcherOptions> = {
|
||||
batchInterval: 16,
|
||||
maxBatchSize: 50,
|
||||
enableDeduplication: true,
|
||||
enableMerge: true,
|
||||
maxWaitTime: 100,
|
||||
autoStart: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* 帖子更新批量处理器类
|
||||
*/
|
||||
export class PostUpdateBatcher {
|
||||
/** 待处理的更新队列 */
|
||||
private pendingUpdates: Map<string, PostUpdate> = new Map();
|
||||
/** 批量处理定时器 */
|
||||
private batchTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
/** 最大等待时间定时器 */
|
||||
private maxWaitTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
/** 监听器集合 */
|
||||
private listeners: Set<PostBatchUpdateListener> = new Set();
|
||||
/** 是否正在批处理中 */
|
||||
private isProcessing: boolean = false;
|
||||
/** 配置选项 */
|
||||
private options: Required<PostBatcherOptions>;
|
||||
/** 处理器是否已启动 */
|
||||
private isStarted: boolean = false;
|
||||
/** 批量处理统计信息 */
|
||||
private stats = {
|
||||
totalBatches: 0,
|
||||
totalUpdates: 0,
|
||||
averageBatchSize: 0,
|
||||
lastBatchTime: 0,
|
||||
};
|
||||
/** 批次 ID 计数器 */
|
||||
private batchIdCounter: number = 0;
|
||||
|
||||
constructor(options: PostBatcherOptions = {}) {
|
||||
this.options = { ...DEFAULT_POST_BATCHER_OPTIONS, ...options };
|
||||
if (this.options.autoStart) {
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动批量处理器
|
||||
*/
|
||||
start(): void {
|
||||
if (this.isStarted) return;
|
||||
this.isStarted = true;
|
||||
this.scheduleBatch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止批量处理器
|
||||
*/
|
||||
stop(): void {
|
||||
this.isStarted = false;
|
||||
this.clearTimers();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加更新操作到批量队列
|
||||
* @param update 帖子更新操作
|
||||
*/
|
||||
addUpdate(update: PostUpdate): void {
|
||||
const key = this.generateUpdateKey(update);
|
||||
|
||||
// 去重:如果已有相同帖子 ID 的待处理更新,进行合并
|
||||
if (this.options.enableDeduplication && this.pendingUpdates.has(key)) {
|
||||
const existing = this.pendingUpdates.get(key)!;
|
||||
|
||||
if (this.options.enableMerge) {
|
||||
const merged = this.mergeUpdates(existing, update);
|
||||
if (merged) {
|
||||
this.pendingUpdates.set(key, merged);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.pendingUpdates.set(key, update);
|
||||
|
||||
// 如果达到最大批量大小,立即刷新
|
||||
if (this.pendingUpdates.size >= this.options.maxBatchSize) {
|
||||
this.flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加多个更新操作
|
||||
* @param updates 更新操作数组
|
||||
*/
|
||||
addUpdates(updates: PostUpdate[]): void {
|
||||
for (const update of updates) {
|
||||
this.addUpdate(update);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即刷新所有待处理更新
|
||||
* @returns 处理的更新数组
|
||||
*/
|
||||
flush(): PostUpdate[] {
|
||||
if (this.pendingUpdates.size === 0 || this.isProcessing) {
|
||||
return [];
|
||||
}
|
||||
|
||||
this.isProcessing = true;
|
||||
this.clearTimers();
|
||||
|
||||
const updates = Array.from(this.pendingUpdates.values());
|
||||
this.pendingUpdates.clear();
|
||||
|
||||
// 更新统计信息
|
||||
this.stats.totalBatches++;
|
||||
this.stats.totalUpdates += updates.length;
|
||||
this.stats.averageBatchSize = this.stats.totalUpdates / this.stats.totalBatches;
|
||||
this.stats.lastBatchTime = Date.now();
|
||||
|
||||
// 通知监听器
|
||||
this.notifyListeners(updates);
|
||||
|
||||
this.isProcessing = false;
|
||||
|
||||
// 重新调度定时器
|
||||
this.scheduleBatch();
|
||||
|
||||
return updates;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新并返回批次对象
|
||||
* @returns 批次对象或 null
|
||||
*/
|
||||
flushAsBatch(): PostUpdateBatch | null {
|
||||
const updates = this.flush();
|
||||
if (updates.length === 0) return null;
|
||||
|
||||
return {
|
||||
batchId: `batch_${++this.batchIdCounter}_${Date.now()}`,
|
||||
updates,
|
||||
createdAt: Date.now(),
|
||||
processed: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅批量更新事件
|
||||
* @param listener 监听器函数
|
||||
* @returns 取消订阅函数
|
||||
*/
|
||||
subscribe(listener: PostBatchUpdateListener): () => void {
|
||||
this.listeners.add(listener);
|
||||
return () => {
|
||||
this.listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订阅
|
||||
* @param listener 监听器函数
|
||||
*/
|
||||
unsubscribe(listener: PostBatchUpdateListener): void {
|
||||
this.listeners.delete(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取待处理更新数量
|
||||
* @returns 待处理数量
|
||||
*/
|
||||
getPendingCount(): number {
|
||||
return this.pendingUpdates.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
* @returns 统计信息
|
||||
*/
|
||||
getStats() {
|
||||
return { ...this.stats };
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有待处理更新
|
||||
* @returns 是否有待处理更新
|
||||
*/
|
||||
hasPendingUpdates(): boolean {
|
||||
return this.pendingUpdates.size > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有待处理更新
|
||||
*/
|
||||
clearPending(): void {
|
||||
this.pendingUpdates.clear();
|
||||
this.clearTimers();
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁批量处理器
|
||||
*/
|
||||
destroy(): void {
|
||||
this.stop();
|
||||
this.clearPending();
|
||||
this.listeners.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成更新键值
|
||||
* @param update 更新操作
|
||||
* @returns 键值
|
||||
*/
|
||||
private generateUpdateKey(update: PostUpdate): string {
|
||||
// 根据更新类型生成唯一键
|
||||
const updateType = (update as PostUpdate).type;
|
||||
switch (updateType) {
|
||||
case PostUpdateType.ADD:
|
||||
case PostUpdateType.BATCH_ADD:
|
||||
return `${updateType}_${(update as any).timestamp}`;
|
||||
case PostUpdateType.UPDATE:
|
||||
case PostUpdateType.BATCH_UPDATE:
|
||||
return `${updateType}_${this.extractPostId(update)}`;
|
||||
case PostUpdateType.DELETE:
|
||||
case PostUpdateType.BATCH_DELETE:
|
||||
return `${updateType}_${this.extractPostId(update)}`;
|
||||
case PostUpdateType.MOVE:
|
||||
return `${updateType}_${(update as any).postId}`;
|
||||
case PostUpdateType.RESET:
|
||||
return `${updateType}_${(update as any).timestamp}`;
|
||||
default:
|
||||
// 兜底处理未知类型
|
||||
return `unknown_${Date.now()}_${Math.random()}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取帖子 ID
|
||||
* @param update 更新操作
|
||||
* @returns 帖子 ID 或空字符串
|
||||
*/
|
||||
private extractPostId(update: PostUpdate): string {
|
||||
const payload = (update as any).payload;
|
||||
if (payload) {
|
||||
if (payload.postId) return payload.postId;
|
||||
if (payload.postIds) return payload.postIds.join(',');
|
||||
}
|
||||
// 直接从更新对象提取
|
||||
if ((update as any).postId) return (update as any).postId;
|
||||
if ((update as any).postIds) return (update as any).postIds.join(',');
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并更新操作
|
||||
* @param existing 已有更新
|
||||
* @param incoming 新更新
|
||||
* @returns 合并后的更新或 null(如果无法合并)
|
||||
*/
|
||||
private mergeUpdates(existing: PostUpdate, incoming: PostUpdate): PostUpdate | null {
|
||||
// 只有同类型的更新才能合并
|
||||
if (existing.type !== incoming.type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (existing.type) {
|
||||
case PostUpdateType.UPDATE:
|
||||
// 合并更新字段
|
||||
return {
|
||||
...existing,
|
||||
payload: {
|
||||
...(existing as any).payload,
|
||||
updates: {
|
||||
...(existing as any).payload?.updates,
|
||||
...(incoming as any).payload?.updates,
|
||||
},
|
||||
},
|
||||
timestamp: incoming.timestamp,
|
||||
};
|
||||
|
||||
case PostUpdateType.BATCH_UPDATE:
|
||||
// 合并批量更新
|
||||
const existingUpdates = (existing as any).payload?.updates || [];
|
||||
const incomingUpdates = (incoming as any).payload?.updates || [];
|
||||
const updatesMap = new Map<string, any>();
|
||||
|
||||
for (const update of [...existingUpdates, ...incomingUpdates]) {
|
||||
const existing = updatesMap.get(update.postId);
|
||||
if (existing) {
|
||||
existing.changes = { ...existing.changes, ...update.changes };
|
||||
} else {
|
||||
updatesMap.set(update.postId, { ...update });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...existing,
|
||||
payload: {
|
||||
...(existing as any).payload,
|
||||
updates: Array.from(updatesMap.values()),
|
||||
},
|
||||
timestamp: incoming.timestamp,
|
||||
};
|
||||
|
||||
case PostUpdateType.BATCH_DELETE:
|
||||
// 合并删除 ID 列表
|
||||
const existingIds = new Set((existing as any).payload?.postIds || []);
|
||||
const incomingIds = (incoming as any).payload?.postIds || [];
|
||||
for (const id of incomingIds) {
|
||||
existingIds.add(id);
|
||||
}
|
||||
return {
|
||||
...existing,
|
||||
payload: {
|
||||
...(existing as any).payload,
|
||||
postIds: Array.from(existingIds),
|
||||
},
|
||||
timestamp: incoming.timestamp,
|
||||
};
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度批量处理
|
||||
*/
|
||||
private scheduleBatch(): void {
|
||||
if (!this.isStarted) return;
|
||||
|
||||
this.clearTimers();
|
||||
|
||||
// 设置最大等待时间定时器
|
||||
this.maxWaitTimer = setTimeout(() => {
|
||||
this.flush();
|
||||
}, this.options.maxWaitTime);
|
||||
|
||||
// 设置批量处理定时器
|
||||
this.batchTimer = setTimeout(() => {
|
||||
this.flush();
|
||||
}, this.options.batchInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有定时器
|
||||
*/
|
||||
private clearTimers(): void {
|
||||
if (this.batchTimer) {
|
||||
clearTimeout(this.batchTimer);
|
||||
this.batchTimer = null;
|
||||
}
|
||||
if (this.maxWaitTimer) {
|
||||
clearTimeout(this.maxWaitTimer);
|
||||
this.maxWaitTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知监听器
|
||||
* @param updates 更新数组
|
||||
*/
|
||||
private notifyListeners(updates: PostUpdate[]): void {
|
||||
if (this.listeners.size === 0) return;
|
||||
|
||||
// 转换为数组以支持迭代
|
||||
const listenersArray = Array.from(this.listeners);
|
||||
for (const listener of listenersArray) {
|
||||
try {
|
||||
listener(updates);
|
||||
} catch (error) {
|
||||
console.error('Error in post batch update listener:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 便捷方法 ====================
|
||||
|
||||
/**
|
||||
* 添加帖子新增操作
|
||||
*/
|
||||
addPost(post: PostIdentifier, index?: number): void {
|
||||
this.addUpdate({
|
||||
type: PostUpdateType.ADD,
|
||||
post,
|
||||
index,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加帖子更新操作
|
||||
*/
|
||||
updatePost(postId: string, updates: Partial<PostIdentifier>): void {
|
||||
this.addUpdate({
|
||||
type: PostUpdateType.UPDATE,
|
||||
postId,
|
||||
updates,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加帖子删除操作
|
||||
*/
|
||||
deletePost(postId: string): void {
|
||||
this.addUpdate({
|
||||
type: PostUpdateType.DELETE,
|
||||
postId,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量添加帖子
|
||||
*/
|
||||
batchAddPosts(posts: PostIdentifier[], startIndex?: number): void {
|
||||
this.addUpdate({
|
||||
type: PostUpdateType.BATCH_ADD,
|
||||
posts,
|
||||
startIndex,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新帖子
|
||||
*/
|
||||
batchUpdatePosts(updates: Array<{ postId: string; changes: Partial<PostIdentifier> }>): void {
|
||||
this.addUpdate({
|
||||
type: PostUpdateType.BATCH_UPDATE,
|
||||
updates,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除帖子
|
||||
*/
|
||||
batchDeletePosts(postIds: string[]): void {
|
||||
this.addUpdate({
|
||||
type: PostUpdateType.BATCH_DELETE,
|
||||
postIds,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置帖子列表
|
||||
*/
|
||||
resetPosts(posts: PostIdentifier[]): void {
|
||||
this.addUpdate({
|
||||
type: PostUpdateType.RESET,
|
||||
posts,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建帖子批量处理器的工厂函数
|
||||
* @param options 配置选项
|
||||
* @returns PostUpdateBatcher 实例
|
||||
*/
|
||||
export function createPostUpdateBatcher(
|
||||
options?: PostBatcherOptions
|
||||
): PostUpdateBatcher {
|
||||
return new PostUpdateBatcher(options);
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
/**
|
||||
* 差异更新模块
|
||||
* 提供消息列表的增量更新功能,减少 UI 重渲染次数
|
||||
* 提供消息列表和帖子列表的增量更新功能,减少 UI 重渲染次数
|
||||
*/
|
||||
|
||||
// ==================== Message 相关 ====================
|
||||
|
||||
// 类型定义
|
||||
export {
|
||||
MessageUpdateType,
|
||||
@@ -38,3 +40,45 @@ export {
|
||||
BatcherOptions,
|
||||
DEFAULT_BATCHER_OPTIONS,
|
||||
} from './MessageUpdateBatcher';
|
||||
|
||||
// ==================== Post 相关 ====================
|
||||
|
||||
// 类型定义
|
||||
export {
|
||||
PostChangeType,
|
||||
PostUpdateType,
|
||||
PostIdentifier,
|
||||
PostChange,
|
||||
PostDiffResult,
|
||||
BasePostUpdate,
|
||||
AddPostUpdate,
|
||||
UpdatePostUpdate,
|
||||
DeletePostUpdate,
|
||||
MovePostUpdate,
|
||||
BatchAddPostUpdate,
|
||||
BatchUpdatePostUpdate,
|
||||
BatchDeletePostUpdate,
|
||||
ResetPostUpdate,
|
||||
PostUpdate,
|
||||
PostUpdateBatch,
|
||||
PostDiffConfig,
|
||||
PostBatchUpdateListener,
|
||||
PostComparator,
|
||||
PostSorter,
|
||||
PostDiffStats,
|
||||
DEFAULT_POST_DIFF_CONFIG,
|
||||
} from './postTypes';
|
||||
|
||||
// 差异计算器
|
||||
export {
|
||||
PostDiffCalculator,
|
||||
createPostDiffCalculator,
|
||||
} from './PostDiffCalculator';
|
||||
|
||||
// 批量处理器
|
||||
export {
|
||||
PostUpdateBatcher,
|
||||
createPostUpdateBatcher,
|
||||
PostBatcherOptions,
|
||||
DEFAULT_POST_BATCHER_OPTIONS,
|
||||
} from './PostUpdateBatcher';
|
||||
|
||||
325
src/infrastructure/diff/postTypes.ts
Normal file
325
src/infrastructure/diff/postTypes.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* Post差异更新类型定义
|
||||
* 用于帖子列表的增量更新,减少 UI 重渲染次数
|
||||
*/
|
||||
|
||||
import { Post } from '../../core/entities/Post';
|
||||
|
||||
// ==================== 变化类型枚举 ====================
|
||||
|
||||
/**
|
||||
* 帖子变化类型枚举
|
||||
*/
|
||||
export enum PostChangeType {
|
||||
/** 新增帖子 */
|
||||
ADDED = 'added',
|
||||
/** 更新帖子 */
|
||||
UPDATED = 'updated',
|
||||
/** 删除帖子 */
|
||||
DELETED = 'deleted',
|
||||
}
|
||||
|
||||
/**
|
||||
* 帖子更新类型枚举
|
||||
*/
|
||||
export enum PostUpdateType {
|
||||
/** 添加单条帖子 */
|
||||
ADD = 'ADD',
|
||||
/** 更新单条帖子 */
|
||||
UPDATE = 'UPDATE',
|
||||
/** 删除单条帖子 */
|
||||
DELETE = 'DELETE',
|
||||
/** 移动帖子位置 */
|
||||
MOVE = 'MOVE',
|
||||
/** 批量添加 */
|
||||
BATCH_ADD = 'BATCH_ADD',
|
||||
/** 批量更新 */
|
||||
BATCH_UPDATE = 'BATCH_UPDATE',
|
||||
/** 批量删除 */
|
||||
BATCH_DELETE = 'BATCH_DELETE',
|
||||
/** 重置整个列表 */
|
||||
RESET = 'RESET',
|
||||
}
|
||||
|
||||
// ==================== 基础接口 ====================
|
||||
|
||||
/**
|
||||
* 帖子唯一标识接口
|
||||
*/
|
||||
export interface PostIdentifier {
|
||||
/** 帖子唯一 ID */
|
||||
id: string;
|
||||
/** 帖子创建时间(可选,用于排序) */
|
||||
createdAt?: string;
|
||||
/** 更新时间(可选,用于检测变化) */
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个帖子变化记录
|
||||
*/
|
||||
export interface PostChange<T extends PostIdentifier = Post> {
|
||||
/** 变化类型 */
|
||||
type: PostChangeType;
|
||||
/** 帖子数据 */
|
||||
post: T;
|
||||
/** 变化前的索引位置(用于删除和移动) */
|
||||
oldIndex?: number;
|
||||
/** 变化后的索引位置(用于新增和移动) */
|
||||
newIndex?: number;
|
||||
/** 变化的字段(用于更新) */
|
||||
changes?: Partial<T>;
|
||||
}
|
||||
|
||||
// ==================== 差异结果接口 ====================
|
||||
|
||||
/**
|
||||
* Post差异计算结果接口
|
||||
*/
|
||||
export interface PostDiffResult<T extends PostIdentifier = Post> {
|
||||
/** 新增的帖子 */
|
||||
added: T[];
|
||||
/** 更新的帖子(包含变化详情) */
|
||||
updated: Array<{
|
||||
post: T;
|
||||
index: number;
|
||||
changes: Partial<T>;
|
||||
}>;
|
||||
/** 删除的帖子 */
|
||||
deleted: Array<{
|
||||
post: T;
|
||||
index: number;
|
||||
}>;
|
||||
/** 移动的帖子 */
|
||||
moved: Array<{
|
||||
post: T;
|
||||
fromIndex: number;
|
||||
toIndex: number;
|
||||
}>;
|
||||
/** 未变更的帖子 */
|
||||
unchanged: T[];
|
||||
/** 是否需要重置(变化比例过大时) */
|
||||
shouldReset: boolean;
|
||||
}
|
||||
|
||||
// ==================== 更新操作接口 ====================
|
||||
|
||||
/**
|
||||
* 基础帖子更新操作接口
|
||||
*/
|
||||
export interface BasePostUpdate {
|
||||
/** 更新类型 */
|
||||
type: PostUpdateType;
|
||||
/** 更新时间戳 */
|
||||
timestamp: number;
|
||||
/** 社区 ID(可选) */
|
||||
communityId?: string;
|
||||
/** 载荷数据(可选,用于合并更新) */
|
||||
payload?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加单条帖子更新
|
||||
*/
|
||||
export interface AddPostUpdate extends BasePostUpdate {
|
||||
type: PostUpdateType.ADD;
|
||||
/** 帖子数据 */
|
||||
post: PostIdentifier;
|
||||
/** 插入位置索引(可选,默认追加到末尾) */
|
||||
index?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新单条帖子更新
|
||||
*/
|
||||
export interface UpdatePostUpdate extends BasePostUpdate {
|
||||
type: PostUpdateType.UPDATE;
|
||||
/** 帖子 ID */
|
||||
postId: string;
|
||||
/** 更新的字段 */
|
||||
updates: Partial<PostIdentifier>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除单条帖子更新
|
||||
*/
|
||||
export interface DeletePostUpdate extends BasePostUpdate {
|
||||
type: PostUpdateType.DELETE;
|
||||
/** 帖子 ID */
|
||||
postId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动帖子更新
|
||||
*/
|
||||
export interface MovePostUpdate extends BasePostUpdate {
|
||||
type: PostUpdateType.MOVE;
|
||||
/** 帖子 ID */
|
||||
postId: string;
|
||||
/** 目标位置索引 */
|
||||
toIndex: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量添加帖子更新
|
||||
*/
|
||||
export interface BatchAddPostUpdate extends BasePostUpdate {
|
||||
type: PostUpdateType.BATCH_ADD;
|
||||
/** 帖子列表 */
|
||||
posts: PostIdentifier[];
|
||||
/** 插入位置索引(可选,默认追加到末尾) */
|
||||
startIndex?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新帖子更新
|
||||
*/
|
||||
export interface BatchUpdatePostUpdate extends BasePostUpdate {
|
||||
type: PostUpdateType.BATCH_UPDATE;
|
||||
/** 批量更新项 */
|
||||
updates: Array<{
|
||||
postId: string;
|
||||
changes: Partial<PostIdentifier>;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除帖子更新
|
||||
*/
|
||||
export interface BatchDeletePostUpdate extends BasePostUpdate {
|
||||
type: PostUpdateType.BATCH_DELETE;
|
||||
/** 要删除的帖子 ID 列表 */
|
||||
postIds: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置帖子列表更新
|
||||
*/
|
||||
export interface ResetPostUpdate extends BasePostUpdate {
|
||||
type: PostUpdateType.RESET;
|
||||
/** 新的帖子列表 */
|
||||
posts: PostIdentifier[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 帖子更新联合类型
|
||||
*/
|
||||
export type PostUpdate =
|
||||
| AddPostUpdate
|
||||
| UpdatePostUpdate
|
||||
| DeletePostUpdate
|
||||
| MovePostUpdate
|
||||
| BatchAddPostUpdate
|
||||
| BatchUpdatePostUpdate
|
||||
| BatchDeletePostUpdate
|
||||
| ResetPostUpdate;
|
||||
|
||||
// ==================== 批量更新接口 ====================
|
||||
|
||||
/**
|
||||
* Post批量更新类型
|
||||
*/
|
||||
export interface PostUpdateBatch {
|
||||
/** 批次 ID */
|
||||
batchId: string;
|
||||
/** 更新列表 */
|
||||
updates: PostUpdate[];
|
||||
/** 创建时间戳 */
|
||||
createdAt: number;
|
||||
/** 是否已处理 */
|
||||
processed: boolean;
|
||||
}
|
||||
|
||||
// ==================== 配置接口 ====================
|
||||
|
||||
/**
|
||||
* Post差异配置接口
|
||||
*/
|
||||
export interface PostDiffConfig {
|
||||
/** 批量处理间隔(毫秒),默认 16ms(60fps) */
|
||||
batchInterval?: number;
|
||||
/** 最大批量大小,默认 50 */
|
||||
maxBatchSize?: number;
|
||||
/** 是否启用去重,默认 true */
|
||||
enableDeduplication?: boolean;
|
||||
/** 是否合并相同帖子的多次更新,默认 true */
|
||||
enableMerge?: boolean;
|
||||
/** 帖子唯一标识字段,默认 'id' */
|
||||
idField?: string;
|
||||
/** 排序字段,默认 'createdAt' */
|
||||
sortField?: string;
|
||||
/** 变化比例阈值,超过此值触发重置,默认 0.5 */
|
||||
resetThreshold?: number;
|
||||
/** 是否检测部分字段更新 */
|
||||
detectPartialUpdates?: boolean;
|
||||
/** 需要检测变化的字段列表 */
|
||||
trackedFields?: (keyof Post)[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认Post差异配置常量
|
||||
*/
|
||||
export const DEFAULT_POST_DIFF_CONFIG: Required<PostDiffConfig> = {
|
||||
batchInterval: 16,
|
||||
maxBatchSize: 50,
|
||||
enableDeduplication: true,
|
||||
enableMerge: true,
|
||||
idField: 'id',
|
||||
sortField: 'createdAt',
|
||||
resetThreshold: 0.5,
|
||||
detectPartialUpdates: true,
|
||||
trackedFields: [
|
||||
'likesCount',
|
||||
'commentsCount',
|
||||
'sharesCount',
|
||||
'viewsCount',
|
||||
'favoritesCount',
|
||||
'isLiked',
|
||||
'isFavorited',
|
||||
'isPinned',
|
||||
'status',
|
||||
'title',
|
||||
'content',
|
||||
'images',
|
||||
'tags',
|
||||
],
|
||||
};
|
||||
|
||||
// ==================== 监听器类型 ====================
|
||||
|
||||
/**
|
||||
* 批量更新监听器类型
|
||||
*/
|
||||
export type PostBatchUpdateListener = (updates: PostUpdate[]) => void;
|
||||
|
||||
/**
|
||||
* Post比较函数类型
|
||||
*/
|
||||
export type PostComparator<T extends PostIdentifier> = (a: T, b: T) => boolean;
|
||||
|
||||
/**
|
||||
* Post排序函数类型
|
||||
*/
|
||||
export type PostSorter<T extends PostIdentifier> = (a: T, b: T) => number;
|
||||
|
||||
// ==================== 统计接口 ====================
|
||||
|
||||
/**
|
||||
* Post差异统计接口
|
||||
*/
|
||||
export interface PostDiffStats {
|
||||
/** 总批次数 */
|
||||
totalBatches: number;
|
||||
/** 总更新数 */
|
||||
totalUpdates: number;
|
||||
/** 平均批量大小 */
|
||||
averageBatchSize: number;
|
||||
/** 上次批次时间 */
|
||||
lastBatchTime: number;
|
||||
/** 新增帖子数 */
|
||||
addedCount: number;
|
||||
/** 更新帖子数 */
|
||||
updatedCount: number;
|
||||
/** 删除帖子数 */
|
||||
deletedCount: number;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* useBreakpointCheck Hook
|
||||
* 断点检查 - 提供断点范围检查功能
|
||||
* @deprecated 请直接使用 useBreakpoint.ts 中的 hooks
|
||||
*/
|
||||
|
||||
export { useBreakpointGTE, useBreakpointLT, useBreakpointBetween } from './useBreakpoint';
|
||||
@@ -32,8 +32,8 @@ import { PostCard, TabBar, SearchBar } from '../../components/business';
|
||||
import { Loading, EmptyState, Text, ImageGallery, ImageGridItem, ResponsiveGrid } from '../../components/common';
|
||||
import { HomeStackParamList, RootStackParamList } from '../../navigation/types';
|
||||
import { useResponsive, useResponsiveSpacing } from '../../hooks/useResponsive';
|
||||
import { useCursorPagination } from '../../hooks/useCursorPagination';
|
||||
import { CursorPaginationRequest } from '../../types/dto';
|
||||
import { useDifferentialPosts } from '../../hooks/useDifferentialPosts';
|
||||
import { processPostUseCase } from '../../core/usecases/ProcessPostUseCase';
|
||||
import { SearchScreen } from './SearchScreen';
|
||||
import { CreatePostScreen } from '../create/CreatePostScreen';
|
||||
import { navigationService } from '../../infrastructure/navigation/navigationService';
|
||||
@@ -55,7 +55,7 @@ type PostType = 'recommend' | 'follow' | 'hot' | 'latest';
|
||||
export const HomeScreen: React.FC = () => {
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { likePost, unlikePost, favoritePost, unfavoritePost, posts: storePosts } = useUserStore();
|
||||
const { posts: storePosts } = useUserStore();
|
||||
const currentUser = useCurrentUser();
|
||||
|
||||
// 使用响应式 hook
|
||||
@@ -89,6 +89,15 @@ export const HomeScreen: React.FC = () => {
|
||||
const postIdsRef = useRef<Set<string>>(new Set());
|
||||
const lastSwipeAtRef = useRef(0);
|
||||
|
||||
// 构建一个以 postId 为 key 的 map,用于快速查找
|
||||
const postsMap = useMemo(() => {
|
||||
const map = new Map<string, Post>();
|
||||
storePosts.forEach(post => {
|
||||
map.set(post.id, post);
|
||||
});
|
||||
return map;
|
||||
}, [storePosts]);
|
||||
|
||||
// 获取当前 tab 对应的帖子类型
|
||||
const getPostType = useCallback((): PostType => {
|
||||
switch (activeIndex) {
|
||||
@@ -100,42 +109,70 @@ export const HomeScreen: React.FC = () => {
|
||||
}
|
||||
}, [activeIndex]);
|
||||
|
||||
// 使用游标分页获取帖子列表
|
||||
// 使用差异更新 Hook 获取帖子列表
|
||||
const listKey = useMemo(() => `home_${getPostType()}`, [getPostType]);
|
||||
|
||||
const {
|
||||
items: posts,
|
||||
isLoading,
|
||||
isRefreshing,
|
||||
posts,
|
||||
loading: isLoading,
|
||||
refreshing: isRefreshing,
|
||||
hasMore,
|
||||
error,
|
||||
loadMore,
|
||||
refresh,
|
||||
} = useCursorPagination<Post, { post_type: PostType }>(
|
||||
async ({ cursor, pageSize, extraParams }) => {
|
||||
const params: CursorPaginationRequest = {
|
||||
cursor,
|
||||
page_size: pageSize,
|
||||
post_type: extraParams?.post_type,
|
||||
};
|
||||
const response = await postService.getPostsCursor(params);
|
||||
return response;
|
||||
},
|
||||
{ pageSize: DEFAULT_PAGE_SIZE },
|
||||
{ post_type: getPostType() }
|
||||
reset,
|
||||
} = useDifferentialPosts<Post>(
|
||||
[],
|
||||
{
|
||||
listKey,
|
||||
enableDiff: true,
|
||||
enableBatching: true,
|
||||
autoSubscribe: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Tab 切换时刷新数据
|
||||
// 加载更多方法
|
||||
const loadMore = useCallback(async () => {
|
||||
if (hasMore && !isLoading) {
|
||||
try {
|
||||
await processPostUseCase.loadMorePosts(listKey);
|
||||
} catch (err) {
|
||||
console.error('加载更多失败:', err);
|
||||
}
|
||||
}
|
||||
}, [listKey, hasMore, isLoading]);
|
||||
|
||||
// 刷新方法 - 先获取正确类型的帖子,再刷新
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
// 先获取正确类型的帖子
|
||||
await processPostUseCase.fetchPosts(
|
||||
{ page: 1, pageSize: DEFAULT_PAGE_SIZE, post_type: getPostType() },
|
||||
listKey
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('刷新失败:', err);
|
||||
}
|
||||
}, [listKey, getPostType]);
|
||||
|
||||
// Tab 切换时刷新数据并重置列表
|
||||
useEffect(() => {
|
||||
reset();
|
||||
refresh();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeIndex]);
|
||||
|
||||
// 同步 store 中的帖子状态到本地(用于点赞、收藏等状态更新)
|
||||
useEffect(() => {
|
||||
if (posts.length === 0) return;
|
||||
|
||||
// 更新 postIdsRef
|
||||
const currentPostIds = new Set(posts.map(p => p.id));
|
||||
postIdsRef.current = currentPostIds;
|
||||
}, [posts, storePosts]);
|
||||
// 将获取到的原始帖子与 store 中的状态合并
|
||||
// 这样 UI 始终显示的是 store 中的最新状态(包括点赞、收藏等)
|
||||
const displayPosts = useMemo(() => {
|
||||
return posts.map(post => {
|
||||
const storePost = postsMap.get(post.id);
|
||||
if (storePost) {
|
||||
// 如果 store 中有这个帖子,使用 store 的最新状态
|
||||
return storePost;
|
||||
}
|
||||
// 如果 store 中没有这个帖子,使用原始数据
|
||||
return post;
|
||||
});
|
||||
}, [posts, postsMap]);
|
||||
|
||||
// 根据屏幕尺寸确定网格列数
|
||||
const gridColumns = useMemo(() => {
|
||||
@@ -242,20 +279,28 @@ export const HomeScreen: React.FC = () => {
|
||||
};
|
||||
|
||||
// 点赞帖子
|
||||
const handleLike = (post: Post) => {
|
||||
if (post.is_liked) {
|
||||
unlikePost(post.id);
|
||||
} else {
|
||||
likePost(post.id);
|
||||
const handleLike = async (post: Post) => {
|
||||
try {
|
||||
if (post.is_liked) {
|
||||
await processPostUseCase.unlikePost(post.id);
|
||||
} else {
|
||||
await processPostUseCase.likePost(post.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('点赞操作失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 收藏帖子
|
||||
const handleBookmark = (post: Post) => {
|
||||
if (post.is_favorited) {
|
||||
unfavoritePost(post.id);
|
||||
} else {
|
||||
favoritePost(post.id);
|
||||
const handleBookmark = async (post: Post) => {
|
||||
try {
|
||||
if (post.is_favorited) {
|
||||
await processPostUseCase.unfavoritePost(post.id);
|
||||
} else {
|
||||
await processPostUseCase.favoritePost(post.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('收藏操作失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -272,7 +317,7 @@ export const HomeScreen: React.FC = () => {
|
||||
Alert.alert('已复制', '帖子链接已复制到剪贴板');
|
||||
};
|
||||
|
||||
// 删除帖子 - 由于 posts 来自 Hook,需要刷新列表
|
||||
// 删除帖子 - 删除后刷新列表
|
||||
const handleDeletePost = async (postId: string) => {
|
||||
try {
|
||||
const success = await postService.deletePost(postId);
|
||||
@@ -377,7 +422,7 @@ export const HomeScreen: React.FC = () => {
|
||||
const columnHeights: number[] = Array(gridColumns).fill(0);
|
||||
|
||||
// 防御性检查:确保 posts 存在且是数组
|
||||
if (!posts || !Array.isArray(posts) || posts.length === 0) {
|
||||
if (!displayPosts || !Array.isArray(displayPosts) || displayPosts.length === 0) {
|
||||
return columns;
|
||||
}
|
||||
|
||||
@@ -385,7 +430,7 @@ export const HomeScreen: React.FC = () => {
|
||||
const totalGap = (gridColumns - 1) * responsiveGap;
|
||||
const columnWidth = (width - responsivePadding * 2 - totalGap) / gridColumns;
|
||||
|
||||
posts.forEach((post) => {
|
||||
displayPosts.forEach((post) => {
|
||||
const postHeight = estimatePostHeight(post, columnWidth);
|
||||
|
||||
// 找到当前高度最小的列
|
||||
@@ -395,7 +440,7 @@ export const HomeScreen: React.FC = () => {
|
||||
});
|
||||
|
||||
return columns;
|
||||
}, [posts, gridColumns, width, responsiveGap, responsivePadding]);
|
||||
}, [displayPosts, gridColumns, width, responsiveGap, responsivePadding]);
|
||||
|
||||
// 渲染单列帖子
|
||||
const renderWaterfallColumn = (column: Post[], columnIndex: number) => (
|
||||
@@ -461,7 +506,7 @@ export const HomeScreen: React.FC = () => {
|
||||
gap={{ xs: 8, sm: 12, md: 16, lg: 20, xl: 24 }}
|
||||
containerStyle={{ paddingHorizontal: responsivePadding, paddingBottom: 80 }}
|
||||
>
|
||||
{posts.map(post => {
|
||||
{displayPosts.map(post => {
|
||||
const authorId = post.author?.id || '';
|
||||
const isPostAuthor = currentUser?.id === authorId;
|
||||
return (
|
||||
@@ -507,7 +552,7 @@ export const HomeScreen: React.FC = () => {
|
||||
// 移动端和宽屏都使用单列 FlatList,宽屏下居中显示
|
||||
return (
|
||||
<FlatList
|
||||
data={posts}
|
||||
data={displayPosts}
|
||||
renderItem={renderPostList}
|
||||
keyExtractor={item => item.id}
|
||||
contentContainerStyle={[
|
||||
|
||||
@@ -32,6 +32,7 @@ import { Post, Comment, VoteResultDTO } 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 { useCursorPagination } from '../../hooks/useCursorPagination';
|
||||
import { CommentItem, VoteCard } from '../../components/business';
|
||||
import { Avatar, Button, Loading, EmptyState, Text, ImageGallery, ImageGrid, ImageGridItem, AdaptiveLayout } from '../../components/common';
|
||||
@@ -62,15 +63,6 @@ export const PostDetailScreen: React.FC = () => {
|
||||
const responsivePadding = useResponsiveSpacing({ xs: 12, sm: 14, md: 16, lg: 20, xl: 24, '2xl': 28, '3xl': 32, '4xl': 40 });
|
||||
const responsiveGap = useResponsiveSpacing({ xs: 8, sm: 10, md: 12, lg: 16, xl: 20, '2xl': 24, '3xl': 28, '4xl': 32 });
|
||||
|
||||
const {
|
||||
likePost,
|
||||
unlikePost,
|
||||
favoritePost,
|
||||
unfavoritePost,
|
||||
likeComment,
|
||||
unlikeComment
|
||||
} = useUserStore();
|
||||
|
||||
const currentUser = useCurrentUser();
|
||||
|
||||
const [post, setPost] = useState<Post | null>(null);
|
||||
@@ -144,14 +136,15 @@ export const PostDetailScreen: React.FC = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
// 从API获取帖子详情
|
||||
const postData = await postService.getPost(postId);
|
||||
// 使用 ProcessPostUseCase 获取帖子详情
|
||||
const postData = await processPostUseCase.fetchPostById(postId);
|
||||
if (postData) {
|
||||
setPost(postData);
|
||||
// 类型转换:将 core/entities/Post 转换为 PostDTO 以保持兼容性
|
||||
setPost(postData as unknown as Post);
|
||||
// 初始化关注状态
|
||||
if (postData.author) {
|
||||
setIsFollowing(postData.author.is_following || false);
|
||||
setIsFollowingMe(postData.author.is_following_me || false);
|
||||
setIsFollowing((postData.author as any).is_following || false);
|
||||
setIsFollowingMe((postData.author as any).is_following_me || false);
|
||||
}
|
||||
// 只在首次加载时记录浏览量
|
||||
if (recordView && !hasRecordedView.current) {
|
||||
@@ -163,7 +156,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
}
|
||||
|
||||
// 如果是投票帖子,立即加载投票数据
|
||||
if (postData.is_vote) {
|
||||
if ((postData as any).is_vote) {
|
||||
setIsVoteLoading(true);
|
||||
try {
|
||||
const voteData = await voteService.getVoteResult(postId);
|
||||
@@ -184,8 +177,8 @@ export const PostDetailScreen: React.FC = () => {
|
||||
setPost(updatedPost);
|
||||
// 初始化关注状态
|
||||
if (updatedPost.author) {
|
||||
setIsFollowing(updatedPost.author.is_following || false);
|
||||
setIsFollowingMe(updatedPost.author.is_following_me || false);
|
||||
setIsFollowing((updatedPost.author as any).is_following || false);
|
||||
setIsFollowingMe((updatedPost.author as any).is_following_me || false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -361,10 +354,11 @@ export const PostDetailScreen: React.FC = () => {
|
||||
|
||||
try {
|
||||
if (oldIsLiked) {
|
||||
await unlikePost(post.id);
|
||||
await processPostUseCase.unlikePost(post.id);
|
||||
} else {
|
||||
await likePost(post.id);
|
||||
await processPostUseCase.likePost(post.id);
|
||||
}
|
||||
// UseCase 会更新 store,本地状态已经是乐观更新的,无需再次更新
|
||||
} catch (error) {
|
||||
console.error('点赞操作失败:', error);
|
||||
// 失败时回滚状态
|
||||
@@ -374,7 +368,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
likes_count: oldLikesCount
|
||||
} : null);
|
||||
}
|
||||
}, [post, likePost, unlikePost]);
|
||||
}, [post]);
|
||||
|
||||
// 收藏帖子
|
||||
const handleFavorite = useCallback(async () => {
|
||||
@@ -393,10 +387,11 @@ export const PostDetailScreen: React.FC = () => {
|
||||
|
||||
try {
|
||||
if (oldIsFavorited) {
|
||||
await unfavoritePost(post.id);
|
||||
await processPostUseCase.unfavoritePost(post.id);
|
||||
} else {
|
||||
await favoritePost(post.id);
|
||||
await processPostUseCase.favoritePost(post.id);
|
||||
}
|
||||
// UseCase 会更新 store,本地状态已经是乐观更新的,无需再次更新
|
||||
} catch (error) {
|
||||
console.error('收藏操作失败:', error);
|
||||
// 失败时回滚状态
|
||||
@@ -406,7 +401,7 @@ export const PostDetailScreen: React.FC = () => {
|
||||
favorites_count: oldFavoritesCount
|
||||
} : null);
|
||||
}
|
||||
}, [post, favoritePost, unfavoritePost]);
|
||||
}, [post]);
|
||||
|
||||
// 分享帖子
|
||||
const handleShare = useCallback(async () => {
|
||||
@@ -527,17 +522,13 @@ export const PostDetailScreen: React.FC = () => {
|
||||
onPress: async () => {
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const success = await postService.deletePost(post.id);
|
||||
if (success) {
|
||||
Alert.alert('删除成功', '帖子已删除', [
|
||||
{
|
||||
text: '确定',
|
||||
onPress: () => navigation.goBack(),
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
Alert.alert('删除失败', '删除帖子时发生错误,请稍后重试');
|
||||
}
|
||||
await processPostUseCase.deletePost(post.id);
|
||||
Alert.alert('删除成功', '帖子已删除', [
|
||||
{
|
||||
text: '确定',
|
||||
onPress: () => navigation.goBack(),
|
||||
},
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('删除帖子失败:', error);
|
||||
Alert.alert('删除失败', '删除帖子时发生错误,请稍后重试');
|
||||
@@ -817,9 +808,9 @@ export const PostDetailScreen: React.FC = () => {
|
||||
|
||||
try {
|
||||
if (oldIsLiked) {
|
||||
await unlikeComment(comment.id);
|
||||
await commentService.unlikeComment(comment.id);
|
||||
} else {
|
||||
await likeComment(comment.id);
|
||||
await commentService.likeComment(comment.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('评论点赞操作失败:', error);
|
||||
|
||||
@@ -265,7 +265,7 @@ export const MessageListScreen: React.FC = () => {
|
||||
}, [isFocused]);
|
||||
|
||||
// 【新架构】使用focus刷新hook,从ChatScreen返回时自动刷新未读数
|
||||
useMessageListRefresh(isFocused);
|
||||
useMessageListRefresh();
|
||||
|
||||
// 同步未读数到userStore(用于TabBar角标显示)
|
||||
useEffect(() => {
|
||||
|
||||
@@ -29,7 +29,8 @@ import { SystemMessageItem } from '../../components/business';
|
||||
import { Text, EmptyState, ResponsiveContainer } from '../../components/common';
|
||||
import { useCursorPagination } from '../../hooks/useCursorPagination';
|
||||
import { RootStackParamList } from '../../navigation/types';
|
||||
import { useMessageManagerSystemUnreadCount, useUserStore } from '../../stores';
|
||||
import { useMessageManagerSystemUnreadCount } from '../../stores';
|
||||
import { messageManager } from '../../stores/messageManager';
|
||||
|
||||
const MESSAGE_TYPES = [
|
||||
{ key: 'all', title: '全部' },
|
||||
@@ -46,7 +47,6 @@ const GROUP_SYSTEM_TYPES = new Set(['group_invite', 'group_join_apply', 'group_j
|
||||
|
||||
export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack }) => {
|
||||
const isFocused = useIsFocused();
|
||||
const fetchMessageUnreadCount = useUserStore(state => state.fetchMessageUnreadCount);
|
||||
const { setSystemUnreadCount, decrementSystemUnreadCount } = useMessageManagerSystemUnreadCount();
|
||||
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
|
||||
@@ -158,11 +158,11 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
||||
setUnreadCount(0);
|
||||
setSystemUnreadCount(0);
|
||||
// 同步更新全局 TabBar 红点
|
||||
fetchMessageUnreadCount();
|
||||
messageManager.fetchUnreadCount();
|
||||
} catch (error) {
|
||||
console.error('一键已读失败:', error);
|
||||
}
|
||||
}, [refresh, fetchMessageUnreadCount, setSystemUnreadCount]);
|
||||
}, [refresh, setSystemUnreadCount]);
|
||||
|
||||
// 页面加载和获得焦点时刷新,并自动标记所有消息为已读
|
||||
// 使用 ref 防止重复执行,避免无限循环
|
||||
@@ -281,7 +281,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
||||
}
|
||||
// 更新本地未读数以及全局 TabBar 红点
|
||||
fetchUnreadCount();
|
||||
fetchMessageUnreadCount();
|
||||
messageManager.fetchUnreadCount();
|
||||
|
||||
// 根据消息类型处理导航
|
||||
const { system_type, extra_data } = message;
|
||||
|
||||
@@ -9,8 +9,6 @@ import {
|
||||
View,
|
||||
TouchableOpacity,
|
||||
Image,
|
||||
findNodeHandle,
|
||||
UIManager,
|
||||
GestureResponderEvent,
|
||||
StyleSheet,
|
||||
Dimensions,
|
||||
@@ -219,22 +217,17 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
||||
// 处理长按并获取位置
|
||||
const handleLongPress = () => {
|
||||
if (bubbleRef.current) {
|
||||
const node = findNodeHandle(bubbleRef.current);
|
||||
if (node) {
|
||||
UIManager.measureInWindow(node, (x, y, width, height) => {
|
||||
const position: MenuPosition = {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
pressX: pressPositionRef.current.x,
|
||||
pressY: pressPositionRef.current.y,
|
||||
};
|
||||
onLongPress(message, position);
|
||||
});
|
||||
} else {
|
||||
onLongPress(message);
|
||||
}
|
||||
bubbleRef.current.measureInWindow((x, y, width, height) => {
|
||||
const position: MenuPosition = {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
pressX: pressPositionRef.current.x,
|
||||
pressY: pressPositionRef.current.y,
|
||||
};
|
||||
onLongPress(message, position);
|
||||
});
|
||||
} else {
|
||||
onLongPress(message);
|
||||
}
|
||||
|
||||
@@ -73,8 +73,6 @@ export interface MessageBubbleProps {
|
||||
otherUser: { id?: string; nickname?: string; avatar?: string | null } | null;
|
||||
isGroupChat: boolean;
|
||||
groupMembers: GroupMemberResponse[];
|
||||
/** @deprecated 发送者信息现在直接从 message.sender 获取,不再使用 senderCache */
|
||||
senderCache?: Map<string, UserDTO>;
|
||||
otherUserLastReadSeq: number;
|
||||
selectedMessageId: string | null;
|
||||
// 消息映射,用于查找被回复的消息
|
||||
|
||||
@@ -49,7 +49,7 @@ export const ProfileScreen: React.FC = () => {
|
||||
// 使用 any 类型来访问根导航
|
||||
const rootNavigation = useNavigation<RootNavigationProp>();
|
||||
const { currentUser, updateUser, fetchCurrentUser } = useAuthStore();
|
||||
const { followUser, unfollowUser, likePost, unlikePost, favoritePost, unfavoritePost, posts: storePosts } = useUserStore();
|
||||
const { followUser, unfollowUser, posts: storePosts } = useUserStore();
|
||||
|
||||
// 响应式布局
|
||||
const { isDesktop, isTablet, width } = useResponsive();
|
||||
@@ -275,9 +275,9 @@ export const ProfileScreen: React.FC = () => {
|
||||
post={post}
|
||||
onPress={() => handlePostPress(post.id)}
|
||||
onUserPress={() => post.author ? handleUserPress(post.author.id) : () => {}}
|
||||
onLike={() => post.is_liked ? unlikePost(post.id) : likePost(post.id)}
|
||||
onLike={() => post.is_liked ? postService.unlikePost(post.id) : postService.likePost(post.id)}
|
||||
onComment={() => handlePostPress(post.id, true)}
|
||||
onBookmark={() => post.is_favorited ? unfavoritePost(post.id) : favoritePost(post.id)}
|
||||
onBookmark={() => post.is_favorited ? postService.unfavoritePost(post.id) : postService.favoritePost(post.id)}
|
||||
onShare={() => {}}
|
||||
onDelete={() => handleDeletePost(post.id)}
|
||||
isPostAuthor={isPostAuthor}
|
||||
@@ -315,9 +315,9 @@ export const ProfileScreen: React.FC = () => {
|
||||
post={post}
|
||||
onPress={() => handlePostPress(post.id)}
|
||||
onUserPress={() => post.author ? handleUserPress(post.author.id) : () => {}}
|
||||
onLike={() => post.is_liked ? unlikePost(post.id) : likePost(post.id)}
|
||||
onLike={() => post.is_liked ? postService.unlikePost(post.id) : postService.likePost(post.id)}
|
||||
onComment={() => handlePostPress(post.id, true)}
|
||||
onBookmark={() => post.is_favorited ? unfavoritePost(post.id) : favoritePost(post.id)}
|
||||
onBookmark={() => post.is_favorited ? postService.unfavoritePost(post.id) : postService.favoritePost(post.id)}
|
||||
onShare={() => {}}
|
||||
onDelete={() => handleDeletePost(post.id)}
|
||||
isPostAuthor={isPostAuthor}
|
||||
@@ -330,7 +330,7 @@ export const ProfileScreen: React.FC = () => {
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [loading, activeTab, posts, favorites, currentUser?.id, handlePostPress, handleUserPress, handleDeletePost, unlikePost, likePost, unfavoritePost, favoritePost]);
|
||||
}, [loading, activeTab, posts, favorites, currentUser?.id, handlePostPress, handleUserPress, handleDeletePost]);
|
||||
|
||||
// 渲染用户信息头部 - 不随 tab 变化,使用 useMemo 缓存
|
||||
const renderUserHeader = useMemo(() => (
|
||||
|
||||
@@ -41,7 +41,7 @@ export const UserScreen: React.FC = () => {
|
||||
// 使用 any 类型来访问根导航
|
||||
const rootNavigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
|
||||
const { followUser, unfollowUser, likePost, unlikePost, favoritePost, unfavoritePost, posts: storePosts } = useUserStore();
|
||||
const { followUser, unfollowUser, posts: storePosts } = useUserStore();
|
||||
const currentUser = useCurrentUser();
|
||||
|
||||
// 响应式布局
|
||||
@@ -319,9 +319,9 @@ export const UserScreen: React.FC = () => {
|
||||
post={post}
|
||||
onPress={() => handlePostPress(post.id)}
|
||||
onUserPress={() => post.author ? handleUserPress(post.author.id) : () => {}}
|
||||
onLike={() => post.is_liked ? unlikePost(post.id) : likePost(post.id)}
|
||||
onLike={() => post.is_liked ? postService.unlikePost(post.id) : postService.likePost(post.id)}
|
||||
onComment={() => handlePostPress(post.id, true)}
|
||||
onBookmark={() => post.is_favorited ? unfavoritePost(post.id) : favoritePost(post.id)}
|
||||
onBookmark={() => post.is_favorited ? postService.unfavoritePost(post.id) : postService.favoritePost(post.id)}
|
||||
onShare={() => {}}
|
||||
onDelete={() => handleDeletePost(post.id)}
|
||||
isPostAuthor={isPostAuthor}
|
||||
@@ -359,9 +359,9 @@ export const UserScreen: React.FC = () => {
|
||||
post={post}
|
||||
onPress={() => handlePostPress(post.id)}
|
||||
onUserPress={() => post.author ? handleUserPress(post.author.id) : () => {}}
|
||||
onLike={() => post.is_liked ? unlikePost(post.id) : likePost(post.id)}
|
||||
onLike={() => post.is_liked ? postService.unlikePost(post.id) : postService.likePost(post.id)}
|
||||
onComment={() => handlePostPress(post.id, true)}
|
||||
onBookmark={() => post.is_favorited ? unfavoritePost(post.id) : favoritePost(post.id)}
|
||||
onBookmark={() => post.is_favorited ? postService.unfavoritePost(post.id) : postService.favoritePost(post.id)}
|
||||
onShare={() => {}}
|
||||
onDelete={() => handleDeletePost(post.id)}
|
||||
isPostAuthor={isPostAuthor}
|
||||
|
||||
@@ -403,17 +403,6 @@ class GroupService {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 兼容旧API的方法(将逐步废弃) ====================
|
||||
|
||||
/**
|
||||
* @deprecated 使用 setGroupName 代替
|
||||
*/
|
||||
async updateGroup(id: number, data: { name: string }): Promise<GroupResponse> {
|
||||
await this.setGroupName(id, data.name);
|
||||
return this.getGroup(id);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出群组服务实例
|
||||
export const groupService = new GroupService();
|
||||
|
||||
|
||||
@@ -671,32 +671,6 @@ class MessageService {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 兼容旧API的方法(将逐步废弃) ====================
|
||||
|
||||
/**
|
||||
* @deprecated 使用 getConversations 代替
|
||||
*/
|
||||
async getConversation(conversationId: string): Promise<ConversationDetailResponse | null> {
|
||||
try {
|
||||
return await this.getConversationById(conversationId);
|
||||
} catch (error) {
|
||||
console.error('获取会话详情失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 使用 createConversation(userId: string) 代替
|
||||
*/
|
||||
async createConversationByString(userId: string): Promise<ConversationResponse | null> {
|
||||
try {
|
||||
return await this.createConversation(userId);
|
||||
} catch (error) {
|
||||
console.error('创建会话失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出消息服务实例
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
/**
|
||||
* 帖子服务
|
||||
* 处理帖子的增删改查、点赞、收藏等功能
|
||||
*
|
||||
* @deprecated 此服务已弃用,请使用 ProcessPostUseCase 代替
|
||||
* 互动操作(点赞、收藏)已委托给 processPostUseCase
|
||||
*/
|
||||
|
||||
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,14 +134,12 @@ class PostService {
|
||||
}
|
||||
|
||||
// 点赞帖子
|
||||
// @deprecated 请使用 processPostUseCase.likePost() 代替
|
||||
async likePost(postId: string): Promise<Post | null> {
|
||||
try {
|
||||
const response = await api.post<Post>(`/posts/${postId}/like`);
|
||||
if (response.code === 0) {
|
||||
return response.data;
|
||||
}
|
||||
console.warn('[postService] likePost failed, code:', response.code);
|
||||
return null;
|
||||
const post = await processPostUseCase.likePost(postId);
|
||||
// 转换为旧版 Post 类型以保持向后兼容
|
||||
return post as unknown as Post;
|
||||
} catch (error) {
|
||||
console.error('[postService] likePost error:', error);
|
||||
return null;
|
||||
@@ -144,14 +147,11 @@ class PostService {
|
||||
}
|
||||
|
||||
// 取消点赞帖子
|
||||
// @deprecated 请使用 processPostUseCase.unlikePost() 代替
|
||||
async unlikePost(postId: string): Promise<Post | null> {
|
||||
try {
|
||||
const response = await api.delete<Post>(`/posts/${postId}/like`);
|
||||
if (response.code === 0) {
|
||||
return response.data;
|
||||
}
|
||||
console.warn('[postService] unlikePost failed, code:', response.code);
|
||||
return null;
|
||||
const post = await processPostUseCase.unlikePost(postId);
|
||||
return post as unknown as Post;
|
||||
} catch (error) {
|
||||
console.error('[postService] unlikePost error:', error);
|
||||
return null;
|
||||
@@ -159,14 +159,11 @@ class PostService {
|
||||
}
|
||||
|
||||
// 收藏帖子
|
||||
// @deprecated 请使用 processPostUseCase.favoritePost() 代替
|
||||
async favoritePost(postId: string): Promise<Post | null> {
|
||||
try {
|
||||
const response = await api.post<Post>(`/posts/${postId}/favorite`);
|
||||
if (response.code === 0) {
|
||||
return response.data;
|
||||
}
|
||||
console.warn('[postService] favoritePost failed, code:', response.code);
|
||||
return null;
|
||||
const post = await processPostUseCase.favoritePost(postId);
|
||||
return post as unknown as Post;
|
||||
} catch (error) {
|
||||
console.error('[postService] favoritePost error:', error);
|
||||
return null;
|
||||
@@ -174,14 +171,11 @@ class PostService {
|
||||
}
|
||||
|
||||
// 取消收藏帖子
|
||||
// @deprecated 请使用 processPostUseCase.unfavoritePost() 代替
|
||||
async unfavoritePost(postId: string): Promise<Post | null> {
|
||||
try {
|
||||
const response = await api.delete<Post>(`/posts/${postId}/favorite`);
|
||||
if (response.code === 0) {
|
||||
return response.data;
|
||||
}
|
||||
console.warn('[postService] unfavoritePost failed, code:', response.code);
|
||||
return null;
|
||||
const post = await processPostUseCase.unfavoritePost(postId);
|
||||
return post as unknown as Post;
|
||||
} catch (error) {
|
||||
console.error('[postService] unfavoritePost error:', error);
|
||||
return null;
|
||||
|
||||
@@ -502,10 +502,9 @@ interface UseMessageListRefreshReturn {
|
||||
* 从 ChatScreen 返回时,MessageManager 内存状态已经是最新的(markAsRead 乐观更新),
|
||||
* 无需再请求服务器(避免时序竞争覆盖已读状态)。
|
||||
* 仅在用户主动下拉刷新时才触发服务器请求。
|
||||
* @deprecated isFocused 参数保留仅为向后兼容,不再触发焦点刷新
|
||||
* 注意:isFocused 参数已移除,不再触发焦点刷新
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function useMessageListRefresh(_isFocused?: boolean): UseMessageListRefreshReturn {
|
||||
export function useMessageListRefresh(): UseMessageListRefreshReturn {
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const lastRefreshTimeRef = useRef<number>(0);
|
||||
|
||||
|
||||
@@ -10,12 +10,10 @@ import { PaginatedData } from '../services/api';
|
||||
import {
|
||||
authService,
|
||||
postService,
|
||||
commentService,
|
||||
notificationService,
|
||||
} from '../services';
|
||||
import { userManager } from './userManager';
|
||||
import { messageManager } from './messageManager';
|
||||
import { createOptimisticUpdaterFactory } from '../utils/optimisticUpdate';
|
||||
|
||||
interface UserState {
|
||||
// 状态
|
||||
@@ -42,17 +40,10 @@ interface UserState {
|
||||
markAllNotificationsAsRead: () => Promise<void>;
|
||||
fetchNotificationBadge: () => Promise<void>;
|
||||
setMessageUnreadCount: (count: number) => void;
|
||||
fetchMessageUnreadCount: () => Promise<void>;
|
||||
addSearchHistory: (keyword: string) => void;
|
||||
clearSearchHistory: () => void;
|
||||
|
||||
// 互动操作
|
||||
likePost: (postId: string) => Promise<void>;
|
||||
unlikePost: (postId: string) => Promise<void>;
|
||||
favoritePost: (postId: string) => Promise<void>;
|
||||
unfavoritePost: (postId: string) => Promise<void>;
|
||||
likeComment: (commentId: string) => Promise<void>;
|
||||
unlikeComment: (commentId: string) => Promise<void>;
|
||||
// 关注用户操作(authService 不管理状态,所以保留在 store)
|
||||
followUser: (userId: string) => Promise<void>;
|
||||
unfollowUser: (userId: string) => Promise<void>;
|
||||
|
||||
@@ -61,9 +52,6 @@ interface UserState {
|
||||
}
|
||||
|
||||
export const useUserStore = create<UserState>((set, get) => {
|
||||
// 创建乐观更新器
|
||||
const optimisticUpdater = createOptimisticUpdaterFactory<UserState>(set, get);
|
||||
|
||||
return {
|
||||
// 初始状态
|
||||
users: [],
|
||||
@@ -252,19 +240,6 @@ export const useUserStore = create<UserState>((set, get) => {
|
||||
set({ messageUnreadCount: count });
|
||||
},
|
||||
|
||||
// 从后端拉取最新消息未读数
|
||||
// @deprecated 请使用 messageManager.fetchUnreadCount() 代替
|
||||
// 此方法保留用于向后兼容
|
||||
fetchMessageUnreadCount: async () => {
|
||||
try {
|
||||
await messageManager.fetchUnreadCount();
|
||||
const unread = messageManager.getUnreadCount();
|
||||
set({ messageUnreadCount: unread.total + unread.system });
|
||||
} catch (error) {
|
||||
console.error('获取消息未读数失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 添加搜索历史
|
||||
addSearchHistory: (keyword: string) => {
|
||||
set(state => {
|
||||
@@ -278,153 +253,7 @@ export const useUserStore = create<UserState>((set, get) => {
|
||||
set({ searchHistory: [] });
|
||||
},
|
||||
|
||||
// 点赞帖子 - 使用乐观更新工具
|
||||
likePost: async (postId: string) => {
|
||||
const originalPosts = get().posts;
|
||||
|
||||
// 乐观更新
|
||||
set(state => ({
|
||||
posts: state.posts.map(p =>
|
||||
p.id === postId ? { ...p, is_liked: true, likes_count: p.likes_count + 1 } : p
|
||||
)
|
||||
}));
|
||||
|
||||
try {
|
||||
const updatedPost = await postService.likePost(postId);
|
||||
if (updatedPost) {
|
||||
// 使用服务器返回的数据更新
|
||||
set(state => ({
|
||||
posts: state.posts.map(p =>
|
||||
p.id === postId ? updatedPost : p
|
||||
)
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('点赞帖子失败:', error);
|
||||
// 回滚
|
||||
set({ posts: originalPosts });
|
||||
}
|
||||
},
|
||||
|
||||
// 取消点赞 - 使用乐观更新工具
|
||||
unlikePost: async (postId: string) => {
|
||||
const originalPosts = get().posts;
|
||||
|
||||
// 乐观更新
|
||||
set(state => ({
|
||||
posts: state.posts.map(p =>
|
||||
p.id === postId ? { ...p, is_liked: false, likes_count: Math.max(0, p.likes_count - 1) } : p
|
||||
)
|
||||
}));
|
||||
|
||||
try {
|
||||
const updatedPost = await postService.unlikePost(postId);
|
||||
if (updatedPost) {
|
||||
// 使用服务器返回的数据更新
|
||||
set(state => ({
|
||||
posts: state.posts.map(p =>
|
||||
p.id === postId ? updatedPost : p
|
||||
)
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('取消点赞帖子失败:', error);
|
||||
// 回滚
|
||||
set({ posts: originalPosts });
|
||||
}
|
||||
},
|
||||
|
||||
// 收藏帖子 - 使用乐观更新工具
|
||||
favoritePost: async (postId: string) => {
|
||||
const originalPosts = get().posts;
|
||||
|
||||
// 乐观更新
|
||||
set(state => ({
|
||||
posts: state.posts.map(p =>
|
||||
p.id === postId ? { ...p, is_favorited: true, favorites_count: p.favorites_count + 1 } : p
|
||||
)
|
||||
}));
|
||||
|
||||
try {
|
||||
const updatedPost = await postService.favoritePost(postId);
|
||||
if (updatedPost) {
|
||||
// 使用服务器返回的数据更新
|
||||
set(state => ({
|
||||
posts: state.posts.map(p =>
|
||||
p.id === postId ? updatedPost : p
|
||||
)
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('收藏帖子失败:', error);
|
||||
// 回滚
|
||||
set({ posts: originalPosts });
|
||||
}
|
||||
},
|
||||
|
||||
// 取消收藏 - 使用乐观更新工具
|
||||
unfavoritePost: async (postId: string) => {
|
||||
const originalPosts = get().posts;
|
||||
|
||||
// 乐观更新
|
||||
set(state => ({
|
||||
posts: state.posts.map(p =>
|
||||
p.id === postId ? { ...p, is_favorited: false, favorites_count: Math.max(0, p.favorites_count - 1) } : p
|
||||
)
|
||||
}));
|
||||
|
||||
try {
|
||||
const updatedPost = await postService.unfavoritePost(postId);
|
||||
if (updatedPost) {
|
||||
// 使用服务器返回的数据更新
|
||||
set(state => ({
|
||||
posts: state.posts.map(p =>
|
||||
p.id === postId ? updatedPost : p
|
||||
)
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('取消收藏帖子失败:', error);
|
||||
// 回滚
|
||||
set({ posts: originalPosts });
|
||||
}
|
||||
},
|
||||
|
||||
// 点赞评论 - 使用乐观更新工具
|
||||
likeComment: async (commentId: string) => {
|
||||
await optimisticUpdater({
|
||||
key: 'posts',
|
||||
optimisticUpdate: (posts) =>
|
||||
posts.map(p => ({
|
||||
...p,
|
||||
top_comment: p.top_comment && p.top_comment.id === commentId
|
||||
? { ...p.top_comment, is_liked: true, likes_count: p.top_comment.likes_count + 1 }
|
||||
: p.top_comment
|
||||
})),
|
||||
rollbackState: (original) => original,
|
||||
apiCall: () => commentService.likeComment(commentId),
|
||||
errorMessage: '点赞评论失败',
|
||||
});
|
||||
},
|
||||
|
||||
// 取消点赞评论 - 使用乐观更新工具
|
||||
unlikeComment: async (commentId: string) => {
|
||||
await optimisticUpdater({
|
||||
key: 'posts',
|
||||
optimisticUpdate: (posts) =>
|
||||
posts.map(p => ({
|
||||
...p,
|
||||
top_comment: p.top_comment && p.top_comment.id === commentId
|
||||
? { ...p.top_comment, is_liked: false, likes_count: Math.max(0, p.top_comment.likes_count - 1) }
|
||||
: p.top_comment
|
||||
})),
|
||||
rollbackState: (original) => original,
|
||||
apiCall: () => commentService.unlikeComment(commentId),
|
||||
errorMessage: '取消点赞评论失败',
|
||||
});
|
||||
},
|
||||
|
||||
// 关注用户 - 使用乐观更新工具
|
||||
// 关注用户 - authService 不管理状态,所以由 store 管理
|
||||
followUser: async (userId: string) => {
|
||||
const originalUsers = get().users;
|
||||
const originalUserCache = get().userCache;
|
||||
@@ -462,7 +291,7 @@ export const useUserStore = create<UserState>((set, get) => {
|
||||
}
|
||||
},
|
||||
|
||||
// 取消关注 - 使用乐观更新工具
|
||||
// 取消关注 - authService 不管理状态,所以由 store 管理
|
||||
unfollowUser: async (userId: string) => {
|
||||
const originalUsers = get().users;
|
||||
const originalUserCache = get().userCache;
|
||||
|
||||
@@ -326,37 +326,6 @@ export interface MessageSyncResponse {
|
||||
has_more: boolean;
|
||||
}
|
||||
|
||||
// ==================== Legacy Message DTOs (Deprecated) ====================
|
||||
|
||||
/**
|
||||
* @deprecated 使用 MessageResponse 代替
|
||||
*/
|
||||
export interface MessageDTO {
|
||||
id: string;
|
||||
conversation_id: string;
|
||||
sender_id: string;
|
||||
content: string;
|
||||
message_type: string;
|
||||
is_read: boolean;
|
||||
created_at: string;
|
||||
sender: UserDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 使用 ConversationResponse 代替
|
||||
*/
|
||||
export interface ConversationDTO {
|
||||
id: string;
|
||||
user1_id: string;
|
||||
user2_id: string;
|
||||
last_message: MessageDTO | null;
|
||||
last_message_at: string;
|
||||
unread_count: number;
|
||||
participant: UserDTO;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// ==================== Auth DTOs ====================
|
||||
|
||||
export interface LoginDTO {
|
||||
|
||||
@@ -189,34 +189,6 @@ export type Conversation = ConversationResponse;
|
||||
*/
|
||||
export type Message = MessageResponse;
|
||||
|
||||
// ============================================
|
||||
// Legacy Message Types (Deprecated)
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* @deprecated 使用 ConversationResponse 代替
|
||||
*/
|
||||
export interface LegacyConversation {
|
||||
id: string;
|
||||
participant: User;
|
||||
lastMessage: LegacyMessage;
|
||||
unreadCount: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 使用 MessageResponse 代替
|
||||
*/
|
||||
export interface LegacyMessage {
|
||||
id: string;
|
||||
conversationId: string;
|
||||
senderId: string;
|
||||
content: string;
|
||||
type: 'text' | 'image' | 'system';
|
||||
isRead: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// API Response Types
|
||||
// ============================================
|
||||
|
||||
Reference in New Issue
Block a user