/** * 家书 FamilyBook · API 令牌存储(鸿蒙端) * * 只存一件事:移动端 API 令牌(`api_tokens` 表签发的那一串)。 * * 🔴 为什么用 `preferences` 而不是本地库 RDB: * 这是**单个键值**(一把令牌),不是"多行记录"。 * KV 存储天生适合,也不必为它建表、迁移。 * 令牌**不参与同步**(服务端 v33 的 53 张业务表里也不含 api_tokens)—— * 它本来就是"这台设备与服务器之间的凭据",各设备各自一份。 * * ⚠️ 令牌是敏感凭证: * · 放 `preferences` 会落在应用沙箱内(其它应用读不到),这对家庭应用够用; * · 若将来要更严(防 root 提取),应迁到 `@ohos.security.asset`(系统密钥库)。 * 现在不做的原因:Asset Store 的 API 面更复杂,而本应用的数据敏感度 * (家庭日程/观影)还不至于需要它。**这是个有意识的取舍,不是遗漏。** * · ⚠️ 无论如何**不要**把令牌写进日志或错误信息里往外抛。 */ import { preferences } from '@kit.ArkData'; import { hilog } from '@kit.PerformanceAnalysisKit'; import { common } from '@kit.AbilityKit'; const DOMAIN: number = 0x0000; const TAG: string = 'FbToken'; /** preferences 实例名(沙箱内的文件名) */ const STORE_NAME: string = 'fb_auth'; /** 令牌的键名 */ const KEY_TOKEN: string = 'api_token'; /** * 令牌存储(同步读、异步写的门面)。 * * 🔴 为什么提供一个**同步的 `get()`**: * 页面渲染时经常需要立刻知道"有没有令牌"(决定要不要发起同步)。 * 若只能异步读,每个调用点都要 await,把简单逻辑搞得支离破碎。 * ⇒ 用"启动时预加载进内存 + 写时同时更新内存"的方式提供同步读。 * ⚠️ 前提是**启动时必须先 `init()`**,否则内存里是空的(会误判成"未登录")。 */ class FbTokenStoreImpl { private cached: string = ''; private store: preferences.Preferences | null = null; private ready: boolean = false; /** * 预加载令牌进内存。应在应用启动时调用一次(见 EntryAbility)。 * ⚠️ 幂等:重复调用直接返回,不会重复读盘。 */ async init(ctx: common.Context): Promise { if (this.ready) { return; } try { this.store = await preferences.getPreferences(ctx, STORE_NAME); const v: preferences.ValueType = await this.store.get(KEY_TOKEN, ''); this.cached = typeof v === 'string' ? v : ''; this.ready = true; hilog.info(DOMAIN, TAG, '令牌预加载完成,%{public}s', this.cached === '' ? '未设置' : '已设置'); } catch (err) { // ⚠️ 读不到就当"没有令牌",**不要**让启动流程挂掉 —— // 用户仍能用本地数据(本地优先的好处)。 hilog.warn(DOMAIN, TAG, '令牌预加载失败:%{public}s', JSON.stringify(err)); this.ready = true; } } /** 同步取令牌(未设置返回空串)。⚠️ 需先 init(),否则会误判为未登录 */ get(): string { if (!this.ready) { hilog.warn(DOMAIN, TAG, '令牌还没预加载完,本次读到的可能是空值'); } return this.cached; } /** 是否已设置令牌(同步) */ has(): boolean { return this.cached !== ''; } /** * 预加载是否已完成(同步)。 * * 🔴 为什么需要它:`get()` 在**没预加载完**时会返回空串(看起来像"未登录")。 * 启动流程里若据此判断,就会出现"明明有令牌却显示登录页"。 * ⇒ 调用方应先用 `isReady()` 确认预加载完成,再做登录态判断。 * (见 Index.bootCheck 里的等待循环。) */ isReady(): boolean { return this.ready; } /** 保存令牌(同时更新内存缓存与磁盘) */ async set(ctx: common.Context, token: string): Promise { try { if (this.store === null) { this.store = await preferences.getPreferences(ctx, STORE_NAME); } await this.store.put(KEY_TOKEN, token); // ⚠️ `put` 只写内存,**必须 `flush`** 才落盘 —— // 不 flush 的话进程被杀就丢了(表现为"登录后又变未登录")。 await this.store.flush(); this.cached = token; this.ready = true; hilog.info(DOMAIN, TAG, '令牌已保存'); return true; } catch (err) { hilog.error(DOMAIN, TAG, '令牌保存失败:%{public}s', JSON.stringify(err)); return false; } } /** 清除令牌(登出) */ async clear(ctx: common.Context): Promise { try { if (this.store === null) { this.store = await preferences.getPreferences(ctx, STORE_NAME); } await this.store.delete(KEY_TOKEN); await this.store.flush(); this.cached = ''; hilog.info(DOMAIN, TAG, '令牌已清除'); } catch (err) { hilog.error(DOMAIN, TAG, '令牌清除失败:%{public}s', JSON.stringify(err)); } } } /** 全局单例 */ export const FB_TOKEN_STORE: FbTokenStoreImpl = new FbTokenStoreImpl();