# OMG 支付完成回跳 App 原生页 —— 已废弃的方案研究 > **废弃说明(2026-08-13)**:本文比较的是曾包含延期支付的旧方案,不是当前实现依据。当前仅支持信用卡和 Apple Pay,只保留 `/pay/omg/result` 与服务端 `/pay/omg/notify`;不存在需要兼容的历史延期交易。当前契约以 `spec.md`、`plan.md`、`contracts/api.md` 为准。 > 状态:已验证(2026-08-11,经并行研究工作流 + 对抗审视) > 范围:OMG(歐買尬/FunPoint) AIO 线上支付;客户端为 uni-app 原生 App;收银台跑在 App 内 `` > 关联:`frontend-integration.md`(§4.3 旧思路已被本文取代)、`contracts/api.md`(B3 `/return`、B5 `/query`、B7 `/paymentInfo`、B8 `OmgReconcileTask`) ## TL;DR 两种可行方案: | 方案 | 一句话 | 推荐 | |------|--------|------| | **A 桥接页(主动回跳)** | OMG 付完款把 web-view POST 到后端 `/return`,后端返回含 `uni.webview.js` 的 HTML,调 `uni.webView.redirectTo` 进原生页 | 信用卡秒回,但 **ATM/超商不触发** → 体验割裂 | | **B 轮询驱动统一结果页** | 把 web-view 当黑盒,**原生侧轮询 `/pay/omg/query` + `/pay/omg/paymentInfo` 驱动一切**,所有方式落到同一个结果页 | **推荐** —— 所有方式一套逻辑,真正统一 | **推荐方案 B。** 信用卡比 A 少 2-3 秒"秒回感",换来全方式统一 + 少一大半代码 + 不踩 JSBridge 的坑。也是项目 `frontend-integration.md:306` 自己说的"最稳的做法"。 --- ## 1. 背景与"统一"的真正含义 有个物理限制绕不开:**ATM / 超商 / BarcodeATM / AFTEE 不可能"付完款立刻回 App"** —— 用户拿虚帐/缴费码后**离开 App、几小时甚至跨天后再去 ATM/便利店缴款**。OMG 只在服务端回调(取号时 `PaymentInfoURL`、实际缴款时 `ReturnURL`),浏览器端不回跳。这是延期支付的本质,任何方案都改不了。 所以"体验统一"的正确含义不是"回 App 的时机一样"(对延期支付不可能),而是: > **所有支付方式,用户最终都落到 App 里同一个原生结果页,由同一套轮询逻辑驱动,按状态显示不同 UI。** 差异只在"结果页显示什么",不在"回 App 的路径"。 方案 A 的割裂根源:桥接页**只对即时支付(信用卡)有效**,ATM/超商得另配一套 → 两套体验。方案 B 把回跳完全交给原生轮询,所有方式天然同一套。 ## 2. 已验证关键事实(两方案共用) 来源:研究工作流(OMG 语义 agent + uni-app web-view agent + 现有代码 agent),交叉核对项目 `spec.md` / `contracts/api.md` 与绿界(ECPay)等价(OMG AIO 克隆绿界)。 | 事实 | 结论 | 依据 | |------|------|------| | `OrderResultURL` 回传方式 | **浏览器 Form POST**(带 RtnCode/CheckMacValue),非 302 GET、非服务端 POST | 绿界 15076/2862;spec.md:22 | | `ReturnURL` | 服务端 POST,须回 `1\|OK` | OmgPayController.java:242 | | 即时支付(Credit)是否触发 OrderResultURL | 是 | 绿界 2878 | | ATM/CVS/BarcodeATM/AFTEE 是否触发 OrderResultURL | **否**(走 `PaymentInfoURL` 取号 + 线下缴款) | 绿界 2878 | | OrderResultURL 能否指 SPA hash 页 | **不能**(POST 不带 hash,前端也读不到 body) | HTTP 语义 | | 原生页 `/pages/xxx` 能否作 302 目标 | **不能**(非 HTTP URL) | uni-app 路由 | | `uni.webView.redirectTo` 回原生 | 可行,需 SDK `uni.webview.1.5.8.js` + 事件 `UniAppJSBridgeReady`,**仅你可控的页面** | uniapp.dcloud.net.cn/component/web-view.html | | `POST /pay/omg/query` | 既查 `payStatus`(0/1/2) 又自愈(`reconcileByQuery` 主动问 OMG) | OmgPayController.java:704-748 | | `GET /pay/omg/paymentInfo/{orderid}` | 返回 `{payType,amount,payStatus,info}`,`info` 非空 = 已取号(虚帐/缴费码+期限) | OmgPayController.java:477-502 | | 当前 `/return` 死循环 bug | 存在(`orderResultUrl` 同时作 OrderResultURL 与 302 目标) | OmgPayController.java:188-190 / 401-402 | > ⚠️ **OMG 官方文档(developers-stage.omg.com.tw)被安全策略挡住,未能直连核对**,「Form POST + 即时支付才触发 OrderResultURL」是从**绿界等价 + 项目 spec** 推断的。**上线前必须在 OMG stage 实测**(见 §8)。方案 B 不依赖 OMG 前景回跳,受这个推断影响最小。 --- ## 3. 方案 A:桥接页(主动回跳) ### 3.1 机制 ``` 用户在 付完款(即时支付) → OMG 用浏览器 Form POST 把 web-view 导到 OrderResultURL(=后端 /pay/omg/return) → /return 返回 text/html(不是 302) → HTML 加载 uni.webview.1.5.8.js,触发 uni.webView.redirectTo → web-view 被替换成原生页 /pages/payResult ``` ### 3.2 后端改动 **`application.yml` 两 key 拆分(照抄 newebpay 模式):** ```yaml omg: order-result-url: https://foodieapi.waimai-paotui.com/pay/omg/return # OMG POST 到这里 front-result-url: https://your-h5-domain/#/pages/payResult # 桥接页 H5 兜底(纯原生可占位) ``` **`OmgPayController.java`:** - 第 111 行后新增 `@Value("${omg.front-result-url}") private String frontResultUrl;` - 第 188-190 行(发 OrderResultURL)**不动** - 第 396-402 行 `sendRedirect` 整段替换为:`response.setContentType("text/html;charset=UTF-8"); response.getWriter().write(buildBridgeHtml(ddId, frontResultUrl));` - 新增 `buildBridgeHtml(ddId, frontResultUrl)`(服务端注入,**转义引号防 XSS**) - `/return` **不加** CheckMacValue 验签(不改状态,真相在 `/notify:277` 验签 + `/query:704` JWT 鉴权) - 把 `uni.webview.1.5.8.js` 放 `ruoyi-admin/src/main/resources/static/`(目录需新建;`ResourcesConfig` 已注册 `/static/**`),同源 HTTPS 提供 ### 3.3 桥接页 HTML(已修正对抗审视发现的 bug) > 设计稿原版 bug:手动按钮 `showBtn()` 只在 `UniAppJSBridgeReady` 回调里 → SDK 没加载就永不出现,原生用户卡空白 web-view。修正版把按钮放到**所有失败路径**都能触达,等待窗口 3s→6s 轮询。 ```html 支付完成
支付完成,正在返回…
``` (`__DDID__`/`__FRONT_URL__` 后端注入。i18n:过渡 shim 留极简文案,权威多语言放原生 `payResult.vue`。) ### 3.4 适用与局限 - ✅ **即时支付(信用卡)**:秒回原生页 - ❌ **ATM/超商/BarcodeATM/AFTEE**:OMG 不触发 OrderResultURL → 桥接页永不加载 → 这部分用户必须另走轮询。若想让取号也"主动回跳",还得**再做一个 `ClientRedirectURL` 桥接页**(同套路),复杂度翻倍且仍是两套体验。 --- ## 4. 方案 B:轮询驱动统一结果页(推荐) ### 4.1 机制 把 OMG 收银台所在的 `` **当黑盒**,**原生侧靠轮询后端接口驱动一切**,完全不依赖 OMG 的浏览器端回跳(不接 `OrderResultURL` / `ClientRedirectURL`)。web-view 宿主页 `omgCheckout.vue` 是原生页,web-view 是它的子视图 —— 宿主页的 JS 一直在跑,可以后台轮询。 ### 4.2 统一轮询逻辑(`omgCheckout.vue` 与 `payResult.vue` 共用) ``` 每 ~2.5s(退避 2s/3s/5s/8s…,最多 ~30s): q = POST /pay/omg/query → payStatus(0/1/2;且自愈:/notify 丢了会主动问 OMG) if payStatus == 1 → 进 payResult(成功) ← 信用卡付完 / ATM 缴完款都走这 if payStatus == 2 → 进 payResult(失败) # payStatus==0 时查取号: info = GET /pay/omg/paymentInfo/{orderid} if info 非空 → 进 payResult(取号页:虚帐/缴费码 + 期限 + 倒计时) ← ATM/超商取号后 else 继续轮询(信用卡待付 / ATM 还没取号) ``` **对所有方式行为一致:** - **信用卡**:payStatus `0→1`,永不显示取号 → 直接成功页 - **ATM/超商**:payStatus 一直 `0`,取号后 `info` 出现 → 取号页(用户记下码、关 App 去线下缴款);**日后重开 App**,订单详情/`payResult` 再轮询 → payStatus `→1` → 成功页 ### 4.3 `payResult.vue` 统一结果页(按状态渲染) | 状态 | 显示 | |------|------| | 取号信息已到(ATM/超商,未付款) | 银行代码/虚帐 或 缴费码 + 期限 + 倒计时 + "请于期限前缴款" | | payStatus=1 | 支付成功(任何方式) | | payStatus=2 | 支付失败,可重试 | | 确认中 | "支付结果确认中,可稍后在订单列表查看" | ### 4.4 后端改动(基本就绪,反而要"减负") 后端**基本不用新增**,反而可以**删掉桥接页相关的一切**: - `POST /pay/omg/query`(`:704`,既查又自愈)✅ 已有 - `GET /pay/omg/paymentInfo/{orderid}`(`:477`,取号信息)✅ 已有 - `POST /pay/omg/notify`(`:242`,真相源)✅ 已有 - `POST /pay/omg/paymentInfo`(`:412`,取号服务端回调)✅ 已有 —— ATM/超商取号靠它,**保留** - **不再需要** `OrderResultURL` → `create()` 第 188-190 行可去掉;`/return` 端点、`uni.webview.1.5.8.js` 托管、桥接页 HTML 全部不用 - `ClientRedirectURL` 也不用接(取号检测靠轮询 `paymentInfo`) ### 4.5 适用 ✅ **所有方式一套逻辑**:信用卡、ATM、超商、BarcodeATM 统一。真相始终以 `/pay/omg/notify` + DB `payStatus` 为准,绝不凭页面跳转判断成功。 --- ## 5. 方案对比 | 维度 | A 桥接页 | B 轮询驱动(推荐) | |------|---------|------------------| | 信用卡回 App 速度 | 秒回(`redirectTo`) | 2-3s(轮询发现 payStatus=1) | | ATM/超商 | **不触发,须另做 `ClientRedirectURL` 桥接 → 两套体验** | 取号页 + 日后重开看结果,**同一套逻辑** | | 统一性 | 信用卡/延期割裂 | **所有方式一套** | | 后端工作量 | 加 config 两 key + `/return` 渲染 + `buildBridgeHtml` + 托管 SDK + 建 static 目录 | **基本零新增**(去掉 OrderResultURL 接线即可) | | 前端工作量 | 宿主页 + 结果页 + 注册路由 + 桥接依赖 JSBridge ready | 宿主页 + 结果页(统一轮询逻辑),无 JSBridge | | 可靠性 | 依赖 OMG 前景回跳(推断未实测)+ JSBridge 时序 + SDK 可达 | 只依赖后端接口(已实现 + 自愈) | | 残留风险 | i18n / 静态目录 / CSP / iOS WKWebView 脚本加载 | 轮询打 OMG `/query`(注意限流,用退避) | ## 6. 推荐与决策 **用方案 B。** 信用卡少 2-3 秒"秒回感",是唯一代价;收益是**全方式统一 + 少一大半代码 + 不踩 JSBridge/SDK/iOS 一系列坑**,且最不受"OMG=绿界"未实测推断的影响。 **何时选 A:** 若产品极度在意信用卡的"秒回"体验、且能接受 ATM/超商另做一套 `ClientRedirectURL` 桥接(复杂度翻倍、仍是两套体验)。即使选 A,**ATM/超商部分也必须叠加 B 的轮询**,所以 A 实际是"B + 信用卡桥接增强"。 > 实务上最常见的落地就是 **B 为主,A 作为信用卡的可选增强**(若日后想要秒回感再叠加,互不冲突)。 --- ## 7. 客户端改动(uni-app,不在工作区,需 App 端做) **两方案共用:** - `pages.json` 注册结果页(不注册则 `redirectTo` / `navigateTo` 静默失败,最易踩的坑) - 路由建议**复用 newebpay 已有的 `/pages/payResult`**(按 `payType` 分支),省一个页面 + 一份 i18n **方案 B 额外(推荐做的全部):** - `omgCheckout.vue`(web-view 宿主):加 `onShow` + `setInterval` 跑 §4.2 统一轮询;终态时 `redirectTo` 到 `payResult` 并停轮询 - `payResult.vue`:`onLoad` 拿 `ddId` → 继续跑 §4.2 轮询 → 按 §4.3 状态表渲染 - 注意 `/query` 有 `@RepeatSubmit(2s)`,轮询间隔 ≥2s 并退避 **方案 A 额外(若选 A):** - `omgCheckout.vue` 需确认收银台确实在 `` 内(不是 `plus.runtime.openURL` 开外部浏览器,否则桥接前提崩塌) - 桥接页加载的 SDK 与 `/return` 同源 HTTPS ## 8. 待验证假设 + 测试清单 **上线前必做(OMG stage):** - [ ] 用测试卡支付,抓 `OrderResultURL` 实际 HTTP 方法(POST/GET)、是否在**支付失败**时也触发(影响方案 A 的 UX 判断;方案 B 不受影响) - [ ] (方案 A)`GET /static/uni.webview.1.5.8.js` 返回 200 + `application/javascript` - [ ] 真机 iOS 跑通(不止 Android) **方案 B 测试:** - [ ] 信用卡:`omgCheckout` → 支付 → 轮询 `/query` 得 payStatus=1 → `payResult` 成功 - [ ] ATM:选 ATM → 取号 → 轮询 `/paymentInfo` 得 `info` → `payResult` 取号页 → 关 App →(stage SimulatePaid)→ 重开 → payStatus=1 → 成功 - [ ] 漏单:禁 `/notify` → `/query` 经 `reconcileByQuery` 自愈到 payStatus=1 - [ ] 幂等:同时 `/notify` 与轮询 → 只核销一次 - [ ] 退避:确认轮询不超 OMG 限流 **方案 A 额外测试:** - [ ] `/return` 返回 200 `text/html`、无 `Location` 头、连点不链式(死循环回归) - [ ] 禁掉 SDK → 6s 后显示手动按钮,不空白 - [ ] `/create` 发出的 `OrderResultURL` = `.../pay/omg/return` ## 9. 关键文件与行号 | 位置 | 作用 | 方案 | |------|------|------| | `OmgPayController.java:188-190` | `create()` 发 `OrderResultURL` | A 保留 / B 可去掉 | | `OmgPayController.java:378-403` | `/pay/omg/return` | A 改渲染 HTML / B 不用 | | `OmgPayController.java:242 / :277` | `/notify` 服务端回调 + 验签(真相源) | 两方案都不动 | | `OmgPayController.java:412` | `/paymentInfo` 取号服务端回调 | 两方案都保留(ATM/超商取号) | | `OmgPayController.java:477-502` | `GET /paymentInfo/{orderid}` 取号信息查询 | **B 轮询目标** | | `OmgPayController.java:704-748` | `POST /query` 既查又自愈 | **B 轮询目标** | | `application.yml:40-49` | `omg:` 配置段 | A 拆两 key / B 可去 OrderResultURL | | `NewebpayPayController.java:285-307` | newebpay `/return` 302 两 key 模式 | A 的同构参考 | | `ResourcesConfig.java:46` | `/static/**` 映射 | 仅 A 需建 static 目录放 SDK |