feat(profile): add chat settings and language options to profile settings
All checks were successful
Frontend CI / ota-android (push) Successful in 13m7s
Frontend CI / build-and-push-web (push) Successful in 16m58s
Frontend CI / build-android-apk (push) Successful in 1h4m18s

- Introduced hrefProfileChatSettings() for navigation to chat settings.
- Updated SettingsScreen to include new settings items for chat settings, language selection, data storage, terms, and privacy policy.
- Enhanced user experience by providing alerts for language and data storage options.

This update improves the profile settings interface by adding more customization options for users.
This commit is contained in:
lafay
2026-04-01 02:17:36 +08:00
parent 20e9d69540
commit 5614b4078a
25 changed files with 2622 additions and 801 deletions

View File

@@ -0,0 +1,5 @@
import { ChatSettingsScreen } from '../../../../src/screens/profile/ChatSettingsScreen';
export default function ChatSettingsRoute() {
return <ChatSettingsScreen />;
}

View File

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

View File

@@ -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<SliderProps> = ({
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 (
<View style={styles.container}>
<View style={styles.labelRow}>
{leftLabel && <Text style={styles.label}>{leftLabel}</Text>}
{showValue && <Text style={styles.value}>{value}</Text>}
{rightLabel && <Text style={styles.label}>{rightLabel}</Text>}
</View>
<TouchableOpacity
style={styles.trackContainer}
onPress={handlePress}
activeOpacity={1}
>
<View style={styles.track}>
<View style={[styles.fill, { width: `${percentage}%`, backgroundColor: colors.primary.main }]} />
</View>
<View style={[styles.thumb, { left: `${percentage}%`, backgroundColor: colors.primary.main }]} />
</TouchableOpacity>
</View>
);
};
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 = () => (
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<View style={[styles.previewContainer, { backgroundColor: currentTheme.secondary }]}>
<View style={[styles.previewMessage, { borderRadius: messageRadius }]}>
<Text style={[styles.previewText, { fontSize }]}>MiMQy</Text>
<Text style={[styles.previewText, { fontSize: fontSize - 2 }]}>
👋
</Text>
<Text style={[styles.previewText, { fontSize: fontSize - 2 }]}>
</Text>
<Text style={styles.previewTime}>AM12:42</Text>
</View>
<View style={[styles.previewReply, { borderRadius: messageRadius, backgroundColor: currentTheme.bubble }]}>
<Text style={[styles.previewText, { fontSize }]}>
😎
</Text>
<Text style={styles.previewTime}>AM12:57 </Text>
</View>
</View>
</View>
);
// 渲染字号滑块
const renderFontSizeSlider = () => (
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<View style={styles.card}>
<Slider
value={fontSize}
min={12}
max={24}
onValueChange={setFontSize}
leftLabel="A"
rightLabel="A"
/>
</View>
</View>
);
// 渲染主题颜色选择
const renderThemeColors = () => (
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.themeList}
>
{THEMES.map((theme, index) => (
<TouchableOpacity
key={theme.id}
style={[
styles.themeItem,
selectedTheme === index && styles.themeItemActive,
{ backgroundColor: theme.secondary },
]}
onPress={() => setSelectedTheme(index)}
activeOpacity={0.8}
>
<View style={styles.themePreview}>
<View style={[styles.themeBubble1, { backgroundColor: '#FFFFFF' }]} />
<View style={[styles.themeBubble2, { backgroundColor: theme.bubble }]} />
</View>
<View style={[styles.themeIcon, { backgroundColor: theme.primary }]}>
<Text style={{ fontSize: 10 }}>{theme.icon}</Text>
</View>
</TouchableOpacity>
))}
</ScrollView>
</View>
);
// 渲染圆角滑块
const renderRadiusSlider = () => (
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<View style={styles.card}>
<Slider
value={messageRadius}
min={0}
max={30}
onValueChange={setMessageRadius}
/>
</View>
</View>
);
// 渲染其他设置项
const renderOtherSettings = () => (
<View style={styles.section}>
<Text style={styles.sectionTitle}></Text>
<View style={styles.card}>
<TouchableOpacity style={styles.settingItem}>
<View style={styles.settingItemLeft}>
<View style={styles.iconContainer}>
<MaterialCommunityIcons name="image-outline" size={20} color={colors.primary.main} />
</View>
<View>
<Text style={styles.settingTitle}></Text>
</View>
</View>
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
</TouchableOpacity>
<TouchableOpacity style={[styles.settingItem, styles.settingItemLast]}>
<View style={styles.settingItemLeft}>
<View style={styles.iconContainer}>
<MaterialCommunityIcons name="palette-outline" size={20} color={colors.primary.main} />
</View>
<View>
<Text style={styles.settingTitle}></Text>
<Text style={styles.settingSubtitle}>MiMQy</Text>
</View>
</View>
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
</TouchableOpacity>
</View>
</View>
);
// 渲染夜间模式开关
const renderNightMode = () => (
<View style={styles.section}>
<View style={styles.card}>
<View style={[styles.settingItem, styles.settingItemLast]}>
<View style={styles.settingItemLeft}>
<View style={styles.iconContainer}>
<MaterialCommunityIcons name="weather-night" size={20} color={colors.primary.main} />
</View>
<Text style={styles.settingTitle}></Text>
</View>
<TouchableOpacity
style={[styles.switch, nightMode && styles.switchActive]}
onPress={() => setNightMode(!nightMode)}
>
<View style={[styles.switchThumb, { alignSelf: nightMode ? 'flex-end' : 'flex-start' }]} />
</TouchableOpacity>
</View>
</View>
</View>
);
// 渲染浏览其他主题
const renderBrowseThemes = () => (
<View style={styles.section}>
<TouchableOpacity style={styles.card}>
<View style={[styles.settingItem, styles.settingItemLast]}>
<View style={styles.settingItemLeft}>
<View style={styles.iconContainer}>
<MaterialCommunityIcons name="apps" size={20} color={colors.primary.main} />
</View>
<Text style={styles.settingTitle}></Text>
</View>
<MaterialCommunityIcons name="chevron-right" size={20} color={colors.text.hint} />
</View>
</TouchableOpacity>
</View>
);
return (
<SafeAreaView style={styles.container} edges={['bottom']}>
<ScrollView
contentContainerStyle={[
styles.scrollContent,
{ paddingHorizontal: responsivePadding }
]}
>
{renderFontSizeSlider()}
{renderChatPreview()}
{renderThemeColors()}
{renderRadiusSlider()}
{renderOtherSettings()}
{renderNightMode()}
{renderBrowseThemes()}
</ScrollView>
</SafeAreaView>
);
};
export default ChatSettingsScreen;

