Feature Specification: 用户业务通知语音提醒
Feature: 035-user-notification-voice
Created: 2026-10-09
Status: 已验证(本期后端 152 项定向测试及模块构建通过,审查问题已修正;完整回归存在 1 项基线泰文语言包失败;未部署,App/真机未验证)
工作分支:后端沿用 test-202609v2。不创建分支或 worktree,不切换、合并或改写其他分支;不修改任何 App 仓库。
Input: 用户要求现有用户消息通知同时提供简短语音提醒,例如“骑手已接单”“您的订单已派送”“您的订单已送达”;已同意沿用现有通知、随 App 打包音频、前台统一播放、事件去重以及消息中心独立入库,采用 spec-kit 流程实施。2026-10-09 用户进一步收缩范围:本期只做前台语音,不做后台/锁屏语音,不修改云函数。
最新责任边界(2026-10-09):App 改动由前端人员完成;本任务只实现后端 eventScene、定向验证及前端接入文档。用户已明确取消此前计划的 eventId、voiceLanguage、recipientUserId、eventTime,不以改名、隐藏字段或额外接口替代。不制作音频、不修改 App/原生插件或云函数;客户端播放条件是交接要求,不计为本任务已验证能力。
已确认方案及范围
- 在同一条既有用户业务通知中只新增 eventScene,供前端选择短语音,不另发语音消息。此前事件唯一标识和语言元数据方案已被用户最新要求取消。
- 只修改后端通知代码、定向测试和本 feature 文档。前端接入文档覆盖前台统一接收、播放、Android/iOS 分流、资源及验收要求,实际 App 实现由前端人员负责。
- 云函数不修改、不部署;后台和锁屏不新增语音、不调整自定义 sound 或通知渠道,保持原有通知行为。用户恢复 App 或点击通知时也不补播后台事件。
- 用户/商家/骑手 App 及其原生插件、平台和商家 PC 均不修改;商家和骑手的既有新单铃声不改。
- 默认 uni 通道不变;eventScene 沿现有完整 payload 传递,FCM/APNs 保留完整 JSON。不新增语言、用户、时间、事件标识,不改凭据、通知声音或设备登录互斥。
- 只给当前实际向用户发送的通知增加语音,不新增取消、退款、预约超时等缺失的业务通知,不修改支付/退款或配送状态机。
- 不使用通话、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: 后端定向测试捕获外卖、闪送、自配送通知,验证成功场景及失败/回滚零通知;实际播放由前端按接入文档另行真机验收。
Acceptance Scenarios:
- Given 外卖待接单或闪送 WAITING_ACCEPTANCE,When 骑手接单成功且事务提交,Then 用户收到原文字通知,并按语言听到“骑手已接单”一次。
- Given 骑手已接单,When 外卖取餐成功或闪送全部取货站完成,Then 用户听到“您的订单已派送”一次;闪送部分取件不新增播报。
- Given 订单配送中,When 送达成功,Then 用户听到“您的订单已送达”一次。
- Given 外卖为商家自配送,When 商家开始配送或确认送达,Then 使用同样的派送/送达短语音,不误播“骑手已接单”。
- Given 抢单、取件或送达失败,或者事务回滚,When 请求返回,Then 不生成该次成功状态通知或语音事件。
User Story 2 - 前端有明确的前台语音接入契约 (Priority: P1)
后端仅给出 eventScene,文档指导前端用自己的语言设置选择音频、在 App 前台统一接收和播放;后台/锁屏保持原有通知。未增加事件 ID,不能承诺跨回调精确去重或仅凭本 payload 区分重复/合法再次发生的同场景动作。
Independent Test: 契约测试校验只有 eventScene 为新增字段;文档列出前台统一接收、后台/锁屏不新增播放、历史不补播,以及没有额外字段时的能力限制。
Acceptance Scenarios:
- Given 用户 App 在前台,When uniPush 或原生通知到达,Then 统一处理场景并播放一次,不叠加普通铃声、第二条本地通知或另一个监听器的语音。
- Given App 在后台或锁屏,When 通知正常到达,Then 保持原有系统通知行为,不设置新的订单语音声音,不启动前台播放器或通话。
- Given 同一通知到达多个回调,When 客户端处理,Then 前端选定唯一处理入口并复用 SDK 已有消息标识(若存在);本期不提供服务端事件 ID,也不保证仅靠订单号/场景可精确去重。
- 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 用户有 App 语言设置,When 前台收到受支持场景,Then 前端按自身设置选择本地短音频;后端文字仍沿用当前 Locale。本期没有语言字段,不承诺两者语言绝对一致,也不改变后端语言设置。
- Given App 语言未知或音频缺失,When 前台处理通知,Then 由前端采用自身默认语种或仅保留文字;不得引用不存在的资源。
- 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 映射。
- 短时间连续通知或多笔订单:前端采用单一入口和串行播放器;订单号加场景不是可靠事件 ID,同场景再次发生时不能盲目永久去重。没有事件时间/版本,不能仅凭 payload 完整判别迟到、乱序或历史。
- App 原生接收回调和 uniPush 回调可能同时到达:必须有明确的通知展示/声音责任方。
- App 转后台时停止前台播放并清理待播队列;恢复前台不补播后台收到的消息或被清理的事件。
- 前台音频须在实际 App 包内且播放器可访问;本期不要求打包为系统通知声音,也不创建场景/语言通知渠道。
- 老版本或缺少音频时保持普通文字通知,不导致接收逻辑失败;发布前须真机验证,不能仅凭 payload 构造测试判定兼容。
- 未登录/已换账号时前端停止和清理队列;保留现有单设备 token 与会话机制。本期无接收用户字段,payload 自身不能判断迟到通知属于哪个账号。
Requirements
Functional Requirements
- FR-001: MUST 沿用现有用户业务通知,每个成功事件只构造同一条含文字和语音信息的通知,不另发语音通知。
- FR-002: MUST 明确标识上表七种事件语义;只覆盖实际存在的用户通知,不新增触发时机或收件人。
- FR-003: MUST 保留历史 ddId/state/type/pushType 契约;新增场景与语音语义不得占用旧 type=0,也不得通过匹配中文正文判断。
- FR-004: MUST 只新增 eventScene 一个字段;取消 eventId、voiceLanguage、recipientUserId、eventTime,不增加替代字段、声音 URL 或语音接口。入库和发送仍复用同一业务 body,但不生成事件 ID。
- FR-005: MUST 提供 eventScene 与短语音文案映射接入文档;语种选择、音频制作与打包由前端负责,建议本地预制、不播敏感或冗长订单信息。本任务不交付音频。
- FR-006: MUST 保持后端文字的当前用户 Locale 机制,不新增语音语言字段;接入文档明确前端按自身语言选资源,并注明不能保证与通知文字语种完全一致。
- FR-007: MUST 在文档明确前端仅在 App 前台统一播放一次;后台/锁屏不启动语音,保持原有通知行为,转后台停止播放并清理队列,恢复时不补播。本任务通过后端元数据支持该规则,不实现客户端播放器。
- FR-008: MUST 在默认 uniPush 和既有 FCM/APNs 的完整 payload 中保留 eventScene;通道选择、声音配置、失败不回退、端别凭据隔离保持不变。
- FR-009: MUST 不修改或部署云函数,不传递新的自定义通知 sound、不新建通知声音渠道;前台从受控的场景/语言映射选择本地音频,不接受任意文件路径或声音 URL。若现有云端不能保留 payload,报告限制并请求方向。
- FR-010: MUST 保留消息中心独立入库;本范围内有有效用户但无设备地址不能跳过记录生成。
- FR-011: MUST 在实际业务状态成功且事务提交后生成成功事件,失败/回滚不发;自配送及出餐等现有非统一提交后路径需按本范围收口。
- FR-012: MUST 隔离推送与播放失败,不改变支付、配送、取消等原业务结果,不把入库成功等同于外部到达或播放成功。
- FR-013: MUST 文档明确前端采用统一接收入口、不在点击/消息列表/恢复时补播;可利用 SDK 已有消息标识,但本期不提供服务端事件 ID、不承诺跨回调精确去重。
- FR-014: MUST 将普通业务推送与通话消息分流,不调用 CallKit/PushKit 或复用 voIPToken 来实现订单语音。
- FR-015: MUST 不修改服务端系统通知声音和渠道配置,文档提醒前端不得强制音量、通话或后台保活绕过限制,须分别核验 Android/iOS 权限、静音和音频中断;不能把媒体播放与通知声音开关视为相同能力。
- FR-016: MUST 保持旧 App 的文字通知和已知跳转字段;文档规定新 App 点击按 pushType 区分外卖/闪送,由前端对接其实际详情页,不把闪送订单号当外卖查询。实际页面或路由不在本任务实现范围。
- FR-017: MUST 仅修改必需后端代码并补充场景、事务及兼容注释,不改 App、原生插件、云函数、后台/锁屏声音、废弃支付入口、商家/骑手新单语音或 PC 页面。
- FR-018: MUST 按 specify → plan → tasks → implement 记录进度,原分支开发,不创建或切换分支;实际测试、构建、音频资源及真机验证分别记录,不将模拟检查报告为完整功能验收。
Key Entities
- 通知场景:既有用户通知的实际成功动作,由 eventScene 描述;不是订单状态,不包含新增事件唯一标识、时间、用户或语种。
- 消息中心记录:已有 push_message;新增场景信息放在 JSON body,列表读取不会自动播放。
- 本地语音资源(前端负责):事件与语言确定的短音频;前端验证播放器可访问、文案、时长及打包结果,本任务仅描述接入要求。
- 播放控制(前端负责):依赖前台状态、统一接收入口和客户端/SDK 已有信息;不改变 isRead,本期不额外提供去重、账号或排序元数据。
Success Criteria
Measurable Outcomes
- SC-001: 上表七种实际用户通知在受测成功动作中均有正确事件分类;失败动作/事务回滚对应成功事件数量为 0。
- SC-002: 新增场景契约的字段集合只有 eventScene,七种允许值完整;后端原文字本地化保持不变,前端文档无不存在的音频 URL 或被取消字段依赖。
- SC-003: 前端接入文档明确有效前台事件在闲置播放器收到后 2 秒内开始播报的目标,以及后台/锁屏不新增播报、恢复不补播规则;实际耗时及声音效果交由前端真机验收。
- SC-004: 同一条后端通知的入库/外发 body 内容一致;文档说明单一前台入口和历史不补播要求,同时明确没有事件 ID/时间/用户/语种字段的限制,不宣称实现精准去重、乱序判断或跨账号校验。
- SC-005: 缺失设备地址或外部通道异常时,范围内受测事件仍产生一条用户消息记录,业务结果不受影响,不出现双通道补发。
- SC-006: 商家/骑手服务端通知、通道配置与四个历史 payload 字段的定向回归无行为改变;App、原生插件、PC、云函数源码零改动,客户端声音与跳转真机核验单独交接。
Assumptions / Dependencies
- 2026-10-09 用户已确认书面规格,并随后明确 App 由前端人员实现,本任务只交付后端与接入文档;本地 App checkout 缺少闪送页不再阻断后端工作,不新增页面、不修改或切换 App 仓库。
- 后端文字沿用 LocaleUtils 的 zh_CN/zh_TW/en_US/th_TH 和繁中默认,不恢复已移除的 vi。本期不传音频语种,前端用自身语言设置;客户端资源与翻译由前端负责。
- 本规格以仓库源码为依据;实际服务器环境变量、现有云端 payload 透传、签名 App 和最终音频尚未验证。云函数及后台/锁屏语音已排除;App 音频、实现、打包和真机由前端负责。
- 音频文件或客户端验收没有完成,不妨碍本任务完成后端代码及文档交付,但必须明确“后端已验证”不等于“App 语音已上线”。
- 不做远端部署、数据库操作或生产推送试发;这些操作需要用户明确要求。代码与本地验证完成后仍单独列出真机/部署未验证项。
- 新规格补充 028 的用户通知与 033 的通道路由;不改变旧任务历史验收状态,本期 App 责任以用户最新要求为准。
平台能力参考
范围变更记录
2026-10-09 用户确认 eventScene 的播放场景用途,并明确其他新增字段不要。已精简规格、计划、任务和接入文档:只保留一个新增场景字段,撤销对服务端事件 ID、语种、接收用户和时间的依赖。产品代码尚未开始。
2026-10-09 用户确认继续后在原分支实施:用户专用 JSON 工厂仅增加 eventScene;骑手、自配送、出餐、LINE/OMG 支付和现有外卖取消接入七场景。无地址仍走消息入库,提交后通知与通知准备失败隔离;商家/骑手 body 保持原样。公共控制器测试集中在 UserOrderNotificationSceneTest,覆盖实际参数、回滚、失败、直接送达及旧状态兼容。未新增 SQL、云函数、App 或 PC 修改。
最终审查发现旧资料编辑复用出餐通知:已改为由真正出餐入口明确传 MERCHANT_READY,旧编辑只保留历史文字通知、不加场景。原状态 0/1/2 的三例失败测试后通过;定向 152/152,独立商家/场景测试 33/33,修复后模块构建通过。完整 admin 回归 947 项中 946 通过、1 项基线泰文 key 缺失失败;详见 tasks.md,不声称全量回归全绿或 App 已上线。