0.0.8 上架收尾:移除同步记录落盘 + 镜像空清单防护 + 前台默认间隔 15 分钟
三处改动(均来自 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。
This commit is contained in:
@@ -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<number> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<string, MirrorOcc[]> = new Map<string, MirrorOcc[]>();
|
||||
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<string, MirrorOcc[]>,
|
||||
winStart: number,
|
||||
winEnd: number
|
||||
): Promise<void> {
|
||||
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<string> = new Set<string>();
|
||||
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<string, calendarManager.Event> = new Map<string, calendarManager.Event>();
|
||||
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<string> = new Set<string>();
|
||||
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);
|
||||
|
||||
@@ -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 小时)+ 网络恢复补同步监听
|
||||
|
||||
@@ -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<number>('syncIntervalMinutes') ?? 1;
|
||||
const minutes: number = AppStorage.get<number>('syncIntervalMinutes') ?? 15;
|
||||
if (Date.now() - this.lastSyncTime < minutes * 60000) {
|
||||
return; // 未到设置的同步间隔
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user