【时光清单|03】HarmonyOS ArkTS 提醒服务实战:计算触发时间并防止重复注册

【时光清单|03】HarmonyOS ArkTS 提醒服务实战:计算触发时间并防止重复注册 【时光清单03】HarmonyOS ArkTS 提醒服务实战计算触发时间并防止重复注册提醒功能最危险的错觉是“通知成功弹出一次就等于每日提醒已经完成”。实际工程里通知权限、内容筛选、触发时间、系统调度、重复注册和用户关闭提醒是六件不同的事。把它们揉进一个按钮回调测试当天往往正常过一天却可能不再触发或者每次进入设置页都多注册一条相同任务。时光清单的真实源码已经实现了通知权限请求、三天内事项筛选、提醒文案拼装和基础文本通知发布。设置页在用户点击“通知提醒”后读取纪念日列表并立即调用sendDailyReminder()。但项目中没有后台任务或日程注册代码sendDailyReminder这个名字描述的是产品意图当前行为仍是“立即筛选并发布一次”。本文基于这条真实链路先把现有能力说清楚再设计可按项目目标 SDK 落地的提醒计划层用纯函数计算下一触发时间用注册指纹识别同一计划用持久化状态保证幂等用 Notification Kit 只承担通知展示。涉及系统后台调度时应按目标 SDK 的官方后台任务能力和审核规则接入不能把普通publish()当成定时器。本文解决五个具体问题区分“通知授权”“立即发布”和“未来触发”的能力边界。复核三天窗口、置顶无关排序和提醒文案的真实算法。计算下一个本地触发时刻处理今天已过、月末和系统时间变化。用稳定指纹防止相同计划重复注册并允许配置变化后替换。给出权限拒绝、空列表、重复调用、关闭提醒和重启恢复的验证矩阵。本文唯一标记CSDN-SERIES:ALL-163202226阅读前先划清证据边界这篇文章同时包含“当前事实”和“建议实现”两者不能混写。当前事实来自本地项目源码的静态复核设置页的按钮回调请求通知权限读取纪念日数据再调用ReminderService.sendDailyReminder()该方法只筛选距离今天零到三天的事项并立即提交基础文本通知。工程搜索没有发现提醒计划注册、系统后台调度、计划指纹、任务 ID、下一触发时间或当日执行键的持久化实现。因此源码能够证明的是“用户点击后立即尝试发布一次”不能证明“系统每天自动触发”。历史证据也必须单列。项目错误记录里存在与其他功能有关的历史构建成功信息但没有提醒调度的注册、重启恢复、跨日触发或真机回归记录。那些旧构建记录不能替代本轮提醒功能的编译、安装和设备验证更不能反推后台计划已经可用。本文不会把历史结果写成当前结果。后文的 Planner、Registry、计划指纹、自然日执行键和任务状态都是针对真实缺口给出的建议实现。它们说明如何设计与验证不代表仓库已经具备这些类或已经成功注册系统任务。具体后台能力仍需依据目标 SDK 官方文档选型并在实际设备上完成注册、触发、重启、权限变化和关闭清理测试。本次复核的核心来源如下文件名用于让结论可以回到代码定位ProfileView.ets 设置页按钮入口与用户提示 ReminderService.ets 权限请求、三天筛选、通知发布与取消 Anniversary.ets 纪念日字段及重复类型 module.json5 当前模块设备声明 form_config.json 卡片更新时间配置不能当作通知调度证据证据强度也要分级代码阅读能证明调用关系和缺失边界纯函数测试能证明时间算法本地构建只能证明当前配置下可编译只有系统注册回读、真实触发日志和设备通知结果才能证明提醒计划实际生效。本轮没有运行新的工程构建也没有执行真机触发所以相关结果均保持“待验证”。一、真实调用链授权成功后立即发布设置页中的handleNotificationPermission()是当前唯一入口private async handleNotificationPermission(): Promisevoid { try { const granted await ReminderService.requestPermission(); if (granted) { const repo AnniversaryRepository.getInstance(); const items await repo.getAll(); await ReminderService.sendDailyReminder(items); promptAction.showToast({ message: 通知提醒已开启, duration: 2000 }); } else { promptAction.showToast({ message: 请在系统设置中允许通知权限, duration: 2000 }); } } catch (_e) { promptAction.showToast({ message: 通知设置失败请重试, duration: 2000 }); } }这条链路依次做了三件事调用系统界面请求通知授权。从AnniversaryRepository取得统一排序后的纪念日列表。立即筛选近期事项并发布一条通知。源码里没有保存“提醒已开启”配置没有计算下次执行时间也没有注册后台任务。因而当前 Toast 的“已开启”比真实行为更强它容易让用户理解为以后每天都会自动提醒。工程上应把反馈改成“通知权限已开启”或“已发送测试提醒”直到调度注册也成功后再显示“每日提醒已开启”。二、权限请求只回答一次交互结果ReminderService.requestPermission()封装了Notification Kit的授权请求static async requestPermission(): Promiseboolean { try { await notificationManager.requestEnableNotification(); return true; } catch (e) { hilog.warn( 0x0000, TAG, requestPermission failed: ${JSON.stringify(e)} ); return false; } }返回true表示这次调用没有抛出异常但业务层还应确认目标 SDK 下的权限查询语义用户可能此前已授权、拒绝或者系统设置发生变化。权限状态不能永久缓存在一个内存布尔值中更不能因为曾经授权就跳过后续发布异常处理。权限与计划状态应该分开状态来源含义通知可用Notification Kit 查询或请求结果系统允许应用展示通知用户启用每日提醒本地 Preferences用户希望创建提醒计划计划已注册调度层持久化状态当前配置对应的任务已建立最近发布结果Notification Kit 调用结果某次通知是否提交成功用户关闭系统通知后本地“每日提醒开关”可以保留但页面应显示权限不可用并提供前往设置的清晰路径。反过来系统允许通知也不代表用户同意应用每天注册提醒。三、真实筛选算法只选未来三天内的事项当前sendDailyReminder()复用了calcDaysRemaining()筛选0 days 3的记录再按目标时间升序const upcoming items .filter((item: Anniversary) { const days calcDaysRemaining(item.targetDate); return days 0 days 3; }) .sort((a: Anniversary, b: Anniversary) a.targetDate - b.targetDate ); if (upcoming.length 0) return;因此已经过去的事项不会提醒四天及以后也不会提醒。置顶状态不会影响提醒顺序最早到来的事项才是主文案对象。这个决定是合理的置顶属于列表展示偏好不应该覆盖时间紧迫度。calcDaysRemaining()会把当前时间和目标时间都归一到本地零点export function calcDaysRemaining( targetDate: number ): number { const now new Date(); const today new Date( now.getFullYear(), now.getMonth(), now.getDate() ).getTime(); const target new Date(targetDate); const targetDay new Date( target.getFullYear(), target.getMonth(), target.getDate() ).getTime(); return Math.ceil( (targetDay - today) / (1000 * 60 * 60 * 24) ); }在中国大陆本地时区这能避免目标时间的小时和分钟影响“剩余几天”。若应用扩展到使用夏令时的地区固定 24 小时除法仍需边界测试更稳的日期算法可使用本地日期序号或逐日移动。四、文案聚合一条通知承载多个临近事项源码只取最早事项生成主句并在有更多事项时追加数量const top upcoming[0]; const days calcDaysRemaining(top.targetDate); let text: string; if (days 0) { text 今天是「${top.title}」的日子; } else if (days 1) { text 明天就是「${top.title}」啦; } else { text 「${top.title}」还有 ${days} 天; } if (upcoming.length 1) { text 还有 ${upcoming.length - 1} 项即将到来; }聚合而不是逐条发布有两个好处减少通知打扰也避免同一批事项生成多个系统通知。需要补上的边界是标题长度和隐私显示。纪念日标题可能包含人名或私人事件在锁屏通知中是否完整展示应由产品隐私策略决定。可以把内容生成提取为纯函数便于测试export interface ReminderContent { title: string; text: string; } export function buildReminderContent( upcoming: Anniversary[] ): ReminderContent | undefined { if (upcoming.length 0) return undefined; const top upcoming[0]; const days calcDaysRemaining(top.targetDate); const first days 0 ? 今天是「${top.title}」的日子 : days 1 ? 明天就是「${top.title}」啦 : 「${top.title}」还有 ${days} 天; const suffix upcoming.length 1 ? 还有 ${upcoming.length - 1} 项即将到来 : ; return { title: 时光清单提醒, text: first suffix }; }纯函数不依赖系统通知权限可以用固定日期和构造数据验证所有文案分支。五、publish 只是展示请求不负责未来调度真实发布方法创建基础文本通知固定使用 ID1001const request: notificationManager.NotificationRequest { id: 1001, content: { notificationContentType: notificationManager.ContentType .NOTIFICATION_CONTENT_BASIC_TEXT, normal: { title, text } }, notificationSlotType: notificationManager.SlotType .SERVICE_INFORMATION }; await notificationManager.publish(request);这里没有触发时间字段也没有后台调度对象因此调用发生时就提交通知。固定 ID 有利于识别应用自己的提醒通知但不同系统版本对同 ID 再发布的展示效果仍应真机验证不能把它直接当作“计划去重机制”。更清晰的职责拆分是ReminderPlanner 计算什么时候应该运行 ReminderRegistry 判断计划是否已注册、负责替换或取消 ReminderContent 筛选事项并生成文案 ReminderPublisher 调用 Notification Kit 展示通知这样即使调度能力因设备、权限或系统策略变化内容算法和通知发布仍能单独测试。当前ReminderService可以逐步拆成这些窄接口而不是一次大改。六、下一触发时间今天未过就今天已过就明天假设产品允许用户选择每天09:00提醒计划层需要从当前本地时间计算下一个触发时间。不要直接写“当前时间加 24 小时”因为跨月、跨年和时区变化都应由日历字段处理。export interface DailyReminderTime { hour: number; minute: number; } export function nextDailyTrigger( now: Date, time: DailyReminderTime ): number { const candidate new Date( now.getFullYear(), now.getMonth(), now.getDate(), time.hour, time.minute, 0, 0 ); if (candidate.getTime() now.getTime()) { candidate.setDate(candidate.getDate() 1); } return candidate.getTime(); }边界定义必须明确当前时间早于 09:00返回今天 09:00。当前时间正好等于 09:00使用后返回明天避免同一时刻重复注册。当前时间晚于 09:00返回明天 09:00。月末和年末由setDate()自动滚动。如果产品允许“开启后立即发送一条测试通知”它应该是独立命令不要复用每日计划的触发计算。否则用户下午开启 09:00 提醒时既收到一条即时通知又可能因错误注册再次收到。七、注册指纹防重复的关键不是一个布尔值“已经注册”不能只保存true。当用户把时间从 09:00 改成 20:00、关闭某类提醒或应用升级了计划版本旧计划必须被替换。可以为计划生成稳定指纹export interface ReminderPlan { hour: number; minute: number; windowDays: number; enabled: boolean; version: number; } export function planFingerprint( plan: ReminderPlan ): string { return [ plan.version, plan.enabled ? on : off, plan.hour, plan.minute, plan.windowDays ].join(:); }示例指纹2:on:9:0:3表示第二版计划、启用、每天 09:00、关注三天窗口。注册状态至少保存export interface ReminderRegistration { fingerprint: string; schedulerTaskId: string; nextTriggerAt: number; registeredAt: number; }执行注册时先比较当前指纹没有旧状态创建计划并保存注册信息。指纹相同且任务仍有效直接返回already_registered。指纹不同取消旧任务创建新任务再原子更新状态。用户关闭取消旧任务并清除注册状态。这才是业务层面的去重。通知请求的id、后台任务的任务 ID 和计划指纹是三个不同标识不要互相替代。八、幂等注册流程先比较再替换最后落盘注册器可以通过窄接口隔离具体平台调度 APIexport interface ReminderScheduler { register(triggerAt: number): Promisestring; cancel(taskId: string): Promisevoid; exists(taskId: string): Promiseboolean; } export type RegisterResult created | replaced | already_registered | disabled;核心流程如下async function ensureReminderPlan( plan: ReminderPlan, old: ReminderRegistration | undefined, scheduler: ReminderScheduler, now: Date ): PromiseRegisterResult { if (!plan.enabled) { if (old) await scheduler.cancel(old.schedulerTaskId); return disabled; } const fingerprint planFingerprint(plan); if (old old.fingerprint fingerprint await scheduler.exists(old.schedulerTaskId)) { return already_registered; } if (old) { await scheduler.cancel(old.schedulerTaskId); } const triggerAt nextDailyTrigger(now, plan); const taskId await scheduler.register(triggerAt); // 只有 register 成功后才持久化新的 Registration。 await saveRegistration({ fingerprint, schedulerTaskId: taskId, nextTriggerAt: triggerAt, registeredAt: Date.now() }); return old ? replaced : created; }实际接入时ReminderScheduler必须使用目标 HarmonyOS SDK 官方支持的后台任务或提醒能力并核对权限、触发精度、设备限制和 AppGallery 审核要求。普通应用不应通过常驻进程、无限定时器或隐藏保活绕过系统调度这既不稳定也可能触碰后台行为审核边界。九、计划触发后还要再次计算内容不要在注册 09:00 计划时就把当前通知正文永久保存。纪念日可能在触发前被新增、修改或删除。正确顺序是在计划真正被唤起时读取最新纪念日列表。以触发时刻重新计算剩余天数。筛选三天窗口并排序。没有近期事项就不发布。生成最新文案并调用 Notification Kit。记录本次执行日期避免同一自然日重复执行。可以增加每日执行键export function localDateKey(date: Date): string { const y date.getFullYear(); const m String(date.getMonth() 1).padStart(2, 0); const d String(date.getDate()).padStart(2, 0); return ${y}-${m}-${d}; } export function executionKey( planFingerprint: string, date: Date ): string { return ${planFingerprint}${localDateKey(date)}; }注册去重解决“不要创建两条同样计划”执行去重解决“同一计划今天不要发布两次”。应用重启、调度重试或多个入口同时触发时两层去重都需要。十、取消边界cancelAll 可能影响其他通知真实源码提供static async cancelAll(): Promisevoid { try { await notificationManager.cancelAll(); } catch (e) { hilog.warn( 0x0000, TAG, cancelAll failed: ${JSON.stringify(e)} ); } }对只有一种通知的早期应用cancelAll()很直接。但如果以后加入习惯打卡提醒、备份完成通知或下载状态它会一起取消。更稳的设计是保存各业务通知 ID并按业务取消关闭“纪念日提醒”时还要取消调度任务而不仅是清除当前通知栏。关闭流程应当是本地开关先进入“处理中”避免连续点击。调度层取消已注册任务。通知层按业务 ID 取消当前展示。清除ReminderRegistration。保存enabledfalse。页面显示关闭成功或明确失败。如果取消调度失败不要直接把本地状态改成关闭成功否则后台任务可能继续执行用户却看不到可关闭入口。十一、异常传播不要记录失败后仍提示成功当前publish()捕获异常后只写hilog返回类型仍是Promisevoidtry { await notificationManager.publish(request); hilog.info(0x0000, TAG, Notification published); } catch (e) { hilog.error( 0x0000, TAG, publish failed: ${JSON.stringify(e)} ); }上层handleNotificationPermission()因而无法知道发布失败仍可能显示“通知提醒已开启”。可将结果改为判别联合export type PublishResult | { ok: true } | { ok: false; reason: string }; static async publish( title: string, text: string ): PromisePublishResult { try { await notificationManager.publish( createNotificationRequest(title, text) ); return { ok: true }; } catch (error) { hilog.error(0x0000, TAG, publish failed); return { ok: false, reason: notification_publish_failed }; } }日志里也应避免输出可能包含私密标题的完整对象。用户可见提示只说明操作失败和恢复方式不暴露内部错误详情。十二、持久化状态配置、注册与执行记录分开Preferences 适合保存少量提醒配置。建议不要把所有信息塞进一个enabled键而是保存版本化结构export interface ReminderState { schemaVersion: 1; plan: ReminderPlan; registration?: ReminderRegistration; lastExecutionKey?: string; updatedAt: number; }各字段职责清晰字段何时更新用途plan用户修改提醒配置还原页面和计算指纹registration系统任务注册成功后判断是否需要重新注册lastExecutionKey某日通知成功发布后防止当日重复发布updatedAt任意配置变化备份、迁移与冲突判断写入顺序很重要。系统注册失败时不要提前保存新的registration通知发布失败时不要提前保存lastExecutionKey否则当天重试会被错误跳过。十三、真机验证矩阵提醒功能必须在真机上验证权限和系统行为模拟器或代码阅读只能覆盖纯算法部分。首次请求通知权限允许后发送测试通知标题和正文正确。首次拒绝权限页面不显示“提醒已开启”并提供恢复路径。系统设置中关闭通知后返回应用页面能识别不可用状态。近期列表为空时不发布通知也不把“无通知”当成异常。今天、明天、两天、三天分别生成正确文案四天后不进入列表。多个事项同时临近时只发布一条聚合通知。同一计划连续调用ensureReminderPlan()第二次返回already_registered。修改提醒时间后旧任务被取消新任务只保留一条。关闭提醒后调度任务、展示通知和注册状态都清理。应用重启后读取注册状态不重复创建相同任务。系统时间跨过设定时刻下一触发时间滚动到次日。月末、年末和闰日的下一触发时间正确。同一自然日调度重试两次只成功发布一次。修改或删除纪念日后触发时使用最新内容。不同设备版本上验证固定通知 ID 的实际替换或并存行为。本地单元测试适合覆盖时间计算、指纹和文案真机测试负责权限弹窗、通知展示、后台调度精度和系统设置变化。两者不能互相替代。十四、常见故障与排查顺序现象常见原因修复方向点击开启后只提醒一次只调用了publish()接入官方调度层并保存注册状态每次进入设置都多一条计划没有计划指纹注册前比较指纹与任务有效性修改时间后新旧时刻都触发只新增未取消指纹变化时先取消旧任务Toast 成功但没有通知publish()吞掉异常向上返回发布结果当天收到两次相同提醒只有注册去重增加自然日执行键关闭后仍继续提醒只调用cancelAll()同时取消调度任务并清注册状态下午开启 09:00 立即又注册今天触发比较边界错误候选时刻 now时滚到明天提醒内容还是旧标题注册时固化正文触发时重新读取仓库排查时按“用户配置、系统权限、注册状态、触发时间、执行键、通知发布”六层逐步确认。看到通知没出现时不要第一步就重复注册否则原本的单一故障会变成重复任务问题。十五、重复日期必须先换算下一次发生日Anniversary模型已经声明了不重复、每年重复和每月重复等类型但当前sendDailyReminder()直接把记录中的targetDate交给calcDaysRemaining()。这意味着静态源码只能证明原始日期与今天之间的差值计算不能证明每年或每月重复事项会自动换算到下一次发生日。若目标日期已经过去直接相减还可能把本应在下个周期提醒的事项排除在零到三天窗口之外。建议把“下一次发生日”做成内容筛选之前的纯函数并且让输入、输出和异常边界清晰。下面只是接口与控制流示意不是当前源码type RepeatMode none | yearly | monthly; interface OccurrenceInput { targetDate: Date; repeatMode: RepeatMode; now: Date; } function resolveNextOccurrence(input: OccurrenceInput): Date | undefined { // 建议实现按本地自然日换算再处理月末、闰日和已过时刻。 // none 且日期已过时返回 undefined重复事项返回不早于今天的下一次日期。 return undefined; }每月重复必须定义“31 日遇到短月”的产品规则是落在当月最后一天还是跳过该月每年重复必须定义 2 月 29 日在平年的处理方式。这里不存在天然唯一答案代码不能替产品做决定。还要统一时区和自然日归一化方式避免一部分逻辑用本地时间另一部分用 UTC最终在午夜附近产生前后一天的偏差。纯函数测试至少覆盖月底、年底、闰年、夏令时环境和用户手动修改系统时间后的重新计算。十六、注册幂等与执行幂等是两把锁计划指纹解决的是“相同配置不要重复注册”自然日执行键解决的是“同一计划当天不要重复发布”。两者保护的阶段不同不能只保留其中一个。当前源码里没有这两种状态因此后面的代码仍属于建议方案。指纹输入应只包含会改变任务语义的规范化字段。对象序列化前先固定字段顺序与时间格式避免同一配置因为属性顺序或默认值表达不同而得到两个指纹interface FingerprintInput { enabled: boolean; hour: number; minute: number; windowDays: number; schemaVersion: number; } function normalizePlan(plan: ReminderPlan): FingerprintInput { return { enabled: plan.enabled, hour: plan.hour, minute: plan.minute, windowDays: plan.windowDays, schemaVersion: 1 }; }执行键则应关联计划和本地自然日。只有通知发布确认成功后才能保存该键若在发布之前保存进程中断或发布失败会让当天后续重试被错误跳过function buildExecutionKey(fingerprint: string, now: Date): string { const year now.getFullYear(); const month String(now.getMonth() 1).padStart(2, 0); const day String(now.getDate()).padStart(2, 0); return ${fingerprint}:${year}-${month}-${day}; }如果系统可能并发唤起两个执行实例仅仅“先读键、后写键”仍有竞态两个实例都可能读到空值然后各发布一次。最终实现需要根据所用持久化能力选择串行队列、互斥边界或原子状态转换并在设备日志中验证并发路径。本文没有把某一种同步手段写成既成事实因为当前工程还没有该执行器也没有并发触发证据。十七、注册与落盘顺序决定状态是否可信提醒状态不是页面开关的镜像而是系统任务结果的本地记录。建议注册流程先读取旧状态并验证旧任务再决定复用、替换或新建。新任务注册成功后才能保存任务 ID、指纹和下一触发时间保存失败时要留下可诊断结果不能直接提示“已开启”。可以让服务向页面返回判别联合使成功、无变化和失败都具有明确含义type EnsurePlanResult | { ok: true; action: registered; taskId: string } | { ok: true; action: already_registered; taskId: string } | { ok: false; stage: permission | cancel | register | persist };配置变化时“先取消旧任务再注册新任务”和“先注册新任务再取消旧任务”各有风险。前者在新注册失败时会丢失原任务后者在旧取消失败时可能短暂并存。工程应结合系统接口是否支持更新、任务数量限制和可查询性选择策略并在返回值中记录失败阶段。无论选择哪种顺序都不能在系统操作失败后把本地状态写成成功也不能仅凭本地任务 ID 推断系统任务仍然有效。关闭提醒同样是一段状态迁移先阻止新执行再取消已知系统任务按产品边界处理已经展示的通知最后清理或标记本地注册状态。当前cancelAll()只能证明它会请求取消应用通知不能证明后台任务已经撤销因为仓库中尚无后台任务注册与取消实现。若应用未来还有其他业务通知直接取消全部通知还可能扩大影响应优先按本功能拥有的通知 ID 和任务 ID 清理。十八、触发时重读数据避免注册时固化旧内容计划任务注册时保存的是“何时唤起”不是未来通知的完整正文。用户可能在注册后修改标题、删除纪念日或调整重复方式若把正文永久固化在任务参数里真正触发时就会展示旧数据。建议触发处理器只接收最小计划标识启动后重新读取仓库、换算下一次发生日、筛选窗口、生成聚合文案再调用 Publisher。触发处理器的建议边界可以写成下面这样interface TriggerResult { status: published | empty | duplicate | failed; executionKey?: string; } async function handleReminderTrigger(planId: string): PromiseTriggerResult { // 建议流程读取当前计划和当前事项检查当日执行键再尝试发布。 // 只有发布成功后才提交新的 executionKey。 return { status: failed }; }空列表不是系统错误。若当天没有零到三天内的事项处理器应返回empty不发布空通知也不把它包装成“提醒服务故障”。是否记录当天已检查需要按产品重试策略决定如果数据可能在当天稍后发生变化过早写入执行键会阻止新事项得到提醒如果系统只允许每天一次固定触发则应在下一次用户编辑后主动重算计划。这个选择需要产品规则、调度能力和测试用例共同约束。十九、验收证据要覆盖从计划到通知的完整链路一个时间计算单元测试通过只能证明给定输入下纯函数输出符合预期一个本地构建通过只能证明当前工具链接受代码一次手动通知出现只能证明权限与发布链路在当时可用。要宣称“每日提醒已经完成”还需保留系统注册结果、任务回读、跨日触发、进程退出后触发、设备重启后恢复、权限撤销、配置替换、关闭清理和同日去重等证据。建议每条验收记录都包含设备与系统版本、应用版本、计划配置、注册任务标识、预计触发时间、实际触发时间、执行键、通知结果和清理结果。涉及用户标题时应脱敏日志不保存私人纪念日内容。调度时间若受系统节能策略影响应记录允许的误差窗口不把“延迟触发”直接等同于“未触发”也不能在没有真实观测时声称精确到分钟。本轮文章复核没有执行上述设备实验因此这里只给出验收方法不给出通过率、触发精度或兼容设备数量。实现完成后应先用最小测试计划验证一台目标设备再扩展到项目声明支持的设备范围当前模块配置只声明phone不应把手机源码外推为平板、折叠屏或其他设备已经通过。总结时光清单的ReminderService已经把权限请求、近期事项筛选、聚合文案和 Notification Kit 发布收在一个可读边界内但它当前完成的是即时通知链路不是每日后台计划。工程上的下一步不是给方法改个名字而是补上计划层和状态模型。稳定提醒需要两次计算、两层去重注册时计算下一触发时间并用计划指纹避免重复任务真正触发时重新读取数据并用自然日执行键避免重复发布。权限、计划和展示结果分别记录关闭时同时撤销调度和通知。这样才能让“已开启提醒”成为可验证的产品事实而不是一次成功弹窗带来的错觉。AI 辅助声明本文由 AI 辅助整理现状分析基于D:\huawei\one8中ReminderService.ets、ProfileView.ets、Anniversary.ets与模块配置的真实源码复核计划层代码为针对当前边界的架构建议具体系统调度接口需按项目目标 HarmonyOS SDK 的官方文档接入并真机验证。