Переглянути джерело

docs(push): document native rollout and verification

qmj 2 тижнів тому
батько
коміт
3893fc97f0

+ 6 - 5
docs/order-push-overview.md

@@ -1,6 +1,6 @@
 # 訂單推送說明(測試參考)
 
-**最後更新**: 2026-06-25
+**最後更新**: 2026-09-29
 **用途**: 列出訂單流程中所有會觸發推送的場景,供測試驗證。
 
 > 本文推送文案以**繁體中文**展示。實際推送時文案會跟隨接收人的語言設置自動切換(支援 簡中 / 越南語 / 英文 / 繁中),但下方表格統一以繁中舉例。
@@ -9,8 +9,9 @@
 
 ## 一、推送基本說明
 
-- 每條推送會出現在**兩個地方**:① 手機通知欄(僅 iOS);② App 內「消息中心」列表(iOS / Android 都有)。
-- **Android 手機沒有通知欄推送**(暫未接入 Android 推送通道),只能在 App 內消息列表看到。
+- 目前服務端普通業務推送**預設仍走 uniPush**;現有測試環境的 iOS 通知欄與 App「消息中心」按原流程核對。消息中心由原業務路徑寫入,外部推送投遞成功與否不能只憑消息記錄判定。
+- 服務端已有 Android FCM / iOS APNs 原生通道,但**尚未啟用**,也未包含 App 端 token 上報改造;不要把原生通知欄接收當成本文件目前的既有結果。啟用條件與真機驗收見 `specs/033-native-push-routing/quickstart.md`。
+- 商家門店訂單通知沿用「已登入且有當前通道可用地址的分管帳號優先;沒有符合條件的分管帳號才由主帳號兜底」的規則,兩類帳號不會同時通知。原生模式用當前 `deviceToken` 判斷,預設 uni 模式用 `cid`。
 - 推送語言**跟隨接收人的語言設置**(簡中 / 越南語 / 英文 / 繁中),同一場景不同語言賬號看到的文案不同。
 - 正文中的 `{訂單號}` 是實際訂單編號,會替換成真實號碼。
 
@@ -56,7 +57,7 @@
 每個場景觸發後,按接收人登錄對應端(用戶 App / 商家端 / 騎手端)檢查:
 
 1. **iOS**:通知欄收到推送 + App 內消息列表有新記錄。
-2. **Android**:App 內消息列表有新記錄(通知欄**不會**有,屬正常)。
+2. **Android(目前預設 uni 模式)**:核對 App 內消息列表;FCM 原生通知欄需要切換原生通道及 App 端配套後另行真機驗證。
 3. **文案**:標題和正文與上表一致;正文末尾的訂單號與實際訂單一致。
 4. **多語言**:把接收賬號切到不同語言,文案應隨之變成對應語言。
 5. **接收人**:確認只有「推送對象」列裡的人收到,其他人不收到。
@@ -67,4 +68,4 @@
 
 - **貨到付款下單(場景 2)的通知不帶訂單號**:商家 / 騎手通知欄只看到「有新訂單」,看不到具體訂單號(點擊進 App 仍可看到訂單詳情)。
 - **商家出餐文案略冗餘**:顯示為 `商家已經出餐,訂單號:,NO:123`(冒號和逗號連在一起),現狀如此。
-- **暫不存在的推送**(本次不在範圍,測試時不用驗證):取消訂單、退款、超時無騎手、抽成返還、自動恢復營業等通知,以及 Android 通知欄推送。
+- **暫不存在的業務通知**(本次不在範圍,測試時不用驗證):取消訂單、退款、超時無騎手、抽成返還、自動恢復營業等通知。Android FCM 服務端能力已實作,但原生模式未啟用、真機推送仍待驗證。

+ 7 - 6
specs/012-im-user-integration/code-status.md

@@ -1,6 +1,6 @@
 # Code Status: 订单/支付代码现状(废弃与在用)
 
