v51c 卡片后台同步:修复"每轮全量重拉",单账号 236 秒 → 10 秒

真机实测根因:服务端 CalDav.php:253 用 preg_replace('/[^A-Za-z0-9._-]/','_',$uid)
派生资源文件名(UID movie-188@familybook → 资源 movie-188_familybook.ics),
而客户端 SyncEngine.syncAccountInner 拿 REPORT 给的**资源名当 uid** 去本地行里查
⇒ byUid 永远查不到 ⇒ 每轮判"变了" ⇒ 全量 GET。ETag 明明一样,比较压根执行不到。

改动:
· SyncEngine —— 加 byEtag 兜底(uid 查不到就用内容哈希认领本地行)+ 复用判定计数日志
  (uid 命中 / etag 兜底命中 / 需补拉);抽 groupPush() 去重。
· EventDb.applyRemote —— existing.find() 的 O(n·m) → keyed.get() 的 O(n)。
· BackgroundSyncRunner —— runOnce 每阶段计时(+Xms)+ pid;预算可由 preferences
  card_budget_ms 覆盖(诊断开关,默认走内置 9 秒)。
· AppSettings —— getCardBudgetMs()。
· EntryFormAbility —— onUpdateForm 日志带 pid。
· MirrorInbound —— 顺带修一个已死的功能:getEvents(undefined, 字段) 在 7.0.0.109 上
  必抛 401 type error!,导致**入站对账 12/12 全失败**;改成"先无参、失败再带字段"
  (照抄 SystemCalendarMirror.queryEvents 的绕法)。

真机验证(DEL-AL20,卡片 onUpdateForm 触发,卡片进程 pid=27411):
  [21:33:06] 卡片驱动同步:开始(可用窗口 9 秒)
  [21:33:09] 「观影」复用判定:uid 命中 0、etag 兜底命中 193、需补拉 0(共 193)
  [21:33:09] 「观影」落库完成:不变193,耗时 1 秒     ← 修复前 124 秒
  [21:33:17] 同步账号完成:拉取 2182 条,耗时 10 秒   ← 修复前 215~236 秒
  [21:33:22] 后台同步[cardTick] 完成:1/1 个账号,总耗时 16 秒
12 个日历本全部跑完 + 出站镜像,9 秒窗口不再"连第一个本都过不去"。

复算(真库快照 + 真服务器 PROPFIND,_tooling/_sc_etag_sim.py):
  旧逻辑补拉 1384 个 → 新逻辑 80 个(降 94.2%);观影 193→0、旅行 23→0、工作 362→5。
  另证服务端 ETag == md5(正文)、三次 PROPFIND 逐字节一致 ⇒ etag 兜底语义正确、非碰巧。
