# 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 内 `<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 模式）：**
```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
<!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.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` 需确认收银台确实在 `<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 → 取号 → 轮询 `/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 |
