Files
FamilyBook_harmonyos/entry/src/main/ets/data/FbSchema.ets
T
laoyang 0397270e8b 日程改为本地库按需查询,修掉每次进页面解析 9.3MB 的架构错误
## 用户指出的问题

「这个数据量很大,不是应该每次同步完了之后解包放在一个数据库里吗?
 然后点开日程,不就是把今天的日程从数据库里查出来吗?
 这个应该速度非常快才对呀!怎么能每次打开都要去读所有的数据呢?」

用户说得完全对。这是**架构错误**,上一轮我只做了算法层的局部优化
(把 O(n²) 改成 O(n)),没动"数据怎么存、怎么取"这个根本问题。

## 改前 vs 改后

改前:
    load() → module_cache 表读 **9.3MB 字符串**
           → JSON.parse **28628 个对象**
           → 全塞进 this.allEvents 内存数组
    渲染   → 在这 28628 条上遍历

`module_cache` 的设计注释本来写着「解析一遍毫秒级,可接受」——
那是按小数据量假设写的,日程涨到 2.8 万条后这个假设就破了。

改后:
    load() → ① `WHERE date BETWEEN ? AND ?`(走 idx_cal_date)→ 立刻渲染
             ② 后台拉全量 → 批量入库 → 重查当前范围
    渲染   → 只在**当前范围**那几十到几百条上遍历

## 改动

### FbSchema / FbDb:新增 cal_events 实体表

字段对齐 FbCalEvent(12 个)+ uid + fetched_at,三个索引:
- `idx_cal_date` —— 按日期查(最高频)
- `idx_cal_date_kind` —— 农历/节日按 kind 过滤
- `idx_cal_uid` —— 同步去重

⚠️ 主键不用服务端 `ref_id`:衍生条目(农历/节气/纪念日)的 ref_id **恒为 0**,
几万条全撞主键。用自增 rowid + (cal_id, uid) 唯一索引。
uid 由 kind|calId|evId|date|time|title哈希 组成。

⚠️ **建表语句与 migrate 两处都改了**,列名逐一比对一致
(这个坑本项目已踩 5 次,加了静态守卫钉住)。

### FbCalStore(新建):本地库读写

- `replaceAll(rows, stamp)` —— 事务内先 DELETE 再分批写(20 行/批,照
  FbMovieRepo 的实测结论,200 行会卡死)。整批替换而非逐条 upsert,
  否则服务端已删的条目会永远留在本地。
- `between(from,to)` / `ofDay(iso)` —— 页面真正用的查询
- `lunarBetween` —— 月视图 42 个格子只要农历文字,单独查比取全量再过滤快
- `count()` / `fetchedAt()` —— 判空与"数据新不新"

⚠️ 命名用 `FbCalStore` 而不是 `FbCalRepo`:后者已存在,是**接口读写**层
(create/update/remove/fetchBooks)。我第一版误覆盖了它,编译报 8 处错误后
从 git 恢复,改名为 Store 区分「本地库」和「接口」。

### FbCalendarPage

- `allEvents` → **`rangeEvents`**(改名强制提醒:这里不再是全量)
- `rangeOfView()` —— 算当前视图需要的日期区间
- `loadFromLocal()` —— 先查本地,有数据立刻渲染
- `persistAndRefresh()` —— 入库完成后重查,界面跟上最新数据
- `reloadRange()` + `@Monitor('showYear','showMonth','mode')` —— 翻月/切视图按需重查
- `rangeSeq` —— 防抖,避免旧结果覆盖新结果

## 验证

### 编译
`build_hap debug` → BUILD SUCCESSFUL

### 静态校验(34 项全过)
`_tooling/check_cal_db.js`,含最关键的两项:
- 建表与迁移**列名逐一相同**(15 列全对上)
- `load()` 里本地查询确实在远程同步**之前**

### 未验证
**真机实测未做**(设备未连接)。需要确认:
- 老库升级(v7→v8)是否顺利建表
- 首次进入(库空)要等全量拉取,之后进入应该秒开
- 翻月是否立刻有数据

## 过程中的一个失误

第一版把新仓储命名为 `FbCalRepo`,**覆盖了已存在的同名文件**。
编译立刻报 8 处 "has no exported member / does not exist on type"。
从 git 恢复原文件后改名 `FbCalStore`,避免了两层职责混在一个类里。

教训:新建文件前先查同名是否存在(`git status` 没报是因为该文件本就在版控里,
我 `cp` 上去把它改成"已修改"状态,看着像正常改动)。
2026-11-03 09:59:40 +08:00

