【时光清单|04】HarmonyOS ArkTS 通知服务实战:管理权限、渠道和点击跳转
在业务代码里出现一个名为 NotificationService 的类,不代表通知链路已经完整。一个可上线的通知模块至少要回答四个问题:什么内容值得提醒,系统是否允许展示,通知应归入哪类消息,用户点击后如何落到应用内的正确页面。
时光清单 的真实源码把其中两个部分分开了:NotificationService.ets 只负责筛选未来七天内的纪念日并生成单条文案;ReminderService.ets 才调用 @kit.NotificationKit 请求通知授权、构造基础文本通知并发布。当前工程没有在通知请求中附加点击代理,也没有在 EntryAbility 中解析通知路由参数。因此,"管理权限、渠道和点击跳转"是基于现状要补齐的工程边界,而不是已经存在的隐藏能力。
本文按当前仓库真实源码拆解一套可复核的分层方案:领域服务只生成通知候选和文案,平台适配层管理授权与通知类别,路由契约把通知点击转换成应用内导航。这样既能避免页面直接拼 NotificationRequest,也不会让纯业务函数承担系统 API 和生命周期责任。

本文重点包括:
- 复核当前七天窗口与提醒文案的真实实现。
- 区分通知权限、用户业务开关和消息类别。
- 设计稳定通知 ID,避免不同业务互相覆盖。
- 用窄路由参数实现通知点击后的详情页跳转。
- 处理冷启动、热启动、无效 ID、权限拒绝和取消通知等边界。
本文唯一标记:
CSDN-SERIES:ALL-163202512
阅读前先区分当前事实、历史证据与建议实现
当前事实 来自 D:\huawei\one8 的静态源码复核。可以确认:NotificationService 不依赖系统通知 API,只筛选剩余 0 到 7 天的事项并生成文案;ReminderService 才调用 @kit.NotificationKit,请求通知开关、筛选剩余 0 到 3 天的事项、构造基础文本请求并调用 publish();设置页只有在用户点击"通知提醒"后才进入这条调用链。
当前发布请求使用固定 id: 1001 和 SlotType.SERVICE_INFORMATION。被检查的请求里没有点击代理或路由参数;EntryAbility.onCreate() 虽然接收 Want,但没有解析通知点击信息;源码中也没有找到 sendDailyReminder() 的后台调度入口。cancelAll() 会取消本应用全部通知,无法按某个纪念日精确取消。
历史证据 只引用有明确记录的内容。PROJECT_ERRORS.md 中没有通知权限、通知展示、点击路由或调度的专项验证记录。文档里出现的历史 assembleHap 通过属于其他 2026 年 5 月修复,不能拿来证明当前通知代码已经构建、已经在设备显示或已经通过发布审核。
建议实现包括业务提醒开关、稳定通知 ID、点击代理、版本化路由、冷启动与热启动处理、精确取消和后台调度。它们是基于现有缺口提出的设计,不是当前代码已经拥有的隐藏能力。
本文实际复核范围如下:
service/NotificationService.ets
service/ReminderService.ets
views/ProfileView.ets
views/PrivacyPolicyView.ets
entryability/EntryAbility.ets
data/AnniversaryRepository.ets
model/Anniversary.ets
routes/RouteNames.ets
module.json5
PROJECT_ERRORS.md
本文没有执行新的 HAP 构建、权限弹窗、通知展示、锁屏展示、点击跳转、冷启动、热启动、后台调度或精确取消测试,因此这些结果都保留为待验证项。
一、当前 NotificationService 是领域筛选器
真实文件只有两个静态方法,没有导入 Notification Kit:
import {
Anniversary,
calcDaysRemaining
} from '../model/Anniversary';
export class NotificationService {
static getUpcomingReminders(
items: Anniversary[]
): Anniversary[] {
return items
.filter((item: Anniversary) => {
const days =
calcDaysRemaining(item.targetDate);
return days >= 0 && days <= 7;
})
.sort((a: Anniversary, b: Anniversary) =>
a.targetDate - b.targetDate
);
}
static getReminderText(
item: Anniversary
): string {
const days =
calcDaysRemaining(item.targetDate);
if (days === 0) {
return `今天是「${item.title}」的日子!`;
}
if (days === 1) {
return `明天就是「${item.title}」啦!`;
}
return `「${item.title}」还有 ${days} 天`;
}
}
这段代码做的是领域决策:
| 决策 | 真实规则 |
|---|---|
| 是否进入候选 | 剩余天数在 0 到 7 之间 |
| 是否包含过期事项 | 不包含 |
| 排序依据 | targetDate 升序 |
| 今天文案 | "今天是......" |
| 明天文案 | "明天就是......" |
| 其余文案 | "还有 N 天" |
它没有权限请求、消息类别、通知 ID、发布、取消或点击跳转。把这个类保留为纯业务层反而是优点:它可以用构造数据直接单元测试,不需要 HarmonyOS 运行环境。

