topfans/frontend/utils/avatarCache.js

303 lines
8.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 头像文件缓存管理工具
//
// ★ 2026-07-31 补充 B加 LRU 上限 50 个文件
// 原实现每看一次头像 → uni.saveFile → _doc/uniapp_save/ 累积, 无 TTL 无上限,
// 生产设备几个月后 uniapp_save/ 累积几百个文件, sandbox-residual 扫描单类就要 1-3s,
// 是「存储空间」页面超时的主因之一。
// 现在:每次写入前 evictOldestIfNeeded() 把 LRU 索引压回 ≤ 50 个,
// 老的 storage 记录 + savedFilePath 同步删除。
//
// LRU 索引用一个独立 storage key__avatar_cache_lru_index__维护有序数组
// [{ hash: number, cachedAt: number }, ...] 按 cachedAt 升序
// 读路径 (getCachedAvatarPath) 不更新索引, 写路径 (downloadAndCacheAvatar) 才插入 + 驱逐。
// 读多写少场景(头像 UI开销可忽略。
const AVATAR_CACHE_CAP = 50
const AVATAR_LRU_INDEX_KEY = '__avatar_cache_lru_index__'
/**
* 从 avatarUrl 生成 hash 值(与 getCacheKey 共用,避免改 storage key 格式)
*/
function getCacheHash(avatarUrl) {
let hash = 0;
for (let i = 0; i < avatarUrl.length; i++) {
hash = ((hash << 5) - hash) + avatarUrl.charCodeAt(i);
hash = hash & hash;
}
return Math.abs(hash);
}
/**
* 生成缓存key
* @param {string} avatarUrl - OSS头像路径
* @returns {string}
*/
function getCacheKey(avatarUrl) {
return `avatar_file_${getCacheHash(avatarUrl)}`;
}
/**
* 读 LRU 索引(容错:解析失败返回空数组)
*/
function getLruIndex() {
try {
const raw = uni.getStorageSync(AVATAR_LRU_INDEX_KEY);
if (raw) {
const arr = JSON.parse(raw);
if (Array.isArray(arr)) return arr;
}
} catch (e) { /* swallow */ }
return [];
}
/**
* 写 LRU 索引(容错:写入失败不影响主流程)
*/
function setLruIndex(arr) {
try {
uni.setStorageSync(AVATAR_LRU_INDEX_KEY, JSON.stringify(arr));
} catch (e) { /* swallow */ }
}
/**
* 按 hash 删 LRU 索引项 + 删对应 storage key + savedFilePath
* @param {number} hash
*/
function evictByHash(hash) {
try {
const storageKey = `avatar_file_${hash}`;
const cached = uni.getStorageSync(storageKey);
if (cached) {
try {
const data = JSON.parse(cached);
if (data?.localPath) {
uni.removeSavedFile({ filePath: data.localPath, fail: () => {} });
}
} catch (e) { /* skip */ }
uni.removeStorageSync(storageKey);
}
} catch (e) { /* swallow */ }
}
/**
* ★ 2026-07-31 LRU 驱逐:每次写入前调, 保持索引 ≤ AVATAR_CACHE_CAP
* 超出部分按 cachedAt 升序逐个删(最老的先删)
* - 删 storage key (avatar_file_<hash>)
* - 删 savedFilePath 文件
* - 删 LRU 索引项
*/
function evictOldestIfNeeded() {
const index = getLruIndex();
while (index.length >= AVATAR_CACHE_CAP) {
const oldest = index.shift();
if (oldest?.hash != null) evictByHash(oldest.hash);
}
setLruIndex(index);
}
/**
* 从本地缓存获取头像文件路径
* @param {string} avatarUrl - OSS头像路径
* @returns {Promise<string|null>} 缓存的本地文件路径如果没有则返回null
*/
export async function getCachedAvatarPath(avatarUrl) {
if (!avatarUrl) return null;
try {
const cacheKey = getCacheKey(avatarUrl);
const cached = uni.getStorageSync(cacheKey);
if (cached) {
const cacheData = JSON.parse(cached);
// 检查缓存的avatar_url是否匹配防止hash碰撞
if (cacheData.avatarUrl === avatarUrl && cacheData.localPath) {
// 验证文件是否还存在
return new Promise((resolve) => {
uni.getFileInfo({
filePath: cacheData.localPath,
success: () => {
console.log('使用缓存的头像文件:', cacheData.localPath);
resolve(cacheData.localPath);
},
fail: () => {
// 文件不存在,清除缓存记录
console.log('缓存文件不存在,清除记录');
uni.removeStorageSync(cacheKey);
resolve(null);
}
});
});
}
}
} catch (error) {
console.error('获取缓存头像路径失败:', error);
}
return null;
}
/**
* 下载并缓存头像文件到本地
* @param {string} avatarUrl - OSS头像路径
* @param {string} realUrl - 真实的预签名URL
* @returns {Promise<string|null>} 本地文件路径失败返回null
*/
export async function downloadAndCacheAvatar(avatarUrl, realUrl) {
if (!avatarUrl || !realUrl) return null;
try {
console.log('开始下载头像文件...');
// 1. 下载文件到临时目录
const downloadResult = await new Promise((resolve, reject) => {
uni.downloadFile({
url: realUrl,
success: (res) => {
if (res.statusCode === 200) {
resolve(res.tempFilePath);
} else {
reject(new Error(`下载失败,状态码: ${res.statusCode}`));
}
},
fail: (err) => {
reject(err);
}
});
});
// 2. 将临时文件保存为永久文件
const savedPath = await new Promise((resolve, reject) => {
uni.saveFile({
tempFilePath: downloadResult,
success: (res) => {
resolve(res.savedFilePath);
},
fail: (err) => {
reject(err);
}
});
});
// 3. 保存文件路径到缓存
const cacheKey = getCacheKey(avatarUrl);
const cacheData = {
avatarUrl: avatarUrl,
localPath: savedPath,
cachedAt: Date.now()
};
uni.setStorageSync(cacheKey, JSON.stringify(cacheData));
// ★ 2026-07-31 补充 BLRU 索引 + 上限驱逐
// 1) 先把 LRU 索引压回 ≤ AVATAR_CACHE_CAP (50)
// 超出部分按 cachedAt 升序驱逐(删 storage key + savedFilePath + 索引项)
// 2) 再插入新条目到索引尾
// 容错:驱逐失败不阻塞主流程(最多导致 uniapp_save/ 仍超 50, 不会更糟)
try {
evictOldestIfNeeded()
const index = getLruIndex()
const hash = getCacheHash(avatarUrl)
// 同 hash 已存在则先移除(避免重复)
const filtered = index.filter((e) => e?.hash !== hash)
filtered.push({ hash, cachedAt: cacheData.cachedAt })
setLruIndex(filtered)
} catch (e) {
console.warn('[avatarCache] LRU index update failed:', e?.message)
}
console.log('头像文件已缓存到:', savedPath);
return savedPath;
} catch (error) {
console.error('下载并缓存头像失败:', error);
return null;
}
}
/**
* 清除指定头像的缓存文件
* @param {string} avatarUrl - OSS头像路径
*/
export function clearAvatarCache(avatarUrl) {
if (!avatarUrl) return;
try {
const cacheKey = getCacheKey(avatarUrl);
const cached = uni.getStorageSync(cacheKey);
if (cached) {
const cacheData = JSON.parse(cached);
// 删除本地文件
if (cacheData.localPath) {
uni.removeSavedFile({
filePath: cacheData.localPath,
success: () => {
console.log('已删除缓存的头像文件:', cacheData.localPath);
},
fail: (err) => {
console.error('删除缓存文件失败:', err);
}
});
}
// 删除缓存记录
uni.removeStorageSync(cacheKey);
// ★ 2026-07-31同步清理 LRU 索引
try {
const hash = getCacheHash(avatarUrl)
const index = getLruIndex()
setLruIndex(index.filter((e) => e?.hash !== hash))
} catch (e) { /* swallow */ }
console.log('已清除头像缓存记录');
}
} catch (error) {
console.error('清除头像缓存失败:', error);
}
}
/**
* 清除所有头像缓存文件
*/
export function clearAllAvatarCache() {
try {
const storageInfo = uni.getStorageInfoSync();
const keys = storageInfo.keys || [];
// 找到所有头像缓存的key并删除
keys.forEach(key => {
if (key.startsWith('avatar_file_')) {
try {
const cached = uni.getStorageSync(key);
if (cached) {
const cacheData = JSON.parse(cached);
// 删除本地文件
if (cacheData.localPath) {
uni.removeSavedFile({
filePath: cacheData.localPath,
success: () => {
console.log('已删除缓存文件:', cacheData.localPath);
},
fail: () => {
// 忽略错误,可能文件已经不存在
}
});
}
}
// 删除缓存记录
uni.removeStorageSync(key);
} catch (e) {
console.error('清除单个缓存失败:', e);
}
}
});
// ★ 2026-07-31清空 LRU 索引
setLruIndex([])
console.log('已清除所有头像缓存');
} catch (error) {
console.error('清除所有头像缓存失败:', error);
}
}