424 lines
21 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 · 本地数据库结构(鸿蒙端)
*
* 设计立场:**本地优先(local-first)**。
* · 本地库是 UI 的**唯一数据源**,界面永远不直接读网络;
* · 网络只在后台把服务端数据"补进来"、把本地改动"推出去";
* · 断网时所有功能照常可用(因为数据本来就在本地)。
*
* 🔴 建表原则:**按实体建表,不按模块建表**。
* 「观影」看起来是一个模块,但服务端它是 6 张表组合出的视图:
* events(kind='movie') + movie_details + cinemas + movie_reviews
* + event_attendees + event_photos
* ⇒ 本地也照**实体**存:`events` 一张、`movie_details` 一张……
* 观影页要的数据用 VIEW(或 JOIN 查询)拼出来。
* ⚠️ 若按模块建表(一张 movies 表塞所有字段),会出现:
* 同一条日程在「日程模块」和「观影模块」各存一份、互相打架。
* 这是本地优先最容易踩的坑。
*
* 🔴 同步字段(每张业务表都有,与服务端 v33 结构对齐):
* · `updated_at` —— 服务端最后修改时间(**由服务端 MySQL 自动维护**);
* · `deleted_at` —— 软删除墓碑(NULL = 未删);
* · `synced_at` —— **本地专有**:上次把它同步下来的时间,
* 用来判断"这条是否已被本地改动覆盖"。
* · `dirty` —— **本地专有**:1 = 有未推送的本地改动。
* ⚠️ 后两个字段是本地专有的,**不属于服务端结构**,回推时不发送。
*/
import { relationalStore } from '@kit.ArkData';
/** 本地库文件名(放在应用沙箱的 databases 目录下) */
export const FB_DB_NAME: string = 'familybook.db';
/**
* 本地库版本号。
*
* ⚠️ 每次改表结构都要 +1,并在 `migrate()` 里补上对应的 ALTER。
* 本地库**不能像服务端那样"每次启动都全量 ALTER 一遍"** ——
* 那是拿用户的手机当试验田。必须按版本逐步迁移。
*/
export const FB_DB_VERSION: number = 8;
/**
* 建表语句。
*
* ⚠️ 只对**首装**(onCreate)执行。老库升级走 `migrate()`。
*/
export const FB_SCHEMA_SQL: string[] = [
// ---------- 观影(4 张实体表)----------
`CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY,
family_id INTEGER NOT NULL,
creator_id INTEGER,
kind TEXT NOT NULL,
title TEXT NOT NULL,
starts_at TEXT,
ends_at TEXT,
location TEXT,
note TEXT,
source_ref TEXT,
created_at TEXT,
updated_at TEXT NOT NULL DEFAULT '',
deleted_at TEXT,
-- ⚠️ 下面两列是**派生/聚合值**(服务端 Movie::attachStats 算出来的),
-- 不是 events 表的原生列。放这里是为了"观影列表一次查询出结果",
-- 免得每渲染一屏都去 GROUP BY movie_reviews。
-- ⚠️ 它们**会被同步覆盖**,所以本地改动不能写这两列。
review_count INTEGER NOT NULL DEFAULT 0,
review_avg REAL NOT NULL DEFAULT 0,
synced_at INTEGER NOT NULL DEFAULT 0,
dirty INTEGER NOT NULL DEFAULT 0
)`,
// 增量拉取的主力索引(与服务端 idx_events_sync 同语义)
`CREATE INDEX IF NOT EXISTS idx_events_sync ON events (family_id, updated_at)`,
`CREATE TABLE IF NOT EXISTS movie_details (
event_id INTEGER PRIMARY KEY,
info TEXT,
poster_key TEXT,
ticket_key TEXT,
cinema_id INTEGER,
douban_meta TEXT,
updated_at TEXT NOT NULL DEFAULT '',
deleted_at TEXT,
synced_at INTEGER NOT NULL DEFAULT 0,
dirty INTEGER NOT NULL DEFAULT 0
)`,
`CREATE TABLE IF NOT EXISTS cinemas (
id INTEGER PRIMARY KEY,
family_id INTEGER NOT NULL,
place TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL DEFAULT '',
address TEXT,
color TEXT NOT NULL DEFAULT '',
created_at TEXT,
updated_at TEXT NOT NULL DEFAULT '',
deleted_at TEXT,
synced_at INTEGER NOT NULL DEFAULT 0,
dirty INTEGER NOT NULL DEFAULT 0
)`,
`CREATE INDEX IF NOT EXISTS idx_cinemas_sync ON cinemas (family_id, updated_at)`,
`CREATE TABLE IF NOT EXISTS movie_reviews (
id INTEGER PRIMARY KEY,
family_id INTEGER NOT NULL,
event_id INTEGER NOT NULL,
person_id INTEGER NOT NULL,
rating INTEGER NOT NULL DEFAULT 0,
content TEXT NOT NULL DEFAULT '',
feelings TEXT,
created_at TEXT,
updated_at TEXT NOT NULL DEFAULT '',
deleted_at TEXT,
synced_at INTEGER NOT NULL DEFAULT 0,
dirty INTEGER NOT NULL DEFAULT 0
)`,
`CREATE INDEX IF NOT EXISTS idx_movie_reviews_event ON movie_reviews (event_id)`,
// ---------- 共用实体(观影页也要用)----------
`CREATE TABLE IF NOT EXISTS persons (
id INTEGER PRIMARY KEY,
family_id INTEGER NOT NULL,
kind TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL DEFAULT '',
nickname TEXT,
gender TEXT,
birthday TEXT,
avatar TEXT,
note TEXT,
-- ⚠️ v2 新增(名册要用):这些在服务端 persons 表里本来就有,
-- 只是之前只为观影的"同看人"建了最小字段集。
-- 名册页要显示单位/职务/口号/邮箱/地址/头像色,所以补齐。
-- 🔴 注意:本段是 SQL 注释、而整条建表语句是 JS 模板字符串,
-- 注释里**绝不能出现反引号** —— 一个反引号就会把模板串提前闭合,
-- 后续全被当成代码(报 "Cannot find name 'persons'")。
-- 这个坑本项目已踩第 4 次,写 SQL 注释时手别抖。
avatar_color TEXT,
email TEXT,
org TEXT,
title TEXT,
slogan TEXT,
address TEXT,
addr_city TEXT,
-- 🔴 v7(v59e)新增:拼音首字母(服务端 Pinyin::initials 算好下发)。
-- 名册搜索「输入 yy 搜杨永全」全靠它;原来端上把它写死成空串 ⇒ 拼音搜索全废。
-- ⚠️ 加列时**两处都要改**:这里(全新安装)+ FbDb.migrate() 的 v6→v7
-- (老库升级)。只改一处就是"新装能搜、老库搜不到"或者反过来。
-- ⚠️ 本段是 SQL 注释、而整条建表语句是 JS 模板字符串 —— 注释里**绝不能
-- 出现反引号**(一个反引号就会把模板串提前闭合,报一堆
-- "Cannot find name Pinyin/FbDb" 这种看着毫不相干的错)。已踩第 5 次。
initials TEXT,
created_at TEXT,
updated_at TEXT NOT NULL DEFAULT '',
deleted_at TEXT,
synced_at INTEGER NOT NULL DEFAULT 0,
dirty INTEGER NOT NULL DEFAULT 0
)`,
`CREATE INDEX IF NOT EXISTS idx_persons_sync ON persons (family_id, updated_at)`,
`CREATE TABLE IF NOT EXISTS event_attendees (
-- ⚠️ 用**复合主键**而不是自增 id:
-- 接口返回的 attendees[].id 实测是 persons.id(服务端 SQL 是
-- "SELECT a.event_id, p.id, ..."),**不是**这张关联表自己的行 id
-- (服务端压根没返回那个)。
-- 我们既然不掌握行 id,就用「事件 + 人」这个业务唯一键做主键 ——
-- 一个事件里同一个人本来就只会出现一次,语义正确且幂等。
--
-- 🔴 注意:本行注释里**不能出现反引号** —— 整个建表语句是 JS 模板字符串,
-- 注释里一个反引号就会把它**提前闭合**,后面全变成代码、编译直接报
-- "Cannot find name 'SELECT'"(这个坑刚踩过)。
event_id INTEGER NOT NULL,
person_id INTEGER NOT NULL,
position INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL DEFAULT '',
deleted_at TEXT,
synced_at INTEGER NOT NULL DEFAULT 0,
dirty INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (event_id, person_id)
)`,
`CREATE INDEX IF NOT EXISTS idx_attendees_event ON event_attendees (event_id)`,
// ---------- 同步元数据(本地专有,不对应任何服务端表)----------
/**
* 每张表各存一条"上次同步到哪"的水位线。
*
* 🔴 为什么**按表**而不是全局一个水位:
* 各表是分别拉取的。若共用一个全局水位,A 表拉成功了、B 表失败了,
* 水位推进之后 B 表那段数据**永远不会再被拉下来**(静默丢数据)。
*/
`CREATE TABLE IF NOT EXISTS sync_state (
table_name TEXT PRIMARY KEY,
last_sync_at TEXT NOT NULL DEFAULT '',
last_ok_at INTEGER NOT NULL DEFAULT 0
)`,
/**
* 待推送队列(本地改动)。
*
* 🔴 为什么要有队列、而不是"改完立刻发请求":
* ① 断网时改动不能丢 —— 排队等着,有网再发;
* ② 用户连点几下,不该发几个请求 —— 同一行合并成一条;
* ③ 请求失败要能重试 —— 记录次数,避免无限重试打爆。
*/
`CREATE TABLE IF NOT EXISTS pending_ops (
id INTEGER PRIMARY KEY,
table_name TEXT NOT NULL,
row_id INTEGER NOT NULL,
op TEXT NOT NULL,
payload TEXT,
attempts INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL DEFAULT 0
)`,
// 同一行只保留最后一次操作(合并重复改动)
`CREATE UNIQUE INDEX IF NOT EXISTS idx_pending_row ON pending_ops (table_name, row_id)`,
/**
* 🔴 v58:模块级同步水位(增量同步用)。
*
* 为什么不能复用上面的 `sync_state`:
* 那张表记的是"**这张表**上次同步到哪个服务端时间",它服务于"按表增量拉"。
* 而 `changes` 接口给的是"**这个模块**的变更指纹" —— 粒度不同(一个模块 = 多张表)。
* 硬塞进同一张表会让两种语义互相覆盖,出问题时根本分不清是谁写的。
*
* `fp` 是服务端指纹的**原样字符串**(`行数:最大id:最大updated_at`)。
* 端上**只做相等比较**,绝不解析它 —— 服务端换算法时端上不用改。
*/
`CREATE TABLE IF NOT EXISTS module_sync (
name TEXT PRIMARY KEY,
fp TEXT NOT NULL DEFAULT '',
synced_at INTEGER NOT NULL DEFAULT 0,
full_at INTEGER NOT NULL DEFAULT 0
)`,
/**
* 🔴 v59:附件缓存台账(海报 / 头像 / 观影照片 / 资源附件)。
*
* 为什么要有这张表而不是"直接看文件在不在":
* · 要记**失败次数**(下载失败要退避重试,不能每次启动都重撞);
* · 要记**大小**(面板上显示"已缓存 12.3MB");
* · 要能在"踢设备"时**整表清空**(不该靠遍历目录猜哪些是我们下的)。
*
* `path` 是沙箱内的文件名(不含目录),内容为 AES 密文(见 `FbAttachQueue`)。
* ⚠️ `key` 是**服务端相对键**(如 `posters/963dd25f.jpg`),与 `media.php?k=` 一致。
*/
`CREATE TABLE IF NOT EXISTS attach_cache (
key TEXT PRIMARY KEY,
path TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0,
state TEXT NOT NULL DEFAULT 'pending',
tries INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL DEFAULT 0
)`,
`CREATE INDEX IF NOT EXISTS idx_attach_state ON attach_cache (state, tries)`,
// ---------- 名册(v2 新增)----------
// 🔴 为什么名册需要**自己**的表,而不是直接用 `persons`:
// `persons` 是"全家的通讯录底账"(本家 800+ 人);
// 而名册是「**我**挑出来的那一小撮」—— 我加的人 + 别人分享给我的。
// 服务端用 `person_roster(family_id, owner_id, person_id)` 表达这层"谁的",
// 端上必须同样有一张,否则分不清"这人是我名册里的,还是只是全家的人"。
// ⚠️ 主键用 (owner_id, person_id):一个人在一个人的名册里只会出现一次。
//
// 🔴🔴 `phones` **必须**在首装语句里(2026-10-18 真机踩出来的真 bug)。
// 它当初只加在 `FbDb.migrate()` 的 v3→v4 那步 ALTER 里,**没进这张建表语句**。
// 后果:**全新安装**(卸载重装、新手机)的库 `store.version` 直接就是
// `FB_DB_VERSION`(5) ⇒ `migrate()` 第一行 `from >= FB_DB_VERSION` 直接 return
// ⇒ 那句 ALTER **永远不执行** ⇒ 表里没有 `phones` 列
// ⇒ 名册同步写 `person_roster` 时 SQLite 报
// `table person_roster has no column named phones`
// ⇒ 整批名册写入失败 ⇒ **名册页永远是空的**(而"老用户升级"那条路反倒没问题,
// 所以这个 bug 只在全新安装上现形 —— 用户正好卸载重装了,才暴露出来)。
//
// ⇒ 纪律:**建表语句是"全新安装的最终形态",必须包含所有历史 ALTER 加过的列**。
// 加列时两处都要改:`FB_SCHEMA_SQL`(新装)+ `migrate()`(老库)。
// 只改 migrate() 等于"老用户有、新用户没有";只改这里则"新用户有、老用户没有"。
// 代码里已加静态守卫钉住这一条(见 `_fb_arkts_guard.js` 的
// 「建表 vs 迁移列一致性」一节)。
`CREATE TABLE IF NOT EXISTS person_roster (
owner_id INTEGER NOT NULL,
person_id INTEGER NOT NULL,
family_id INTEGER NOT NULL,
group_id INTEGER NOT NULL DEFAULT 0,
sort_order INTEGER NOT NULL DEFAULT 0,
source TEXT NOT NULL DEFAULT '',
updated_at TEXT NOT NULL DEFAULT '',
synced_at INTEGER NOT NULL DEFAULT 0,
phones TEXT NOT NULL DEFAULT '[]',
PRIMARY KEY (owner_id, person_id)
)`,
`CREATE INDEX IF NOT EXISTS idx_roster_owner ON person_roster (owner_id, sort_order)`,
// 标签:一个人可以有多个标签(服务端 v28 起把"分组"降级成"标签词表",
// 理由:一个人既是小学同学又是初中同学,分组放不下、标签可以)。
`CREATE TABLE IF NOT EXISTS person_tags (
owner_id INTEGER NOT NULL,
person_id INTEGER NOT NULL,
tag TEXT NOT NULL,
updated_at TEXT NOT NULL DEFAULT '',
synced_at INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (owner_id, person_id, tag)
)`,
`CREATE INDEX IF NOT EXISTS idx_tags_owner ON person_tags (owner_id, tag)`,
// ---------- 通用模块缓存(v3 新增)----------
// 🔴 为什么要这张表:日历/任务/纪念日/旅程/账本/资源/藏书/笔记/通知/首页
// 这些模块的数据**目前只需要"拉一次 + 展示"**,没有离线写入的需求。
// 为它们各建实体表(几十列 × 10 张)成本极高、收益极低;
// 而完全不缓存又会让"断网打不开、每次转圈"。
// ⇒ 折中:**原样缓存接口 JSON**,UI 每次解析。
// ⚠️ 代价:拉回来就要解析一遍(10 个模块毫秒级,可接受)。
// ⚠️ 将来某模块要"离线可写"(任务打勾、账本记账)时,
// **那个模块**单独拆实体表 —— 不要整体重构。
`CREATE TABLE IF NOT EXISTS module_cache (
name TEXT PRIMARY KEY,
payload TEXT NOT NULL DEFAULT '',
fetched_at INTEGER NOT NULL DEFAULT 0
)`,
// ---------- 日程实体表(v8 新增)----------
//
// 用户 2026-11-03 指出:「数据量这么大,不是应该同步完放进数据库,
// 点开日程就把今天的查出来吗?怎么能每次都去读全部数据?」
//
// 他说得对。原来日程走 `module_cache`(存 9.3MB JSON 原样文本),
// 每次进页面都要读 9.3MB + JSON.parse 28628 个对象 + 全塞内存。
// 那张表的注释本来说「解析一遍毫秒级,可接受」—— 那是按小数据量假设写的,
// 日程 2.8 万条之后这个假设就破了。
//
// ⇒ 日程单独建实体表(跟 events 表给观影用的思路一致),
// 同步时批量入库,页面按日期范围查。
//
// ⚠️ 为什么主键不用 id:
// 服务端 `ref_id` 对衍生条目(农历/节气/纪念日)恒为 0,
// 几万条全是 0 会撞主键。用自增 rowid + (cal_id, uid) 唯一索引。
`CREATE TABLE IF NOT EXISTS cal_events (
row_id INTEGER PRIMARY KEY AUTOINCREMENT,
uid TEXT NOT NULL DEFAULT '' COMMENT '服务端条目稳定标识',
ev_id INTEGER NOT NULL DEFAULT 0 COMMENT '服务端 ref_id(自定义日程才是真 id)',
cal_id INTEGER NOT NULL DEFAULT 0 COMMENT '所属日历本 id;0=衍生条目',
title TEXT NOT NULL DEFAULT '',
date TEXT NOT NULL DEFAULT '' COMMENT 'YYYY-MM-DD',
time TEXT NOT NULL DEFAULT '' COMMENT 'HH:mm;全天为空',
end_time TEXT NOT NULL DEFAULT '',
cal_name TEXT NOT NULL DEFAULT '',
color TEXT NOT NULL DEFAULT '',
all_day INTEGER NOT NULL DEFAULT 0,
place TEXT NOT NULL DEFAULT '',
note TEXT NOT NULL DEFAULT '',
kind TEXT NOT NULL DEFAULT '' COMMENT 'custom / lunar / holiday / anniv …',
fetched_at INTEGER NOT NULL DEFAULT 0 COMMENT '这批数据的拉取时刻(全量替换时用来防并发)'
)`,
// 按日期查是最高频的(「今天的日程」「本月」),必须索引
`CREATE INDEX IF NOT EXISTS idx_cal_date ON cal_events (date)`,
// 农历/节日按 kind 过滤(渲染日历时区分「真日程」和「衍生条目」)
`CREATE INDEX IF NOT EXISTS idx_cal_date_kind ON cal_events (date, kind)`,
// 同步时按 uid 去重/更新
`CREATE UNIQUE INDEX IF NOT EXISTS idx_cal_uid ON cal_events (cal_id, uid)`,
];
/** 建表语句分组说明(供日志/排查用) */
export interface FbTableGroup {
/** 分组名(如「观影」「同步元数据」) */
group: string;
/** 该组包含的表名 */
tables: string[];
}
/** 本地库里的表清单(按用途分组)—— 删除库/排查时用 */
export const FB_TABLE_GROUPS: FbTableGroup[] = [
{
group: '观影',
tables: ['events', 'movie_details', 'cinemas', 'movie_reviews']
},
{
group: '共用实体',
tables: ['persons', 'event_attendees']
},
{
group: '名册',
tables: ['person_roster', 'person_tags']
},
{
group: '日程',
tables: ['cal_events']
},
{
group: '同步元数据(本地专有)',
tables: ['sync_state', 'pending_ops', 'module_sync', 'attach_cache']
}
];
/** 需要参与同步的业务表(与服务端 v33 的 53 张对齐,这里先放已接入的部分) */
export const FB_SYNCED_TABLES: string[] = [
'events', 'movie_details', 'cinemas', 'movie_reviews', 'persons', 'event_attendees',
'person_roster', 'person_tags'
];
/** 配置本地数据库(SecurityLevel 取 S1:家庭应用,不含敏感凭证) */
export const FB_STORE_CONFIG: relationalStore.StoreConfig = {
name: FB_DB_NAME,
securityLevel: relationalStore.SecurityLevel.S1,
/**
* 🔴 v58(用户 2026-10-17 定 L3:**数据在但读不出**)—— 打开整库加密。
*
* 为什么这里开个开关就够了(而不像注释里原来写的那样"应该走 KeyStore"):
* 鸿蒙 RDB 的 `encrypt: true` **本身就走系统密钥库** ——
* 密钥由系统按「应用 + 设备」生成并托管在 HUKS 里,应用拿不到、也导不出明文。
* 所以:库文件被拷走(root / 拆机 / 备份包)**没有那把密钥就是一堆密文**。
* 这正是用户要的"捡到手机也读不出"。
*
* ⚠️⚠️ **加密属性一经创建不可修改**(ArkData 的硬约束):
* 已装用户手里的是**明文库**,带 `encrypt:true` 打开会直接失败。
* 处理办法在 `FbDb.doOpen()`:失败 → 删掉旧库 → 用加密参数重建 → 重新全量同步。
* 敢删是因为**本地库只是服务端数据的缓存**(见本文件头"本地优先"那段),
* 用户真正的东西都在服务器上;删除只会让下一次同步变慢,不会丢数据。
* ⚠️ 但它**不等于安全擦除**:闪存上的旧块可能还有残留。真正的保证来自"新库是密文",
* 不要把 `deleteRdbStore` 说成"数据已被彻底销毁"。
*
* ⚠️ 万一本机不支持加密(老系统/异常),`FbDb` 会**降级用明文打开**并把原因记进
* `errorText()` —— 一个家庭应用宁可少一层保护,也不能变成打不开。
* 这条降级路径必须在设置页的「诊断」里看得见,不能静默。
*/
encrypt: true
};