Prechádzať zdrojové kódy

docs(聚合支付): 开放文档改写为最短接入路径

按「越简单越好对接」重写:§1 给 4 条硬规则与可直接复制的签名函数,
§2 给完整可运行的 requests 示例,其余接口只留最小请求体与状态表。
口径逐条对过代码:pay_data 只按 http/https 渲染、退款幂等比较订单+金额、
回调任意 2xx 判成功且最多 3 次、回调允许内网但拒绝 localhost。

8 个 H2 标题与 unifiedDoc.ts 的 parts 保持不变,切片器不受影响。
alphaH 1 týždeň pred
rodič
commit
cd578ff380

+ 190 - 226
frontend/src/views/module_payment/apikey/docs/unified-openapi.md

@@ -1,312 +1,276 @@
 ## 1. 概述
 
-「聚合支付」是平台接入的**另一条产品线**。它与「支付宝收单」**不是同一套接口**:端点路径、金额单位、状态枚举都不同,两份文档的字段不可混用、枚举值不可互相套用。
+聚合支付是平台接入的**另一条产品线**,用来收款和退款。它和「支付宝收单」是两套接口:地址、金额单位、状态值都不一样,不能混用。
 
-- **认证**:平台 **API Key**(请求头 `Authorization: ApiKey <key>` 与 `Signature`,按请求 JSON body 计算 HMAC-SHA256)。这**不是**上游的 MD5 签名 —— 上游签名由平台在服务端完成,调用方不需要、也不应该持有上游 `appSecret`。
-- **金额**:全链路金额的单位(unit)都是**分**(整型 int),1 元 = 100 分。不要按「元」传小数。
-- **报文体**:JSON,字段 snake_case。响应核心字段为 `{ code, msg, data }`,另含冗余的 `status_code`、`success`(调用方只需读 `code/msg/data`),`code=0` 表示成功。
-- **业务错误**:按平台约定返回 **HTTP 200 + body `code`**(参数校验为 HTTP 400、认证失败为 HTTP 401/403)。判断成功请以 `code == 0` 为准,不要只看 HTTP 状态。
-- **租户**:只从 API Key 取,请求体里没有 `tenant_id`。
-- **`mch_no`**:**统一下单**与**获取渠道用户 ID** 在租户配置了多条商户号时**必须**指定,否则返回 400「当前租户配置了多个商户号,请在请求中指定 mch_no」——平台不替调用方挑。订单查询、退款查询与发起退款**可不传**:两个查询接口不按 `mch_no` 过滤;发起退款仅在传入时做一致性校验,与订单快照不一致返回 404。
+接入只需要记住 4 条:
 
-端点一览:
+1. 地址:`https://qcsj88888.com/api/v1`。
+2. 每个请求都用平台 **API Key** 认证,带 3 个请求头:`Content-Type: application/json`、`Authorization: ApiKey <api_key>`、`Signature: <签名>`。
+3. 金额单位(unit)永远是**分**,传整数。`100` 就是 1.00 元。
+4. HTTP 200 不代表成功,**`code=0` 才是成功**。失败时看 `msg`。
 
-| # | 方法 | 路径 | 说明 |
-|---|---|---|---|
-| 1 | POST | `/payment/openapi/unified/order/create` | 统一下单 |
-| 2 | POST | `/payment/openapi/unified/order/query` | 订单查询 |
-| 3 | POST | `/payment/openapi/unified/refund` | 统一退款 |
-| 4 | POST | `/payment/openapi/unified/refund/query` | 退款查询 |
-| 5 | POST | `/payment/openapi/unified/channel-user/jump-url` | 获取渠道用户跳转 URL |
+**5 个接口**
 
