# 原生通知语音接入(2026-10-26) 本次追加覆盖 FCM/APNs 用户通知的前台、后台和锁屏语音。uniPush、消息中心 body、原业务 payload、商家/骑手默认声音保持原规则。不使用 VoIP,不新增通知或声音下载接口。服务端默认推送通道仍为 uni,测试入口直接使用原生发送。 ## 资源交付 音频位于 [assets/native-wav](assets/native-wav/),完整文件和渠道映射见 [manifest.json](assets/native-wav/manifest.json)。63 个 WAV:9 场景 × 7 语种,16 kHz、单声道、16-bit Linear PCM,均不足 30 秒。由用户提供的 MP3 转换,原目录未修改;zh-CN/zh-TW 的语音分别统一选用提供的 zh-Hans/zh-Hant 源文件,不合并或覆盖原文件。 | 场景字段 | 值 | 文件名前缀 | | --- | --- | --- | | eventScene | RIDER_ACCEPTED | rider_accepted | | eventScene | ORDER_DELIVERING | order_delivering | | eventScene | ORDER_DELIVERED | order_delivered | | eventScene | MERCHANT_ACCEPTED | merchant_accepted | | eventScene | MERCHANT_READY | merchant_ready | | eventScene | PAYMENT_SUCCEEDED | payment_succeeded | | eventScene | ORDER_CANCELLED | order_cancelled | | eventType | FLASH_PICKUP_STOP_COMPLETED | flash_pickup_stop_completed | | eventType | FLASH_DELIVERY_STOP_COMPLETED | flash_delivery_stop_completed | 文件格式:`{场景小写}_{语言后缀}.wav`,例如 `rider_accepted_zh_hant.wav`。 | 传入/已保存语言示例 | 资源后缀 | | --- | --- | | zhHant、zh-Hant、zh-TW、zh_TW、tw、zh-Hant-TW | zh_hant | | zhHans、zh-Hans、zh-CN、zh_CN、zh-Hans-CN | zh_hans | | en、en-US | en | | th、th-TH | th | | id、id-ID、in | id | | ms、ms-MY | ms | | tl、fil、fil-PH | tl | | 未设置、空白、未知(含 vi、未指定繁简的 zh) | zh_hant | 业务通知读取现有 `GET /changeLanguages?lang=zhHant&id=用户ID` 保存的语言,使用与该接口相同的 Jedis DB 0 和原始用户 ID 键;App 在登录/切换语言时需调用既有接口。此处不改造该旧接口权限或签名。缓存不可用时通知仍发送,声音使用繁中。测试入口直接读请求 language,不查用户或语言缓存。缺场景/未知场景不猜语音,保留原默认通知声音。 ## iOS 将 WAV 作为资源加入用户 App target,确认文件真正包含在安装包的 bundle 中;也可使用系统支持的 Library/Sounds 目录。后端 `aps.sound` 直接指定文件名,同时在 userInfo 顶层放 `sound`,供前台处理。 ```json { "aps": { "alert": {"title": "订单通知", "body": "骑手已接单"}, "sound": "rider_accepted_zh_hant.wav" }, "sound": "rider_accepted_zh_hant.wav", "payload": "{\"eventScene\":\"RIDER_ACCEPTED\"}" } ``` WAV 是 Apple 支持的系统通知声音格式,不需要再转 CAF;不能只改 MP3 扩展名。申请通知及声音权限。前台可在系统回调允许 sound,或按顶层 sound 手动播放,二选一以免双响。后台/锁屏由系统播放,点击通知、加载历史消息、恢复前台不补播。静音、专注模式及用户权限可能抑制声音,后端不能强制绕过。 APNs 网关仍按现有 `apns-environment` 选择,与本功能无关;签名环境必须匹配 Token。测试接口成功仅表示推送服务接受,不表示手机已播放。 ## Android 将 WAV 复制到用户 App 原生工程 `res/raw/`,文件名保持小写字母与下划线。首次启动时按 manifest 创建全部 63 个声音渠道,确保后台通知到达前渠道已存在。渠道 ID 为 `user_voice_{不含扩展名的资源名}_v1`,例如 `user_voice_rider_accepted_zh_hant_v1`。 创建渠道时:`IMPORTANCE_HIGH`,sound URI 指向该 raw 资源,AudioAttributes 使用 `USAGE_NOTIFICATION`;渠道名称需由 App 国际化。后端发送 notification + data: ```json { "android": { "priority": "high", "notification": { "sound": "rider_accepted_zh_hant", "default_sound": false, "channel_id": "user_voice_rider_accepted_zh_hant_v1" } }, "data": { "sound": "rider_accepted_zh_hant.wav", "channelId": "user_voice_rider_accepted_zh_hant_v1", "payload": "{\"eventScene\":\"RIDER_ACCEPTED\"}" } } ``` Android 8+ 以渠道声音为准,后端传 sound 无法覆盖已有渠道设置;渠道缺失可能回退默认渠道。渠道一旦创建便不能靠重新 setSound 修改,未来更换绑定声音需双方约定新版本渠道 ID。Android 13+ 申请通知权限。后台/锁屏使用系统通知;前台由 App 根据 data.sound 播放,或使用 data.channelId 发本地通知,二选一。用户关闭渠道声音时不能保证响铃。 ## Apipost 测试 匿名 `POST /system/push/testNative`,`Content-Type: application/json`,不切换业务推送模式,不需要用户 ID。沿用已有 token 手填方式,新增可选 language;场景仍在 payload 中,payload 是字符串。 ```json { "appType": "user", "platform": "android", "deviceToken": "填写手机当前原生Token", "title": "语音测试", "content": "骑手已接单", "language": "zhHant", "payload": "{\"eventScene\":\"RIDER_ACCEPTED\"}" } ``` iOS 将 platform 改为 ios 并填写对应 APNs Token。闪送站点示例:`payload` 使用 `{"eventType":"FLASH_PICKUP_STOP_COMPLETED"}` 的 JSON 字符串。省略 language 或传未知值,应使用繁中声音。老请求不含场景时仍按原默认声音发送。 ## 验收边界 - 后端定向测试验证场景/语言映射、同一语言缓存源、缓存失败回退、FCM/APNs 载荷、匿名测试入口及 uni 行为隔离;音频逐个验证编码和时长。 - App 需验证资源打包、渠道初始化及权限后,分别测试 iOS/Android 前台、后台和锁屏,检查正确语种、单次播放、点击不补播。 - 本仓库不修改 App,不部署服务,不代表已完成真机通知投递、音频内容人工听审或系统播放验证。新用户 App 未带资源/渠道时,服务端参数本身不能使其播放这些语音。 平台依据:[Apple 自定义通知声音](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/SupportingNotificationsinYourApp.html)、[FCM 消息字段](https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages)、[Android 通知渠道](https://developer.android.com/develop/ui/views/notifications/channels)。