-**Date**: 2026-06-25
+**Date**: 2026-09-29
 **Scope**: 全局项目现状笔记(挂在 012-im-user-integration 目录下,供后续开发参考)
 
 排查问题或新增功能时,务必区分"已废弃代码"与"在用入口"。仓库里有一批支付/订单类**代码还在但已不再使用**,参考它们的逻辑或在其上做增量改动都会改到死代码。
@@ -24,14 +24,15 @@
 
 ## 推送通道现状
 
-- `PayPush` 封装 `userPush / shPush / qsPush`(及 `*HandleLocal` 带 i18n 版)。
-- **无 Android FCM 通道**(cte_server 有 `FCMPush` + channelId qsRing/sjRing/yhRing,本项目已砍掉);仅 iOS uni 云函数(`msduser/msdrider/msdstore`)+ 入库(`PushEvent`→`push_message` 表,App 内消息中心)。
-- `cidType` 参数目前形同虚设,调用处一律传 `""`。
+- `PayPush` 封装 `userPush / shPush / qsPush`(及 `*HandleLocal` 带 i18n 版)。这些普通业务包装方法现委托 `BusinessPushService`;底层 uni 云函数 `msduser/msdrider/msdstore` 保留。
+- `business-push.mode` **默认 `uni`,原生通道尚未启用**。服务端已实现按用户/商家/骑手三端分别路由 Android FCM 和 iOS APNs;切换为 `native` 要部署三端外部凭据并重启,且需要 App 端提供真实 `deviceToken`。配置和真机检查见 `specs/033-native-push-routing/quickstart.md`。
+- `cid` 只供 uniPush;原生路由按当前账号的 `cidType` (`android`/`ios`) 与 `deviceToken` 选择 FCM/APNs,不能使用 `cid` 或 `voIPToken` 替代。`PayPush` 旧包装签名中的 `cidType` 参数仍不承担平台选择。
+- `PushEvent`→`push_message` 仍由原业务路径入库,外部发送通道不代替消息中心。音视频通话、废弃入口和临时测试推送接口未迁入此路由。
 - 异步推送用 `AsyncManager.me().execute(TimerTask)`(cte 用 `@Async` + `TransactionSynchronization.afterCommit`)。
 
 ## 订单推送覆盖度(基于在用代码)
 
-**已实现(代码定稿,均走 uniapp 云函数 + 入库,无 FCM;2026-06-25 确认范围):**
+**已实现(当前默认仍走 uniPush;原生服务端路由已备好但未启用):**
 - 用户下单 → 商家(`UserOrderController:274`)
 - 货到付款下单 → 商家+骑手(`PosOrderController.sendHdfkMessage:1180`)
 - 商家出餐 → 用户+骑手(`PosOrderShOprateController.chuCan:341`)
@@ -44,4 +45,4 @@
 - 取消/申诉类通知(用户取消、商家取消、骑手取餐失败/取消、到付解冻)—— `OrderAppealController` 整片注释,全部缺失
 - 定时任务类通知(超时无骑手退款、抽成返还、自动恢复营业)—— `TestTask` 废弃,全部缺失
 - 管理员改派骑手通知(无 assignRider 推送)
-- Android FCM 通道未接入,Android 端无系统通知栏推送
+- Android FCM / iOS APNs 虽已有服务端实现,但 App 端 token 上报与六种真机投递尚未在本功能中验证,不能把服务端代码交付视为生产原生推送已启用

+ 4 - 3
specs/033-native-push-routing/plan.md

@@ -23,6 +23,7 @@
 
 1. 原生模式有 `deviceToken`、无 `cid` 的用户仍能被收件人筛选并发送;默认 uni 模式仍按 CID 判断。
 2. 用户、骑手、商家(含夜市及子账号)不会混用 Firebase 项目或 APNs topic;相同设备的商家收件人去重按当前通道地址。
+   门店订单收件人仍是分管账号优先、主账号兜底,不同时发给两类账号。
 3. iOS 开发/生产证书与网关一致;缺失、过期、无法解锁或无私钥的证书在原生模式启动时明确失败。
 4. LINE Pay/OMG 等本来已经自行发布 `PushEvent` 的路径不会因迁移产生第二条消息记录,且 CID 为空时原生发送仍可进行。
 5. `VideoCallController`、`/testPushMessage` 和测试批量接口继续走现有行为;业务通知 payload 的 `ddId/state/type/pushType` 不丢失。