View File

@@ -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()}
<View style={styles.footer}>

View File

@@ -228,8 +228,16 @@ class AuthService {
try {
const response = await api.get<UserResponse>('/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;
}
}

View File

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

View File

@@ -215,12 +215,17 @@ export const useAuthStore = create<AuthState>((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 {

View File

@@ -76,6 +76,7 @@ interface CallState {
toggleMinimize: () => void;
toggleVideo: () => Promise<void>;
setVideoEnabled: (enabled: boolean) => Promise<void>;
reset: () => void;
}
// Module-level variables for timers
@@ -821,4 +822,38 @@ export const callStore = create<CallState>((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,
});
},
}));

View File

@@ -0,0 +1,326 @@
/**
* GroupManager - 蝢斤<E89DA2>蝞∠<E89D9E><E288A0><EFBFBD><E79285><EFBFBD><E59A97><EFBFBD><EFBFBD><EFBFBD>嚗?
*
* 雿輻鍂 Zustand store 餈𥡝<E9A488><F0A5A19D><EFBFBD><E59786><EFBFBD>?
* 靽脲<E99DBD>銝擧唂 API <20><><EFBFBD><EFBFBD>𤾸<EFBFBD>摰?
*/
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';
// ==================== 颲<>𨭌<EFBFBD>賣㺭 ====================
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 {
// ==================== 蝢斤<E89DA2><E696A4>𡑒”<F0A19192><EFBFBD> ====================
/**
* <20><EFBFBD>蝢斤<E89DA2><E696A4>𡑒”
*/
async getGroups(
page = 1,
pageSize = GROUP_LIST_PAGE_SIZE,
forceRefresh = false
): Promise<GroupListResponse> {
const store = useGroupManagerStore.getState();
const entry = store.groupsListEntry;
// 蝚砌<E89D9A>憿萎<E686BF><E8908E><EFBFBD><E39787><EFBFBD><EFBFBD>摮?
if (page === 1 && !forceRefresh && entry && !isCacheExpired(entry)) {
return buildGroupListResponse(entry.data, pageSize);
}
// 蝚砌<E89D9A>憿萎<E686BF>蝻枏<E89DBB><EFBFBD><E9A488>嚗𡁜<E59A97><F0A1819C><EFBFBD><E595A3>?
if (page === 1 && !forceRefresh && entry && isCacheExpired(entry)) {
this.refreshGroupsInBackground(page, pageSize);
return buildGroupListResponse(entry.data, pageSize);
}
// 蝚砌<E89D9A>憿蛛<E686BF>撠肽<E692A0><E882BD>砍𧑐蝻枏<E89DBB>
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);
}
}
// <20>𤏸絲霂瑟<E99C82>
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);
});
}
/**
* <20>𥕦遣蝢斤<E89DA2><E696A4>𡑒”<F0A19192>唳旿皞琜<E79A9E><E7909C><EFBFBD> Sources 璅<E79285>嚗?
*/
createGroupListSource(
kind: RemoteGroupListSourceKind = 'cursor',
pageSize: number = GROUP_LIST_PAGE_SIZE
): IGroupListPagedSource {
return createRemoteGroupListSource(kind, pageSize);
}
// ==================== 蝢斤<E89DA2>霂行<E99C82><E8A18C><EFBFBD> ====================
/**
* <20><EFBFBD>蝢斤<E89DA2>霂行<E99C82>
*/
async getGroup(groupId: string, forceRefresh = false): Promise<GroupResponse> {
const store = useGroupManagerStore.getState();
const entry = store.groupsMap.get(groupId);
// <20><EFBFBD>蝻枏<E89DBB>
if (!forceRefresh && entry && !isCacheExpired(entry)) {
return entry.data;
}
// 餈<><E9A488>蝻枏<E89DBB>嚗𡁜<E59A97><F0A1819C><EFBFBD><E595A3>?
if (!forceRefresh && entry && isCacheExpired(entry)) {
this.refreshGroupInBackground(groupId);
return entry.data;
}
// 撠肽<E692A0><E882BD>砍𧑐蝻枏<E89DBB>
if (!forceRefresh) {
const localGroup = await getGroupCache(groupId);
if (localGroup) {
store.setGroup(groupId, localGroup);
this.refreshGroupInBackground(groupId);
return localGroup;
}
}
// <20>𤏸絲霂瑟<E99C82>
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);
});
}
// ==================== 蝢斤<E89DA2><E696A4>𣂼<EFBFBD><F0A382BC><EFBFBD> ====================
/**
* <20><EFBFBD>蝢斤<E89DA2><E696A4>𣂼<EFBFBD><F0A382BC>𡑒”
*/
async getMembers(
groupId: string,
page = 1,
pageSize = GROUP_MEMBER_LIST_PAGE_SIZE,
forceRefresh = false
): Promise<GroupMemberListResponse> {
const key = buildMembersKey(groupId, page, pageSize);
const store = useGroupManagerStore.getState();
const entry = store.membersMap.get(key);
// <20><EFBFBD>蝻枏<E89DBB>
if (!forceRefresh && entry && !isCacheExpired(entry)) {
return buildMemberListResponse(entry.data, page, pageSize);
}
// 餈<><E9A488>蝻枏<E89DBB>嚗𡁜<E59A97><F0A1819C><EFBFBD><E595A3>?
if (!forceRefresh && entry && isCacheExpired(entry)) {
this.refreshMembersInBackground(groupId, page, pageSize);
return buildMemberListResponse(entry.data, page, pageSize);
}
// 蝚砌<E89D9A>憿蛛<E686BF>撠肽<E692A0><E882BD>砍𧑐蝻枏<E89DBB>
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);
}
}
// <20>𤏸絲霂瑟<E99C82>
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<typeof user> => 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<typeof user> => Boolean(user))
).catch(() => {});
return response;
}).catch((error) => {
console.error('[GroupManager] refreshMembersInBackground error:', error);
});
}
/**
* <20>𥕦遣蝢斤<E89DA2><E696A4>𣂼<EFBFBD><F0A382BC>𡑒”<F0A19192>唳旿皞琜<E79A9E><E7909C><EFBFBD> Sources 璅<E79285>嚗?
*/
createGroupMemberListSource(
groupId: string,
kind: RemoteGroupListSourceKind = 'cursor',
pageSize: number = GROUP_MEMBER_LIST_PAGE_SIZE
): IGroupMemberListPagedSource {
return createRemoteGroupMemberListSource(groupId, kind, pageSize);
}
// ==================== 蝻枏<E89DBB>蝞∠<E89D9E> ====================
/**
* 雿輻<E99BBF>摮睃仃<E79D83>?
*/
invalidate(groupId?: string): void {
if (!groupId) {
useGroupManagerStore.getState().invalidateAll();
return;
}
useGroupManagerStore.getState().invalidateGroup(groupId);
}
/**
* 皜<><EFBFBD><E88684><EFBFBD>㗇㺭<E39787>?
*/
clear(): void {
useGroupManagerStore.getState().reset();
}
// ==================== 霂瑟<E99C82><E7919F><EFBFBD> ====================
private async dedupe<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
const store = useGroupManagerStore.getState();
const pending = store.getPendingRequest<T>(key);
if (pending) return pending;
const request = fetcher().finally(() => {
useGroupManagerStore.getState().deletePendingRequest(key);
});
store.setPendingRequest(key, request);
return request;
}
}
// ==================== <20><EFBFBD>撖澆枂 ====================
export const groupManager = new GroupManager();
// 撖澆枂蝐颱誑<E9A2B1><EFBFBD>蝐餃<E89D90><EFBFBD><E79289><EFBFBD>瘚贝<E7989A>
export { GroupManager };
export default groupManager;

