# OMG（歐買尬/FunPoint）支付 — 前端接入文档

**适用端**：客户 App（uni-app） ｜ **后端分支**：`OmgPayController` 已完成 ｜ **日期**：2026-08-14

---

## 0. 先搞懂一件事：OMG 没有「支付 App」

OMG（欧买尬/FunPoint）是**网页托管式支付网关**（AIO = All-In-One 收银台），跟蓝新/绿界同类。它的设计就是**没有独立 App**——付款发生在 **OMG 的网页收银台**上。

| 支付 | 有没有 App | 付款在哪发生 | 前端怎么触发 |
|------|-----------|-------------|------------|
| LINE Pay | 有 LINE App | 唤起 LINE App 内 | deep link 唤起 |
| **OMG / FunPoint** | **没有 App** | **OMG 的网页收银台** | **form post 跳网页** |

后端固定使用 `ChoosePayment=Credit`、`UnionPay=2`，OMG 收银台只提供 **信用卡与 Apple Pay**。**前端不需要集成任何支付 SDK，只需要把用户「跳过去」再「接回来」。**

---

## 1. 整体流程（必读）

```
[App: 订单详情页 点"去支付"]
        │
        │  ① POST /pay/omg/create   JSON {"orderId":"..."} (Header: token)
        ▼
[后端] 校验订单 → 组参+签名 → 返回 form 字段（含 gatewayUrl）
        │
        │  返回 { data: { gatewayUrl, MerchantID, MerchantTradeNo, CheckMacValue, ... } }
        ▼
[App] ② 把 form 字段拼到 URL，打开 <web-view> 加载 omgPay.html
        │
        │  omgPay.html 自动 submit 隐藏表单 → 跳转
        ▼
[OMG 收银台网页]  用户使用信用卡或 Apple Pay 付款
        │
        │  ③ 付款后 OMG 把 web-view 跳回 OrderResultURL
        │     后端 /pay/omg/result 返回安全桥接页 → App Link
        ▼
[App: 支付结果页] 读 ddId → 查订单状态 → 展示结果

   ─── 与此同时（服务端，前端无感）───
   OMG POST 后端 /pay/omg/notify  → 验签+核销 → 订单 payStatus 置 1 + 推送
   （订单状态以这条服务端回调为准，不要只信网页跳转）
```

> **关键原则**：网页跳回结果页 ≠ 一定支付成功。结果页必须**再查一次订单 `payStatus`** 来判断，因为服务端回调可能晚到几秒。

---

## 2. 前端要做的事（总览）

| # | 事项 | 在哪做 |
|---|------|--------|
| 1 | 放一个 `omgPay.html` 到 `static/`，作为 web-view 收银台跳转页 | uni-app 工程 |
| 2 | 发起支付：调 `/pay/omg/create` → 拼 URL → 打开 web-view | 发起支付页 |
| 3 | 支付结果页：接收 `orderId` → 调 `/pay/omg/query` → 展示成功/失败/确认中 | 支付结果页 |
| 4 | 不实现 ATM/超商取号及延期支付返回入口 | 无新增页面 |
| 5 | （可选）取消订单退款：调 `/pay/omg/refund` | 订单取消逻辑 |
| 6 | 告诉后端两个前端地址（见 §6 配置依赖） | 沟通 |
| 7 | 所有面向用户文字走 i18n 4 语言 | zh/tw/en/vi |
| 8 | iOS 联调时同时收集 `[OMG-RETURN]` 与 `[OMG-APP]` 日志 | HBuilderX + Safari Web Inspector |

---

## 3. 接口清单

> baseURL：前端约定的后端网关地址（如 `https://api.xxx.com`），下面路径在其后拼接。
> 所有需登录接口：**Header 必须带 `token`（用户 JWT）**。

### 3.1 发起支付 — `POST /pay/omg/create`

**用途**：生成一次 OMG 收银台跳转所需的全部参数。

| 项 | 内容 |
|----|------|
| Method | `POST` |
| Header | `token: <用户JWT>` |
| 入参 | JSON：`{"orderId":"订单号"}` |
| 鉴权 | 登录用户 + 必须是订单本人 |

**请求示例**
```
POST {baseURL}/pay/omg/create
Header: token: eyJhbGciOi...
Content-Type: application/json

{"orderId":"DD202608060001"}
```

