适用端:客户 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=Credit、UnionPay=2,OMG 收银台只提供 信用卡与 Apple Pay。前端不需要集成任何支付 SDK,只需要把用户「跳过去」再「接回来」。
[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来判断,因为服务端回调可能晚到几秒。
| # | 事项 | 在哪做 |
|---|---|---|
| 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 |
baseURL:前端约定的后端网关地址(如
https://api.xxx.com),下面路径在其后拼接。 所有需登录接口:Header 必须带token(用户 JWT)。
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 验签失败。前端只负责原样透传。
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 验签通过也不等于可以直接宣称已付款。
POST /pay/omg/queryApp 返回后用 Header token 和 JSON {"orderId":"..."} 调用。接口只查询该订单唯一有效支付尝试;若成功回调丢失,会向 OMG 查询并同步本地支付与订单状态。
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 |
以下为参考代码,需在 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/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);超过仍未成功提示「支付结果确认中,请稍后在订单列表查看」。
payStatus(订单 + 流水)| 值 | 含义 |
|---|---|
| 0 | 未支付 |
| 1 | 已支付 |
| 2 | 已退款(订单)/ 失败(流水) |
| 3 | 已退款(流水) |
payType(OMG 回覆的付款方式)| payType | 名称 | 即时/延期 | 可自动退款 |
|---|---|---|---|
Credit_CreditCard |
信用卡(含 Apple Pay;银联已隐藏) | 即时 | ✅ |
当前开发测试范围只有信用卡和 Apple Pay,不存在延期支付历史交易兼容。
后端这几个 URL 来自 application.yml 的 omg.* 配置,前端要把自己页面的线上地址给后端配置:
| 配置项 | 含义 | 需要的值(示例) |
|---|---|---|
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.appscheme,并保证该路径映射到现有支付成功页面。
CheckMacValue,改一个字符 OMG 验签就失败。payStatus 确认。window.open、不能 iframe 嵌套。MerchantTradeDate 含 / 和空格,不编码会截断。MerchantTradeNo),后端按 OMG 交易号 tradeNo 幂等,前端重复点「支付」不会重复扣款,但仍建议按钮防抖(后端已加 1s @RepeatSubmit)。ChoosePayment=Credit、UnionPay=2,不实现延期支付入口。所有面向用户的文字必须走 $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。