@@ -40,7 +41,7 @@
 
 `application.yml` 仅保留 `business-push.mode: uni` 和环境变量占位符;部署时分别提供 `business-push.apps.user`、`business-push.apps.merchant`、`business-push.apps.rider` 下的 `fcm-credentials-path`、`apns-certificate-path`、`apns-certificate-password`、`apns-topic`。`business-push.apns-environment` 为 `development` 或 `production`。只有 `native` 模式校验并加载三端凭据;配置中没有凭据内容。iOS 证书须对应所选环境与 topic。
 
-FCM 请求的 data 至少包含字符串形式的 `title`、`body`、原始 `payload`,并保留现有可解析 payload 的 `ddId/state/type/pushType` 字段;APNs 使用 `aps.alert.title/body`、普通通知 sound 和同名业务字段。骑手端沿用现有 `pushsound.caf`,其余端沿用现有默认声音。过大或无法解析的 payload 必须记录可定位的失败类别,不得以截断方式改变业务字段。
+FCM 请求的 data 至少包含字符串形式的 `title`、`body`、原始 `payload`,并保留现有可解析 payload 的 `ddId/state/type/pushType` 字段;APNs 使用 `aps.alert.title/body`、普通通知 sound 和同名业务字段。iOS 骑手端沿用现有 `pushsound.caf`,其余 iOS 端使用默认声音;Android FCM 使用默认声音,并为骑手标记 `qsRing` 通知渠道,最终自定义铃声取决于 App 是否创建该渠道(App 端不在本任务内)。过大或无法解析的 payload 必须记录可定位的失败类别,不得以截断方式改变业务字段。
 
 ## File Structure
 
@@ -107,7 +108,7 @@ FCM 请求的 data 至少包含字符串形式的 `title`、`body`、原始 `pay
 
 **Interfaces:** Direct业务外发调用 `BusinessPushService.send(...)`;收件人资格和去重调用 `addressKey(InfoUser)`,以当前通道的地址而非固定 CID 判定。
 
-- [ ] Write/adjust tests: CID 空而 native token 有效的商家子账号、用户及骑手仍可收;uni 模式 CID 空保持原判断;商家同一地址只外发一次但原有消息入库保持;LINE Pay/OMG 发布事件各一次且原生可发;测试/视频路径不被路由改动。
+- [ ] Write/adjust tests: CID 空而 native token 有效的商家子账号、用户及骑手仍可收;uni 模式 CID 空保持原判断;分管账号可用时只通知分管账号、不可用时才回退主账号;商家同一地址只外发一次但原有消息入库保持;LINE Pay/OMG 发布事件各一次且原生可发;测试/视频路径不被路由改动。
 - [ ] Run `mvn -pl ruoyi-admin -am -Dtest=MerchantNotificationRouterTest,DeliveryOrderNotificationServiceTest,OmgPaymentNotifyServiceTest,PosOrderShOprateControllerTest,PosOrderQsOprateControllerTest,PosOrderAdminStatusControllerTest,LinePayControllerTest -Dsurefire.failIfNoSpecifiedTests=false test` with JDK 21 and verify the new assertions fail first.
 - [ ] 迁移在用直接调用,替换仅 CID 判断,保持原事务后、异步及文案时机;不碰失效方法。
 - [ ] Re-run the same directed command and confirm pass; inspect staged names and commit only Task 5 files.
@@ -131,4 +132,4 @@ FCM 请求的 data 至少包含字符串形式的 `title`、`body`、原始 `pay
 
 ## Verification Command Setup
 
