From 5614b4078a7c3452d43506cd953dfdc291dc9d03 Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Wed, 1 Apr 2026 02:17:36 +0800 Subject: [PATCH] feat(profile): add chat settings and language options to profile settings - Introduced hrefProfileChatSettings() for navigation to chat settings. - Updated SettingsScreen to include new settings items for chat settings, language selection, data storage, terms, and privacy policy. - Enhanced user experience by providing alerts for language and data storage options. This update improves the profile settings interface by adding more customization options for users. --- app/(app)/(tabs)/profile/chat-settings.tsx | 5 + src/navigation/hrefs.ts | 4 + src/screens/profile/ChatSettingsScreen.tsx | 499 +++++++++++++++++++++ src/screens/profile/SettingsScreen.tsx | 56 ++- src/services/authService.ts | 12 +- src/services/wsService.ts | 3 + src/stores/authStore.ts | 11 +- src/stores/callStore.ts | 35 ++ src/stores/group/GroupManager.ts | 326 ++++++++++++++ src/stores/group/groupStore.ts | 287 ++++++++++++ src/stores/group/hooks.ts | 140 ++++++ src/stores/group/index.ts | 27 ++ src/stores/groupManager.ts | 400 +---------------- src/stores/index.ts | 58 ++- src/stores/post/PostManager.ts | 174 +++++++ src/stores/post/hooks.ts | 102 +++++ src/stores/post/index.ts | 25 ++ src/stores/post/postStore.ts | 232 ++++++++++ src/stores/postManager.ts | 226 +--------- src/stores/user/UserManager.ts | 178 ++++++++ src/stores/user/hooks.ts | 136 ++++++ src/stores/user/index.ts | 27 ++ src/stores/user/userStore.ts | 211 +++++++++ src/stores/userManager.ts | 207 +-------- src/stores/utils/cache.ts | 42 ++ 25 files changed, 2622 insertions(+), 801 deletions(-) create mode 100644 app/(app)/(tabs)/profile/chat-settings.tsx create mode 100644 src/screens/profile/ChatSettingsScreen.tsx create mode 100644 src/stores/group/GroupManager.ts create mode 100644 src/stores/group/groupStore.ts create mode 100644 src/stores/group/hooks.ts create mode 100644 src/stores/group/index.ts create mode 100644 src/stores/post/PostManager.ts create mode 100644 src/stores/post/hooks.ts create mode 100644 src/stores/post/index.ts create mode 100644 src/stores/post/postStore.ts create mode 100644 src/stores/user/UserManager.ts create mode 100644 src/stores/user/hooks.ts create mode 100644 src/stores/user/index.ts create mode 100644 src/stores/user/userStore.ts create mode 100644 src/stores/utils/cache.ts diff --git a/app/(app)/(tabs)/profile/chat-settings.tsx b/app/(app)/(tabs)/profile/chat-settings.tsx new file mode 100644 index 0000000..1c5ec98 --- /dev/null +++ b/app/(app)/(tabs)/profile/chat-settings.tsx @@ -0,0 +1,5 @@ +import { ChatSettingsScreen } from '../../../../src/screens/profile/ChatSettingsScreen'; + +export default function ChatSettingsRoute() { + return ; +} diff --git a/src/navigation/hrefs.ts b/src/navigation/hrefs.ts index 00243b2..c543dda 100644 --- a/src/navigation/hrefs.ts +++ b/src/navigation/hrefs.ts @@ -61,6 +61,10 @@ export function hrefProfileBlocked(): string { return '/profile/blocked-users'; } +export function hrefProfileChatSettings(): string { + return '/profile/chat-settings'; +} + export function hrefProfileMyPosts(): string { return '/profile/my-posts'; } diff --git a/src/screens/profile/ChatSettingsScreen.tsx b/src/screens/profile/ChatSettingsScreen.tsx new file mode 100644 index 0000000..926250a --- /dev/null +++ b/src/screens/profile/ChatSettingsScreen.tsx @@ -0,0 +1,499 @@ +/** + * 聊天设置页 ChatSettingsScreen + * 胡萝卜BBS - 聊天个性化设置 + */ + +import React, { useMemo, useState, useCallback } from 'react'; +import { + View, + StyleSheet, + TouchableOpacity, + ScrollView, + Dimensions, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useRouter } from 'expo-router'; +import { MaterialCommunityIcons } from '@expo/vector-icons'; +import { + useAppColors, + spacing, + fontSizes, + borderRadius, + type AppColors, +} from '../../theme'; +import { Text } from '../../components/common'; +import { useResponsiveSpacing } from '../../hooks'; + +const { width: SCREEN_WIDTH } = Dimensions.get('window'); +const CARD_MAX_WIDTH = 720; + +// 主题配置 +const THEMES = [ + { id: 'default', name: '默认绿', primary: '#4CAF50', secondary: '#E8F5E9', bubble: '#DCF8C6', icon: '🏠' }, + { id: 'yellow', name: '活力黄', primary: '#FFC107', secondary: '#FFF8E1', bubble: '#FFECB3', icon: '🐥' }, + { id: 'blue', name: '清新蓝', primary: '#2196F3', secondary: '#E3F2FD', bubble: '#BBDEFB', icon: '☃️' }, + { id: 'purple', name: '梦幻紫', primary: '#9C27B0', secondary: '#F3E5F5', bubble: '#E1BEE7', icon: '💎' }, + { id: 'teal', name: '自然青', primary: '#009688', secondary: '#E0F2F1', bubble: '#B2DFDB', icon: '🦁' }, +] as const; + +interface SliderProps { + value: number; + min: number; + max: number; + onValueChange: (value: number) => void; + leftLabel?: string; + rightLabel?: string; + showValue?: boolean; +} + +// 自定义滑块组件 +const Slider: React.FC = ({ + value, + min, + max, + onValueChange, + leftLabel, + rightLabel, + showValue = true, +}) => { + const colors = useAppColors(); + const styles = useMemo(() => createSliderStyles(colors), [colors]); + + const percentage = ((value - min) / (max - min)) * 100; + + const handlePress = useCallback((event: any) => { + const { locationX } = event.nativeEvent; + const sliderWidth = SCREEN_WIDTH - 64; // 考虑边距 + const newPercentage = Math.max(0, Math.min(100, (locationX / sliderWidth) * 100)); + const newValue = Math.round(min + (newPercentage / 100) * (max - min)); + onValueChange(newValue); + }, [min, max, onValueChange]); + + return ( + + + {leftLabel && {leftLabel}} + {showValue && {value}} + {rightLabel && {rightLabel}} + + + + + + + + + ); +}; + +function createSliderStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + marginVertical: spacing.sm, + }, + labelRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: spacing.xs, + }, + label: { + fontSize: fontSizes.sm, + color: colors.text.secondary, + }, + value: { + fontSize: fontSizes.md, + fontWeight: '600', + color: colors.primary.main, + }, + trackContainer: { + height: 24, + justifyContent: 'center', + }, + track: { + height: 4, + backgroundColor: colors.divider, + borderRadius: 2, + }, + fill: { + height: '100%', + borderRadius: 2, + }, + thumb: { + position: 'absolute', + width: 20, + height: 20, + borderRadius: 10, + marginLeft: -10, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.2, + shadowRadius: 2, + elevation: 3, + }, + }); +} + +function createStyles(colors: AppColors) { + return StyleSheet.create({ + container: { + flex: 1, + backgroundColor: colors.background.default, + }, + scrollContent: { + paddingVertical: spacing.md, + }, + section: { + marginBottom: spacing.lg, + maxWidth: CARD_MAX_WIDTH, + alignSelf: 'center', + width: '100%', + paddingHorizontal: spacing.lg, + }, + sectionTitle: { + fontSize: fontSizes.sm, + fontWeight: '600', + color: colors.text.secondary, + marginBottom: spacing.sm, + marginLeft: spacing.xs, + }, + card: { + backgroundColor: colors.background.paper, + borderRadius: borderRadius.lg, + padding: spacing.md, + }, + // 聊天预览样式 + previewContainer: { + backgroundColor: '#E8F5E9', + borderRadius: borderRadius.lg, + padding: spacing.md, + minHeight: 140, + }, + previewMessage: { + backgroundColor: '#FFFFFF', + borderRadius: borderRadius.md, + padding: spacing.sm, + marginBottom: spacing.sm, + maxWidth: '80%', + alignSelf: 'flex-start', + }, + previewReply: { + borderRadius: borderRadius.md, + padding: spacing.sm, + maxWidth: '80%', + alignSelf: 'flex-end', + }, + previewText: { + fontSize: fontSizes.sm, + color: '#333', + }, + previewTime: { + fontSize: fontSizes.xs, + color: '#999', + marginTop: 2, + alignSelf: 'flex-end', + }, + // 主题选择样式 + themeList: { + flexDirection: 'row', + gap: spacing.sm, + }, + themeItem: { + width: 70, + height: 90, + borderRadius: borderRadius.md, + borderWidth: 2, + borderColor: 'transparent', + overflow: 'hidden', + padding: spacing.xs, + }, + themeItemActive: { + borderColor: colors.primary.main, + }, + themePreview: { + flex: 1, + justifyContent: 'center', + gap: 4, + }, + themeBubble1: { + height: 12, + borderRadius: 6, + width: '70%', + }, + themeBubble2: { + height: 12, + borderRadius: 6, + width: '50%', + alignSelf: 'flex-end', + }, + themeIcon: { + position: 'absolute', + bottom: 4, + right: 4, + width: 18, + height: 18, + borderRadius: 9, + justifyContent: 'center', + alignItems: 'center', + }, + themeName: { + fontSize: fontSizes.xs, + textAlign: 'center', + marginTop: 4, + color: colors.text.secondary, + }, + // 设置项样式 + settingItem: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: spacing.md, + borderBottomWidth: 1, + borderBottomColor: colors.divider, + }, + settingItemLast: { + borderBottomWidth: 0, + }, + settingItemLeft: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.md, + }, + iconContainer: { + width: 36, + height: 36, + borderRadius: borderRadius.md, + backgroundColor: colors.primary.light + '20', + justifyContent: 'center', + alignItems: 'center', + }, + settingTitle: { + fontSize: fontSizes.md, + color: colors.text.primary, + }, + settingSubtitle: { + fontSize: fontSizes.sm, + color: colors.text.secondary, + marginTop: 2, + }, + // 开关样式 + switch: { + width: 50, + height: 28, + borderRadius: 14, + backgroundColor: colors.divider, + padding: 2, + }, + switchActive: { + backgroundColor: colors.primary.main, + }, + switchThumb: { + width: 24, + height: 24, + borderRadius: 12, + backgroundColor: '#fff', + }, + }); +} + +export const ChatSettingsScreen: React.FC = () => { + const router = useRouter(); + const colors = useAppColors(); + const styles = useMemo(() => createStyles(colors), [colors]); + const responsivePadding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 }); + + // 状态 + const [fontSize, setFontSize] = useState(16); + const [messageRadius, setMessageRadius] = useState(17); + const [selectedTheme, setSelectedTheme] = useState(0); + const [nightMode, setNightMode] = useState(false); + + const currentTheme = THEMES[selectedTheme]; + + // 渲染聊天预览 + const renderChatPreview = () => ( + + 预览 + + + MiMQy + + 早上好!👋 + + + 你知道现在几点吗? + + AM12:42 + + + + 现在是东京的早晨😎 + + AM12:57 ✓ + + + + ); + + // 渲染字号滑块 + const renderFontSizeSlider = () => ( + + 消息字号 + + + + + ); + + // 渲染主题颜色选择 + const renderThemeColors = () => ( + + 主题颜色 + + {THEMES.map((theme, index) => ( + setSelectedTheme(index)} + activeOpacity={0.8} + > + + + + + + {theme.icon} + + + ))} + + + ); + + // 渲染圆角滑块 + const renderRadiusSlider = () => ( + + 消息圆角 + + + + + ); + + // 渲染其他设置项 + const renderOtherSettings = () => ( + + 其他 + + + + + + + + 更改聊天壁纸 + + + + + + + + + + + + 更改名称颜色 + MiMQy + + + + + + + ); + + // 渲染夜间模式开关 + const renderNightMode = () => ( + + + + + + + + 切换到夜间模式 + + setNightMode(!nightMode)} + > + + + + + + ); + + // 渲染浏览其他主题 + const renderBrowseThemes = () => ( + + + + + + + + 浏览其他主题 + + + + + + ); + + return ( + + + {renderFontSizeSlider()} + {renderChatPreview()} + {renderThemeColors()} + {renderRadiusSlider()} + {renderOtherSettings()} + {renderNightMode()} + {renderBrowseThemes()} + + + ); +}; + +export default ChatSettingsScreen; diff --git a/src/screens/profile/SettingsScreen.tsx b/src/screens/profile/SettingsScreen.tsx index 749505f..bfc9d0d 100644 --- a/src/screens/profile/SettingsScreen.tsx +++ b/src/screens/profile/SettingsScreen.tsx @@ -23,19 +23,17 @@ import { borderRadius, useThemePreference, useSetThemePreference, - useThemeStore, type AppColors, } from '../../theme'; import { useAuthStore } from '../../stores'; import { Text } from '../../components/common'; import { useResponsive, useResponsiveSpacing } from '../../hooks'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import * as hrefs from '../../navigation/hrefs'; // 设置卡片最大宽度 const SETTINGS_CARD_MAX_WIDTH = 720; -import * as hrefs from '../../navigation/hrefs'; - interface SettingsItem { key: string; title: string; @@ -64,6 +62,8 @@ const SETTINGS_GROUPS_BASE = [ icon: 'bell-outline', items: [ { key: 'notification_settings', title: '通知设置', icon: 'bell-cog-outline', showArrow: true, subtitle: '推送、震动、提示音' }, + { key: 'language', title: '语言设置', icon: 'translate', showArrow: true, subtitle: '简体中文' }, + { key: 'data_storage', title: '数据与存储', icon: 'database-outline', showArrow: true }, ], }, { @@ -72,6 +72,8 @@ const SETTINGS_GROUPS_BASE = [ items: [ { key: 'about', title: '关于我们', icon: 'information-outline', showArrow: true, subtitle: `版本 ${APP_VERSION}` }, { key: 'help', title: '帮助与反馈', icon: 'help-circle-outline', showArrow: true }, + { key: 'terms', title: '用户协议', icon: 'file-document-outline', showArrow: true }, + { key: 'privacy_policy', title: '隐私政策', icon: 'shield-outline', showArrow: true }, ], }, ]; @@ -196,9 +198,16 @@ export const SettingsScreen: React.FC = () => { themePreference === 'system' ? '跟随系统' : themePreference === 'dark' ? '深色' : '浅色'; return [ { - title: '显示与外观', + title: '个性化', icon: 'palette-outline', items: [ + { + key: 'chat_settings', + title: '聊天设置', + icon: 'message-text-outline', + showArrow: true, + subtitle: '字号、主题、壁纸', + }, { key: 'theme', title: '主题模式', @@ -214,17 +223,13 @@ export const SettingsScreen: React.FC = () => { const handleItemPress = (key: string) => { switch (key) { + case 'chat_settings': + router.push(hrefs.hrefProfileChatSettings()); + break; case 'theme': { - // 获取调试信息 - const { Appearance } = require('react-native'); - const systemScheme = Appearance?.getColorScheme?.() || 'undefined'; - const resolvedScheme = useThemeStore.getState().resolvedScheme; - const storedPref = useThemeStore.getState().preference; - const storedSystem = useThemeStore.getState().systemScheme; - Alert.alert( '主题模式', - `选择应用界面明暗\n\n[调试信息]\n系统主题: ${systemScheme}\n存储偏好: ${storedPref}\n存储系统: ${storedSystem}\n实际应用: ${resolvedScheme}`, + '选择应用界面明暗', [ { text: '取消', style: 'cancel' }, { @@ -249,6 +254,31 @@ export const SettingsScreen: React.FC = () => { ); break; } + case 'language': { + Alert.alert( + '语言设置', + '选择应用显示语言', + [ + { text: '取消', style: 'cancel' }, + { text: '简体中文', onPress: () => {} }, + { text: '繁體中文', onPress: () => {} }, + { text: 'English', onPress: () => {} }, + ] + ); + break; + } + case 'data_storage': { + Alert.alert('数据与存储', '缓存清理功能即将上线!'); + break; + } + case 'terms': { + Alert.alert('用户协议', '用户协议页面即将上线!'); + break; + } + case 'privacy_policy': { + Alert.alert('隐私政策', '隐私政策页面即将上线!'); + break; + } case 'edit_profile': router.push(hrefs.hrefProfileEdit()); break; @@ -348,7 +378,9 @@ export const SettingsScreen: React.FC = () => { const renderContent = () => ( <> + {/* 设置分组 */} {settingsGroups.map((group) => renderGroup(group))} + {renderLogoutButton()} diff --git a/src/services/authService.ts b/src/services/authService.ts index 0311b8b..090e98d 100644 --- a/src/services/authService.ts +++ b/src/services/authService.ts @@ -228,8 +228,16 @@ class AuthService { try { const response = await api.get('/users/me'); return response.data; - } catch (error) { - console.error('[AuthService] fetchCurrentUserFromAPI 失败:', error); + } catch (error: any) { + // 401 错误(未登录/登录过期)是预期情况,不需要打印错误日志 + const isAuthError = error?.code === 401 || + String(error?.message ?? '').includes('登录') || + String(error?.message ?? '').includes('token') || + String(error?.message ?? '').includes('unauthorized'); + + if (!isAuthError) { + console.error('[AuthService] fetchCurrentUserFromAPI 失败:', error); + } return null; } } diff --git a/src/services/wsService.ts b/src/services/wsService.ts index 13d9d4f..d69b5c6 100644 --- a/src/services/wsService.ts +++ b/src/services/wsService.ts @@ -972,6 +972,9 @@ class WebSocketService { this.connected = false; this.isConnecting = false; this.isFallbackMode = false; + this.reconnectAttempts = 0; // 重置重连计数 + this.lastEventId = ''; // 重置事件ID + this.lastActivityAt = 0; // 重置活动时间 if (this.ws) { this.ws.close(); diff --git a/src/stores/authStore.ts b/src/stores/authStore.ts index 580022d..8b26624 100644 --- a/src/stores/authStore.ts +++ b/src/stores/authStore.ts @@ -215,12 +215,17 @@ export const useAuthStore = create((set) => ({ await authService.logout(); // 2. 停止 SSE wsService.stop(); - // 3. 清除 DB 中的用户缓存(DB 此时一定已初始化) + // 3. 重置通话状态(清理所有事件订阅和定时器) + callStore.getState().reset(); + // 4. 清除 DB 中的用户缓存(DB 此时一定已初始化) await clearCurrentUserCache().catch(() => {}); - // 4. 关闭数据库连接 + // 5. 关闭数据库连接 await closeDatabase(); - // 5. 清除持久化的 userId + // 6. 清除持久化的 userId await clearUserId(); + // 7. 清除 userManager 的内存缓存 + const { userManager } = await import('./userManager'); + userManager.invalidateUsers(); } catch (error) { console.error('[AuthStore] 登出失败:', error); } finally { diff --git a/src/stores/callStore.ts b/src/stores/callStore.ts index ad59b95..aac6371 100644 --- a/src/stores/callStore.ts +++ b/src/stores/callStore.ts @@ -76,6 +76,7 @@ interface CallState { toggleMinimize: () => void; toggleVideo: () => Promise; setVideoEnabled: (enabled: boolean) => Promise; + reset: () => void; } // Module-level variables for timers @@ -821,4 +822,38 @@ export const callStore = create((set, get) => ({ console.error('[CallStore] Failed to toggle video:', err); } }, + + // 重置所有状态,用于登出时清理 + reset: () => { + // 清理所有定时器和资源 + cleanupResources(); + + // 清理 initCall 的订阅 + if (initCallUnsub) { + initCallUnsub(); + initCallUnsub = null; + } + + // 清理 invited 订阅 + if (unsubInvited) { + unsubInvited(); + unsubInvited = null; + } + + // 清理 WebRTC 资源 + webrtcManager.dispose(); + + // 清理已处理的通话ID缓存 + processedCallIds.clear(); + + // 重置状态 + set({ + currentCall: null, + incomingCall: null, + callDuration: 0, + peerStream: null, + localStream: null, + isMinimized: false, + }); + }, })); diff --git a/src/stores/group/GroupManager.ts b/src/stores/group/GroupManager.ts new file mode 100644 index 0000000..0121344 --- /dev/null +++ b/src/stores/group/GroupManager.ts @@ -0,0 +1,326 @@ +/** + * GroupManager - 群组管理核心模块(重构版? + * + * 使用 Zustand store 进行状态管? + * 保持与旧 API 的向后兼? + */ + +import type { + GroupListResponse, + GroupMemberListResponse, + GroupMemberResponse, + GroupResponse, +} from '../../types/dto'; +import { groupService } from '../../services/groupService'; +import { + getAllGroupsCache, + getGroupCache, + getGroupMembersCache, + saveGroupCache, + saveGroupMembersCache, + saveGroupsCache, + saveUsersCache, +} from '../../services/database'; +import { useGroupManagerStore } from './groupStore'; +import { createCacheEntry, isCacheExpired, DEFAULT_TTL } from '../utils/cache'; +import { + IGroupListPagedSource, + IGroupMemberListPagedSource, + createRemoteGroupListSource, + createRemoteGroupMemberListSource, + RemoteGroupListSourceKind, + GROUP_LIST_PAGE_SIZE, + GROUP_MEMBER_LIST_PAGE_SIZE, +} from '../groupListSources'; + +// ==================== 辅助函数 ==================== + +function buildMembersKey(groupId: string, page: number, pageSize: number): string { + return `members:${groupId}:${page}:${pageSize}`; +} + +function buildGroupListResponse(groups: GroupResponse[], pageSize: number): GroupListResponse { + return { + list: groups, + total: groups.length, + page: 1, + page_size: pageSize, + total_pages: 1, + }; +} + +function buildMemberListResponse( + members: GroupMemberResponse[], + page: number, + pageSize: number +): GroupMemberListResponse { + return { + list: members, + total: members.length, + page, + page_size: pageSize, + total_pages: 1, + }; +} + +// ==================== GroupManager ?==================== + +class GroupManager { + // ==================== 群组列表相关 ==================== + + /** + * 获取群组列表 + */ + async getGroups( + page = 1, + pageSize = GROUP_LIST_PAGE_SIZE, + forceRefresh = false + ): Promise { + const store = useGroupManagerStore.getState(); + const entry = store.groupsListEntry; + + // 第一页且有有效缓? + if (page === 1 && !forceRefresh && entry && !isCacheExpired(entry)) { + return buildGroupListResponse(entry.data, pageSize); + } + + // 第一页且缓存过期:后台刷? + if (page === 1 && !forceRefresh && entry && isCacheExpired(entry)) { + this.refreshGroupsInBackground(page, pageSize); + return buildGroupListResponse(entry.data, pageSize); + } + + // 第一页:尝试本地缓存 + if (page === 1 && !forceRefresh) { + const localGroups = await getAllGroupsCache(); + if (localGroups.length > 0) { + const newEntry = createCacheEntry(localGroups, DEFAULT_TTL.GROUP); + store.setGroupsListEntry(newEntry); + this.refreshGroupsInBackground(page, pageSize); + return buildGroupListResponse(localGroups, pageSize); + } + } + + // 发起请求 + return this.dedupe(`groups:list:${page}:${pageSize}`, async () => { + const store = useGroupManagerStore.getState(); + const response = await groupService.getGroups(page, pageSize); + const groups = response.list || []; + if (page === 1) { + store.setGroupsList(groups); + } + await saveGroupsCache(groups).catch(() => {}); + return response; + }); + } + + private refreshGroupsInBackground(page = 1, pageSize = GROUP_LIST_PAGE_SIZE): void { + this.dedupe(`groups:bg:list:${page}:${pageSize}`, async () => { + const store = useGroupManagerStore.getState(); + const response = await groupService.getGroups(page, pageSize); + const groups = response.list || []; + if (page === 1) { + store.setGroupsList(groups); + } + saveGroupsCache(groups).catch(() => {}); + return response; + }).catch((error) => { + console.error('[GroupManager] refreshGroupsInBackground error:', error); + }); + } + + /** + * 创建群组列表数据源(用于 Sources 模式? + */ + createGroupListSource( + kind: RemoteGroupListSourceKind = 'cursor', + pageSize: number = GROUP_LIST_PAGE_SIZE + ): IGroupListPagedSource { + return createRemoteGroupListSource(kind, pageSize); + } + + // ==================== 群组详情相关 ==================== + + /** + * 获取群组详情 + */ + async getGroup(groupId: string, forceRefresh = false): Promise { + const store = useGroupManagerStore.getState(); + const entry = store.groupsMap.get(groupId); + + // 有效缓存 + if (!forceRefresh && entry && !isCacheExpired(entry)) { + return entry.data; + } + + // 过期缓存:后台刷? + if (!forceRefresh && entry && isCacheExpired(entry)) { + this.refreshGroupInBackground(groupId); + return entry.data; + } + + // 尝试本地缓存 + if (!forceRefresh) { + const localGroup = await getGroupCache(groupId); + if (localGroup) { + store.setGroup(groupId, localGroup); + this.refreshGroupInBackground(groupId); + return localGroup; + } + } + + // 发起请求 + return this.dedupe(`groups:detail:${groupId}`, async () => { + const store = useGroupManagerStore.getState(); + const group = await groupService.getGroup(groupId); + store.setGroup(groupId, group); + await saveGroupCache(group).catch(() => {}); + return group; + }); + } + + private refreshGroupInBackground(groupId: string): void { + this.dedupe(`groups:detail:bg:${groupId}`, async () => { + const store = useGroupManagerStore.getState(); + const group = await groupService.getGroup(groupId); + store.setGroup(groupId, group); + saveGroupCache(group).catch(() => {}); + return group; + }).catch((error) => { + console.error('[GroupManager] refreshGroupInBackground error:', error); + }); + } + + // ==================== 群组成员相关 ==================== + + /** + * 获取群组成员列表 + */ + async getMembers( + groupId: string, + page = 1, + pageSize = GROUP_MEMBER_LIST_PAGE_SIZE, + forceRefresh = false + ): Promise { + const key = buildMembersKey(groupId, page, pageSize); + const store = useGroupManagerStore.getState(); + const entry = store.membersMap.get(key); + + // 有效缓存 + if (!forceRefresh && entry && !isCacheExpired(entry)) { + return buildMemberListResponse(entry.data, page, pageSize); + } + + // 过期缓存:后台刷? + if (!forceRefresh && entry && isCacheExpired(entry)) { + this.refreshMembersInBackground(groupId, page, pageSize); + return buildMemberListResponse(entry.data, page, pageSize); + } + + // 第一页:尝试本地缓存 + if (page === 1 && !forceRefresh) { + const localMembers = await getGroupMembersCache(groupId); + if (localMembers.length > 0) { + store.setMembers(groupId, localMembers, page, pageSize); + this.refreshMembersInBackground(groupId, page, pageSize); + return buildMemberListResponse(localMembers, page, pageSize); + } + } + + // 发起请求 + return this.dedupe(`groups:members:${key}`, async () => { + const store = useGroupManagerStore.getState(); + const response = await groupService.getMembers(groupId, page, pageSize); + const members = response.list || []; + store.setMembers(groupId, members, page, pageSize); + if (page === 1) { + await saveGroupMembersCache(groupId, members).catch(() => {}); + } + await saveUsersCache( + members + .map((member) => member.user) + .filter((user): user is NonNullable => Boolean(user)) + ).catch(() => {}); + return response; + }); + } + + private refreshMembersInBackground( + groupId: string, + page = 1, + pageSize = GROUP_MEMBER_LIST_PAGE_SIZE + ): void { + const key = buildMembersKey(groupId, page, pageSize); + this.dedupe(`groups:members:bg:${key}`, async () => { + const store = useGroupManagerStore.getState(); + const response = await groupService.getMembers(groupId, page, pageSize); + const members = response.list || []; + store.setMembers(groupId, members, page, pageSize); + if (page === 1) { + saveGroupMembersCache(groupId, members).catch(() => {}); + } + saveUsersCache( + members + .map((member) => member.user) + .filter((user): user is NonNullable => Boolean(user)) + ).catch(() => {}); + return response; + }).catch((error) => { + console.error('[GroupManager] refreshMembersInBackground error:', error); + }); + } + + /** + * 创建群组成员列表数据源(用于 Sources 模式? + */ + createGroupMemberListSource( + groupId: string, + kind: RemoteGroupListSourceKind = 'cursor', + pageSize: number = GROUP_MEMBER_LIST_PAGE_SIZE + ): IGroupMemberListPagedSource { + return createRemoteGroupMemberListSource(groupId, kind, pageSize); + } + + // ==================== 缓存管理 ==================== + + /** + * 使缓存失? + */ + invalidate(groupId?: string): void { + if (!groupId) { + useGroupManagerStore.getState().invalidateAll(); + return; + } + useGroupManagerStore.getState().invalidateGroup(groupId); + } + + /** + * 清除所有数? + */ + clear(): void { + useGroupManagerStore.getState().reset(); + } + + // ==================== 请求去重 ==================== + + private async dedupe(key: string, fetcher: () => Promise): Promise { + const store = useGroupManagerStore.getState(); + const pending = store.getPendingRequest(key); + if (pending) return pending; + + const request = fetcher().finally(() => { + useGroupManagerStore.getState().deletePendingRequest(key); + }); + store.setPendingRequest(key, request); + return request; + } +} + +// ==================== 单例导出 ==================== + +export const groupManager = new GroupManager(); + +// 导出类以支持类型检查和测试 +export { GroupManager }; + +export default groupManager; diff --git a/src/stores/group/groupStore.ts b/src/stores/group/groupStore.ts new file mode 100644 index 0000000..4fbff22 --- /dev/null +++ b/src/stores/group/groupStore.ts @@ -0,0 +1,287 @@ +/** + * 群组状态 Zustand Store + */ + +import { create } from 'zustand'; +import type { + GroupResponse, + GroupMemberResponse, +} from '../../types/dto'; +import { CacheEntry, createCacheEntry, isCacheExpired, DEFAULT_TTL } from '../utils/cache'; + +// ==================== 状态接口 ==================== + +export interface GroupManagerState { + // 群组列表缓存 + groupsListEntry: CacheEntry | null; + + // 群组详情缓存 + groupsMap: Map>; + + // 群组成员缓存 (key: members:groupId:page:pageSize) + membersMap: Map>; + + // 加载状态 + isLoadingGroups: boolean; + loadingGroupIds: Set; + loadingMemberKeys: Set; + + // 待处理请求(去重) + pendingRequests: Map>; +} + +export interface GroupManagerActions { + // 获取状态 + getGroupsList: () => GroupResponse[] | null; + getGroup: (groupId: string) => GroupResponse | null; + getMembers: (groupId: string, page?: number, pageSize?: number) => GroupMemberResponse[] | null; + isGroupsListExpired: () => boolean; + isGroupExpired: (groupId: string) => boolean; + isMembersExpired: (groupId: string, page?: number, pageSize?: number) => boolean; + isLoadingGroup: (groupId: string) => boolean; + isLoadingMembers: (groupId: string, page?: number, pageSize?: number) => boolean; + + // 设置状态 + setGroupsList: (groups: GroupResponse[]) => void; + setGroupsListEntry: (entry: CacheEntry) => void; + setGroup: (groupId: string, group: GroupResponse) => void; + setMembers: (groupId: string, members: GroupMemberResponse[], page?: number, pageSize?: number) => void; + setLoadingGroups: (loading: boolean) => void; + setLoadingGroup: (groupId: string, loading: boolean) => void; + setLoadingMembers: (groupId: string, loading: boolean, page?: number, pageSize?: number) => void; + + // 缓存管理 + invalidateGroupsList: () => void; + invalidateGroup: (groupId: string) => void; + invalidateMembers: (groupId: string) => void; + invalidateAll: () => void; + + // 请求去重 + getPendingRequest: (key: string) => Promise | undefined; + setPendingRequest: (key: string, request: Promise) => void; + deletePendingRequest: (key: string) => void; + clearPendingRequests: () => void; + + // 重置 + reset: () => void; +} + +// ==================== 辅助函数 ==================== + +function buildMembersKey(groupId: string, page: number = 1, pageSize: number = 20): string { + return `members:${groupId}:${page}:${pageSize}`; +} + +// ==================== 初始状态 ==================== + +const initialState: GroupManagerState = { + groupsListEntry: null, + groupsMap: new Map(), + membersMap: new Map(), + isLoadingGroups: false, + loadingGroupIds: new Set(), + loadingMemberKeys: new Set(), + pendingRequests: new Map(), +}; + +// ==================== Store 创建 ==================== + +export type GroupManagerStore = GroupManagerState & GroupManagerActions; + +export const useGroupManagerStore = create((set, get) => ({ + // ==================== 初始状态 ==================== + ...initialState, + + // ==================== 获取状态 ==================== + + getGroupsList: () => { + const entry = get().groupsListEntry; + if (!entry) return null; + if (isCacheExpired(entry)) return null; + return entry.data; + }, + + getGroup: (groupId: string) => { + const entry = get().groupsMap.get(groupId); + if (!entry) return null; + if (isCacheExpired(entry)) return null; + return entry.data; + }, + + getMembers: (groupId: string, page = 1, pageSize = 20) => { + const key = buildMembersKey(groupId, page, pageSize); + const entry = get().membersMap.get(key); + if (!entry) return null; + if (isCacheExpired(entry)) return null; + return entry.data; + }, + + isGroupsListExpired: () => { + const entry = get().groupsListEntry; + if (!entry) return true; + return isCacheExpired(entry); + }, + + isGroupExpired: (groupId: string) => { + const entry = get().groupsMap.get(groupId); + if (!entry) return true; + return isCacheExpired(entry); + }, + + isMembersExpired: (groupId: string, page = 1, pageSize = 20) => { + const key = buildMembersKey(groupId, page, pageSize); + const entry = get().membersMap.get(key); + if (!entry) return true; + return isCacheExpired(entry); + }, + + isLoadingGroup: (groupId: string) => { + return get().loadingGroupIds.has(groupId); + }, + + isLoadingMembers: (groupId: string, page = 1, pageSize = 20) => { + const key = buildMembersKey(groupId, page, pageSize); + return get().loadingMemberKeys.has(key); + }, + + // ==================== 设置状态 ==================== + + setGroupsList: (groups: GroupResponse[]) => { + set({ + groupsListEntry: createCacheEntry(groups, DEFAULT_TTL.GROUP), + }); + }, + + setGroupsListEntry: (entry: CacheEntry) => { + set({ groupsListEntry: entry }); + }, + + setGroup: (groupId: string, group: GroupResponse) => { + set(state => { + const newGroupsMap = new Map(state.groupsMap); + newGroupsMap.set(groupId, createCacheEntry(group, DEFAULT_TTL.GROUP)); + return { groupsMap: newGroupsMap }; + }); + }, + + setMembers: (groupId: string, members: GroupMemberResponse[], page = 1, pageSize = 20) => { + const key = buildMembersKey(groupId, page, pageSize); + set(state => { + const newMembersMap = new Map(state.membersMap); + newMembersMap.set(key, createCacheEntry(members, DEFAULT_TTL.GROUP_MEMBERS)); + return { membersMap: newMembersMap }; + }); + }, + + setLoadingGroups: (loading: boolean) => { + set({ isLoadingGroups: loading }); + }, + + setLoadingGroup: (groupId: string, loading: boolean) => { + set(state => { + const newLoadingGroupIds = new Set(state.loadingGroupIds); + if (loading) { + newLoadingGroupIds.add(groupId); + } else { + newLoadingGroupIds.delete(groupId); + } + return { loadingGroupIds: newLoadingGroupIds }; + }); + }, + + setLoadingMembers: (groupId: string, loading: boolean, page = 1, pageSize = 20) => { + const key = buildMembersKey(groupId, page, pageSize); + set(state => { + const newLoadingMemberKeys = new Set(state.loadingMemberKeys); + if (loading) { + newLoadingMemberKeys.add(key); + } else { + newLoadingMemberKeys.delete(key); + } + return { loadingMemberKeys: newLoadingMemberKeys }; + }); + }, + + // ==================== 缓存管理 ==================== + + invalidateGroupsList: () => { + set({ groupsListEntry: null }); + }, + + invalidateGroup: (groupId: string) => { + set(state => { + const newGroupsMap = new Map(state.groupsMap); + newGroupsMap.delete(groupId); + + // 删除相关的成员缓存 + const newMembersMap = new Map(state.membersMap); + for (const key of newMembersMap.keys()) { + if (key.startsWith(`members:${groupId}:`)) { + newMembersMap.delete(key); + } + } + + return { groupsMap: newGroupsMap, membersMap: newMembersMap }; + }); + }, + + invalidateMembers: (groupId: string) => { + set(state => { + const newMembersMap = new Map(state.membersMap); + for (const key of newMembersMap.keys()) { + if (key.startsWith(`members:${groupId}:`)) { + newMembersMap.delete(key); + } + } + return { membersMap: newMembersMap }; + }); + }, + + invalidateAll: () => { + set({ + groupsListEntry: null, + groupsMap: new Map(), + membersMap: new Map(), + pendingRequests: new Map(), + }); + }, + + // ==================== 请求去重 ==================== + + getPendingRequest: (key: string) => { + return get().pendingRequests.get(key) as Promise | undefined; + }, + + setPendingRequest: (key: string, request: Promise) => { + set(state => { + const newPendingRequests = new Map(state.pendingRequests); + newPendingRequests.set(key, request); + return { pendingRequests: newPendingRequests }; + }); + }, + + deletePendingRequest: (key: string) => { + set(state => { + const newPendingRequests = new Map(state.pendingRequests); + newPendingRequests.delete(key); + return { pendingRequests: newPendingRequests }; + }); + }, + + clearPendingRequests: () => { + set({ pendingRequests: new Map() }); + }, + + // ==================== 重置 ==================== + + reset: () => { + set({ + ...initialState, + groupsMap: new Map(), + membersMap: new Map(), + loadingGroupIds: new Set(), + loadingMemberKeys: new Set(), + pendingRequests: new Map(), + }); + }, +})); \ No newline at end of file diff --git a/src/stores/group/hooks.ts b/src/stores/group/hooks.ts new file mode 100644 index 0000000..7ae76a4 --- /dev/null +++ b/src/stores/group/hooks.ts @@ -0,0 +1,140 @@ +/** + * 群组相关 React Hooks + */ + +import { useCallback, useEffect, useState } from 'react'; +import { useGroupManagerStore } from './groupStore'; +import { groupManager } from './GroupManager'; +import type { GroupResponse, GroupMemberResponse } from '../../types/dto'; +import { GROUP_LIST_PAGE_SIZE, GROUP_MEMBER_LIST_PAGE_SIZE } from '../groupListSources'; + +// ==================== useGroups ==================== + +export interface UseGroupsResult { + groups: GroupResponse[]; + isLoading: boolean; + refresh: () => Promise; +} + +/** + * 获取群组列表 hook + */ +export function useGroups(): UseGroupsResult { + const groups = useGroupManagerStore(state => state.groupsListEntry?.data ?? []); + const isLoading = useGroupManagerStore(state => state.isLoadingGroups); + const [hasFetched, setHasFetched] = useState(false); + + useEffect(() => { + if (!hasFetched) { + setHasFetched(true); + useGroupManagerStore.getState().setLoadingGroups(true); + groupManager.getGroups(1, GROUP_LIST_PAGE_SIZE).finally(() => { + useGroupManagerStore.getState().setLoadingGroups(false); + }); + } + }, [hasFetched]); + + const refresh = useCallback(async () => { + useGroupManagerStore.getState().setLoadingGroups(true); + try { + await groupManager.getGroups(1, GROUP_LIST_PAGE_SIZE, true); + } finally { + useGroupManagerStore.getState().setLoadingGroups(false); + } + }, []); + + return { groups, isLoading, refresh }; +} + +// ==================== useGroup ==================== + +export interface UseGroupResult { + group: GroupResponse | null; + isLoading: boolean; + refresh: () => Promise; +} + +/** + * 获取单个群组 hook + */ +export function useGroup(groupId: string | undefined | null): UseGroupResult { + const group = useGroupManagerStore(state => + groupId ? (state.groupsMap.get(groupId)?.data ?? null) : null + ); + const isLoading = useGroupManagerStore(state => + groupId ? state.loadingGroupIds.has(groupId) : false + ); + const [hasFetched, setHasFetched] = useState(false); + + useEffect(() => { + if (!groupId) return; + if (!hasFetched) { + setHasFetched(true); + useGroupManagerStore.getState().setLoadingGroup(groupId, true); + groupManager.getGroup(groupId).finally(() => { + useGroupManagerStore.getState().setLoadingGroup(groupId, false); + }); + } + }, [groupId, hasFetched]); + + const refresh = useCallback(async () => { + if (!groupId) return null; + useGroupManagerStore.getState().setLoadingGroup(groupId, true); + try { + return await groupManager.getGroup(groupId, true); + } finally { + useGroupManagerStore.getState().setLoadingGroup(groupId, false); + } + }, [groupId]); + + return { group, isLoading, refresh }; +} + +// ==================== useGroupMembers ==================== + +export interface UseGroupMembersResult { + members: GroupMemberResponse[]; + isLoading: boolean; + refresh: () => Promise; +} + +/** + * 获取群组成员 hook + */ +export function useGroupMembers( + groupId: string | undefined | null, + page = 1, + pageSize = GROUP_MEMBER_LIST_PAGE_SIZE +): UseGroupMembersResult { + const membersKey = groupId ? `members:${groupId}:${page}:${pageSize}` : ''; + const members = useGroupManagerStore(state => + membersKey ? (state.membersMap.get(membersKey)?.data ?? []) : [] + ); + const isLoading = useGroupManagerStore(state => + membersKey ? state.loadingMemberKeys.has(membersKey) : false + ); + const [hasFetched, setHasFetched] = useState(false); + + useEffect(() => { + if (!groupId) return; + if (!hasFetched) { + setHasFetched(true); + useGroupManagerStore.getState().setLoadingMembers(groupId, true, page, pageSize); + groupManager.getMembers(groupId, page, pageSize).finally(() => { + useGroupManagerStore.getState().setLoadingMembers(groupId, false, page, pageSize); + }); + } + }, [groupId, page, pageSize, hasFetched]); + + const refresh = useCallback(async () => { + if (!groupId) return; + useGroupManagerStore.getState().setLoadingMembers(groupId, true, page, pageSize); + try { + await groupManager.getMembers(groupId, page, pageSize, true); + } finally { + useGroupManagerStore.getState().setLoadingMembers(groupId, false, page, pageSize); + } + }, [groupId, page, pageSize]); + + return { members, isLoading, refresh }; +} diff --git a/src/stores/group/index.ts b/src/stores/group/index.ts new file mode 100644 index 0000000..9e0231f --- /dev/null +++ b/src/stores/group/index.ts @@ -0,0 +1,27 @@ +/** + * 群组模块统一导出 + */ + +// ==================== Store ==================== +export { + useGroupManagerStore, + type GroupManagerState, + type GroupManagerActions, + type GroupManagerStore, +} from './groupStore'; + +// ==================== Manager ==================== +export { groupManager, GroupManager } from './GroupManager'; + +// ==================== Hooks ==================== +export { + useGroups, + useGroup, + useGroupMembers, + type UseGroupsResult, + type UseGroupResult, + type UseGroupMembersResult, +} from './hooks'; + +// ==================== 默认导出 ==================== +export { default } from './GroupManager'; \ No newline at end of file diff --git a/src/stores/groupManager.ts b/src/stores/groupManager.ts index afd00f3..753939e 100644 --- a/src/stores/groupManager.ts +++ b/src/stores/groupManager.ts @@ -1,387 +1,21 @@ /** - * GroupManager - 群组管理核心模块 + * @deprecated 此文件已废弃,请从 './group' 导入 + * 保留此文件仅为向后兼容 * - * 架构特点: - * - 使用 CachedManager 基类,消除重复的缓存/去重逻辑 - * - 支持 Sources 模式进行分页加载 + * 迁移说明: + * - groupManager 现在位于 ./group/GroupManager.ts + * - 使用 Zustand 进行状态管理 + * - 推荐使用 hooks: useGroups, useGroup, useGroupMembers */ -import { - GroupListResponse, - GroupMemberListResponse, - GroupMemberResponse, - GroupResponse, -} from '../types/dto'; -import { groupService } from '../services/groupService'; -import { - getAllGroupsCache, - getGroupCache, - getGroupMembersCache, - saveGroupCache, - saveGroupMembersCache, - saveGroupsCache, - saveUsersCache, -} from '../services/database'; -import { CacheEvent } from './cacheBus'; -import { CachedManager, CacheEntry, createCacheEntry, isCacheExpired } from './baseManager'; -import { - IGroupListPagedSource, - IGroupMemberListPagedSource, - createRemoteGroupListSource, - createRemoteGroupMemberListSource, - RemoteGroupListSourceKind, - GROUP_LIST_PAGE_SIZE, - GROUP_MEMBER_LIST_PAGE_SIZE, -} from './groupListSources'; - -// ==================== 类型定义 ==================== - -interface GroupManagerSnapshot { - groupIds: string[]; - memberGroupIds: string[]; -} - -type GroupManagerEvent = - | CacheEvent<{ groups: GroupResponse[] }> - | CacheEvent<{ groupId: string; group: GroupResponse | null }> - | CacheEvent<{ groupId: string; members: GroupMemberResponse[] }> - | CacheEvent - | CacheEvent<{ error: unknown; context: string }>; - -// ==================== 常量 ==================== - -const GROUP_TTL = 60 * 1000; -const MEMBERS_TTL = 120 * 1000; - -// ==================== Manager 实现 ==================== - -class GroupManager extends CachedManager { - /** 群组详情缓存 */ - private groupDetailCache = new Map>(); - /** 群组成员缓存 */ - private groupMembersCache = new Map>(); - - protected getSnapshot(): GroupManagerSnapshot { - return { - groupIds: [...this.groupDetailCache.keys()], - memberGroupIds: [...this.groupMembersCache.keys()], - }; - } - - // ==================== 群组列表相关 ==================== - - /** - * 获取群组列表 - */ - async getGroups(page = 1, pageSize = GROUP_LIST_PAGE_SIZE, forceRefresh = false): Promise { - const key = `list:${page}:${pageSize}`; - - // 第一页且有有效缓存 - if (page === 1 && !forceRefresh && this.hasValidCache(key)) { - const groups = this.getFromCache(key)!; - return this.buildGroupListResponse(groups, pageSize); - } - - // 第一页且缓存过期:后台刷新 - if (page === 1 && !forceRefresh && this.hasExpiredCache(key)) { - this.refreshGroupsInBackground(page, pageSize); - const groups = this.getFromCache(key)!; - return this.buildGroupListResponse(groups, pageSize); - } - - // 第一页:尝试本地缓存 - if (page === 1 && !forceRefresh) { - const localGroups = await getAllGroupsCache(); - if (localGroups.length > 0) { - this.setCache(key, localGroups, GROUP_TTL); - this.notify({ - type: 'list_updated', - payload: { groups: localGroups }, - timestamp: Date.now(), - }); - this.refreshGroupsInBackground(page, pageSize); - return this.buildGroupListResponse(localGroups, pageSize); - } - } - - // 发起请求 - return this.dedupe(`groups:list:${page}:${pageSize}`, async () => { - const response = await groupService.getGroups(page, pageSize); - const groups = response.list || []; - if (page === 1) { - this.setCache(key, groups, GROUP_TTL); - } - await saveGroupsCache(groups).catch(() => {}); - this.notify({ - type: 'list_updated', - payload: { groups }, - timestamp: Date.now(), - }); - return response; - }); - } - - private buildGroupListResponse(groups: GroupResponse[], pageSize: number): GroupListResponse { - return { - list: groups, - total: groups.length, - page: 1, - page_size: pageSize, - total_pages: 1, - }; - } - - private refreshGroupsInBackground(page = 1, pageSize = GROUP_LIST_PAGE_SIZE): void { - const key = `list:${page}:${pageSize}`; - this.dedupe(`groups:bg:${key}`, async () => { - const response = await groupService.getGroups(page, pageSize); - const groups = response.list || []; - if (page === 1) { - this.setCache(key, groups, GROUP_TTL); - saveGroupsCache(groups).catch(() => {}); - } - this.notify({ - type: 'list_updated', - payload: { groups }, - timestamp: Date.now(), - }); - return response; - }).catch((error) => { - this.notify({ - type: 'error', - payload: { error, context: 'refreshGroupsInBackground' }, - timestamp: Date.now(), - }); - }); - } - - /** - * 创建群组列表数据源(用于 Sources 模式) - */ - createGroupListSource( - kind: RemoteGroupListSourceKind = 'cursor', - pageSize: number = GROUP_LIST_PAGE_SIZE - ): IGroupListPagedSource { - return createRemoteGroupListSource(kind, pageSize); - } - - // ==================== 群组详情相关 ==================== - - /** - * 获取群组详情 - */ - async getGroup(groupId: string, forceRefresh = false): Promise { - const cached = this.groupDetailCache.get(groupId); - - // 有效缓存 - if (!forceRefresh && cached && !isCacheExpired(cached) && cached.data) { - return cached.data; - } - - // 过期缓存:后台刷新 - if (!forceRefresh && cached && isCacheExpired(cached) && cached.data) { - this.refreshGroupInBackground(groupId); - return cached.data; - } - - // 尝试本地缓存 - if (!forceRefresh) { - const localGroup = await getGroupCache(groupId); - if (localGroup) { - this.groupDetailCache.set(groupId, createCacheEntry(localGroup, GROUP_TTL)); - this.notify({ - type: 'detail_updated', - payload: { groupId, group: localGroup }, - timestamp: Date.now(), - }); - this.refreshGroupInBackground(groupId); - return localGroup; - } - } - - // 发起请求 - return this.dedupe(`groups:detail:${groupId}`, async () => { - const group = await groupService.getGroup(groupId); - this.groupDetailCache.set(groupId, createCacheEntry(group, GROUP_TTL)); - await saveGroupCache(group).catch(() => {}); - this.notify({ - type: 'detail_updated', - payload: { groupId, group }, - timestamp: Date.now(), - }); - return group; - }); - } - - private refreshGroupInBackground(groupId: string): void { - this.dedupe(`groups:detail:bg:${groupId}`, async () => { - const group = await groupService.getGroup(groupId); - this.groupDetailCache.set(groupId, createCacheEntry(group, GROUP_TTL)); - saveGroupCache(group).catch(() => {}); - this.notify({ - type: 'detail_updated', - payload: { groupId, group }, - timestamp: Date.now(), - }); - return group; - }).catch((error) => { - this.notify({ - type: 'error', - payload: { error, context: 'refreshGroupInBackground' }, - timestamp: Date.now(), - }); - }); - } - - // ==================== 群组成员相关 ==================== - - /** - * 获取群组成员列表 - */ - async getMembers( - groupId: string, - page = 1, - pageSize = GROUP_MEMBER_LIST_PAGE_SIZE, - forceRefresh = false - ): Promise { - const key = `members:${groupId}:${page}:${pageSize}`; - const cached = this.groupMembersCache.get(key); - - // 有效缓存 - if (!forceRefresh && cached && !isCacheExpired(cached)) { - return this.buildMemberListResponse(cached.data, page, pageSize); - } - - // 过期缓存:后台刷新 - if (!forceRefresh && cached && isCacheExpired(cached)) { - this.refreshMembersInBackground(groupId, page, pageSize); - return this.buildMemberListResponse(cached.data, page, pageSize); - } - - // 第一页:尝试本地缓存 - if (page === 1 && !forceRefresh) { - const localMembers = await getGroupMembersCache(groupId); - if (localMembers.length > 0) { - this.groupMembersCache.set(key, createCacheEntry(localMembers, MEMBERS_TTL)); - this.notify({ - type: 'detail_updated', - payload: { groupId, members: localMembers }, - timestamp: Date.now(), - }); - this.refreshMembersInBackground(groupId, page, pageSize); - return this.buildMemberListResponse(localMembers, page, pageSize); - } - } - - // 发起请求 - return this.dedupe(`groups:members:${key}`, async () => { - const response = await groupService.getMembers(groupId, page, pageSize); - const members = response.list || []; - this.groupMembersCache.set(key, createCacheEntry(members, MEMBERS_TTL)); - if (page === 1) { - await saveGroupMembersCache(groupId, members).catch(() => {}); - } - await saveUsersCache( - members - .map((member) => member.user) - .filter((user): user is NonNullable => Boolean(user)) - ).catch(() => {}); - this.notify({ - type: 'detail_updated', - payload: { groupId, members }, - timestamp: Date.now(), - }); - return response; - }); - } - - private buildMemberListResponse( - members: GroupMemberResponse[], - page: number, - pageSize: number - ): GroupMemberListResponse { - return { - list: members, - total: members.length, - page, - page_size: pageSize, - total_pages: 1, - }; - } - - private refreshMembersInBackground( - groupId: string, - page = 1, - pageSize = GROUP_MEMBER_LIST_PAGE_SIZE - ): void { - const key = `members:${groupId}:${page}:${pageSize}`; - this.dedupe(`groups:members:bg:${key}`, async () => { - const response = await groupService.getMembers(groupId, page, pageSize); - const members = response.list || []; - this.groupMembersCache.set(key, createCacheEntry(members, MEMBERS_TTL)); - if (page === 1) { - saveGroupMembersCache(groupId, members).catch(() => {}); - } - saveUsersCache( - members - .map((member) => member.user) - .filter((user): user is NonNullable => Boolean(user)) - ).catch(() => {}); - this.notify({ - type: 'detail_updated', - payload: { groupId, members }, - timestamp: Date.now(), - }); - return response; - }).catch((error) => { - this.notify({ - type: 'error', - payload: { error, context: 'refreshMembersInBackground' }, - timestamp: Date.now(), - }); - }); - } - - /** - * 创建群组成员列表数据源(用于 Sources 模式) - */ - createGroupMemberListSource( - groupId: string, - kind: RemoteGroupListSourceKind = 'cursor', - pageSize: number = GROUP_MEMBER_LIST_PAGE_SIZE - ): IGroupMemberListPagedSource { - return createRemoteGroupMemberListSource(groupId, kind, pageSize); - } - - // ==================== 缓存管理 ==================== - - /** - * 使缓存失效 - */ - invalidate(groupId?: string): void { - if (!groupId) { - this.clearCache(); - this.groupDetailCache.clear(); - this.groupMembersCache.clear(); - return; - } - this.groupDetailCache.delete(groupId); - [...this.groupMembersCache.keys()].forEach((key) => { - if (key.startsWith(`members:${groupId}:`)) { - this.groupMembersCache.delete(key); - } - }); - } - - /** - * 清除所有数据 - */ - clear(): void { - this.clearCache(); - this.groupDetailCache.clear(); - this.groupMembersCache.clear(); - this.clearPendingRequests(); - } -} - -export const groupManager = new GroupManager(); +export { + groupManager, + GroupManager, + useGroupManagerStore, + useGroups, + useGroup, + useGroupMembers, + type GroupManagerState, + type GroupManagerActions, + type GroupManagerStore, +} from './group'; \ No newline at end of file diff --git a/src/stores/index.ts b/src/stores/index.ts index 8205c0d..c384553 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -13,7 +13,6 @@ export { } from './userStore'; // MessageManager 新架构导出(已替换 conversationStore) -// 重构:从 ./message 目录导入,移除了 messageManager.ts 重导出文件 export { messageManager, MessageManager, useMessageStore } from './message'; export type { MessageEvent, @@ -39,9 +38,49 @@ export { createRemoteGroupMemberListSource, } from './groupMemberListSources'; export { enrichGroupMembersWithUserProfiles } from './groupMemberProfileResolver'; -export { postManager } from './postManager'; -export { groupManager } from './groupManager'; -export { userManager } from './userManager'; + +// ==================== 新架构 Manager 导出 ==================== + +// User 模块(userManager 已迁移到 Zustand) +export { + userManager, + UserManager, + useUserManagerStore, + useCurrentUser as useCurrentUserFromManager, + useUser, + useUsers, + type UserManagerState, + type UserManagerActions, + type UserManagerStore, +} from './user'; + +// Group 模块(groupManager 已迁移到 Zustand) +export { + groupManager, + GroupManager, + useGroupManagerStore, + useGroups, + useGroup, + useGroupMembers, + type GroupManagerState, + type GroupManagerActions, + type GroupManagerStore, +} from './group'; + +// Post 模块(postManager 已迁移到 Zustand) +export { + postManager, + PostManager, + usePostManagerStore, + usePostList, + usePostDetail, + type PostManagerState, + type PostManagerActions, + type PostManagerStore, +} from './post'; + +// ==================== 其他 Store ==================== + export { useHomeTabBarVisibilityStore, } from './homeTabBarVisibilityStore'; @@ -79,3 +118,14 @@ export { useGroupTyping, useGroupMuted, } from './messageManagerHooks'; + +// ==================== 工具函数 ==================== + +export { + CacheEntry, + createCacheEntry, + isCacheExpired, + isCacheValid, + getCacheRemainingTTL, + DEFAULT_TTL, +} from './utils/cache'; \ No newline at end of file diff --git a/src/stores/post/PostManager.ts b/src/stores/post/PostManager.ts new file mode 100644 index 0000000..0a4875b --- /dev/null +++ b/src/stores/post/PostManager.ts @@ -0,0 +1,174 @@ +/** + * PostManager - 帖子管理核心模块(重构版? + * + * 使用 Zustand store 进行状态管? + * 保持与旧 API 的向后兼? + */ + +import type { Post } from '../../types'; +import { postService } from '../../services/postService'; +import { usePostManagerStore } from './postStore'; +import { createCacheEntry, isCacheExpired, DEFAULT_TTL } from '../utils/cache'; +import { + IPostListPagedSource, + PostListTab, + createRemotePostListSource, + RemotePostListSourceKind, + POST_LIST_PAGE_SIZE, +} from '../postListSources'; + +// ==================== 辅助函数 ==================== + +function buildListKey(type: string, page: number, pageSize: number): string { + return `list:${type}:${page}:${pageSize}`; +} + +// ==================== PostManager ?==================== + +class PostManager { + // ==================== 列表相关 ==================== + + /** + * 获取帖子列表(传统分页模式) + */ + async getPosts( + type: PostListTab = 'hot', + page = 1, + pageSize = POST_LIST_PAGE_SIZE, + forceRefresh = false + ): Promise { + const key = buildListKey(type, page, pageSize); + const store = usePostManagerStore.getState(); + const entry = store.listsMap.get(key); + + // 检查有效缓? + if (!forceRefresh && entry && !isCacheExpired(entry)) { + return entry.data; + } + + // 过期缓存:后台刷新,立即返回旧数? + if (!forceRefresh && entry && isCacheExpired(entry)) { + this.refreshPostsInBackground(key, type, page, pageSize); + return entry.data; + } + + // 无缓存:发起请求 + return this.dedupe(`posts:${key}`, async () => { + const store = usePostManagerStore.getState(); + const response = await postService.getPosts(page, pageSize, type); + const posts = response.list || []; + store.setPostsList(type, page, pageSize, posts); + return posts; + }); + } + + private refreshPostsInBackground( + key: string, + type: PostListTab, + page: number, + pageSize: number + ): void { + this.dedupe(`posts:bg:${key}`, async () => { + const store = usePostManagerStore.getState(); + const response = await postService.getPosts(page, pageSize, type); + const posts = response.list || []; + store.setPostsList(type, page, pageSize, posts); + return posts; + }).catch((error) => { + console.error('[PostManager] refreshPostsInBackground error:', error); + }); + } + + /** + * 创建帖子列表数据源(用于 Sources 模式? + */ + createPostListSource( + options: { tab?: PostListTab; channelId?: string; pageSize?: number } = {}, + kind: RemotePostListSourceKind = 'cursor' + ): IPostListPagedSource { + return createRemotePostListSource(kind, options); + } + + // ==================== 详情相关 ==================== + + /** + * 获取帖子详情 + */ + async getPostDetail(postId: string, forceRefresh = false): Promise { + const store = usePostManagerStore.getState(); + const entry = store.detailsMap.get(postId); + + // 有效缓存 + if (!forceRefresh && entry && !isCacheExpired(entry)) { + return entry.data; + } + + // 过期缓存:后台刷? + if (!forceRefresh && entry && isCacheExpired(entry) && entry.data) { + this.refreshPostDetailInBackground(postId); + return entry.data; + } + + // 无缓存:发起请求 + return this.dedupe(`posts:detail:${postId}`, async () => { + const store = usePostManagerStore.getState(); + const post = await postService.getPost(postId); + store.setPostDetail(postId, post); + return post; + }); + } + + private refreshPostDetailInBackground(postId: string): void { + this.dedupe(`posts:detail:bg:${postId}`, async () => { + const store = usePostManagerStore.getState(); + const post = await postService.getPost(postId); + store.setPostDetail(postId, post); + return post; + }).catch((error) => { + console.error('[PostManager] refreshPostDetailInBackground error:', error); + }); + } + + // ==================== 缓存管理 ==================== + + /** + * 使缓存失? + */ + invalidate(postId?: string): void { + if (postId) { + usePostManagerStore.getState().invalidateDetail(postId); + return; + } + usePostManagerStore.getState().invalidateAll(); + } + + /** + * 清除所有数? + */ + clear(): void { + usePostManagerStore.getState().reset(); + } + + // ==================== 请求去重 ==================== + + private async dedupe(key: string, fetcher: () => Promise): Promise { + const store = usePostManagerStore.getState(); + const pending = store.getPendingRequest(key); + if (pending) return pending; + + const request = fetcher().finally(() => { + usePostManagerStore.getState().deletePendingRequest(key); + }); + store.setPendingRequest(key, request); + return request; + } +} + +// ==================== 单例导出 ==================== + +export const postManager = new PostManager(); + +// 导出类以支持类型检查和测试 +export { PostManager }; + +export default postManager; diff --git a/src/stores/post/hooks.ts b/src/stores/post/hooks.ts new file mode 100644 index 0000000..afca93d --- /dev/null +++ b/src/stores/post/hooks.ts @@ -0,0 +1,102 @@ +/** + * 帖子相关 React Hooks + */ + +import { useCallback, useEffect, useState } from 'react'; +import { usePostManagerStore } from './postStore'; +import { postManager } from './PostManager'; +import type { Post } from '../../types'; +import { POST_LIST_PAGE_SIZE } from '../postListSources'; +import type { PostListTab } from '../postListSources'; + +// ==================== usePostList ==================== + +export interface UsePostListResult { + posts: Post[]; + isLoading: boolean; + refresh: () => Promise; +} + +/** + * 获取帖子列表 hook + */ +export function usePostList( + type: PostListTab = 'hot', + page = 1, + pageSize = POST_LIST_PAGE_SIZE +): UsePostListResult { + const posts = usePostManagerStore(state => { + const key = `list:${type}:${page}:${pageSize}`; + return state.listsMap.get(key)?.data ?? []; + }); + const isLoading = usePostManagerStore(state => { + const key = `list:${type}:${page}:${pageSize}`; + return state.loadingListKeys.has(key); + }); + const [hasFetched, setHasFetched] = useState(false); + + useEffect(() => { + if (!hasFetched) { + setHasFetched(true); + usePostManagerStore.getState().setLoadingList(type, page, pageSize, true); + postManager.getPosts(type, page, pageSize).finally(() => { + usePostManagerStore.getState().setLoadingList(type, page, pageSize, false); + }); + } + }, [type, page, pageSize, hasFetched]); + + const refresh = useCallback(async () => { + usePostManagerStore.getState().setLoadingList(type, page, pageSize, true); + try { + await postManager.getPosts(type, page, pageSize, true); + } finally { + usePostManagerStore.getState().setLoadingList(type, page, pageSize, false); + } + }, [type, page, pageSize]); + + return { posts, isLoading, refresh }; +} + +// ==================== usePostDetail ==================== + +export interface UsePostDetailResult { + post: Post | null; + isLoading: boolean; + refresh: () => Promise; +} + +/** + * 获取帖子详情 hook + */ +export function usePostDetail(postId: string | undefined | null): UsePostDetailResult { + const post = usePostManagerStore(state => + postId ? (state.detailsMap.get(postId)?.data ?? null) : null + ); + const isLoading = usePostManagerStore(state => + postId ? state.loadingDetailIds.has(postId) : false + ); + const [hasFetched, setHasFetched] = useState(false); + + useEffect(() => { + if (!postId) return; + if (!hasFetched) { + setHasFetched(true); + usePostManagerStore.getState().setLoadingDetail(postId, true); + postManager.getPostDetail(postId).finally(() => { + usePostManagerStore.getState().setLoadingDetail(postId, false); + }); + } + }, [postId, hasFetched]); + + const refresh = useCallback(async () => { + if (!postId) return null; + usePostManagerStore.getState().setLoadingDetail(postId, true); + try { + return await postManager.getPostDetail(postId, true); + } finally { + usePostManagerStore.getState().setLoadingDetail(postId, false); + } + }, [postId]); + + return { post, isLoading, refresh }; +} diff --git a/src/stores/post/index.ts b/src/stores/post/index.ts new file mode 100644 index 0000000..3bc2e36 --- /dev/null +++ b/src/stores/post/index.ts @@ -0,0 +1,25 @@ +/** + * 帖子模块统一导出 + */ + +// ==================== Store ==================== +export { + usePostManagerStore, + type PostManagerState, + type PostManagerActions, + type PostManagerStore, +} from './postStore'; + +// ==================== Manager ==================== +export { postManager, PostManager } from './PostManager'; + +// ==================== Hooks ==================== +export { + usePostList, + usePostDetail, + type UsePostListResult, + type UsePostDetailResult, +} from './hooks'; + +// ==================== 默认导出 ==================== +export { default } from './PostManager'; \ No newline at end of file diff --git a/src/stores/post/postStore.ts b/src/stores/post/postStore.ts new file mode 100644 index 0000000..21eea0b --- /dev/null +++ b/src/stores/post/postStore.ts @@ -0,0 +1,232 @@ +/** + * 帖子状态 Zustand Store + */ + +import { create } from 'zustand'; +import type { Post } from '../../types'; +import { CacheEntry, createCacheEntry, isCacheExpired, DEFAULT_TTL } from '../utils/cache'; + +// ==================== 状态接口 ==================== + +export interface PostManagerState { + // 帖子列表缓存 (key: list:type:page:pageSize) + listsMap: Map>; + + // 帖子详情缓存 + detailsMap: Map>; + + // 加载状态 + loadingListKeys: Set; + loadingDetailIds: Set; + + // 待处理请求(去重) + pendingRequests: Map>; +} + +export interface PostManagerActions { + // 获取状态 + getPostsList: (type: string, page: number, pageSize: number) => Post[] | null; + getPostDetail: (postId: string) => Post | null; + isListExpired: (type: string, page: number, pageSize: number) => boolean; + isDetailExpired: (postId: string) => boolean; + isLoadingList: (type: string, page: number, pageSize: number) => boolean; + isLoadingDetail: (postId: string) => boolean; + + // 设置状态 + setPostsList: (type: string, page: number, pageSize: number, posts: Post[]) => void; + setPostDetail: (postId: string, post: Post | null) => void; + setLoadingList: (type: string, page: number, pageSize: number, loading: boolean) => void; + setLoadingDetail: (postId: string, loading: boolean) => void; + + // 缓存管理 + invalidateList: (type?: string, page?: number, pageSize?: number) => void; + invalidateDetail: (postId: string) => void; + invalidateAll: () => void; + + // 请求去重 + getPendingRequest: (key: string) => Promise | undefined; + setPendingRequest: (key: string, request: Promise) => void; + deletePendingRequest: (key: string) => void; + clearPendingRequests: () => void; + + // 重置 + reset: () => void; +} + +// ==================== 辅助函数 ==================== + +function buildListKey(type: string, page: number, pageSize: number): string { + return `list:${type}:${page}:${pageSize}`; +} + +// ==================== 初始状态 ==================== + +const initialState: PostManagerState = { + listsMap: new Map(), + detailsMap: new Map(), + loadingListKeys: new Set(), + loadingDetailIds: new Set(), + pendingRequests: new Map(), +}; + +// ==================== Store 创建 ==================== + +export type PostManagerStore = PostManagerState & PostManagerActions; + +export const usePostManagerStore = create((set, get) => ({ + // ==================== 初始状态 ==================== + ...initialState, + + // ==================== 获取状态 ==================== + + getPostsList: (type: string, page: number, pageSize: number) => { + const key = buildListKey(type, page, pageSize); + const entry = get().listsMap.get(key); + if (!entry) return null; + if (isCacheExpired(entry)) return null; + return entry.data; + }, + + getPostDetail: (postId: string) => { + const entry = get().detailsMap.get(postId); + if (!entry) return null; + if (isCacheExpired(entry)) return null; + return entry.data; + }, + + isListExpired: (type: string, page: number, pageSize: number) => { + const key = buildListKey(type, page, pageSize); + const entry = get().listsMap.get(key); + if (!entry) return true; + return isCacheExpired(entry); + }, + + isDetailExpired: (postId: string) => { + const entry = get().detailsMap.get(postId); + if (!entry) return true; + return isCacheExpired(entry); + }, + + isLoadingList: (type: string, page: number, pageSize: number) => { + const key = buildListKey(type, page, pageSize); + return get().loadingListKeys.has(key); + }, + + isLoadingDetail: (postId: string) => { + return get().loadingDetailIds.has(postId); + }, + + // ==================== 设置状态 ==================== + + setPostsList: (type: string, page: number, pageSize: number, posts: Post[]) => { + const key = buildListKey(type, page, pageSize); + set(state => { + const newListsMap = new Map(state.listsMap); + newListsMap.set(key, createCacheEntry(posts, DEFAULT_TTL.POST_LIST)); + return { listsMap: newListsMap }; + }); + }, + + setPostDetail: (postId: string, post: Post | null) => { + set(state => { + const newDetailsMap = new Map(state.detailsMap); + newDetailsMap.set(postId, createCacheEntry(post, DEFAULT_TTL.POST_DETAIL)); + return { detailsMap: newDetailsMap }; + }); + }, + + setLoadingList: (type: string, page: number, pageSize: number, loading: boolean) => { + const key = buildListKey(type, page, pageSize); + set(state => { + const newLoadingListKeys = new Set(state.loadingListKeys); + if (loading) { + newLoadingListKeys.add(key); + } else { + newLoadingListKeys.delete(key); + } + return { loadingListKeys: newLoadingListKeys }; + }); + }, + + setLoadingDetail: (postId: string, loading: boolean) => { + set(state => { + const newLoadingDetailIds = new Set(state.loadingDetailIds); + if (loading) { + newLoadingDetailIds.add(postId); + } else { + newLoadingDetailIds.delete(postId); + } + return { loadingDetailIds: newLoadingDetailIds }; + }); + }, + + // ==================== 缓存管理 ==================== + + invalidateList: (type?: string, page?: number, pageSize?: number) => { + if (type !== undefined && page !== undefined && pageSize !== undefined) { + const key = buildListKey(type, page, pageSize); + set(state => { + const newListsMap = new Map(state.listsMap); + newListsMap.delete(key); + return { listsMap: newListsMap }; + }); + } else { + set({ listsMap: new Map() }); + } + }, + + invalidateDetail: (postId: string) => { + set(state => { + const newDetailsMap = new Map(state.detailsMap); + newDetailsMap.delete(postId); + return { detailsMap: newDetailsMap }; + }); + }, + + invalidateAll: () => { + set({ + listsMap: new Map(), + detailsMap: new Map(), + pendingRequests: new Map(), + }); + }, + + // ==================== 请求去重 ==================== + + getPendingRequest: (key: string) => { + return get().pendingRequests.get(key) as Promise | undefined; + }, + + setPendingRequest: (key: string, request: Promise) => { + set(state => { + const newPendingRequests = new Map(state.pendingRequests); + newPendingRequests.set(key, request); + return { pendingRequests: newPendingRequests }; + }); + }, + + deletePendingRequest: (key: string) => { + set(state => { + const newPendingRequests = new Map(state.pendingRequests); + newPendingRequests.delete(key); + return { pendingRequests: newPendingRequests }; + }); + }, + + clearPendingRequests: () => { + set({ pendingRequests: new Map() }); + }, + + // ==================== 重置 ==================== + + reset: () => { + set({ + ...initialState, + listsMap: new Map(), + detailsMap: new Map(), + loadingListKeys: new Set(), + loadingDetailIds: new Set(), + pendingRequests: new Map(), + }); + }, +})); \ No newline at end of file diff --git a/src/stores/postManager.ts b/src/stores/postManager.ts index 2f8a741..e9067b4 100644 --- a/src/stores/postManager.ts +++ b/src/stores/postManager.ts @@ -1,214 +1,20 @@ /** - * PostManager - 帖子管理核心模块 + * @deprecated 此文件已废弃,请从 './post' 导入 + * 保留此文件仅为向后兼容 * - * 架构特点: - * - 使用 CachedManager 基类,消除重复的缓存/去重逻辑 - * - 支持 Sources 模式进行分页加载 + * 迁移说明: + * - postManager 现在位于 ./post/PostManager.ts + * - 使用 Zustand 进行状态管理 + * - 推荐使用 hooks: usePostList, usePostDetail */ -import { Post } from '../types'; -import { postService } from '../services/postService'; -import { CacheEvent } from './cacheBus'; -import { CachedManager, CacheEntry, createCacheEntry, isCacheExpired } from './baseManager'; -import { - IPostListPagedSource, - PostListTab, - createRemotePostListSource, - RemotePostListSourceKind, - POST_LIST_PAGE_SIZE, -} from './postListSources'; - -// ==================== 类型定义 ==================== - -interface PostListPayload { - key: string; - posts: Post[]; -} - -interface PostDetailPayload { - postId: string; - post: Post | null; -} - -interface PostManagerSnapshot { - listKeys: string[]; - detailKeys: string[]; -} - -type PostManagerEvent = - | CacheEvent - | CacheEvent - | CacheEvent - | CacheEvent<{ error: unknown; context: string }>; - -// ==================== 常量 ==================== - -const LIST_TTL = 30 * 1000; -const DETAIL_TTL = 60 * 1000; - -// ==================== Manager 实现 ==================== - -class PostManager extends CachedManager { - /** 详情缓存 */ - private detailCache = new Map>(); - - protected getSnapshot(): PostManagerSnapshot { - return { - listKeys: [...this.memoryCache.keys()], - detailKeys: [...this.detailCache.keys()], - }; - } - - // ==================== 列表相关 ==================== - - private listKey(type: string, page: number, pageSize: number): string { - return `list:${type}:${page}:${pageSize}`; - } - - /** - * 获取帖子列表(传统分页模式) - */ - async getPosts( - type: PostListTab = 'hot', - page = 1, - pageSize = POST_LIST_PAGE_SIZE, - forceRefresh = false - ): Promise { - const key = this.listKey(type, page, pageSize); - - // 检查有效缓存 - if (!forceRefresh && this.hasValidCache(key)) { - return this.getFromCache(key)!; - } - - // 过期缓存:后台刷新,立即返回旧数据 - if (!forceRefresh && this.hasExpiredCache(key)) { - this.refreshPostsInBackground(key, type, page, pageSize); - return this.getFromCache(key)!; - } - - // 无缓存:发起请求 - return this.dedupe(`posts:${key}`, async () => { - const response = await postService.getPosts(page, pageSize, type); - const posts = response.list || []; - this.setCache(key, posts, LIST_TTL); - this.notify({ - type: 'list_updated', - payload: { key, posts }, - timestamp: Date.now(), - }); - return posts; - }); - } - - private refreshPostsInBackground( - key: string, - type: PostListTab, - page: number, - pageSize: number - ): void { - this.dedupe(`posts:bg:${key}`, async () => { - const response = await postService.getPosts(page, pageSize, type); - const posts = response.list || []; - this.setCache(key, posts, LIST_TTL); - this.notify({ - type: 'list_updated', - payload: { key, posts }, - timestamp: Date.now(), - }); - return posts; - }).catch((error) => { - this.notify({ - type: 'error', - payload: { error, context: 'refreshPostsInBackground' }, - timestamp: Date.now(), - }); - }); - } - - /** - * 创建帖子列表数据源(用于 Sources 模式) - */ - createPostListSource( - options: { tab?: PostListTab; channelId?: string; pageSize?: number } = {}, - kind: RemotePostListSourceKind = 'cursor' - ): IPostListPagedSource { - return createRemotePostListSource(kind, options); - } - - // ==================== 详情相关 ==================== - - /** - * 获取帖子详情 - */ - async getPostDetail(postId: string, forceRefresh = false): Promise { - const cached = this.detailCache.get(postId); - - // 有效缓存 - if (!forceRefresh && cached && !isCacheExpired(cached)) { - return cached.data; - } - - // 过期缓存:后台刷新 - if (!forceRefresh && cached && isCacheExpired(cached) && cached.data) { - this.refreshPostDetailInBackground(postId); - return cached.data; - } - - // 无缓存:发起请求 - return this.dedupe(`posts:detail:${postId}`, async () => { - const post = await postService.getPost(postId); - this.detailCache.set(postId, createCacheEntry(post, DETAIL_TTL)); - this.notify({ - type: 'detail_updated', - payload: { postId, post }, - timestamp: Date.now(), - }); - return post; - }); - } - - private refreshPostDetailInBackground(postId: string): void { - this.dedupe(`posts:detail:bg:${postId}`, async () => { - const post = await postService.getPost(postId); - this.detailCache.set(postId, createCacheEntry(post, DETAIL_TTL)); - this.notify({ - type: 'detail_updated', - payload: { postId, post }, - timestamp: Date.now(), - }); - return post; - }).catch((error) => { - this.notify({ - type: 'error', - payload: { error, context: 'refreshPostDetailInBackground' }, - timestamp: Date.now(), - }); - }); - } - - // ==================== 缓存管理 ==================== - - /** - * 使缓存失效 - */ - invalidate(postId?: string): void { - if (postId) { - this.detailCache.delete(postId); - return; - } - this.clearCache(); - this.detailCache.clear(); - } - - /** - * 清除所有数据 - */ - clear(): void { - this.clearCache(); - this.detailCache.clear(); - this.clearPendingRequests(); - } -} - -export const postManager = new PostManager(); +export { + postManager, + PostManager, + usePostManagerStore, + usePostList, + usePostDetail, + type PostManagerState, + type PostManagerActions, + type PostManagerStore, +} from './post'; \ No newline at end of file diff --git a/src/stores/user/UserManager.ts b/src/stores/user/UserManager.ts new file mode 100644 index 0000000..04afd3e --- /dev/null +++ b/src/stores/user/UserManager.ts @@ -0,0 +1,178 @@ +/** + * UserManager - 用户管理核心模块(重构版? + * + * 使用 Zustand store 进行状态管? + * 保持与旧 API 的向后兼? + */ + +import type { UserDTO } from '../../types/dto'; +import { authService } from '../../services/authService'; +import { + getCurrentUserCache, + getUserCache, + saveCurrentUserCache, + saveUserCache, +} from '../../services/database'; +import { useUserManagerStore } from './userStore'; +import { createCacheEntry, isCacheExpired, DEFAULT_TTL } from '../utils/cache'; + +// ==================== UserManager ?==================== + +class UserManager { + /** + * 获取当前用户 + * 支持缓存过期检查和后台刷新 + */ + async getCurrentUser(forceRefresh = false): Promise { + const store = useUserManagerStore.getState(); + const entry = store.currentUserEntry; + + // 有效缓存 + if (!forceRefresh && entry && !isCacheExpired(entry)) { + return entry.data; + } + + // 过期缓存:后台刷新,立即返回旧数? + if (!forceRefresh && entry && isCacheExpired(entry)) { + this.refreshCurrentUserInBackground(); + return entry.data; + } + + // 尝试从本地数据库加载 + if (!forceRefresh) { + const cachedFromDb = await getCurrentUserCache(); + if (cachedFromDb) { + const newEntry = createCacheEntry(cachedFromDb, DEFAULT_TTL.USER); + store.setCurrentUserEntry(newEntry); + this.refreshCurrentUserInBackground(); + return cachedFromDb; + } + } + + // 发起 API 请求 + return this.dedupe('users:current', async () => { + const user = await authService.fetchCurrentUserFromAPI(); + const newEntry = createCacheEntry(user, DEFAULT_TTL.USER); + store.setCurrentUserEntry(newEntry); + + if (user) { + saveCurrentUserCache(user).catch(() => {}); + saveUserCache(user).catch(() => {}); + } + return user; + }); + } + + /** + * 后台刷新当前用户 + */ + private refreshCurrentUserInBackground(): void { + this.dedupe('users:current:bg', async () => { + const store = useUserManagerStore.getState(); + const user = await authService.fetchCurrentUserFromAPI(); + const newEntry = createCacheEntry(user, DEFAULT_TTL.USER); + store.setCurrentUserEntry(newEntry); + + if (user) { + saveCurrentUserCache(user).catch(() => {}); + saveUserCache(user).catch(() => {}); + } + return user; + }).catch((error) => { + console.error('[UserManager] refreshCurrentUserInBackground error:', error); + }); + } + + /** + * 根据 ID 获取用户 + */ + async getUserById(userId: string, forceRefresh = false): Promise { + const store = useUserManagerStore.getState(); + const entry = store.usersMap.get(userId); + + // 有效缓存 + if (!forceRefresh && entry && !isCacheExpired(entry)) { + return entry.data; + } + + // 过期缓存:后台刷新,立即返回旧数? + if (!forceRefresh && entry && isCacheExpired(entry)) { + this.refreshUserByIdInBackground(userId); + return entry.data; + } + + // 尝试从本地数据库加载 + if (!forceRefresh) { + const cachedFromDb = await getUserCache(userId); + if (cachedFromDb) { + const newEntry = createCacheEntry(cachedFromDb, DEFAULT_TTL.USER); + store.setUser(userId, cachedFromDb); + this.refreshUserByIdInBackground(userId); + return cachedFromDb; + } + } + + // 发起 API 请求 + return this.dedupe(`users:detail:${userId}`, async () => { + const store = useUserManagerStore.getState(); + const user = await authService.fetchUserByIdFromAPI(userId); + const newEntry = createCacheEntry(user, DEFAULT_TTL.USER); + store.setUser(userId, user); + + if (user) { + saveUserCache(user).catch(() => {}); + } + return user; + }); + } + + /** + * 后台刷新用户 + */ + private refreshUserByIdInBackground(userId: string): void { + this.dedupe(`users:detail:bg:${userId}`, async () => { + const store = useUserManagerStore.getState(); + const user = await authService.fetchUserByIdFromAPI(userId); + const newEntry = createCacheEntry(user, DEFAULT_TTL.USER); + store.setUser(userId, user); + + if (user) { + saveUserCache(user).catch(() => {}); + } + return user; + }).catch((error) => { + console.error('[UserManager] refreshUserByIdInBackground error:', error); + }); + } + + /** + * 使缓存失? + */ + invalidateUsers(): void { + useUserManagerStore.getState().invalidateAll(); + } + + /** + * 请求去重 + */ + private async dedupe(key: string, fetcher: () => Promise): Promise { + const store = useUserManagerStore.getState(); + const pending = store.getPendingRequest(key); + if (pending) return pending; + + const request = fetcher().finally(() => { + useUserManagerStore.getState().deletePendingRequest(key); + }); + store.setPendingRequest(key, request); + return request; + } +} + +// ==================== 单例导出 ==================== + +export const userManager = new UserManager(); + +// 导出类以支持类型检查和测试 +export { UserManager }; + +export default userManager; diff --git a/src/stores/user/hooks.ts b/src/stores/user/hooks.ts new file mode 100644 index 0000000..8933feb --- /dev/null +++ b/src/stores/user/hooks.ts @@ -0,0 +1,136 @@ +/** + * 用户相关 React Hooks + */ + +import { useCallback, useEffect, useState } from 'react'; +import { useUserManagerStore } from './userStore'; +import { userManager } from './UserManager'; +import type { UserDTO } from '../../types/dto'; + +// ==================== useCurrentUser ==================== + +export interface UseCurrentUserResult { + user: UserDTO | null; + isLoading: boolean; + refresh: () => Promise; +} + +/** + * 获取当前用户 hook + * 自动加载,支持缓存和后台刷新 + */ +export function useCurrentUser(): UseCurrentUserResult { + const user = useUserManagerStore(state => state.currentUser); + const isLoading = useUserManagerStore(state => state.isLoadingCurrentUser); + const [hasFetched, setHasFetched] = useState(false); + + useEffect(() => { + if (!hasFetched) { + setHasFetched(true); + useUserManagerStore.getState().setLoadingCurrentUser(true); + userManager.getCurrentUser().finally(() => { + useUserManagerStore.getState().setLoadingCurrentUser(false); + }); + } + }, [hasFetched]); + + const refresh = useCallback(async () => { + useUserManagerStore.getState().setLoadingCurrentUser(true); + try { + return await userManager.getCurrentUser(true); + } finally { + useUserManagerStore.getState().setLoadingCurrentUser(false); + } + }, []); + + return { user, isLoading, refresh }; +} + +// ==================== useUser ==================== + +export interface UseUserResult { + user: UserDTO | null; + isLoading: boolean; + refresh: () => Promise; +} + +/** + * 根据 ID 获取用户 hook + * 自动加载,支持缓存和后台刷新 + */ +export function useUser(userId: string | undefined | null): UseUserResult { + const user = useUserManagerStore(state => + userId ? (state.usersMap.get(userId)?.data ?? null) : null + ); + const isLoading = useUserManagerStore(state => + userId ? state.loadingUserIds.has(userId) : false + ); + const [hasFetched, setHasFetched] = useState(false); + + useEffect(() => { + if (!userId) return; + if (!hasFetched) { + setHasFetched(true); + useUserManagerStore.getState().setLoadingUser(userId, true); + userManager.getUserById(userId).finally(() => { + useUserManagerStore.getState().setLoadingUser(userId, false); + }); + } + }, [userId, hasFetched]); + + const refresh = useCallback(async () => { + if (!userId) return null; + useUserManagerStore.getState().setLoadingUser(userId, true); + try { + return await userManager.getUserById(userId, true); + } finally { + useUserManagerStore.getState().setLoadingUser(userId, false); + } + }, [userId]); + + return { user, isLoading, refresh }; +} + +// ==================== useUsers ==================== + +export interface UseUsersResult { + getUser: (id: string) => UserDTO | null; + refreshUser: (id: string) => Promise; +} + +/** + * 多用户获?hook + * 用于批量获取多个用户信息 + */ +export function useUsers(userIds: string[]): UseUsersResult { + const usersMap = useUserManagerStore(state => state.usersMap); + const loadingUserIds = useUserManagerStore(state => state.loadingUserIds); + const [fetchedIds, setFetchedIds] = useState>(new Set()); + + useEffect(() => { + userIds.forEach(id => { + if (!fetchedIds.has(id) && !loadingUserIds.has(id)) { + setFetchedIds(prev => new Set(prev).add(id)); + useUserManagerStore.getState().setLoadingUser(id, true); + userManager.getUserById(id).finally(() => { + useUserManagerStore.getState().setLoadingUser(id, false); + }); + } + }); + }, [userIds, fetchedIds, loadingUserIds]); + + const getUser = useCallback((id: string) => { + return usersMap.get(id)?.data ?? null; + }, [usersMap]); + + const refreshUser = useCallback(async (id: string) => { + useUserManagerStore.getState().setLoadingUser(id, true); + try { + return await userManager.getUserById(id, true); + } finally { + useUserManagerStore.getState().setLoadingUser(id, false); + } + }, []); + + return { getUser, refreshUser }; +} diff --git a/src/stores/user/index.ts b/src/stores/user/index.ts new file mode 100644 index 0000000..84680e4 --- /dev/null +++ b/src/stores/user/index.ts @@ -0,0 +1,27 @@ +/** + * 用户模块统一导出 + */ + +// ==================== Store ==================== +export { + useUserManagerStore, + type UserManagerState, + type UserManagerActions, + type UserManagerStore, +} from './userStore'; + +// ==================== Manager ==================== +export { userManager, UserManager } from './UserManager'; + +// ==================== Hooks ==================== +export { + useCurrentUser, + useUser, + useUsers, + type UseCurrentUserResult, + type UseUserResult, + type UseUsersResult, +} from './hooks'; + +// ==================== 默认导出 ==================== +export { default } from './UserManager'; \ No newline at end of file diff --git a/src/stores/user/userStore.ts b/src/stores/user/userStore.ts new file mode 100644 index 0000000..753bd85 --- /dev/null +++ b/src/stores/user/userStore.ts @@ -0,0 +1,211 @@ +/** + * 用户状态 Zustand Store + * 管理当前用户和其他用户信息 + */ + +import { create } from 'zustand'; +import type { UserDTO } from '../../types/dto'; +import { CacheEntry, createCacheEntry, isCacheExpired, DEFAULT_TTL } from '../utils/cache'; + +// ==================== 状态接口 ==================== + +export interface UserManagerState { + // 当前用户 + currentUser: UserDTO | null; + currentUserEntry: CacheEntry | null; + + // 其他用户缓存 + usersMap: Map>; + + // 加载状态 + isLoadingCurrentUser: boolean; + loadingUserIds: Set; + + // 待处理请求(去重) + pendingRequests: Map>; +} + +export interface UserManagerActions { + // 获取状态 + getCurrentUser: () => UserDTO | null; + getUser: (userId: string) => UserDTO | null; + isCurrentUserExpired: () => boolean; + isUserExpired: (userId: string) => boolean; + isLoadingUser: (userId: string) => boolean; + + // 设置状态 + setCurrentUser: (user: UserDTO | null) => void; + setCurrentUserEntry: (entry: CacheEntry) => void; + setUser: (userId: string, user: UserDTO | null) => void; + setLoadingCurrentUser: (loading: boolean) => void; + setLoadingUser: (userId: string, loading: boolean) => void; + + // 缓存管理 + invalidateCurrentUser: () => void; + invalidateUser: (userId: string) => void; + invalidateAll: () => void; + + // 请求去重 + getPendingRequest: (key: string) => Promise | undefined; + setPendingRequest: (key: string, request: Promise) => void; + deletePendingRequest: (key: string) => void; + clearPendingRequests: () => void; + + // 重置 + reset: () => void; +} + +// ==================== 初始状态 ==================== + +const initialState: UserManagerState = { + currentUser: null, + currentUserEntry: null, + usersMap: new Map(), + isLoadingCurrentUser: false, + loadingUserIds: new Set(), + pendingRequests: new Map(), +}; + +// ==================== Store 创建 ==================== + +export type UserManagerStore = UserManagerState & UserManagerActions; + +export const useUserManagerStore = create((set, get) => ({ + // ==================== 初始状态 ==================== + ...initialState, + + // ==================== 获取状态 ==================== + + getCurrentUser: () => { + const entry = get().currentUserEntry; + if (!entry) return null; + if (isCacheExpired(entry)) return null; + return entry.data; + }, + + getUser: (userId: string) => { + const entry = get().usersMap.get(userId); + if (!entry) return null; + if (isCacheExpired(entry)) return null; + return entry.data; + }, + + isCurrentUserExpired: () => { + const entry = get().currentUserEntry; + if (!entry) return true; + return isCacheExpired(entry); + }, + + isUserExpired: (userId: string) => { + const entry = get().usersMap.get(userId); + if (!entry) return true; + return isCacheExpired(entry); + }, + + isLoadingUser: (userId: string) => { + return get().loadingUserIds.has(userId); + }, + + // ==================== 设置状态 ==================== + + setCurrentUser: (user: UserDTO | null) => { + set({ + currentUser: user, + currentUserEntry: createCacheEntry(user, DEFAULT_TTL.USER), + }); + }, + + setCurrentUserEntry: (entry: CacheEntry) => { + set({ + currentUser: entry.data, + currentUserEntry: entry, + }); + }, + + setUser: (userId: string, user: UserDTO | null) => { + set(state => { + const newUsersMap = new Map(state.usersMap); + newUsersMap.set(userId, createCacheEntry(user, DEFAULT_TTL.USER)); + return { usersMap: newUsersMap }; + }); + }, + + setLoadingCurrentUser: (loading: boolean) => { + set({ isLoadingCurrentUser: loading }); + }, + + setLoadingUser: (userId: string, loading: boolean) => { + set(state => { + const newLoadingUserIds = new Set(state.loadingUserIds); + if (loading) { + newLoadingUserIds.add(userId); + } else { + newLoadingUserIds.delete(userId); + } + return { loadingUserIds: newLoadingUserIds }; + }); + }, + + // ==================== 缓存管理 ==================== + + invalidateCurrentUser: () => { + set({ + currentUser: null, + currentUserEntry: null, + }); + }, + + invalidateUser: (userId: string) => { + set(state => { + const newUsersMap = new Map(state.usersMap); + newUsersMap.delete(userId); + return { usersMap: newUsersMap }; + }); + }, + + invalidateAll: () => { + set({ + currentUser: null, + currentUserEntry: null, + usersMap: new Map(), + pendingRequests: new Map(), + }); + }, + + // ==================== 请求去重 ==================== + + getPendingRequest: (key: string) => { + return get().pendingRequests.get(key) as Promise | undefined; + }, + + setPendingRequest: (key: string, request: Promise) => { + set(state => { + const newPendingRequests = new Map(state.pendingRequests); + newPendingRequests.set(key, request); + return { pendingRequests: newPendingRequests }; + }); + }, + + deletePendingRequest: (key: string) => { + set(state => { + const newPendingRequests = new Map(state.pendingRequests); + newPendingRequests.delete(key); + return { pendingRequests: newPendingRequests }; + }); + }, + + clearPendingRequests: () => { + set({ pendingRequests: new Map() }); + }, + + // ==================== 重置 ==================== + + reset: () => { + set({ + ...initialState, + usersMap: new Map(), + loadingUserIds: new Set(), + pendingRequests: new Map(), + }); + }, +})); \ No newline at end of file diff --git a/src/stores/userManager.ts b/src/stores/userManager.ts index df88929..724a384 100644 --- a/src/stores/userManager.ts +++ b/src/stores/userManager.ts @@ -1,188 +1,21 @@ -import { UserDTO } from '../types/dto'; -import { authService } from '../services/authService'; -import { - getCurrentUserCache, - getUserCache, - saveCurrentUserCache, - saveUserCache, -} from '../services/database'; -import { CacheBus, CacheEvent } from './cacheBus'; - -interface UserManagerSnapshot { - hasCurrentUser: boolean; - cachedUserIds: string[]; -} - -type UserManagerEvent = - | CacheEvent<{ user: UserDTO | null }> - | CacheEvent<{ userId: string; user: UserDTO | null }> - | CacheEvent - | CacheEvent<{ error: unknown; context: string }>; - -const USER_TTL = 5 * 60 * 1000; - -interface CacheEntry { - data: T; - timestamp: number; - ttl: number; -} - -class UserManager extends CacheBus { - private currentUserCache: CacheEntry | null = null; - private userCache = new Map>(); - private pendingRequests = new Map>(); - - protected getSnapshot(): UserManagerSnapshot { - return { - hasCurrentUser: !!this.currentUserCache?.data, - cachedUserIds: [...this.userCache.keys()], - }; - } - - private isExpired(entry: CacheEntry): boolean { - return Date.now() - entry.timestamp > entry.ttl; - } - - private async dedupe(key: string, fetcher: () => Promise): Promise { - const pending = this.pendingRequests.get(key) as Promise | undefined; - if (pending) return pending; - const request = fetcher().finally(() => { - this.pendingRequests.delete(key); - }); - this.pendingRequests.set(key, request); - return request; - } - - async getCurrentUser(forceRefresh = false): Promise { - if (!forceRefresh && this.currentUserCache && !this.isExpired(this.currentUserCache)) { - return this.currentUserCache.data; - } - - if (!forceRefresh && this.currentUserCache && this.isExpired(this.currentUserCache)) { - this.refreshCurrentUserInBackground(); - return this.currentUserCache.data; - } - - if (!forceRefresh) { - const cachedFromDb = await getCurrentUserCache(); - if (cachedFromDb) { - this.currentUserCache = { data: cachedFromDb, timestamp: Date.now(), ttl: USER_TTL }; - this.notify({ - type: 'detail_updated', - payload: { user: cachedFromDb }, - timestamp: Date.now(), - }); - this.refreshCurrentUserInBackground(); - return cachedFromDb; - } - } - - return this.dedupe('users:current', async () => { - const user = await authService.fetchCurrentUserFromAPI(); - this.currentUserCache = { data: user, timestamp: Date.now(), ttl: USER_TTL }; - if (user) { - saveCurrentUserCache(user).catch(() => {}); - saveUserCache(user).catch(() => {}); - } - this.notify({ - type: 'detail_updated', - payload: { user }, - timestamp: Date.now(), - }); - return user; - }); - } - - private refreshCurrentUserInBackground(): void { - this.dedupe('users:current:bg', async () => { - const user = await authService.fetchCurrentUserFromAPI(); - this.currentUserCache = { data: user, timestamp: Date.now(), ttl: USER_TTL }; - if (user) { - saveCurrentUserCache(user).catch(() => {}); - saveUserCache(user).catch(() => {}); - } - this.notify({ - type: 'detail_updated', - payload: { user }, - timestamp: Date.now(), - }); - return user; - }).catch((error) => { - this.notify({ - type: 'error', - payload: { error, context: 'refreshCurrentUserInBackground' }, - timestamp: Date.now(), - }); - }); - } - - async getUserById(userId: string, forceRefresh = false): Promise { - const cached = this.userCache.get(userId); - if (!forceRefresh && cached && !this.isExpired(cached)) { - return cached.data; - } - - if (!forceRefresh && cached && this.isExpired(cached)) { - this.refreshUserByIdInBackground(userId); - return cached.data; - } - - if (!forceRefresh) { - const cachedFromDb = await getUserCache(userId); - if (cachedFromDb) { - this.userCache.set(userId, { data: cachedFromDb, timestamp: Date.now(), ttl: USER_TTL }); - this.notify({ - type: 'detail_updated', - payload: { userId, user: cachedFromDb }, - timestamp: Date.now(), - }); - this.refreshUserByIdInBackground(userId); - return cachedFromDb; - } - } - - return this.dedupe(`users:detail:${userId}`, async () => { - const user = await authService.fetchUserByIdFromAPI(userId); - this.userCache.set(userId, { data: user, timestamp: Date.now(), ttl: USER_TTL }); - if (user) { - saveUserCache(user).catch(() => {}); - } - this.notify({ - type: 'detail_updated', - payload: { userId, user }, - timestamp: Date.now(), - }); - return user; - }); - } - - private refreshUserByIdInBackground(userId: string): void { - this.dedupe(`users:detail:bg:${userId}`, async () => { - const user = await authService.fetchUserByIdFromAPI(userId); - this.userCache.set(userId, { data: user, timestamp: Date.now(), ttl: USER_TTL }); - if (user) { - saveUserCache(user).catch(() => {}); - } - this.notify({ - type: 'detail_updated', - payload: { userId, user }, - timestamp: Date.now(), - }); - return user; - }).catch((error) => { - this.notify({ - type: 'error', - payload: { error, context: 'refreshUserByIdInBackground' }, - timestamp: Date.now(), - }); - }); - } - - invalidateUsers(): void { - this.currentUserCache = null; - this.userCache.clear(); - } -} - -export const userManager = new UserManager(); +/** + * @deprecated 此文件已废弃,请从 './user' 导入 + * 保留此文件仅为向后兼容 + * + * 迁移说明: + * - userManager 现在位于 ./user/UserManager.ts + * - 使用 Zustand 进行状态管理 + * - 推荐使用 hooks: useCurrentUser, useUser, useUsers + */ +export { + userManager, + UserManager, + useUserManagerStore, + useCurrentUser, + useUser, + useUsers, + type UserManagerState, + type UserManagerActions, + type UserManagerStore, +} from './user'; \ No newline at end of file diff --git a/src/stores/utils/cache.ts b/src/stores/utils/cache.ts new file mode 100644 index 0000000..61160a8 --- /dev/null +++ b/src/stores/utils/cache.ts @@ -0,0 +1,42 @@ +/** + * 缓存工具函数 + * 提取自 baseManager.ts,用于 Zustand store 的 TTL 缓存管理 + */ + +/** 缓存条目结构 */ +export interface CacheEntry { + data: T; + timestamp: number; + ttl: number; +} + +/** 创建缓存条目 */ +export function createCacheEntry(data: T, ttl: number): CacheEntry { + return { data, timestamp: Date.now(), ttl }; +} + +/** 检查缓存是否过期 */ +export function isCacheExpired(entry: CacheEntry): boolean { + return Date.now() - entry.timestamp > entry.ttl; +} + +/** 检查缓存是否有效(存在且未过期) */ +export function isCacheValid(entry: CacheEntry | undefined | null): boolean { + if (!entry) return false; + return !isCacheExpired(entry); +} + +/** 获取缓存的剩余有效时间(毫秒) */ +export function getCacheRemainingTTL(entry: CacheEntry): number { + const elapsed = Date.now() - entry.timestamp; + return Math.max(0, entry.ttl - elapsed); +} + +/** 默认 TTL 常量 */ +export const DEFAULT_TTL = { + USER: 5 * 60 * 1000, // 5 分钟 + POST_LIST: 30 * 1000, // 30 秒 + POST_DETAIL: 60 * 1000, // 1 分钟 + GROUP: 60 * 1000, // 1 分钟 + GROUP_MEMBERS: 120 * 1000, // 2 分钟 +}; \ No newline at end of file