----
-
-## 2. 统一下单
-
-```
-POST /payment/openapi/unified/order/create
+| 用途 | 接口 |
+|---|---|
+| 下单 | `POST /payment/openapi/unified/order/create` |
+| 查单 | `POST /payment/openapi/unified/order/query` |
+| 退款 | `POST /payment/openapi/unified/refund` |
+| 查退款 | `POST /payment/openapi/unified/refund/query` |
+| 获取渠道用户 ID(只有 JSAPI 需要) | `POST /payment/openapi/unified/channel-user/jump-url` |
+
+**签名**
+
+把下面这段复制到你的代码里,每次请求调用 `sign(请求体, api_secret)`:
+
+```python
+import hmac, hashlib, json
+from urllib.parse import quote
+
+def sign(body: dict, api_secret: str) -> str:
+    parts = []
+    for key in sorted(body):
+        value = body[key]
+        if value is None or value == "" or value == {} or value == [] or key == "sign":
+            continue
+        if isinstance(value, (dict, list)):
+            text = json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
+        elif isinstance(value, bool):
+            text = "true" if value else "false"
+        else:
+            text = str(value)
+        parts.append(f"{key}={quote(text, safe='')}")
+    raw = "&".join(parts)
+    return hmac.new(api_secret.encode(), raw.encode(), hashlib.sha256).hexdigest()
 ```
 
-**请求字段**
+签名只算请求体的顶层字段,和 URL、请求头无关。`api_secret` 只在本地使用,不要发给任何人,也不要写进前端代码。
 
-| 字段 | 类型 | 必填 | 说明 |
-|---|---|---|---|
-| `mch_no` | string | 否 | 租户配置了多个商户号时必填 |
-| `amount` | int | 是 | 金额,**单位分**,必须 > 0 |
-| `way_code` | string | 是 | 支付方式,见下方 16 种之一 |
-| `subject` | string | 是 | 商品标题,≤128 字符 |
-| `body` | string | 否 | 商品描述,≤512 字符;为空时平台用 `subject` 顶上 |
-| `out_trade_no` | string | 是 | 调用方订单号,≤64 字符,**幂等键**,缺失返回 400 |
-| `client_ip` | string | 否 | 发起支付的客户端 IP;不传取请求来源 IP |
-| `notify_url` | string | 否 | 本笔订单的下游回调地址,≤512 字符;传了才推这个地址 |
-| `return_url` | string | 否 | 上游同步跳转地址(浏览器),≤512 字符 |
-| `expired_time` | int | 否 | 过期时间(秒),默认 7200 |
-| `channel_extra` | object | 否 | 按 `way_code` 传入的额外参数,见下方规则。平台收到后序列化成紧凑 JSON 字符串再发上游 |
-| `ext_param` | string | 否 | 上游原样返回的透传参数 |
+**商户号 `mch_no`**
 
-> 分账(`division_mode`)不在本期范围:可传,平台忽略、不转发(按上游默认 0 处理)。
+租户只有一个商户号时可以不传;有多个商户号时,**下单**和**获取渠道用户 ID** 必须传。查单、退款、查退款可以不传。
 
-**16 种 `wayCode` 与 `channelExtra` 规则**
-
-| `wayCode` | 支付方式 | `channelExtra` 规则 |
-|---|---|---|
-| `QR_CASHIER` | 聚合扫码(用户扫商家) | 可选 `payDataType=codeUrl/codeImgUrl`(上游默认 `codeUrl`) |
-| `AUTO_BAR` | 聚合条码(商家扫用户) | 必传付款码 |
-| `ALI_BAR` | 支付宝条码 | 必传付款码 |
-| `ALI_JSAPI` | 支付宝生活号 | 必传 `buyerUserId` |
-| `ALI_APP` | 支付宝 APP | 无额外参数 |
-| `ALI_WAP` | 支付宝 WAP | 可选 `payDataType=form/codeImgUrl/payUrl`(上游默认 `payUrl`) |
-| `ALI_PC` | 支付宝 PC 网站 | 可选 `payDataType=form/payUrl`(上游默认 `payUrl`) |
-| `ALI_QR` | 支付宝二维码 | 可选 `payDataType=codeUrl/codeImgUrl` |
-| `WX_BAR` | 微信条码 | 必传付款码 |
-| `WX_JSAPI` | 微信公众号 | 必传 `openid`;服务商模式另传 `wxAppId` |
-| `WX_LITE` | 微信小程序 | 必传 `openid`;服务商模式另传 `wxAppId` |
-| `WX_APP` | 微信 APP | 无额外参数 |
-| `WX_H5` | 微信 H5 | 无必传;已取得渠道用户 ID 时可选传 `openid`。返回 `orderState=0`、`payDataType=payurl`,`payData` 为承接页地址 |
-| `WX_NATIVE` | 微信扫码 | 可选 `payDataType=codeUrl/codeImgUrl` |
-| `YSF_BAR` | 云闪付条码 | 必传付款码 |
-| `YSF_JSAPI` | 云闪付 JSAPI | 可选 `openid`;本期只转发该字段,其余参数不转发 |
-
-**付款码键名**:条码类 `wayCode` 的付款码字段,平台**同时接受** `auth_code` 与 `authCode` 两个键名;转发上游时默认只发 `auth_code`,平台配置为 `both` 时两个键都发。调用方任选一种写法即可。
+---
 
-**`payDataType` 默认值**:不传 `payDataType` 时,平台**不注入**默认值,由上游取它自己的默认值;上表括号里的默认值只是上游行为说明。二维码类可选 `codeUrl` / `codeImgUrl`。
+## 2. 统一下单
 
-**敏感值**:付款码、`openid`、`buyerUserId` 的值**不落库、不进日志、不回显**。平台只保存 `channelExtra` 的键名,值只在本次请求的内存里使用一次。请调用方同样不要把这些值写进自己的日志。
+必填只有 4 个:`out_trade_no`(你的订单号,最长 64 字符)、`amount`(分)、`way_code`、`subject`(最长 128 字符)。多商户再加 `mch_no`。可选:`body`、`notify_url`、`return_url`、`expired_time`(秒,默认 7200)、`ext_param`、`client_ip`。
 
-**幂等与重复提交**
+完整调用示例(把 §1 的 `sign` 函数放在同一个文件里):
 
-- 同一 `out_trade_no`、**参数相同**重复提交:返回已有订单,**不会**再打上游、不会重复扣款。
-- 同一 `out_trade_no`、**参数不同**:返回 `code=409`。
-- 缺 `out_trade_no`:返回 `code=400`。
-- 上游调用失败/超时时,平台可能已保留本地订单。不要直接换单号重发:用本单 `out_trade_no` 调订单查询确认,或原样重放(幂等键会把已有订单还给你)。
+```python
+import requests
 
