# 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=Credit`、`UnionPay=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,打开 加载 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.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 | --- ## 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`) ```json { "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`。 ```html 跳转支付中
正在跳转至支付页面
``` ### 4.2 发起支付页(点「去支付」) ```js // 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) }); } ``` ```html ``` > 注意:iOS 上 web-view 不可另开窗、收银台不可被 iframe 套——本方案是完整页 web-view,符合要求。 ### 4.3 支付结果页 `pages/pay/omgResult` 后端 `/pay/omg/result` 返回桥接页并打开 `pages/OrderList/paySuccess/paySuccess`。App 结果页取得 `ddId` 后查询后端确认支付状态。 ```js // 支付成功页面加载后,从链接参数取得 ddId 并开始查询。 ``` 原生订单详情页(回来后)**轮询确认状态**: ```js // 信用卡/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.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.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=Credit`、`UnionPay=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`。