return-to-app-flow.md 15 KB

OMG 支付完成回跳 App 原生页 —— 已废弃的方案研究

废弃说明(2026-08-13):本文比较的是曾包含延期支付的旧方案,不是当前实现依据。当前仅支持信用卡和 Apple Pay,只保留 /pay/omg/result 与服务端 /pay/omg/notify;不存在需要兼容的历史延期交易。当前契约以 spec.mdplan.mdcontracts/api.md 为准。

状态:已验证(2026-08-11,经并行研究工作流 + 对抗审视) 范围:OMG(歐買尬/FunPoint) AIO 线上支付;客户端为 uni-app 原生 App;收银台跑在 App 内 <web-view> 关联: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 机制

用户在 <web-view> 付完款(即时支付)
  → 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 模式):

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.jsruoyi-admin/src/main/resources/static/(目录需新建;ResourcesConfig 已注册 /static/**),同源 HTTPS 提供

3.3 桥接页 HTML(已修正对抗审视发现的 bug)

设计稿原版 bug:手动按钮 showBtn() 只在 UniAppJSBridgeReady 回调里 → SDK 没加载就永不出现,原生用户卡空白 web-view。修正版把按钮放到所有失败路径都能触达,等待窗口 3s→6s 轮询。

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>支付完成</title>
<style>body{font-family:-apple-system,sans-serif;text-align:center;padding:48px 20px;color:#333}
#tip{font-size:16px;margin:16px 0}
a{color:#1989fa;padding:10px 20px;border:1px solid #1989fa;border-radius:4px;text-decoration:none;display:inline-block;margin-top:12px}</style>
</head>
<body>
<div id="tip">支付完成,正在返回…</div>
<div id="fb" style="display:none"><a id="btn" href="javascript:void(0)">点此返回商家</a></div>
<script src="https://foodieapi.waimai-paotui.com/static/uni.webview.1.5.8.js"></script>
<script>
(function(){
  var DDID="__DDID__", FRONT="__FRONT_URL__", done=false, tries=0;
  var nativeUrl="/pages/payResult?ddId="+encodeURIComponent(DDID);
  var spaUrl=FRONT+(FRONT.indexOf("?")>=0?"&":"?")+"ddId="+encodeURIComponent(DDID);
  function hasBridge(){return (typeof uni!=="undefined")&&uni.webView&&uni.webView.redirectTo;}
  function goNative(){if(done)return true;if(hasBridge()){done=true;uni.webView.redirectTo({url:nativeUrl});return true;}return false;}
  function showBtn(){
    document.getElementById("tip").textContent="若未自动返回,请点下方按钮";
    document.getElementById("fb").style.display="block";
    document.getElementById("btn").onclick=function(){ if(!goNative()) location.href=spaUrl; };
  }
  document.addEventListener("UniAppJSBridgeReady",function(){ if(!goNative()) showBtn(); });
  var iv=setInterval(function(){
    if(goNative()){clearInterval(iv);} else if(++tries>=20){clearInterval(iv);showBtn();}
  },300);
})();
</script>
</body>
</html>

__DDID__/__FRONT_URL__ 后端注入。i18n:过渡 shim 留极简文案,权威多语言放原生 payResult.vue。)

3.4 适用与局限

  • 即时支付(信用卡):秒回原生页
  • ATM/超商/BarcodeATM/AFTEE:OMG 不触发 OrderResultURL → 桥接页永不加载 → 这部分用户必须另走轮询。若想让取号也"主动回跳",还得再做一个 ClientRedirectURL 桥接页(同套路),复杂度翻倍且仍是两套体验。

4. 方案 B:轮询驱动统一结果页(推荐)

4.1 机制

把 OMG 收银台所在的 <web-view> 当黑盒原生侧靠轮询后端接口驱动一切,完全不依赖 OMG 的浏览器端回跳(不接 OrderResultURL / ClientRedirectURL)。web-view 宿主页 omgCheckout.vue 是原生页,web-view 是它的子视图 —— 宿主页的 JS 一直在跑,可以后台轮询。

4.2 统一轮询逻辑(omgCheckout.vuepayResult.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/超商取号靠它,保留
  • 不再需要 OrderResultURLcreate() 第 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 统一轮询;终态时 redirectTopayResult 并停轮询
  • payResult.vueonLoadddId → 继续跑 §4.2 轮询 → 按 §4.3 状态表渲染
  • 注意 /query@RepeatSubmit(2s),轮询间隔 ≥2s 并退避

方案 A 额外(若选 A):

  • omgCheckout.vue 需确认收银台确实在 <web-view> 内(不是 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 → 取号 → 轮询 /paymentInfoinfopayResult 取号页 → 关 App →(stage SimulatePaid)→ 重开 → payStatus=1 → 成功
  • 漏单:禁 /notify/queryreconcileByQuery 自愈到 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