-**响应 `data`(订单 VO)**
+BASE = "https://qcsj88888.com/api/v1"
+API_KEY = "你的 api_key"
+API_SECRET = "你的 api_secret"
 
-`order_no`、`out_trade_no`、`mch_no`、`way_code`、`amount`、`refunded_amount`、`refundable_amount`、`trade_status`、`upstream_pay_order_id`、`upstream_channel_order_no`、`pay_data_type`、`pay_data`、`err_code`、`err_msg`、`pay_time`、`created_time`、`updated_time`、`notify_status`。
+body = {
+    "mch_no": "你的商户号",
+    "out_trade_no": "T20261020001",
+    "amount": 100,
+    "way_code": "QR_CASHIER",
+    "subject": "测试商品",
+    "notify_url": "https://你的域名/pay/notify",
+}
+headers = {
+    "Content-Type": "application/json",
+    "Authorization": f"ApiKey {API_KEY}",
+    "Signature": sign(body, API_SECRET),
+}
+print(requests.post(f"{BASE}/payment/openapi/unified/order/create", json=body, headers=headers).json())
+```
 
-> `refundable_amount = amount - SUM(退款金额 WHERE 退款 state ∈ {0,1,2})` —— 在途退款也占用额度,与退款闸门同一公式。
->
-> **`pay_data` 的渲染安全**:`pay_data` 是上游原样返回的内容(二维码 URL / 表单 HTML / 跳转链接)。渲染前**必须校验协议,只允许 `http/https`**;表单 HTML 只能放进受信页面,不要直接插进你的站点 DOM(防 XSS)。
+成功返回:
 
-### 2.1 H5 托管支付页
+```json
+{
+  "code": 0,
+  "msg": "成功",
+  "data": {
+    "order_no": "UP1960000000000000001",
+    "out_trade_no": "T20261020001",
+    "trade_status": "0",
+    "amount": 100,
+    "pay_data_type": "codeUrl",
+    "pay_data": "https://qr.example.com/xxx"
+  }
+}
+```
 
-公开页面固定地址:`https://qcsj88888.com/pay/unified-h5.html`。适合没有自有收银台、或需要把支付链接直接发给顾客的场景。
+拿到 `pay_data` 后直接给用户:`codeUrl` 转成二维码,`codeImgUrl` 放进 `<img>`,`payUrl` 跳转。渲染前确认协议是 `http/https`;`form` 是表单 HTML,不要直接插进你的页面。
 
