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