refactor: migrate post operations to use case architecture with differential updates
Some checks failed
Frontend CI / build-and-push-web (push) Failing after 3m11s
Frontend CI / ota-android (push) Failing after 16m3s
Frontend CI / build-android-apk (push) Has been cancelled

Migrate post-related operations from direct service/store calls to a new use case architecture featuring differential updates. Key changes include: (1) introduce ProcessPostUseCase to handle like, unlike, favorite, unfavorite, delete, and fetch operations with proper state management, (2) add useDifferentialPosts hook for efficient list rendering with minimal re-renders, (3) add PostDiffCalculator and PostUpdateBatcher in infrastructure layer for incremental updates, (4) deprecate NotificationItem component in favor of SystemMessageItem, (5) remove deprecated interactive methods from userStore, (6) add posts_cache table to local datasource, (7) clean up legacy MessageDTO and ConversationDTO types, (8) remove deprecated hook useBreakpointCheck, (9) refactor MessageBubble to use measureInWindow directly for better React Native compatibility
This commit is contained in:
lafay
2026-03-21 20:55:36 +08:00
parent 25071d2303
commit d273569911
30 changed files with 4395 additions and 572 deletions

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}