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)。
This commit is contained in:
2026-10-18 03:36:04 +08:00
parent 7eaa641437
commit 776c95ba0b
64 changed files with 18297 additions and 1519 deletions
+131
View File
@@ -0,0 +1,131 @@
/**
* 家书 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();