View File

@@ -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<GroupResponse[]> | null;
// 群组详情缓存
groupsMap: Map<string, CacheEntry<GroupResponse>>;
// 群组成员缓存 (key: members:groupId:page:pageSize)
membersMap: Map<string, CacheEntry<GroupMemberResponse[]>>;
// 加载状态
isLoadingGroups: boolean;
loadingGroupIds: Set<string>;
loadingMemberKeys: Set<string>;
// 待处理请求(去重)
pendingRequests: Map<string, Promise<any>>;
}
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<GroupResponse[]>) => 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: <T>(key: string) => Promise<T> | undefined;
setPendingRequest: <T>(key: string, request: Promise<T>) => 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<GroupManagerStore>((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<GroupResponse[]>) => {
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: <T>(key: string) => {
return get().pendingRequests.get(key) as Promise<T> | undefined;
},
setPendingRequest: <T>(key: string, request: Promise<T>) => {
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(),
});
},
}));

140
src/stores/group/hooks.ts Normal file
View File

@@ -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<void>;
}
/**
* 获取群组列表 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<GroupResponse | null>;
}
/**
* 获取单个群组 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<void>;
}
/**
* 获取群组成员 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 };
}

27
src/stores/group/index.ts Normal file
View File

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

View File

@@ -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<GroupManagerSnapshot>
| CacheEvent<{ error: unknown; context: string }>;
// ==================== 常量 ====================
const GROUP_TTL = 60 * 1000;
const MEMBERS_TTL = 120 * 1000;
// ==================== Manager 实现 ====================
class GroupManager extends CachedManager<GroupManagerSnapshot, GroupManagerEvent> {
/** 群组详情缓存 */
private groupDetailCache = new Map<string, CacheEntry<GroupResponse | null>>();
/** 群组成员缓存 */
private groupMembersCache = new Map<string, CacheEntry<GroupMemberResponse[]>>();
protected getSnapshot(): GroupManagerSnapshot {
return {
groupIds: [...this.groupDetailCache.keys()],
memberGroupIds: [...this.groupMembersCache.keys()],
};
}
// ==================== 群组列表相关 ====================
/**
* 获取群组列表
*/
async getGroups(page = 1, pageSize = GROUP_LIST_PAGE_SIZE, forceRefresh = false): Promise<GroupListResponse> {
const key = `list:${page}:${pageSize}`;
// 第一页且有有效缓存
if (page === 1 && !forceRefresh && this.hasValidCache(key)) {
const groups = this.getFromCache<GroupResponse[]>(key)!;
return this.buildGroupListResponse(groups, pageSize);
}
// 第一页且缓存过期:后台刷新
if (page === 1 && !forceRefresh && this.hasExpiredCache(key)) {
this.refreshGroupsInBackground(page, pageSize);
const groups = this.getFromCache<GroupResponse[]>(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<GroupResponse> {
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<GroupMemberListResponse> {
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<typeof user> => 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<typeof user> => 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';

View File

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

View File

@@ -0,0 +1,174 @@
/**
* PostManager - 撣硋<E692A3>蝞∠<E89D9E><E288A0><EFBFBD><E79285><EFBFBD><E59A97><EFBFBD><EFBFBD><EFBFBD>嚗?
*
* 雿輻鍂 Zustand store 餈𥡝<E9A488><F0A5A19D><EFBFBD><E59786><EFBFBD>?
* 靽脲<E99DBD>銝擧唂 API <20><><EFBFBD><EFBFBD>𤾸<EFBFBD>摰?
*/
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';
// ==================== 颲<>𨭌<EFBFBD>賣㺭 ====================
function buildListKey(type: string, page: number, pageSize: number): string {
return `list:${type}:${page}:${pageSize}`;
}
// ==================== PostManager 蝐?====================
class PostManager {
// ==================== <20>𡑒”<F0A19192><EFBFBD> ====================
/**
* <20><EFBFBD>撣硋<E692A3><E7A18B>𡑒”嚗<E2809D><E59A97>蝏笔<E89D8F>憿菜芋撘𧶏<E69298>
*/
async getPosts(
type: PostListTab = 'hot',
page = 1,
pageSize = POST_LIST_PAGE_SIZE,
forceRefresh = false
): Promise<Post[]> {
const key = buildListKey(type, page, pageSize);
const store = usePostManagerStore.getState();
const entry = store.listsMap.get(key);
// 璉<><E79289><EFBFBD><E4BAA4><EFBFBD><EFBFBD>摮?
if (!forceRefresh && entry && !isCacheExpired(entry)) {
return entry.data;
}
// 餈<><E9A488>蝻枏<E89DBB>嚗𡁜<E59A97><F0A1819C><EFBFBD><E595A3><EFBFBD>蝡见朖餈𥪜<E9A488><F0A5AA9C>扳㺭<E689B3>?
if (!forceRefresh && entry && isCacheExpired(entry)) {
this.refreshPostsInBackground(key, type, page, pageSize);
return entry.data;
}
// <20><EFBFBD>摮矋<E691AE><E79F8B>𤏸絲霂瑟<E99C82>
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);
});
}
/**
* <20>𥕦遣撣硋<E692A3><E7A18B>𡑒”<F0A19192>唳旿皞琜<E79A9E><E7909C><EFBFBD> Sources 璅<E79285>嚗?
*/
createPostListSource(
options: { tab?: PostListTab; channelId?: string; pageSize?: number } = {},
kind: RemotePostListSourceKind = 'cursor'
): IPostListPagedSource {
return createRemotePostListSource(kind, options);
}
// ==================== 霂行<E99C82><E8A18C><EFBFBD> ====================
/**
* <20><EFBFBD>撣硋<E692A3>霂行<E99C82>
*/
async getPostDetail(postId: string, forceRefresh = false): Promise<Post | null> {
const store = usePostManagerStore.getState();
const entry = store.detailsMap.get(postId);
// <20><EFBFBD>蝻枏<E89DBB>
if (!forceRefresh && entry && !isCacheExpired(entry)) {
return entry.data;
}
// 餈<><E9A488>蝻枏<E89DBB>嚗𡁜<E59A97><F0A1819C><EFBFBD><E595A3>?
if (!forceRefresh && entry && isCacheExpired(entry) && entry.data) {
this.refreshPostDetailInBackground(postId);
return entry.data;
}
// <20><EFBFBD>摮矋<E691AE><E79F8B>𤏸絲霂瑟<E99C82>
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);
});
}
// ==================== 蝻枏<E89DBB>蝞∠<E89D9E> ====================
/**
* 雿輻<E99BBF>摮睃仃<E79D83>?
*/
invalidate(postId?: string): void {
if (postId) {
usePostManagerStore.getState().invalidateDetail(postId);
return;
}
usePostManagerStore.getState().invalidateAll();
}
/**
* 皜<><EFBFBD><E88684><EFBFBD>㗇㺭<E39787>?
*/
clear(): void {
usePostManagerStore.getState().reset();
}
// ==================== 霂瑟<E99C82><E7919F><EFBFBD> ====================
private async dedupe<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
const store = usePostManagerStore.getState();
const pending = store.getPendingRequest<T>(key);
if (pending) return pending;
const request = fetcher().finally(() => {
usePostManagerStore.getState().deletePendingRequest(key);
});
store.setPendingRequest(key, request);
return request;
}
}
// ==================== <20><EFBFBD>撖澆枂 ====================
export const postManager = new PostManager();
// 撖澆枂蝐颱誑<E9A2B1><EFBFBD>蝐餃<E89D90><EFBFBD><E79289><EFBFBD>瘚贝<E7989A>
export { PostManager };
export default postManager;