**成功返回**（`code=200`，取 `data`）
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "status": "CREATED",
    "gatewayUrl": "https://payment.funpoint.com.tw/Cashier/AioCheckOut/V5",
    "formFields": {
      "MerchantID": "3xxxxxx2",
      "MerchantTradeNo": "OMG20260806143012xxx",
      "MerchantTradeDate": "2026/08/06 14:30:12",
      "PaymentType": "aio",
      "TotalAmount": "350",
      "TradeDesc": "food order DD202608060001",
      "ItemName": "order DD202608060001",
      "ReturnURL": "https://api.xxx.com/pay/omg/notify",
      "ChoosePayment": "Credit",
      "UnionPay": "2",
      "EncryptType": "1",
      "InvoiceMark": "N",
      "NeedExtraPaidInfo": "Y",
      "OrderResultURL": "https://api.xxx.com/pay/omg/result",
      "CheckMacValue": "A1B2C3D4E5F6...(64位)"
    }
  }
}
```

**常见错误返回**
| code / msg | 含义 | 前端处理 |
|-----------|------|---------|
| 请先登录 | token 缺失/失效 | 跳登录 |
| 无权操作该订单 | 非本人订单 | 提示即可 |
| 订单已支付 | 已付过 | 刷新订单状态 |
| 订单已取消，不可重新支付 | state=4 | 引导重下单 |
| 该门店暂不支持线上支付 | 门店未开通 OMG | 提示「该门店暂不支持在线支付」 |
| 请求过于频繁 | 1 秒内重复点 | 防抖 |

> ⚠️ **绝对不能修改 `data` 里任何字段的值**（尤其 `CheckMacValue`）。任何改动都会导致 OMG 验签失败。前端只负责**原样透传**。

---

### 3.2 支付结果返回 — `POST /pay/omg/result`

**不是前端直接调的接口**。付款完成后 OMG Client POST 到 `/pay/omg/result`，后端返回 no-store 安全桥接页并唤醒 App，不修改支付状态。

```
com.twanmsdyh.app://pages/OrderList/paySuccess/paySuccess?ddId=DD202608060001
```

App 打开后必须调用 `/pay/omg/query` 确认结果；`/result` 验签通过也不等于可以直接宣称已付款。

---

### 3.3 支付状态查询 — `POST /pay/omg/query`

App 返回后用 Header `token` 和 JSON `{"orderId":"..."}` 调用。接口只查询该订单唯一有效支付尝试；若成功回调丢失，会向 OMG 查询并同步本地支付与订单状态。

---

### 3.4 退款（取消已支付订单）— `POST /pay/omg/refund`

**用途**：用户取消已支付的信用卡/Apple Pay 订单时退款。

| 项 | 内容 |
|----|------|
| Method | `POST` |
| Header | `token: <用户JWT>` |
| 入参 | query：`orderid` = ddId |
| 鉴权 | 订单本人 |

**请求**
```
POST {baseURL}/pay/omg/refund?orderid=DD202608060001
Header: token: eyJ...
```

**返回**
| code=200 | 信用卡/Apple Pay 退刷成功，`pos_order.pay_status` 置 2 |

---

## 4. uni-app 参考实现

> 以下为参考代码，需在 uni-app 工程内按实际路由/请求封装适配。

### 4.1 收银台跳转页 `static/omgPay.html`

放到 `static/` 目录（web-view 可加载本地文件）。它从 URL query 读取后端返回的所有字段，自动 submit 到 `gatewayUrl`。

```html
<!DOCTYPE html>
<html lang="zh-TW">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
  <title>跳转支付中</title>
  <style>
    body { text-align:center; padding:60px 20px; font-family:-apple-system,sans-serif; color:#888;background:#fff }
    .dots:after { content:'...'; animation:d 1s steps(1) infinite }
    @keyframes d { 50%{content:''} }
  </style>
</head>
<body>
  <div id="tip">正在跳转至支付页面<span class="dots"></span></div>
  <form id="payForm" method="post" action=""></form>
  <script>
    (function () {
      var qs = location.search.substring(1);
      if (!qs) { document.getElementById('tip').innerText = '参数缺失'; return; }
      var params = {};
      qs.split('&').forEach(function (kv) {
        var i = kv.indexOf('=');
        if (i > -1) params[decodeURIComponent(kv.slice(0, i))] = decodeURIComponent(kv.slice(i + 1));
      });
      var gateway = params.gatewayUrl;
      if (!gateway) { document.getElementById('tip').innerText = '缺少 gatewayUrl'; return; }
      var form = document.getElementById('payForm');
      form.action = gateway;
      Object.keys(params).forEach(function (k) {
        if (k === 'gatewayUrl') return;          // gatewayUrl 仅作 action，不作表单字段
        var el = document.createElement('input');
        el.type = 'hidden'; el.name = k; el.value = params[k];
        form.appendChild(el);
      });
      form.submit();
    })();
  </script>
</body>
</html>
```

### 4.2 发起支付页（点「去支付」）

```js
// 1. 调发起接口
uni.request({
  url: baseURL + '/pay/omg/create',
  method: 'POST',
  header: { token: uni.getStorageSync('token') },
  data: { orderid: ddId },            // 也可用 ?orderid= 拼在 url 上
  success: (res) => {
    const d = res.data;
    if (d.code !== 200) { uni.showToast({ title: d.msg, icon: 'none' }); return; }
    goCheckout(d.data);               // data = form 字段
  }
});

// 2. 拼 query 打开 web-view 收银台
function goCheckout(form) {
  // ⚠️ 每个值都要 encodeURIComponent，否则 / 空格 = 等字符会破坏 URL
  const query = Object.keys(form)
    .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(form[k]))
    .join('&');
  uni.navigateTo({
    url: '/pages/pay/omgCheckout?pay=' + encodeURIComponent(query)
  });
}
```

```html
<!-- pages/pay/omgCheckout.vue —— 只放一个 web-view -->
<template>
  <web-view :src="src"></web-view>
