适用端:客户 App(uni-app) | 后端分支:OmgPayController 已完成 | 日期:2026-08-06
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,只需要把用户「跳过去」再「接回来」。
[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来判断,因为服务端回调可能晚到几秒。
| # | 事项 | 在哪做 |
|---|---|---|
| 1 | 放一个 omgPay.html 到 static/,作为 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 |
baseURL:前端约定的后端网关地址(如
https://api.xxx.com),下面路径在其后拼接。 所有需登录接口:Header 必须带token(用户 JWT)。
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 验签失败。前端只负责原样透传。
GET/POST /pay/omg/return(后端自动 302)不是前端直接调的接口。付款完成后 OMG 把网页跳到后端 /pay/omg/return,后端反查出 ddId 后 302 重定向到前端结果页:
{omg.order-result-url}?ddId=DD202608060001
前端只需:注册一个结果页路由接收 query 上的 ddId(见 §4.3)。omg.order-result-url 由后端配置(§6),需要前端把这个页面地址给后端。
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 先享后付信息 |
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,后端已记工单 |
以下为参考代码,需在 uni-app 工程内按实际路由/请求封装适配。
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>
// 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,符合要求。
pages/pay/omgResult后端 /pay/omg/return 会 302 到 omgResult?ddId=xxx。这个页面在 web-view 内被加载——加载后建议立即跳回原生页或直接渲染结果。最稳的做法:web-view 监听 URL 变化,命中 omgResult 就 navigateBack 回原生订单详情,由原生页轮询状态。
// 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);超过仍未成功提示「支付结果确认中,请稍后在订单列表查看」。
用户选 ATM/超商后,付款不是即时的——OMG 先给一个虚拟帐号/缴费码,用户在期限内自己去缴。前端展示来自 §3.3 的 info:
<view>银行代码:{{ info.BankCode }}</view>
<view>虚拟帐号:{{ info.vAccount }}</view>
<view>缴费期限:{{ info.ExpireDate }}</view>
<!-- 超商则展示 PaymentNo -->
并提示(i18n):「请在期限内完成缴费,缴费后订单将自动变为已支付」。
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,无需前端轮询到成功。
后端这几个 URL 来自 application.yml 的 omg.* 配置,前端要把自己页面的线上地址给后端配置:
| 配置项 | 含义 | 需要的值(示例) |
|---|---|---|
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。测试期可用测试域名。
CheckMacValue,改一个字符 OMG 验签就失败。payStatus 确认。window.open、不能 iframe 嵌套。MerchantTradeDate 含 / 和空格,不编码会截断。MerchantTradeNo),后端按 OMG 交易号 tradeNo 幂等,前端重复点「支付」不会重复扣款,但仍建议按钮防抖(后端已加 1s @RepeatSubmit)。所有面向用户的文字必须走 $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。