# 新 OMG 支付 App 对接文档

**适用对象**：用户端 App（uni-app）前端开发人员
**接口版本**：`specs/020-omg-payment-rebuild` 新实现
**当前环境**：OMG 测试环境
**支付方式目标**：App 只提供信用卡、Apple Pay，OMG 直接进入所选渠道
**更新时间**：2026-08-18

> 本文只描述当前新 OMG 实现。`specs/016-omg-payment` 中的旧 Controller、旧流水、旧退款和旧前端文档已经退役，不得作为接入依据。

## 1. 接入结论

OMG 是网页托管式收银台，App 不需要接入 OMG SDK。前端只需要完成以下流程：

1. 创建订单时把 OMG 支付方式设置为 `payType = "2"`。
2. 在 App 自有界面让用户选择信用卡或 Apple Pay，分别使用 `paymentMethod = "CREDIT"` 或 `"APPLE_PAY"`。
3. 调用 `POST /pay/omg/create` 获取所选渠道的 `gatewayUrl` 和完整 `formFields`。
4. 在当前 App WebView 页面中，把所有 `formFields` 原样以 HTML Form POST 到 `gatewayUrl`。
5. 用户付款后，OMG 将浏览器 POST 到后端 `/pay/omg/result`；后端验证返回资料后，优先通过 uni-app Bridge 打开支付结果页，Bridge 不可用时再通过 App Scheme 兜底。
6. App 结果页取得 `ddId`，调用 `POST /pay/omg/query` 确认最终支付状态。
7. 只有 query 返回 `status = "PAID"` 才能展示支付成功。

App 需要新增“信用卡 / Apple Pay”选择，但不接入原生 Apple Pay SDK。后端只接受两个稳定枚举并生成单渠道签名表单，因此 OMG 页面不会再展示超商快付、AFTEE 或其他渠道选择项。用户可见文案必须接入 App 现有 i18n。

当前测试环境不支持退款。`POST /pay/omg/refund` 只会返回“测试环境不支持退款”，不会发起真实退款，也不会修改订单。

## 2. 整体流程

```text
[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 已收单，更不表示已付款。
- 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 订单号字段

create/retry 请求使用：

```json
{"orderId":"业务订单号","paymentMethod":"CREDIT 或 APPLE_PAY"}
```

query/refund 请求仍使用 `{"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",
  "paymentMethod": "CREDIT"
}
```

前置条件：

- 订单属于当前登录用户。
- 必须是单门店父订单；多门店子订单不支持。
- 订单 `payType` 必须为字符串 `"2"`。
- 订单未付款、金额大于 0，且当前订单状态允许付款。
- 订单门店已启用有效 OMG 凭证。
- 同一订单当前不能存在另一个未结束的 OMG 支付尝试。

create/retry 只能传 `orderId` 与 `paymentMethod`；`paymentMethod` 仅允许 `CREDIT/APPLE_PAY`。query/refund 仍只传 `orderId`。金额、商户号、回调地址、网关地址、OMG 原始付款参数和签名都由后端决定。

### 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 位十六进制签名>"
    }
  }
}
```

上例为信用卡响应。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` 为准。前端不要维护字段白名单，不要自行新增、删除、改名、格式化或重新计算字段。

### 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, 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;
  }
}
```

收银台页面只需要接收：

```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"` | 检查创建订单时的支付方式 |
| `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，也不要缓存并重新提交旧表单。

## 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 编码后的业务订单号>
```

在 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 注册作为兜底。

### 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 尚未确认付款渠道时为空；完成渠道选择/付款并在 query 或回调中回传后才有值。信用卡与 Apple Pay 可能同样回传 `Credit_CreditCard`，App 不用它做二级支付方式选择 |
| `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`，稍后人工重试 |

## 6.6 退出支付页后重新支付

App 返回业务页面后原支付 WebView 会销毁，本流程不要求保存或恢复 WebView。旧 `gatewayUrl + formFields` 也不能当作“继续付款网址”缓存重放：其中的 `MerchantTradeNo` 只能建单一次，重新提交会被 OMG 以重复订单编号拒绝。

用户点击“重新支付”时调用：

```http
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`：

```json
{
  "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 推荐处理：

```js
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` | 提示稍后重试，不复用旧表单 |

## 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 路由桥接页；iOS 优先由父 uni-app 页面执行路由，其他环境使用官方 Bridge，失败时才使用 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. App 只传 `CREDIT/APPLE_PAY`；不得传 `ChoosePayment`、`UnionPay`、`IgnorePayment` 等 OMG 原始字段，也不得通过 CSS 或 DOM 修改第三方支付页面。
14. 当前没有真实退款能力。

## 10. App 验收清单

### 10.1 创建与收银台

- [ ] 创建订单时 OMG 使用 `payType = "2"`。
- [ ] App 只显示信用卡与 Apple Pay，并把选择映射为 `CREDIT/APPLE_PAY`；用户可见文案使用现有 i18n。
- [ ] create 请求 Header 包含 token，JSON 只传 `orderId` 与 `paymentMethod`。
- [ ] 支付按钮有防重复点击处理。
- [ ] create 成功后，当前完整 WebView 使用 Form POST 打开 `gatewayUrl`。
- [ ] 所有 `formFields` 原样提交，`gatewayUrl` 不作为 input。
- [ ] 没有 iframe、GET 跳转或新窗口。
- [ ] `CREDIT` 直接进入信用卡流程，表单为 `ChoosePayment=Credit + UnionPay=2`，不显示其他渠道。
- [ ] `APPLE_PAY` 在门店已开通且设备支持时直接进入 Apple Pay 流程，表单为 `ChoosePayment=ApplePay`，不显示其他渠道。
- [ ] 两种表单都不包含 `ChoosePayment=ALL` 或 `IgnorePayment`，页面不出现超商快付、AFTEE。

### 10.2 App 返回与状态确认

- [ ] App 已注册 `com.twanmsdyh.app` Scheme。
- [ ] Scheme 路由可打开 `pages/OrderList/paySuccess/paySuccess`。
- [ ] iOS 从 OMG 信用卡付款完成后可由支付 WebView 自动进入 `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/retry` | 必须 | 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 的配置，前端不得覆盖。
