名册加内存缓存 + 编辑框自己拉一份 (#92)
用户 2026-11-05:「速度比较慢。刚点开的时候显示的是『成员 #1、成员 #3』
这样的样式,是不是因为没读到人的信息?」
是。占位项(`成员 #id`)就是 roster 还没到位时的兜底。
**慢的真因**:`FbPersonRepo.list()` 是**全表查询**
(800 人 + JOIN person_roster + 标签 GROUP_CONCAT 子查询),
而每个用到选人的组件都在首次打开时各自调一次 ——
首页、任务面板、观影、旅程、资源、账本…逐个来一遍,
同一份数据反复查、反复解析。
① FbPersonRepo 加内存缓存(5 分钟)
· 缓存**只对整表查询**有意义;带关键词时在内存里过滤(同一份 `matches` 口径,
行为与"查库再过滤"完全一致,且更快)。
· 缓存的是**同一份数组引用**:调用方都只读不改(现有代码符合),
不做深拷贝(拷 800 个对象本身就不便宜)。
· 新增 `FbPersonRepo.invalidate()`。
② 同步完成后失效
`FbSyncAll` 收尾时,只要这次不是"全部跳过",就 `FbPersonRepo.invalidate()`。
⚠️ 不清的话:用户改了头像/名字、同步完再点开还是旧的那份,要等 5 分钟过期。
⚠️ 只在 "skipped < total" 时清:全部跳过说明服务端没变,缓存本身有效。
③ 编辑框自己也在 aboutToAppear 里要一份
父页面(首页)的 `dayRoster` 是异步加载的,点开那一瞬可能还是空。
⇒ 组件自己调 `list()`(有缓存时是**瞬时返回**),
查找时"先查自己那份、再查父页面传的"。
这样占位项一闪而过甚至看不到。
⚠️ 两份合并而不是只认一份:父页面可能后到(`@Param` 变化会重画),
组件自己拉的也可能失败(那就靠父页面的)。
编译通过,已装真机。
This commit is contained in:
@@ -27,7 +27,8 @@ import { FbPersonPickSheet } from './FbPersonPickSheet';
|
||||
import { FbAvatar } from './Widgets';
|
||||
import { fbImageSrc } from '../data/FbAttachQueue';
|
||||
import { fbCnDate } from './FbDateTimeField';
|
||||
import { FbRosterPerson } from '../data/FbPersonRepo';
|
||||
import { FbRosterPerson, FbPersonRepo } from '../data/FbPersonRepo';
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { FB_TOKEN_STORE } from '../data/FbTokenStore';
|
||||
import { createDay, updateDay, deleteDay, FbDayDraft } from '../data/FbDayRepo';
|
||||
import { FbApiResult } from '../data/FbHttp';
|
||||
@@ -82,10 +83,29 @@ export struct FbDayEditSheet {
|
||||
@Local personIds: number[] = [];
|
||||
@Local personPickOpen: boolean = false;
|
||||
@Local saving: boolean = false;
|
||||
/**
|
||||
* 组件自己拉到的名册(#92b)。
|
||||
*
|
||||
* 🔴 用户 2026-11-05:「速度比较慢。刚点开的时候显示的是『成员 #1、成员 #3』
|
||||
* 这样的样式,是不是因为没读到人的信息?」
|
||||
*
|
||||
* 是。父页面(首页)的 `dayRoster` 是**异步**加载的,点开编辑框那一瞬
|
||||
* 可能还是空;而 `pickedSlots()` 查不到就出占位。
|
||||
*
|
||||
* ⇒ 组件自己也在 `aboutToAppear` 里要一份:
|
||||
* `FbPersonRepo.list()` 现在有内存缓存(#92),
|
||||
* 首页已经拉过的话这里是**瞬时返回**,占位一闪而过甚至看不到。
|
||||
* ⚠️ 用**两份合并**(先 self 再 param)而不是只认其中一份:
|
||||
* 父页面可能后到(那时 `@Param` 变化会重画),
|
||||
* 组件自己拉的也可能失败(那就靠父页面的)。
|
||||
*/
|
||||
@Local selfRoster: FbRosterPerson[] = [];
|
||||
@Local confirmingDelete: boolean = false;
|
||||
@Local note2: string = '';
|
||||
|
||||
aboutToAppear(): void {
|
||||
/* #92b:自己确保拿到名册(有缓存时瞬时) */
|
||||
this.ensureRoster();
|
||||
this.title = this.editingTitle;
|
||||
this.kind = this.editingKind;
|
||||
this.note = this.editingNote;
|
||||
@@ -123,11 +143,36 @@ export struct FbDayEditSheet {
|
||||
* 原来直接"查不到就跳过" ⇒ chips 区一片空白 ⇒ 看着像数据丢了。
|
||||
* 可数据其实一直在 `personIds` 里。
|
||||
*/
|
||||
/**
|
||||
* 自己拉一的名册(#92b)。
|
||||
* ⚠️ 失败不弹错:名册拿不到时 `pickedSlots()` 会出占位项,
|
||||
* 编辑功能本身不受影响(保存时用的是 `personIds`,不依赖名册)。
|
||||
*/
|
||||
private async ensureRoster(): Promise<void> {
|
||||
if (this.selfRoster.length > 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.selfRoster = await FbPersonRepo.inst().list();
|
||||
} catch (err) {
|
||||
hilog.warn(0x0000, 'FbDayEditSheet', '拉名册失败:%{public}s', JSON.stringify(err));
|
||||
}
|
||||
}
|
||||
|
||||
/** 按 id 找人:先查自己那份,再查父页面传的(#92b) */
|
||||
private findPerson(id: number): FbRosterPerson | undefined {
|
||||
let p: FbRosterPerson | undefined =
|
||||
this.selfRoster.find((x: FbRosterPerson): boolean => x.id === id);
|
||||
if (p === undefined) {
|
||||
p = this.roster.find((x: FbRosterPerson): boolean => x.id === id);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
private pickedSlots(): FbPickSlot[] {
|
||||
const out: FbPickSlot[] = [];
|
||||
for (const id of this.personIds) {
|
||||
const p: FbRosterPerson | undefined =
|
||||
this.roster.find((x: FbRosterPerson): boolean => x.id === id);
|
||||
const p: FbRosterPerson | undefined = this.findPerson(id);
|
||||
if (p !== undefined) {
|
||||
out.push({
|
||||
id: id,
|
||||
|
||||
@@ -279,6 +279,40 @@ export class FbPersonRepo {
|
||||
private static _inst: FbPersonRepo | null = null;
|
||||
private db: FbDb = FbDb.inst();
|
||||
|
||||
/**
|
||||
* 名册的内存缓存(#92)。
|
||||
*
|
||||
* 🔴 用户 2026-11-05:「速度比较慢。刚点开的时候显示的是『成员 #1、成员 #3』
|
||||
* 这样的样式,是不是因为没读到人的信息?」
|
||||
* 是 —— 占位项就是 roster 还没到时的兜底。
|
||||
*
|
||||
* **慢的真因**:`list()` 是**全表查询**(800 人 + JOIN person_roster +
|
||||
* 标签 GROUP_CONCAT 子查询),而每个用到选人的组件都在自己
|
||||
* `aboutToAppear` / 首次打开时调一次 —— 首页、任务面板、观影、旅程、
|
||||
* 资源、账本…逐个来一遍。同一份数据反复查、反复解析。
|
||||
*
|
||||
* ⚠️ 缓存**只对无关键词的整表查询**生效(`list('')`):
|
||||
* 带关键词时是"查完再过滤"(内存过滤),走缓存同样正确且更快,
|
||||
* 所以其实也命中 —— 见下面的实现。
|
||||
* ⚠️ 失效:名册变动后必须 `invalidate()`。目前名册只在"同步"时变,
|
||||
* 同步入口会调它(见 `FbSync` / 各页 `load()`)。
|
||||
* ⚠️ 缓存的是**同一份数组引用**。调用方只读不改(现有代码都符合),
|
||||
* 所以不做深拷贝 —— 拷贝 800 个对象本身就不便宜。
|
||||
*/
|
||||
private static _cache: FbRosterPerson[] | null = null;
|
||||
private static _cacheAt: number = 0;
|
||||
/** 缓存有效期(毫秒)。5 分钟 —— 名册不是高频变化的数据 */
|
||||
private static readonly CACHE_MS: number = 5 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* 让名册缓存失效(同步完成 / 有人改名改头像后调用)。
|
||||
* ⚠️ 调用后下一次 `list()` 会重新查库。
|
||||
*/
|
||||
static invalidate(): void {
|
||||
FbPersonRepo._cache = null;
|
||||
FbPersonRepo._cacheAt = 0;
|
||||
}
|
||||
|
||||
/** 单例(与服务端 `Person` 一样是纯静态工具,端上用单例便于将来加缓存) */
|
||||
static inst(): FbPersonRepo {
|
||||
if (FbPersonRepo._inst === null) {
|
||||
@@ -498,24 +532,39 @@ export class FbPersonRepo {
|
||||
* 手机上是实打实的卡顿。
|
||||
*/
|
||||
async list(keyword: string = ''): Promise<FbRosterPerson[]> {
|
||||
/* #92:整表查一次就缓存住 —— 见 `_cache` 的注释 */
|
||||
const now: number = Date.now();
|
||||
let base: FbRosterPerson[] | null = FbPersonRepo._cache;
|
||||
if (base === null || now - FbPersonRepo._cacheAt > FbPersonRepo.CACHE_MS) {
|
||||
const sql: string =
|
||||
'SELECT p.id, p.name, p.nickname, p.gender, p.birthday, p.avatar, p.avatar_color,' +
|
||||
' p.note, p.email, p.org, p.title, p.slogan, p.addr_city, p.initials,' +
|
||||
' r.sort_order, r.phones,' +
|
||||
' (SELECT GROUP_CONCAT(t.tag, "\\u0001") FROM person_tags t' +
|
||||
' (SELECT GROUP_CONCAT(t.tag, "\u0001") FROM person_tags t' +
|
||||
' WHERE t.person_id = p.id AND t.owner_id = r.owner_id) AS tags' +
|
||||
' FROM person_roster r JOIN persons p ON p.id = r.person_id' +
|
||||
' ORDER BY r.sort_order ASC, p.id ASC';
|
||||
|
||||
const rows: FbRow[] = await this.db.queryRows(sql, []);
|
||||
const out: FbRosterPerson[] = [];
|
||||
base = [];
|
||||
for (const r of rows) {
|
||||
const p: FbRosterPerson = this.rowToPerson(r);
|
||||
if (keyword !== '' && !matches(p, keyword)) {
|
||||
continue;
|
||||
base.push(this.rowToPerson(r));
|
||||
}
|
||||
FbPersonRepo._cache = base;
|
||||
FbPersonRepo._cacheAt = now;
|
||||
}
|
||||
|
||||
/* 关键词过滤在内存里做(与原来同一份 `matches` 口径)——
|
||||
缓存命中也走这条,行为与"查库再过滤"完全一致。 */
|
||||
if (keyword === '') {
|
||||
return base;
|
||||
}
|
||||
const out: FbRosterPerson[] = [];
|
||||
for (const p of base) {
|
||||
if (matches(p, keyword)) {
|
||||
out.push(p);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -302,6 +302,15 @@ export async function syncAll(onStep?: FbSyncStepCb): Promise<FbSyncAllResult> {
|
||||
st.failed = failed;
|
||||
st.finish(note);
|
||||
|
||||
/* 🔴 #92:同步刚把新的名册写进本地库 ⇒ **必须让内存缓存失效**,
|
||||
否则用户改了头像/名字、同步完再点开还是旧的那份(要等 5 分钟过期)。
|
||||
⚠️ 只在"真的同步了东西"时清:全部跳过说明服务端没变,缓存本身是有效的。
|
||||
⚠️ `FbPersonRepo` 的缓存是**静态**的(跨组件共享),
|
||||
所以这里清一次,所有页面都生效。 */
|
||||
if (skipped < total) {
|
||||
FbPersonRepo.invalidate();
|
||||
}
|
||||
|
||||
const result: FbSyncAllResult = {
|
||||
ok: failed.length === 0,
|
||||
done: done,
|
||||
|
||||
Reference in New Issue
Block a user