frontend-integration.md 17 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=ALL 已把 信用卡 / Apple Pay / ATM / 超商 / AFTEE 全聚合在一个收银台里,用户在那个网页上自己选方式付款。前端不需要集成任何支付 SDK,只需要把用户「跳过去」再「接回来」。


1. 整体流程(必读)

[App: 订单详情页 点"去支付"]
        │
        │  ① POST /pay/omg/create?orderid={ddId}   (Header: token)
        ▼
[后端] 校验订单 → 组参+签名 → 返回 form 字段(含 gatewayUrl)
        │
        │  返回 { data: { gatewayUrl, MerchantID, MerchantTradeNo, CheckMacValue, ... } }
        ▼
[App] ② 把 form 字段拼到 URL,打开 <web-view> 加载 omgPay.html
        │
        │  omgPay.html 自动 submit 隐藏表单 → 跳转
        ▼
[OMG 收银台网页]  用户选支付方式(信用卡/Apple Pay/ATM/超商)并付款
        │
        │  ③ 付款后 OMG 把 web-view 跳回 OrderResultURL
        │     后端 /pay/omg/return 302 → 前端结果页?ddId=xxx
        ▼
[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 支付结果页:接收 ddId → 查状态 → 展示成功/失败/取号 支付结果页
4 ATM/超商取号展示页:调 /pay/omg/paymentInfo/{orderid} 展示虚帐/缴费码 取号页(可与结果页合并)
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>
入参 query:orderid = 订单号 ddId
鉴权 登录用户 + 必须是订单本人

请求示例

POST {baseURL}/pay/omg/create?orderid=DD202608060001
Header: token: eyJhbGciOi...

成功返回code=200,取 data

{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "gatewayUrl": "https://payment.funpoint.com.tw/Cashier/AioCheckOut/V5",
    "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": "ALL",
    "EncryptType": "1",
    "InvoiceMark": "N",
    "NeedExtraPaidInfo": "Y",
    "OrderResultURL": "https://app.xxx.com/.../omgResult",
    "PaymentInfoURL": "https://api.xxx.com/pay/omg/paymentInfo",
    "CheckMacValue": "A1B2C3D4E5F6...(64位)"
  }
}

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

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


3.2 支付结果页跳转 — GET/POST /pay/omg/return(后端自动 302)

不是前端直接调的接口。付款完成后 OMG 把网页跳到后端 /pay/omg/return,后端反查出 ddId302 重定向到前端结果页:

{omg.order-result-url}?ddId=DD202608060001

前端只需:注册一个结果页路由接收 query 上的 ddId(见 §4.3)。omg.order-result-url 由后端配置(§6),需要前端把这个页面地址给后端。


3.3 ATM/超商取号查询 — GET /pay/omg/paymentInfo/{orderid}

用途:延期支付(ATM/超商/AFTEE)取号后,展示虚拟帐号 / 缴费码 / 期限。即时支付(信用卡)也可用它查 payStatus

内容
Method GET
Header token: <用户JWT>
入参 path:orderid = ddId
鉴权 登录用户 + 订单本人

请求示例

GET {baseURL}/pay/omg/paymentInfo/DD202608060001
Header: token: eyJ...

返回

{
  "code": 200,
  "data": {
    "payType": "ATM_FIRST",        // 付款方式(见 §5)
    "amount": 350,
    "payStatus": 0,                // 0未支付/1已支付/2失败/3已退款
    "info": {                       // 取号信息(信用卡即时支付时 info 为空 {})
      "BankCode": "807",
      "vAccount": "99123450000012",
      "ExpireDate": "2026/08/09",
      "TradeNo": "..."
    }
  }
}

info 字段说明(延期支付才有):

付款方式 payType info 里的关键字段
ATM_* BankCode(银行代码) vAccount(虚拟帐号) ExpireDate(缴费期限)
CVS_* PaymentNo(缴费码) ExpireDate(期限)
BarcodeATM_* 同 CVS
AFTEE_AFTEE AFTEE 先享后付信息

3.4 退款(取消已支付订单)— POST /pay/omg/refund

用途:用户取消已支付订单时退款(仅信用卡/Apple Pay 可自动退;ATM/超商需人工,接口会返回提示)。

内容
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 | | code=500 + 「该支付方式需人工在 OMG 后台退款」 | ATM/超商/BarcodeATM,无自动退款 API,后端已记工单 |


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/return 会 302 到 omgResult?ddId=xxx。这个页面在 web-view 内被加载——加载后建议立即跳回原生页或直接渲染结果。最稳的做法:web-view 监听 URL 变化,命中 omgResultnavigateBack 回原生订单详情,由原生页轮询状态。

