# Feature Specification: 可切换的原生业务推送通道 **Feature Branch**: `033-native-push-routing` **Created**: 2026-09-29 **Status**: 服务端路由与随包 classpath 凭据已实现;默认 `uni`,原生通道未启用且真机投递未验证 **Input**: 为现有普通业务通知增加 Android FCM 与 iOS APNs 原生发送能力,以服务端配置切换,重启后生效;交付时继续默认使用 uniPush,不启用原生通道。仅涉及服务端;音视频通话推送不在范围内。 ## 背景与边界 当前业务推送通过 `PayPush` 调用用户、商家、骑手三套 uniCloud 云函数,部分路径通过 `PushEvent` 写入 `push_message`。`info_user` 已有 `cid`、`cidType`、`deviceToken`:`cid` 是 uniPush 标识,`deviceToken` 承载 Android FCM token 或 iOS APNs device token,两者不可互用。用户已确认同一账号只允许一台手机登录,新手机登录后旧手机登录状态失效,因此本功能沿用现有单设备字段,不新增推送设备表。 本功能覆盖当前仍在使用的普通业务通知,包括订单、闪送、支付结果及平台/商家通知等现有发送路径;只替换发送通道,不增加新的业务触发时机或文案。不修改已废弃的支付/订单入口、临时测试推送接口,也不改动音视频通话推送。App 端 token 获取、上报及通知展示不在本功能实现范围;服务端以已有上报字段为输入。 ## User Scenarios & Testing *(mandatory)* ### User Story 1 - 默认继续使用 uniPush (Priority: P1) 运维部署新版本后,不做任何切换,普通业务通知仍按现有路径发送,不因原生凭据尚未部署而影响服务启动。 **Why this priority**: 先交付原生能力但暂不启用,必须保证当前线上推送行为不变。 **Independent Test**: 不设置通道配置,触发用户、商家、骑手各一条现有业务通知,验证只调用对应 uniCloud 通道。 **Acceptance Scenarios**: 1. **Given** 未配置推送通道或配置为 `uni`,**When** 任一普通业务通知触发,**Then** 仅走现有 uniPush 通道,不调用 FCM/APNs。 2. **Given** 未部署原生通道凭据,**When** 服务以默认配置启动,**Then** 启动成功且现有 uniPush 可用。 3. **Given** 已部署原生发送代码,**When** 音视频通话推送触发,**Then** 继续按原有独立流程处理,不受普通业务通知开关影响。 --- ### User Story 2 - 配置切换后 Android 走 FCM (Priority: P1) 运维将普通业务推送配置切换为原生并重启服务后,Android 接收方通过对应 App 的 FCM 项目收到现有通知。 **Why this priority**: 这是 Android 原生通知的核心能力,同时必须避免误把 uniPush CID 当作 FCM token。 **Independent Test**: 在原生配置下,分别向用户、商家、骑手 Android 账号触发通知,验证使用各自 Firebase 项目和 `deviceToken`,且未调用 uniCloud。 **Acceptance Scenarios**: 1. **Given** 通道为 `native`、接收方系统为 Android 且有有效 `deviceToken`,**When** 普通业务通知触发,**Then** 通过该接收端对应的 Firebase 项目向 `deviceToken` 发送一次;不得把 `cid` 传给 FCM。 2. **Given** 用户、商家、骑手分别使用不同 Firebase 项目,**When** 三端各有通知触发,**Then** 每条通知使用对应项目凭据,不串用项目。 3. **Given** 普通商家、夜市商家或商家子账号是接收方,**When** 原生通知触发,**Then** 均按商家端凭据路由。 4. **Given** 门店存在符合当前通道发送条件的已登录分管账号,**When** 订单通知触发,**Then** 只通知这些分管账号,不同时通知主账号;如果没有符合条件的分管账号,才回退通知可用的主账号。切换原生通道不得改变这一收件人优先级。 --- ### User Story 3 - 配置切换后 iOS 走 APNs (Priority: P1) 同一原生配置下,iOS 接收方通过对应 App 的 APNs 普通通知证书收到现有通知。 **Why this priority**: 只有 Android FCM 不满足“三端全切原生”的目标。 **Independent Test**: 在原生配置下,分别向用户、商家、骑手 iOS 账号触发通知,验证使用各自普通推送证书、正确环境与 `deviceToken`,且未调用 uniCloud。 **Acceptance Scenarios**: 1. **Given** 通道为 `native`、接收方系统为 iOS 且有有效 `deviceToken`,**When** 普通业务通知触发,**Then** 通过该接收端对应的 APNs 普通通知证书向 `deviceToken` 发送一次;不得使用 `voIPToken`。 2. **Given** 运行环境为开发或生产,**When** 发送 iOS 通知,**Then** 选择相应 APNs 网关并使用该接收端对应的同一份普通推送证书,不混用三端凭据。 3. **Given** 原有通知带有订单标识、业务类型或点击跳转所需 payload,**When** 改走 APNs,**Then** 这些业务字段仍可供接收端识别。 --- ### User Story 4 - 安全切换、失败隔离与回退 (Priority: P1) 运维能通过修改服务端配置并重启,在 uniPush 与原生通道间切换。单次通知只选择一个通道;发送失败不影响订单等业务操作。 **Why this priority**: 切换期间必须避免重复通知、业务流程被第三方推送故障阻断,以及随包凭据被写入日志或向非授权方分发。 **Independent Test**: 用缺失 token、原生服务异常、缺失凭据及配置回退场景验证发送结果、业务结果与消息中心记录。 **Acceptance Scenarios**: 1. **Given** 通道为 `native` 且原生 token 缺失或发送失败,**When** 普通业务通知触发,**Then** 记录可定位的失败信息,不自动补发 uniPush,也不改变原业务操作结果。 2. **Given** 原路径会通过 `PushEvent` 写入消息中心,**When** 任一通道发送成功、失败或 token 缺失,**Then** 保持原有入库语义,每次业务通知最多写入一条记录,不因切换重复入库。 3. **Given** 通道从 `native` 改回 `uni` 并重启,**When** 后续通知触发,**Then** 重新使用 `cid` 和 uniPush,不再调用 FCM/APNs。 4. **Given** 原生模式所需的三端 FCM 或 APNs 凭据配置缺失、无法读取,**When** 服务启动,**Then** 明确报错并拒绝以不完整的原生配置启动;默认 uni 模式不受影响。 ### Edge Cases - `cidType` 为空或不是 `android`/`ios`:原生模式下不猜测平台,不尝试使用错误通道;记录失败,保持业务和原有消息中心行为。 - `deviceToken` 为空、失效或来自另一 App/项目:不把 `cid` 当作替代 token;记录失败结果,失效 token 的后续处理应避免误删新手机刚更新的 token。 - 同一账号换手机或 token 刷新:后端始终以当前账号最新上报的 `cidType` 与 `deviceToken` 为发送目标;本功能不改变现有登录互斥机制。 - 批量发送含不同接收端或系统:按接收端、系统及相应项目/证书分组,不把不同项目 token 混发,也不因部分失败重复发送成功项。 - 业务代码直接调用底层 uniPush 方法:在范围内的普通业务通知必须纳入统一路由;已废弃代码、临时测试推送接口及音视频通话流程除外。 - 在用路径存在“CID 为空则不选择收件人/不触发推送”的前置判断:默认 uni 模式维持原判断效果;原生模式应以当前账号的 `deviceToken` 判断可发送性,不得因 CID 为空漏掉有原生 token 的收件人。 ## Requirements *(mandatory)* ### Functional Requirements - **FR-001**: 服务端 MUST 提供普通业务推送的全局通道配置,取值为 `uni`/`native`,未配置时默认为 `uni`;修改后重启生效,不新增管理后台实时开关。 - **FR-002**: 默认 `uni` 模式 MUST 保持现有用户、商家、骑手 uniCloud 发送行为与消息中心入库行为;不得要求原生凭据存在。 - **FR-003**: `native` 模式 MUST 根据接收端类型和 `cidType` 路由:Android 使用对应 App 的 FCM,iOS 使用对应 App 的 APNs;普通商家、夜市商家及商家子账号均属于商家端。 - **FR-003a**: 门店订单通知 MUST 保持现有“符合条件的分管账号优先、没有时主账号兜底”规则,不得因切换通道而同时推送两类账号;分管账号的发送地址资格按当前通道判断。 - **FR-004**: 原生发送 MUST 只使用 `deviceToken`,uniPush MUST 只使用 `cid`;不得相互复制或替代,`voIPToken` 不得用于普通通知。 - **FR-005**: 用户、商家、骑手三套 Firebase 项目及 APNs 普通通知证书 MUST 独立配置并正确选择;iOS MUST 区分开发与生产环境。 - **FR-006**: 三端 FCM 服务账号、普通 APNs 证书及其 `packetName/pwd` 配置 MUST 分别以 `user`、`merchant`、`rider` 明确命名并放在 `ruoyi-admin/src/main/resources/business-push/`,由 classpath 读取。用户已明确要求这些真实凭据提交 Git 并随 JAR 打包;不得把私钥或密码写入日志、规格正文或推送响应。仓库和构建产物必须按含密钥材料管理。 - **FR-006a**: 每端 `ios.p12` 与 `iosdev.p12` 经核对文件完全相同,资源中每端只保留一份 APNs 普通推送 `.p12`。`development`/`production` 只切换 APNs 网关,不切换证书文件;三端证书、Firebase 项目和 topic 不得混用。 - **FR-007**: 原生模式的必要凭据不完整时 MUST 在启动阶段明确失败;默认 uni 模式 MUST 可在无原生凭据时正常启动。 - **FR-008**: 所有当前在用的普通业务推送路径 MUST 使用一致的通道选择规则,包括适用的单发、批量及直接调用现有底层方法的路径;不改变业务触发时机、文案和 payload 语义。临时测试推送接口不在范围内。 - **FR-009**: 单次业务通知 MUST 只使用一个外部发送通道;原生发送失败或缺少 token 时 MUST NOT 自动回退到 uniPush,且 MUST NOT 使原业务操作失败。 - **FR-010**: 对现有会入库的推送,通道切换 MUST NOT 增加或漏掉 `push_message` 记录;消息中心记录与外部发送结果保持解耦。 - **FR-011**: 实现范围 MUST 排除音视频通话推送、已废弃订单/支付入口及 App 端代码;不新增推送业务场景或改变登录互斥规则。 - **FR-012**: 本功能 MUST 沿用现有 `info_user` 单设备字段,不新增数据库字段或设备表。 - **FR-013**: 外卖和闪送新单向多名骑手开放时,仅在 `native` 模式对 Android 骑手按相同标题、正文及 payload 分组,使用骑手端 FCM multicast 发送;iOS 骑手保留逐设备 APNs 请求,`uni` 模式保留原有逐骑手发送方式,不接入旧批量方法。各骑手的语言、消息中心记录和发送失败处理 MUST 保持独立,原生失败不得回退 uniPush。 - **FR-014**: 服务端 MUST 提供常驻的匿名 HTTP 原生推送测试接口,调用者直接提供接收端 `appType`、设备 `platform`、`deviceToken`、标题和正文;接口不读取账号、不验证 token 归属、不切换全局业务推送模式。Android 直发对应端 FCM,iOS 固定走 APNs 正式网关。只按需初始化所选端凭据,成功响应只表示 FCM/APNs 接受请求,失败响应提供不含 token/密钥的错误类别。该接口不发布业务事件、不写入消息中心。公网部署时任何调用者均可使用该接口发送测试消息,属于明确接受的匿名调用风险。 ### Key Entities *(include if feature involves data)* - **接收账号**:已有账号记录,包含接收端类型、`cid`、`cidType`、`deviceToken`;同一账号仅保留当前手机的一组推送标识。 - **普通业务通知**:已有业务事件生成的标题、正文、payload 与接收方;发送渠道是可替换属性,不改变业务含义。 - **消息中心记录**:现有 `push_message`,继续按原路径写入,不作为外部发送成功的证明。 - **通道配置与凭据**:服务端全局 `uni`/`native` 配置,以及已提交资源目录的三端 FCM/APNs 凭据;不属于业务数据库数据。 ## Success Criteria *(mandatory)* ### Measurable Outcomes - **SC-001**: 未配置开关的部署中,受测在用普通业务通知 100% 仍选择原 uniPush 通道,且无原生凭据时服务可启动。 - **SC-002**: 原生配置下,用户、商家、骑手 × Android、iOS 六种路由组合均选中正确凭据与 `deviceToken`,未把 `cid` 或 `voIPToken` 发送到原生通道。 - **SC-003**: 受测通知在正常发送、token 缺失和发送异常场景下,单次均不出现双通道外发;现有会入库的路径每次最多生成一条消息记录。 - **SC-004**: 模拟 FCM/APNs 不可用或 token 无效时,原业务操作结果与不发送推送时一致;失败可通过日志定位到接收端、平台与错误类别,日志不包含完整 token 或凭据。 - **SC-005**: 测试配置切回 `uni` 并重启后,后续受测通知恢复原 uniPush 发送路径;音视频通话路径保持原行为。 - **SC-006**: 保持业务模式为 `uni` 时,匿名测试接口仍能按所传端类型与平台分别向 FCM/APNs 正式网关发送,不需要修改配置或重启;错误响应不泄露设备 token,业务发送路线保持原样。 ## Assumptions - 用户已确认同一账号只允许一台手机登录,且新手机登录后旧手机登录状态失效;本功能依赖现有机制,不重新实现登录互斥。 - App 端会上报真实 FCM/APNs `deviceToken` 与正确的 `cidType`;App 端变更和真机展示不包含在此次服务端交付范围。 - 原生能力交付后仍保持 `uni` 为默认值;凭据虽随包提供,只有运维确认 App 端 token、将服务端配置切为 `native` 并重启,才会启用原生通道。 - 音视频通话使用其他推送流程,不受本次普通业务通知通道开关控制;`voIPToken` 及 VoIP 证书不用于本功能。 - 证书与 Firebase 服务账号由用户提供并提交到本仓库;仓库访问、构建产物分发和凭据轮换由部署方按含密钥材料管理。默认 `uni` 下不加载这些凭据。