</template>
<script>
export default {
  data: () => ({ src: '' }),
  onLoad(opt) {
    // 把拼好的 query 透传给本地 omgPay.html
    this.src = '/static/omgPay.html?' + decodeURIComponent(opt.pay);
  }
}
</script>
```

> 注意：iOS 上 web-view 不可另开窗、收银台不可被 iframe 套——本方案是完整页 web-view，符合要求。

### 4.3 支付结果页 `pages/pay/omgResult`

后端 `/pay/omg/result` 返回桥接页并打开 `pages/OrderList/paySuccess/paySuccess`。桥接页通过匿名只读地址 `/pay/omg/bridge.js` 加载后端自托管的 `uni.webview.1.5.8.js`：在 App 的 `<web-view>` 内等待 `UniAppJSBridgeReady`；iOS App-Plus 优先让支付子 WebView 的父 uni-app 页面执行 `uni.redirectTo`，规避旧运行时内部 `UniPlusBridge` 不存在造成的 `ReferenceError`，Android 等其他环境继续调用 `uni.webView.redirectTo`。Bridge 不可用或未完成页面交接时再使用固定 App Scheme，外部浏览器仍可通过 Scheme 返回。

App 前端不需要加载 Bridge SDK，也不需要在支付页增加按钮；只需确保目标页面已在 `pages.json` 注册且不是 `tabBar` 页面：

```text
/pages/OrderList/paySuccess/paySuccess?ddId=<订单号>
```

Bridge 成功属于 uni-app 内部路由，不会触发 `plus.runtime.arguments` 或 `newintent`；只有进入 Scheme 兜底时才会出现这些 App 唤起事件。

```js
// 支付成功页面加载后，从链接参数取得 ddId 并开始查询。
```

原生订单详情页（回来后）**轮询确认状态**：

```js
// 信用卡/Apple Pay：查询唯一有效支付尝试
const poll = (ddId) => {
  uni.request({
    url: baseURL + '/pay/omg/query',
    method: 'POST',
    header: { token: uni.getStorageSync('token') },
    data: { orderId: ddId },
    success: (res) => {
      const d = res.data.data;
      if (d.status === 'PAID')        { showSuccess(); }
      else if (d.status === 'FAILED') { showFail(); }
      else { setTimeout(() => poll(ddId), 2000); }
    }
  });
};
```

> 轮询建议：每 2s 一次，最多 15 次（30s）；超过仍未成功提示「支付结果确认中，请稍后在订单列表查看」。

### 4.4 iOS / uni-app App 唤起全链路诊断日志

后端中转页会在浏览器控制台输出统一前缀 `[OMG-RETURN]`，覆盖页面加载、Bridge 就绪与环境、Bridge 跳转、Scheme 兜底、手动触摸/点击、页面可见性和脚本异常。日志对象已序列化为 JSON，不再只显示 `[object Object]`。App 端需要按下面代码输出 `[OMG-APP]`，两组日志应在同一次支付中一起收集。

所有日志只保留 Scheme 的协议/主机/路径、参数名和订单号后四位，不打印 token、CheckMacValue、OMG 表单或完整订单号。

#### 4.4.1 `App.vue`：App 生命周期和 Scheme 参数

```js
const OMG_APP_LOG = '[OMG-APP]'
let newIntentRegistered = false

