// frontend/utils/preloadApi/storage.js // 缓存适配器 — 用 uni.setStorage 系(SQLite 后端,不受 Android 10+ 分区存储影响) // // Android 10+ 适配说明(2026-07-13): // 旧实现用 plus.io.requestFileSystem(PRIVATE_DOC=2, ...) + getDirectory + getFile + // createWriter 写文件缓存,在某些 HBuilderX 版本 + targetSdkVersion >= 29 下, // DCloud 内部仍把 _doc/ PRIVATE_DOC 映射到 PUBLIC_DOCUMENTS 路径(外部存储), // 分区存储机制下报 "targetSdkVersion设置>=29后在Android10+系统设备不支持当前路径"。 // 修正:完全绕开 plus.io,改用 uni.setStorage(底层 plus.storage 是 SQLite, // 走应用数据目录,与 Android 分区存储无关,无大小限制)。 // ── djb2 hash(与 core.js 共用逻辑,此处独立一份避免循环依赖)── function hashStr(str) { let hash = 5381 for (let i = 0; i < str.length; i++) { hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0 } return (hash >>> 0).toString(16) } // ── key 工具 ── // 统一前缀,方便 clearForUser 时按前缀删 const KEY_PREFIX = 'preload' function makeKey(userId, cacheKey) { return `${KEY_PREFIX}:${userId || 'guest'}:${hashStr(cacheKey)}` } function makeUserPrefix(userId) { return `${KEY_PREFIX}:${userId || 'guest'}:` } function makeGlobalPrefix() { return `${KEY_PREFIX}:` } // ── 公共 API ── /** * 读缓存条目 * @returns {Promise<{data, ts, ttl}|null>} null = 未命中 */ export async function readEntry(userId, cacheKey) { try { const value = uni.getStorageSync(makeKey(userId, cacheKey)) if (!value) return null // value 可能是字符串也可能是对象,统一处理 const obj = typeof value === 'string' ? JSON.parse(value) : value if (!obj || typeof obj.ts !== 'number') return null return obj } catch (e) { return null } } /** * 写缓存条目(fire-and-forget,内部捕获异常) * 与旧实现兼容:签名不变,但底层从 plus.io 改为 uni.setStorage */ export function writeEntry(userId, cacheKey, data, ts, ttl) { const key = makeKey(userId, cacheKey) const payload = { data, ts, ttl } try { uni.setStorageSync(key, payload) } catch (e) { console.warn('[preload] storage write failed:', key, e.message || e) } } /** * 删除指定用户的全部缓存条目 * 注意:uni 没有"按前缀删"接口,需要 getStorageInfo 拿所有 key 后过滤 */ export async function clearForUser(userId) { try { const info = uni.getStorageInfoSync() const prefix = makeUserPrefix(userId) const toRemove = (info.keys || []).filter((k) => k.startsWith(prefix)) toRemove.forEach((k) => { try { uni.removeStorageSync(k) } catch (e) { /* skip */ } }) } catch (e) { console.warn('[preload] clearForUser failed:', userId, e.message || e) } } /** * 获取用户缓存总大小(字节) * uni.getStorageInfoSync().currentSize 单位是 KB,需乘 1024 */ export async function getTotalCacheSize(userId) { try { const info = uni.getStorageInfoSync() const prefix = makeUserPrefix(userId) const keys = (info.keys || []).filter((k) => k.startsWith(prefix)) if (keys.length === 0) return 0 // info.currentSize 是全局所有 storage 的 KB 数,无法按 user 拆。 // 退而求其次:遍历用户的每个 key,getStorageSync 估算字节大小 let totalBytes = 0 for (const k of keys) { try { const v = uni.getStorageSync(k) if (v != null) { // 粗估:JSON 序列化字节数 totalBytes += JSON.stringify(v).length } } catch (e) { /* skip */ } } return totalBytes } catch (e) { return 0 } } /** * FIFO 淘汰最旧条目,直到总大小 < maxSize 字节 * uni 没有"按 mtime 排序"接口,改用 ts 字段(我们在写入时设置的) */ export async function evictOldest(userId, maxSize) { try { const prefix = makeUserPrefix(userId) const info = uni.getStorageInfoSync() const keys = (info.keys || []).filter((k) => k.startsWith(prefix)) if (keys.length === 0) return // 收集每个 entry 的 ts const entries = [] for (const k of keys) { try { const v = uni.getStorageSync(k) const obj = typeof v === 'string' ? JSON.parse(v) : v if (obj && typeof obj.ts === 'number') { entries.push({ key: k, ts: obj.ts }) } } catch (e) { /* skip corrupt entry */ } } // 按 ts 升序(最旧的在前) entries.sort((a, b) => a.ts - b.ts) // 累加当前估算大小,超出 maxSize * 0.8 就开始删 let totalSize = 0 for (const e of entries) { try { const v = uni.getStorageSync(e.key) if (v != null) totalSize += JSON.stringify(v).length } catch (err) { /* skip */ } } for (const e of entries) { if (totalSize <= maxSize * 0.8) break try { const v = uni.getStorageSync(e.key) if (v != null) totalSize -= JSON.stringify(v).length uni.removeStorageSync(e.key) } catch (err) { /* skip */ } } } catch (e) { console.warn('[preload] evictOldest failed:', userId, e.message || e) } }