# 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,打开 加载 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.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 | --- ## 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`) ```json { "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`,后端反查出 `ddId` 后 **302 重定向**到前端结果页: ``` {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... ``` **返回** ```json { "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`。 ```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/return` 会 302 到 `omgResult?ddId=xxx`。这个页面在 web-view 内被加载——加载后建议**立即跳回原生页**或直接渲染结果。最稳的做法:web-view 监听 URL 变化,命中 `omgResult` 就 `navigateBack` 回原生订单详情,由原生页轮询状态。 ```js // pages/pay/omgCheckout.vue 增强:监听收银台跳到结果页 // uni-app web-view 可通过 @message 或 bindmessage 接收 H5 postMessage // 更简单:让 omgResult 页 postMessage 通知原生关闭 web-view ``` 原生订单详情页(回来后)**轮询确认状态**: ```js // 信用卡即时支付:轮询 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`: ```html 银行代码:{{ info.BankCode }} 虚拟帐号:{{ info.vAccount }} 缴费期限:{{ info.ExpireDate }} ``` 并提示(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.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`。测试期可用测试域名。 --- ## 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`。