二、平台发布在 ReminderService,不要混淆两层
项目中的 ReminderService 才真正接触系统通知:
import {
notificationManager
} from '@kit.NotificationKit';
static async requestPermission():
Promise<boolean> {
try {
await notificationManager
.requestEnableNotification();
return true;
} catch (e) {
hilog.warn(
0x0000,
TAG,
`requestPermission failed: ${
JSON.stringify(e)
}`
);
return false;
}
}
发布请求使用基础文本内容和 SERVICE_INFORMATION 类别:
const 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);
因此当前真实结构可以概括为:
NotificationService
负责:候选筛选、排序、单条文案
ReminderService
负责:授权请求、聚合文案、Notification Kit 发布、全部取消
后续重构时不必把两者强行合并。更合适的方向是给它们更准确的名字,例如 ReminderContentService 与 HarmonyNotificationPublisher,再新增独立的点击路由层。
三、权限状态不能等同于业务开关
设置页当前在点击"通知提醒"时请求系统授权,成功后立即发送一次提醒。完整产品至少有两类状态:
- 系统通知权限:由 HarmonyOS 管理,用户可在系统设置中修改。
- 应用提醒开关:由用户在"时光清单"内选择,保存在本地。
两者组合后才得到实际行为:
| 系统权限 | 应用开关 | 页面状态 | 是否发布 |
|---|---|---|---|
| 允许 | 开启 | 已开启 | 是 |
| 允许 | 关闭 | 已关闭 | 否 |
| 拒绝 | 开启 | 需要授权 | 否 |
| 拒绝 | 关闭 | 已关闭 | 否 |
不要在每次进入页面时自动拉起授权弹窗。更符合体验的流程是:用户主动开启提醒时请求;拒绝后说明用途;再次操作时根据系统规则引导到设置,而不是循环打扰。
可以定义业务层可读状态:
export type NotificationAccessState =
| 'enabled'
| 'permission_required'
| 'disabled_by_user';
export function resolveAccessState(
systemAllowed: boolean,
reminderEnabled: boolean
): NotificationAccessState {
if (!reminderEnabled) {
return 'disabled_by_user';
}
return systemAllowed
? 'enabled'
: 'permission_required';
}
这个函数不负责拉弹窗,只把两个来源的状态转成页面文案和控件状态。
四、消息类别不是装饰字段
当前请求使用:
notificationSlotType:
notificationManager.SlotType
.SERVICE_INFORMATION
消息类别会影响系统对通知的组织和用户设置体验。开发者应根据通知真实用途选择类别,不能为了更显眼而滥用高优先级、进行中通知或与业务不符的类型。纪念日临近提示属于普通服务信息,而不是通话、导航、媒体播放或持续进度。
平台适配层可以把业务类别映射集中起来:
export type BusinessNoticeType =
'anniversary' | 'habit' | 'backup';
export function resolveSlotType(
type: BusinessNoticeType
): notificationManager.SlotType {
switch (type) {
case 'anniversary':
case 'habit':
case 'backup':
return notificationManager
.SlotType.SERVICE_INFORMATION;
}
}
即使当前三个业务都映射到同一类别,集中映射仍有价值:以后调整不会散落在多个页面;代码审查也能快速确认是否存在不当类别。
华为的通知消息管理规则还要求通知内容与样式符合场景,不应利用通知故意后台拉起应用、唤醒其他应用或诱导跳转。通知点击应服务于用户刚刚看到的内容,而不是把用户送到无关首页或外部下载页。
五、通知 ID 要按业务稳定生成
真实发布代码固定使用 id: 1001。只有一种提醒时很简单;如果未来习惯、备份和纪念日都发布通知,全部使用同一个 ID 可能互相覆盖,全部使用随机 ID又会堆积重复消息。
可以为业务划分 ID 空间:
export class NotificationIds {
static readonly ANNIVERSARY_BASE = 100000;
static readonly HABIT_BASE = 200000;
static readonly BACKUP_RESULT = 300001;
}
export function stableNumber(
value: string
): number {
let hash = 0;
for (let i = 0; i < value.length; i++) {
hash = ((hash << 5) - hash) +
value.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash);
}
export function anniversaryNoticeId(
anniversaryId: string
): number {
return NotificationIds.ANNIVERSARY_BASE +
stableNumber(anniversaryId) % 90000;
}
同一纪念日得到稳定 ID,更新文案时仍能定位同一通知;不同业务落在不同区间,取消时不容易误伤。哈希存在理论碰撞,如果业务规模扩大,可保存 ID 映射或使用平台允许的组合标识。
六、点击跳转先定义路由契约
通知点击不应把整个 Anniversary JSON 塞进参数。通知可能在记录修改或删除后才被点击,旧快照会产生过时内容。更稳的做法只传业务类型、实体 ID 和协议版本:
export interface NotificationRoute {
version: 1;
action: 'open_anniversary';
anniversaryId: string;
source: 'local_notification';
}
export function createAnniversaryRoute(
id: string
): NotificationRoute {
return {
version: 1,
action: 'open_anniversary',
anniversaryId: id,
source: 'local_notification'
};
}
点击后应用用 anniversaryId 重新查询仓库。若记录已删除,回到列表并提示"该事项已不存在",而不是打开一张过期详情。
路由协议需要版本号,因为应用升级后通知栏里可能仍保留旧通知。解析器遇到未知版本时应安全回退首页,不要直接解构并假定字段存在。

