Commit Graph
3 Commits
Author SHA1 Message Date
laoyang 77b494a274 修「名册永远是空的」:person_roster.phones 只在迁移里加过、没进建表语句
用户 2026-10-18:「名册现在没有任何数据啊,看看咋回事。」

真机日志(唯一直接证据):
  E .../Config: table person_roster has no column named phones
  (SQLite errno 2,名册整批 batchInsert 全部失败)

根因(两条安装路径的行为分叉):
  `person_roster.phones` 当初**只加在 `FbDb.migrate()` 的 v3→v4 ALTER 里**,
  **没进 `FbSchema.FB_SCHEMA_SQL` 的建表语句**。于是:
    · 老库升级 → 走 migrate() → 执行 ALTER → 有 phones ✅
    · 全新安装 → store.version 一上来就等于 FB_DB_VERSION(5)
                 ⇒ migrate() 首行 `from >= FB_DB_VERSION` 直接 return
                 ⇒ 那句 ALTER **永远不执行** → 没有 phones ❌
  用户这次是**卸载重装**(前端流程要求),正好走的是坏的那条路。

为什么藏了这么久(两个"静默"叠加):
  ① `pageHeaderItem` 的空状态文案是「登录后会从服务器拉取你名册里的人」——
     一个**已经登录**的人被提示去登录,完全猜不到真实原因;
  ② `syncInBackground()` 里 `repo.sync()` 的**异常没人接**:
     异常从 async 函数冒出去,`bootstrap()` 之后没有任何 catch
     ⇒ 整个后台同步静默死掉,`syncNote` 还是 ''(看着像"同步成功")。

三处修复:
  ① `FB_SCHEMA_SQL` 的 person_roster 补上 `phones`(新装用户直接对);
  ② `FB_DB_VERSION` 5→6 + 新增"结构自愈"迁移:**主动探测列是否存在**
     (`PRAGMA table_info`,不靠异常控制流 —— 否则会把库损坏之类的真原因
      一起吞成"大概已经加过了"),缺则补 `ALTER`。这一条是给**已经装坏的库**
     (version=5 但缺列)用的:它们的版本号已经到位,不会再进任何 `from < N`
     分支,只能靠新版本号再捞一次。
  ③ `syncInBackground()` 包 try/catch,把异常落成**用户看得见的文案**
     (「名册写入本地库失败:<原因>」)。纪律:后台任务的异常必须就地
     变成一句话,不能指望"调用方会 catch"——调用方往往是 fire-and-forget。

真机验证(两条路径都跑了):
  · 旧库自愈:`本地库迁移:v5 → v6` → `v6 迁移完成:补上 person_roster.phones`
    → `[5] 本地库就绪,版本=6`;随后名册页**数据全出来了**
    (标签云 全部/大学同学 55/同事 40/实验室 23/合作伙伴/爸爸/家人;
      人员 爸爸/妈妈/岳父/岳母/杨永全 等),缺列报错 0 条。
  · 全新安装(清数据重来):`version=0` → 首装建表 22 条 → 直接 v6,
    迁移无事可做(正确:建表已含全部列)。

新增守卫(防这类"新装 vs 升级分叉"复发):
  `_fb_guard_schema_columns.js`(11 条)静态解析 `FB_SCHEMA_SQL` 的建表列
  与 `migrate()` 的全部 `ALTER ... ADD COLUMN`,断言**每个 ALTER 加过的列
  都必须在建表语句里**;另钉住 v6 存在性探测、版本号、后台同步 try/catch。
  `_fb_schema_falsify.py` 6 项注入 ⇒ 6/0(每项都成功打红对应断言)。
  已挂进流水线(run_guarded_suite 全量 b59b)。

顺带修 `_fb_v57_falsify.py` 的一处**过时注入锚点**:它注入的是
`allowSkip: this.app.lockReason === ''`,而该参数已按用户要求整个删除
⇒ 改成反方向注入「把 onSkip 后门加回来必须被抓」(12/0 通过)。