-PowerShell 中仅对当前命令临时设置 `$env:JAVA_HOME='C:\Users\99029\.jdks\graalvm-jdk-21.0.12+7.1'` 和 `$env:PATH="$env:JAVA_HOME\bin;$env:PATH"`。计划审阅通过后执行测试;此阶段没有运行编译或测试。
+PowerShell 中仅对当前命令临时设置 `$env:JAVA_HOME='C:\Users\99029\.jdks\graalvm-jdk-21.0.12+7.1'` 和 `$env:PATH="$env:JAVA_HOME\bin;$env:PATH"`。2026-09-29 已运行 13 类定向测试(83 个用例,0 失败)及 `mvn -pl ruoyi-admin -am -DskipTests package`,退出码均为 0;真实 FCM/APNs 凭据、设备投递和 App 展示未在本机验证,启用前按 `quickstart.md` 执行真机验收。

+ 32 - 0
specs/033-native-push-routing/quickstart.md

@@ -0,0 +1,32 @@
+# 普通业务推送:原生通道部署与回退
+
+**状态(2026-09-29)**:服务端具备 FCM/APNs 路由能力,默认 `uni`,**尚未启用原生推送**。本功能不处理音视频通话,也不包含 App 端 token 获取、上报和展示改造;未完成真机验证前不要切换生产环境。
+
+## 配置与凭据
+
+`ruoyi-admin/src/main/resources/application.yml` 使用下列环境变量。凭据文件应放在受保护的部署目录,运行账号只需读取权限;不要把 JSON、`.p12`、密码或真实 token 提交到仓库、放进镜像公开层或写入日志。
+
+| 设置 | 环境变量 | 默认/要求 |
+| --- | --- | --- |
+| 通道 | `BUSINESS_PUSH_MODE` | `uni`;可选 `native`,重启后生效 |
+| APNs 环境 | `BUSINESS_PUSH_APNS_ENVIRONMENT` | `production`;测试包用 `development` |
+| 用户端 FCM 服务账号文件 | `BUSINESS_PUSH_USER_FCM_CREDENTIALS_PATH` | 原生模式必填 |
+| 商家端 FCM 服务账号文件 | `BUSINESS_PUSH_MERCHANT_FCM_CREDENTIALS_PATH` | 原生模式必填 |
+| 骑手端 FCM 服务账号文件 | `BUSINESS_PUSH_RIDER_FCM_CREDENTIALS_PATH` | 原生模式必填 |
+
+用户、商家、骑手三端还分别需要 `BUSINESS_PUSH_{USER|MERCHANT|RIDER}_APNS_CERTIFICATE_PATH`、`BUSINESS_PUSH_{USER|MERCHANT|RIDER}_APNS_CERTIFICATE_PASSWORD`、`BUSINESS_PUSH_{USER|MERCHANT|RIDER}_APNS_TOPIC`。路径指向各 App 对应的**普通推送** PKCS#12 (`.p12`) 证书;topic 为对应 App 的 Bundle ID,不是 VoIP topic。三端 Firebase 服务账号也必须分别属于对应 App 的 Firebase 项目。默认 `uni` 模式不需要这些凭据;`native` 模式启动时会校验三端配置并加载文件,缺失、不可读或证书无私钥/已过期时启动失败。
+
+`BUSINESS_PUSH_APNS_ENVIRONMENT=development` 使用 APNs sandbox 网关,`production` 使用正式网关。所部署证书必须支持所选环境并与 topic 匹配;这两项对应关系仍需部署者核实,配置校验无法替代 Apple 端的实际发送验证。证书到期前应定期检查并更新三端证书,更新后重启服务,再做各端测试。
+
+## 启用前条件
+
+1. App 端分别上报真实的 Android FCM token 或 iOS APNs device token 至 `info_user.deviceToken`,并把 `cidType` 设置为 `android` 或 `ios`;`cid` 只用于 uniPush,不能复制到 `deviceToken`。本次交付不实现 App 端上报。
+2. 确认新手机登录后旧手机会话失效,数据库中账号只保留当前设备 token;原生发送时服务端重新读取该账号的当前 `cidType/deviceToken`。
+3. 在非生产环境配置六套凭据、对应 APNs 环境,设 `BUSINESS_PUSH_MODE=native` 并重启。先检查服务启动,再用三端 Android/iOS 各一台真机触发订单、闪送和支付通知;核对通知栏、点击 payload、消息中心、语言与接收人。iOS 骑手使用 `pushsound.caf`;Android 骑手 FCM 带 `qsRing` 通知渠道标识,须由 App 创建并配置该渠道才能保证自定义铃声,本次不修改 App。门店订单有可用分管账号时只通知分管账号,没有时才回退主账号。
+4. 重点覆盖 CID 为空而原生 token 有效、token 缺失/失效、账号换机、预约闪送进入可抢单窗口,以及发送失败但业务操作成功。某条原生通知失败时不会自动回退 uniPush;原路径已有的消息中心记录应保持一次。
+
+## 切换与回退
+
+生产切换须先完成 App 端配套和真机验收。部署受保护凭据后将 `BUSINESS_PUSH_MODE` 设为 `native`,按实例逐一重启并检查启动及推送错误日志;一条通知只会选择当前实例的一个通道,滚动重启期间不同实例可能短暂处于不同模式。若原生投递异常,改回 `BUSINESS_PUSH_MODE=uni` 并逐一重启;之后的新通知重新使用 `cid`,已失败的通知不会由服务端自动补发。回退时确认 App 仍维护有效的 uniPush CID。
+
+自动化测试只验证路由、请求构造和错误隔离;真实 Firebase 项目、APNs 凭据、设备 token、通知展示与回退操作仍需部署环境验证。

