|
|
@@ -0,0 +1,510 @@
|
|
|
+<!doctype html>
|
|
|
+<html lang="zh-CN">
|
|
|
+<head>
|
|
|
+<meta charset="utf-8">
|
|
|
+<title>聚合支付开放接口对接指南</title>
|
|
|
+<style>
|
|
|
+ @page { size: A4; margin: 12mm 12.5mm 11mm 12.5mm; }
|
|
|
+ :root {
|
|
|
+ --ink: #1a1d21;
|
|
|
+ --muted: #5b6570;
|
|
|
+ --accent: #14508c;
|
|
|
+ --line: #d7dde5;
|
|
|
+ --soft: #f3f6fa;
|
|
|
+ --warn: #b8791a;
|
|
|
+ --warn-bg: #fdf7ea;
|
|
|
+ }
|
|
|
+ * { box-sizing: border-box; }
|
|
|
+ html { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
|
+ body {
|
|
|
+ margin: 0;
|
|
|
+ font-family: "Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", sans-serif;
|
|
|
+ font-size: 8.8pt;
|
|
|
+ line-height: 1.42;
|
|
|
+ color: var(--ink);
|
|
|
+ }
|
|
|
+ .head { border-top: 4px solid var(--accent); padding-top: 8px; margin-bottom: 8px; }
|
|
|
+ .kicker { font-size: 8pt; letter-spacing: 1.5px; color: var(--accent); text-transform: uppercase; }
|
|
|
+ h1 { font-size: 20pt; margin: 2px 0 2px; letter-spacing: 0.5px; }
|
|
|
+ .meta { font-size: 8.6pt; color: var(--muted); }
|
|
|
+
|
|
|
+ .quick {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ gap: 3px 10px;
|
|
|
+ background: var(--soft);
|
|
|
+ border: 1px solid var(--line);
|
|
|
+ border-radius: 4px;
|
|
|
+ padding: 6px 10px;
|
|
|
+ margin: 8px 0 4px;
|
|
|
+ font-size: 8.8pt;
|
|
|
+ }
|
|
|
+ .quick div { white-space: nowrap; }
|
|
|
+ .quick span { color: var(--muted); display: inline-block; min-width: 62px; }
|
|
|
+
|
|
|
+ h2 {
|
|
|
+ font-size: 11.6pt;
|
|
|
+ margin: 11px 0 4px;
|
|
|
+ padding-left: 7px;
|
|
|
+ border-left: 3.5px solid var(--accent);
|
|
|
+ break-after: avoid;
|
|
|
+ }
|
|
|
+ h3 { font-size: 9.6pt; margin: 8px 0 3px; color: #24343f; break-after: avoid; }
|
|
|
+ p { margin: 3px 0 5px; }
|
|
|
+ ul, ol { margin: 3px 0 6px; padding-left: 18px; }
|
|
|
+ li { margin: 1.5px 0; }
|
|
|
+
|
|
|
+ code {
|
|
|
+ font-family: Consolas, "Cascadia Mono", "Courier New", monospace;
|
|
|
+ font-size: 8.4pt;
|
|
|
+ background: #eef1f5;
|
|
|
+ padding: 0 2px;
|
|
|
+ border-radius: 2px;
|
|
|
+ word-break: break-word;
|
|
|
+ }
|
|
|
+ pre {
|
|
|
+ background: var(--soft);
|
|
|
+ border: 1px solid #e2e7ee;
|
|
|
+ border-radius: 3px;
|
|
|
+ padding: 5px 8px;
|
|
|
+ margin: 4px 0 7px;
|
|
|
+ font-family: Consolas, "Cascadia Mono", "Courier New", monospace;
|
|
|
+ font-size: 7.4pt;
|
|
|
+ line-height: 1.36;
|
|
|
+ white-space: pre-wrap;
|
|
|
+ word-break: break-word;
|
|
|
+ break-inside: avoid;
|
|
|
+ }
|
|
|
+ pre code { background: none; padding: 0; font-size: inherit; }
|
|
|
+ pre.long { break-inside: auto; }
|
|
|
+
|
|
|
+ table {
|
|
|
+ width: 100%;
|
|
|
+ border-collapse: collapse;
|
|
|
+ margin: 4px 0 8px;
|
|
|
+ font-size: 8.1pt;
|
|
|
+ }
|
|
|
+ th, td { border: 1px solid var(--line); padding: 2.2px 5px; vertical-align: top; text-align: left; }
|
|
|
+ th { background: #eaf0f7; font-weight: 600; white-space: nowrap; }
|
|
|
+ tr { break-inside: avoid; }
|
|
|
+ thead { display: table-header-group; }
|
|
|
+ td code { font-size: 7.9pt; }
|
|
|
+ .nowrap { white-space: nowrap; }
|
|
|
+
|
|
|
+ .note {
|
|
|
+ border-left: 3px solid var(--warn);
|
|
|
+ background: var(--warn-bg);
|
|
|
+ padding: 4px 9px;
|
|
|
+ margin: 5px 0 7px;
|
|
|
+ font-size: 8.9pt;
|
|
|
+ break-inside: avoid;
|
|
|
+ }
|
|
|
+ .note b { color: var(--warn); }
|
|
|
+ .cols2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
|
+ .check { list-style: none; padding-left: 0; }
|
|
|
+ .check li { padding-left: 16px; text-indent: -16px; margin: 2px 0; }
|
|
|
+ .check li::before { content: "\2610\00a0"; color: var(--muted); }
|
|
|
+ .small { font-size: 8.2pt; color: var(--muted); }
|
|
|
+ .pdf-out { break-inside: avoid; }
|
|
|
+</style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+
|
|
|
+<header class="head">
|
|
|
+ <div class="kicker">商户服务端对接文档</div>
|
|
|
+ <h1>聚合支付 开放接口</h1>
|
|
|
+ <div class="meta">版本 1.0 · 2026-10-21 · 5 个接口 · 请求签名 HMAC-SHA256</div>
|
|
|
+</header>
|
|
|
+
|
|
|
+<div class="quick">
|
|
|
+ <div><span>接口根地址</span><code>https://qcsj88888.com/api/v1</code></div>
|
|
|
+ <div><span>请求方式</span><code>POST</code>,<code>application/json</code></div>
|
|
|
+ <div><span>金额单位</span><code>分</code>,整数(<code>100</code> = 1.00 元)</div>
|
|
|
+ <div><span>成功标志</span>响应体 <code>code = 0</code></div>
|
|
|
+</div>
|
|
|
+
|
|
|
+<h2>1 请求约定</h2>
|
|
|
+
|
|
|
+<p>所有接口用 POST 提交 JSON,响应也是 JSON。每个请求带三个请求头:</p>
|
|
|
+
|
|
|
+<table>
|
|
|
+ <thead><tr><th style="width:30%">请求头</th><th>值</th></tr></thead>
|
|
|
+ <tr><td><code>Content-Type</code></td><td><code>application/json</code></td></tr>
|
|
|
+ <tr><td><code>Authorization</code></td><td><code>ApiKey <你的 api_key></code>,注意 <code>ApiKey</code> 后面有一个空格</td></tr>
|
|
|
+ <tr><td><code>Signature</code></td><td>对请求体算出的 HMAC-SHA256,64 位小写十六进制,算法见 §2</td></tr>
|
|
|
+</table>
|
|
|
+
|
|
|
+<p>响应包为 <code>{"code":0,"msg":"成功","data":{...},"status_code":200,"success":true}</code>。只看 <code>code</code> 和 <code>data</code>:<code>code=0</code> 成功,业务数据在 <code>data</code>;<code>code≠0</code> 失败,原因看 <code>msg</code>。<code>status_code</code>、<code>success</code> 与 <code>code</code> 重复,可忽略,下文示例省略这两个字段。<b>HTTP 200 不代表成功。</b></p>
|
|
|
+
|
|
|
+<table>
|
|
|
+ <thead><tr><th style="width:34%">用途</th><th>方法 + 路径(拼在根地址后面)</th></tr></thead>
|
|
|
+ <tr><td>统一下单</td><td><code>POST /payment/openapi/unified/order/create</code></td></tr>
|
|
|
+ <tr><td>查询订单</td><td><code>POST /payment/openapi/unified/order/query</code></td></tr>
|
|
|
+ <tr><td>发起退款</td><td><code>POST /payment/openapi/unified/refund</code></td></tr>
|
|
|
+ <tr><td>查询退款</td><td><code>POST /payment/openapi/unified/refund/query</code></td></tr>
|
|
|
+ <tr><td>获取渠道用户 ID(仅 JSAPI / 小程序)</td><td><code>POST /payment/openapi/unified/channel-user/jump-url</code></td></tr>
|
|
|
+</table>
|
|
|
+
|
|
|
+<p><b>商户号 <code>mch_no</code>:</b>租户只有一个商户号时不传。有多个商户号时,<b>下单</b>和<b>获取渠道用户 ID</b> 必须传;查单、退款、查退款按单号寻址,不必传。</p>
|
|
|
+
|
|
|
+<h2>2 签名</h2>
|
|
|
+
|
|
|
+<p>签名只算请求体的顶层字段,与 URL、请求头、时间戳无关。</p>
|
|
|
+
|
|
|
+<ol>
|
|
|
+ <li>取请求体的顶层字段。</li>
|
|
|
+ <li>去掉 <code>sign</code>、<code>null</code>、空字符串、空对象 <code>{}</code>、空数组 <code>[]</code>。</li>
|
|
|
+ <li>按字段名 ASCII 升序排列。</li>
|
|
|
+ <li>每个值先 URL 编码(RFC 3986:空格 <code>%20</code>、星号 <code>%2A</code>、波浪号保留)。对象和数组先按键排序输出紧凑 JSON(无空格、中文不转义),再整体编码。</li>
|
|
|
+ <li>用 <code>&</code> 拼成 <code>k=v&k=v</code>。</li>
|
|
|
+ <li>以 <code>api_secret</code> 为密钥做 HMAC-SHA256,取小写十六进制,放入 <code>Signature</code> 头。</li>
|
|
|
+</ol>
|
|
|
+
|
|
|
+<pre><code>from urllib.parse import quote
|
|
|
+import hashlib, hmac, json
|
|
|
+
|
|
|
+
|
|
|
+def sign(body: dict, api_secret: str) -> str:
|
|
|
+ parts = []
|
|
|
+ for key in sorted(body):
|
|
|
+ value = body[key]
|
|
|
+ if key == "sign" or value is None or value == "" or value == {} or value == []:
|
|
|
+ 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(key + "=" + quote(text, safe=""))
|
|
|
+ raw = "&".join(parts)
|
|
|
+ return hmac.new(api_secret.encode(), raw.encode(), hashlib.sha256).hexdigest()
|
|
|
+</code></pre>
|
|
|
+
|
|
|
+<div class="note"><b>密钥只在服务端使用。</b>不要把 <code>api_secret</code> 写进浏览器、App 或前端仓库。签名算的是你实际发出的那个请求体,改了任何字段都要重算。</div>
|
|
|
+
|
|
|
+<h2>3 下单</h2>
|
|
|
+
|
|
|
+<h3>3.1 必填参数</h3>
|
|
|
+<table>
|
|
|
+ <thead><tr><th style="width:18%">字段</th><th style="width:11%">类型</th><th>说明</th></tr></thead>
|
|
|
+ <tr><td><code>out_trade_no</code></td><td>string(64)</td><td>你的订单号,<b>幂等键</b>。同一租户内唯一。</td></tr>
|
|
|
+ <tr><td><code>amount</code></td><td>integer</td><td>金额,单位<b>分</b>,必须大于 0。</td></tr>
|
|
|
+ <tr><td><code>way_code</code></td><td>string(16)</td><td>支付方式,见 §4。</td></tr>
|
|
|
+ <tr><td><code>subject</code></td><td>string(128)</td><td>商品标题。</td></tr>
|
|
|
+ <tr><td><code>mch_no</code></td><td>string(64)</td><td>多商户时必传;单商户可空。</td></tr>
|
|
|
+</table>
|
|
|
+
|
|
|
+<h3>3.2 可选参数</h3>
|
|
|
+<table>
|
|
|
+ <thead><tr><th style="width:18%">字段</th><th style="width:11%">类型</th><th>说明</th></tr></thead>
|
|
|
+ <tr><td><code>body</code></td><td>string(512)</td><td>商品描述。不传时用 <code>subject</code>。</td></tr>
|
|
|
+ <tr><td><code>notify_url</code></td><td>string(512)</td><td>异步通知地址,必须是公网可访问的 http/https 地址。见 §10。</td></tr>
|
|
|
+ <tr><td><code>return_url</code></td><td>string(512)</td><td>支付完成后浏览器跳回的地址(部分支付方式使用)。</td></tr>
|
|
|
+ <tr><td><code>expired_time</code></td><td>integer</td><td>订单有效期,单位秒,默认 7200(2 小时)。</td></tr>
|
|
|
+ <tr><td><code>client_ip</code></td><td>string(64)</td><td>用户终端 IP。不传时取请求来源 IP。</td></tr>
|
|
|
+ <tr><td><code>ext_param</code></td><td>string(512)</td><td>透传参数,上游原样返回。</td></tr>
|
|
|
+ <tr><td><code>channel_extra</code></td><td>object</td><td>支付方式附加参数,见 §4。</td></tr>
|
|
|
+</table>
|
|
|
+
|
|
|
+<h3>3.3 完整调用示例</h3>
|
|
|
+<pre><code>import requests, uuid
|
|
|
+
|
|
|
+BASE = "https://qcsj88888.com/api/v1"
|
|
|
+API_KEY = "你的 api_key"
|
|
|
+API_SECRET = "你的 api_secret"
|
|
|
+
|
|
|
+
|
|
|
+def post(path: str, body: dict) -> dict:
|
|
|
+ headers = {
|
|
|
+ "Content-Type": "application/json",
|
|
|
+ "Authorization": "ApiKey " + API_KEY,
|
|
|
+ "Signature": sign(body, API_SECRET), # sign() 见 §2
|
|
|
+ }
|
|
|
+ resp = requests.post(BASE + path, json=body, headers=headers, timeout=15)
|
|
|
+ return resp.json()
|
|
|
+
|
|
|
+
|
|
|
+body = {
|
|
|
+ "out_trade_no": "T" + uuid.uuid4().hex[:16],
|
|
|
+ "amount": 100, # 分
|
|
|
+ "way_code": "QR_CASHIER",
|
|
|
+ "subject": "测试商品",
|
|
|
+ "notify_url": "https://example.com/pay/notify",
|
|
|
+}
|
|
|
+print(post("/payment/openapi/unified/order/create", body))
|
|
|
+</code></pre>
|
|
|
+
|
|
|
+<h3>3.4 成功响应</h3>
|
|
|
+<pre><code>{
|
|
|
+ "code": 0,
|
|
|
+ "msg": "成功",
|
|
|
+ "data": {
|
|
|
+ "order_no": "UP1960000000000000001",
|
|
|
+ "out_trade_no": "T20261020001",
|
|
|
+ "way_code": "QR_CASHIER",
|
|
|
+ "amount": 100,
|
|
|
+ "refundable_amount": 100,
|
|
|
+ "trade_status": "0",
|
|
|
+ "pay_data_type": "codeUrl",
|
|
|
+ "pay_data": "https://qr.example.com/xxx",
|
|
|
+ "created_time": "2026-10-20T12:00:00+08:00"
|
|
|
+ }
|
|
|
+}
|
|
|
+</code></pre>
|
|
|
+
|
|
|
+<p><code>order_no</code> 是平台订单号。<code>out_trade_no</code> 是你的单号,后续查单、退款都用它。把 <code>pay_data</code> 交给用户完成支付,渲染方式见 §4.2。</p>
|
|
|
+
|
|
|
+<h2>4 支付方式与 pay_data</h2>
|
|
|
+
|
|
|
+<h3>4.1 支持的 way_code</h3>
|
|
|
+<table>
|
|
|
+ <thead><tr><th style="width:15%">way_code</th><th style="width:34%">场景</th><th>channel_extra</th></tr></thead>
|
|
|
+ <tr><td><code>QR_CASHIER</code></td><td>聚合扫码,用户扫你</td><td>可选 <code>payDataType</code>:<code>codeUrl</code> / <code>codeImgUrl</code></td></tr>
|
|
|
+ <tr><td><code>AUTO_BAR</code> <code>ALI_BAR</code> <code>WX_BAR</code> <code>YSF_BAR</code></td><td>条码支付,你扫用户(聚合 / 支付宝 / 微信 / 云闪付)</td><td>必传 <code>{"auth_code":"付款码"}</code></td></tr>
|
|
|
+ <tr><td><code>ALI_JSAPI</code></td><td>支付宝生活号</td><td>必传 <code>{"buyerUserId":"..."}</code></td></tr>
|
|
|
+ <tr><td><code>WX_JSAPI</code></td><td>微信公众号</td><td>必传 <code>{"openid":"..."}</code>;服务商模式加 <code>wxAppId</code></td></tr>
|
|
|
+ <tr><td><code>WX_LITE</code></td><td>微信小程序</td><td>必传 <code>{"openid":"..."}</code>;服务商模式加 <code>wxAppId</code></td></tr>
|
|
|
+ <tr><td><code>YSF_JSAPI</code></td><td>云闪付 JSAPI</td><td>可选 <code>{"openid":"..."}</code></td></tr>
|
|
|
+ <tr><td><code>ALI_APP</code> <code>WX_APP</code></td><td>原生 APP 支付(支付宝 / 微信)</td><td>不用传</td></tr>
|
|
|
+ <tr><td><code>ALI_WAP</code></td><td>支付宝手机网站</td><td>可选 <code>payDataType</code>:<code>form</code> / <code>codeImgUrl</code> / <code>payUrl</code></td></tr>
|
|
|
+ <tr><td><code>ALI_PC</code></td><td>支付宝电脑网站</td><td>可选 <code>payDataType</code>:<code>form</code> / <code>payUrl</code></td></tr>
|
|
|
+ <tr><td><code>ALI_QR</code></td><td>支付宝二维码</td><td>可选 <code>payDataType</code>:<code>codeUrl</code> / <code>codeImgUrl</code></td></tr>
|
|
|
+ <tr><td><code>WX_NATIVE</code></td><td>微信扫码</td><td>可选 <code>payDataType</code>:<code>codeUrl</code> / <code>codeImgUrl</code></td></tr>
|
|
|
+ <tr><td><code>WX_H5</code></td><td>微信 H5</td><td>可不传;已拿到 <code>openid</code> 时可传</td></tr>
|
|
|
+</table>
|
|
|
+
|
|
|
+<p><code>way_code</code> 大小写敏感。条码类的付款码可写 <code>auth_code</code>,也可写 <code>authCode</code>。付款码、<code>openid</code>、<code>buyerUserId</code> 只用于本次请求:平台不落库、不写日志。</p>
|
|
|
+
|
|
|
+<h3>4.2 pay_data 怎么用</h3>
|
|
|
+<table>
|
|
|
+ <thead><tr><th style="width:16%">pay_data_type</th><th style="width:30%">pay_data 内容</th><th>用法</th></tr></thead>
|
|
|
+ <tr><td><code>codeUrl</code></td><td>支付链接</td><td>生成二维码给用户扫</td></tr>
|
|
|
+ <tr><td><code>codeImgUrl</code></td><td>二维码图片 URL</td><td>放进 <code><img src></code></td></tr>
|
|
|
+ <tr><td><code>payUrl</code></td><td>支付页链接</td><td>浏览器跳转</td></tr>
|
|
|
+ <tr><td><code>form</code></td><td>HTML 表单字符串</td><td>作为独立页面提交;不要插进现有页面</td></tr>
|
|
|
+ <tr><td><code>wxapp</code></td><td>JSON 字符串,微信支付参数</td><td><code>JSON.parse</code> 后传给 <code>WeixinJSBridge.invoke('getBrandWCPayRequest', ...)</code></td></tr>
|
|
|
+</table>
|
|
|
+
|
|
|
+<div class="note"><b>只渲染 http/https 地址。</b>把 <code>pay_data</code> 当数据,不要当代码执行。<code>form</code> 是完整表单 HTML,插入页面会破坏现有 DOM。</div>
|
|
|
+
|
|
|
+<h2>5 幂等与失败重试</h2>
|
|
|
+
|
|
|
+<ul>
|
|
|
+ <li><b>同一 <code>out_trade_no</code></b>,金额、<code>way_code</code>、<code>subject</code> 全部相同:返回原订单,<code>code=0</code>,不会重复扣款。</li>
|
|
|
+ <li><b>同一 <code>out_trade_no</code></b>,以上任一项不同:返回 <code>code=409</code>「out_trade_no 已存在且参数不一致」。换单号再下。</li>
|
|
|
+ <li><b><code>code=500</code> 且 msg 以「本地订单已保留」开头</b>:上游调用失败或超时,这笔<b>可能已经成功</b>。不要换单号重下,先查单。确认未支付且没有可用的 <code>pay_data</code> 后,用<b>新的</b> <code>out_trade_no</code> 重新下单。</li>
|
|
|
+ <li>退款同理:<code>out_refund_no</code> 是幂等键,同一单号参数不同返回 <code>code=409</code>。</li>
|
|
|
+ <li>调用前先把 <code>out_trade_no</code> 持久化。所有网络超时、连接中断,一律先查单再决定。</li>
|
|
|
+</ul>
|
|
|
+
|
|
|
+<h2>6 查询订单</h2>
|
|
|
+
|
|
|
+<pre><code>{"out_trade_no": "T20261020001"}</code></pre>
|
|
|
+
|
|
|
+<p>也可以传 <code>{"order_no": "UP1960000000000000001"}</code>。两个都传时以 <code>order_no</code> 为准。响应的 <code>data</code> 与下单相同。</p>
|
|
|
+
|
|
|
+<table>
|
|
|
+ <thead><tr><th style="width:14%">trade_status</th><th>含义</th></tr></thead>
|
|
|
+ <tr><td><code>0</code></td><td>待支付</td></tr>
|
|
|
+ <tr><td><code>1</code></td><td>支付中</td></tr>
|
|
|
+ <tr><td><code>2</code></td><td>支付成功(之后可能变成 <code>5</code> 已退款)</td></tr>
|
|
|
+ <tr><td><code>3</code></td><td>支付失败</td></tr>
|
|
|
+ <tr><td><code>4</code></td><td>已撤销</td></tr>
|
|
|
+ <tr><td><code>5</code></td><td>已退款</td></tr>
|
|
|
+ <tr><td><code>6</code></td><td>已关闭</td></tr>
|
|
|
+</table>
|
|
|
+
|
|
|
+<p><code>3</code> / <code>4</code> / <code>5</code> / <code>6</code> 是终态。建议每 3 秒查一次,到终态停止。支付结果以查单或异步通知中的 <code>trade_status</code> 为准。</p>
|
|
|
+
|
|
|
+<h2>7 发起退款</h2>
|
|
|
+
|
|
|
+<pre><code>{
|
|
|
+ "out_trade_no": "T20261020001",
|
|
|
+ "out_refund_no": "R20261020001",
|
|
|
+ "refund_amount": 100,
|
|
|
+ "refund_reason": "测试退款",
|
|
|
+ "notify_url": "https://example.com/refund/notify"
|
|
|
+}</code></pre>
|
|
|
+
|
|
|
+<p>定位订单用 <code>order_no</code> 或 <code>out_trade_no</code>,至少传一个,都传时以 <code>order_no</code> 为准。<code>mch_no</code> 可选,传了必须与订单一致。</p>
|
|
|
+
|
|
|
+<table>
|
|
|
+ <thead><tr><th style="width:18%">字段</th><th style="width:11%">类型</th><th>说明</th></tr></thead>
|
|
|
+ <tr><td><code>out_refund_no</code></td><td>string(64)</td><td>你的退款单号,<b>幂等键</b>。</td></tr>
|
|
|
+ <tr><td><code>refund_amount</code></td><td>integer</td><td>退款金额,分。</td></tr>
|
|
|
+ <tr><td><code>refund_reason</code></td><td>string(64)</td><td>退款原因,必填。</td></tr>
|
|
|
+ <tr><td><code>notify_url</code></td><td>string(512)</td><td>本笔退款的通知地址,可选。</td></tr>
|
|
|
+</table>
|
|
|
+
|
|
|
+<ul>
|
|
|
+ <li>只有 <code>trade_status=2</code> 的订单能退款。</li>
|
|
|
+ <li><code>refund_amount</code> 不能超过 <code>refundable_amount</code>。在途退款也占用额度。</li>
|
|
|
+ <li>同一订单可以多次部分退款,每次换一个 <code>out_refund_no</code>。</li>
|
|
|
+</ul>
|
|
|
+
|
|
|
+<pre><code>{
|
|
|
+ "code": 0,
|
|
|
+ "msg": "成功",
|
|
|
+ "data": {
|
|
|
+ "refund_no": "UR1960000000000000001",
|
|
|
+ "out_refund_no": "R20261020001",
|
|
|
+ "order_no": "UP1960000000000000001",
|
|
|
+ "mch_no": "M202610200001",
|
|
|
+ "amount": 100,
|
|
|
+ "state": "1",
|
|
|
+ "created_time": "2026-10-20T12:05:00+08:00"
|
|
|
+ }
|
|
|
+}
|
|
|
+</code></pre>
|
|
|
+
|
|
|
+<table>
|
|
|
+ <thead><tr><th style="width:12%">state</th><th>含义</th></tr></thead>
|
|
|
+ <tr><td><code>0</code></td><td>退款生成</td></tr>
|
|
|
+ <tr><td><code>1</code></td><td>退款中</td></tr>
|
|
|
+ <tr><td><code>2</code></td><td>退款成功</td></tr>
|
|
|
+ <tr><td><code>3</code></td><td>退款失败</td></tr>
|
|
|
+ <tr><td><code>4</code></td><td>退款关闭</td></tr>
|
|
|
+</table>
|
|
|
+
|
|
|
+<p><code>2</code> / <code>3</code> / <code>4</code> 是终态。</p>
|
|
|
+
|
|
|
+<h2>8 查询退款</h2>
|
|
|
+
|
|
|
+<pre><code>{"out_refund_no": "R20261020001"}</code></pre>
|
|
|
+
|
|
|
+<p>也可以传 <code>{"refund_no": "UR1960000000000000001"}</code>。两个都传时以 <code>refund_no</code> 为准。响应与退款相同。不要只等通知:主动查到 <code>state=2</code> 再确认退款成功。</p>
|
|
|
+
|
|
|
+<h2>9 获取渠道用户 ID(JSAPI / 小程序)</h2>
|
|
|
+
|
|
|
+<p><code>WX_JSAPI</code>、<code>WX_LITE</code>、<code>ALI_JSAPI</code> 需要先拿渠道用户 ID。<code>YSF_JSAPI</code> 可选。上游签名要用 <code>api_secret</code>,所以这一步必须由服务端换取 URL,浏览器不接触密钥。</p>
|
|
|
+
|
|
|
+<pre><code>{
|
|
|
+ "mch_no": "M202610200001",
|
|
|
+ "redirect_url": "https://example.com/channel/callback",
|
|
|
+ "ext_param": "order=1"
|
|
|
+}</code></pre>
|
|
|
+
|
|
|
+<pre><code>{"code": 0, "msg": "成功", "data": {"url": "https://...签名后的上游地址"}}</code></pre>
|
|
|
+
|
|
|
+<p>把用户浏览器跳到返回的 <code>url</code>。授权完成后,<code>redirect_url</code> 会带上 <code>channelUserId</code>(微信还会带 <code>channelAppId</code>):</p>
|
|
|
+
|
|
|
+<ul>
|
|
|
+ <li>微信:把 <code>channelUserId</code> 作为 <code>openid</code> 放进下单的 <code>channel_extra</code>;服务商模式再把 <code>channelAppId</code> 作为 <code>wxAppId</code>。</li>
|
|
|
+ <li>支付宝:把 <code>channelUserId</code> 作为 <code>buyerUserId</code> 放进下单的 <code>channel_extra</code>。</li>
|
|
|
+</ul>
|
|
|
+
|
|
|
+<p><code>if_code</code> 不用传,平台固定使用 <code>fuioupay</code>。</p>
|
|
|
+
|
|
|
+<h2>10 异步通知</h2>
|
|
|
+
|
|
|
+<p>平台用 <b>form POST</b>(<code>application/x-www-form-urlencoded</code>)推送,固定三个字段:</p>
|
|
|
+
|
|
|
+<table>
|
|
|
+ <thead><tr><th style="width:18%">字段</th><th>说明</th></tr></thead>
|
|
|
+ <tr><td><code>notify_id</code></td><td>事件 ID。同一事件重试时不变,用它去重。</td></tr>
|
|
|
+ <tr><td><code>timestamp</code></td><td>毫秒时间戳。</td></tr>
|
|
|
+ <tr><td><code>content</code></td><td>JSON 字符串,结构见下。</td></tr>
|
|
|
+</table>
|
|
|
+
|
|
|
+<p>支付事件(<code>event=ORDER</code>):</p>
|
|
|
+<pre class="long"><code>{
|
|
|
+ "event": "ORDER",
|
|
|
+ "order": {
|
|
|
+ "order_no": "UP1960000000000000001",
|
|
|
+ "out_trade_no": "T20261020001",
|
|
|
+ "mch_no": "M202610200001",
|
|
|
+ "way_code": "QR_CASHIER",
|
|
|
+ "amount": 100,
|
|
|
+ "refunded_amount": 0,
|
|
|
+ "refundable_amount": 100,
|
|
|
+ "trade_status": "2",
|
|
|
+ "upstream_pay_order_id": "19522112587029827620865",
|
|
|
+ "upstream_channel_order_no": "4200001234202610200000000000",
|
|
|
+ "pay_time": "2026-10-20T12:00:00+08:00",
|
|
|
+ "err_code": null,
|
|
|
+ "err_msg": null
|
|
|
+ },
|
|
|
+ "upstream": {}
|
|
|
+}
|
|
|
+</code></pre>
|
|
|
+
|
|
|
+<p>退款事件(<code>event=REFUND</code>):</p>
|
|
|
+<pre class="long"><code>{
|
|
|
+ "event": "REFUND",
|
|
|
+ "refund": {
|
|
|
+ "refund_no": "UR1960000000000000001",
|
|
|
+ "out_refund_no": "R20261020001",
|
|
|
+ "order_no": "UP1960000000000000001",
|
|
|
+ "amount": 100,
|
|
|
+ "state": "2",
|
|
|
+ "upstream_refund_order_id": "...",
|
|
|
+ "success_time": "2026-10-20T12:06:00+08:00",
|
|
|
+ "err_code": null,
|
|
|
+ "err_msg": null
|
|
|
+ },
|
|
|
+ "order": { "...": "订单当前快照,字段同 ORDER 事件" },
|
|
|
+ "upstream": {}
|
|
|
+}
|
|
|
+</code></pre>
|
|
|
+
|
|
|
+<p><code>upstream</code> 是上游回调字段的白名单投影(<code>payOrderId</code>、<code>state</code>、<code>channelOrderNo</code> 等),没有快照时是空对象。</p>
|
|
|
+
|
|
|
+<h3>10.1 推送时机</h3>
|
|
|
+<table>
|
|
|
+ <thead><tr><th style="width:42%">事件</th><th>什么时候推</th></tr></thead>
|
|
|
+ <tr><td><code>ORDER</code></td><td>订单状态变成 <code>2</code> / <code>3</code> / <code>4</code> / <code>6</code></td></tr>
|
|
|
+ <tr><td><code>ORDER</code> + <code>REFUND</code></td><td>全额退款导致订单变成 <code>5</code>,两条都推</td></tr>
|
|
|
+ <tr><td><code>REFUND</code></td><td>退款状态变成 <code>2</code> / <code>3</code> / <code>4</code></td></tr>
|
|
|
+ <tr><td>不推</td><td>订单 <code>0</code> / <code>1</code>、退款 <code>0</code> / <code>1</code></td></tr>
|
|
|
+ <tr><td>只推 <code>REFUND</code></td><td>部分退款成功、订单仍是 <code>2</code>;新的 <code>refunded_amount</code> 从订单快照读</td></tr>
|
|
|
+</table>
|
|
|
+
|
|
|
+<h3>10.2 接收规则</h3>
|
|
|
+<ul>
|
|
|
+ <li>返回任意 <b>2xx</b> 即视为接收成功,响应体不限。</li>
|
|
|
+ <li>失败后每分钟重试一次,<b>最多投递 3 次</b>(含首次)。</li>
|
|
|
+ <li>用 <code>notify_id</code> 去重。重复通知直接返回 2xx。</li>
|
|
|
+ <li>回调<b>不带签名</b>。不放心时以查单或查退款的结果为准,不要只凭回调发货。</li>
|
|
|
+ <li><code>notify_url</code> 必须公网可达,平台会拒绝 <code>localhost</code>、<code>127.0.0.1</code>、内网地址。</li>
|
|
|
+</ul>
|
|
|
+
|
|
|
+<h2>11 错误码与排查</h2>
|
|
|
+
|
|
|
+<table>
|
|
|
+ <thead><tr><th style="width:12%">HTTP</th><th style="width:12%">code</th><th style="width:30%">含义</th><th>处理</th></tr></thead>
|
|
|
+ <tr><td>200</td><td><code>0</code></td><td>成功</td><td>处理 <code>data</code></td></tr>
|
|
|
+ <tr><td>200</td><td><code>400</code></td><td>业务参数不合法</td><td>看 <code>msg</code></td></tr>
|
|
|
+ <tr><td>200</td><td><code>404</code></td><td>订单或退款单不存在</td><td>核对单号;确认是同一租户</td></tr>
|
|
|
+ <tr><td>200</td><td><code>409</code></td><td>幂等键已存在且参数不一致</td><td>换单号,或先查询确认原单</td></tr>
|
|
|
+ <tr><td>200</td><td><code>500</code></td><td>上游失败或超时</td><td>不要换单号,先查单</td></tr>
|
|
|
+ <tr><td>400</td><td><code>400</code></td><td>JSON 格式错误或字段校验失败</td><td>看 <code>msg</code> 里的字段名</td></tr>
|
|
|
+ <tr><td>401</td><td><code>10401</code></td><td>认证失败</td><td>检查 <code>ApiKey </code> 前缀、key、签名</td></tr>
|
|
|
+ <tr><td>404</td><td><code>404</code></td><td>路径不存在</td><td>检查 URL 拼写</td></tr>
|
|
|
+ <tr><td>500</td><td><code>500</code></td><td>服务端异常</td><td>稍后重试;持续出现联系平台</td></tr>
|
|
|
+</table>
|
|
|
+
|
|
|
+<h3>11.1 常见问题</h3>
|
|
|
+<ul>
|
|
|
+ <li><b>一直 401:</b>最常见是签名对不上。确认 <code>Signature</code> 是对<b>实际发送的 body</b> 算的;<code>Authorization</code> 是 <code>ApiKey</code> + 一个空格 + key;<code>api_key</code> 与 <code>api_secret</code> 是同一对。</li>
|
|
|
+ <li><b>金额差 100 倍:</b>金额单位是分。<code>1.00 元</code> 传 <code>100</code>。请求体字段全部用 snake_case。</li>
|
|
|
+ <li><b>收不到通知:</b><code>notify_url</code> 要公网可达,不能是 <code>localhost</code> 或内网 IP;返回非 2xx 会重试,3 次后停止。</li>
|
|
|
+</ul>
|
|
|
+
|
|
|
+<h2>12 联调清单</h2>
|
|
|
+<ul class="check">
|
|
|
+ <li>用 1 元(<code>100</code> 分)跑通「下单 → 支付 → 查单 → 全额退款 → 查退款」,确认所有金额都是<b>分</b>。</li>
|
|
|
+ <li><code>notify_url</code> 公网可达,返回 2xx;重复通知按 <code>notify_id</code> 去重。</li>
|
|
|
+ <li>同一 <code>out_trade_no</code> 原样重放返回同一笔订单;改金额重放返回 <code>code=409</code>。</li>
|
|
|
+ <li>按业务选对 <code>way_code</code>;条码类传 <code>channel_extra.auth_code</code>;JSAPI 类先拿 <code>openid</code>。</li>
|
|
|
+ <li><code>pay_data</code> 只按 http/https 渲染;<code>form</code> 不插入现有页面。</li>
|
|
|
+ <li>退款后主动查 <code>refund/query</code>,确认 <code>state=2</code>。</li>
|
|
|
+ <li>把 <code>out_trade_no</code>、<code>out_refund_no</code> 持久化,接口超时不换号,先查单。</li>
|
|
|
+</ul>
|
|
|
+
|
|
|
+<p class="small" style="margin-top:8px; border-top:1px solid var(--line); padding-top:5px;">
|
|
|
+ 聚合支付开放接口 v1 · 2026-10-21 · 接口如有变更以平台通知为准
|
|
|
+</p>
|
|
|
+
|
|
|
+</body>
|
|
|
+</html>
|