102
src/stores/post/hooks.ts Normal file
View File

@@ -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<void>;
}
/**
* 获取帖子列表 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<Post | null>;
}
/**
* 获取帖子详情 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 };
}

25
src/stores/post/index.ts Normal file
View File

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

View File

@@ -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<string, CacheEntry<Post[]>>;
// 帖子详情缓存
detailsMap: Map<string, CacheEntry<Post | null>>;
// 加载状态
loadingListKeys: Set<string>;
loadingDetailIds: Set<string>;
// 待处理请求(去重)
pendingRequests: Map<string, Promise<any>>;
}
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: <T>(key: string) => Promise<T> | undefined;
setPendingRequest: <T>(key: string, request: Promise<T>) => 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<PostManagerStore>((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: <T>(key: string) => {
return get().pendingRequests.get(key) as Promise<T> | undefined;
},
setPendingRequest: <T>(key: string, request: Promise<T>) => {
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(),
});
},
}));

View File

@@ -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<PostListPayload>
| CacheEvent<PostDetailPayload>
| CacheEvent<PostManagerSnapshot>
| CacheEvent<{ error: unknown; context: string }>;
// ==================== 常量 ====================
const LIST_TTL = 30 * 1000;
const DETAIL_TTL = 60 * 1000;
// ==================== Manager 实现 ====================
class PostManager extends CachedManager<PostManagerSnapshot, PostManagerEvent> {
/** 详情缓存 */
private detailCache = new Map<string, CacheEntry<Post | null>>();
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<Post[]> {
const key = this.listKey(type, page, pageSize);
// 检查有效缓存
if (!forceRefresh && this.hasValidCache(key)) {
return this.getFromCache<Post[]>(key)!;
}
// 过期缓存:后台刷新,立即返回旧数据
if (!forceRefresh && this.hasExpiredCache(key)) {
this.refreshPostsInBackground(key, type, page, pageSize);
return this.getFromCache<Post[]>(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<Post | null> {
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';

View File

@@ -0,0 +1,178 @@
/**
* UserManager - 用户管ç<C2A1>†æ ¸å¿ƒæ¨¡å<C2A1>—(é‡<C3A9>构版ï¼?
*
* 使用 Zustand store è¿è¡Œçжæ€<C3A6>管ç<C2A1>?
* ä¿<C3A4>æŒ<C3A6>与旧 API çš„å<E2809E>å<E28098>Žå…¼å®?
*/
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 {
/**
* 获å<C2B7>当å‰<C3A5>用户
* 支æŒ<C3A6>缓存过期检查åŒå<C592>Žå<C5BD>°åˆ·æ°
*/
async getCurrentUser(forceRefresh = false): Promise<UserDTO | null> {
const store = useUserManagerStore.getState();
const entry = store.currentUserEntry;
// 有效缓存
if (!forceRefresh && entry && !isCacheExpired(entry)) {
return entry.data;
}
// 过期缓存:å<C5A1>Žå<C5BD>°åˆ·æ°ï¼Œç«å<E280B9>³è¿”åžæ—§æ•°æ<C2B0>?
if (!forceRefresh && entry && isCacheExpired(entry)) {
this.refreshCurrentUserInBackground();
return entry.data;
}
// å°<C3A5>试从本地数æ<C2B0>®åº“加载
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;
});
}
/**
* å<>Žå<C5BD>°åˆ·æ°å½“å‰<C3A5>用户
*/
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);
});
}
/**
* æ ¹æ<C2B9>® ID 获å<C2B7>用户
*/
async getUserById(userId: string, forceRefresh = false): Promise<UserDTO | null> {
const store = useUserManagerStore.getState();
const entry = store.usersMap.get(userId);
// 有效缓存
if (!forceRefresh && entry && !isCacheExpired(entry)) {
return entry.data;
}
// 过期缓存:å<C5A1>Žå<C5BD>°åˆ·æ°ï¼Œç«å<E280B9>³è¿”åžæ—§æ•°æ<C2B0>?
if (!forceRefresh && entry && isCacheExpired(entry)) {
this.refreshUserByIdInBackground(userId);
return entry.data;
}
// å°<C3A5>试从本地数æ<C2B0>®åº“加载
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;
});
}
/**
* å<>Žå<C5BD>°åˆ·æ°ç”¨æˆ·
*/
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();
}
/**
* 请æ±åŽ»é‡<C3A9>
*/
private async dedupe<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
const store = useUserManagerStore.getState();
const pending = store.getPendingRequest<T>(key);
if (pending) return pending;
const request = fetcher().finally(() => {
useUserManagerStore.getState().deletePendingRequest(key);
});
store.setPendingRequest(key, request);
return request;
}
}
// ==================== å<>•ä¾å¯¼å‡º ====================
export const userManager = new UserManager();
// 导出类以支æŒ<C3A6>ç±»åžæ£€æŸ¥åŒæµè¯•
export { UserManager };
export default userManager;