七、NotificationRequest 中的点击代理由平台层构造
完整通知请求应由平台适配层统一创建,并在目标 SDK 支持的请求字段中附加指向本应用的点击代理。由于具体 WantAgent 创建签名会随 SDK 版本演进,业务层不要直接依赖它,可以先抽象为窄接口:
export interface NotificationClickAgent {
readonly platformValue: object;
}
export interface ClickAgentFactory {
createForRoute(
route: NotificationRoute
): Promise<NotificationClickAgent>;
}
export interface LocalNotice {
id: number;
title: string;
text: string;
type: BusinessNoticeType;
route?: NotificationRoute;
}
发布器的步骤是:
- 校验通知 ID、标题和正文。
- 根据业务类型映射消息类别。
- 路由存在时,通过工厂创建本应用的点击代理。
- 构造
NotificationRequest。 - 调用
notificationManager.publish()。 - 返回明确的成功或失败结果。
点击代理只能指向用户预期的本应用页面,不能借通知无关拉起其他应用。接入时以项目当前 HarmonyOS SDK 的官方 WantAgent 与 NotificationRequest 文档为准,并在真机验证冷、热启动行为。
八、EntryAbility 要同时处理冷启动与热启动
当前 EntryAbility.onCreate() 接收 Want,但没有解析通知路由:
onCreate(
want: Want,
launchParam: AbilityConstant.LaunchParam
): void {
hilog.info(
DOMAIN,
TAG,
'Ability onCreate'
);
DataStore.getInstance()
.initSync(this.context);
AppStore.bootstrap(this.context);
DataStore.getInstance()
.init(this.context);
this.hydratePersistentState();
}
点击通知可能发生在两种状态:
- 应用未运行:走
onCreate,随后窗口和页面栈才建立。 - 应用已运行:系统把新的 Want 交给已有 Ability,需在对应回调处理。
路由不能在页面栈尚未初始化时直接 pushPath()。可以先暂存"待处理路由",等 Index 页面和 NavPathStack 准备好后消费:
export class PendingRouteStore {
private static pending:
NotificationRoute | null = null;
static put(route: NotificationRoute): void {
PendingRouteStore.pending = route;
}
static take():
NotificationRoute | null {
const value = PendingRouteStore.pending;
PendingRouteStore.pending = null;
return value;
}
}
take() 读取后立即清空,可以避免窗口重建或页面重新出现时重复跳转。
九、路由解析必须拒绝脏参数
通知参数来自应用自己创建的 Want,但仍可能是旧版本、字段缺失或被系统恢复后的异常值。解析器应显式校验:
export function parseNotificationRoute(
params: Record<string, Object>
): NotificationRoute | undefined {
const version = params['routeVersion'];
const action = params['routeAction'];
const id = params['anniversaryId'];
if (version !== 1) return undefined;
if (action !== 'open_anniversary') {
return undefined;
}
if (typeof id !== 'string' ||
id.length === 0 ||
id.length > 128) {
return undefined;
}
return {
version: 1,
action: 'open_anniversary',
anniversaryId: id,
source: 'local_notification'
};
}
不要把参数直接拼进文件路径、URL 或数据库语句。这个项目只需要把 ID 交给 AnniversaryRepository.getById(),查不到就回退。
十、导航恢复:先查最新实体,再进入详情
项目的列表页当前通过:
this.pathStack.pushPath({
name: RouteNames.DETAIL,
param: JSON.stringify(item)
});
通知点击更适合用 ID 重新查询:
async function openNotificationRoute(
route: NotificationRoute,
repo: AnniversaryRepository,
pathStack: NavPathStack
): Promise<'opened' | 'not_found'> {
const item = await repo.getById(
route.anniversaryId
);
if (!item) return 'not_found';
pathStack.pushPath({
name: RouteNames.DETAIL,
param: JSON.stringify(item)
});
return 'opened';
}
这样详情页仍沿用现有参数格式,而通知层只持有稳定 ID。以后路由改为详情页自己按 ID 加载,也只需修改这一处适配。
热启动还要防止重复入栈:用户连续点同一通知或系统重复分发 Want 时,如果当前已经是同一详情,不应再次 push。可以用消费令牌、当前路由比较或 replacePath 策略处理。
十一、发布结果要向上返回
当前 ReminderService.publish() 捕获异常后只写日志,上层不知道是否成功。通知服务更适合返回判别结果:
export type NoticePublishResult =
| { ok: true; id: number }
| {
ok: false;
reason:
| 'permission_denied'
| 'invalid_content'
| 'publish_failed';
};
平台错误不要原样暴露给用户,日志也不应输出完整私人标题。页面只需要知道:是否需要授权、内容是否无效、系统发布是否失败。由上层决定 Toast、重试或设置入口。
发布前还应限制内容:
export function normalizeNoticeText(
value: string,
maxLength: number
): string {
const text = value.trim();
if (text.length <= maxLength) return text;
return text.slice(0, maxLength - 1) + '...';
}
通知标题和正文不要重复,私人事项可提供"隐藏详细标题"的用户选项,锁屏时只显示"有一项重要日子即将到来"。
十二、取消要精确,不要默认 cancelAll
真实 ReminderService.cancelAll() 会取消应用全部通知:
static async cancelAll(): Promise<void> {
try {
await notificationManager.cancelAll();
} catch (e) {
hilog.warn(
0x0000,
TAG,
`cancelAll failed: ${JSON.stringify(e)}`
);
}
}
如果应用未来只有纪念日通知,它可以作为"一键清空通知栏"功能;如果加入习惯提醒和备份通知,关闭某一个功能时应按 ID 精确取消。
建议区分:
| 用户操作 | 取消范围 |
|---|---|
| 删除某个纪念日 | 取消该实体对应通知 |
| 关闭纪念日提醒 | 取消纪念日 ID 区间和调度计划 |
| 清空所有应用通知 | 明确确认后调用 cancelAll() |
| 关闭系统通知权限 | 不自动改写业务数据 |
取消系统通知与取消未来调度也不是一回事。若后台任务仍存在,它下一次可能继续尝试发布;关闭提醒时必须同时处理调度层。
十三、通知规范与上架边界
通知是用户可见的系统级界面,不能只从"API 能不能调通"判断完成。至少应遵守这些边界:
- 只在用户明确开启后发送可预期的提醒。
- 标题、正文和跳转目标保持一致。
- 不伪装其他应用,不隐藏应用来源。
- 不用普通纪念日提醒冒充通话、导航或进行中任务。
- 不通过通知故意后台拉起无关流程。
- 点击只进入本应用与当前内容相关的页面。
- 提供关闭提醒和清理通知的可达入口。
- 隐私政策与实际通知内容保持一致。
可对照华为官方通知消息管理规则检查样式、内容分类和点击跳转约束。对于本地通知与目标 SDK API,再以项目构建时对应版本的 HarmonyOS 官方文档为准。
十四、验证矩阵
纯业务层可以先跑快速测试:
- 今天、明天和第七天进入候选,第八天不进入。
- 已经过期的事项被排除。
- 多条候选按目标日期升序。
- 三种文案分支正确。
- 长标题被安全截断。
- 相同纪念日 ID 生成稳定通知 ID。
- 不同业务 ID 空间不冲突。
- 路由缺版本、缺 action、空 ID 时解析失败。
真机重点验证系统行为:
- 首次允许通知后发布成功。
- 拒绝权限后不继续发布,并能恢复设置。
- 通知类别与业务用途一致。
- 同 ID 再发布的更新或覆盖行为符合预期。
- 应用关闭时点击通知,冷启动后进入正确详情。
- 应用前台时点击通知,不重复创建 Ability 或重复入栈。
- 事项已删除后点击旧通知,安全回退列表。
- 连续点击通知不会叠加多个同一详情页。
- 删除事项后对应通知被精确取消。
- 关闭纪念日提醒不会取消无关业务通知。
- 深浅色下通知落地页文字、状态栏与返回路径清晰。
- 手机之外的设备若不在模块
deviceTypes范围,不宣称已适配。
当前 module.json5 只声明 phone,因此文章和上架材料都不应把平板通知跳转写成已验证能力。
十五、常见故障与排查顺序
| 现象 | 常见原因 | 修复方向 |
|---|---|---|
| 有文案却没有通知 | 只调用 NotificationService |
经过平台 Publisher 发布 |
| Toast 成功但通知没出现 | 发布异常被吞掉 | 返回明确发布结果 |
| 点击通知只进首页 | 没有点击代理或路由参数 | 构造版本化路由 |
| 冷启动偶尔不跳详情 | 页面栈尚未就绪 | 暂存路由,页面就绪后消费 |
| 热启动叠加多个详情页 | 重复 Want 未去重 | 比较当前路由或消费令牌 |
| 旧通知打开旧标题 | 参数携带完整实体 | 只传 ID,点击后重查 |
| 关闭纪念日提醒清掉全部通知 | 使用 cancelAll() |
按业务 ID 精确取消 |
| 习惯通知覆盖纪念日通知 | 所有业务固定 ID 1001 | 划分稳定 ID 空间 |
排查时从领域候选开始,再查权限、请求内容、通知 ID、点击代理、Ability 收参和页面栈。把每层输入输出记录为非敏感诊断信息,比反复点击发布按钮更容易定位问题。
十六、先解决七天候选与三天发布的规则分叉
当前有两套很接近但并不相同的筛选规则。NotificationService.getUpcomingReminders() 接受 0 到 7 天,ReminderService.sendDailyReminder() 又独立实现 0 到 3 天。前者没有被后者复用。只看类名很容易误以为"七天候选服务"决定了实际通知,真实发布路径却只使用后三天规则。
这种分叉未必是错误,也可能是产品有意区分"应用内即将到来列表"和"系统通知窗口"。问题在于规则没有被命名。未来有人把七天改成十四天时,可能只改其中一处;测试也难以判断哪一个窗口才是产品承诺。
建议把窗口变成显式策略,并让调用方说明场景:
// 建议实现,当前源码仍有两段独立筛选。
export interface ReminderWindow {
minDays: number;
maxDays: number;
}
export const IN_APP_UPCOMING: ReminderWindow = { minDays: 0, maxDays: 7 };
export const SYSTEM_NOTICE: ReminderWindow = { minDays: 0, maxDays: 3 };
筛选函数只负责执行策略,文案和发布都复用同一批候选。这样文章、设置页说明和测试可以明确写"应用内展示未来七天,系统提醒未来三天",而不是用两个魔法数字猜产品意图。重复事项还需要额外确认:当前 calcDaysRemaining() 直接读取 targetDate,没有在通知服务中把 yearly 或 monthly 重复规则换算到下一次发生日期,不能据此宣称重复纪念日提醒已经完整。
十七、权限请求返回 true,不代表通知已经展示
requestPermission() 返回 true 只说明 requestEnableNotification() 没有抛出异常。随后 sendDailyReminder() 可能因为没有 0 到 3 天候选而直接返回,也可能在 publish() 中遇到异常。当前 publish() 把异常记录后返回 void,上层仍会继续显示"通知提醒已开启"。因此这个 Toast 更接近"权限流程未抛错",不是"通知已成功展示"的可靠证据。
建议返回分阶段结果,让页面使用准确文案:
// 建议实现。
export type ReminderRunResult =
| { ok: true; state: 'published'; noticeId: number }
| { ok: true; state: 'no_candidate' }
| { ok: false; state: 'permission_denied' | 'publish_failed' };
用户允许权限但近期没有事项时,可以提示"提醒权限已开启,暂无三天内事项";发布失败时显示"设置已保留,本次通知发送失败";只有收到平台发布成功结果后,才提示"已发送测试提醒"。即便 publish() 返回成功,也仍然不能仅凭方法返回值证明用户在通知栏实际看见消息,最终展示、同 ID 覆盖和系统策略仍需设备侧观察。
日志同样要保持克制。当前错误日志使用 JSON.stringify(e),没有主动记录标题正文,这是好边界;后续若增加请求诊断,也只记录结果枚举、通知 ID 和平台错误码,不写私人事项标题、备注或完整路由参数。
十八、业务开关要持久化,但不能伪装系统权限
当前设置页"通知提醒"是一个可点击菜单项,不是可读取的开关。源码没有持久化"用户希望接收纪念日提醒"的布尔偏好,也没有在页面重进时恢复启用状态。隐私政策说明通知权限可选、拒绝不影响核心功能,这与用户主动触发授权的入口是一致的;但完整设置体验还需要把应用偏好和系统权限分开显示。
建议状态模型至少包含:
// 建议实现,不代表当前 DataStore 已有这些键。
interface ReminderPreference {
enabledByUser: boolean;
privacyMode: 'show_title' | 'hide_title';
noticeWindowDays: 1 | 3 | 7;
updatedAt: number;
}
enabledByUser 是应用自己的偏好,系统权限仍由 Notification Kit 或系统设置决定。用户关闭业务开关时,应停止未来调度并精确取消纪念日通知,但不应替用户改写系统总权限;系统权限被关闭时,也不应悄悄把业务偏好改成关闭,否则用户重新授权后无法恢复原选择。
锁屏隐私是通知业务不可忽略的一层。纪念日标题可能包含姓名、关系或私人事件,默认展示完整标题前应确认产品策略。提供"隐藏详情"后,锁屏通知可以只写"有一项重要日子即将到来",点击进入应用后再读取本地最新记录。本文只确认隐私页面提到了通知用途,未验证系统锁屏样式或隐私选项。
十九、所谓每日提醒,目前没有调度证据
方法名 sendDailyReminder() 容易让人理解为"每天自动执行",但静态调用搜索只发现 ProfileView.handleNotificationPermission() 在用户点击入口后调用它。被检查的模块配置和 Ability 中没有通知专用的定时任务、后台任务或系统调度入口。因此"Daily"描述的是函数意图,不是已建立的执行频率。
建议先定义调度与发布的接口边界,具体 API 再按目标 SDK 官方文档落地:
// 建议接口,当前源码没有对应调度实现。
export interface ReminderScheduler {
enable(policy: ReminderSchedulePolicy): Promise<void>;
disable(): Promise<void>;
rescheduleAfterDataChange(): Promise<void>;
}
export interface ReminderSchedulePolicy {
localHour: number;
windowDays: number;
}
调度触发后仍然调用同一个领域筛选与发布器,不要在后台入口复制第三套规则。新增、编辑、删除纪念日后,需要判断是否重算计划;关闭提醒时同时取消计划;设备重启、时区变化和系统日期变化后是否恢复,都要以实际平台能力和设备日志验证。
"每天 09:00"也不应写成绝对准点承诺,除非目标平台 API 明确保证并经过设备观察。更稳妥的产品文案是说明提醒时间范围或系统可能根据资源策略调整,工程记录则保存预期窗口与实际回调时间。
二十、通知链路要按证据层级验收
通知功能不能用一次 Toast 或一次方法返回值作为整体完成证据。可以把验收分为五层:静态源码确认候选、文案、权限调用和请求字段;本地构建确认类型与 API 可编译;设备权限确认允许、拒绝和再次设置;系统展示确认类别、同 ID 行为、锁屏隐私和取消;导航生命周期确认冷启动、热启动、脏参数、记录删除和重复点击。
每次设备测试至少记录"动作、设备与系统版本、前置权限、候选数据、预期结果、实际通知 ID、实际跳转、关键日志时间"。通知没有出现时,先确认是否存在三天内候选,再看权限状态、请求构造与发布结果;点击只到首页时,再查点击代理、Want 参数、Ability 回调和页面栈是否就绪。按层排查比反复重新授权更容易定位。
当前文章只完成静态源码层,并明确引用没有通知专项记录的历史边界。没有执行的构建、真机展示、点击跳转、后台调度、发布审核、用户数据和平台评分,一律不写成已经完成。模块当前只声明 phone,其他设备只有在配置、构建、运行和材料都实际验证后,才能进入对外说明。
总结
时光清单 当前的 NotificationService 实际上是一个清晰的提醒内容领域服务:筛选七天窗口、排序并生成文案;Notification Kit 的权限与发布则在 ReminderService。这份拆分为完整通知架构提供了良好起点,但点击跳转和精确取消还没有进入真实源码。
可靠的通知链路应保持三层边界:领域层决定提醒什么,平台层决定如何授权、分类、发布和取消,路由层决定点击后如何安全进入最新业务状态。再配合稳定通知 ID、版本化参数、冷/热启动双路径和真机验证,通知才会从"一次能弹出"升级成可维护、可审核、可回退的系统能力。
AI 辅助声明:本文由 AI 辅助整理,现状结论基于
D:\huawei\one8中NotificationService.ets、ReminderService.ets、EntryAbility.ets、纪念日仓库与模块配置的真实源码复核;点击代理与路由代码属于面向现有边界的工程方案,需按项目目标 SDK 官方接口实现并真机验证。