# 收银页对接与终端能力矩阵

> docId：`XD-OP-06` ｜ version：`1.0.0` ｜ status：`PUBLISHED`

本篇回答一个问题：**把收款材料交给买家之后，各终端上会发生什么**。终端不同、通道不同，买家看到的行为也不同——照着矩阵做，不要自己发明分支。

## 1. 终端能力矩阵（四端 × 两通道）

| 终端 | 支付宝 | 微信 |
| --- | --- | --- |
| **PC 浏览器** | 渲染官方收款码，**页内扫码**，不跳转 | 渲染**平台收银页地址**的二维码；买家扫码后进入手机微信内完成支付（中转链） |
| **手机 · 微信外浏览器** | 渲染官方收款码 + **手势主按钮「打开支付宝」**（点击内顶层打开官方收款码）；按钮下就地给兜底：**长按二维码识别**，识别不了就**截屏 → 支付宝扫一扫 → 相册** | **推荐路径**：一键「复制链接并打开微信」+ 三步说明；本页**没有**可切换的支付宝入口 |
| **手机 · 微信内** | **不自动跳转**（微信会拦截外部唤起）；给一句「复制链接到系统浏览器打开」的摘要引导（非逐字文案） | **零点击**：进入即自动完成授权与**支付面板调起** |
| **商家自有 App / 宿主内** | 必须先**跳系统浏览器**，落地后按「手机·微信外」分支给手势按钮 | 必须先**跳系统浏览器**，落地后按「手机·微信外」走推荐路径 |

> **通道放行由平台整体控制（不区分应用，也没有自助查询面），粒度是「通道 × 交易形态」。** **微信 JSAPI 已开放**：买家在微信内打开收银页即可调起微信支付（仅限微信内浏览器）；微信外手机浏览器与商家 App 内没有「直接拉起」形态。上面这张矩阵描述的是**能力面**（两个通道的终端行为都已按此实现）；其他通道或交易形态尚未放行时，用它下单会得到 `422 OPEN_API_CHANNEL_UNAVAILABLE`——这是**可预期的正常拒绝**，不要按故障处理、也不要重试刷量。**接入前与对接人确认你要用的通道与交易形态是否已放行**，不要靠试探下单去确认。
>
> **桌面微信内置浏览器也命中「微信内」分支**（平台按 UA 判定，不为桌面微信单独分派形态）：链接若在 PC 微信里被打开，页面同样走微信内的授权/调起链。若买家在 PC 微信里完不成支付，让其**用手机微信打开同一条链接**即可——不要为此在自有容器里另做一套。

**结构性说明（不是实现细节，是这套设计的原因）**：平台侧**没有**「微信正扫」形态——微信收款只有「在微信内打开页面后由页面调起」这一条路，所以微信的「PC 扫码」本质是**中转链**：二维码里放的是平台收银页地址，扫码后落到手机微信内页面，再由页面调起支付面板。请按这个前提设计你的 UX 文案，不要向买家承诺「扫码直接出付款码」。

### 商家 App / 宿主内的硬约束（违反即不可用）

- ✅ 用系统浏览器打开收银链（等价「跳转外部浏览器」语义）。
- ❌ **禁止**在你自己的 App 的 WebView / 内嵌浏览器里打开收银页——容器 UA 与浏览器特性失真会导致终端判定错误，买家会落到拉不起来的页面。
- ❌ 商家侧**没有**「App 内直接调起微信支付」的形态；App 内的微信支付同样走推荐路径。

## 2. 用法一：自渲染官方收款码（商家 PC 页面）

下单响应里的 `qrCode` 就是**官方收款码**，可直接在**你自己的 PC 页面**渲染成二维码，与平台收银页同形。

```text
取响应 qrCode
 ├─ 为空（null） → 该单不下发收款码（渠道不符 / 非官方码 / 无有效尝试）：改用 payUrl 中转链
 └─ 非空 → 渲染前做一次前缀防呆复检（只接受官方收款码前缀）
            └─ 通过 → 渲染二维码 → 买家扫码 → 轮询你自己的服务端拿终态
            └─ 不通过 → 不渲染、不报错、不打印内容，回退 payUrl 中转链
```

红线：

- 仅**支付宝渠道**、仅**官方前缀**才会下发；前端不得拼装、改写、代理转发该地址。
- **不得**把 `qrCode` 写进日志、埋点、门户管理面或事件通知。
- **只在下发响应有效**：查询接口恒为 `null`；需要新码走**重拉起**端点（新尝试=新码）。过期旧码**不可复用**。

## 3. 用法二：`payUrl` 中转链（两种标准用法）

`payUrl` 是平台收银页地址，两种用法都标准：

| 用法 | 怎么做 | 终端语义 |
| --- | --- | --- |
| 渲染成二维码 | 把 `payUrl` 渲染成二维码放在你的 PC 页面 | 被扫方=PC，扫码方=手机 → 手机进入收银页 → 按矩阵分端 |
| `302` 跳转 | 把买家浏览器 `302` 到 `payUrl` | 买家的浏览器直接进入收银页 → 按矩阵分端 |

