## 背景
用户 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 那一轮,因故未提交;
本次一并整理提交,与品牌改名(家书→岁月家)无关。
122 lines
5.5 KiB
Plaintext
122 lines
5.5 KiB
Plaintext
/**
|
||
* 家书 FamilyBook · 页面数据接入辅助(鸿蒙端)
|
||
*
|
||
* 各模块页面的数据装配**都是同一套动作**:
|
||
* ① 页面出现 → ② 调仓库 `load()` → ③ 先拿缓存渲染、再拿网络刷新 → ④ 解析成端上类型。
|
||
* 这份文件把 ②④ 收成一处,页面只写"我关心哪个模块、怎么解析"。
|
||
*
|
||
* 🔴 为什么要抽(而不是每页各写一遍):
|
||
* 10 个模块 × 同一段 20 行样板 = 200 行重复代码,
|
||
* 而重复代码的代价不是"多打几个字",是**改一处漏九处**
|
||
* (本项目在别的地方已经吃过这个亏)。
|
||
*/
|
||
|
||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||
import { FbModuleLoad, FbModuleName, FbModuleRepo } from './FbModuleRepo';
|
||
|
||
const DOMAIN: number = 0x0000;
|
||
const TAG: string = 'FbModuleLoad';
|
||
|
||
/**
|
||
* 一次性加载(**直接给路径**,不必登记在上面的静态表里)。
|
||
*
|
||
* ⚠️ 给"两级接口"用(如账本明细 `/ledger/<id>/entries` —— 路径带 id,
|
||
* 没法在静态表里穷举)。缓存键也用完整路径,天然互不冲突。
|
||
*/
|
||
export async function loadPath<T>(cacheKey: string, path: string, parse: (json: string) => T,
|
||
sink: (data: T, fromCache: boolean) => void,
|
||
onNote?: (note: string) => void): Promise<void> {
|
||
await FbModuleRepo.inst().load(cacheKey, path, (r: FbModuleLoad): void => {
|
||
let data: T;
|
||
try {
|
||
data = parse(r.json);
|
||
} catch (err) {
|
||
hilog.error(DOMAIN, TAG, '解析 %{public}s 失败:%{public}s', cacheKey, JSON.stringify(err));
|
||
return;
|
||
}
|
||
if (onNote !== undefined && r.error !== '') {
|
||
onNote(r.error);
|
||
}
|
||
sink(data, r.fromCache);
|
||
});
|
||
}
|
||
|
||
/** 模块 → 接口路径 */
|
||
export const FB_MODULE_PATH: Record<string, string> = {
|
||
'home': '/api/v1/home',
|
||
'calendar': '/api/v1/calendar',
|
||
'days': '/api/v1/days',
|
||
'tasks': '/api/v1/tasks',
|
||
'trips': '/api/v1/trips',
|
||
'ledger': '/api/v1/ledger',
|
||
'resources': '/api/v1/resources',
|
||
'books': '/api/v1/books',
|
||
'notes': '/api/v1/notes',
|
||
'notifications': '/api/v1/notifications',
|
||
};
|
||
|
||
/**
|
||
* 装配一个模块:读缓存 + 拉网络,每次拿到**解析后**的数据回调。
|
||
*
|
||
* @param name 模块名
|
||
* @param parse JSON → 端上类型的解析函数
|
||
* @param sink 数据到位回调(会被调 1~2 次:缓存一次、网络一次)
|
||
* @param onNote 可选:提示文案(同步失败/未登录时给页面显示一行小字)
|
||
* @param pathOverride 可选:**改掉这个模块的请求路径**(带查询串)。
|
||
*
|
||
* 🔴🔴 v59(2026-10-18,用户报「列表这个视图,今天的数据缺一条」的真根因):
|
||
* 服务端 `/api/v1/calendar` 的 `from`/`to` **默认只给"本月"**
|
||
* (`Api::str('from', date('Y-m-01'))` / `date('Y-m-t')`)。
|
||
* 而日程页的**列表视图**要渲染 `-7 ~ +60` 天 —— 跨到下个月去,
|
||
* 下月那一段**服务端根本没返回** ⇒ 落在下月的日程(包括"今天"往后那几天)
|
||
* 在列表里凭空少掉。
|
||
* ⚠️ 月视图看起来正常,是因为它本来就只看本月 ⇒ 这个 bug 只在列表暴露。
|
||
* ⇒ 日程页显式传 `from`/`to`(覆盖它要显示的那一整段),不要依赖默认值。
|
||
*
|
||
* ⚠️ 只改**请求路径**、不动缓存键(缓存仍按模块名存):
|
||
* 否则同一个模块会出现两个缓存键、切视图时互相覆盖。
|
||
* ⚠️ 这也意味着"列表拉的比月视图多"时,缓存里会留下更宽的一段 ——
|
||
* 这是**好事**(月视图也能直接命中缓存),但**不要**指望缓存里的范围
|
||
* 与当前视图严格一致;页面永远按自己需要的日期去索引。
|
||
*/
|
||
/**
|
||
* @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,
|
||
timeoutMs?: number): Promise<void> {
|
||
|
||
const path: string = pathOverride !== undefined && pathOverride !== ''
|
||
? pathOverride
|
||
: (FB_MODULE_PATH[name] !== undefined ? FB_MODULE_PATH[name] : '');
|
||
if (path === '') {
|
||
hilog.warn(DOMAIN, TAG, '模块 %{public}s 没有配路径', name);
|
||
return;
|
||
}
|
||
|
||
await FbModuleRepo.inst().load(name, path, (r: FbModuleLoad): void => {
|
||
// ⚠️ 解析失败**不能抛**:缓存/接口的形状变了(或确实是空),
|
||
// 抛出去会让页面白屏。这里给个空数组兜住,并留一条日志。
|
||
let data: T;
|
||
try {
|
||
data = parse(r.json);
|
||
} catch (err) {
|
||
hilog.error(DOMAIN, TAG, '解析 %{public}s 失败:%{public}s', name, JSON.stringify(err));
|
||
return;
|
||
}
|
||
if (onNote !== undefined && r.error !== '') {
|
||
onNote(r.error);
|
||
}
|
||
sink(data, r.fromCache);
|
||
});
|
||
} |