plan.md 14 KB

可切换原生业务推送 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 在不改变默认 uniPush 行为的前提下,为当前在用的普通业务通知增加可通过服务端配置和重启启用的三端 Android FCM / iOS APNs 原生发送能力。

Architecture: PayPush 的业务封装与现有直接业务发送点统一委托给 BusinessPushService。服务按全局 business-push.mode 选择现有 uniCloud 方法或原生发送器;原生时按显式接收端类型与数据库中当前账号的 cidType/deviceToken 选择三套独立凭据。PushEvent 继续由原调用点处理,路由层只负责外发。

Tech Stack: Java 21、Spring Boot、现有 Firebase Admin SDK、JDK HTTP/2 HttpClient、PKCS#12 APNs 普通通知证书、JUnit/Mockito。

Spec: specs/033-native-push-routing/spec.md

Global Constraints

  • 当前工作分支为 test-202609v2;用户要求不创建新分支。保留工作区已有未提交修改,尤其不修改与功能无关的 InfoUserController.java 测试接口。
  • business-push.mode 只允许 uni/native,缺省为 uni,切换后重启生效;不做后台实时开关。默认模式不能要求原生凭据。
  • cid 仅用于 uniPush,deviceToken 仅用于普通 FCM/APNs,voIPToken 不参与;单账号单设备,不改数据库结构。
  • 三端 Firebase 服务账号属于不同项目,必须初始化三个命名 FirebaseApp;iOS 使用三端各自的普通推送 .p12,按部署环境选择 APNs 开发/生产地址。凭据路径和密码从外部环境注入,不提交密钥内容。
  • 只迁移在用的普通业务通知;音视频通话、已废弃入口和临时测试推送接口保持原行为。已有 PushEvent 入库时机与次数不改变,外发失败不影响业务且不自动转发 uniPush。
  • 后端集成代码放在 ruoyi-admin,不让 ruoyi-system 反向依赖应用层;不执行数据库 SQL。仅当前 Maven 命令临时使用 JDK 21,保留现有换行风格。

Review Focus

  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 不丢失。

Technical Context

  • 现有发送点:ruoyi-admin/src/main/java/com/ruoyi/app/utils/PayPush.java 的三端包装方法、外卖/闪送通知服务、商家路由,以及 LINE Pay/OMG 支付通知的直接调用。
  • 现有持久化:ruoyi-system 的 InfoUser 和 PushEvent -> push_message,无需 Entity、Mapper 或 SQL 变更。
  • 参考实现:D:/QtwCode/CTE/cte_server 的 FirebaseConfig/FCMPush 可借鉴 SDK 用法,但其单项目初始化和用 CID 发 FCM 不可照搬。
  • 本机全局 Java 为 17;C:/Users/qmj/.jdks/graalvm-jdk-21.0.7 不存在。执行时仅在命令环境使用已发现的 C:/Users/99029/.jdks/graalvm-jdk-21.0.12+7.1,不改全局配置。

External Configuration Contract

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 和同名业务字段。iOS 骑手端沿用现有 pushsound.caf,其余 iOS 端使用默认声音;Android FCM 使用默认声音,并为骑手标记 qsRing 通知渠道,最终自定义铃声取决于 App 是否创建该渠道(App 端不在本任务内)。过大或无法解析的 payload 必须记录可定位的失败类别,不得以截断方式改变业务字段。

File Structure