136
src/stores/user/hooks.ts Normal file
View File

@@ -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<UserDTO | null>;
}
/**
* 获å<C2B7>当å‰<C3A5>用户 hook
* 自动加载,支æŒ<C3A6>缓存åŒå<C592>Žå<C5BD>°åˆ·æ°
*/
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<UserDTO | null>;
}
/**
* æ ¹æ<C2B9>® ID 获å<C2B7>用户 hook
* 自动加载,支æŒ<C3A6>缓存åŒå<C592>Žå<C5BD>°åˆ·æ°
*/
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<UserDTO | null>;
}
/**
* 多用户获å<C2B7>?hook
* 用于批é‡<C3A9>获å<C2B7>多个用户信æ<C2A1>¯
*/
export function useUsers(userIds: string[]): UseUsersResult {
const usersMap = useUserManagerStore(state => state.usersMap);
const loadingUserIds = useUserManagerStore(state => state.loadingUserIds);
const [fetchedIds, setFetchedIds] = useState<Set<string>>(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 };
}

27
src/stores/user/index.ts Normal file
View File

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

View File

@@ -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<UserDTO | null> | null;
// 其他用户缓存
usersMap: Map<string, CacheEntry<UserDTO | null>>;
// 加载状态
isLoadingCurrentUser: boolean;
loadingUserIds: Set<string>;
// 待处理请求(去重)
pendingRequests: Map<string, Promise<any>>;
}
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<UserDTO | null>) => 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: <T>(key: string) => Promise<T> | undefined;
setPendingRequest: <T>(key: string, request: Promise<T>) => 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<UserManagerStore>((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<UserDTO | null>) => {
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: <T>(key: string) => {
return get().pendingRequests.get(key) as Promise<T> | undefined;
},
setPendingRequest: <T>(key: string, request: Promise<T>) => {
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(),
});
},
}));