验证:hvigor BUILD SUCCESSFUL;_fb_arkts_guard.js 328/0;b57 12/0;新守卫 11/0。
2026-10-18 20:40:30 +08:00
laoyang 2132ee42d2 v0.8.0 本地加密 / 被踢清数据 / 增量同步 / 附件队列 / 顶栏同步进度
用户 2026-10-18 两条:
「直接做 L3 锁界面 + 本地加密(HUKS,踢设备即吊销密钥):数据在但读不出,
  重登后恢复密钥即可用。」
「右上角头像和提醒前面加同步进度…首次全量、之后增量;进来先展示本地数据,
  一边同步一边更新;附件后台队列同步(名册头像 / 电影海报 / 观影照片 / 资源附件)。」

① 整库加密(FbSchema v4→v5,新增 module_sync / attach_cache 两张表)
   RDB encrypt=true。加密属性一经创建不可修改 ⇒ openWithEncryption 三段式:
   直接开 → 失败删库重建 → 再失败降级明文(degraded 字段,设置页如实显示,
   不静默降级)。

② 两层密钥(FbKeyStore)
   HUKS 里 KEK(fb_kek_v1,不可导出)包裹 32 字节 DEK;revoke() 删 KEK
   ⇒ 已落盘附件密文在数学上解不开,这就是"踢设备即吊销密钥"。
   重新登录重新生成 DEK 并重下附件(不做服务端托管,为了省一次重下
   把能解你本地数据的密钥放到服务端不值)。
   会话属性里 PURPOSE 只出现一次:原来把含 ENCRYPT|DECRYPT 的生成属性直接
   拿去开会话再 push 一个 PURPOSE,同一 tag 出现两次在 HUKS 里行为未定义
   ⇒ 拆成 kekBaseProps / kekGenProps / kekSessionProps。

③ AES-256-GCM(FbCrypto)
   落盘 iv(12)|ct|tag(16);变换串必须带 padding 段(AES256|GCM|PKCS7,
   缺了抛 801);解密从尾部切 tag。

④ 附件后台队列(FbAttachQueue)
   attach_cache 台账(pending/done/fail,MAX_TRIES=3);密文放 filesDir、
   明文临时放 cacheDir;扩展名白名单筛 key。
   图片地址收成统一入口 fbImageSrc(key,token):本地有就用 file://,
   没有才回网络 —— 收成一个函数才漏不掉(5 个页面全部改过来)。

⑤ 被踢 / 退出清数据
   lockOut → 清令牌 → FB_ATTACH.wipe(第一步就是吊销 KEK)→ FbDb.wipe
   (先关句柄再 deleteRdbStore,并发去重,open 会等它)→ 同步状态复位。
   重新登录后重建密钥 + 库 + 附件索引,否则之后只读网络不写缓存且不报错。

⑥ 增量同步(FbSyncAll 重写)
   先问 /api/v1/changes;指纹相同就跳过。本地没水位必须拉(否则老用户升级后
   一个模块都不同步);24h 陈旧上限兜"改无 updated_at 表的已有行";
   /changes 拿不到时判 ping status>0 —— 断网直接收工,不退回全量
   (否则 12 个模块各等 15 秒)。
   家谱也进了预同步列表(原来只在打开时拉,离线是一片空白);
   总步数由 CACHE_MODULES.length 算出,不写死。

⑦ 删除传播
   服务端是物理删除、没有墓碑 ⇒ 观影同步改成"先清后插"
   (persons 跨模块共用,不清)。

⑧ 顶栏同步进度
   头像与铃铛之间加三态按钮(转圈 / 红感叹号 / 灰对勾)+ bindSheet 面板:
   进度条、「N 个模块没有变化」、附件行、失败清单、立即同步(防连点)。
   状态中心 FbSyncState(@ObservedV2 + @Trace,AppStorageV2 单例);
   界面直接读 @Trace 字段,不提供派生 getter。

验证:hvigor BUILD SUCCESSFUL;_fb_arkts_guard.js 290/0(本批新增 59 条);
_fb_v58_falsify.py 31 项注入 38/0;b57 反向验证 12/0(锚点跟上本批 logout 改动);
本地 e2e 1519/0;全量流水线 PUSH_RC=0。
2026-10-18 16:01:41 +08:00
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