Files
FamilyBook_harmonyos/entry/src/main/ets/data/FbTokenStore.ets
T
laoyang 776c95ba0b v0.7.0 接上真实服务器:登录 / 设备会话 / 全量同步与各模块真实数据
从「演示数据壳子」走到「真的能连家里的服务器用」。

登录(用户 2026-10-17「完整的登录功能」)
  · 首次启动先填服务器地址 → /api/v1/ping 自报门户(认得出"这地址上真是家书")
  · 手机号或用户名 + 密码 → 拿令牌;失败提示统一,不泄露账号是否存在
  · FbDeviceStore:首次启动生成并持久化设备 UUID(三方应用拿不到 UDID),
    设备名取机型(marketName),并上报 platform / app_version

设备会话(v57,用户 2026-10-17)
  · 一把令牌 = 一台设备的一条会话;每次启动打一次 /me(有网就验)
  · 两种锁分开:need_login(401/被踢 → 清令牌)与 offline_expired
    (离线超 24h → 只锁界面、不清数据、联网自动恢复)——
    混成一种就会违背用户要的"一直联网就不用每天输密码"
  · 运行期任何请求 401 → 网络层全局回调立刻锁定跳登录页(FbHttp 的 authProbe
    让登录/校验类请求跳过,避免"两个地方同时跳登录页")
  · 退出登录:先清本地令牌(主防线),再尽力通知服务端
  · 设置页显示版本号与「这台设备」

数据层
  · FbDb / FbSchema / FbParse / 各 Repo(日程 / 名册 / 观影 / 模块)+ FbSyncAll
  · 首页改为服务端 /home(农历/节气/纪念日/生日/天气/统计/往年的今天)
  · 日程页用 FbDayEventPanel,与网页端显示方式/红线/并排三处一致

验证:ArkTS 静态守卫 231/0;hvigor 编译 BUILD SUCCESSFUL(entry-default-signed.hap)。
2026-10-18 03:36:04 +08:00

131 lines
5.1 KiB
Plaintext
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.
/**
* 家书 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<void> {
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<boolean> {
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<void> {
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();