废弃说明(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 内
<web-view>关联:frontend-integration.md(§4.3 旧思路已被本文取代)、contracts/api.md(B3/return、B5/query、B7/paymentInfo、B8OmgReconcileTask)
两种可行方案:
| 方案 | 一句话 | 推荐 |
|---|---|---|
| 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 自己说的"最稳的做法"。
有个物理限制绕不开:ATM / 超商 / BarcodeATM / AFTEE 不可能"付完款立刻回 App" —— 用户拿虚帐/缴费码后离开 App、几小时甚至跨天后再去 ATM/便利店缴款。OMG 只在服务端回调(取号时 PaymentInfoURL、实际缴款时 ReturnURL),浏览器端不回跳。这是延期支付的本质,任何方案都改不了。
所以"体验统一"的正确含义不是"回 App 的时机一样"(对延期支付不可能),而是:
所有支付方式,用户最终都落到 App 里同一个原生结果页,由同一套轮询逻辑驱动,按状态显示不同 UI。 差异只在"结果页显示什么",不在"回 App 的路径"。
方案 A 的割裂根源:桥接页只对即时支付(信用卡)有效,ATM/超商得另配一套 → 两套体验。方案 B 把回跳完全交给原生轮询,所有方式天然同一套。
来源:研究工作流(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 前景回跳,受这个推断影响最小。
用户在 <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
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:
@Value("${omg.front-result-url}") private String frontResultUrl;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 提供设计稿原版 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。)
ClientRedirectURL 桥接页(同套路),复杂度翻倍且仍是两套体验。把 OMG 收银台所在的 <web-view> 当黑盒,原生侧靠轮询后端接口驱动一切,完全不依赖 OMG 的浏览器端回跳(不接 OrderResultURL / ClientRedirectURL)。web-view 宿主页 omgCheckout.vue 是原生页,web-view 是它的子视图 —— 宿主页的 JS 一直在跑,可以后台轮询。
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 还没取号)
对所有方式行为一致:
0→1,永不显示取号 → 直接成功页0,取号后 info 出现 → 取号页(用户记下码、关 App 去线下缴款);日后重开 App,订单详情/payResult 再轮询 → payStatus →1 → 成功页payResult.vue 统一结果页(按状态渲染)| 状态 | 显示 |
|---|---|
| 取号信息已到(ATM/超商,未付款) | 银行代码/虚帐 或 缴费码 + 期限 + 倒计时 + "请于期限前缴款" |
| payStatus=1 | 支付成功(任何方式) |
| payStatus=2 | 支付失败,可重试 |
| 确认中 | "支付结果确认中,可稍后在订单列表查看" |
后端基本不用新增,反而可以删掉桥接页相关的一切:
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)✅ 所有方式一套逻辑:信用卡、ATM、超商、BarcodeATM 统一。真相始终以 /pay/omg/notify + DB payStatus 为准,绝不凭页面跳转判断成功。
| 维度 | 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(注意限流,用退避) |
用方案 B。 信用卡少 2-3 秒"秒回感",是唯一代价;收益是全方式统一 + 少一大半代码 + 不踩 JSBridge/SDK/iOS 一系列坑,且最不受"OMG=绿界"未实测推断的影响。
何时选 A: 若产品极度在意信用卡的"秒回"体验、且能接受 ATM/超商另做一套 ClientRedirectURL 桥接(复杂度翻倍、仍是两套体验)。即使选 A,ATM/超商部分也必须叠加 B 的轮询,所以 A 实际是"B + 信用卡桥接增强"。
实务上最常见的落地就是 B 为主,A 作为信用卡的可选增强(若日后想要秒回感再叠加,互不冲突)。
两方案共用:
pages.json 注册结果页(不注册则 redirectTo / navigateTo 静默失败,最易踩的坑)/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 需确认收银台确实在 <web-view> 内(不是 plus.runtime.openURL 开外部浏览器,否则桥接前提崩塌)/return 同源 HTTPS上线前必做(OMG stage):
OrderResultURL 实际 HTTP 方法(POST/GET)、是否在支付失败时也触发(影响方案 A 的 UX 判断;方案 B 不受影响)GET /static/uni.webview.1.5.8.js 返回 200 + application/javascript方案 B 测试:
omgCheckout → 支付 → 轮询 /query 得 payStatus=1 → payResult 成功/paymentInfo 得 info → payResult 取号页 → 关 App →(stage SimulatePaid)→ 重开 → payStatus=1 → 成功/notify → /query 经 reconcileByQuery 自愈到 payStatus=1/notify 与轮询 → 只核销一次方案 A 额外测试:
/return 返回 200 text/html、无 Location 头、连点不链式(死循环回归)/create 发出的 OrderResultURL = .../pay/omg/return| 位置 | 作用 | 方案 |
|---|---|---|
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 |