frontend-integration.md 14 KB

OMG(歐買尬/FunPoint)支付 — 前端接入文档

适用端:客户 App(uni-app) | 后端分支OmgPayController 已完成 | 日期:2026-08-06


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=CreditUnionPay=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.htmlstatic/,作为 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

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

{
  "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

<!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 发起支付页(点「去支付」)

// 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)
  });
}
<!-- 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。App 结果页取得 ddId 后查询后端确认支付状态。

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

原生订单详情页(回来后)轮询确认状态

// 信用卡/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);超过仍未成功提示「支付结果确认中,请稍后在订单列表查看」。

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

payStatus(订单 + 流水)

含义
0 未支付
1 已支付
2 已退款(订单)/ 失败(流水)
3 已退款(流水)

payType(OMG 回覆的付款方式)

payType 名称 即时/延期 可自动退款
Credit_CreditCard 信用卡(含 Apple Pay;银联已隐藏) 即时

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


6. 配置依赖(需前端 ↔ 后端约定)

后端这几个 URL 来自 application.ymlomg.* 配置,前端要把自己页面的线上地址给后端配置

配置项 含义 需要的值(示例)
omgpay.order-result-url OMG 付款完成 Client POST 地址 https://api.xxx.com/pay/omg/result
omgpay.app-return-url 后端桥接页唤醒 App 的固定地址 com.twanmsdyh.app://pages/OrderList/paySuccess/paySuccess
omgpay.notify-url 服务端结果回调(前端不用管) https://api.xxx.com/pay/omg/notify

App 必须注册 com.twanmsdyh.app scheme,并保证该路径映射到现有支付成功页面。


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=CreditUnionPay=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