文件 职责
ruoyi-admin/src/main/java/com/ruoyi/app/push/RecipientApp.java USER / MERCHANT / RIDER 显式接收端标识
ruoyi-admin/src/main/java/com/ruoyi/app/push/BusinessPushProperties.java 全局模式、APNs 环境、三端外部凭据路径与 topic 的配置绑定和原生模式校验
ruoyi-admin/src/main/java/com/ruoyi/app/push/FcmPushSender.java 三个命名 FirebaseApp;Android 单发、请求构造与失效错误分类
ruoyi-admin/src/main/java/com/ruoyi/app/push/ApnsPushSender.java 三个证书型 HTTP/2 客户端;APNs alert、custom payload 与响应处理
ruoyi-admin/src/main/java/com/ruoyi/app/push/BusinessPushService.java 统一外发路由、当前账号读取、通道地址判断及安全日志
ruoyi-admin/src/main/java/com/ruoyi/app/utils/PayPush.java 原业务包装方法转委托;三端 uniCloud 低层方法保留给 uni 模式及排除的调用方
ruoyi-admin/src/main/resources/application.yml 显式默认 uni,仅放非秘密配置与环境变量占位符
在用业务调用点 移除只看 CID 的前置排除,把普通业务外发接到统一路由;不改触发时机和入库
ruoyi-admin/src/test/java/com/ruoyi/app/push/*Test.java 配置、路由、FCM/APNs 构造及错误单测;相关现有业务测试覆盖直连点
specs/033-native-push-routing/quickstart.md、specs/012-im-user-integration/code-status.md、docs/order-push-overview.md 安全配置、启用/回退、现状说明

Task 1: 配置与默认模式

Files: Create RecipientApp.java, BusinessPushProperties.java, BusinessPushPropertiesTest.java; modify application.yml.

Interfaces: BusinessPushProperties.Mode { UNI, NATIVE }; BusinessPushProperties.getApp(RecipientApp) returns one app's FCM credentials path and APNs certificate path/password/topic; getApnsEnvironment() returns DEVELOPMENT/PRODUCTION.

  • Write tests: missing mode resolves to UNI; uni accepts empty native credentials; native rejects missing any of three FCM paths, APNs certificate path/password/topic, or invalid environment. Credentials content errors belong to Tasks 2–3.
  • Run mvn -pl ruoyi-admin -am -Dtest=BusinessPushPropertiesTest -Dsurefire.failIfNoSpecifiedTests=false test with JDK 21; verify the new tests fail before implementation.
  • Implement property binding and native-only startup validation; keep all secret values out of application.yml and logs.
  • Re-run the directed test and confirm pass; stage only Task 1 files, inspect staged names, commit.

Task 2: 三项目 Android FCM 发送

Files: Create FcmPushSender.java, FcmPushSenderTest.java.

Interfaces: void send(RecipientApp app, String token, String title, String content, String payload); only available in NATIVE mode. The sender owns three named FirebaseApp instances and closes them on shutdown.

  • Write tests: each app resolves its own named Firebase project; sends to token, never CID; title/content plus existing payload fields survive as string data; UNREGISTERED and project mismatch are classified without full token logging; repeated initialization does not conflict.
  • Run mvn -pl ruoyi-admin -am -Dtest=FcmPushSenderTest -Dsurefire.failIfNoSpecifiedTests=false test with JDK 21 and verify failure before implementation.
  • Initialize credentials from protected filesystem paths, build FCM notification + Android high-priority data, and send through the app-specific FirebaseMessaging instance; do not adopt CTE's single-default-app/CID assumptions.
  • Re-run the same directed command and confirm pass; inspect staged names, commit only Task 2 files.

Task 3: 三端 iOS APNs 普通通知

Files: Create ApnsPushSender.java, ApnsPushSenderTest.java.

Interfaces: void send(RecipientApp app, String token, String title, String content, String payload); only available in NATIVE mode. The request uses apns-topic from that app's config and apns-push-type: alert.

  • Write tests: each app selects its own topic/certificate; DEVELOPMENT/PRODUCTION selects sandbox/production host; alert title/body, sound and custom payload preserve business fields; malformed/expired/no-private-key PKCS#12 fails initialization; non-2xx response is categorized without printing secrets.
  • Run mvn -pl ruoyi-admin -am -Dtest=ApnsPushSenderTest -Dsurefire.failIfNoSpecifiedTests=false test with JDK 21 and verify failure before implementation.
  • Load selected-environment PKCS#12 into per-app TLS client, reuse JDK HTTP/2 connections, send to /3/device/{token}; use ordinary alert push, not VoIP; use rider pushsound.caf and the existing default sound for other apps.
  • Re-run the same directed command and confirm pass; inspect staged names, commit only Task 3 files.

Task 4: 统一外发路由与现有包装方法

Files: Create BusinessPushService.java, BusinessPushServiceTest.java; modify PayPush.java.

Interfaces: void send(RecipientApp app, Long userId, String cid, String title, String content, String payload); String addressKey(InfoUser recipient) returns null if the configured channel lacks a valid address. Native mode reloads current InfoUser by userId to obtain cidType/deviceToken; platform values are trimmed and matched case-insensitively, with unknown values rejected. PayPush keeps its existing static wrapper signatures and retrieves the Spring-managed router through the project's SpringUtils bridge; existing low-level instance methods remain unchanged for excluded callers.

  • Write tests: default uni routes to the three existing low层方法且不读取 native sender;native 六种端/系统组合选正确 sender/token;空/未知 cidType 或空 token 不发送;sender 异常不传播且不回退;切换不增加 PushEvent 次数。
  • Run mvn -pl ruoyi-admin -am -Dtest=BusinessPushServiceTest -Dsurefire.failIfNoSpecifiedTests=false test with JDK 21 and verify failure before implementation.
  • Implement BusinessPushService and delegate PayPush.userPush/shPush/qsPush to it after原有 PublisherEvent;不改变低层 uniCloud 方法,以隔离视频/测试调用。
  • Re-run the same directed command and confirm pass; inspect staged names, commit only Task 4 files.

Task 5: 迁移在用普通业务发送点及 CID 前置判断

Files: Modify MerchantNotificationRouter.java, LinePayOrderNotificationService.java, OmgPaymentNotifyService.java, PosOrderShOprateController.java, PosOrderQsOprateController.java, PosOrderController.java; modify their相关现有测试。仅确需的其他在用业务调用点纳入本任务,禁止修改 VideoCallController、废弃入口或测试推送接口。

Interfaces: Direct业务外发调用 BusinessPushService.send(...);收件人资格和去重调用 addressKey(InfoUser),以当前通道的地址而非固定 CID 判定。

  • 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.

Task 6: 文档与整体验证

Files: Create specs/033-native-push-routing/quickstart.md; update specs/012-im-user-integration/code-status.md, docs/order-push-overview.md, specs/033-native-push-routing/tasks.md.

  • Document credential deployment (paths/env vars only, no values), dev/prod APNs selection, default-off, restart cutover/rollback, certificate expiry monitoring and real-device smoke checklist.
  • Run mvn -pl ruoyi-admin -am -Dtest=BusinessPushPropertiesTest,FcmPushSenderTest,ApnsPushSenderTest,BusinessPushServiceTest,MerchantNotificationRouterTest,DeliveryOrderNotificationServiceTest,OmgPaymentNotifyServiceTest,PosOrderShOprateControllerTest,PosOrderQsOprateControllerTest,PosOrderAdminStatusControllerTest,LinePayControllerTest -Dsurefire.failIfNoSpecifiedTests=false test under JDK 21; then run mvn -pl ruoyi-admin -am -DskipTests package and inspect exit codes.
  • Inspect git diff --check, full diff and staged file list for unrelated edits, secrets, encoding or line-ending churn; commit only documentation/verification updates. Record any unrun live FCM/APNs test explicitly.

Spec Coverage

Spec Tasks
FR-001–002, US1 1, 4, 6
FR-003–007, US2–3 1, 2, 3, 4
FR-008–011, US4 4, 5, 6
FR-012 / no schema 1–6 (no schema task)

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"。2026-09-29 已运行 13 类定向测试(83 个用例,0 失败)及 mvn -pl ruoyi-admin -am -DskipTests package,退出码均为 0;真实 FCM/APNs 凭据、设备投递和 App 展示未在本机验证,启用前按 quickstart.md 执行真机验收。