适用对象:用户端 App(uni-app)前端开发人员
接口版本:specs/020-omg-payment-rebuild 新实现
当前环境:OMG 测试环境
支付方式目标:App 只提供信用卡、Apple Pay,OMG 直接进入所选渠道
更新时间:2026-08-18
本文只描述当前新 OMG 实现。
specs/016-omg-payment中的旧 Controller、旧流水、旧退款和旧前端文档已经退役,不得作为接入依据。
OMG 是网页托管式收银台,App 不需要接入 OMG SDK。前端只需要完成以下流程:
payType = "2"。paymentMethod = "CREDIT" 或 "APPLE_PAY"。POST /pay/omg/create 获取所选渠道的 gatewayUrl 和完整 formFields。formFields 原样以 HTML Form POST 到 gatewayUrl。/pay/omg/result;后端验证返回资料后,优先通过 uni-app Bridge 打开支付结果页,Bridge 不可用时再通过 App Scheme 兜底。ddId,调用 POST /pay/omg/query 确认最终支付状态。status = "PAID" 才能展示支付成功。App 需要新增“信用卡 / Apple Pay”选择,但不接入原生 Apple Pay SDK。后端只接受两个稳定枚举并生成单渠道签名表单,因此 OMG 页面不会再展示超商快付、AFTEE 或其他渠道选择项。用户可见文案必须接入 App 现有 i18n。
当前测试环境不支持退款。POST /pay/omg/refund 只会返回“测试环境不支持退款”,不会发起真实退款,也不会修改订单。
[App 创建订单,payType="2"]
|
v
[App 选择 CREDIT / APPLE_PAY]
|
| POST /pay/omg/create
| Header: token
| Body: {"orderId":"...","paymentMethod":"..."}
v
[后端返回 gatewayUrl + formFields]
|
| 当前 WebView 原样 Form POST
v
[OMG 测试环境:直接进入所选付款流程]
|
+-------------------------------+
| |
| 服务端付款通知 | 浏览器付款结果返回
| POST /pay/omg/notify | POST /pay/omg/result
| 前端不调用 | 前端不直接调用
v v
[后端验签并更新付款事实] [后端安全桥接页打开 App 结果路由]
|
| iOS: 父 uni-app 页面 redirectTo
| 其他环境: uni.webView.redirectTo
| 失败时: App Scheme 兜底
v
[App 支付结果页]
|
| POST /pay/omg/query
v
[PAID 才展示支付成功]
必须注意:
create.status = "CREATED" 只表示后端已创建本地支付尝试和签名表单,不表示 OMG 已收单,更不表示已付款。/pay/omg/query 返回的 data.status 为准。以下接口路径都拼接在 App 当前使用的后端 Base URL 后,例如:
https://foodieapi.waimai-paotui.com/pay/omg/create
App 主动调用的 create、query 和 refund 接口都必须携带登录 token:
token: <用户登录 token>
token 只发送给本项目后端,绝对不能放入 OMG Form,也不能发送给 gatewayUrl。
成功响应:
{
"code": 200,
"msg": "操作成功",
"data": {}
}
业务失败响应:
{
"code": 500,
"msg": "已国际化的错误提示",
"data": {
"status": "稳定的业务状态码"
}
}
处理规则:
code。code === 200 时再读取 data。code !== 200 时直接向用户展示后端返回的 msg;msg 已按项目语言国际化。data.status,不要匹配 msg 文本。create/retry 请求使用:
{"orderId":"业务订单号","paymentMethod":"CREDIT 或 APPLE_PAY"}
query/refund 请求仍使用 {"orderId":"业务订单号"}。
后端 App Scheme 回跳参数使用:
?ddId=<业务订单号>
App 收到 ddId 后,把它作为 orderId 调用 query。不要把 create/query 的 JSON 字段写成 ddId、orderid 或其他名称。
POST /pay/omg/create
Content-Type: application/json
token: <用户登录 token>
{
"orderId": "991786433092835",
"paymentMethod": "CREDIT"
}
前置条件:
payType 必须为字符串 "2"。create/retry 只能传 orderId 与 paymentMethod;paymentMethod 仅允许 CREDIT/APPLE_PAY。query/refund 仍只传 orderId。金额、商户号、回调地址、网关地址、OMG 原始付款参数和签名都由后端决定。
当前测试环境返回示例:
{
"code": 200,
"msg": "操作成功",
"data": {
"status": "CREATED",
"gatewayUrl": "https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5",
"formFields": {
"MerchantID": "1000031",
"MerchantTradeNo": "OMGR8K3P7W2M9C4X6A1B",
"MerchantTradeDate": "2026/08/14 10:30:00",
"PaymentType": "aio",
"TotalAmount": "100",
"TradeDesc": "Foodie order 991786433092835",
"ItemName": "Order 991786433092835",
"ReturnURL": "https://foodieapi.waimai-paotui.com/pay/omg/notify",
"OrderResultURL": "https://foodieapi.waimai-paotui.com/pay/omg/result",
"ChoosePayment": "Credit",
"UnionPay": "2",
"EncryptType": "1",
"InvoiceMark": "N",
"NeedExtraPaidInfo": "Y",
"CheckMacValue": "<64 位十六进制签名>"
}
}
}
上例为信用卡响应。Apple Pay 响应的 ChoosePayment 为 ApplePay,且不包含 UnionPay 或 IgnorePayment。后端不会返回 ChoosePayment=ALL。
字段说明:
| 字段 | 类型 | App 用法 |
|---|---|---|
status |
string | 固定为 CREATED;不能当作支付成功 |
gatewayUrl |
string | HTML Form 的 action,本身不是 Form 字段 |
formFields |
object | 所有键值都必须原样创建为隐藏 input 并 POST |
MerchantTradeNo |
string | 后端生成的 OMG 交易编号;前端只透传,不作为业务订单号 |
TotalAmount |
string | 后端根据订单金额生成;前端不得修改 |
ChoosePayment |
string | 后端按 paymentMethod 生成 Credit 或 ApplePay;前端不得修改 |
UnionPay |
string | 仅信用卡表单存在且固定为 2,用于隐藏银联;Apple Pay 表单不包含该字段 |
ReturnURL |
string | OMG 服务端付款通知地址;前端不调用 |
OrderResultURL |
string | OMG 浏览器付款结果地址;前端不直接调用 |
CheckMacValue |
string | 表单签名;修改任意已签名字段都会导致 OMG 拒绝 |
后端当前实际返回的字段集合以 formFields 为准。前端不要维护字段白名单,不要自行新增、删除、改名、格式化或重新计算字段。
必须遵守:
POST,不能把字段拼成 GET URL。Content-Type 由浏览器 Form 提交为 application/x-www-form-urlencoded。window.open 新窗口。gatewayUrl 只作为 Form action,不能创建名为 gatewayUrl 的 input。formFields 中的每个键值都创建为隐藏 input,并保持值完全不变。formFields、CheckMacValue 或 token 写入业务日志、埋点、崩溃上报或远程调试日志。浏览器侧核心代码:
function submitOmgForm(gatewayUrl, formFields) {
if (!gatewayUrl || !formFields || typeof formFields !== 'object') {
throw new Error('OMG payment payload is invalid');
}
const form = document.createElement('form');
form.method = 'POST';
form.action = gatewayUrl;
form.acceptCharset = 'UTF-8';
Object.entries(formFields).forEach(([name, value]) => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = name;
input.value = value == null ? '' : String(value);
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
}
建议通过页面 eventChannel、内存状态或原生桥接把 create 响应交给收银台页面,再由收银台页面的 renderjs 创建并提交 Form。
不要把整个 formFields 拼入页面 URL query。这样会把 CheckMacValue 和完整支付表单暴露在浏览记录、路由日志或错误上报中。
发起页示例:
async function startOmgPayment(orderId, paymentMethod) {
if (this.omgSubmitting) return;
this.omgSubmitting = true;
try {
const response = await request({
url: '/pay/omg/create',
method: 'POST',
header: { token: uni.getStorageSync('token') },
data: { orderId, paymentMethod }
});
if (response.code !== 200) {
uni.showToast({ title: response.msg, icon: 'none' });
return;
}
const payload = response.data;
uni.navigateTo({
url: '/pages/pay/omgCheckout/omgCheckout',
success: ({ eventChannel }) => {
eventChannel.emit('omgCheckoutPayload', payload);
}
});
} finally {
this.omgSubmitting = false;
}
}
收银台页面只需要接收:
{
status: 'CREATED',
gatewayUrl: 'https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5',
formFields: { /* 后端原样返回的全部字段 */ }
}
然后在该页面的 WebView/renderjs 环境调用:
submitOmgForm(payload.gatewayUrl, payload.formFields);
具体页面路径和请求封装按 App 现有结构调整,Form POST 规则不能改变。
data.status |
含义 | App 处理建议 |
|---|---|---|
AUTH_REQUIRED |
token 无法取得登录用户 | 返回登录页 |
ORDER_REQUIRED |
orderId 缺失或格式不合法 |
提示后返回订单页 |
ORDER_NOT_AVAILABLE |
订单不存在或不属于当前用户 | 展示后端 msg |
MULTI_STORE_ORDER_NOT_SUPPORTED |
当前是多门店子订单 | 展示后端 msg,不可继续支付 |
ORDER_STATE_NOT_PAYABLE |
当前订单状态不可付款 | 刷新订单数据 |
ORDER_ALREADY_PAID |
订单已经付款 | 进入订单结果页并刷新状态 |
ORDER_AMOUNT_INVALID |
订单金额异常 | 展示后端 msg |
PAYMENT_TYPE_INVALID |
订单 payType 不是 "2" |
检查创建订单时的支付方式 |
PAYMENT_METHOD_INVALID |
paymentMethod 缺失或不是 CREDIT/APPLE_PAY |
停止支付并检查 App 渠道映射 |
STORE_CREDENTIAL_UNAVAILABLE |
门店未启用有效 OMG 凭证 | 展示后端 msg |
PAYMENT_ATTEMPT_EXISTS |
已有未结束的支付尝试 | 用户确认重新支付时调用 /pay/omg/retry,不要循环调用 create |
PAYMENT_CONFIGURATION_INVALID |
后端测试网关或回调配置不合法 | 展示后端 msg,通知后端排查 |
PAYMENT_CREATION_FAILED |
本次创建失败 | 展示后端 msg,稍后重试 |
同一订单创建成功后,重复点击不会返回旧表单,而是返回 PAYMENT_ATTEMPT_EXISTS。因此支付按钮必须防重复点击;如果用户只是等待支付结果,调用 query;如果用户已经退出支付页并明确再次支付,调用 retry。不要循环调用 create,也不要缓存并重新提交旧表单。
/pay/omg/resultPOST /pay/omg/result
Content-Type: application/x-www-form-urlencoded
该接口由 OMG 收银台调用,不是 App API。后端会验证返回表单的签名、商户号、OMG 交易编号和金额,然后返回一个禁止缓存的安全桥接页面。
桥接页面的目标路由固定为:
com.twanmsdyh.app://pages/OrderList/paySuccess/paySuccess?ddId=<URL 编码后的业务订单号>
在 uni-app App 的 <web-view> 中,页面先加载后端自托管的 Bridge SDK。iOS App-Plus 环境由支付子 WebView 的父 uni-app 页面执行 uni.redirectTo,避免旧运行时调用不存在的 UniPlusBridge;Android 等其他 App 环境继续使用 uni.webView.redirectTo。上述方式不可用或没有完成页面交接时,页面才使用 com.twanmsdyh.app Scheme 兜底。
页面同时保留“返回 App”按钮,自动跳转失败时用户可手动点击。App 端现有支付 WebView 页面不需要为本次 iOS 修复新增接口或页面,但仍必须保留 Scheme 注册作为兜底。
App 必须注册以下 Scheme:
com.twanmsdyh.app
并把以下路由映射到现有支付结果页:
pages/OrderList/paySuccess/paySuccess
结果页读取参数:
ddId=<业务订单号>
收到 Scheme 后的正确处理:
ddId。/pay/omg/query,请求体使用 { "orderId": ddId }。data.status 展示结果。Scheme 参数可以被外部伪造。禁止因为进入支付结果路由或拿到 ddId 就直接展示付款成功。
POST /pay/omg/query
Content-Type: application/json
token: <用户登录 token>
{
"orderId": "991786433092835"
}
query 不是简单读取本地状态。后端会使用当前支付尝试的凭证快照查询 OMG 测试网关、验证完整响应,并在付款回调丢失时补偿订单付款状态。因此 App 不应高频调用。
已付款示例:
{
"code": 200,
"msg": "操作成功",
"data": {
"status": "PAID",
"tradeStatus": "1",
"merchantTradeNo": "OMGR8K3P7W2M9C4X6A1B",
"tradeNo": "26081400000000000001",
"amount": 100,
"paymentDate": "2026/08/14 10:32:10",
"tradeDate": "2026/08/14 10:30:00",
"paymentType": "Credit_CreditCard",
"handlingCharge": "0",
"paymentTypeChargeFee": "3"
}
}
未付款时,tradeNo、paymentDate、paymentType 等字段可能为 null 或空字符串。前端应以 status 为主,不要依赖某个详情字段是否为空判断成功。
字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
status |
string | 前端判断结果的标准化状态 |
tradeStatus |
string | OMG 原始交易状态,仅用于排查或辅助展示 |
merchantTradeNo |
string | 后端生成的 OMG 特店交易编号 |
tradeNo |
string/null | OMG 金流交易编号;始终按字符串处理 |
amount |
number | 订单金额,整数 TWD |
paymentDate |
string/null | OMG 付款时间,台北时区 |
tradeDate |
string/null | OMG 建单时间,台北时区 |
paymentType |
string/null | OMG 尚未确认付款渠道时为空;完成渠道选择/付款并在 query 或回调中回传后才有值。信用卡与 Apple Pay 可能同样回传 Credit_CreditCard,App 不用它做二级支付方式选择 |
handlingCharge |
string/null | 手续费相关原始值 |
paymentTypeChargeFee |
string/null | 付款方式手续费原始值 |
status 处理status |
含义 | App 行为 |
|---|---|---|
PAID |
已确认付款 | 展示支付成功,刷新订单 |
UNPAID |
OMG 当前仍未付款 | 保持“结果确认中”,按受控频率继续查询 |
FAILED |
当前支付尝试已失败 | 展示支付未完成,停止本轮轮询 |
UNKNOWN |
OMG 返回了当前系统未归一化的状态 | 不得当作成功;提示确认中并受控重试 |
OMG 原始 tradeStatus 常见值:
tradeStatus |
标准化结果 |
|---|---|
1 |
PAID |
0 |
UNPAID |
10200095 |
FAILED |
| 其他 | UNKNOWN |
App 从 Scheme 返回后立即查询一次。如果结果是 UNPAID 或 UNKNOWN:
PAID 或 FAILED 时立即停止。msg。参考代码:
async function confirmOmgPayment(orderId) {
const maxAttempts = 10;
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
const response = await request({
url: '/pay/omg/query',
method: 'POST',
header: { token: uni.getStorageSync('token') },
data: { orderId }
});
if (response.code !== 200) {
uni.showToast({ title: response.msg, icon: 'none' });
return;
}
const status = response.data.status;
if (status === 'PAID') {
showPaymentSuccess();
return;
}
if (status === 'FAILED') {
showPaymentIncomplete();
return;
}
await new Promise(resolve => setTimeout(resolve, 3000));
}
showPaymentConfirming();
}
实际代码需要结合页面生命周期取消定时器或异步任务,避免离开页面后继续请求。
data.status |
含义 | App 处理建议 |
|---|---|---|
AUTH_REQUIRED |
登录信息不可用 | 返回登录页 |
ORDER_REQUIRED |
orderId 缺失或不合法 |
停止查询并返回订单页 |
ORDER_NOT_AVAILABLE |
订单不存在或不属于当前用户 | 展示后端 msg |
PAYMENT_QUERY_NOT_AVAILABLE |
当前没有可查询的 OMG 支付尝试 | 刷新订单后展示后端 msg |
PAYMENT_QUERY_FAILED |
OMG 查询失败、响应非法或身份校验失败 | 展示后端 msg,稍后人工重试 |
App 返回业务页面后原支付 WebView 会销毁,本流程不要求保存或恢复 WebView。旧 gatewayUrl + formFields 也不能当作“继续付款网址”缓存重放:其中的 MerchantTradeNo 只能建单一次,重新提交会被 OMG 以重复订单编号拒绝。
用户点击“重新支付”时调用:
POST /pay/omg/retry
Content-Type: application/json
token: <用户登录 token>
{
"orderId": "991786433092835",
"paymentMethod": "APPLE_PAY"
}
后端会先使用旧尝试的凭证快照向 OMG 查询真实状态,然后按以下规则处理:
| OMG 查询结果 | 后端处理 |
|---|---|
PAID |
不创建新支付,返回 ORDER_ALREADY_PAID |
FAILED |
旧尝试已经结束,生成新的 MerchantTradeNo 和新表单 |
本地无进行中的尝试(旧尝试已被自动补偿关闭为终态 FAILED/SUPERSEDED,active 指针已释放) |
不再查询旧尝试,直接生成新的 MerchantTradeNo 和新表单(等价首次 create;订单已付会被 create 的校验以 ORDER_ALREADY_PAID 拒绝) |
UNPAID 且 paymentType 为空 |
原子地把旧尝试改为 SUPERSEDED,生成新的 MerchantTradeNo 和新表单 |
UNPAID 且 paymentType 非空 |
为避免覆盖可能正在授权的交易,返回 PAYMENT_RETRY_NOT_AVAILABLE |
UNKNOWN、查询失败或并发状态已改变 |
不修改旧尝试,返回对应错误 |
retry 成功响应与 create 完全相同。App 收到后创建新的支付 WebView,将新的全部 formFields 以 Form POST 提交到新的 gatewayUrl:
{
"code": 200,
"msg": "操作成功",
"data": {
"status": "CREATED",
"gatewayUrl": "https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5",
"formFields": {
"MerchantTradeNo": "OMGR8K3P7W2M9C4X6A1B"
}
}
}
paymentType 是 OMG 查询结果,不是 App 传参;paymentMethod 才是 App 本次选择并提交给 create/retry 的受控枚举。用户重新支付时可以重新选择信用卡或 Apple Pay。
App 推荐处理:
async function retryOmgPayment(orderId, paymentMethod) {
const response = await request({
url: '/pay/omg/retry',
method: 'POST',
header: { token: uni.getStorageSync('token') },
data: { orderId, paymentMethod }
});
if (response.code === 200) {
openNewOmgWebView(response.data.gatewayUrl, response.data.formFields);
return;
}
if (response.data?.status === 'ORDER_ALREADY_PAID') {
await confirmOmgPayment(orderId);
return;
}
uni.showToast({ title: response.msg, icon: 'none' });
}
retry 可能返回 query、create 的既有错误,也可能返回:
data.status |
App 处理建议 |
|---|---|
PAYMENT_RETRY_NOT_AVAILABLE |
不自动循环;刷新订单并调用 query 确认状态 |
PAYMENT_RETRY_FAILED |
提示稍后重试,不复用旧表单 |
/pay/omg/notifyPOST /pay/omg/notify
Content-Type: application/x-www-form-urlencoded
这是 OMG 到后端的 server-to-server 回调:
1|OK 或 0|ERROR 响应。/pay/omg/result这是 OMG 收银台到后端的浏览器返回接口:
POST /pay/omg/refund
Content-Type: application/json
token: <用户登录 token>
{
"orderId": "991786433092835"
}
{
"code": 500,
"msg": "OMG 测试环境不支持退款,请在正式环境启用后操作",
"data": {
"status": "PAYMENT_REFUND_UNAVAILABLE_IN_TEST_ENVIRONMENT"
}
}
当前接口只是测试阶段的安全拒绝边界:
因此当前 App 不应把 /pay/omg/refund 接入订单取消流程,也不能向用户展示“退款成功”。正式环境退款需要后端另行实现并确认契约后,前端才能接入。
orderId,不是 ddId;ddId 只出现在 App Scheme 参数中。"2";LINE Pay 是 "3",不要混用。CREATED 不是支付成功。status === "PAID" 才能展示支付成功。formFields;尤其不能修改 TotalAmount、回调地址和 CheckMacValue。formFields 或 CheckMacValue 放入 URL、日志、埋点或错误上报。tradeNo、merchantTradeNo 始终按字符串处理,不转换为 JavaScript Number。PAYMENT_ATTEMPT_EXISTS 不能靠重复调用 create 解决,应进入 query 确认流程。CREDIT/APPLE_PAY;不得传 ChoosePayment、UnionPay、IgnorePayment 等 OMG 原始字段,也不得通过 CSS 或 DOM 修改第三方支付页面。payType = "2"。CREDIT/APPLE_PAY;用户可见文案使用现有 i18n。orderId 与 paymentMethod。gatewayUrl。formFields 原样提交,gatewayUrl 不作为 input。CREDIT 直接进入信用卡流程,表单为 ChoosePayment=Credit + UnionPay=2,不显示其他渠道。APPLE_PAY 在门店已开通且设备支持时直接进入 Apple Pay 流程,表单为 ChoosePayment=ApplePay,不显示其他渠道。ChoosePayment=ALL 或 IgnorePayment,页面不出现超商快付、AFTEE。com.twanmsdyh.app Scheme。pages/OrderList/paySuccess/paySuccess。pages/OrderList/paySuccess/paySuccess。ddId。ddId 作为 orderId 调用 query。PAID 才展示成功。UNPAID/UNKNOWN 受控轮询,不误判为失败或成功。FAILED 停止本轮轮询并展示未完成。msg。data.status,不匹配中文文案。PAYMENT_ATTEMPT_EXISTS 不会触发 create 重试循环。CheckMacValue 不出现在日志、URL、埋点或上报中。| 用途 | Method | 路径 | token | 调用方 |
|---|---|---|---|---|
| 创建支付 | POST | /pay/omg/create |
必须 | App |
| 查询结果 | POST | /pay/omg/query |
必须 | App |
| 重新支付 | POST | /pay/omg/retry |
必须 | App |
| 退款安全拒绝 | POST | /pay/omg/refund |
必须 | 当前 App 不调用 |
| 付款结果通知 | POST | /pay/omg/notify |
不需要 | OMG 服务器 |
| 浏览器结果返回 | POST | /pay/omg/result |
不需要 | OMG 收银台 |
后端当前配置为:
omgpay:
return-url: https://foodieapi.waimai-paotui.com/pay/omg/notify
order-result-url: https://foodieapi.waimai-paotui.com/pay/omg/result
app-return-url: com.twanmsdyh.app://pages/OrderList/paySuccess/paySuccess
App 接入人员只需要确保 Scheme 和结果页路由与 app-return-url 一致。return-url 和 order-result-url 是后端与 OMG 的配置,前端不得覆盖。