-**链接参数**
+**16 种 `way_code`**
 
-| 键 | 必填 | 说明 |
+| `way_code` | 场景 | `channelExtra` |
 |---|---|---|
-| `mch_no` | 是 | 上游商户号,全局唯一 |
-| `out_trade_no` | 是 | 调用方订单号,≤64;幂等键 |
-| `amount` | 是 | 分,正则 `\d{1,15}` 且 > 0 |
-| `subject` | 是 | 标题,≤128 |
-| `way_code` | 是 | 仅支持 `QR_CASHIER`、`ALI_QR`、`WX_NATIVE`、`WX_H5`、`ALI_WAP`、`ALI_PC`、`ALI_APP`、`WX_APP` |
-| `sign` | 是 | 除 `sign` 外全部非空参数的 MD5 签名(32 位大写) |
-
-可选 `body`、`return_url`、`notify_url`、`ext_param`(各 ≤512)也可直接放进链接;页面会把链接上实际存在的字段原样带进建单请求。**链接不接受** `channel_extra`,出现未知参数一律拒绝。
-
-**页面流程**:打开链接只展示订单摘要,**点「确认支付」才建单**(防止链接预览/爬虫提前建单);建单成功后页面每 3 秒查询一次本单结果,`trade_status` 为 `2/3/4/5/6`(成功、失败、撤销、退款、关闭)时停止轮询,最长 30 分钟。
-
-**后台生成链接**:`GET /payment/unified/cashier/order/{orderNo}/h5-link`,需要 `module_payment:unified:cashier:list` 权限。返回 `{"url": "..."}`,只带 6 个必填参数(`notify_url`/`ext_param` 已在订单行上,不重复暴露)。
+| `QR_CASHIER` | 聚合扫码,用户扫你 | 可选 `payDataType`:`codeUrl` / `codeImgUrl` |
+| `AUTO_BAR` | 聚合条码,你扫用户 | 必传 `{"auth_code":"付款码"}` |
+| `ALI_BAR` | 支付宝条码 | 必传 `{"auth_code":"付款码"}` |
+| `WX_BAR` | 微信条码 | 必传 `{"auth_code":"付款码"}` |
+| `YSF_BAR` | 云闪付条码 | 必传 `{"auth_code":"付款码"}` |
+| `ALI_JSAPI` | 支付宝生活号 | 必传 `{"buyerUserId":"..."}` |
+| `WX_JSAPI` | 微信公众号 | 必传 `{"openid":"..."}`,服务商模式加 `wxAppId` |
+| `WX_LITE` | 微信小程序 | 必传 `{"openid":"..."}`,服务商模式加 `wxAppId` |
+| `YSF_JSAPI` | 云闪付 JSAPI | 可选 `{"openid":"..."}` |
+| `ALI_APP` | 支付宝 APP | 不用传 |
+| `WX_APP` | 微信 APP | 不用传 |
+| `ALI_WAP` | 支付宝手机网站 | 可选 `payDataType`:`form` / `codeImgUrl` / `payUrl` |
+| `ALI_PC` | 支付宝 PC 网站 | 可选 `payDataType`:`form` / `payUrl` |
+| `ALI_QR` | 支付宝二维码 | 可选 `payDataType`:`codeUrl` / `codeImgUrl` |
+| `WX_NATIVE` | 微信扫码 | 可选 `payDataType`:`codeUrl` / `codeImgUrl` |
+| `WX_H5` | 微信 H5 | 可不传;已拿到 `openid` 时可传 |
+
+条码类的付款码可以写 `auth_code`,也可以写 `authCode`。付款码、`openid`、`buyerUserId` 这些值平台不落库、不写日志,只用于本次请求。
+
+**`out_trade_no` 是幂等键**
+
+- 同一个 `out_trade_no`、参数相同,重复提交:返回第一笔订单,不会再扣款。
+- 同一个 `out_trade_no`、参数不同(金额、`way_code`、`subject` 任意一个变了):返回 `code=409`。要改参数,请换一个新的 `out_trade_no`。
+- 上游调用失败或超时返回 `code=500`,提示「本地订单已保留」:这笔可能已经成功。**不要换单号**,先用这个 `out_trade_no` 查单。
+
+**没有收银台?用 H5 链接**
+
+在后台收银台点「生成 H5 支付链接」,把链接发给顾客。顾客打开后点「确认支付」才建单,页面会自己轮询结果。链接由后台生成,不需要你签名。
 
 ---
 
 ## 3. 订单查询
 