// pages/pay/omgCheckout.vue 增强:监听收银台跳到结果页
// uni-app web-view 可通过 @message 或 bindmessage 接收 H5 postMessage
// 更简单:让 omgResult 页 postMessage 通知原生关闭 web-view

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

// 信用卡即时支付:轮询 paymentInfo 看是否 payStatus=1
const poll = (ddId) => {
  uni.request({
    url: baseURL + '/pay/omg/paymentInfo/' + ddId,
    header: { token: uni.getStorageSync('token') },
    success: (res) => {
      const d = res.data.data;
      if (d.payStatus === 1)      { showSuccess(); }       // 支付成功
      else if (d.payStatus === 2) { showFail(); }          // 失败
      else if (d.payType && d.payType.startsWith('ATM') ||
               d.payType && d.payType.startsWith('CVS'))   { showPickup(d.info); } // 延期→展示取号
      else { setTimeout(() => poll(ddId), 2000); }         // 待回调,继续轮询
    }
  });
};

轮询建议:每 2s 一次,最多 15 次(30s);超过仍未成功提示「支付结果确认中,请稍后在订单列表查看」。

4.4 ATM/超商取号展示(延期支付)

用户选 ATM/超商后,付款不是即时的——OMG 先给一个虚拟帐号/缴费码,用户在期限内自己去缴。前端展示来自 §3.3 的 info

<view>银行代码:{{ info.BankCode }}</view>
<view>虚拟帐号:{{ info.vAccount }}</view>
<view>缴费期限:{{ info.ExpireDate }}</view>
<!-- 超商则展示 PaymentNo -->

并提示(i18n):「请在期限内完成缴费,缴费后订单将自动变为已支付」。


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

payStatus(订单 + 流水)

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

payType(OMG 回覆的付款方式)

payType 名称 即时/延期 可自动退款
Credit_CreditCard 信用卡(含 Apple Pay / 银联) 即时
BarcodeATM_CHINATRUST 超商快付代碼繳费 即时 ❌ 人工
ATM_FIRST/CHINATRUST/UBOT/KGI 各银行 ATM 延期(取虚帐) ❌ 人工
CVS_CVS/FAMILY/IBON/HILIFE 超商代碼繳费 延期(取码) ❌ 人工
AFTEE_AFTEE AFTEE 先享后付 延期 ❌ 人工

即时支付(信用卡/Apple Pay)→ 结果页轮询 payStatus 变 1 即成功。 延期支付(ATM/超商/AFTEE)→ 结果页展示取号信息,等用户线下缴款后 OMG 异步回调才置 1,无需前端轮询到成功。


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

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

配置项 含义 需要的值(示例)
omg.order-result-url 付款后跳回的前端结果页 https://app.xxx.com/#/pages/pay/omgResult
omg.payment-info-url ATM/超商取号回调(后端地址,前端不用管 https://api.xxx.com/pay/omg/paymentInfo
omg.return-url 服务端结果回调(后端地址,前端不用管 https://api.xxx.com/pay/omg/notify

前端只需要确认:omgResult 页面的完整 URL(含 hash 路由或 history 路由),交给后端填 omg.order-result-url。测试期可用测试域名。


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. 延期支付别轮询到成功——ATM/超商是线下缴款,可能几小时甚至跨天,前端展示取号信息即可,不要无限轮询。
  7. 退款只对信用卡/Apple Pay 自动——ATM/超商选了就退不了,接口会返回「需人工」,前端按提示展示。

8. i18n 提示(项目硬性要求)

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

key 中文 用途
payOmg.goPay 去支付 发起按钮
payOmg.redirecting 正在跳转至支付页面 omgPay.html / 跳转中
payOmg.success 支付成功 结果页
payOmg.fail 支付失败 结果页
payOmg.confirming 支付结果确认中 轮询中
payOmg.bankCode 银行代码 取号展示
payOmg.vAccount 虚拟帐号 取号展示
payOmg.paymentNo 缴费代码 超商取号
payOmg.expireDate 缴费期限 取号展示
payOmg.atmTip 请在期限内完成缴费,缴费后订单将自动变为已支付 延期支付提示
payOmg.storeNotSupported 该门店暂不支持在线支付 门店未开通
payOmg.refundManual 该支付方式需人工退款,请联系客服 退款人工提示

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


附:后端接口文件对照

前端动作 后端方法 文件
发起支付 OmgPayController.create app/pay/OmgPayController.java
结果页 302 OmgPayController.returnCallback 同上
取号查询 OmgPayController.getPaymentInfo 同上
退款 OmgPayController.refund 同上
服务端回调(前端无感) OmgPayController.notify / paymentInfoCallback 同上

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