0.0.8 修复:系统日历重复提醒 / 已过日程仍提醒
用户反馈:① 日程已开始/已过去,系统日历仍提醒;② 同一提前量(如提前 15 分钟) 提醒两次;③ 怀疑"打开 App 后又写一次系统日历"就多提醒一次。 诊断见《系统日历重复提醒-根因诊断.md》。用户怀疑正确,确为两个 bug。 修复一:消除双通道重复提醒(提醒两次) - 根因:镜像到系统日历后,**系统日历自己会提醒**(条目 reminderTime), 而 ReminderService 为**所有**本地日程排代理提醒、**完全不区分**该本是否已镜像 → 同一条日程、同一提前量被两个应用各弹一次。 - 改法:新增 `ReminderService.mirroredKeys()` —— 取"已镜像到系统日历"的日历本 calKey 集合(仅在「镜像开关已开 + 有系统日历权限 + 有选中本」时才非空), `refreshReminders` 与 `tickReminders` **两处都跳过**这些本,改由系统日历提醒。 - ⚠️ 保守设计:判定失败一律返回**空集**(不跳过任何本)→ 宁可多提醒,绝不漏提醒。 - ⚠️ 交互完整性:镜像开关变化时必须**立即失效缓存并重排提醒**(否则会漏提醒): · 开启镜像 → 交回系统日历负责;· 关闭镜像 → **立刻恢复 App 提醒**; · 勾选本变化、权限被系统回收 → 同样失效缓存并重排。 新增 `ReminderService.invalidateMirrorCache()`。 修复二:消除"已过去的日程仍提醒" - 根因:镜像窗口含**过去 12 个月**(windowStart 往前推 12 月),过去的日程也在系统日历里; 而 `updateEvent` 是**整条覆写**(带 reminderTime),系统在覆写时会**重新计算提醒时刻**, 时刻已过就**立刻补发**一条 → 表现为"过去了还提醒"。 - 改法:`toSystemEvent` 写入时只带**提醒时刻尚未到**的提醒(新增 `futureReminders()`)。 已过期提醒已无意义 → 不写,从根上消除覆写补发。 ⭐ 全天日程按系统口径以**当天 09:00** 为提醒基准(新增 `reminderBaseMs()`)。 - 刻意**不收缩镜像窗口**:保住历史日程在系统日历中的可见性,只去掉提醒。 修复三:让"改了提醒时间"能同步到系统日历(反向 bug) - 根因:`diffOf` 只比较 title/isAllDay/start/end/description/location, **不比较 reminderTime** → 用户改了提醒时间被判"无差异",永不触发 updateEvent → 系统日历侧提醒一直不更新。 - 改法:`diffOf` 纳入 `reminderTime` 比较,用规范化 key(排序去重)避免因顺序/重复误判, 且与修复二同口径(只比"会被写入的"提醒),不会导致每轮都重写。 验证: - 编译通过(BUILD SUCCESSFUL)。 - 离线用例全部通过: · 修复二:已过去/提醒刚过/昨天全天 → 不写提醒;未来日程/明天全天 → 保留; · 修复三:提醒改了/新增 → 触发覆写;相同集合(顺序不同、含重复)→ 不覆写。 - 手机未连接 → 未装机(待用户插线测试)。
This commit is contained in:
@@ -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<string, string> = new Map();
|
||||
private static calNameLoadedAt: number = 0;
|
||||
|
||||
/** 已镜像到系统日历的 calKey 集合缓存(避免重复排提醒),30 秒刷新一次 */
|
||||
private static mirroredCalKeys: Set<string> = new Set();
|
||||
private static mirroredLoadedAt: number = 0;
|
||||
|
||||
/**
|
||||
* ⭐⭐ 取"已镜像到系统日历"的日历本 calKey 集合(0.0.8 修复双通道重复提醒)。
|
||||
*
|
||||
* 背景:镜像到系统日历后,**系统日历自己会提醒**(条目的 reminderTime)。
|
||||
* 若 App 再为同一个本排代理提醒 → 同一条日程提醒两次。
|
||||
* 因此这里返回"该由系统日历负责提醒"的本,App 侧跳过它们。
|
||||
*
|
||||
* ⚠️ 返回空集 = 不跳过任何本(保守:宁可多提醒,不可漏提醒)。
|
||||
* 仅在「镜像开关已开 + 有系统日历权限 + 有选中本」时才真正跳过。
|
||||
*/
|
||||
private static async mirroredKeys(context: common.Context): Promise<Set<string>> {
|
||||
if (Date.now() - ReminderService.mirroredLoadedAt < 30000
|
||||
&& ReminderService.mirroredCalKeys.size > 0) {
|
||||
return ReminderService.mirroredCalKeys;
|
||||
}
|
||||
const out: Set<string> = new Set<string>();
|
||||
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<string> {
|
||||
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<string> = 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<string> = 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) {
|
||||
|
||||
Reference in New Issue
Block a user