要点：

- 用 `payUrl` **原值**，不要拼接参数、不要加自己的 query。
- 「扫码 → 平台收银页 → 自动拉起」这两步依赖平台收银页的终端分派能力（见矩阵），你的页面**不需要**判断终端。
- 买家在收银页点「**返回商家**」才会回跳你冻结的 `returnUrl`（**不是自动跳转**）。按钮由**收银页本次会话首次观察到支付成功**时提供，回跳凭证**一次性**：刷新过页面，或此前已点击过一次（含浏览器回退后再点）时，**不再提供或不再可用**。凭证有效期 = 支付窗口（**默认 600 秒，平台侧可调**）+ 终态展示宽限 600 秒；超时后点击会看到一个**中性提示页**（不是报错 JSON，也不会泄露订单信息）。**扫码支付（PC 出码 → 手机付款）不受此限**：只要收银页保持打开并轮询到支付成功，按钮照常提供并可回跳。它只是导航，**不是终态**——见下一节。
- **微信内（JSAPI）支付完成后**：微信会接管完成页（收银页随之关闭），买家不经过收银页的「返回商家」按钮；买家可从**微信支付记录中的「商家小票」卡片**点「返回商家」回到你冻结的 `returnUrl`——该通道由平台侧配置、无需商家开通，落点与收银页按钮是**同一个** `returnUrl`；同样是**买家点击的导航**：不是自动跳转，也不是终态。因此微信内流程请你把 `returnUrl` 落地页按「展示 + 轮询自己的服务端」设计。

## 4. 手机微信外：推荐路径（逐字引导文案）

手机浏览器（微信外）打开一笔**微信支付**单时，平台收银页会给出下面这组引导。商家在自己的页面/文案里若需要复述，请**逐字**使用（这是一份被两侧共同引用的文本源）：

| 键 | 逐字文本 |
| --- | --- |
| 标题 | `请在微信中打开完成支付` |
| 说明 | `微信支付需要在微信内打开本链接；点击下方按钮复制链接并打开微信；链接发给自己即可。` |
| 主按钮 | `复制链接并打开微信` |
| 步骤 ① | `打开微信` |
| 步骤 ② | `把链接粘贴发送给「文件传输助手」` |
| 步骤 ③ | `点开链接即可进入支付` |
| 复制成功反馈 | `已复制链接，正在打开微信…` |
| 复制失败反馈 | `未能自动复制，请长按下方链接手动复制` |
| 手动复制框可访问名 | `支付链接` |
| 取不到链接时 | `支付链接获取失败，请返回商家页面重新打开` |
| 如实出口（无切换面） | `如需使用支付宝支付，请联系商家重新选择支付方式。` |

**不要**向买家承诺「点一下就到支付面板」：微信不允许第三方应用代发消息，第 ②③ 步必须由买家在微信内完成。平台能做的是把「复制链接 + 唤起微信」合成一步。

## 4b. 手机微信外：支付宝单的手势引导（逐字引导文案）

手机浏览器（微信外）打开一笔**支付宝单**时，平台收银页渲染官方收款码，并给一个**手势主按钮**。同样请**逐字**使用（同一份文本源）：

| 键 | 逐字文本 |
| --- | --- |
| 打开支付宝主按钮 | `打开支付宝` |
| 按钮下就地说明 | `没打开？长按二维码识别；识别不了就截屏保存，打开支付宝 → 扫一扫 → 相册 → 选刚截的图` |

**为什么不是「进入即自动打开」**：浏览器只把**用户手势触发的那次打开**交给支付宝 App；无手势的自动跳转会被「自动拉起应用/自动跳转」策略吃掉（真机实测：两个主流手机浏览器均连导航都不发生）。**另外**：平台收银页里的二维码是以 canvas 绘制的，部分浏览器/微信内**长按识别可能无效**——因此**截屏 → 支付宝扫一扫 → 相册**才是各端都成立的通用兜底。也别用「复制支付链接到支付宝打开」——支付宝没有「粘贴链接付款」入口，那是死路。

## 5. 终态从哪来

无论走哪条路径，**收银页打开 / 回跳 / 用户点完成都不是终态**。商家必须：

1. 接事件通知（[webhook 验签](./05-webhook-verification.md)）作为主路径；
2. 接查单接口作为补偿与对账路径；
3. `returnUrl` 落地页只做展示与轮询自己的服务端。

## 6. 本地验证矩阵

`demo/h5-cashier/` 内置终端预览页（可手动切换 PC / 手机微信外 / 手机微信内三种形态）与可运行示例（**demo 只在公开仓提供，不在门户的两个下载包内**）：

```bash
node demo/h5-cashier/server.js
```

<!-- xd-terminal-matrix-mirror -->
```json
{
  "channels": [
    "ALIPAY_H5",
    "WECHAT_H5"
  ],
  "terminals": [
    "pc",
    "mobileOutsideWechat",
    "mobileInsideWechat",
    "merchantHost"
  ]
}
```
<!-- /xd-terminal-matrix-mirror -->