function summarizeSchemeArgument(raw) {
  const value = typeof raw === 'string' ? raw : ''
  if (!value) return { present: false }

  const url = value.match(/^([a-z][a-z0-9+.-]*):\/\/([^/?#]*)([^?#]*)/i)
  const idMatch = value.match(/[?&]ddId=([^&#]*)/i)
  let ddId = ''
  if (idMatch) {
    try { ddId = decodeURIComponent(idMatch[1]) } catch (e) { ddId = '' }
  }
  return {
    present: true,
    protocol: url ? url[1].toLowerCase() + ':' : 'unknown',
    host: url ? url[2] : '',
    pathname: url ? url[3] : '',
    orderRef: ddId ? '***' + ddId.slice(-4) : ''
  }
}

function currentRouteSummary() {
  const pages = getCurrentPages()
  const page = pages.length ? pages[pages.length - 1] : null
  return {
    route: page && page.route ? page.route : '',
    optionKeys: page && page.options ? Object.keys(page.options) : []
  }
}

function logRuntimeArguments(source) {
  // #ifdef APP-PLUS
  const runtimeReady = typeof plus !== 'undefined' && plus.runtime
  console.info(OMG_APP_LOG, source, {
    runtimeReady: !!runtimeReady,
    launcher: runtimeReady ? plus.runtime.launcher : '',
    argument: summarizeSchemeArgument(runtimeReady ? plus.runtime.arguments : ''),
    route: currentRouteSummary()
  })
  // #endif
}

function registerNewIntentLogger() {
  // #ifdef APP-PLUS
  if (newIntentRegistered || typeof document === 'undefined') return

  const register = () => {
    if (newIntentRegistered) return
    document.addEventListener('newintent', () => logRuntimeArguments('newintent'), false)
    newIntentRegistered = true
    console.info(OMG_APP_LOG, 'newintent_listener_registered')
  }

  if (typeof plus !== 'undefined') register()
  else document.addEventListener('plusready', register, false)
  // #endif
}

export default {
  onLaunch(options) {
    console.info(OMG_APP_LOG, 'onLaunch', {
      path: options && options.path ? options.path : '',
      queryKeys: options && options.query ? Object.keys(options.query) : []
    })
    registerNewIntentLogger()
    logRuntimeArguments('onLaunch_runtime')
  },
  onShow(options) {
    console.info(OMG_APP_LOG, 'onShow', {
      path: options && options.path ? options.path : '',
      queryKeys: options && options.query ? Object.keys(options.query) : [],
      route: currentRouteSummary()
    })
    logRuntimeArguments('onShow_runtime')
  },
  onHide() {
    console.info(OMG_APP_LOG, 'onHide', { route: currentRouteSummary() })
  },
  onError(error) {
    console.error(OMG_APP_LOG, 'app_error', {
      errorType: typeof error,
      route: currentRouteSummary()
    })
  }
}
```

App 已经在前台时，第三方 Scheme 正常唤起至少应看到 `newintent` 或新的 `onShow_runtime`，其中 `argument.present=true`，且 `protocol=com.twanmsdyh.app:`。App 从未启动时，应看到 `onLaunch_runtime` 携带同样的脱敏参数摘要。

#### 4.4.2 `omgCheckout.vue`：支付子 WebView 生命周期

App-vue 的 `<web-view>` 在 App 平台是父页面下的子 WebView，不要依赖组件的 `@load/@error`。在现有 `omgCheckout.vue` 的 `onReady` 和 `methods` 中加入以下代码：

```js
function summarizeWebviewUrl(raw) {
  const value = typeof raw === 'string' ? raw : ''
  const url = value.match(/^([a-z][a-z0-9+.-]*):\/\/([^/?#]*)([^?#]*)/i)
  return {
    protocol: url ? url[1].toLowerCase() + ':' : '',
    host: url ? url[2] : '',
    pathname: url ? url[3] : ''
  }
}

export default {
  data: () => ({
    src: '',
    omgWebviewLoggerAttached: false
  }),
  onLoad(opt) {
    this.src = '/static/omgPay.html?' + decodeURIComponent(opt.pay)
    console.info('[OMG-APP]', 'checkout_onLoad', {
      hasPayPayload: !!opt.pay,
      payloadLength: opt.pay ? String(opt.pay).length : 0
    })
  },
  onReady() {
    // #ifdef APP-PLUS
    ;[100, 500, 1200].forEach(delay => {
      setTimeout(() => this.attachOmgWebviewLogger(delay), delay)
    })
    // #endif
  },
  onShow() {
    console.info('[OMG-APP]', 'checkout_onShow')
  },
  onHide() {
    console.info('[OMG-APP]', 'checkout_onHide')
  },
  onUnload() {
    console.info('[OMG-APP]', 'checkout_onUnload')
  },
  methods: {
    attachOmgWebviewLogger(delay) {
      // #ifdef APP-PLUS
      if (this.omgWebviewLoggerAttached) return
      const parent = this.$scope && this.$scope.$getAppWebview
        ? this.$scope.$getAppWebview()
        : null
      const children = parent ? parent.children() : []
      const child = children && children.length ? children[0] : null

      if (!child) {
        console.warn('[OMG-APP]', 'checkout_webview_missing', { delayMs: delay })
        return
      }

      this.omgWebviewLoggerAttached = true
      const detail = event => ({
        event,
        id: child.id || '',
        url: summarizeWebviewUrl(child.getURL ? child.getURL() : '')
      })
      ;['loading', 'loaded', 'error', 'show', 'hide', 'close'].forEach(event => {
        child.addEventListener(event, () => {
          const payload = detail(event)
          if (event === 'error') console.error('[OMG-APP]', 'checkout_webview_event', payload)
          else console.info('[OMG-APP]', 'checkout_webview_event', payload)
        })
      })
      console.info('[OMG-APP]', 'checkout_webview_logger_attached', detail('attached'))
      // #endif
    }
  }
}
```

#### 4.4.3 真机日志采集步骤

1. 使用 HBuilderX 运行 iOS 真机，先清空控制台并过滤 `OMG-APP`。
2. iPhone 打开“设置 → Safari → 高级 → Web 检查器”；Mac Safari 在“开发”菜单选择该设备和支付子 WebView，过滤 `OMG-RETURN`。
3. 从 App 发起一笔 OMG 信用卡支付。正常情况会在 `bridge_redirect_start` 后直接进入结果页；若中转页停留超过 3 秒，再点击一次“返回 App”。
4. 导出从 `[OMG-RETURN] page_loaded` 到 `bridge_redirect_start` 或 `scheme_fallback_start` 后最后一条生命周期日志，以及同一时段全部 `[OMG-APP]` 日志。
5. 日志不得包含 token、CheckMacValue、HashKey、HashIV、完整订单号或银行卡资料；发现后先删除敏感内容再传给后端排查。

#### 4.4.4 日志判读

| 日志表现 | 失败边界 |
|----------|----------|
| 没有 `[OMG-RETURN] page_loaded` | OMG 没有进入 `/pay/omg/result`，或 Safari 检查器选错 WebView |
| 有 `bridge_environment` 且 `plus=true`，随后出现 `bridge_redirect_start` 并进入结果页 | App 内 Bridge 回跳成功；不会出现 `newintent`，属于正常行为 |
| 出现 `bridge_environment` 但 `plus/nvue/uvue` 均为 `false` | 当前页面不在 uni-app App Bridge 环境，后端会进入 Scheme 兜底 |
| 只有 `bridge_ready_timeout`，没有任何 `bridge_ready/bridge_environment` | 先直接访问 `/pay/omg/bridge.js`，必须返回 HTTP 200、`application/javascript` 和 SDK 内容；若返回 JSON/401，Bridge 不会执行 |
| 出现 `scheme_fallback_start`，外部浏览器可打开同一 Scheme，但 App 内页面仍停留 | 当前子 WebView 未把同 App Scheme 交给 iOS；继续检查 Bridge SDK 是否返回 200 及 `bridge_environment` 日志 |
| 中转页出现 `visibilitychange/pagehide`，但 App 没有任何新日志 | iOS 没有找到可处理该 Scheme 的安装包，重点检查 iOS `urltypes` 与重新打包结果 |
| App 出现 `newintent/onShow_runtime` 且 `argument.present=true`，但路由没变化 | App 已收到 Scheme，路由解析或 `navigateTo/redirectTo` 逻辑有问题 |
| App 已进入 `pages/OrderList/paySuccess/paySuccess`，但结果不更新 | 回跳已成功，继续检查 `/pay/omg/query` 请求与返回 |

> `[OMG-RETURN]` 日志来自后端返回的子 WebView 页面，通常需要 Safari Web Inspector 查看；`[OMG-APP]` 来自 uni-app 逻辑层，可在 HBuilderX 真机控制台查看。两者不是同一个 JavaScript 执行环境。

## 5. 状态码 / 付款方式速查

### `payStatus`（订单 + 流水）
| 值 | 含义 |
|----|------|
| 0 | 未支付 |
| 1 | 已支付 |
| 2 | 已退款（订单）/ 失败（流水） |
| 3 | 已退款（流水） |

### `payType`（OMG 回覆的付款方式）
| payType | 名称 | 即时/延期 | 可自动退款 |
|---------|------|----------|-----------|
| `Credit_CreditCard` | 信用卡（含 Apple Pay；银联已隐藏） | 即时 | ✅ |

> 当前开发测试范围只有信用卡和 Apple Pay，不存在延期支付历史交易兼容。

---

## 6. 配置依赖（需前端 ↔ 后端约定）

后端这几个 URL 来自 `application.yml` 的 `omg.*` 配置，**前端要把自己页面的线上地址给后端配置**：

| 配置项 | 含义 | 需要的值（示例） |
|--------|------|----------------|
| `omgpay.order-result-url` | OMG 付款完成 Client POST 地址 | `https://api.xxx.com/pay/omg/result` |
| `omgpay.app-return-url` | Bridge 不可用时唤醒 App 的固定 Scheme，同时用于推导固定 App 内路由 | `com.twanmsdyh.app://pages/OrderList/paySuccess/paySuccess` |
| `omgpay.notify-url` | 服务端结果回调（前端不用管） | `https://api.xxx.com/pay/omg/notify` |

> App 必须在 `pages.json` 注册 `/pages/OrderList/paySuccess/paySuccess`，同时注册 `com.twanmsdyh.app` Scheme 作为外部浏览器和 Bridge 异常时的兜底。

---

## 7. 注意事项 / 易踩坑

1. **不要修改任何 form 字段值**——尤其 `CheckMacValue`，改一个字符 OMG 验签就失败。
2. **结果不能只信网页跳转**——OMG 网页跳回 ≠ 服务端回调已完成，必须轮询 `payStatus` 确认。
3. **iOS web-view 不可另开窗**——收银台必须是完整页 web-view，不能 `window.open`、不能 iframe 嵌套。
4. **query 传参必须 encodeURIComponent**——`MerchantTradeDate` 含 `/` 和空格，不编码会截断。
5. **多次发起**——同一订单可重复发起（每次新 `MerchantTradeNo`），后端按 OMG 交易号 `tradeNo` 幂等，前端重复点「支付」不会重复扣款，但仍建议按钮防抖（后端已加 1s `@RepeatSubmit`）。
6. **付款方式固定**——表单只使用 `ChoosePayment=Credit`、`UnionPay=2`，不实现延期支付入口。
7. **退款范围**——当前订单只有信用卡/Apple Pay。

---

## 8. i18n 提示（项目硬性要求）

所有面向用户的文字必须走 `$t()`，4 语言（zh/tw/en/vi）同步添加，禁止硬编码中文。本功能涉及的文案 key 建议：

| key | 中文 | 用途 |
|-----|------|------|
| `payOmg.goPay` | 去支付 | 发起按钮 |
| `payOmg.redirecting` | 正在跳转至支付页面 | omgPay.html / 跳转中 |
| `payOmg.success` | 支付成功 | 结果页 |
| `payOmg.fail` | 支付失败 | 结果页 |
| `payOmg.confirming` | 支付结果确认中 | 轮询中 |
| `payOmg.storeNotSupported` | 该门店暂不支持在线支付 | 门店未开通 |

> key 必须用有意义英文（驼峰），加到 4 个语言文件**对应层级对象内**（参考项目 CLAUDE.md 的 i18n 规范）。

---

## 附：后端接口文件对照

| 前端动作 | 后端方法 | 文件 |
|---------|---------|------|
| 发起支付 | `OmgPayController.create` | `app/pay/OmgPayController.java` |
| 支付结果桥接页 | `OmgPaymentReturnController.result` / `back` | `app/omgpay/OmgPaymentReturnController.java` |
| 状态查询 | `OmgPaymentController.query` | `app/omgpay/OmgPaymentController.java` |
| 退款 | `OmgPaymentController.refund` | 同上 |
| 服务端回调（前端无感） | `OmgPaymentController.notify` | 同上 |

后端契约详见 `specs/016-omg-payment/contracts/api.md`。