+ 3 - 1
specs/033-native-push-routing/spec.md

@@ -4,7 +4,7 @@
 
 **Created**: 2026-09-29
 
-**Status**: Draft(对话方案已确认;待书面规格审阅)
+**Status**: 服务端已实现并通过定向测试;默认 `uni`,原生通道未启用且真机投递未验证
 
 **Input**: 为现有普通业务通知增加 Android FCM 与 iOS APNs 原生发送能力,以服务端配置切换,重启后生效;交付时继续默认使用 uniPush,不启用原生通道。仅涉及服务端;音视频通话推送不在范围内。
 
@@ -45,6 +45,7 @@
 1. **Given** 通道为 `native`、接收方系统为 Android 且有有效 `deviceToken`,**When** 普通业务通知触发,**Then** 通过该接收端对应的 Firebase 项目向 `deviceToken` 发送一次;不得把 `cid` 传给 FCM。
 2. **Given** 用户、商家、骑手分别使用不同 Firebase 项目,**When** 三端各有通知触发,**Then** 每条通知使用对应项目凭据,不串用项目。
 3. **Given** 普通商家、夜市商家或商家子账号是接收方,**When** 原生通知触发,**Then** 均按商家端凭据路由。
+4. **Given** 门店存在符合当前通道发送条件的已登录分管账号,**When** 订单通知触发,**Then** 只通知这些分管账号,不同时通知主账号;如果没有符合条件的分管账号,才回退通知可用的主账号。切换原生通道不得改变这一收件人优先级。
 
 ---
 
@@ -95,6 +96,7 @@
 - **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**: 原生凭据及证书密码 MUST 从服务端受保护的外部配置读取,不得写入源码、规格、日志或打包进公开构件。

+ 9 - 9
specs/033-native-push-routing/tasks.md

@@ -6,24 +6,24 @@
 
 ## Phase 1: 默认通道与配置(US1)
 
-- [ ] T001 [US1] 在 `ruoyi-admin/src/main/java/com/ruoyi/app/push/` 定义接收端枚举、`business-push` 配置及原生模式启动校验;`ruoyi-admin/src/main/resources/application.yml` 仅设置默认 `uni` 与非秘密占位符。
-- [ ] T002 [US1] 在 `ruoyi-admin/src/test/java/com/ruoyi/app/push/BusinessPushPropertiesTest.java` 覆盖默认启动、原生凭据缺失/无效和三端配置。
+- [x] T001 [US1] 在 `ruoyi-admin/src/main/java/com/ruoyi/app/push/` 定义接收端枚举、`business-push` 配置及原生模式启动校验;`ruoyi-admin/src/main/resources/application.yml` 仅设置默认 `uni` 与非秘密占位符。
+- [x] T002 [US1] 在 `ruoyi-admin/src/test/java/com/ruoyi/app/push/BusinessPushPropertiesTest.java` 覆盖默认启动、原生凭据缺失/无效和三端配置。
 
 ## Phase 2: 原生发送器(US2、US3)
 
