日程改为全量同步 + 修复大响应超时(laoyang/FamilyBook#31)
## 背景
用户 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:<from>:<to>`):某次请求失败后 `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 那一轮,因故未提交;
本次一并整理提交,与品牌改名(家书→岁月家)无关。
This commit is contained in:
@@ -79,10 +79,22 @@ export const FB_MODULE_PATH: Record<string, string> = {
|
||||
* 这是**好事**(月视图也能直接命中缓存),但**不要**指望缓存里的范围
|
||||
* 与当前视图严格一致;页面永远按自己需要的日期去索引。
|
||||
*/
|
||||
export async function loadModule<T>(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<T>(name: string, parse: (json: string) => T,
|
||||
sink: (data: T, fromCache: boolean) => void,
|
||||
onNote?: (note: string) => void,
|
||||
pathOverride?: string): Promise<void> {
|
||||
pathOverride?: string,
|
||||
timeoutMs?: number): Promise<void> {
|
||||
|
||||
const path: string = pathOverride !== undefined && pathOverride !== ''
|
||||
? pathOverride
|
||||
|
||||
@@ -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<void> {
|
||||
async load(name: string, path: string, sink: FbModuleSink, timeoutMs?: number): Promise<void> {
|
||||
// ① 本地缓存(若还没读过本地库,`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);
|
||||
|
||||
@@ -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<void> {
|
||||
/* 🔴🔴 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<FbCalEvent[]>('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<FbCalEvent[]>(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();
|
||||
|
||||
Reference in New Issue
Block a user