-```
-POST /payment/openapi/unified/order/query
-```
-
-**请求字段**
-
-| 字段 | 类型 | 必填 | 说明 |
-|---|---|---|---|
-| `mch_no` | string | 否 | 可不传;本接口不按 `mch_no` 过滤 |
-| `order_no` | string | 二选一 | 平台订单号 |
-| `out_trade_no` | string | 二选一 | 调用方订单号 |
-
-`order_no` 与 `out_trade_no` 至少传一个;都传时以 `order_no` 为准。
+用你的订单号查:
 
-**响应**:与下单相同的订单 VO。
+```python
+body = {"out_trade_no": "T20261020001"}
+# 也可以传 {"order_no": "UP1960000000000000001"},两个都传时以 order_no 为准
+```
 
-**`trade_status` 状态枚举**
+返回的 `data` 和下单一样。
 
-| 值 | 含义 | 是否终态 |
-|---|---|---|
-| `0` | 订单生成 | 否 |
-| `1` | 支付中 | 否 |
-| `2` | 支付成功 | 否(可转 5) |
-| `3` | 支付失败 | 是 |
-| `4` | 已撤销 | 是 |
-| `5` | 已退款 | 是 |
-| `6` | 订单关闭 | 是 |
+| `trade_status` | 含义 |
+|---|---|
+| `0` | 待支付 |
+| `1` | 支付中 |
+| `2` | 支付成功(之后还可能变成 `5` 已退款) |
+| `3` | 支付失败 |
+| `4` | 已撤销 |
+| `5` | 已退款 |
+| `6` | 已关闭 |
 
-状态直接采用上游枚举,平台不做本地转义。
+`3` / `4` / `5` / `6` 是终态。建议每 3 秒查一次,到终态就停。
 
 ---
 
 ## 4. 统一退款
 
+```python
+body = {
+    "out_trade_no": "T20261020001",   # 也可以传 order_no
+    "out_refund_no": "R20261020001",  # 你的退款单号,不能重复
+    "refund_amount": 100,             # 分
+    "refund_reason": "测试退款",       # 必填,最长 64 字符
+    "notify_url": "https://你的域名/refund/notify",  # 可选
+}
 ```
