diff --git a/entry/src/main/ets/common/ReminderService.ets b/entry/src/main/ets/common/ReminderService.ets index 5248c51..5557c0d 100644 --- a/entry/src/main/ets/common/ReminderService.ets +++ b/entry/src/main/ets/common/ReminderService.ets @@ -12,6 +12,7 @@ import { BusinessError } from '@kit.BasicServicesKit'; import { EventDb, LocalEvent } from './EventDb'; import { AppSettings } from './AppSettings'; import { AccountStore } from './AccountStore'; +import { SystemCalendarMirror, BookRef } from './SystemCalendarMirror'; import { LogUtil } from './LogUtil'; export class ReminderService { @@ -26,6 +27,50 @@ export class ReminderService { private static calNameCache: Map = new Map(); private static calNameLoadedAt: number = 0; + /** 已镜像到系统日历的 calKey 集合缓存(避免重复排提醒),30 秒刷新一次 */ + private static mirroredCalKeys: Set = new Set(); + private static mirroredLoadedAt: number = 0; + + /** + * ⭐⭐ 取"已镜像到系统日历"的日历本 calKey 集合(0.0.8 修复双通道重复提醒)。 + * + * 背景:镜像到系统日历后,**系统日历自己会提醒**(条目的 reminderTime)。 + * 若 App 再为同一个本排代理提醒 → 同一条日程提醒两次。 + * 因此这里返回"该由系统日历负责提醒"的本,App 侧跳过它们。 + * + * ⚠️ 返回空集 = 不跳过任何本(保守:宁可多提醒,不可漏提醒)。 + * 仅在「镜像开关已开 + 有系统日历权限 + 有选中本」时才真正跳过。 + */ + private static async mirroredKeys(context: common.Context): Promise> { + if (Date.now() - ReminderService.mirroredLoadedAt < 30000 + && ReminderService.mirroredCalKeys.size > 0) { + return ReminderService.mirroredCalKeys; + } + const out: Set = new Set(); + try { + const on: boolean = await AppSettings.getMirrorEnabled(context); + if (on && await SystemCalendarMirror.hasPermission()) { + const refs: BookRef[] = await SystemCalendarMirror.selectedBookRefs(context); + for (const r of refs) { + if (r.calKey !== '') { + out.add(r.calKey); + } + } + } + } catch (err) { + // 判定失败 → 返回空集(不跳过,避免漏提醒) + } + ReminderService.mirroredCalKeys = out; + ReminderService.mirroredLoadedAt = Date.now(); + return out; + } + + /** 镜像开关变化后调用,让缓存立即失效(如用户关闭镜像 → 立刻恢复 App 提醒) */ + static invalidateMirrorCache(): void { + ReminderService.mirroredLoadedAt = 0; + ReminderService.mirroredCalKeys = new Set(); + } + /** 日历本名称(通知文案用):缓存 60 秒,'local' = 本机日历 */ private static async calName(context: common.Context, calKey: string): Promise { if (Date.now() - ReminderService.calNameLoadedAt > 60000 @@ -131,10 +176,18 @@ export class ReminderService { await EventDb.queryRemindable(context, now, now + ReminderService.WINDOW); // 提醒静音的日历本(如别人分享的本):其中所有日程都不再提醒 const muted: string[] = await AppSettings.getMutedReminderKeys(context); + // ⭐⭐ 已镜像到系统日历的本:由系统日历负责提醒,App 不再重复排(消除"提醒两次") + const mirrored: Set = await ReminderService.mirroredKeys(context); + if (mirrored.size > 0) { + LogUtil.write(`提醒跳过 ${mirrored.size} 个已镜像到系统日历的日历本(交由系统日历提醒)`); + } for (const e of rows) { if (muted.includes(e.calKey)) { continue; } + if (mirrored.has(e.calKey)) { + continue; // ⭐ 已镜像 → 系统日历会提醒,这里跳过避免重复 + } if (published >= ReminderService.MAX_REMINDERS) { break; } @@ -211,11 +264,16 @@ export class ReminderService { const rows: LocalEvent[] = await EventDb.queryRemindable(context, now, now + ReminderService.WINDOW); const muted: string[] = await AppSettings.getMutedReminderKeys(context); + // ⭐⭐ 已镜像到系统日历的本:由系统日历负责提醒,App 侧不再补发(消除"提醒两次") + const mirrored: Set = await ReminderService.mirroredKeys(context); let fired: number = 0; for (const e of rows) { if (muted.includes(e.calKey)) { continue; } + if (mirrored.has(e.calKey)) { + continue; // ⭐ 已镜像 → 交由系统日历提醒 + } const rems: number[] = e.reminders.length > 0 ? e.reminders : (e.reminder > 0 ? [e.reminder] : []); for (const rem of rems) { diff --git a/entry/src/main/ets/common/SystemCalendarMirror.ets b/entry/src/main/ets/common/SystemCalendarMirror.ets index 9da9816..2b50ff4 100644 --- a/entry/src/main/ets/common/SystemCalendarMirror.ets +++ b/entry/src/main/ets/common/SystemCalendarMirror.ets @@ -1268,14 +1268,62 @@ export class SystemCalendarMirror { ev.location = { location: e.location }; } const reminders: number[] = SystemCalendarMirror.remindersOf(e); - if (reminders.length > 0) { - ev.reminderTime = reminders; + // ⭐⭐ 只写"提醒时刻尚未到"的提醒(0.0.8 修复"已过日程仍提醒")。 + // 原因:写进系统日历的 reminderTime 一旦其时刻已过,系统会在**任何一次覆写**时 + // 立即补发一条通知 → 表现为"日程都过去了还提醒"。这类提醒已无意义,索性不写。 + const future: number[] = SystemCalendarMirror.futureReminders( + reminders, startTs, e.isAllDay, Date.now()); + if (future.length > 0) { + ev.reminderTime = future; } // ⚠️ 这里**故意不设 recurrenceRule**:重复日程由我们自己按窗口展开成多条实例, // 交给系统日历展开会因 RecurrenceRule 表达不全而出现错误的重复(09-19 事故)。 return ev; } + /** + * 提醒时刻的"基准时刻"(系统口径): + * - 普通日程:以**开始时刻**为基准(提前 N 分钟 = start - N); + * - 全天日程:系统文档规定以**当日 09:00**为基准(提前 N 分钟 = 当天 09:00 - N)。 + */ + private static reminderBaseMs(startTs: number, isAllDay: boolean): number { + if (isAllDay) { + const d: Date = new Date(startTs); + d.setHours(9, 0, 0, 0); + return d.getTime(); + } + return startTs; + } + + /** 过滤出"提醒时刻还没到"的提醒(用于避免写入已过期的提醒而触发补发) */ + private static futureReminders( + reminders: number[], + startTs: number, + isAllDay: boolean, + now: number + ): number[] { + const base: number = SystemCalendarMirror.reminderBaseMs(startTs, isAllDay); + const kept: number[] = []; + for (const r of reminders) { + if (r > 0 && base - r * 60000 > now) { + kept.push(r); + } + } + return kept; + } + + /** 提醒数组的规范化字符串(排序去重后拼接),用于比较 */ + private static reminderKey(reminders: number[]): string { + const s: number[] = reminders.slice().sort((a: number, b: number): number => a - b); + const out: string[] = []; + for (const n of s) { + if (out.indexOf(String(n)) < 0) { + out.push(String(n)); + } + } + return out.join(','); + } + /** 本地时区当天 0 点的时间戳 */ private static dayStart(ts: number): number { const d: Date = new Date(ts); @@ -1338,6 +1386,15 @@ export class SystemCalendarMirror { if ((cur.location?.location ?? '') !== (want.location?.location ?? '')) { return 'location'; } + // ⭐ 0.0.8:提醒也要比较 —— 否则用户改了提醒时间,系统日历侧**永远不会更新** + // (旧版 diffOf 不比 reminderTime,改动被判定为"无差异" → 不触发 updateEvent)。 + // ⚠️ 写入侧已过滤掉"已过期"的提醒(见 toSystemEvent / futureReminders), + // 所以这里的比较也用同一口径,避免"每轮都因提醒不同而重写"。 + const curRem: number[] = cur.reminderTime ?? []; + const wantRem: number[] = want.reminderTime ?? []; + if (SystemCalendarMirror.reminderKey(curRem) !== SystemCalendarMirror.reminderKey(wantRem)) { + return `reminder ${SystemCalendarMirror.reminderKey(curRem) || '(无)'} != ${SystemCalendarMirror.reminderKey(wantRem) || '(无)'}`; + } return ''; } diff --git a/entry/src/main/ets/pages/SettingsPage.ets b/entry/src/main/ets/pages/SettingsPage.ets index a0fe9a4..3ce37aa 100644 --- a/entry/src/main/ets/pages/SettingsPage.ets +++ b/entry/src/main/ets/pages/SettingsPage.ets @@ -110,6 +110,8 @@ struct SettingsPage { if (!ok && this.context !== undefined) { this.mirrorEnabled = false; AppSettings.setMirrorEnabled(this.context, false); + // 权限被回收 → 镜像失效 → 提醒应交回 App 负责 + ReminderService.invalidateMirrorCache(); } }); } @@ -518,13 +520,19 @@ struct SettingsPage { } this.mirrorEnabled = isOn; await AppSettings.setMirrorEnabled(this.context, isOn); + // ⭐ 镜像开关变化 → 立即失效提醒侧的"已镜像本"缓存,并重排提醒 + // (开启:这些本改由系统日历提醒,App 不再重复;关闭:必须立刻恢复 App 提醒,避免漏提醒) + ReminderService.invalidateMirrorCache(); if (isOn) { this.getUIContext().getPromptAction().showToast({ message: '已开启镜像:勾选要同步的日历本后点「立即执行镜像」' }); + await ReminderService.refreshReminders(this.context as common.UIAbilityContext); return; } const n: number = await SystemCalendarMirror.removeAll(this.context); + // 关闭镜像后,这些本的提醒要交回 App 负责 + await ReminderService.refreshReminders(this.context as common.UIAbilityContext); this.getUIContext().getPromptAction().showToast({ message: n > 0 ? `已关闭镜像,并清除系统日历中的 ${n} 个镜像账户` : '已关闭镜像' }); @@ -545,6 +553,9 @@ struct SettingsPage { return; } await AppSettings.setMirrorBookIds(this.context, this.mirrorKeys); + // ⭐ 勾选本变化 → 提醒侧"已镜像本"集合变化:失效缓存并重排提醒 + ReminderService.invalidateMirrorCache(); + await ReminderService.refreshReminders(this.context as common.UIAbilityContext); } /**