/* eslint-disable no-console, @typescript-eslint/no-explicit-any, @typescript-eslint/no-empty-function */ 'use client'; /** * 仅在浏览器端使用的数据库工具,目前基于 localStorage 实现。 * 之所以单独拆分文件,是为了避免在客户端 bundle 中引入 `fs`, `path` 等 Node.js 内置模块, * 从而解决诸如 "Module not found: Can't resolve 'fs'" 的问题。 * * 功能: * 1. 获取全部播放记录(getAllPlayRecords)。 * 2. 保存播放记录(savePlayRecord)。 * 3. 数据库存储模式下的混合缓存策略,提升用户体验。 * * 如后续需要在客户端读取收藏等其它数据,可按同样方式在此文件中补充实现。 */ import { getAuthInfoFromBrowserCookie } from './auth'; // ---- 类型 ---- export interface PlayRecord { title: string; source_name: string; year: string; cover: string; index: number; // 第几集 total_episodes: number; // 总集数 play_time: number; // 播放进度(秒) total_time: number; // 总进度(秒) save_time: number; // 记录保存时间(时间戳) search_title?: string; // 搜索时使用的标题 } // ---- 收藏类型 ---- export interface Favorite { title: string; source_name: string; year: string; cover: string; total_episodes: number; save_time: number; search_title?: string; } // ---- 缓存数据结构 ---- interface CacheData { data: T; timestamp: number; version: string; } interface UserCacheStore { playRecords?: CacheData>; favorites?: CacheData>; searchHistory?: CacheData; } // ---- 常量 ---- // 新的键名(KatelyaTV)与旧键名(MoonTV)保持向后兼容 const PLAY_RECORDS_KEY = 'katelyatv_play_records'; const FAVORITES_KEY = 'katelyatv_favorites'; const SEARCH_HISTORY_KEY = 'katelyatv_search_history'; const LEGACY_PLAY_RECORDS_KEY = 'moontv_play_records'; const LEGACY_FAVORITES_KEY = 'moontv_favorites'; const LEGACY_SEARCH_HISTORY_KEY = 'moontv_search_history'; // 缓存相关常量 const CACHE_PREFIX = 'katelyatv_cache_'; const _LEGACY_CACHE_PREFIX = 'moontv_cache_'; // 保留用于将来的迁移功能 const CACHE_VERSION = '1.0.0'; const CACHE_EXPIRE_TIME = 60 * 60 * 1000; // 一小时缓存过期 // ---- 环境变量 ---- const STORAGE_TYPE = (() => { const raw = (typeof window !== 'undefined' && (window as any).RUNTIME_CONFIG?.STORAGE_TYPE) || (process.env.STORAGE_TYPE as | 'localstorage' | 'redis' | 'd1' | 'upstash' | undefined) || 'localstorage'; return raw; })(); // ---------------- 搜索历史相关常量 ---------------- // 搜索历史最大保存条数 const SEARCH_HISTORY_LIMIT = 20; // ---- 缓存管理器 ---- class HybridCacheManager { private static instance: HybridCacheManager; static getInstance(): HybridCacheManager { if (!HybridCacheManager.instance) { HybridCacheManager.instance = new HybridCacheManager(); } return HybridCacheManager.instance; } /** * 获取当前用户名 */ private getCurrentUsername(): string | null { const authInfo = getAuthInfoFromBrowserCookie(); return authInfo?.username || null; } /** * 生成用户专属的缓存key */ private getUserCacheKey(username: string): string { return `${CACHE_PREFIX}${username}`; } /** * 获取用户缓存数据 */ private getUserCache(username: string): UserCacheStore { if (typeof window === 'undefined') return {}; try { const cacheKey = this.getUserCacheKey(username); const cached = localStorage.getItem(cacheKey); return cached ? JSON.parse(cached) : {}; } catch (error) { console.warn('获取用户缓存失败:', error); return {}; } } /** * 保存用户缓存数据 */ private saveUserCache(username: string, cache: UserCacheStore): void { if (typeof window === 'undefined') return; try { const cacheKey = this.getUserCacheKey(username); localStorage.setItem(cacheKey, JSON.stringify(cache)); } catch (error) { console.warn('保存用户缓存失败:', error); } } /** * 检查缓存是否有效 */ private isCacheValid(cache: CacheData): boolean { const now = Date.now(); return ( cache.version === CACHE_VERSION && now - cache.timestamp < CACHE_EXPIRE_TIME ); } /** * 创建缓存数据 */ private createCacheData(data: T): CacheData { return { data, timestamp: Date.now(), version: CACHE_VERSION, }; } /** * 获取缓存的播放记录 */ getCachedPlayRecords(): Record | null { const username = this.getCurrentUsername(); if (!username) return null; const userCache = this.getUserCache(username); const cached = userCache.playRecords; if (cached && this.isCacheValid(cached)) { return cached.data; } return null; } /** * 缓存播放记录 */ cachePlayRecords(data: Record): void { const username = this.getCurrentUsername(); if (!username) return; const userCache = this.getUserCache(username); userCache.playRecords = this.createCacheData(data); this.saveUserCache(username, userCache); } /** * 获取缓存的收藏 */ getCachedFavorites(): Record | null { const username = this.getCurrentUsername(); if (!username) return null; const userCache = this.getUserCache(username); const cached = userCache.favorites; if (cached && this.isCacheValid(cached)) { return cached.data; } return null; } /** * 缓存收藏 */ cacheFavorites(data: Record): void { const username = this.getCurrentUsername(); if (!username) return; const userCache = this.getUserCache(username); userCache.favorites = this.createCacheData(data); this.saveUserCache(username, userCache); } /** * 获取缓存的搜索历史 */ getCachedSearchHistory(): string[] | null { const username = this.getCurrentUsername(); if (!username) return null; const userCache = this.getUserCache(username); const cached = userCache.searchHistory; if (cached && this.isCacheValid(cached)) { return cached.data; } return null; } /** * 缓存搜索历史 */ cacheSearchHistory(data: string[]): void { const username = this.getCurrentUsername(); if (!username) return; const userCache = this.getUserCache(username); userCache.searchHistory = this.createCacheData(data); this.saveUserCache(username, userCache); } /** * 清除指定用户的所有缓存 */ clearUserCache(username?: string): void { const targetUsername = username || this.getCurrentUsername(); if (!targetUsername) return; try { const cacheKey = this.getUserCacheKey(targetUsername); localStorage.removeItem(cacheKey); } catch (error) { console.warn('清除用户缓存失败:', error); } } /** * 清除所有过期缓存 */ clearExpiredCaches(): void { if (typeof window === 'undefined') return; try { const keysToRemove: string[] = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key?.startsWith(CACHE_PREFIX)) { try { const cache = JSON.parse(localStorage.getItem(key) || '{}'); // 检查是否有任何缓存数据过期 let hasValidData = false; for (const [, cacheData] of Object.entries(cache)) { if (cacheData && this.isCacheValid(cacheData as CacheData)) { hasValidData = true; break; } } if (!hasValidData) { keysToRemove.push(key); } } catch { // 解析失败的缓存也删除 keysToRemove.push(key); } } } keysToRemove.forEach((key) => localStorage.removeItem(key)); } catch (error) { console.warn('清除过期缓存失败:', error); } } } // 获取缓存管理器实例 const cacheManager = HybridCacheManager.getInstance(); // ---- 错误处理辅助函数 ---- /** * 数据库操作失败时的通用错误处理 * 立即从数据库刷新对应类型的缓存以保持数据一致性 */ async function handleDatabaseOperationFailure( dataType: 'playRecords' | 'favorites' | 'searchHistory', error: any ): Promise { console.error(`数据库操作失败 (${dataType}):`, error); try { let freshData: any; let eventName: string; switch (dataType) { case 'playRecords': freshData = await fetchFromApi>( `/api/playrecords` ); cacheManager.cachePlayRecords(freshData); eventName = 'playRecordsUpdated'; break; case 'favorites': freshData = await fetchFromApi>( `/api/favorites` ); cacheManager.cacheFavorites(freshData); eventName = 'favoritesUpdated'; break; case 'searchHistory': freshData = await fetchFromApi(`/api/searchhistory`); cacheManager.cacheSearchHistory(freshData); eventName = 'searchHistoryUpdated'; break; } // 触发更新事件通知组件 window.dispatchEvent( new CustomEvent(eventName, { detail: freshData, }) ); } catch (refreshErr) { console.error(`刷新${dataType}缓存失败:`, refreshErr); } } // 页面加载时清理过期缓存 if (typeof window !== 'undefined') { setTimeout(() => cacheManager.clearExpiredCaches(), 1000); } // ---- 工具函数 ---- async function fetchFromApi(path: string): Promise { const res = await fetch(path); if (!res.ok) throw new Error(`请求 ${path} 失败: ${res.status}`); return (await res.json()) as T; } /** * 生成存储key */ export function generateStorageKey(source: string, id: string): string { return `${source}+${id}`; } // ---- API ---- /** * 读取全部播放记录。 * D1 存储模式下使用混合缓存策略:优先返回缓存数据,后台异步同步最新数据。 * 在服务端渲染阶段 (window === undefined) 时返回空对象,避免报错。 */ export async function getAllPlayRecords(): Promise> { // 服务器端渲染阶段直接返回空,交由客户端 useEffect 再行请求 if (typeof window === 'undefined') { return {}; } // 数据库存储模式:使用混合缓存策略(包括 redis、d1、upstash) if (STORAGE_TYPE !== 'localstorage') { // 优先从缓存获取数据 const cachedData = cacheManager.getCachedPlayRecords(); if (cachedData) { // 返回缓存数据,同时后台异步更新 fetchFromApi>(`/api/playrecords`) .then((freshData) => { // 只有数据真正不同时才更新缓存 if (JSON.stringify(cachedData) !== JSON.stringify(freshData)) { cacheManager.cachePlayRecords(freshData); // 触发数据更新事件,供组件监听 window.dispatchEvent( new CustomEvent('playRecordsUpdated', { detail: freshData, }) ); } }) .catch((err) => { console.warn('后台同步播放记录失败:', err); }); return cachedData; } else { // 缓存为空,直接从 API 获取并缓存 try { const freshData = await fetchFromApi>( `/api/playrecords` ); cacheManager.cachePlayRecords(freshData); return freshData; } catch (err) { console.error('获取播放记录失败:', err); return {}; } } } // localstorage 模式 try { const primary = localStorage.getItem(PLAY_RECORDS_KEY); const fallback = localStorage.getItem(LEGACY_PLAY_RECORDS_KEY); const raw = primary ?? fallback; if (!raw) return {}; return JSON.parse(raw) as Record; } catch (err) { console.error('读取播放记录失败:', err); return {}; } } /** * 保存播放记录。 * 数据库存储模式下使用乐观更新:先更新缓存(立即生效),再异步同步到数据库。 */ export async function savePlayRecord( source: string, id: string, record: PlayRecord ): Promise { const key = generateStorageKey(source, id); // 数据库存储模式:乐观更新策略(包括 redis、d1、upstash) if (STORAGE_TYPE !== 'localstorage') { // 立即更新缓存 const cachedRecords = cacheManager.getCachedPlayRecords() || {}; cachedRecords[key] = record; cacheManager.cachePlayRecords(cachedRecords); // 触发立即更新事件 window.dispatchEvent( new CustomEvent('playRecordsUpdated', { detail: cachedRecords, }) ); // 异步同步到数据库 try { const res = await fetch('/api/playrecords', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ key, record }), }); if (!res.ok) { throw new Error(`保存播放记录失败: ${res.status}`); } } catch (err) { await handleDatabaseOperationFailure('playRecords', err); throw err; } return; } // localstorage 模式 if (typeof window === 'undefined') { console.warn('无法在服务端保存播放记录到 localStorage'); return; } try { const allRecords = await getAllPlayRecords(); allRecords[key] = record; localStorage.setItem(PLAY_RECORDS_KEY, JSON.stringify(allRecords)); window.dispatchEvent( new CustomEvent('playRecordsUpdated', { detail: allRecords, }) ); } catch (err) { console.error('保存播放记录失败:', err); throw err; } } /** * 删除播放记录。 * 数据库存储模式下使用乐观更新:先更新缓存,再异步同步到数据库。 */ export async function deletePlayRecord( source: string, id: string ): Promise { const key = generateStorageKey(source, id); // 数据库存储模式:乐观更新策略(包括 redis、d1、upstash) if (STORAGE_TYPE !== 'localstorage') { // 立即更新缓存 const cachedRecords = cacheManager.getCachedPlayRecords() || {}; delete cachedRecords[key]; cacheManager.cachePlayRecords(cachedRecords); // 触发立即更新事件 window.dispatchEvent( new CustomEvent('playRecordsUpdated', { detail: cachedRecords, }) ); // 异步同步到数据库 try { const res = await fetch( `/api/playrecords?key=${encodeURIComponent(key)}`, { method: 'DELETE', } ); if (!res.ok) throw new Error(`删除播放记录失败: ${res.status}`); } catch (err) { await handleDatabaseOperationFailure('playRecords', err); throw err; } return; } // localstorage 模式 if (typeof window === 'undefined') { console.warn('无法在服务端删除播放记录到 localStorage'); return; } try { const allRecords = await getAllPlayRecords(); delete allRecords[key]; localStorage.setItem(PLAY_RECORDS_KEY, JSON.stringify(allRecords)); window.dispatchEvent( new CustomEvent('playRecordsUpdated', { detail: allRecords, }) ); } catch (err) { console.error('删除播放记录失败:', err); throw err; } } /* ---------------- 搜索历史相关 API ---------------- */ /** * 获取搜索历史。 * 数据库存储模式下使用混合缓存策略:优先返回缓存数据,后台异步同步最新数据。 */ export async function getSearchHistory(): Promise { // 服务器端渲染阶段直接返回空 if (typeof window === 'undefined') { return []; } // 数据库存储模式:使用混合缓存策略(包括 redis、d1、upstash) if (STORAGE_TYPE !== 'localstorage') { // 优先从缓存获取数据 const cachedData = cacheManager.getCachedSearchHistory(); if (cachedData) { // 返回缓存数据,同时后台异步更新 fetchFromApi(`/api/searchhistory`) .then((freshData) => { // 只有数据真正不同时才更新缓存 if (JSON.stringify(cachedData) !== JSON.stringify(freshData)) { cacheManager.cacheSearchHistory(freshData); // 触发数据更新事件 window.dispatchEvent( new CustomEvent('searchHistoryUpdated', { detail: freshData, }) ); } }) .catch((err) => { console.warn('后台同步搜索历史失败:', err); }); return cachedData; } else { // 缓存为空,直接从 API 获取并缓存 try { const freshData = await fetchFromApi(`/api/searchhistory`); cacheManager.cacheSearchHistory(freshData); return freshData; } catch (err) { console.error('获取搜索历史失败:', err); return []; } } } // localStorage 模式 try { const primary = localStorage.getItem(SEARCH_HISTORY_KEY); const fallback = localStorage.getItem(LEGACY_SEARCH_HISTORY_KEY); const raw = primary ?? fallback; if (!raw) return []; const arr = JSON.parse(raw) as string[]; // 仅返回字符串数组 return Array.isArray(arr) ? arr : []; } catch (err) { console.error('读取搜索历史失败:', err); return []; } } /** * 将关键字添加到搜索历史。 * 数据库存储模式下使用乐观更新:先更新缓存,再异步同步到数据库。 */ export async function addSearchHistory(keyword: string): Promise { const trimmed = keyword.trim(); if (!trimmed) return; // 数据库存储模式:乐观更新策略(包括 redis、d1、upstash) if (STORAGE_TYPE !== 'localstorage') { // 立即更新缓存 const cachedHistory = cacheManager.getCachedSearchHistory() || []; const newHistory = [trimmed, ...cachedHistory.filter((k) => k !== trimmed)]; // 限制长度 if (newHistory.length > SEARCH_HISTORY_LIMIT) { newHistory.length = SEARCH_HISTORY_LIMIT; } cacheManager.cacheSearchHistory(newHistory); // 触发立即更新事件 window.dispatchEvent( new CustomEvent('searchHistoryUpdated', { detail: newHistory, }) ); // 异步同步到数据库 try { const res = await fetch('/api/searchhistory', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ keyword: trimmed }), }); if (!res.ok) throw new Error(`保存搜索历史失败: ${res.status}`); } catch (err) { await handleDatabaseOperationFailure('searchHistory', err); } return; } // localStorage 模式 if (typeof window === 'undefined') return; try { const history = await getSearchHistory(); const newHistory = [trimmed, ...history.filter((k) => k !== trimmed)]; // 限制长度 if (newHistory.length > SEARCH_HISTORY_LIMIT) { newHistory.length = SEARCH_HISTORY_LIMIT; } localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(newHistory)); window.dispatchEvent( new CustomEvent('searchHistoryUpdated', { detail: newHistory, }) ); } catch (err) { console.error('保存搜索历史失败:', err); } } /** * 清空搜索历史。 * 数据库存储模式下使用乐观更新:先更新缓存,再异步同步到数据库。 */ export async function clearSearchHistory(): Promise { // 数据库存储模式:乐观更新策略(包括 redis、d1、upstash) if (STORAGE_TYPE !== 'localstorage') { // 立即更新缓存 cacheManager.cacheSearchHistory([]); // 触发立即更新事件 window.dispatchEvent( new CustomEvent('searchHistoryUpdated', { detail: [], }) ); // 异步同步到数据库 try { const res = await fetch(`/api/searchhistory`, { method: 'DELETE', }); if (!res.ok) throw new Error(`清空搜索历史失败: ${res.status}`); } catch (err) { await handleDatabaseOperationFailure('searchHistory', err); } return; } // localStorage 模式 if (typeof window === 'undefined') return; localStorage.removeItem(SEARCH_HISTORY_KEY); window.dispatchEvent( new CustomEvent('searchHistoryUpdated', { detail: [], }) ); } /** * 删除单条搜索历史。 * 数据库存储模式下使用乐观更新:先更新缓存,再异步同步到数据库。 */ export async function deleteSearchHistory(keyword: string): Promise { const trimmed = keyword.trim(); if (!trimmed) return; // 数据库存储模式:乐观更新策略(包括 redis、d1、upstash) if (STORAGE_TYPE !== 'localstorage') { // 立即更新缓存 const cachedHistory = cacheManager.getCachedSearchHistory() || []; const newHistory = cachedHistory.filter((k) => k !== trimmed); cacheManager.cacheSearchHistory(newHistory); // 触发立即更新事件 window.dispatchEvent( new CustomEvent('searchHistoryUpdated', { detail: newHistory, }) ); // 异步同步到数据库 try { const res = await fetch( `/api/searchhistory?keyword=${encodeURIComponent(trimmed)}`, { method: 'DELETE', } ); if (!res.ok) throw new Error(`删除搜索历史失败: ${res.status}`); } catch (err) { await handleDatabaseOperationFailure('searchHistory', err); } return; } // localStorage 模式 if (typeof window === 'undefined') return; try { const history = await getSearchHistory(); const newHistory = history.filter((k) => k !== trimmed); localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(newHistory)); window.dispatchEvent( new CustomEvent('searchHistoryUpdated', { detail: newHistory, }) ); } catch (err) { console.error('删除搜索历史失败:', err); } } // ---------------- 收藏相关 API ---------------- /** * 获取全部收藏。 * 数据库存储模式下使用混合缓存策略:优先返回缓存数据,后台异步同步最新数据。 */ export async function getAllFavorites(): Promise> { // 服务器端渲染阶段直接返回空 if (typeof window === 'undefined') { return {}; } // 数据库存储模式:使用混合缓存策略(包括 redis、d1、upstash) if (STORAGE_TYPE !== 'localstorage') { // 优先从缓存获取数据 const cachedData = cacheManager.getCachedFavorites(); if (cachedData) { // 返回缓存数据,同时后台异步更新 fetchFromApi>(`/api/favorites`) .then((freshData) => { // 只有数据真正不同时才更新缓存 if (JSON.stringify(cachedData) !== JSON.stringify(freshData)) { cacheManager.cacheFavorites(freshData); // 触发数据更新事件 window.dispatchEvent( new CustomEvent('favoritesUpdated', { detail: freshData, }) ); } }) .catch((err) => { console.warn('后台同步收藏失败:', err); }); return cachedData; } else { // 缓存为空,直接从 API 获取并缓存 try { const freshData = await fetchFromApi>( `/api/favorites` ); cacheManager.cacheFavorites(freshData); return freshData; } catch (err) { console.error('获取收藏失败:', err); return {}; } } } // localStorage 模式 try { const primary = localStorage.getItem(FAVORITES_KEY); const fallback = localStorage.getItem(LEGACY_FAVORITES_KEY); const raw = primary ?? fallback; if (!raw) return {}; return JSON.parse(raw) as Record; } catch (err) { console.error('读取收藏失败:', err); return {}; } } /** * 保存收藏。 * 数据库存储模式下使用乐观更新:先更新缓存,再异步同步到数据库。 */ export async function saveFavorite( source: string, id: string, favorite: Favorite ): Promise { const key = generateStorageKey(source, id); // 数据库存储模式:乐观更新策略(包括 redis、d1、upstash) if (STORAGE_TYPE !== 'localstorage') { // 立即更新缓存 const cachedFavorites = cacheManager.getCachedFavorites() || {}; cachedFavorites[key] = favorite; cacheManager.cacheFavorites(cachedFavorites); // 触发立即更新事件 window.dispatchEvent( new CustomEvent('favoritesUpdated', { detail: cachedFavorites, }) ); // 异步同步到数据库 try { const res = await fetch('/api/favorites', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ key, favorite }), }); if (!res.ok) throw new Error(`保存收藏失败: ${res.status}`); } catch (err) { await handleDatabaseOperationFailure('favorites', err); throw err; } return; } // localStorage 模式 if (typeof window === 'undefined') { console.warn('无法在服务端保存收藏到 localStorage'); return; } try { const allFavorites = await getAllFavorites(); allFavorites[key] = favorite; localStorage.setItem(FAVORITES_KEY, JSON.stringify(allFavorites)); window.dispatchEvent( new CustomEvent('favoritesUpdated', { detail: allFavorites, }) ); } catch (err) { console.error('保存收藏失败:', err); throw err; } } /** * 删除收藏。 * 数据库存储模式下使用乐观更新:先更新缓存,再异步同步到数据库。 */ export async function deleteFavorite( source: string, id: string ): Promise { const key = generateStorageKey(source, id); // 数据库存储模式:乐观更新策略(包括 redis、d1、upstash) if (STORAGE_TYPE !== 'localstorage') { // 立即更新缓存 const cachedFavorites = cacheManager.getCachedFavorites() || {}; delete cachedFavorites[key]; cacheManager.cacheFavorites(cachedFavorites); // 触发立即更新事件 window.dispatchEvent( new CustomEvent('favoritesUpdated', { detail: cachedFavorites, }) ); // 异步同步到数据库 try { const res = await fetch(`/api/favorites?key=${encodeURIComponent(key)}`, { method: 'DELETE', }); if (!res.ok) throw new Error(`删除收藏失败: ${res.status}`); } catch (err) { await handleDatabaseOperationFailure('favorites', err); throw err; } return; } // localStorage 模式 if (typeof window === 'undefined') { console.warn('无法在服务端删除收藏到 localStorage'); return; } try { const allFavorites = await getAllFavorites(); delete allFavorites[key]; localStorage.setItem(FAVORITES_KEY, JSON.stringify(allFavorites)); window.dispatchEvent( new CustomEvent('favoritesUpdated', { detail: allFavorites, }) ); } catch (err) { console.error('删除收藏失败:', err); throw err; } } /** * 判断是否已收藏。 * 数据库存储模式下使用混合缓存策略:优先返回缓存数据,后台异步同步最新数据。 */ export async function isFavorited( source: string, id: string ): Promise { const key = generateStorageKey(source, id); // 数据库存储模式:使用混合缓存策略(包括 redis、d1、upstash) if (STORAGE_TYPE !== 'localstorage') { const cachedFavorites = cacheManager.getCachedFavorites(); if (cachedFavorites) { // 返回缓存数据,同时后台异步更新 fetchFromApi>(`/api/favorites`) .then((freshData) => { // 只有数据真正不同时才更新缓存 if (JSON.stringify(cachedFavorites) !== JSON.stringify(freshData)) { cacheManager.cacheFavorites(freshData); // 触发数据更新事件 window.dispatchEvent( new CustomEvent('favoritesUpdated', { detail: freshData, }) ); } }) .catch((err) => { console.warn('后台同步收藏失败:', err); }); return !!cachedFavorites[key]; } else { // 缓存为空,直接从 API 获取并缓存 try { const freshData = await fetchFromApi>( `/api/favorites` ); cacheManager.cacheFavorites(freshData); return !!freshData[key]; } catch (err) { console.error('检查收藏状态失败:', err); return false; } } } // localStorage 模式 const allFavorites = await getAllFavorites(); return !!allFavorites[key]; } /** * 清空全部播放记录 * 数据库存储模式下使用乐观更新:先更新缓存,再异步同步到数据库。 */ export async function clearAllPlayRecords(): Promise { // 数据库存储模式:乐观更新策略(包括 redis、d1、upstash) if (STORAGE_TYPE !== 'localstorage') { // 立即更新缓存 cacheManager.cachePlayRecords({}); // 触发立即更新事件 window.dispatchEvent( new CustomEvent('playRecordsUpdated', { detail: {}, }) ); // 异步同步到数据库 try { const res = await fetch(`/api/playrecords`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, }); if (!res.ok) throw new Error(`清空播放记录失败: ${res.status}`); } catch (err) { await handleDatabaseOperationFailure('playRecords', err); throw err; } return; } // localStorage 模式 if (typeof window === 'undefined') return; localStorage.removeItem(PLAY_RECORDS_KEY); localStorage.removeItem(LEGACY_PLAY_RECORDS_KEY); window.dispatchEvent( new CustomEvent('playRecordsUpdated', { detail: {}, }) ); } /** * 清空全部收藏 * 数据库存储模式下使用乐观更新:先更新缓存,再异步同步到数据库。 */ export async function clearAllFavorites(): Promise { // 数据库存储模式:乐观更新策略(包括 redis、d1、upstash) if (STORAGE_TYPE !== 'localstorage') { // 立即更新缓存 cacheManager.cacheFavorites({}); // 触发立即更新事件 window.dispatchEvent( new CustomEvent('favoritesUpdated', { detail: {}, }) ); // 异步同步到数据库 try { const res = await fetch(`/api/favorites`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, }); if (!res.ok) throw new Error(`清空收藏失败: ${res.status}`); } catch (err) { await handleDatabaseOperationFailure('favorites', err); throw err; } return; } // localStorage 模式 if (typeof window === 'undefined') return; localStorage.removeItem(FAVORITES_KEY); localStorage.removeItem(LEGACY_FAVORITES_KEY); window.dispatchEvent( new CustomEvent('favoritesUpdated', { detail: {}, }) ); } // ---------------- 混合缓存辅助函数 ---------------- /** * 清除当前用户的所有缓存数据 * 用于用户登出时清理缓存 */ export function clearUserCache(): void { if (STORAGE_TYPE !== 'localstorage') { cacheManager.clearUserCache(); } } /** * 手动刷新所有缓存数据 * 强制从服务器重新获取数据并更新缓存 */ export async function refreshAllCache(): Promise { if (STORAGE_TYPE === 'localstorage') return; try { // 并行刷新所有数据 const [playRecords, favorites, searchHistory] = await Promise.allSettled([ fetchFromApi>(`/api/playrecords`), fetchFromApi>(`/api/favorites`), fetchFromApi(`/api/searchhistory`), ]); if (playRecords.status === 'fulfilled') { cacheManager.cachePlayRecords(playRecords.value); window.dispatchEvent( new CustomEvent('playRecordsUpdated', { detail: playRecords.value, }) ); } if (favorites.status === 'fulfilled') { cacheManager.cacheFavorites(favorites.value); window.dispatchEvent( new CustomEvent('favoritesUpdated', { detail: favorites.value, }) ); } if (searchHistory.status === 'fulfilled') { cacheManager.cacheSearchHistory(searchHistory.value); window.dispatchEvent( new CustomEvent('searchHistoryUpdated', { detail: searchHistory.value, }) ); } } catch (err) { console.error('刷新缓存失败:', err); } } /** * 获取缓存状态信息 * 用于调试和监控缓存健康状态 */ export function getCacheStatus(): { hasPlayRecords: boolean; hasFavorites: boolean; hasSearchHistory: boolean; username: string | null; } { if (STORAGE_TYPE === 'localstorage') { return { hasPlayRecords: false, hasFavorites: false, hasSearchHistory: false, username: null, }; } const authInfo = getAuthInfoFromBrowserCookie(); return { hasPlayRecords: !!cacheManager.getCachedPlayRecords(), hasFavorites: !!cacheManager.getCachedFavorites(), hasSearchHistory: !!cacheManager.getCachedSearchHistory(), username: authInfo?.username || null, }; } // ---------------- React Hook 辅助类型 ---------------- export type CacheUpdateEvent = | 'playRecordsUpdated' | 'favoritesUpdated' | 'searchHistoryUpdated'; /** * 用于 React 组件监听数据更新的事件监听器 * 使用方法: * * useEffect(() => { * const unsubscribe = subscribeToDataUpdates('playRecordsUpdated', (data) => { * setPlayRecords(data); * }); * return unsubscribe; * }, []); */ export function subscribeToDataUpdates( eventType: CacheUpdateEvent, callback: (data: T) => void ): () => void { if (typeof window === 'undefined') { return () => {}; } const handleUpdate = (event: CustomEvent) => { callback(event.detail); }; window.addEventListener(eventType, handleUpdate as EventListener); return () => { window.removeEventListener(eventType, handleUpdate as EventListener); }; } /** * 预加载所有用户数据到缓存 * 适合在应用启动时调用,提升后续访问速度 */ export async function preloadUserData(): Promise { if (STORAGE_TYPE === 'localstorage') return; // 检查是否已有有效缓存,避免重复请求 const status = getCacheStatus(); if (status.hasPlayRecords && status.hasFavorites && status.hasSearchHistory) { return; } // 后台静默预加载,不阻塞界面 refreshAllCache().catch((err) => { console.warn('预加载用户数据失败:', err); }); }