refactor: migrate post operations to use case architecture with differential updates
Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
This commit is contained in:
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) => {
|
||||
const handleLike = async (post: Post) => {
|
||||
try {
|
||||
if (post.is_liked) {
|
||||
unlikePost(post.id);
|
||||
await processPostUseCase.unlikePost(post.id);
|
||||
} else {
|
||||
likePost(post.id);
|
||||
await processPostUseCase.likePost(post.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('点赞操作失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 收藏帖子
|
||||
const handleBookmark = (post: Post) => {
|
||||
const handleBookmark = async (post: Post) => {
|
||||
try {
|
||||
if (post.is_favorited) {
|
||||
unfavoritePost(post.id);
|
||||
await processPostUseCase.unfavoritePost(post.id);
|
||||
} else {
|
||||
favoritePost(post.id);
|
||||
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) {
|
||||
await processPostUseCase.deletePost(post.id);
|
||||
Alert.alert('删除成功', '帖子已删除', [
|
||||
{
|
||||
text: '确定',
|
||||
onPress: () => navigation.goBack(),
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
Alert.alert('删除失败', '删除帖子时发生错误,请稍后重试');
|
||||
}
|
||||
} 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,9 +217,7 @@ 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) => {
|
||||
bubbleRef.current.measureInWindow((x, y, width, height) => {
|
||||
const position: MenuPosition = {
|
||||
x,
|
||||
y,
|
||||
@@ -235,9 +231,6 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
||||
} else {
|
||||
onLongPress(message);
|
||||
}
|
||||
} 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