View File

@@ -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<UserManagerSnapshot>
| CacheEvent<{ error: unknown; context: string }>;
const USER_TTL = 5 * 60 * 1000;
interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number;
}
class UserManager extends CacheBus<UserManagerSnapshot, UserManagerEvent> {
private currentUserCache: CacheEntry<UserDTO | null> | null = null;
private userCache = new Map<string, CacheEntry<UserDTO | null>>();
private pendingRequests = new Map<string, Promise<any>>();
protected getSnapshot(): UserManagerSnapshot {
return {
hasCurrentUser: !!this.currentUserCache?.data,
cachedUserIds: [...this.userCache.keys()],
};
}
private isExpired(entry: CacheEntry<any>): boolean {
return Date.now() - entry.timestamp > entry.ttl;
}
private async dedupe<T>(key: string, fetcher: () => Promise<T>): Promise<T> {
const pending = this.pendingRequests.get(key) as Promise<T> | 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<UserDTO | null> {
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<UserDTO | null> {
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';

42
src/stores/utils/cache.ts Normal file
View File

@@ -0,0 +1,42 @@
/**
* 缓存工具函数
* 提取自 baseManager.ts用于 Zustand store 的 TTL 缓存管理
*/
/** 缓存条目结构 */
export interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number;
}
/** 创建缓存条目 */
export function createCacheEntry<T>(data: T, ttl: number): CacheEntry<T> {
return { data, timestamp: Date.now(), ttl };
}
/** 检查缓存是否过期 */
export function isCacheExpired(entry: CacheEntry<any>): boolean {
return Date.now() - entry.timestamp > entry.ttl;
}
/** 检查缓存是否有效(存在且未过期) */
export function isCacheValid<T>(entry: CacheEntry<T> | undefined | null): boolean {
if (!entry) return false;
return !isCacheExpired(entry);
}
/** 获取缓存的剩余有效时间(毫秒) */
export function getCacheRemainingTTL(entry: CacheEntry<any>): 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 分钟
};