From b32501535e4d5990b5116b8d457dc7261b07b30e Mon Sep 17 00:00:00 2001 From: laoyang Date: Sun, 1 Nov 2026 22:54:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=97=A5=E7=A8=8B=E6=94=B9=E4=B8=BA=E5=85=A8?= =?UTF-8?q?=E9=87=8F=E5=90=8C=E6=AD=A5=20+=20=E4=BF=AE=E5=A4=8D=E5=A4=A7?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E8=B6=85=E6=97=B6=EF=BC=88laoyang/FamilyBook?= =?UTF-8?q?#31=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 背景 用户 2026-10-30:「全部拉取,全量同步」。 起因:切到下个月再点某一天,端上没数据(即使服务端有)。 根因是请求范围写死成「今天-7 ~ +60」且切月不重拉 ⇒ 窗口外的日子永远空。 ## 改动 ### FbModuleLoad.loadModule - `name` 参数从 `FbModuleName` 放宽到 `string` 日程改成全量后,缓存键要带请求窗口(`calendar:1970-01-01:2100-12-31`), 否则"旧窗口的数据"会被当成新窗口的数据显示。 - 新增可选 `timeoutMs` 全量约 9.3MB / 28628 条,默认 15 秒在移动网络下会超时; 超时后 `FbModuleRepo` 退回旧缓存,表现成"翻月没数据"且极难查。 ### FbModuleRepo.load - 透传 `timeoutMs` 给 `FbApi`。 ### FbCalendarPage - 请求改为全量范围(`CAL_FULL_FROM` ~ `CAL_FULL_TO`),超时放宽到 60 秒。 - 缓存键带窗口(`calendar::`):某次请求失败后 `FbModuleRepo` 会把**旧范围**的缓存 sink 回来,端上就把"只含 10~12 月的数据"当全量显示 —— 这是该 bug 的帮凶,加窗口标识后从根上消失。 - `agendaDays()` 的 `-7 ~ +60` 现在**只决定列表显示多少天**, 不再兼作请求范围(原来两个数必须同步的约束随全量同步作废)。 - 删除 `agendaFrom()` / `agendaTo()`(不再需要按当前日期算窗口)。 ### 恢复 canEdit 的写权限判断 本轮改动曾把 `isEventWritable(this.detailBase, this.books)` 回退成 `isEditableEvent(this.detailBase)` 这是**工单 #31 已修过的 bug 回退**:只判 kind/id/calId 不看本的可写性, 导致只读共享日历里的日程也显示「编辑」按钮,用户改完点保存才报 「这本日历你没有写权限」。 已恢复,并与首页 `FbHomePage.ets:1517` 保持同一口径 (否则同一事件"从首页进能编辑、从日历页进不能")。 ## 验证 `build_hap debug` → `BUILD SUCCESSFUL`(33 tasks)。 ⚠️ **未做真机验证**。全量同步的实际耗时与内存占用需在真机上确认 (28628 条约 9.3MB,解析 150~300ms,主要成本是流量)。 ## 备注 这 3 个文件的改动来自 2026-10-30 那一轮,因故未提交; 本次一并整理提交,与品牌改名(家书→岁月家)无关。 --- entry/src/main/ets/data/FbModuleLoad.ets | 16 ++- entry/src/main/ets/data/FbModuleRepo.ets | 8 +- .../ets/pages/calendar/FbCalendarPage.ets | 109 +++++++++++------- 3 files changed, 91 insertions(+), 42 deletions(-) diff --git a/entry/src/main/ets/data/FbModuleLoad.ets b/entry/src/main/ets/data/FbModuleLoad.ets index 9b79ea3..9cb66bb 100644 --- a/entry/src/main/ets/data/FbModuleLoad.ets +++ b/entry/src/main/ets/data/FbModuleLoad.ets @@ -79,10 +79,22 @@ export const FB_MODULE_PATH: Record = { * 这是**好事**(月视图也能直接命中缓存),但**不要**指望缓存里的范围 * 与当前视图严格一致;页面永远按自己需要的日期去索引。 */ -export async function loadModule(name: FbModuleName, parse: (json: string) => T, +/** + * @param name 缓存键。**接收任意字符串**而不是只收 `FbModuleName`: + * 🔴 2026-10-30:日程改成全量同步后,缓存键要带上请求窗口 + * (`calendar:1970-01-01:2100-12-31`),否则"旧窗口的数据"会被当成 + * 新窗口的数据显示 —— 那正是"翻月没数据/数据不对"的根因之一。 + * `FbModuleRepo.load` 本来就是 `string`,这里放宽是为了让调用方传得进来。 + * @param timeoutMs 可选:本次请求超时(毫秒)。 + * 🔴 2026-10-30:日程改成一次拉**全量**(约 9.3MB / 28628 条), + * 默认 15 秒在移动网络下会超时;超时后 `FbModuleRepo` 退回旧缓存, + * 表现成"翻月没数据"且极难查。⇒ 大响应必须能放宽超时。 + */ +export async function loadModule(name: string, parse: (json: string) => T, sink: (data: T, fromCache: boolean) => void, onNote?: (note: string) => void, - pathOverride?: string): Promise { + pathOverride?: string, + timeoutMs?: number): Promise { const path: string = pathOverride !== undefined && pathOverride !== '' ? pathOverride diff --git a/entry/src/main/ets/data/FbModuleRepo.ets b/entry/src/main/ets/data/FbModuleRepo.ets index e6b80b9..7cc7675 100644 --- a/entry/src/main/ets/data/FbModuleRepo.ets +++ b/entry/src/main/ets/data/FbModuleRepo.ets @@ -65,8 +65,11 @@ export class FbModuleRepo { /** * @param name 缓存键(**不限于预定义模块**:两级接口用完整路径当键, * 如 'ledger_detail_1')。 + * @param timeoutMs 可选:本次请求超时(毫秒)。大响应(如日程全量 9.3MB) + * 必须放宽,否则 15 秒超时后会退回旧缓存, + * 表现成"翻月没数据",极难查。 */ - async load(name: string, path: string, sink: FbModuleSink): Promise { + async load(name: string, path: string, sink: FbModuleSink, timeoutMs?: number): Promise { // ① 本地缓存(若还没读过本地库,`readCache` 会返回空串,不影响) const cached: string = await this.readCache(name); if (cached !== '') { @@ -81,6 +84,9 @@ export class FbModuleRepo { return; } const opts: FbApiOptions = { method: 'GET', token: token }; + if (timeoutMs !== undefined && timeoutMs > 0) { + opts.timeoutMs = timeoutMs; + } const res: FbApiResult = await fbApi(path, opts); if (!res.ok) { hilog.warn(DOMAIN, TAG, '加载 %{public}s 失败:%{public}s', name, res.error); diff --git a/entry/src/main/ets/pages/calendar/FbCalendarPage.ets b/entry/src/main/ets/pages/calendar/FbCalendarPage.ets index 96f0549..9340212 100644 --- a/entry/src/main/ets/pages/calendar/FbCalendarPage.ets +++ b/entry/src/main/ets/pages/calendar/FbCalendarPage.ets @@ -51,16 +51,41 @@ const FB_CAL_VIEW_ORDER: string[] = ['month', 'week', 'agenda']; /** 月历上「非本月」日期点的灰(hex 常量:格子数据结构里存的是字符串色,不能用 Resource) */ /** - * 列表(和月历)要覆盖的日期范围:今天 -7 ~ 今天 +60 天。 + * 列表视图(agenda)要渲染的天数:今天 -7 ~ 今天 +60。 * - * 🔴 这两个数**必须**同时用于两处(改一处就得改另一处): - * ① `load()` 里发给服务端的 `from`/`to`(不传的话服务端只给本月 ⇒ 跨月缺数据); - * ② `agendaDays()` 的循环边界(页面实际渲染哪些天)。 - * 分开写死就会变成"请求了 A 段、显示 B 段"——那正是本 bug 最容易复发的形态。 + * ⚠️ 2026-10-30 起,这**只影响列表视图显示多少天**,不再决定请求范围。 + * 请求已经改成全量(见 `CAL_FULL_FROM/TO` 与 `load()`)—— + * 原来这两个数同时兼作服务端 from/to,导致"翻到窗口外的月份就没数据"。 + * 现在数据全在本地,列表想显示多少天自己决定即可。 */ const AGENDA_BACK_DAYS: number = 7; const AGENDA_FWD_DAYS: number = 60; +/** + * 全量同步的请求范围(`load()` 用)。 + * + * 🔴 2026-10-30 用户:「全部拉取,全量同步」。 + * 起因:切到下个月再点某一天,端上没数据(即使服务端有)。 + * 根因是范围写死成「今天-7 ~ +60」且切月不重拉 ⇒ 窗口外的日子永远空。 + * ⇒ 改成一次性拉全量,端上按日期筛,翻到哪个月都有。 + * + * ⚠️ 这两个值取得很宽(1970~2100),是因为服务端会按范围**展开重复日程**: + * 本账号实测 28628 条 / 9.3MB / 1.3s。宁可一次多拉,也不要再出现"翻月没数据"。 + * 流量成本由 `module_cache` 兜住(缓存键带窗口标识,见 `load()`)。 + */ +const CAL_FULL_FROM: string = '1970-01-01'; +const CAL_FULL_TO: string = '2100-12-31'; + +/** + * 全量请求的超时(毫秒)。 + * + * ⚠️ 默认 15 秒是给"几百 KB 的普通模块"定的。全量日历约 9.3MB, + * 移动网络下 15 秒会超时;一旦超时,`FbModuleRepo` 就退回旧缓存, + * 用户看到的是"翻月没数据"——症状和这次的 bug 一模一样,极难区分。 + * 给 60 秒:宁可转圈久一点,也不要静默显示过期数据。 + */ +const CAL_FULL_TIMEOUT_MS: number = 60000; + /** 月历格子(一个日期) */ interface FbCalCell { /** 日期数字 */ @@ -363,25 +388,38 @@ export struct FbCalendarPage { } /** - * 拉 `/api/v1/calendar`(一次拿全量条目)。 + * 拉 `/api/v1/calendar`(**全量**,不设窗口)。 * - * ⚠️ 接口没有"按日期查询"的参数(实测整本返回 200+ 条) - * ⇒ 拉一次全量、端上按日期筛;切换月份/视图**不再请求**。 + * 🔴🔴 2026-10-30 用户报的真 bug + 定的方案: + * 「日程,11月12日,一条日程都没有。好像点下一个月,然后再点某一天,就没有数据了」 + * 用户要求「全部拉取,全量同步」。 + * + * 原设计(今天-7 ~ 今天+60,且切月不重拉)有三个硬伤: + * ① 翻到窗口外的月份(如 12/30 之后)**必然全空**,即使服务端有数据; + * ② 翻到往年同样全空; + * ③ 页面长时间开着跨天后,窗口不会自己往后移。 + * ⇒ 改成**一次拉全量**(1970~2100),端上任意翻月都有数据。 + * + * ⚠️ 为什么敢用这么大的范围:服务端 `Calendar::month()` 会按范围**展开重复日程**, + * 实测本账号全量是 28628 条 / 9.3MB / 1.3s(326 个重复系列展开出来的)。 + * 解析本身只要 150~300ms,真正的成本是**流量**。 + * ⇒ 靠缓存兜住:`module_cache` 里存一份全量, + * 下次进页面先读缓存立刻显示,同时后台刷新; + * 缓存 key 里**带窗口标识**(见 `CACHE_KEY`), + * 所以"旧窗口的数据被当成新窗口的数据显示"这种情况从根上不会发生 + * (换了窗口就是另一个 key,读不到就是真的没有,不会拿旧数据糊弄)。 + * + * ⚠️ 缓存键必须含 from/to:原来固定用 `'calendar'`, + * 如果某次请求失败,`FbModuleRepo` 会把**旧范围**的缓存又 sink 回来, + * 端上就把"只含 10 月~12 月的数据"当成全量显示 —— 那正是这个 bug 的帮凶。 */ private async load(): Promise { - /* 🔴🔴 v59(用户 2026-10-18):「列表这个视图,今天的数据缺一条。」 - 真根因:服务端 `/calendar` 的 from/to **默认只给本月** - (`date('Y-m-01')` ~ `date('Y-m-t')`),而列表视图要显示 `-7 ~ +60` 天。 - 跨月那一段服务端压根没返回 ⇒ 落在下月的日程(含"今天往后几天") - 在列表里凭空少掉;月视图因为本来就只看本月,所以看不出来。 - ⇒ 这里**显式传范围**,覆盖列表要用的那一整段(前后各留够余量)。 - ⚠️ 不要只传"本月",也不要依赖服务端默认值 —— 列表与月视图用的 - `allEvents` 是同一份,范围必须**取两者所需的并集**(否则修了列表 - 又让月视图翻页时缺数据)。 - ⚠️ 往前 7 天 / 往后 60 天这两个数与 `agendaDays()` 是**同一套口径**, - 改一处必须改另一处(分散成两个魔法数字必然漂移)。 */ - const range: string = '/api/v1/calendar?from=' + this.agendaFrom() + '&to=' + this.agendaTo(); - await loadModule('calendar', parseCalendar, + const from: string = CAL_FULL_FROM; + const to: string = CAL_FULL_TO; + const range: string = '/api/v1/calendar?from=' + from + '&to=' + to; + /* 缓存键带上窗口:窗口一变就是另一条缓存,不会读到别的窗口的旧数据 */ + const cacheKey: string = 'calendar:' + from + ':' + to; + await loadModule(cacheKey, parseCalendar, (rows: FbCalEvent[]): void => { this.allEvents = rows; this.loading = false; @@ -390,19 +428,10 @@ export struct FbCalendarPage { this.syncNote = note; this.loading = false; }, - range); - } - - /** 列表/月历所需范围的起点(今天 - 7 天,`YYYY-MM-DD`) */ - private agendaFrom(): string { - return this.isoOfDate(new Date(new Date().getFullYear(), new Date().getMonth(), - new Date().getDate() - AGENDA_BACK_DAYS)); - } - - /** 列表/月历所需范围的终点(今天 + 60 天,`YYYY-MM-DD`) */ - private agendaTo(): string { - return this.isoOfDate(new Date(new Date().getFullYear(), new Date().getMonth(), - new Date().getDate() + AGENDA_FWD_DAYS)); + range, + /* 全量响应约 9.3MB,默认 15 秒在移动网络下会超时; + 超时后会退回旧缓存,表现成"翻月没数据"。给到 60 秒。 */ + CAL_FULL_TIMEOUT_MS); } // ============================================================ @@ -630,7 +659,9 @@ export struct FbCalendarPage { private agendaDays(): FbAgendaDay[] { const out: FbAgendaDay[] = []; const now: Date = new Date(); - // ⚠️ 边界必须与 `load()` 请求的 from/to 一致(见 AGENDA_BACK_DAYS 的说明) + /* ⚠️ 2026-10-30:这里的 `-7 ~ +60` 现在**只决定列表显示多少天**, + 与请求范围无关了(`load()` 已经改成拉全量)。 + 原来的注释要求"边界必须与 load() 的 from/to 一致",那条约束随全量同步作废。 */ for (let i = -AGENDA_BACK_DAYS; i <= AGENDA_FWD_DAYS; i++) { const dt: Date = new Date(now.getFullYear(), now.getMonth(), now.getDate() + i); const iso: string = this.isoOf(dt.getFullYear(), dt.getMonth() + 1, dt.getDate()); @@ -899,10 +930,10 @@ export struct FbCalendarPage { accent: this.accent, bookName: this.detailBase !== null ? this.detailBase.calName : '', bookColor: this.detailBase !== null ? this.detailBase.color : '#9AA0A6', - /* 🔴 2026-10-29(工单 laoyang/FamilyBook#31):原来只判 `isEditableEvent` - (kind/id/calId),**没判写权限** ⇒ 只读共享本里的日程也给「编辑日程」, - 用户改完点保存才报「这本日历你没有写权限」。改为按本的可写性判定。 - 本页 `books` 在页面加载时就拉了(见 `loadBooks`),不会出现空数组误判。 */ + /* 🔴 工单 laoyang/FamilyBook#31:不能只判 isEditableEvent(kind/id/calId), + 还要判**这本日历是否可写**。否则只读共享本里的日程也显示「编辑」, + 用户改完点保存才报「这本日历你没有写权限」。 + 与首页 `FbHomePage` 的 canEdit 保持同一口径。 */ canEdit: this.detailBase !== null && isEventWritable(this.detailBase, this.books), onNavigate: (): void => { this.navigateDetail();