-POST /payment/openapi/unified/refund
-```
-
-**请求字段**
-
-| 字段 | 类型 | 必填 | 说明 |
-|---|---|---|---|
-| `mch_no` | string | 否 | 可不传;传入时必须与订单快照一致,不一致返回 404 |
-| `order_no` | string | 二选一 | 平台订单号 |
-| `out_trade_no` | string | 二选一 | 调用方订单号 |
-| `refund_amount` | int | 是 | 退款金额,**单位分**,必须 > 0 |
-| `refund_reason` | string | 是 | 退款原因,≤64 字符 |
-| `out_refund_no` | string | 是 | 调用方退款单号,≤64 字符,**幂等键**,缺失返回 400 |
-| `notify_url` | string | 否 | 本笔退款的下游回调地址,≤512 字符 |
-| `ext_param` | string | 否 | 透传参数 |
-
-**规则**
 
-- `trade_status = 5`(已退款)再发起退款:返回 400「已无可退金额」。
-- 其他 `trade_status != 2` 的订单:返回 400「订单未支付成功,不能退款」。
-- 同一 `out_refund_no`、**参数相同**重复提交:返回已有退款单;**参数不同**:返回 `code=409`。
-- 退款金额不得超过 `refundable_amount`(在途退款也占用额度);超额返回 400「退款金额超过可退金额,当前可退 {available} 分」。
-- 同一订单可多次部分退款,每次用不同的 `out_refund_no`。
+规则:
 
-**响应 `data`(退款 VO)**
+- 只有 `trade_status=2` 的订单能退款。
+- `refund_amount` 不能超过 `refundable_amount`;在途退款也占用额度。
+- 同一订单可以多次部分退款,每次换一个 `out_refund_no`。
+- 同一个 `out_refund_no`、订单和退款金额都相同,重复提交返回原退款单;不一致返回 `code=409`。
 
-`refund_no`、`out_refund_no`、`order_no`、`mch_no`、`amount`、`state`、`upstream_refund_order_id`、`upstream_channel_order_no`、`err_code`、`err_msg`、`success_time`、`created_time`、`updated_time`、`notify_status`。
+返回的 `data` 主要看 `refund_no`、`out_refund_no`、`amount`、`state`:
 
-**退款 `state` 枚举**
-
-| 值 | 含义 | 是否终态 |
-|---|---|---|
-| `0` | 退款生成 | 否 |
-| `1` | 退款中 | 否 |
-| `2` | 退款成功 | 是 |
-| `3` | 退款失败 | 是 |
-| `4` | 退款关闭 | 是 |
+`state`:`0` 生成 / `1` 退款中 / `2` 成功 / `3` 失败 / `4` 关闭。`2` / `3` / `4` 是终态。
 
 ---
 
 ## 5. 退款查询
 
+```python
+body = {"out_refund_no": "R20261020001"}
+# 也可以传 {"refund_no": "UR1960000000000000001"},两个都传时以 refund_no 为准
 ```
-POST /payment/openapi/unified/refund/query
-```
-
-**请求字段**
-
-| 字段 | 类型 | 必填 | 说明 |
-|---|---|---|---|
-| `mch_no` | string | 否 | 可不传;本接口不按 `mch_no` 过滤 |
-| `refund_no` | string | 二选一 | 平台退款单号 |
-| `out_refund_no` | string | 二选一 | 调用方退款单号 |
-
-`refund_no` 与 `out_refund_no` 至少传一个;都传时以 `refund_no` 为准。
-
-**响应**:与退款相同的退款 VO。`state=2/3/4` 是终态,不需要再轮询。
 
-> 不要只依赖回调判断退款结果:回调可能因网络原因延迟或失败(最多投递 3 次)。退款查询是最终依据。
+返回和退款一样。不要只等回调,主动查到 `state=2` 再确认退款成功。
 
 ---
 
 ## 6. 获取渠道用户 ID
 
-```
-POST /payment/openapi/unified/channel-user/jump-url
-```
-
-上游获取渠道用户 ID 的接口是 GET + 浏览器 302,并且要用 `appSecret` 对 URL 参数签名。**因此这一步必须由服务端完成**:调用方用自己的 API Key 向平台换一条已签名的上游 URL,再让浏览器 302 过去。**浏览器不接触密钥**,调用方也不应自己拼接上游地址。
-
-**请求字段**
+只有 JSAPI / 小程序类可能用到这一步(`ALI_JSAPI`、`WX_JSAPI`、`WX_LITE`、`YSF_JSAPI`;其中 `YSF_JSAPI` 的 `openid` 可选)。上游要用 `appSecret` 对跳转地址签名,所以**必须由服务端**换 URL,浏览器不接触密钥。
 
-| 字段 | 类型 | 必填 | 说明 |
-|---|---|---|---|
-| `mch_no` | string | 否 | 租户配置了多个商户号时必填 |
-| `redirect_url` | string | 是 | 授权完成后浏览器跳回的地址,必须是 `http/https` |
-| `if_code` | string | 否 | 渠道标识:`fuioupay`(服务商模式)/ `wxpay`(自有公众号)/ `AUTO`;空则用商户配置,默认 `fuioupay` |
-| `ext_param` | string | 否 | 透传参数,原样带到上游 |
-
-**响应**
-
-| 字段 | 说明 |
-|---|---|
-| `url` | 含签名的完整上游跳转地址(`appSecret` 只参与签名,不出现在 URL 里) |
+```python
+body = {
+    "mch_no": "你的商户号",                    # 多商户时必填
+    "redirect_url": "https://你的域名/callback",  # 授权后浏览器跳回这里
+    "if_code": "fuioupay",                    # 可不传,不传时用商户配置(默认 fuioupay)
+}
+# POST /payment/openapi/unified/channel-user/jump-url
+# 返回 {"code":0,"data":{"url":"https://...签名后的上游地址"}}
+```
 
