Explorar el Código

docs: specify foreground-only user notification voice

qmj hace 1 semana
padre
commit
b0c367e983

+ 31 - 0
specs/035-user-notification-voice/checklists/requirements.md

@@ -0,0 +1,31 @@
+# Specification Quality Checklist: 用户业务通知语音提醒
+
+**Created**: 2026-10-09
+
+**Feature**: [spec.md](../spec.md)
+
+**进度**:实现中;以下是规格文本自检,不是生产代码测试、资源验收或真机验证。
+
+## 范围与需求自检
+
+- [x] CHK001 记录同一条通知同时提供文字/短语音,不额外发第二条语音通知。
+- [x] CHK002 覆盖截图三个核心节点和其他实际存在的用户通知,列出逐场景文案。
+- [x] CHK003 明确外卖/闪送/商家自配送差异,部分取件和跳过开始配送不产生虚构事件。
+- [x] CHK004 保留旧 payload,明确不能复用 type=0 或根据旧 state 猜场景。
+- [x] CHK005 记录无 CID/token 消息中心仍入库,事务失败/回滚不产生成功通知。
+- [x] CHK006 仅做前台语音,记录事件去重、历史不补播、转后台停止播放及平台声音限制。
+- [x] CHK007 记录四语言实际前台音频、App 打包资源及旧客户端普通通知回退验收。
+- [x] CHK008 排除云函数修改、后台/锁屏语音、通话、PC、商家/骑手新单语音、缺失通知补建和远端部署。
+- [x] CHK009 记录现有云端 payload 透传需联调验证与源码分支事实,未声称线上或真机已经验证。
+- [x] CHK010 使用当前 spec-kit 模板必需的用户故事、验收场景、需求、实体、可衡量结果和依赖,无模板占位符。
+
+## 进入下一阶段
+
+- [ ] CHK011 用户审阅并确认书面 spec 后进入 plan;尚未开始编写实现计划、任务分解或产品代码。
+- [ ] CHK012 实现计划确定后再生成 tasks,并选择执行方式;不得把本清单的自检结果计为实现完成。
+
+## 用户已确认与待审阅的区别
+
+用户已确认总体方案和原分支 spec-kit 工作方式,不重复询问这些已确认事项。本次新增书面 spec 需按方案设计技能审阅,再进入计划与实现;期间不创建/切换分支,不提交或推送产品代码。
+
+2026-10-09 用户收缩本期范围:只做前台语音提醒,后台/锁屏保持原有通知,云函数不修改、不部署。此变更已同步到规格范围、用户故事、需求和验收条件;产品代码尚未开始。

+ 165 - 0
specs/035-user-notification-voice/spec.md

@@ -0,0 +1,165 @@
+# 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**:
+
+1. **Given** 外卖待接单或闪送 WAITING_ACCEPTANCE,**When** 骑手接单成功且事务提交,**Then** 用户收到原文字通知,并按语言听到“骑手已接单”一次。
+2. **Given** 骑手已接单,**When** 外卖取餐成功或闪送全部取货站完成,**Then** 用户听到“您的订单已派送”一次;闪送部分取件不新增播报。
+3. **Given** 订单配送中,**When** 送达成功,**Then** 用户听到“您的订单已送达”一次。
+4. **Given** 外卖为商家自配送,**When** 商家开始配送或确认送达,**Then** 使用同样的派送/送达短语音,不误播“骑手已接单”。
+5. **Given** 抢单、取件或送达失败,或者事务回滚,**When** 请求返回,**Then** 不生成该次成功状态通知或语音事件。
+
+### User Story 2 - 只在前台播放且不重复 (Priority: P1)
+
+通知在 App 前台由统一接收逻辑播放;后台/锁屏保持原有通知,不启动语音播放器,不依赖 JavaScript 在后台持续运行。
+
+**Independent Test**: Android 和 iOS 前台收到同一场景只播一次;后台、锁屏、点击通知和恢复 App 均不新增或补播语音。
+
+**Acceptance Scenarios**:
+
+1. **Given** 用户 App 在前台,**When** uniPush 或原生通知到达,**Then** 统一处理场景并播放一次,不叠加普通铃声、第二条本地通知或另一个监听器的语音。
+2. **Given** App 在后台或锁屏,**When** 通知正常到达,**Then** 保持原有系统通知行为,不设置新的订单语音声音,不启动前台播放器或通话。
+3. **Given** 同一通知到达多个回调,**When** 客户端处理,**Then** 使用相同事件标识避免重复展示/播报。
+4. **Given** 用户点击通知、打开消息列表或恢复历史数据,**When** 详情导航或消息加载,**Then** 不再次播报历史事件。
+5. **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**:
+
+1. **Given** 用户语言为简中、繁中、英文或越南语,**When** 受支持的业务通知触发,**Then** 文字和音频使用同一种语言;不播订单号、联系方式或地址。
+2. **Given** 用户语言未知或没有该语种音频,**When** 通知触发,**Then** 使用明确的默认语种策略;不得展示原始 i18n key 或引用不存在的资源。
+3. **Given** 商家或骑手是通知接收人,**When** 同一业务动作发生,**Then** 其原有铃声和 payload 行为不因用户语音改造而改变。
+
+### User Story 4 - 不影响消息中心及业务操作 (Priority: P1)
+
+设备没有 CID/token 或外部推送失败时,用户仍可在消息中心查看已经成功发生的业务事件。
+
+**Independent Test**: 对有用户但无推送地址、通道异常、旧 App、音频缺失和事务回滚分别检查通知/入库调用与业务结果。
+
+**Acceptance Scenarios**:
+
+1. **Given** 用户存在但无可用 CID/token,**When** 范围内事件成功发生,**Then** 消息中心仍保存一条记录,跳过外部发送,不改变业务结果。
+2. **Given** uniPush/FCM/APNs 投递失败,**When** 发送执行,**Then** 不回滚订单,不再次向另一通道发送,不重复入库。
+3. **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 配套,也不改变旧任务的历史验收状态。
+
+## 平台能力参考
+
+- [DCloud 服务端推送 API](https://doc.dcloud.net.cn/uniCloud/uni-cloud-push/api.html):本期沿用 payload 传递,不启用自定义 sound 或离线语音提醒。
+- [Apple UNNotificationSound](https://developer.apple.com/documentation/usernotifications/unnotificationsound) 与 [Android 通知渠道](https://developer.android.com/develop/ui/compose/notifications/channels):系统通知声音是独立能力,本期维持现状,不增加资源或渠道适配。
+- [Firebase Android 消息接收](https://firebase.google.com/docs/cloud-messaging/android/receive-messages):notification + data 在后台由系统展示,不能假定 JS/前台 onMessageReceived 会立即执行。