-- [ ] T003 [US2] 在 `FcmPushSender.java` 实现三命名 FirebaseApp 和按接收端 FCM 发送;在 `FcmPushSenderTest.java` 验证项目隔离、payload、失效错误与无敏感日志。
-- [ ] T004 [US3] 在 `ApnsPushSender.java` 实现三端证书型 APNs HTTP/2 发送及环境选择;在 `ApnsPushSenderTest.java` 验证 topic、payload、证书错误与响应分类。
+- [x] T003 [US2] 在 `FcmPushSender.java` 实现三命名 FirebaseApp 和按接收端 FCM 发送;在 `FcmPushSenderTest.java` 验证项目隔离、payload、失效错误与无敏感日志。
+- [x] T004 [US3] 在 `ApnsPushSender.java` 实现三端证书型 APNs HTTP/2 发送及环境选择;在 `ApnsPushSenderTest.java` 验证 topic、payload、证书错误与响应分类。
 
 ## Phase 3: 统一路由与业务集成(US1–US4)
 
-- [ ] T005 [US1–US4] 在 `BusinessPushService.java` 实现 uni/native 选择、当前账号 token 读取、地址判定及异常隔离;在 `BusinessPushServiceTest.java` 覆盖默认 uni、六种原生组合及不自动回退。
-- [ ] T006 [US1–US4] 修改 `PayPush.java` 的普通业务包装方法委托 T005;保留低层 uniCloud 方法供 uni 模式及排除路径使用,验证消息中心不重复入库。
-- [ ] T007 [US2–US4] 将在用普通业务直连与 CID 前置过滤迁移到统一路由:`MerchantNotificationRouter.java`、`LinePayOrderNotificationService.java`、`OmgPaymentNotifyService.java` 和当前有效订单 Controller;同步更新相关测试。不改音视频、废弃入口或临时测试推送接口。
+- [x] T005 [US1–US4] 在 `BusinessPushService.java` 实现 uni/native 选择、当前账号 token 读取、地址判定及异常隔离;在 `BusinessPushServiceTest.java` 覆盖默认 uni、六种原生组合及不自动回退。
+- [x] T006 [US1–US4] 修改 `PayPush.java` 的普通业务包装方法委托 T005;保留低层 uniCloud 方法供 uni 模式及排除路径使用,验证消息中心不重复入库。
+- [x] T007 [US2–US4] 将在用普通业务直连与 CID 前置过滤迁移到统一路由:`MerchantNotificationRouter.java`、`LinePayOrderNotificationService.java`、`OmgPaymentNotifyService.java` 和当前有效订单 Controller;同步更新相关测试。门店订单仍是分管账号优先、无符合条件分管账号时主账号兜底,不同时通知两类账号。不改音视频、废弃入口或临时测试推送接口。
 
 ## Phase 4: 文档与验证
 
-- [ ] T008 在 `specs/033-native-push-routing/quickstart.md` 记录三端外部凭据配置、默认关闭、启用/回退、真机检查;更新 `specs/012-im-user-integration/code-status.md` 与 `docs/order-push-overview.md`。
-- [ ] T009 使用 JDK 21 运行定向测试和 `ruoyi-admin` 模块构建;核对完整 diff、暂存范围及未执行的外部 FCM/APNs 真机验证,再提交文档收口。
+- [x] T008 在 `specs/033-native-push-routing/quickstart.md` 记录三端外部凭据配置、默认关闭、启用/回退、真机检查;更新 `specs/012-im-user-integration/code-status.md` 与 `docs/order-push-overview.md`。
+- [x] T009 使用 JDK 21 运行定向测试和 `ruoyi-admin` 模块构建;核对完整 diff、暂存范围及未执行的外部 FCM/APNs 真机验证,再提交文档收口。
 
 ## Dependencies