-拿到 `url` 后把**用户浏览器**跳过去。授权完成后,上游会回跳 `redirect_url` 并带上 `channelUserId`、`channelAppId`、`appId`、`extParam`:
+把用户浏览器跳到返回的 `url`。授权完成后,`redirect_url` 会带上 `channelUserId`:
 
-- `WX_JSAPI` / `WX_LITE`:把 `channelUserId` 放进 `channel_extra.openid`,`channelAppId` 放进 `channel_extra.wxAppId`;
-- `ALI_JSAPI`:把 `channelUserId` 放进 `channel_extra.buyerUserId`。
+- 微信:把 `channelUserId` 作为 `openid` 放进下单的 `channelExtra`;服务商模式再把 `channelAppId` 作为 `wxAppId`。
+- 支付宝:把 `channelUserId` 作为 `buyerUserId` 放进下单的 `channelExtra`。
 
 ---
 
 ## 7. 异步通知
 
-回调是 **form POST**(`application/x-www-form-urlencoded`),字段固定为:
+平台用 **form POST**(`application/x-www-form-urlencoded`)推给你,固定 3 个字段:
 
-| 字段 | 值 |
+| 字段 | 说明 |
 |---|---|
-| `notify_id` | 确定性事件 ID:订单事件为 `uo_<order_no>_<trade_status>`,退款事件为 `ur_<refund_no>_<state>`。同一事件的重试保持同一个 id,接收方可直接按它去重 |
-| `timestamp` | 当前毫秒时间戳 |
-| `content` | JSON 字符串,结构见下 |
+| `notify_id` | 事件 ID,同一事件重试时不变,可以用它去重 |
+| `timestamp` | 毫秒时间戳 |
+| `content` | JSON 字符串,`event` 是 `ORDER` 或 `REFUND` |
 
-**`content` 结构**
+支付成功示例:
 
 ```json
 {
   "event": "ORDER",
   "order": {
-    "order_no": "UP...",
-    "out_trade_no": "C...",
-    "mch_no": "M...",
+    "order_no": "UP1960000000000000001",
+    "out_trade_no": "T20261020001",
+    "mch_no": "M202610200001",
     "way_code": "QR_CASHIER",
     "amount": 100,
+    "trade_status": "2",
     "refunded_amount": 0,
     "refundable_amount": 100,
-    "trade_status": "2",
-    "upstream_pay_order_id": "P...",
-    "upstream_channel_order_no": "...",
-    "pay_time": "2026-10-20T12:00:00+08:00",
-    "err_code": null,
-    "err_msg": null
-  },
-  "upstream": { "state": "2", "successTime": 1760000000000 }
+    "pay_time": "2026-10-20T12:00:00+08:00"
+  }
 }
 ```
 
-退款事件的 `event` 为 `REFUND`,带 `refund` 对象(`refund_no`、`out_refund_no`、`order_no`、`amount`、`state`、`upstream_refund_order_id`、`success_time`、`err_code`、`err_msg`),并**同时带 `order` 的当前快照**(`trade_status`、`refunded_amount`、`refundable_amount`)。
-
-**事件类型**
+退款事件的 `event` 是 `REFUND`,带 `refund` 对象(`refund_no`、`out_refund_no`、`state`、`amount`),同时带订单快照。
 
-| 本地变化 | 推 ORDER | 推 REFUND | 说明 |
-|---|---|---|---|
-| `trade_status` 变为 `2` | 是 | — | 支付成功 |
-| `trade_status` 变为 `3/4/6` | 是 | — | 失败 / 撤销 / 关闭 |
-| `trade_status` 从 `2` 变为 `5`(全额退款导致) | 是 | 见下行 | 订单终态 5 |
-| 退款 `state` 变为 `2/3/4` | — | 是 | 退款成功 / 失败 / 关闭 |
-| 部分退款成功(订单仍为 `2`) | **否** | 是 | 订单状态没变;退款事件里带订单快照,新的 `refunded_amount` 从那里取 |
+**什么时候推**
 
