Feature Specification: 用户业务通知语音提醒
Feature: 035-user-notification-voice
Created: 2026-10-09
Status: 实现中(specify:书面规格待用户审阅;尚未进入 plan/tasks/implement)
工作分支:后端沿用 test-202609v2;用户 App 沿用本地当前 master。不创建分支或 worktree,不切换、合并或改写其他分支。
Input: 用户要求现有用户消息通知同时提供简短语音提醒,例如“骑手已接单”“您的订单已派送”“您的订单已送达”;已同意沿用现有通知、随 App 打包音频、前台统一播放、事件去重以及消息中心独立入库,采用 spec-kit 流程实施。2026-10-09 用户进一步收缩范围:本期只做前台语音,不做后台/锁屏语音,不修改云函数。
已确认方案及范围
- 在同一条既有业务通知中增加场景信息和语音,不额外发送第二条语音消息。
- 后端、用户 App 的前台统一接收和播放,以及为前台分流必需的 Android/iOS 接收代码在范围内。用户本次批准的跨端方案包含 App 配套,覆盖此前其他任务“不修改 App”的局部要求。
- 云函数不修改、不部署;后台和锁屏不新增语音、不调整自定义 sound 或通知渠道,保持原有通知行为。用户恢复 App 或点击通知时也不补播后台事件。
- 平台与商家 PC 不改;商家和骑手 App 的既有新单铃声不改。
- 默认
uni 通道不变;新增场景、语言及事件标识沿现有 payload 传递,已有 FCM/APNs 也保留这些信息供前台接收。不自动启用原生通道,不改凭据、通知声音或设备登录互斥。
- 只给当前实际向用户发送的通知增加语音,不新增取消、退款、预约超时等缺失的业务通知,不修改支付/退款或配送状态机。
- 不使用通话、PushKit、VoIP token 或音频后台保活来绕过系统通知限制。
- 不新增消息表、音频表、设备表或用户设置页;场景与事件信息保存在现有消息
body。若计划阶段证明需要新增数据库字段,必须先说明原因,迁移 SQL 仅登记,不执行。
现有代码事实(只读检查,非部署验证)
PayPush 用户业务包装分别发布 PushEvent 和调用 BusinessPushService;监听器写入 push_message,外部发送成功不代表消息中心入库成功,反之亦然。
BusinessPushProperties 和 application.yml 默认 uni;FCM/APNs 已有发送代码。未读取线上环境变量,不声称线上已启用原生通道。
- 外卖骑手接单/取餐/送达按
deliveryStatus 1/2/3;现有用户 payload 读取操作前 state,不能作为语音分类依据。
- 闪送用户状态通知携带
pushType=3、字符串状态,type=-1;新单 type=0 只面向骑手。现有支付成功和部分取消路径也使用 type=0,禁止将该值重新解释为用户某一条语音。
- 外卖骑手通知与自配送通知的部分调用方按可用推送地址提前返回,导致无设备地址时连消息中心记录也缺失;本特性需在这些用户通知路径中解耦。
- 用户 App 的 uniPush 接收逻辑主要展示通知,原生
push-received 目前进入通话相关处理;Android 原生本地通知固定一个默认渠道,iOS 前台 delegate 直接允许声音。
- 本地
msd-user-push/index.js 已转发 payload,但没有透传 sound;后端 URL 调用云端 msduser。本期不需要自定义 sound,不修改云函数;前台联调需检查现有通道能否保留新增 payload,不能把本地源码等同于云端部署版本。若部署版本丢弃新增信息,报告阻断项,不擅自修改云函数。
User Scenarios & Testing
User Story 1 - 用户听到配送进度 (Priority: P1)
外卖和闪送用户在 App 前台时,不盯着订单详情也能通过一句语音了解订单进度,文字通知仍保留订单标识;本期不承诺后台或锁屏播报。
Why this priority: 接单、开始配送和送达是截图明确要求的三个节点。
Independent Test: 用户 App 保持前台,分别在外卖和闪送真实成功的接单、取餐/取件、送达节点检查用户通知事件与声音。
Acceptance Scenarios:
- Given 外卖待接单或闪送 WAITING_ACCEPTANCE,When 骑手接单成功且事务提交,Then 用户收到原文字通知,并按语言听到“骑手已接单”一次。
- Given 骑手已接单,When 外卖取餐成功或闪送全部取货站完成,Then 用户听到“您的订单已派送”一次;闪送部分取件不新增播报。
- Given 订单配送中,When 送达成功,Then 用户听到“您的订单已送达”一次。
- Given 外卖为商家自配送,When 商家开始配送或确认送达,Then 使用同样的派送/送达短语音,不误播“骑手已接单”。
- Given 抢单、取件或送达失败,或者事务回滚,When 请求返回,Then 不生成该次成功状态通知或语音事件。
User Story 2 - 只在前台播放且不重复 (Priority: P1)
通知在 App 前台由统一接收逻辑播放;后台/锁屏保持原有通知,不启动语音播放器,不依赖 JavaScript 在后台持续运行。
Independent Test: Android 和 iOS 前台收到同一场景只播一次;后台、锁屏、点击通知和恢复 App 均不新增或补播语音。
Acceptance Scenarios:
- Given 用户 App 在前台,When uniPush 或原生通知到达,Then 统一处理场景并播放一次,不叠加普通铃声、第二条本地通知或另一个监听器的语音。
- Given App 在后台或锁屏,When 通知正常到达,Then 保持原有系统通知行为,不设置新的订单语音声音,不启动前台播放器或通话。
- Given 同一通知到达多个回调,When 客户端处理,Then 使用相同事件标识避免重复展示/播报。
- Given 用户点击通知、打开消息列表或恢复历史数据,When 详情导航或消息加载,Then 不再次播报历史事件。
- Given 系统限制发声、当前账号不匹配或 App 已转后台,When 前台播放器处理事件,Then 不强制发声、不为订单语音申请后台保活。前台媒体播放与系统通知声音设置不完全相同,具体平台行为需单独验证。
User Story 3 - 其他已有用户通知与多语言 (Priority: P2)
已有商家出餐、支付成功、商家自配送接单及已存在的用户取消通知也有独立短语音,不能全部套用接单音频。
Independent Test: 按下表各事件和四语言抽检;核对语音语言、文字通知、订单归属及通知对象。
| 事件语义 |
简体中文语音文案 |
现有触发范围 |
| RIDER_ACCEPTED |
骑手已接单 |
外卖/闪送骑手成功接单;已有旧入口对应通知 |
| ORDER_DELIVERING |
您的订单已派送 |
外卖取餐、闪送全部取件、商家自配送开始 |
| ORDER_DELIVERED |
您的订单已送达 |
外卖/闪送送达、商家自配送完成 |
| MERCHANT_ACCEPTED |
商家已接单 |
已有商家自配送接单用户通知 |
| MERCHANT_READY |
商家已出餐 |
已有商家出餐用户通知 |
| PAYMENT_SUCCEEDED |
您的订单已支付 |
在用 LINE Pay/OMG 支付成功用户通知 |
| ORDER_CANCELLED |
您的订单已取消 |
现有外卖取消链路实际通知用户的情况;不新增闪送用户取消通知 |
Acceptance Scenarios:
- Given 用户语言为简中、繁中、英文或越南语,When 受支持的业务通知触发,Then 文字和音频使用同一种语言;不播订单号、联系方式或地址。
- Given 用户语言未知或没有该语种音频,When 通知触发,Then 使用明确的默认语种策略;不得展示原始 i18n key 或引用不存在的资源。
- Given 商家或骑手是通知接收人,When 同一业务动作发生,Then 其原有铃声和 payload 行为不因用户语音改造而改变。
User Story 4 - 不影响消息中心及业务操作 (Priority: P1)
设备没有 CID/token 或外部推送失败时,用户仍可在消息中心查看已经成功发生的业务事件。
Independent Test: 对有用户但无推送地址、通道异常、旧 App、音频缺失和事务回滚分别检查通知/入库调用与业务结果。
Acceptance Scenarios:
- Given 用户存在但无可用 CID/token,When 范围内事件成功发生,Then 消息中心仍保存一条记录,跳过外部发送,不改变业务结果。
- Given uniPush/FCM/APNs 投递失败,When 发送执行,Then 不回滚订单,不再次向另一通道发送,不重复入库。
- Given 旧 App 或不能识别场景/音频的客户端,When 收到新增 payload,Then 保持原文字通知可用,降级到普通通知;不要求旧 App 播报新音频。
Edge Cases
- 自配送允许跳过开始配送直接确认送达:只通知实际成功的送达事件,不补造“开始配送”。
- 平台/旧入口使用的数字状态不同于当前外卖 deliveryStatus 和闪送字符串状态:语音事件从实际动作产生,不按一个通用 state 映射。
- 短时间连续多个事件或多笔订单:前台避免重叠播放;不同订单事件不得被一个全局布尔标记误去重;已被更新状态替代的待播事件不倒序播报。
- App 原生接收回调和 uniPush 回调可能同时到达:必须有明确的通知展示/声音责任方。
- App 转后台时停止前台播放并清理待播队列;恢复前台不补播后台收到的消息或被清理的事件。
- 前台音频须在实际 App 包内且播放器可访问;本期不要求打包为系统通知声音,也不创建场景/语言通知渠道。
- 老版本或缺少音频时保持普通文字通知,不导致接收逻辑失败;发布前须真机验证,不能仅凭 payload 构造测试判定兼容。
- 未登录/已换账号时不播另一账号的前台缓存事件;设备通知仍遵守现有单设备 token 与会话机制。
Requirements
Functional Requirements
- FR-001: MUST 沿用现有用户业务通知,每个成功事件只构造同一条含文字和语音信息的通知,不另发语音通知。
- FR-002: MUST 明确标识上表七种事件语义;只覆盖实际存在的用户通知,不新增触发时机或收件人。
- FR-003: MUST 保留历史 ddId/state/type/pushType 契约;新增场景与语音语义不得占用旧 type=0,也不得通过匹配中文正文判断。
- FR-004: MUST 为一次业务事件生成并复用事件唯一标识,同一通知的入库、通道发送和所有客户端回调携带一致标识;转发不得重新生成。
- FR-005: MUST 为受支持场景提供简中、繁中、英文、越南语简短音频,随用户 App 打包,不在每次通知时在线合成/下载,不播敏感或冗长订单信息。
- FR-006: MUST 以接收用户语言决定文字和语音;已有其他语言无音频时明确回落,避免缺失资源或非法文件名。
- FR-007: MUST 仅在 App 前台由统一接收逻辑播放一次;后台/锁屏不启动语音,保持原有通知行为,转后台停止播放并清理队列,恢复时不补播。
- FR-008: MUST 在默认 uniPush、已有 FCM/APNs payload 中保留场景、语言及事件标识供前台处理;当前通道选择、声音配置、失败不回退、端别凭据隔离保持不变。
- FR-009: MUST 不修改或部署云函数,不传递新的自定义通知 sound、不新建通知声音渠道;前台从受控的场景/语言映射选择本地音频,不接受任意文件路径或声音 URL。若现有云端不能保留 payload,报告限制并请求方向。
- FR-010: MUST 保留消息中心独立入库;本范围内有有效用户但无设备地址不能跳过记录生成。
- FR-011: MUST 在实际业务状态成功且事务提交后生成成功事件,失败/回滚不发;自配送及出餐等现有非统一提交后路径需按本范围收口。
- FR-012: MUST 隔离推送与播放失败,不改变支付、配送、取消等原业务结果,不把入库成功等同于外部到达或播放成功。
- FR-013: MUST 对重复回调去重,并正确区分订单/事件;通知点击、消息列表、历史恢复不触发补播。
- FR-014: MUST 将普通业务推送与通话消息分流,不调用 CallKit/PushKit 或复用 voIPToken 来实现订单语音。
- FR-015: MUST 保持原有系统通知权限和声音设置,不通过强制音量、通话或后台保活绕过限制;前台播放器的权限、静音和音频中断行为在 Android/iOS 分别验证,不把媒体播放与通知声音开关视为相同能力。
- FR-016: MUST 保持旧 App 的文字通知、已知跳转字段及普通声音回退;对支持新功能的 App,点击按 pushType 路由外卖/闪送详情,不把闪送订单号当外卖查询。
- FR-017: MUST 仅修改本功能必需代码,补充关键场景、前台生命周期、去重、事务及兼容注释,不改云函数、后台/锁屏声音、废弃支付入口、商家/骑手新单语音或 PC 页面。
- FR-018: MUST 按 specify → plan → tasks → implement 记录进度,原分支开发,不创建或切换分支;实际测试、构建、音频资源及真机验证分别记录,不将模拟检查报告为完整功能验收。
Key Entities
- 业务通知事件:关联原订单、接收用户、成功动作、事件唯一标识、语言与短语音场景;不是一份新的订单状态。
- 消息中心记录:已有 push_message;新增场景信息放在 JSON body,列表读取不会自动播放。
- 本地语音资源:事件与语言确定的短音频;Android/iOS 前台播放器可访问,文件格式、时长、内容及打包结果分别验证,不作为本期系统通知声音。
- 播放/通知去重状态:客户端区分订单、事件与账号;不是 isRead,不改变消息已读状态。
Success Criteria
Measurable Outcomes
- SC-001: 上表七种实际用户通知在受测成功动作中均有正确事件分类;失败动作/事务回滚对应成功事件数量为 0。
- SC-002: 每种受支持语言的通知文字与语音内容匹配;所有发布音频均已实际生成、可解码、语句正确且在最终打包资源中可访问。
- SC-003: 在权限和声音允许、消息已到设备的条件下,前台闲置播放器在收到有效事件后 2 秒内开始一次播报;后台/锁屏保持原有通知且不启动新增语音,恢复 App 不补播。
- SC-004: 同一事件重复进入原生/uniPush 回调、点击通知、恢复 App、打开历史消息的测试中,前台不重复播报;一次正常事件没有两条展示通知或叠加声音。
- SC-005: 缺失设备地址或外部通道异常时,范围内受测事件仍产生一条用户消息记录,业务结果不受影响,不出现双通道补发。
- SC-006: 商家/骑手通知、音视频通话与四个历史 payload 字段的定向回归无行为改变;旧 App 普通通知回退和新 App 外卖/闪送点击路径完成真机核验。
Assumptions / Dependencies
- 本规格以仓库源码为依据;实际服务器环境变量、现有云端 payload 透传、签名 App 和最终打包音频尚未验证。云函数与后台/锁屏语音已由用户明确排除,不作为本期交付项。
- 本轮不切换 App 的 master 分支。若该 checkout 并非用户维护的目标 App 源码,需在跨端实施前由用户指定路径/分支,不能自行切换。
- 音频必须是可交付的实际资源,不能以空文件、占位文件或待提供说明代替完成;若本机无法制作/核听所需语种,应记录资源依赖并请求用户提供,不宣称闭环完成。
- 音频合成如需外部付费服务或上传数据,须先说明并取得相应授权;预制资源不包含订单号、姓名、手机号、地址。
- 不做远端部署、数据库操作或生产推送试发;这些操作需要用户明确要求。代码与本地验证完成后仍单独列出真机/部署未验证项。
- 新规格补充 028 的用户通知与 033 的通道路由;不将其旧版“仅服务端/App 不在范围”的边界套用到本次已批准的 App 配套,也不改变旧任务的历史验收状态。
平台能力参考