This commit is contained in:
2026-10-15 21:39:54 +08:00
parent 8ae4e11507
commit 24d8117ce7
6 changed files with 132 additions and 21 deletions
+14
View File
@@ -24,6 +24,9 @@ export class AppSettings {
// 服务卡片每 30 分钟被系统刷新一次(form_config 的 updateDuration=1), // 服务卡片每 30 分钟被系统刷新一次(form_config 的 updateDuration=1),
// 借这个时机顺带跑一次后台同步 —— 得到"约 30 分钟一次"的定期同步。 // 借这个时机顺带跑一次后台同步 —— 得到"约 30 分钟一次"的定期同步。
private static readonly KEY_CARD_SYNC_AT: string = 'card_sync_at'; private static readonly KEY_CARD_SYNC_AT: string = 'card_sync_at';
/** ⭐ 诊断开关:卡片驱动同步的时间预算(毫秒)。0/未设置 = 用内置默认(见 BackgroundSyncRunner)。
* 留给真机排查用 —— 不用重编译就能做"9 秒 vs 长窗口"对照实验。 */
private static readonly KEY_CARD_BUDGET_MS: string = 'card_budget_ms';
private static readonly KEY_FULL_REFETCH: string = 'full_refetch_done'; // 一次性全量重拉已完成 private static readonly KEY_FULL_REFETCH: string = 'full_refetch_done'; // 一次性全量重拉已完成
private static readonly KEY_DEFAULT_VIEW: string = 'default_view'; // 打开 App 默认视图 private static readonly KEY_DEFAULT_VIEW: string = 'default_view'; // 打开 App 默认视图
private static readonly KEY_DISPLAY_STYLE: string = 'display_style'; // 日程显示方式:'timeline' | 'list' private static readonly KEY_DISPLAY_STYLE: string = 'display_style'; // 日程显示方式:'timeline' | 'list'
@@ -350,6 +353,17 @@ export class AppSettings {
} }
} }
/** ⭐ 诊断用:卡片驱动同步的时间预算(毫秒)。0 = 未设置(用内置默认值)。 */
static async getCardBudgetMs(context: common.Context): Promise<number> {
try {
const store: preferences.Preferences =
await preferences.getPreferences(context, AppSettings.STORE);
return await store.get(AppSettings.KEY_CARD_BUDGET_MS, 0) as number;
} catch (err) {
return 0;
}
}
/** /**
* 是否还需要一次性全量重拉:修复历史同步(REPORT 剥离 VALARM 时期)落库的残缺数据。 * 是否还需要一次性全量重拉:修复历史同步(REPORT 剥离 VALARM 时期)落库的残缺数据。
* 全量重拉期间忽略 etag 复用,所有资源 GET 完整 ICS;成功完成后标记,恢复增量模式。 * 全量重拉期间忽略 etag 复用,所有资源 GET 完整 ICS;成功完成后标记,恢复增量模式。
@@ -10,6 +10,7 @@
import { common } from '@kit.AbilityKit'; import { common } from '@kit.AbilityKit';
import { backgroundTaskManager } from '@kit.BackgroundTasksKit'; import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { BusinessError } from '@kit.BasicServicesKit'; import { BusinessError } from '@kit.BasicServicesKit';
import process from '@ohos.process';
import { DavAccount, AccountStore, TYPE_CALDAV } from './AccountStore'; import { DavAccount, AccountStore, TYPE_CALDAV } from './AccountStore';
import { SyncEngine } from './SyncEngine'; import { SyncEngine } from './SyncEngine';
import { CardDataService } from './CardDataService'; import { CardDataService } from './CardDataService';
@@ -57,6 +58,15 @@ export class BackgroundSyncRunner {
/** 本次卡片同步申请的短时任务 requestId(0 = 未申请到) */ /** 本次卡片同步申请的短时任务 requestId(0 = 未申请到) */
private static cardTransientId: number = 0; private static cardTransientId: number = 0;
/** 当前进程 pid(取不到返回 -1)。用来判断卡片回调是否与主 App 同进程。 */
private static pidOf(): number {
try {
return process.pid;
} catch (err) {
return -1;
}
}
/** /**
* ⭐⭐ 由**服务卡片定时刷新**(`onUpdateForm`)驱动的后台同步(0.0.8)。 * ⭐⭐ 由**服务卡片定时刷新**(`onUpdateForm`)驱动的后台同步(0.0.8)。
* *
@@ -88,8 +98,16 @@ export class BackgroundSyncRunner {
// ⚠️ 配额:短时任务单日总配额 10 分钟、单次最长 3 分钟(低电量 1 分钟), // ⚠️ 配额:短时任务单日总配额 10 分钟、单次最长 3 分钟(低电量 1 分钟),
// 且**被挂起后才计时**(10 秒内跑完不消耗)→ 对我们很划算。 // 且**被挂起后才计时**(10 秒内跑完不消耗)→ 对我们很划算。
const windowMs: number = await BackgroundSyncRunner.acquireCardWindow(); const windowMs: number = await BackgroundSyncRunner.acquireCardWindow();
const budget: number = Math.min(windowMs, BackgroundSyncRunner.CARD_MAX_TOTAL_MS); let budget: number = Math.min(windowMs, BackgroundSyncRunner.CARD_MAX_TOTAL_MS);
LogUtil.write(`卡片驱动同步:开始(卡片 30 分钟定时刷新触发,可用窗口 ${Math.round(budget / 1000)} 秒)`); // ⭐ 诊断开关(preferences:card_budget_ms):>0 时**覆盖**上面算出的可用窗口。
// 用途:在真机上回答"卡片进程究竟能活多久""一次完整同步要多久",
// 从而决定卡片这条路该做多少事 —— 不必为此重新编译。
const overrideMs: number = await AppSettings.getCardBudgetMs(context);
if (overrideMs > 0) {
budget = overrideMs;
LogUtil.write(`卡片驱动同步:预算被诊断开关覆盖为 ${Math.round(budget / 1000)} 秒`);
}
LogUtil.write(`卡片驱动同步:开始(卡片 30 分钟定时刷新触发,pid=${BackgroundSyncRunner.pidOf()},可用窗口 ${Math.round(budget / 1000)} 秒)`);
// 软超时:预算内没跑完就主动放弃(幂等同步,中断不损坏数据,下轮会重试) // 软超时:预算内没跑完就主动放弃(幂等同步,中断不损坏数据,下轮会重试)
const worker: Promise<void> = const worker: Promise<void> =
BackgroundSyncRunner.runOnce(context, 'cardTick').then((): void => {}); BackgroundSyncRunner.runOnce(context, 'cardTick').then((): void => {});
@@ -164,8 +182,12 @@ export class BackgroundSyncRunner {
*/ */
static async runOnce(context: common.Context, tag: string): Promise<BackgroundSyncResult> { static async runOnce(context: common.Context, tag: string): Promise<BackgroundSyncResult> {
const r = new BackgroundSyncResult(); const r = new BackgroundSyncResult();
// ⭐ 阶段计时:后台同步被时间预算切断时,这些 `+Xms` 就是"钱花在哪了"的直接证据。
const t0: number = Date.now();
const el = (): number => Date.now() - t0;
try { try {
const accounts: DavAccount[] = await AccountStore.loadAll(context); const accounts: DavAccount[] = await AccountStore.loadAll(context);
LogUtil.write(`后台同步[${tag}] 阶段:载入账号 ${accounts.length} 个 +${el()}ms`);
if (accounts.length === 0) { if (accounts.length === 0) {
LogUtil.write(`后台同步[${tag}]:无账号,跳过`); LogUtil.write(`后台同步[${tag}]:无账号,跳过`);
return r; return r;
@@ -176,12 +198,14 @@ export class BackgroundSyncRunner {
} catch (err) { } catch (err) {
// 导入失败不影响正常同步 // 导入失败不影响正常同步
} }
LogUtil.write(`后台同步[${tag}] 阶段:系统日历导入 +${el()}ms`);
// 入站对账:把用户在系统日历里的改动读回来置 dirty(必须在同步之前) // 入站对账:把用户在系统日历里的改动读回来置 dirty(必须在同步之前)
try { try {
await MirrorInbound.reconcile(context); await MirrorInbound.reconcile(context);
} catch (err) { } catch (err) {
// 对账失败不影响正常同步 // 对账失败不影响正常同步
} }
LogUtil.write(`后台同步[${tag}] 阶段:入站对账 +${el()}ms`);
for (const acc of accounts) { for (const acc of accounts) {
if (acc.type !== TYPE_CALDAV) { if (acc.type !== TYPE_CALDAV) {
continue; continue;
@@ -196,13 +220,16 @@ export class BackgroundSyncRunner {
r.failed++; r.failed++;
LogUtil.write(`后台同步[${tag}] id=${acc.id} 失败: ${e.message}`); LogUtil.write(`后台同步[${tag}] id=${acc.id} 失败: ${e.message}`);
} }
LogUtil.write(`后台同步[${tag}] 阶段:账号 ${acc.id} 同步 +${el()}ms(ok=${r.ok} failed=${r.failed})`);
} }
// 全部账号同步成功才结束"一次性全量重拉"状态 // 全部账号同步成功才结束"一次性全量重拉"状态
if (r.attempted > 0 && r.ok === r.attempted) { if (r.attempted > 0 && r.ok === r.attempted) {
await AppSettings.markFullRefetchDone(context); await AppSettings.markFullRefetchDone(context);
} }
await SyncEngine.settleLocalEvents(context); await SyncEngine.settleLocalEvents(context);
LogUtil.write(`后台同步[${tag}] 阶段:本地收尾 +${el()}ms`);
await SyncEngine.pruneOrphanRows(context, accounts); await SyncEngine.pruneOrphanRows(context, accounts);
LogUtil.write(`后台同步[${tag}] 阶段:孤儿行清理 +${el()}ms`);
// ⚠️ 必须回写账号:syncAccount 会就地更新 acc(服务器日历本颜色、写权限等)。 // ⚠️ 必须回写账号:syncAccount 会就地更新 acc(服务器日历本颜色、写权限等)。
// 缺了这一步,后台同步刷到的颜色只留在内存里、进程退出即丢失。 // 缺了这一步,后台同步刷到的颜色只留在内存里、进程退出即丢失。
await AccountStore.saveAll(context, accounts); await AccountStore.saveAll(context, accounts);
@@ -215,6 +242,7 @@ export class BackgroundSyncRunner {
} catch (err) { } catch (err) {
// 镜像失败不影响同步 // 镜像失败不影响同步
} }
LogUtil.write(`后台同步[${tag}] 阶段:出站镜像 +${el()}ms`);
// 同步后刷新卡片 + 重建提醒,保证后台同步的成果直接可见 // 同步后刷新卡片 + 重建提醒,保证后台同步的成果直接可见
try { try {
await CardDataService.pushToAllForms(context); await CardDataService.pushToAllForms(context);
@@ -222,10 +250,10 @@ export class BackgroundSyncRunner {
} catch (err) { } catch (err) {
// 卡片/提醒刷新失败不影响同步结果 // 卡片/提醒刷新失败不影响同步结果
} }
LogUtil.write(`后台同步[${tag}] 完成:${r.ok}/${r.attempted} 个账号`); LogUtil.write(`后台同步[${tag}] 完成:${r.ok}/${r.attempted} 个账号,总耗时 ${Math.round(el() / 1000)} 秒`);
} catch (err) { } catch (err) {
const e = err as BusinessError; const e = err as BusinessError;
LogUtil.write(`后台同步[${tag}] 流程异常: ${e.message}`); LogUtil.write(`后台同步[${tag}] 流程异常: ${e.message}(已耗时 ${Math.round(el() / 1000)} 秒)`);
} }
return r; return r;
} }
+19 -6
View File
@@ -425,7 +425,22 @@ export class EventDb {
rs.close(); rs.close();
} }
const remoteKeys: string[] = []; // ⭐ 2226(2026-10-15 提速):下方逐条要判"这条远端事件本地有没有对应行"。
// 原先对每条都 `existing.find(...)` ⇒ O(n·m)(工作本 735 行 → 54 万次比较,
// 每次还要现拼 `uid_startTime_recId` 字符串),2184 行全库累计近 100 万次。
// 卡片后台同步的时间预算只有 9 秒,这笔开销吃不起 → 预先建 Map,改成 O(1) 查。
// 语义与 `existing.find(x => !x.dirty && x.uid===r.uid && x.startTime===r.startTime
// && x.recurrenceId===r.recurrenceId)` 完全一致:dirty 行不入索引、同键取**最先**出现的那行。
const keyed: Map<string, LocalEvent> = new Map<string, LocalEvent>();
for (const e of existing) {
if (e.dirty) {
continue;
}
const k: string = `${e.uid}_${e.startTime}_${e.recurrenceId}`;
if (!keyed.has(k)) {
keyed.set(k, e);
}
}
// ⭐ 同一次解析里可能出现同一资源的多条相同 VEVENT(同 uid + 同 DTSTART)。 // ⭐ 同一次解析里可能出现同一资源的多条相同 VEVENT(同 uid + 同 DTSTART)。
// 旧版用循环前的 existing 快照判重,插入后不回写 → 第二条仍判为"新增"又插一行, // 旧版用循环前的 existing 快照判重,插入后不回写 → 第二条仍判为"新增"又插一行,
// 在本地库留下 href/etag/remote_path 完全相同的冗余行(2026-09-19 实测 10 组)。 // 在本地库留下 href/etag/remote_path 完全相同的冗余行(2026-09-19 实测 10 组)。
@@ -438,10 +453,7 @@ export class EventDb {
continue; // 同一资源内的重复条目 → 只保留一条 continue; // 同一资源内的重复条目 → 只保留一条
} }
seenKeys.add(key); seenKeys.add(key);
remoteKeys.push(key); const found: LocalEvent | undefined = keyed.get(key);
const found = existing.find((x: LocalEvent): boolean =>
!x.dirty && x.uid === r.uid && x.startTime === r.startTime
&& x.recurrenceId === r.recurrenceId);
if (found === undefined) { if (found === undefined) {
// 新增 // 新增
const e = new LocalEvent(); const e = new LocalEvent();
@@ -517,7 +529,8 @@ export class EventDb {
continue; continue;
} }
const localKey: string = `${local.uid}_${local.startTime}_${local.recurrenceId}`; const localKey: string = `${local.uid}_${local.startTime}_${local.recurrenceId}`;
if (!remoteKeys.includes(localKey)) { // ⭐ 同样从 O(m) 的数组 includes 换成 Set 查询(见上方 keyed 的说明)
if (!seenKeys.has(localKey)) {
const del = new relationalStore.RdbPredicates('events'); const del = new relationalStore.RdbPredicates('events');
del.equalTo('id', local.id); del.equalTo('id', local.id);
await store.delete(del); await store.delete(del);
+20 -2
View File
@@ -82,6 +82,25 @@ export class MirrorInbound {
/* ==================== 单个日历本 ==================== */ /* ==================== 单个日历本 ==================== */
/**
* 取回该账户下已有日程。
*
* 🔴 2026-10-15 实测(DEL-AL20 / 7.0.0.109):`getEvents(undefined, 字段数组)` 在这个
* 系统版本上**必抛** `401 type error!`。而本文件原先正是这么调的 ⇒ "入站对账"对
* **每一个**日历本都失败(真机日志 12/12 全是 `401 type error!`),
* 整个"把系统日历里的改动回写 CalDAV"等于死的(还白费 12 次调用)。
* 出站镜像(SystemCalendarMirror.queryEvents)早就踩过同一个坑,改成"先无参、失败再带字段"。
* 这里沿用同一策略:无参调用返回的 Event 里 identifier 同样有值。
*/
private static async queryEvents(cal: calendarManager.Calendar): Promise<calendarManager.Event[]> {
try {
return await cal.getEvents();
} catch (err) {
LogUtil.write(`入站对账 getEvents() 失败,退回带字段:${(err as BusinessError).message}`);
return await cal.getEvents(undefined, MirrorInbound.QUERY_FIELDS);
}
}
private static async reconcileBook( private static async reconcileBook(
context: common.Context, context: common.Context,
cm: calendarManager.CalendarManager, cm: calendarManager.CalendarManager,
@@ -109,8 +128,7 @@ export class MirrorInbound {
} }
const snaps: Map<string, MirrorSnapshotRow> = await MirrorSnapshot.loadMap(context, bucket); const snaps: Map<string, MirrorSnapshotRow> = await MirrorSnapshot.loadMap(context, bucket);
const events: calendarManager.Event[] = await cal.getEvents( const events: calendarManager.Event[] = await MirrorInbound.queryEvents(cal);
undefined, MirrorInbound.QUERY_FIELDS);
// 分成两类:带我们 identifier 的(可判定改/删) vs 不是我们的(用户新建) // 分成两类:带我们 identifier 的(可判定改/删) vs 不是我们的(用户新建)
const mine: Map<string, calendarManager.Event> = new Map<string, calendarManager.Event>(); const mine: Map<string, calendarManager.Event> = new Map<string, calendarManager.Event>();
+36 -8
View File
@@ -160,12 +160,12 @@ export class SyncEngine {
// 现有行按 uid 分组(同一资源的覆盖实例共享 uid 与 etag) // 现有行按 uid 分组(同一资源的覆盖实例共享 uid 与 etag)
const existingRows: LocalEvent[] = await EventDb.queryByCalKey(context, calKey, 'event'); const existingRows: LocalEvent[] = await EventDb.queryByCalKey(context, calKey, 'event');
const byUid: Map<string, LocalEvent[]> = new Map(); const byUid: Map<string, LocalEvent[]> = new Map();
// ⭐ 再按 **etag** 分组,给下面"要不要 GET 这个资源"做兜底(见 §复用判定 注释)
const byEtag: Map<string, LocalEvent[]> = new Map();
for (const row of existingRows) { for (const row of existingRows) {
const arr: LocalEvent[] | undefined = byUid.get(row.uid); SyncEngine.groupPush(byUid, row.uid, row);
if (arr === undefined) { if (row.etag !== '') {
byUid.set(row.uid, [row]); SyncEngine.groupPush(byEtag, row.etag, row);
} else {
arr.push(row);
} }
} }
const originMatch = /https?:\/\/[^/]+/i.exec(href); const originMatch = /https?:\/\/[^/]+/i.exec(href);
@@ -174,9 +174,27 @@ export class SyncEngine {
let fetched: number = 0; let fetched: number = 0;
let parseFail: number = 0; let parseFail: number = 0;
const needFetch: RemoteItem[] = []; const needFetch: RemoteItem[] = [];
let hitUid: number = 0; // 按"资源名 = uid"命中
let hitEtag: number = 0; // 按 etag 内容哈希兜底命中
for (const it of items) { for (const it of items) {
const resUid: string = SyncEngine.resUidFromHref(it.href); const resUid: string = SyncEngine.resUidFromHref(it.href);
const group: LocalEvent[] | undefined = byUid.get(resUid); let group: LocalEvent[] | undefined = byUid.get(resUid);
if (group !== undefined) {
hitUid++;
} else if (it.etag !== '') {
// ⭐ 复用判定(2026-10-15 修复):**服务器不保证"资源文件名 = ICS 里的 UID"**。
// 家书 CalDav.php:253 用 preg_replace('/[^A-Za-z0-9._-]/', '_', $uid) 生成文件名:
// UID `movie-188@familybook` 的资源叫 `movie-188_familybook.ics`。
// 旧版只按资源名去 `byUid` 查 → 永远查不到 → 该资源**每轮都被判成"变了"**,
// 于是每次都全量 GET(实测 2184 个资源 → 单次同步 200~300 秒:卡片后台同步
// 那 9 秒窗口连一个日历本都跑不完)。
// etag 是**内容哈希**(服务端由正文 md5 得出):相等即内容一致,
// 与"资源叫什么名字"无关 —— 用它兜底既安全又与服务器实现解耦。
group = byEtag.get(it.etag);
if (group !== undefined) {
hitEtag++;
}
}
if (!fullRefetch && group !== undefined && group.length > 0 && !group[0].dirty if (!fullRefetch && group !== undefined && group.length > 0 && !group[0].dirty
&& group[0].etag === it.etag) { && group[0].etag === it.etag) {
// etag 未变:直接用本地行还原远端数据(含 reminder 等完整字段) // etag 未变:直接用本地行还原远端数据(含 reminder 等完整字段)
@@ -187,6 +205,7 @@ export class SyncEngine {
} }
needFetch.push(it); needFetch.push(it);
} }
LogUtil.write(`日历本[${i}]「${calName}」复用判定:uid 命中 ${hitUid}、etag 兜底命中 ${hitEtag}、需补拉 ${needFetch.length}(共 ${items.length})`);
// 并发 GET 补拉(每批 5 个),避免全量重拉时串行请求超时 // 并发 GET 补拉(每批 5 个),避免全量重拉时串行请求超时
const batchSize: number = 5; const batchSize: number = 5;
for (let b: number = 0; b < needFetch.length; b += batchSize) { for (let b: number = 0; b < needFetch.length; b += batchSize) {
@@ -390,6 +409,16 @@ export class SyncEngine {
} }
} }
/** Map<key, 行数组> 追加一行(没有该 key 就建数组) */
private static groupPush(m: Map<string, LocalEvent[]>, key: string, row: LocalEvent): void {
const arr: LocalEvent[] | undefined = m.get(key);
if (arr === undefined) {
m.set(key, [row]);
} else {
arr.push(row);
}
}
/** 从资源 URL 提取 UID(解析失败时的兜底) */ /** 从资源 URL 提取 UID(解析失败时的兜底) */
private static uidFromHref(href: string): string { private static uidFromHref(href: string): string {
const segs: string[] = href.split('/').filter((s: string): boolean => s !== ''); const segs: string[] = href.split('/').filter((s: string): boolean => s !== '');
@@ -401,8 +430,7 @@ export class SyncEngine {
} }
/** 从资源 URL 提取 UID(URL 解码后,用于与 DB 中 parse 出的真实 uid 匹配) */ /** 从资源 URL 提取 UID(URL 解码后,用于与 DB 中 parse 出的真实 uid 匹配) */
private static resUidFromHref(href: string): string { private static resUidFromHref(href: string): string { const segs: string[] = href.split('/').filter((s: string): boolean => s !== '');
const segs: string[] = href.split('/').filter((s: string): boolean => s !== '');
if (segs.length === 0) { if (segs.length === 0) {
return String(Date.now()); return String(Date.now());
} }
@@ -3,11 +3,21 @@
import { FormExtensionAbility, formBindingData, formProvider } from '@kit.FormKit'; import { FormExtensionAbility, formBindingData, formProvider } from '@kit.FormKit';
import { Want } from '@kit.AbilityKit'; import { Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit'; import { BusinessError } from '@kit.BasicServicesKit';
import process from '@ohos.process';
import { CardDataService, CardData } from '../common/CardDataService'; import { CardDataService, CardData } from '../common/CardDataService';
import { BackgroundSyncRunner } from '../common/BackgroundSyncRunner'; import { BackgroundSyncRunner } from '../common/BackgroundSyncRunner';
import { LogUtil } from '../common/LogUtil'; import { LogUtil } from '../common/LogUtil';
export default class EntryFormAbility extends FormExtensionAbility { export default class EntryFormAbility extends FormExtensionAbility {
/** 当前进程 pid(取不到返回 -1):用于判断卡片回调是否与主 App 同进程 */
private static pidOf(): number {
try {
return process.pid;
} catch (err) {
return -1;
}
}
/** 组装更新数据并推送到指定卡片 */ /** 组装更新数据并推送到指定卡片 */
private pushData(formId: string, dayOffset: number = 0): void { private pushData(formId: string, dayOffset: number = 0): void {
CardDataService.buildCardData(this.context, dayOffset).then((data: CardData): void => { CardDataService.buildCardData(this.context, dayOffset).then((data: CardData): void => {
@@ -41,7 +51,7 @@ export default class EntryFormAbility extends FormExtensionAbility {
onUpdateForm(formId: string): void { onUpdateForm(formId: string): void {
LogUtil.init(this.context); LogUtil.init(this.context);
LogUtil.write(`系统触发卡片更新: formId=${formId}`); LogUtil.write(`系统触发卡片更新: formId=${formId}(进程 pid=${EntryFormAbility.pidOf()})`);
CardDataService.registerForm(this.context, formId); CardDataService.registerForm(this.context, formId);
// ⭐ ① 先用**本地缓存**立刻刷新卡片(几毫秒,不依赖网络) // ⭐ ① 先用**本地缓存**立刻刷新卡片(几毫秒,不依赖网络)
// 保留用户翻到的日期(系统定时刷新不应把用户翻走的日期拽回今天) // 保留用户翻到的日期(系统定时刷新不应把用户翻走的日期拽回今天)