-`trade_status` 变为 `1` 与退款 `state` 变为 `1` 是过程态,**不推送**。
+- 支付状态变成 `2`、`3`、`4`、`6`:推 `ORDER`。
+- 全额退款导致订单变成 `5`:同时推 `ORDER` 和 `REFUND`。
+- 退款状态变成 `2`、`3`、`4`:推 `REFUND`。
+- 支付中 `1`、退款中 `1` 不推。
+- 部分退款成功、订单还是 `2`:只推 `REFUND`,新的 `refunded_amount` 从订单快照里取。
 
-**投递与去重**
+**怎么回**
 
-- 接收方返回任意 **2xx** 即视为成功;非 2xx 或异常记为 `FAILED`。
-- 失败后平台每分钟重试一次,`notify_count` **含首次**,即**最多投递 3 次**(首次 + 2 次重试)。超过后不再自动重试。
-- `notify_id` 在同一事件的所有重试里不变,接收方**可去重**:收到相同 `notify_id` 直接返回 2xx 即可。
-- 回调地址优先级:订单/退款快照 `callback_url` → API Key 配置的 `return_url` → 租户 `open_conf.return_url`。退款先看退款快照,再看订单快照。
-- 地址为空时平台记为 `FAILED`(不会静默跳过);补配地址后重试仍可能捞起。
-- 下游回调**不带签名**。接收方如果对来源有疑问,应以订单查询 / 退款查询的结果为准,不要仅凭回调报文发货或退款。
+- 返回任意 **2xx** 就算接收成功。
+- 失败后每分钟重试一次,**最多投递 3 次**(含首次)。
+- 同一个 `notify_id` **可去重**:收到重复的直接返回 2xx。
+- 回调不带签名。不放心就以订单查询或退款查询的结果为准,不要只凭回调发货。
 
 ---
 
 ## 8. 联调检查清单
 
-- [ ] 用 **1 分或 1 元的小额实弹**跑通「下单 → 支付 → 订单查询 → 全额退款 → 退款查询」,并核对金额单位是**分**。
-- [ ] **回调地址公网可达**:`notify_url` 不要填 `localhost`、`127.0.0.1` 或内网地址,测试环境也要能被平台访问到。平台拒绝 `localhost`、`127.0.0.0/8`、`::1`、`0.0.0.0`、`169.254.169.254`。
-- [ ] **退款查询**单独验证:不只等回调,用 `refund_no` / `out_refund_no` 主动查一次,确认 `state=2`。
-- [ ] **幂等重放**:同一 `out_trade_no` 原样重复下单,应返回同一订单;改参数重放应返回 `code=409`。退款同理(`out_refund_no`)。
-- [ ] **16 种 `wayCode`** 按业务需要逐个确认 `channelExtra`:条码类传付款码,`ALI_JSAPI` 传 `buyerUserId`,`WX_JSAPI`/`WX_LITE` 传 `openid`(服务商模式另传 `wxAppId`)。
-- [ ] **`pay_data` 渲染**:先校验协议只允许 `http/https`;表单 HTML 只放进受信页面。
-- [ ] **回调幂等**:用 `notify_id` 去重,重复通知不重复记账。
-- [ ] **渠道用户 ID** 走平台的服务端接口换 URL,浏览器与前端代码里不出现 `appSecret`。
-- [ ] 配置了多个商户号时,**统一下单**与**获取渠道用户 ID** 请求显式指定 `mch_no`;查询与退款接口可不传。
+- [ ] 用 1 分或 1 元跑通「下单 → 支付 → 查单 → 全额退款 → 查退款」,确认金额单位是**分**。
+- [ ] `notify_url` 能被平台访问到,不要填 `localhost` 或 `127.0.0.1`。
+- [ ] 同一个 `out_trade_no` 原样重放返回同一笔订单;改参数重放返回 `code=409`。
+- [ ] 按业务选 `16 种` `way_code` 中的一种,条码类记得传 `channelExtra.auth_code`。
+- [ ] `pay_data` 只按 `http/https` 渲染,表单 HTML 不直接插进页面。
+- [ ] 退款后主动查一次 `refund/query`,确认 `state=2`。
+- [ ] 用 `notify_id` 做回调去重,重复通知不重复记账。