|
|
@@ -0,0 +1,632 @@
|
|
|
+# 新 OMG 支付 App 对接文档
|
|
|
+
|
|
|
+**适用对象**:用户端 App(uni-app)前端开发人员
|
|
|
+**接口版本**:`specs/020-omg-payment-rebuild` 新实现
|
|
|
+**当前环境**:OMG 测试环境
|
|
|
+**支付方式**:信用卡、Apple Pay
|
|
|
+**更新时间**:2026-08-14
|
|
|
+
|
|
|
+> 本文只描述当前新 OMG 实现。`specs/016-omg-payment` 中的旧 Controller、旧流水、旧退款和旧前端文档已经退役,不得作为接入依据。
|
|
|
+
|
|
|
+## 1. 接入结论
|
|
|
+
|
|
|
+OMG 是网页托管式收银台,App 不需要接入 OMG SDK。前端只需要完成以下流程:
|
|
|
+
|
|
|
+1. 创建订单时把 OMG 支付方式设置为 `payType = "2"`。
|
|
|
+2. 调用 `POST /pay/omg/create` 获取 `gatewayUrl` 和完整的 `formFields`。
|
|
|
+3. 在当前 App WebView 页面中,把所有 `formFields` 原样以 HTML Form POST 到 `gatewayUrl`。
|
|
|
+4. 用户付款后,OMG 将浏览器 POST 到后端 `/pay/omg/result`;后端验证返回资料后,通过 App Scheme 打开支付结果页。
|
|
|
+5. App 结果页取得 `ddId`,调用 `POST /pay/omg/query` 确认最终支付状态。
|
|
|
+6. 只有 query 返回 `status = "PAID"` 才能展示支付成功。
|
|
|
+
|
|
|
+当前测试环境不支持退款。`POST /pay/omg/refund` 只会返回“测试环境不支持退款”,不会发起真实退款,也不会修改订单。
|
|
|
+
|
|
|
+## 2. 整体流程
|
|
|
+
|
|
|
+```text
|
|
|
+[App 创建订单,payType="2"]
|
|
|
+ |
|
|
|
+ | POST /pay/omg/create
|
|
|
+ | Header: token
|
|
|
+ | Body: {"orderId":"..."}
|
|
|
+ v
|
|
|
+[后端返回 gatewayUrl + formFields]
|
|
|
+ |
|
|
|
+ | 当前 WebView 原样 Form POST
|
|
|
+ v
|
|
|
+[OMG 测试收银台:信用卡 / Apple Pay]
|
|
|
+ |
|
|
|
+ +-------------------------------+
|
|
|
+ | |
|
|
|
+ | 服务端付款通知 | 浏览器付款结果返回
|
|
|
+ | POST /pay/omg/notify | POST /pay/omg/result
|
|
|
+ | 前端不调用 | 前端不直接调用
|
|
|
+ v v
|
|
|
+[后端验签并更新付款事实] [后端安全桥接页打开 App Scheme]
|
|
|
+ |
|
|
|
+ | com.twanmsdyh.app://pages/...
|
|
|
+ | ?ddId=<业务订单号>
|
|
|
+ v
|
|
|
+ [App 支付结果页]
|
|
|
+ |
|
|
|
+ | POST /pay/omg/query
|
|
|
+ v
|
|
|
+ [PAID 才展示支付成功]
|
|
|
+```
|
|
|
+
|
|
|
+必须注意:
|
|
|
+
|
|
|
+- `create.status = "CREATED"` 只表示后端已创建本地支付尝试和签名表单,不表示 OMG 已收单,更不表示已付款。
|
|
|
+- App Scheme 成功打开只表示浏览器返回资料通过后端校验,不表示服务端付款通知已经处理完成。
|
|
|
+- 订单最终支付结果必须以 `/pay/omg/query` 返回的 `data.status` 为准。
|
|
|
+- 当前新回调只更新支付事实,不推进订单、配送状态,也不发送 App 推送;App 不得依赖推送判断付款结果。
|
|
|
+
|
|
|
+## 3. 公共约定
|
|
|
+
|
|
|
+### 3.1 Base URL
|
|
|
+
|
|
|
+以下接口路径都拼接在 App 当前使用的后端 Base URL 后,例如:
|
|
|
+
|
|
|
+```text
|
|
|
+https://foodieapi.waimai-paotui.com/pay/omg/create
|
|
|
+```
|
|
|
+
|
|
|
+### 3.2 登录鉴权
|
|
|
+
|
|
|
+App 主动调用的 create、query 和 refund 接口都必须携带登录 token:
|
|
|
+
|
|
|
+```http
|
|
|
+token: <用户登录 token>
|
|
|
+```
|
|
|
+
|
|
|
+token 只发送给本项目后端,绝对不能放入 OMG Form,也不能发送给 `gatewayUrl`。
|
|
|
+
|
|
|
+### 3.3 AjaxResult 外层结构
|
|
|
+
|
|
|
+成功响应:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 200,
|
|
|
+ "msg": "操作成功",
|
|
|
+ "data": {}
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+业务失败响应:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 500,
|
|
|
+ "msg": "已国际化的错误提示",
|
|
|
+ "data": {
|
|
|
+ "status": "稳定的业务状态码"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+处理规则:
|
|
|
+
|
|
|
+- 先判断外层 `code`。
|
|
|
+- `code === 200` 时再读取 `data`。
|
|
|
+- `code !== 200` 时直接向用户展示后端返回的 `msg`;`msg` 已按项目语言国际化。
|
|
|
+- 需要按场景分支时使用 `data.status`,不要匹配 `msg` 文本。
|
|
|
+
|
|
|
+### 3.4 订单号字段
|
|
|
+
|
|
|
+接口请求统一使用:
|
|
|
+
|
|
|
+```json
|
|
|
+{"orderId":"业务订单号"}
|
|
|
+```
|
|
|
+
|
|
|
+后端 App Scheme 回跳参数使用:
|
|
|
+
|
|
|
+```text
|
|
|
+?ddId=<业务订单号>
|
|
|
+```
|
|
|
+
|
|
|
+App 收到 `ddId` 后,把它作为 `orderId` 调用 query。不要把 create/query 的 JSON 字段写成 `ddId`、`orderid` 或其他名称。
|
|
|
+
|
|
|
+## 4. 创建支付
|
|
|
+
|
|
|
+### 4.1 接口
|
|
|
+
|
|
|
+```http
|
|
|
+POST /pay/omg/create
|
|
|
+Content-Type: application/json
|
|
|
+token: <用户登录 token>
|
|
|
+
|
|
|
+{
|
|
|
+ "orderId": "991786433092835"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+前置条件:
|
|
|
+
|
|
|
+- 订单属于当前登录用户。
|
|
|
+- 必须是单门店父订单;多门店子订单不支持。
|
|
|
+- 订单 `payType` 必须为字符串 `"2"`。
|
|
|
+- 订单未付款、金额大于 0,且当前订单状态允许付款。
|
|
|
+- 订单门店已启用有效 OMG 凭证。
|
|
|
+- 同一订单当前不能存在另一个未结束的 OMG 支付尝试。
|
|
|
+
|
|
|
+客户端只能传 `orderId`。金额、商户号、回调地址、网关地址、付款渠道和签名都由后端决定。
|
|
|
+
|
|
|
+### 4.2 成功响应
|
|
|
+
|
|
|
+当前测试环境返回示例:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "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 位十六进制签名>"
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+字段说明:
|
|
|
+
|
|
|
+| 字段 | 类型 | App 用法 |
|
|
|
+|---|---|---|
|
|
|
+| `status` | string | 固定为 `CREATED`;不能当作支付成功 |
|
|
|
+| `gatewayUrl` | string | HTML Form 的 `action`,本身不是 Form 字段 |
|
|
|
+| `formFields` | object | 所有键值都必须原样创建为隐藏 input 并 POST |
|
|
|
+| `MerchantTradeNo` | string | 后端生成的 OMG 交易编号;前端只透传,不作为业务订单号 |
|
|
|
+| `TotalAmount` | string | 后端根据订单金额生成;前端不得修改 |
|
|
|
+| `ChoosePayment` | string | 当前固定 `Credit`,包含信用卡与 Apple Pay |
|
|
|
+| `UnionPay` | string | 当前固定 `2`,隐藏银联选项 |
|
|
|
+| `ReturnURL` | string | OMG 服务端付款通知地址;前端不调用 |
|
|
|
+| `OrderResultURL` | string | OMG 浏览器付款结果地址;前端不直接调用 |
|
|
|
+| `CheckMacValue` | string | 表单签名;修改任意已签名字段都会导致 OMG 拒绝 |
|
|
|
+
|
|
|
+后端当前实际返回的字段集合以 `formFields` 为准。前端不要维护字段白名单,不要自行新增、删除、改名、格式化或重新计算字段。
|
|
|
+
|
|
|
+### 4.3 Form POST 要求
|
|
|
+
|
|
|
+必须遵守:
|
|
|
+
|
|
|
+- 使用 `POST`,不能把字段拼成 GET URL。
|
|
|
+- `Content-Type` 由浏览器 Form 提交为 `application/x-www-form-urlencoded`。
|
|
|
+- 在当前完整 WebView 页面中跳转,不使用 iframe,不使用 `window.open` 新窗口。
|
|
|
+- `gatewayUrl` 只作为 Form `action`,不能创建名为 `gatewayUrl` 的 input。
|
|
|
+- `formFields` 中的每个键值都创建为隐藏 input,并保持值完全不变。
|
|
|
+- 不把 `formFields`、`CheckMacValue` 或 token 写入业务日志、埋点、崩溃上报或远程调试日志。
|
|
|
+
|
|
|
+浏览器侧核心代码:
|
|
|
+
|
|
|
+```js
|
|
|
+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();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 4.4 uni-app 页面传值建议
|
|
|
+
|
|
|
+建议通过页面 `eventChannel`、内存状态或原生桥接把 create 响应交给收银台页面,再由收银台页面的 renderjs 创建并提交 Form。
|
|
|
+
|
|
|
+不要把整个 `formFields` 拼入页面 URL query。这样会把 `CheckMacValue` 和完整支付表单暴露在浏览记录、路由日志或错误上报中。
|
|
|
+
|
|
|
+发起页示例:
|
|
|
+
|
|
|
+```js
|
|
|
+async function startOmgPayment(orderId) {
|
|
|
+ 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 }
|
|
|
+ });
|
|
|
+
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+收银台页面只需要接收:
|
|
|
+
|
|
|
+```js
|
|
|
+{
|
|
|
+ status: 'CREATED',
|
|
|
+ gatewayUrl: 'https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5',
|
|
|
+ formFields: { /* 后端原样返回的全部字段 */ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+然后在该页面的 WebView/renderjs 环境调用:
|
|
|
+
|
|
|
+```js
|
|
|
+submitOmgForm(payload.gatewayUrl, payload.formFields);
|
|
|
+```
|
|
|
+
|
|
|
+具体页面路径和请求封装按 App 现有结构调整,Form POST 规则不能改变。
|
|
|
+
|
|
|
+### 4.5 创建错误状态
|
|
|
+
|
|
|
+| `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"` | 检查创建订单时的支付方式 |
|
|
|
+| `STORE_CREDENTIAL_UNAVAILABLE` | 门店未启用有效 OMG 凭证 | 展示后端 `msg` |
|
|
|
+| `PAYMENT_ATTEMPT_EXISTS` | 已有未结束的支付尝试 | 不得再次创建;转到结果确认流程调用 query |
|
|
|
+| `PAYMENT_CONFIGURATION_INVALID` | 后端测试网关或回调配置不合法 | 展示后端 `msg`,通知后端排查 |
|
|
|
+| `PAYMENT_CREATION_FAILED` | 本次创建失败 | 展示后端 `msg`,稍后重试 |
|
|
|
+
|
|
|
+同一订单创建成功后,重复点击不会返回旧表单,而是返回 `PAYMENT_ATTEMPT_EXISTS`。因此支付按钮必须防重复点击;收到该状态时调用 query 确认已有尝试,不要循环调用 create。
|
|
|
+
|
|
|
+## 5. 浏览器结果返回与 App Scheme
|
|
|
+
|
|
|
+### 5.1 `/pay/omg/result`
|
|
|
+
|
|
|
+```http
|
|
|
+POST /pay/omg/result
|
|
|
+Content-Type: application/x-www-form-urlencoded
|
|
|
+```
|
|
|
+
|
|
|
+该接口由 OMG 收银台调用,不是 App API。后端会验证返回表单的签名、商户号、OMG 交易编号和金额,然后返回一个禁止缓存的安全桥接页面。
|
|
|
+
|
|
|
+桥接页面自动打开:
|
|
|
+
|
|
|
+```text
|
|
|
+com.twanmsdyh.app://pages/OrderList/paySuccess/paySuccess?ddId=<URL 编码后的业务订单号>
|
|
|
+```
|
|
|
+
|
|
|
+页面同时保留“返回 App”按钮,自动唤起失败时用户可手动点击。
|
|
|
+
|
|
|
+### 5.2 App 必须完成的配置
|
|
|
+
|
|
|
+App 必须注册以下 Scheme:
|
|
|
+
|
|
|
+```text
|
|
|
+com.twanmsdyh.app
|
|
|
+```
|
|
|
+
|
|
|
+并把以下路由映射到现有支付结果页:
|
|
|
+
|
|
|
+```text
|
|
|
+pages/OrderList/paySuccess/paySuccess
|
|
|
+```
|
|
|
+
|
|
|
+结果页读取参数:
|
|
|
+
|
|
|
+```text
|
|
|
+ddId=<业务订单号>
|
|
|
+```
|
|
|
+
|
|
|
+收到 Scheme 后的正确处理:
|
|
|
+
|
|
|
+1. 读取 `ddId`。
|
|
|
+2. 从 App 本地安全存储读取当前登录 token。
|
|
|
+3. 调用 `/pay/omg/query`,请求体使用 `{ "orderId": ddId }`。
|
|
|
+4. 根据 query 的 `data.status` 展示结果。
|
|
|
+
|
|
|
+Scheme 参数可以被外部伪造。禁止因为进入支付结果路由或拿到 `ddId` 就直接展示付款成功。
|
|
|
+
|
|
|
+## 6. 查询支付结果
|
|
|
+
|
|
|
+### 6.1 接口
|
|
|
+
|
|
|
+```http
|
|
|
+POST /pay/omg/query
|
|
|
+Content-Type: application/json
|
|
|
+token: <用户登录 token>
|
|
|
+
|
|
|
+{
|
|
|
+ "orderId": "991786433092835"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+query 不是简单读取本地状态。后端会使用当前支付尝试的凭证快照查询 OMG 测试网关、验证完整响应,并在付款回调丢失时补偿订单付款状态。因此 App 不应高频调用。
|
|
|
+
|
|
|
+### 6.2 成功响应
|
|
|
+
|
|
|
+已付款示例:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "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 实际付款方式 |
|
|
|
+| `handlingCharge` | string/null | 手续费相关原始值 |
|
|
|
+| `paymentTypeChargeFee` | string/null | 付款方式手续费原始值 |
|
|
|
+
|
|
|
+### 6.3 `status` 处理
|
|
|
+
|
|
|
+| `status` | 含义 | App 行为 |
|
|
|
+|---|---|---|
|
|
|
+| `PAID` | 已确认付款 | 展示支付成功,刷新订单 |
|
|
|
+| `UNPAID` | OMG 当前仍未付款 | 保持“结果确认中”,按受控频率继续查询 |
|
|
|
+| `FAILED` | 当前支付尝试已失败 | 展示支付未完成,停止本轮轮询 |
|
|
|
+| `UNKNOWN` | OMG 返回了当前系统未归一化的状态 | 不得当作成功;提示确认中并受控重试 |
|
|
|
+
|
|
|
+OMG 原始 `tradeStatus` 常见值:
|
|
|
+
|
|
|
+| `tradeStatus` | 标准化结果 |
|
|
|
+|---|---|
|
|
|
+| `1` | `PAID` |
|
|
|
+| `0` | `UNPAID` |
|
|
|
+| `10200095` | `FAILED` |
|
|
|
+| 其他 | `UNKNOWN` |
|
|
|
+
|
|
|
+### 6.4 轮询建议
|
|
|
+
|
|
|
+App 从 Scheme 返回后立即查询一次。如果结果是 `UNPAID` 或 `UNKNOWN`:
|
|
|
+
|
|
|
+- 每 3 秒查询一次。
|
|
|
+- 最多再查询 10 次,总计约 30 秒。
|
|
|
+- App 页面进入后台或销毁时停止轮询。
|
|
|
+- 任意一次返回 `PAID` 或 `FAILED` 时立即停止。
|
|
|
+- query 返回业务错误时停止自动轮询并展示后端 `msg`。
|
|
|
+- 达到上限仍未终态时提示“支付结果确认中,请稍后在订单列表查看”,不要展示支付失败。
|
|
|
+
|
|
|
+参考代码:
|
|
|
+
|
|
|
+```js
|
|
|
+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();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+实际代码需要结合页面生命周期取消定时器或异步任务,避免离开页面后继续请求。
|
|
|
+
|
|
|
+### 6.5 查询错误状态
|
|
|
+
|
|
|
+| `data.status` | 含义 | App 处理建议 |
|
|
|
+|---|---|---|
|
|
|
+| `AUTH_REQUIRED` | 登录信息不可用 | 返回登录页 |
|
|
|
+| `ORDER_REQUIRED` | `orderId` 缺失或不合法 | 停止查询并返回订单页 |
|
|
|
+| `ORDER_NOT_AVAILABLE` | 订单不存在或不属于当前用户 | 展示后端 `msg` |
|
|
|
+| `PAYMENT_QUERY_NOT_AVAILABLE` | 当前没有可查询的 OMG 支付尝试 | 刷新订单后展示后端 `msg` |
|
|
|
+| `PAYMENT_QUERY_FAILED` | OMG 查询失败、响应非法或身份校验失败 | 展示后端 `msg`,稍后人工重试 |
|
|
|
+
|
|
|
+## 7. 后端专用接口
|
|
|
+
|
|
|
+### 7.1 付款通知 `/pay/omg/notify`
|
|
|
+
|
|
|
+```http
|
|
|
+POST /pay/omg/notify
|
|
|
+Content-Type: application/x-www-form-urlencoded
|
|
|
+```
|
|
|
+
|
|
|
+这是 OMG 到后端的 server-to-server 回调:
|
|
|
+
|
|
|
+- App 不调用。
|
|
|
+- 不携带用户 token。
|
|
|
+- 后端负责验签、幂等处理和更新付款事实。
|
|
|
+- App 不需要解析其 `1|OK` 或 `0|ERROR` 响应。
|
|
|
+
|
|
|
+### 7.2 浏览器结果 `/pay/omg/result`
|
|
|
+
|
|
|
+这是 OMG 收银台到后端的浏览器返回接口:
|
|
|
+
|
|
|
+- App 不主动调用。
|
|
|
+- 后端只验证返回资料并生成 App Scheme 桥接页。
|
|
|
+- 该接口本身不修改支付状态。
|
|
|
+
|
|
|
+## 8. 退款接口:当前不可用
|
|
|
+
|
|
|
+### 8.1 接口形式
|
|
|
+
|
|
|
+```http
|
|
|
+POST /pay/omg/refund
|
|
|
+Content-Type: application/json
|
|
|
+token: <用户登录 token>
|
|
|
+
|
|
|
+{
|
|
|
+ "orderId": "991786433092835"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 8.2 当前固定响应
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 500,
|
|
|
+ "msg": "OMG 测试环境不支持退款,请在正式环境启用后操作",
|
|
|
+ "data": {
|
|
|
+ "status": "PAYMENT_REFUND_UNAVAILABLE_IN_TEST_ENVIRONMENT"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 8.3 必须理解的行为
|
|
|
+
|
|
|
+当前接口只是测试阶段的安全拒绝边界:
|
|
|
+
|
|
|
+- 不调用 OMG 正式环境退款接口。
|
|
|
+- 不读取或修改订单、支付、退款状态。
|
|
|
+- 不创建退款记录。
|
|
|
+- 不写入付款通知日志。
|
|
|
+- 不支持由 App 传退款金额、OMG 交易号、网关地址或退款动作。
|
|
|
+
|
|
|
+因此当前 App 不应把 `/pay/omg/refund` 接入订单取消流程,也不能向用户展示“退款成功”。正式环境退款需要后端另行实现并确认契约后,前端才能接入。
|
|
|
+
|
|
|
+## 9. 安全与易错点
|
|
|
+
|
|
|
+1. 创建支付请求字段是 `orderId`,不是 `ddId`;`ddId` 只出现在 App Scheme 参数中。
|
|
|
+2. 订单 OMG 支付类型是字符串 `"2"`;LINE Pay 是 `"3"`,不要混用。
|
|
|
+3. `CREATED` 不是支付成功。
|
|
|
+4. App Scheme 到达不是支付成功。
|
|
|
+5. 只有 query 的 `status === "PAID"` 才能展示支付成功。
|
|
|
+6. 不修改、过滤或重新计算 `formFields`;尤其不能修改 `TotalAmount`、回调地址和 `CheckMacValue`。
|
|
|
+7. 不把 Form 改成 GET,不使用 iframe,不使用新窗口。
|
|
|
+8. 不把 token 发送给 OMG,也不把 token 放入页面 URL。
|
|
|
+9. 不把完整 `formFields` 或 `CheckMacValue` 放入 URL、日志、埋点或错误上报。
|
|
|
+10. `tradeNo`、`merchantTradeNo` 始终按字符串处理,不转换为 JavaScript Number。
|
|
|
+11. query 会访问 OMG 网关,不做高频轮询。
|
|
|
+12. `PAYMENT_ATTEMPT_EXISTS` 不能靠重复调用 create 解决,应进入 query 确认流程。
|
|
|
+13. 当前测试收银台只开放信用卡与 Apple Pay,不实现 ATM、CVS、BarcodeATM、银联、延期付款或取号页面。
|
|
|
+14. 当前没有真实退款能力。
|
|
|
+
|
|
|
+## 10. App 验收清单
|
|
|
+
|
|
|
+### 10.1 创建与收银台
|
|
|
+
|
|
|
+- [ ] 创建订单时 OMG 使用 `payType = "2"`。
|
|
|
+- [ ] create 请求 Header 包含 token,JSON 只传 `orderId`。
|
|
|
+- [ ] 支付按钮有防重复点击处理。
|
|
|
+- [ ] create 成功后,当前完整 WebView 使用 Form POST 打开 `gatewayUrl`。
|
|
|
+- [ ] 所有 `formFields` 原样提交,`gatewayUrl` 不作为 input。
|
|
|
+- [ ] 没有 iframe、GET 跳转或新窗口。
|
|
|
+- [ ] 测试收银台显示正确金额,并只提供信用卡/Apple Pay 范围。
|
|
|
+
|
|
|
+### 10.2 App 返回与状态确认
|
|
|
+
|
|
|
+- [ ] App 已注册 `com.twanmsdyh.app` Scheme。
|
|
|
+- [ ] Scheme 路由可打开 `pages/OrderList/paySuccess/paySuccess`。
|
|
|
+- [ ] 结果页能读取 `ddId`。
|
|
|
+- [ ] 结果页把 `ddId` 作为 `orderId` 调用 query。
|
|
|
+- [ ] `PAID` 才展示成功。
|
|
|
+- [ ] `UNPAID`/`UNKNOWN` 受控轮询,不误判为失败或成功。
|
|
|
+- [ ] `FAILED` 停止本轮轮询并展示未完成。
|
|
|
+- [ ] 轮询超时展示“确认中”,并引导用户稍后查看订单。
|
|
|
+- [ ] 页面离开或进入后台时停止轮询。
|
|
|
+
|
|
|
+### 10.3 错误与安全
|
|
|
+
|
|
|
+- [ ] 业务错误直接显示后端国际化 `msg`。
|
|
|
+- [ ] 需要分支判断时使用 `data.status`,不匹配中文文案。
|
|
|
+- [ ] `PAYMENT_ATTEMPT_EXISTS` 不会触发 create 重试循环。
|
|
|
+- [ ] token、完整 Form 和 `CheckMacValue` 不出现在日志、URL、埋点或上报中。
|
|
|
+- [ ] 当前订单取消流程没有调用 OMG refund 并宣称退款成功。
|
|
|
+- [ ] 所有 App 新增用户可见文字已加入 zh、tw、en、vi 四种语言。
|
|
|
+
|
|
|
+## 11. 接口速查
|
|
|
+
|
|
|
+| 用途 | Method | 路径 | token | 调用方 |
|
|
|
+|---|---|---|---|---|
|
|
|
+| 创建支付 | POST | `/pay/omg/create` | 必须 | App |
|
|
|
+| 查询结果 | POST | `/pay/omg/query` | 必须 | App |
|
|
|
+| 退款安全拒绝 | POST | `/pay/omg/refund` | 必须 | 当前 App 不调用 |
|
|
|
+| 付款结果通知 | POST | `/pay/omg/notify` | 不需要 | OMG 服务器 |
|
|
|
+| 浏览器结果返回 | POST | `/pay/omg/result` | 不需要 | OMG 收银台 |
|
|
|
+
|
|
|
+## 12. 当前后端配置依赖
|
|
|
+
|
|
|
+后端当前配置为:
|
|
|
+
|
|
|
+```yaml
|
|
|
+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 的配置,前端不得覆盖。
|