From 942f71bfdfb0371962a3fe7bff16ea97387d9208 Mon Sep 17 00:00:00 2001 From: laoyang Date: Fri, 16 Oct 2026 19:38:21 +0800 Subject: [PATCH] =?UTF-8?q?0.0.8=20=E4=B8=8A=E6=9E=B6=E6=94=B6=E5=B0=BE?= =?UTF-8?q?=EF=BC=9A=E7=A7=BB=E9=99=A4=E5=90=8C=E6=AD=A5=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E8=90=BD=E7=9B=98=20+=20=E9=95=9C=E5=83=8F=E7=A9=BA=E6=B8=85?= =?UTF-8?q?=E5=8D=95=E9=98=B2=E6=8A=A4=20+=20=E5=89=8D=E5=8F=B0=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E9=97=B4=E9=9A=94=2015=20=E5=88=86=E9=92=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三处改动(均来自 2026-10-16 真机日志实测): 1) 移除「同步记录」落盘 · LogUtil 顶部新增 ENABLE_FILE_LOG=false(发行版只输出 console), 真机排查时改 true 即可恢复落盘,所有调用点无需改动; · init() 里顺手 unlink 历史版本遗留的 sync.log / mirror_diag.txt, 保证升级上来的设备沙箱也干净; · SystemCalendarMirror 删掉 dumpDiagnosis 整个方法(不再导出 mirror_diag.txt), 连带去掉已无引用的 fs 依赖; · SettingsPage「清理残留镜像账户」提示语由"见同步日志"改为"见下方"(清单本就在页面上)。 2) 镜像防护:系统日历清单为 0 时跳过创建 · 依据:2026-10-16 07:55:00 日志 `系统日历账户清单(共 0)` → `镜像完成:本=0 … 失败=12`,12 个 bookId 连续 `201 CreateCalendar failed!`。 · 真因:设备重启后尚未解锁(direct boot),日历数据在加密存储里读不到, getAllCalendars() 返回空列表 ⇒ createCalendar 必然失败,还可能留下半创建账户。 · 正常设备至少有一个本机日历,故"清单为 0"只可能是服务未就绪 ⇒ syncNow() 在 plan/syncBook 之前直接跳过本轮,等解锁后的下一次同步自然恢复。 3) 前台自动同步默认间隔 1 分钟 → 15 分钟 · 依据:2026-10-16 17:16–17:20 日志,5 分钟内连跑 5 轮 "12 本全量比对 + 出站镜像 + 提醒刷新",随后 `17:20:43 短时任务超时(配额用尽),后台同步将被挂起` —— 前台把短时任务配额烧光,反而拖垮了真正的后台同步通道。 · 设备 preferences 里并无 sync_interval_minutes 键 ⇒ 走的就是这个默认值。 · AppSettings.getSyncIntervalMinutes 默认值 + Index.startAutoSync 的 setOrCreate 与 ?? 兜底 + SettingsPage 的 @State 初值,四处一并改为 15。 · 用户仍可在设置页选 1/5/15/30 分钟 或 1 小时。 验证:assembleHap 与 assembleApp 均 BUILD SUCCESSFUL。 --- entry/src/main/ets/common/AppSettings.ets | 16 +- entry/src/main/ets/common/LogUtil.ets | 35 +++- .../main/ets/common/SystemCalendarMirror.ets | 164 ++---------------- .../main/ets/entryability/EntryAbility.ets | 2 +- entry/src/main/ets/pages/Index.ets | 5 +- entry/src/main/ets/pages/SettingsPage.ets | 7 +- 6 files changed, 70 insertions(+), 159 deletions(-) diff --git a/entry/src/main/ets/common/AppSettings.ets b/entry/src/main/ets/common/AppSettings.ets index 4e66e37..ccbed45 100644 --- a/entry/src/main/ets/common/AppSettings.ets +++ b/entry/src/main/ets/common/AppSettings.ets @@ -648,15 +648,23 @@ export class AppSettings { } } - /** 自动同步间隔(分钟),默认 1 分钟 */ + /** + * 自动同步间隔(分钟),默认 15 分钟。 + * + * ⭐ 0.0.8:默认值由 1 分钟改为 15 分钟。原因(真机日志 2026-10-16 17:16–17:20 实测): + * 1 分钟间隔会让「App 打开期间」几乎不间断地跑"12 个日历本全量比对 + 出站镜像 + 提醒刷新", + * 5 分钟内连跑 5 轮后触发系统提示「短时任务超时(配额用尽),后台同步将被挂起」—— + * 反而把真正的后台同步通道一起拖垮。改为 15 分钟后,前台同步仍足够及时,且不再烧配额。 + * 用户仍可在设置页自行改成 1 / 5 / 15 / 30 分钟 或 1 小时。 + */ static async getSyncIntervalMinutes(context: common.Context): Promise { try { const store: preferences.Preferences = await preferences.getPreferences(context, AppSettings.STORE); - const v: number = await store.get(AppSettings.KEY_SYNC_INTERVAL, 1) as number; - return v > 0 ? v : 1; + const v: number = await store.get(AppSettings.KEY_SYNC_INTERVAL, 15) as number; + return v > 0 ? v : 15; } catch (err) { - return 1; + return 15; } } diff --git a/entry/src/main/ets/common/LogUtil.ets b/entry/src/main/ets/common/LogUtil.ets index 474fc64..86f0f75 100644 --- a/entry/src/main/ets/common/LogUtil.ets +++ b/entry/src/main/ets/common/LogUtil.ets @@ -1,30 +1,57 @@ // entry/src/main/ets/common/LogUtil.ets -// 同步日志:写入应用 filesDir/sync.log(含时间戳),超 512KB 自动截断保留后半。 -// 设置页「同步日志」卡片可查看/复制/清空,方便真机测试时诊断同步与提醒问题。 +// 日志工具:默认**只输出到 console(hilog)**,不落盘。 +// +// ⭐ 0.0.8 起:发行版**不再写 sync.log**(原「同步记录」/诊断落盘功能已移除)。 +// 需要真机排查同步/提醒问题时,把下面的 ENABLE_FILE_LOG 改成 true 重新编译即可恢复 +// 落盘(含 512KB 自动截断保留后半),其余调用点一行都不用动。 +// 关闭状态下会在 init 时顺手删掉历史版本遗留的 sync.log 与 mirror_diag.txt, +// 保证升级上来的设备沙箱也是干净的。 import { common } from '@kit.AbilityKit'; import { fileIo as fs } from '@kit.CoreFileKit'; import { util } from '@kit.ArkTS'; import { BusinessError } from '@kit.BasicServicesKit'; export class LogUtil { + /** ⭐ 落盘开关:发行版置 false(仅 console);真机排查时改 true 恢复文件日志 */ + private static readonly ENABLE_FILE_LOG: boolean = false; + private static dir: string = ''; private static path: string = ''; private static readonly MAX_BYTES: number = 512 * 1024; static init(context: common.Context): void { try { - LogUtil.path = `${context.filesDir}/sync.log`; + LogUtil.dir = context.filesDir; + LogUtil.path = `${LogUtil.dir}/sync.log`; } catch (err) { + LogUtil.dir = ''; LogUtil.path = ''; } + if (!LogUtil.ENABLE_FILE_LOG && LogUtil.dir !== '') { + LogUtil.removeLegacyDebugFiles(); + } + } + + /** 删除历史版本遗留的调试落盘文件(sync.log / mirror_diag.txt) */ + private static removeLegacyDebugFiles(): void { + for (const name of ['sync.log', 'mirror_diag.txt']) { + try { + fs.unlinkSync(`${LogUtil.dir}/${name}`); + } catch (err) { + // 文件不存在属正常,忽略 + } + } } static logFilePath(): string { return LogUtil.path; } - /** 写一行日志:console + 文件(文件失败静默,不影响业务) */ + /** 写一行日志:console + 文件(发行版不落盘;文件失败静默,不影响业务) */ static write(msg: string): void { console.info(`SyncLog ${msg}`); + if (!LogUtil.ENABLE_FILE_LOG) { + return; + } if (LogUtil.path === '') { return; } diff --git a/entry/src/main/ets/common/SystemCalendarMirror.ets b/entry/src/main/ets/common/SystemCalendarMirror.ets index 0458727..4142f57 100644 --- a/entry/src/main/ets/common/SystemCalendarMirror.ets +++ b/entry/src/main/ets/common/SystemCalendarMirror.ets @@ -37,7 +37,6 @@ import { common, abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit'; import { BusinessError } from '@kit.BasicServicesKit'; import { calendarManager } from '@kit.CalendarKit'; -import { fileIo as fs } from '@kit.CoreFileKit'; import { AccountStore, BookPalette, DavAccount } from './AccountStore'; import { EventDb, LocalEvent } from './EventDb'; import { IcsTime, IcsUtil } from './IcsUtil'; @@ -594,16 +593,31 @@ export class SystemCalendarMirror { LogUtil.write( `镜像前清理:删除 ${purge.removed} 个孤儿镜像账户,失败 ${purge.failed} 个,清理后仍剩余 ${purge.remaining} 个`); + // ⭐ 0.0.8 防护:设备「重启后尚未解锁」时(direct boot),日历数据在加密存储里读不到, + // getAllCalendars() 会返回空列表,此时 createCalendar 必然失败——真机实测表现为 + // 12 个镜像账户连续 `201 CreateCalendar failed!`(2026-10-16 07:55),还可能留下半创建账户。 + // 正常设备至少有一个本机日历,所以「清单为 0」只可能是日历服务未就绪 ⇒ 本轮直接跳过创建, + // 等下一次同步(解锁后)自然恢复。 + let calCount: number = 0; + try { + calCount = (await cm.getAllCalendars()).length; + } catch (err) { + LogUtil.write(`镜像跳过:读不到系统日历账户列表 ${(err as BusinessError).message}`); + return res; + } + if (calCount === 0) { + LogUtil.write('镜像跳过:系统日历账户清单为 0(日历服务未就绪,如设备重启后尚未解锁),本轮不创建镜像账户'); + return res; + } + const winStart: number = SystemCalendarMirror.windowStart(); const winEnd: number = SystemCalendarMirror.windowEnd(); // ⭐ 0.0.8:读取用户所选的显示名格式(仅影响**新建**账户的名字;已有账户不在此处改名) const format: number = await AppSettings.getMirrorDisplayFormat(context); - const plans: Map = new Map(); for (const ref of refs) { try { const plan: MirrorOcc[] = await SystemCalendarMirror.planFor(context, ref, winStart, winEnd, res); - plans.set(ref.bookId, plan); await SystemCalendarMirror.syncBook(context, cm, ref, plan, winStart, winEnd, res, format); res.books++; } catch (err) { @@ -612,8 +626,7 @@ export class SystemCalendarMirror { LogUtil.write(`镜像失败 bookId=${ref.bookId}: ${e.code ?? ''} ${e.message}`); } } - // ⭐ 每次镜像后自动导出一份对照诊断,便于"系统日历里有、App 里没有"这类问题逐条比对 - await SystemCalendarMirror.dumpDiagnosis(context, cm, refs, plans, winStart, winEnd); + // ⭐ 0.0.8:不再导出 mirror_diag.txt(诊断落盘功能已随「同步记录」一并移除) LogUtil.write( `镜像完成:本=${res.books} 新增=${res.added} 更新=${res.updated} 删除=${res.deleted} 失败=${res.failed} ` + `跳过=${res.skipped}(窗口外单点=${res.winOut} 已删除=${res.markDel} 系统备份=${res.syscal})` + @@ -854,7 +867,7 @@ export class SystemCalendarMirror { /** * ⭐ 算出"某个日历本在窗口内应该有哪些发生" —— **唯一的口径来源**。 - * 实际写入(syncBook)与诊断导出(dumpDiagnosis)都用它,保证诊断看到的就是真正写进去的。 + * 实际写入(syncBook)以它为准,保证"计划"与"写入"完全一致。 */ private static async planFor( context: common.Context, @@ -1015,145 +1028,6 @@ export class SystemCalendarMirror { `窗口=${SystemCalendarMirror.fmt(winStart)}~${SystemCalendarMirror.fmt(winEnd)}`); } - /** - * ⭐ 诊断导出:把"App 侧计划写入"与"系统日历里实际存在"的完整对照写到 - * `${filesDir}/mirror_diag.txt`(可用 hdc 直接取出分析)。 - * - * 为什么需要它:用户反馈"系统日历里有、App 里没有"这种问题靠描述很难定位, - * 必须把两侧的清单原样摆在一起逐条比。每次镜像后自动重写一份。 - */ - private static async dumpDiagnosis( - context: common.Context, - cm: calendarManager.CalendarManager, - refs: BookRef[], - plans: Map, - winStart: number, - winEnd: number - ): Promise { - try { - const lines: string[] = []; - lines.push(`镜像诊断 ${new Date().toLocaleString()}`); - lines.push(`窗口 ${SystemCalendarMirror.fmt(winStart)} ~ ${SystemCalendarMirror.fmt(winEnd)} ` + - `(${winStart} ~ ${winEnd})`); - const cals: calendarManager.Calendar[] = await cm.getAllCalendars(); - // ⭐ 全设备日历账户清单:一眼看出有没有"本应用建的、但不属于当前选中本"的残留账户 - // (历史 calKey 命名的账户、重复账户)。每次镜像都导出一份,省得再靠按钮抓。 - const keepNames: Set = new Set(); - for (const r of refs) { - keepNames.add(r.sysAccountName); - } - let mineN: number = 0; - let keepN: number = 0; - lines.push(''); - lines.push(`[全设备日历账户] 共 ${cals.length} 个日历本:`); - for (const c of cals) { - let accName: string = ''; - let disp: string = ''; - let n: number = -1; - try { - accName = c.getAccount().name; - disp = c.getAccount().displayName ?? ''; - } catch (err) { - // 忽略 - } - try { - n = (await SystemCalendarMirror.queryEvents(c)).length; - } catch (err) { - // 忽略 - } - let tag: string = 'other'; - if (accName.startsWith(SystemCalendarMirror.ACCOUNT_PREFIX)) { - if (keepNames.has(accName)) { - tag = 'KEEP'; - keepN++; - } else { - tag = 'MINE(残留)'; - mineN++; - } - } - lines.push(` [${tag}] #${c.id} acc=${accName} display=${disp} 条目=${n}`); - } - lines.push(` 小结:本应用账户 ${keepN + mineN} 个(本次保留 ${keepN},残留 ${mineN})`); - for (const ref of refs) { - const plan: MirrorOcc[] | undefined = plans.get(ref.bookId); - const list: MirrorOcc[] = plan !== undefined ? plan : []; - let cal: calendarManager.Calendar | undefined = undefined; - for (const c of cals) { - if (c.getAccount().name === ref.sysAccountName) { - cal = c; - break; - } - } - lines.push(''); - lines.push(`[本] ${ref.name} | bookId=${ref.bookId} | calKey=${ref.calKey} | 账户=${ref.sysAccountName} | 账户存在=${cal !== undefined}`); - if (cal === undefined) { - continue; - } - const sysEvents: calendarManager.Event[] = await SystemCalendarMirror.queryEvents(cal); - const sysMap: Map = new Map(); - for (const ev of sysEvents) { - const id: string = ev.identifier ?? ''; - if (id !== '') { - sysMap.set(id, ev); - } - } - lines.push(` 计划写入=${list.length} 系统日历实际=${sysEvents.length}(其中带 identifier=${sysMap.size})`); - - // ① 系统日历里有、计划里没有 → 多出来的(正常会被删掉;删不掉就是残留) - const planIds: Set = new Set(); - for (const o of list) { - planIds.add(o.identifier); - } - const extra: string[] = []; - for (const id of sysMap.keys()) { - if (planIds.has(id)) { - continue; - } - const ev: calendarManager.Event | undefined = sysMap.get(id); - if (ev === undefined) { - continue; - } - extra.push(` ${SystemCalendarMirror.stamp(ev.startTime)} ${ev.title ?? ''} [${id}]`); - } - lines.push(` ① 系统多出(${extra.length}):`); - for (let i: number = 0; i < extra.length && i < 120; i++) { - lines.push(extra[i]); - } - - // ② 计划里有、系统日历没有 → 漏写 - const miss: string[] = []; - for (const o of list) { - if (!sysMap.has(o.identifier)) { - miss.push(` ${SystemCalendarMirror.stamp(o.start)} ${o.title} [${o.identifier}]`); - } - } - lines.push(` ② 漏写(${miss.length}):`); - for (let i: number = 0; i < miss.length && i < 60; i++) { - lines.push(miss[i]); - } - - // ③ 系统日历里该账户的**全部**条目(供逐条核对,最多 400 行) - lines.push(` ③ 系统日历条目(${sysEvents.length},最多列 400):`); - const sorted: calendarManager.Event[] = sysEvents.slice().sort( - (a: calendarManager.Event, b: calendarManager.Event): number => a.startTime - b.startTime); - for (let i: number = 0; i < sorted.length && i < 400; i++) { - const ev: calendarManager.Event = sorted[i]; - lines.push(` ${SystemCalendarMirror.stamp(ev.startTime)}~${SystemCalendarMirror.stamp(ev.endTime)} ` + - `${ev.isAllDay === true ? '[全天]' : ''}${ev.title ?? ''} [${ev.identifier ?? ''}]`); - } - } - const path: string = `${context.filesDir}/mirror_diag.txt`; - const text: string = lines.join('\n') + '\n'; - const file: fs.File = fs.openSync(path, - fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE | fs.OpenMode.TRUNC); - fs.writeSync(file.fd, text); - fs.closeSync(file.fd); - LogUtil.write(`镜像诊断已导出:${path}(${lines.length} 行)`); - } catch (err) { - LogUtil.write(`导出镜像诊断失败: ${(err as BusinessError).message}`); - } - } - /** 时间戳 → `MM-DD HH:mm` */ private static stamp(ts: number): string { const d: Date = new Date(ts); diff --git a/entry/src/main/ets/entryability/EntryAbility.ets b/entry/src/main/ets/entryability/EntryAbility.ets index ebf20a9..4031210 100644 --- a/entry/src/main/ets/entryability/EntryAbility.ets +++ b/entry/src/main/ets/entryability/EntryAbility.ets @@ -11,7 +11,7 @@ const DOMAIN = 0x0000; export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate'); - // 文件日志初始化:所有页面的同步日志统一写入 filesDir/sync.log + // 日志初始化:发行版只输出 console(不再落盘 sync.log),同时清理历史遗留调试文件 LogUtil.init(this.context); LogUtil.write('---- App 启动 ----'); // 后台同步:注册延迟任务兜底(2 小时)+ 网络恢复补同步监听 diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 12726dc..cdcaa7e 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -399,7 +399,8 @@ struct Index { /** 自动同步:每 15 秒检查一次,到达设置的间隔(AppStorage.syncIntervalMinutes)才真正同步 */ private startAutoSync(): void { if (this.autoSyncTimer === -1) { - AppStorage.setOrCreate('syncIntervalMinutes', 1); + // ⭐ 0.0.8:默认间隔 1 分钟 → 15 分钟(见 AppSettings.getSyncIntervalMinutes 注释) + AppStorage.setOrCreate('syncIntervalMinutes', 15); const ctx = this.getUIContext().getHostContext(); if (ctx !== undefined) { AppSettings.getSyncIntervalMinutes(ctx).then((v: number): void => { @@ -412,7 +413,7 @@ struct Index { if (tickCtx !== undefined && !this.syncing) { ReminderService.tickReminders(tickCtx as common.UIAbilityContext); } - const minutes: number = AppStorage.get('syncIntervalMinutes') ?? 1; + const minutes: number = AppStorage.get('syncIntervalMinutes') ?? 15; if (Date.now() - this.lastSyncTime < minutes * 60000) { return; // 未到设置的同步间隔 } diff --git a/entry/src/main/ets/pages/SettingsPage.ets b/entry/src/main/ets/pages/SettingsPage.ets index 3ce37aa..f112512 100644 --- a/entry/src/main/ets/pages/SettingsPage.ets +++ b/entry/src/main/ets/pages/SettingsPage.ets @@ -28,7 +28,7 @@ class BackupTarget { @Component struct SettingsPage { @State showSystem: boolean = true; - @State intervalMinutes: number = 1; + @State intervalMinutes: number = 15; // ⭐ 0.0.8:默认 15 分钟(原 1 分钟会烧光短时任务配额) @State backgroundSync: boolean = false; @State sysMode: string = 'display'; // display | backup @State backupKey: string = ''; // ⭐ 备份目标本的**稳定标识** bookId(不再用会漂移的 calKey) @@ -604,7 +604,7 @@ struct SettingsPage { /** * 「清理残留镜像账户(诊断)」按钮: - * ① 把系统日历里**全部**账户(含 id / name / displayName / type)写进同步日志; + * ① 读系统日历里**全部**账户(含 id / name / displayName / type)并显示在下方清单里; * ② 把"我们建的、但不属于当前选中本"的账户全部删掉(逐个独立 try,失败也继续); * ③ Toast 汇总成功/失败/残留数量,残留账户名直接显示出来。 */ @@ -623,7 +623,8 @@ struct SettingsPage { if (r.remaining > 0) { msg += `;仍残留 ${r.remaining} 个:${r.remainingNames.join(' , ')}`; } - msg += '(完整账户清单见同步日志)'; + // ⭐ 0.0.8:不再写同步日志 → 提示语改为指向页面下方那份清单(accountDump) + msg += '(完整账户清单见下方)'; this.getUIContext().getPromptAction().showToast({ message: msg, duration: 6000 }); } catch (err) { const e = err as BusinessError;