dev #5

Merged
lan merged 38 commits from dev into master 2026-03-25 20:54:10 +08:00
30 changed files with 4395 additions and 572 deletions
Showing only changes of commit d273569911 - Show all commits

View 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模块对齐。

View File

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

View File

@@ -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
View 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');
};

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

View File

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

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

View 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>;
}

View File

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

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

View 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);
}

View 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);
}

View File

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

View 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 {
/** 批量处理间隔(毫秒),默认 16ms60fps */
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;
}

View File

@@ -1,7 +0,0 @@
/**
* useBreakpointCheck Hook
* 断点检查 - 提供断点范围检查功能
* @deprecated 请直接使用 useBreakpoint.ts 中的 hooks
*/
export { useBreakpointGTE, useBreakpointLT, useBreakpointBetween } from './useBreakpoint';

View File

@@ -32,8 +32,8 @@ import { PostCard, TabBar, SearchBar } from '../../components/business';
import { Loading, EmptyState, Text, ImageGallery, ImageGridItem, ResponsiveGrid } from '../../components/common';
import { HomeStackParamList, RootStackParamList } from '../../navigation/types';
import { useResponsive, useResponsiveSpacing } from '../../hooks/useResponsive';
import { useCursorPagination } from '../../hooks/useCursorPagination';
import { CursorPaginationRequest } from '../../types/dto';
import { useDifferentialPosts } from '../../hooks/useDifferentialPosts';
import { processPostUseCase } from '../../core/usecases/ProcessPostUseCase';
import { SearchScreen } from './SearchScreen';
import { CreatePostScreen } from '../create/CreatePostScreen';
import { navigationService } from '../../infrastructure/navigation/navigationService';
@@ -55,7 +55,7 @@ type PostType = 'recommend' | 'follow' | 'hot' | 'latest';
export const HomeScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp>();
const insets = useSafeAreaInsets();
const { likePost, unlikePost, favoritePost, unfavoritePost, posts: storePosts } = useUserStore();
const { posts: storePosts } = useUserStore();
const currentUser = useCurrentUser();
// 使用响应式 hook
@@ -89,6 +89,15 @@ export const HomeScreen: React.FC = () => {
const postIdsRef = useRef<Set<string>>(new Set());
const lastSwipeAtRef = useRef(0);
// 构建一个以 postId 为 key 的 map用于快速查找
const postsMap = useMemo(() => {
const map = new Map<string, Post>();
storePosts.forEach(post => {
map.set(post.id, post);
});
return map;
}, [storePosts]);
// 获取当前 tab 对应的帖子类型
const getPostType = useCallback((): PostType => {
switch (activeIndex) {
@@ -100,42 +109,70 @@ export const HomeScreen: React.FC = () => {
}
}, [activeIndex]);
// 使用游标分页获取帖子列表
// 使用差异更新 Hook 获取帖子列表
const listKey = useMemo(() => `home_${getPostType()}`, [getPostType]);
const {
items: posts,
isLoading,
isRefreshing,
posts,
loading: isLoading,
refreshing: isRefreshing,
hasMore,
error,
loadMore,
refresh,
} = useCursorPagination<Post, { post_type: PostType }>(
async ({ cursor, pageSize, extraParams }) => {
const params: CursorPaginationRequest = {
cursor,
page_size: pageSize,
post_type: extraParams?.post_type,
};
const response = await postService.getPostsCursor(params);
return response;
},
{ pageSize: DEFAULT_PAGE_SIZE },
{ post_type: getPostType() }
reset,
} = useDifferentialPosts<Post>(
[],
{
listKey,
enableDiff: true,
enableBatching: true,
autoSubscribe: true,
}
);
// Tab 切换时刷新数据
// 加载更多方法
const loadMore = useCallback(async () => {
if (hasMore && !isLoading) {
try {
await processPostUseCase.loadMorePosts(listKey);
} catch (err) {
console.error('加载更多失败:', err);
}
}
}, [listKey, hasMore, isLoading]);
// 刷新方法 - 先获取正确类型的帖子,再刷新
const refresh = useCallback(async () => {
try {
// 先获取正确类型的帖子
await processPostUseCase.fetchPosts(
{ page: 1, pageSize: DEFAULT_PAGE_SIZE, post_type: getPostType() },
listKey
);
} catch (err) {
console.error('刷新失败:', err);
}
}, [listKey, getPostType]);
// Tab 切换时刷新数据并重置列表
useEffect(() => {
reset();
refresh();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeIndex]);
// 同步 store 中的帖子状态到本地(用于点赞、收藏等状态更新)
useEffect(() => {
if (posts.length === 0) return;
// 更新 postIdsRef
const currentPostIds = new Set(posts.map(p => p.id));
postIdsRef.current = currentPostIds;
}, [posts, storePosts]);
// 将获取到的原始帖子与 store 中的状态合并
// 这样 UI 始终显示的是 store 中的最新状态(包括点赞、收藏等)
const displayPosts = useMemo(() => {
return posts.map(post => {
const storePost = postsMap.get(post.id);
if (storePost) {
// 如果 store 中有这个帖子,使用 store 的最新状态
return storePost;
}
// 如果 store 中没有这个帖子,使用原始数据
return post;
});
}, [posts, postsMap]);
// 根据屏幕尺寸确定网格列数
const gridColumns = useMemo(() => {
@@ -242,20 +279,28 @@ export const HomeScreen: React.FC = () => {
};
// 点赞帖子
const handleLike = (post: Post) => {
if (post.is_liked) {
unlikePost(post.id);
} else {
likePost(post.id);
const handleLike = async (post: Post) => {
try {
if (post.is_liked) {
await processPostUseCase.unlikePost(post.id);
} else {
await processPostUseCase.likePost(post.id);
}
} catch (error) {
console.error('点赞操作失败:', error);
}
};
// 收藏帖子
const handleBookmark = (post: Post) => {
if (post.is_favorited) {
unfavoritePost(post.id);
} else {
favoritePost(post.id);
const handleBookmark = async (post: Post) => {
try {
if (post.is_favorited) {
await processPostUseCase.unfavoritePost(post.id);
} else {
await processPostUseCase.favoritePost(post.id);
}
} catch (error) {
console.error('收藏操作失败:', error);
}
};
@@ -272,7 +317,7 @@ export const HomeScreen: React.FC = () => {
Alert.alert('已复制', '帖子链接已复制到剪贴板');
};
// 删除帖子 - 由于 posts 来自 Hook需要刷新列表
// 删除帖子 - 删除后刷新列表
const handleDeletePost = async (postId: string) => {
try {
const success = await postService.deletePost(postId);
@@ -377,7 +422,7 @@ export const HomeScreen: React.FC = () => {
const columnHeights: number[] = Array(gridColumns).fill(0);
// 防御性检查:确保 posts 存在且是数组
if (!posts || !Array.isArray(posts) || posts.length === 0) {
if (!displayPosts || !Array.isArray(displayPosts) || displayPosts.length === 0) {
return columns;
}
@@ -385,7 +430,7 @@ export const HomeScreen: React.FC = () => {
const totalGap = (gridColumns - 1) * responsiveGap;
const columnWidth = (width - responsivePadding * 2 - totalGap) / gridColumns;
posts.forEach((post) => {
displayPosts.forEach((post) => {
const postHeight = estimatePostHeight(post, columnWidth);
// 找到当前高度最小的列
@@ -395,7 +440,7 @@ export const HomeScreen: React.FC = () => {
});
return columns;
}, [posts, gridColumns, width, responsiveGap, responsivePadding]);
}, [displayPosts, gridColumns, width, responsiveGap, responsivePadding]);
// 渲染单列帖子
const renderWaterfallColumn = (column: Post[], columnIndex: number) => (
@@ -461,7 +506,7 @@ export const HomeScreen: React.FC = () => {
gap={{ xs: 8, sm: 12, md: 16, lg: 20, xl: 24 }}
containerStyle={{ paddingHorizontal: responsivePadding, paddingBottom: 80 }}
>
{posts.map(post => {
{displayPosts.map(post => {
const authorId = post.author?.id || '';
const isPostAuthor = currentUser?.id === authorId;
return (
@@ -507,7 +552,7 @@ export const HomeScreen: React.FC = () => {
// 移动端和宽屏都使用单列 FlatList宽屏下居中显示
return (
<FlatList
data={posts}
data={displayPosts}
renderItem={renderPostList}
keyExtractor={item => item.id}
contentContainerStyle={[

View File

@@ -32,6 +32,7 @@ import { Post, Comment, VoteResultDTO } from '../../types';
import { useUserStore } from '../../stores';
import { useCurrentUser } from '../../stores/authStore';
import { postService, commentService, uploadService, authService, showPrompt, voteService } from '../../services';
import { processPostUseCase } from '../../core/usecases/ProcessPostUseCase';
import { useCursorPagination } from '../../hooks/useCursorPagination';
import { CommentItem, VoteCard } from '../../components/business';
import { Avatar, Button, Loading, EmptyState, Text, ImageGallery, ImageGrid, ImageGridItem, AdaptiveLayout } from '../../components/common';
@@ -62,15 +63,6 @@ export const PostDetailScreen: React.FC = () => {
const responsivePadding = useResponsiveSpacing({ xs: 12, sm: 14, md: 16, lg: 20, xl: 24, '2xl': 28, '3xl': 32, '4xl': 40 });
const responsiveGap = useResponsiveSpacing({ xs: 8, sm: 10, md: 12, lg: 16, xl: 20, '2xl': 24, '3xl': 28, '4xl': 32 });
const {
likePost,
unlikePost,
favoritePost,
unfavoritePost,
likeComment,
unlikeComment
} = useUserStore();
const currentUser = useCurrentUser();
const [post, setPost] = useState<Post | null>(null);
@@ -144,14 +136,15 @@ export const PostDetailScreen: React.FC = () => {
}
try {
// 从API获取帖子详情
const postData = await postService.getPost(postId);
// 使用 ProcessPostUseCase 获取帖子详情
const postData = await processPostUseCase.fetchPostById(postId);
if (postData) {
setPost(postData);
// 类型转换:将 core/entities/Post 转换为 PostDTO 以保持兼容性
setPost(postData as unknown as Post);
// 初始化关注状态
if (postData.author) {
setIsFollowing(postData.author.is_following || false);
setIsFollowingMe(postData.author.is_following_me || false);
setIsFollowing((postData.author as any).is_following || false);
setIsFollowingMe((postData.author as any).is_following_me || false);
}
// 只在首次加载时记录浏览量
if (recordView && !hasRecordedView.current) {
@@ -163,7 +156,7 @@ export const PostDetailScreen: React.FC = () => {
}
// 如果是投票帖子,立即加载投票数据
if (postData.is_vote) {
if ((postData as any).is_vote) {
setIsVoteLoading(true);
try {
const voteData = await voteService.getVoteResult(postId);
@@ -184,8 +177,8 @@ export const PostDetailScreen: React.FC = () => {
setPost(updatedPost);
// 初始化关注状态
if (updatedPost.author) {
setIsFollowing(updatedPost.author.is_following || false);
setIsFollowingMe(updatedPost.author.is_following_me || false);
setIsFollowing((updatedPost.author as any).is_following || false);
setIsFollowingMe((updatedPost.author as any).is_following_me || false);
}
}
}
@@ -361,10 +354,11 @@ export const PostDetailScreen: React.FC = () => {
try {
if (oldIsLiked) {
await unlikePost(post.id);
await processPostUseCase.unlikePost(post.id);
} else {
await likePost(post.id);
await processPostUseCase.likePost(post.id);
}
// UseCase 会更新 store本地状态已经是乐观更新的无需再次更新
} catch (error) {
console.error('点赞操作失败:', error);
// 失败时回滚状态
@@ -374,7 +368,7 @@ export const PostDetailScreen: React.FC = () => {
likes_count: oldLikesCount
} : null);
}
}, [post, likePost, unlikePost]);
}, [post]);
// 收藏帖子
const handleFavorite = useCallback(async () => {
@@ -393,10 +387,11 @@ export const PostDetailScreen: React.FC = () => {
try {
if (oldIsFavorited) {
await unfavoritePost(post.id);
await processPostUseCase.unfavoritePost(post.id);
} else {
await favoritePost(post.id);
await processPostUseCase.favoritePost(post.id);
}
// UseCase 会更新 store本地状态已经是乐观更新的无需再次更新
} catch (error) {
console.error('收藏操作失败:', error);
// 失败时回滚状态
@@ -406,7 +401,7 @@ export const PostDetailScreen: React.FC = () => {
favorites_count: oldFavoritesCount
} : null);
}
}, [post, favoritePost, unfavoritePost]);
}, [post]);
// 分享帖子
const handleShare = useCallback(async () => {
@@ -527,17 +522,13 @@ export const PostDetailScreen: React.FC = () => {
onPress: async () => {
setIsDeleting(true);
try {
const success = await postService.deletePost(post.id);
if (success) {
Alert.alert('删除成功', '帖子已删除', [
{
text: '确定',
onPress: () => navigation.goBack(),
},
]);
} else {
Alert.alert('删除失败', '删除帖子时发生错误,请稍后重试');
}
await processPostUseCase.deletePost(post.id);
Alert.alert('删除成功', '帖子已删除', [
{
text: '确定',
onPress: () => navigation.goBack(),
},
]);
} catch (error) {
console.error('删除帖子失败:', error);
Alert.alert('删除失败', '删除帖子时发生错误,请稍后重试');
@@ -817,9 +808,9 @@ export const PostDetailScreen: React.FC = () => {
try {
if (oldIsLiked) {
await unlikeComment(comment.id);
await commentService.unlikeComment(comment.id);
} else {
await likeComment(comment.id);
await commentService.likeComment(comment.id);
}
} catch (error) {
console.error('评论点赞操作失败:', error);

View File

@@ -265,7 +265,7 @@ export const MessageListScreen: React.FC = () => {
}, [isFocused]);
// 【新架构】使用focus刷新hook从ChatScreen返回时自动刷新未读数
useMessageListRefresh(isFocused);
useMessageListRefresh();
// 同步未读数到userStore用于TabBar角标显示
useEffect(() => {

View File

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

View File

@@ -9,8 +9,6 @@ import {
View,
TouchableOpacity,
Image,
findNodeHandle,
UIManager,
GestureResponderEvent,
StyleSheet,
Dimensions,
@@ -219,22 +217,17 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
// 处理长按并获取位置
const handleLongPress = () => {
if (bubbleRef.current) {
const node = findNodeHandle(bubbleRef.current);
if (node) {
UIManager.measureInWindow(node, (x, y, width, height) => {
const position: MenuPosition = {
x,
y,
width,
height,
pressX: pressPositionRef.current.x,
pressY: pressPositionRef.current.y,
};
onLongPress(message, position);
});
} else {
onLongPress(message);
}
bubbleRef.current.measureInWindow((x, y, width, height) => {
const position: MenuPosition = {
x,
y,
width,
height,
pressX: pressPositionRef.current.x,
pressY: pressPositionRef.current.y,
};
onLongPress(message, position);
});
} else {
onLongPress(message);
}

View File

@@ -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;
// 消息映射,用于查找被回复的消息

View File

@@ -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(() => (

View File

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

View File

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

View File

@@ -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;
}
}
}
// 导出消息服务实例

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
// ============================================