聚合支付开放接口对接指南.html 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. <!doctype html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="utf-8">
  5. <title>聚合支付开放接口对接指南</title>
  6. <style>
  7. @page { size: A4; margin: 12mm 12.5mm 11mm 12.5mm; }
  8. :root {
  9. --ink: #1a1d21;
  10. --muted: #5b6570;
  11. --accent: #14508c;
  12. --line: #d7dde5;
  13. --soft: #f3f6fa;
  14. --warn: #b8791a;
  15. --warn-bg: #fdf7ea;
  16. }
  17. * { box-sizing: border-box; }
  18. html { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
  19. body {
  20. margin: 0;
  21. font-family: "Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", sans-serif;
  22. font-size: 8.8pt;
  23. line-height: 1.42;
  24. color: var(--ink);
  25. }
  26. .head { border-top: 4px solid var(--accent); padding-top: 8px; margin-bottom: 8px; }
  27. .kicker { font-size: 8pt; letter-spacing: 1.5px; color: var(--accent); text-transform: uppercase; }
  28. h1 { font-size: 20pt; margin: 2px 0 2px; letter-spacing: 0.5px; }
  29. .meta { font-size: 8.6pt; color: var(--muted); }
  30. .quick {
  31. display: grid;
  32. grid-template-columns: 1fr 1fr;
  33. gap: 3px 10px;
  34. background: var(--soft);
  35. border: 1px solid var(--line);
  36. border-radius: 4px;
  37. padding: 6px 10px;
  38. margin: 8px 0 4px;
  39. font-size: 8.8pt;
  40. }
  41. .quick div { white-space: nowrap; }
  42. .quick span { color: var(--muted); display: inline-block; min-width: 62px; }
  43. h2 {
  44. font-size: 11.6pt;
  45. margin: 11px 0 4px;
  46. padding-left: 7px;
  47. border-left: 3.5px solid var(--accent);
  48. break-after: avoid;
  49. }
  50. h3 { font-size: 9.6pt; margin: 8px 0 3px; color: #24343f; break-after: avoid; }
  51. p { margin: 3px 0 5px; }
  52. ul, ol { margin: 3px 0 6px; padding-left: 18px; }
  53. li { margin: 1.5px 0; }
  54. code {
  55. font-family: Consolas, "Cascadia Mono", "Courier New", monospace;
  56. font-size: 8.4pt;
  57. background: #eef1f5;
  58. padding: 0 2px;
  59. border-radius: 2px;
  60. word-break: break-word;
  61. }
  62. pre {
  63. background: var(--soft);
  64. border: 1px solid #e2e7ee;
  65. border-radius: 3px;
  66. padding: 5px 8px;
  67. margin: 4px 0 7px;
  68. font-family: Consolas, "Cascadia Mono", "Courier New", monospace;
  69. font-size: 7.4pt;
  70. line-height: 1.36;
  71. white-space: pre-wrap;
  72. word-break: break-word;
  73. break-inside: avoid;
  74. }
  75. pre code { background: none; padding: 0; font-size: inherit; }
  76. pre.long { break-inside: auto; }
  77. table {
  78. width: 100%;
  79. border-collapse: collapse;
  80. margin: 4px 0 8px;
  81. font-size: 8.1pt;
  82. }
  83. th, td { border: 1px solid var(--line); padding: 2.2px 5px; vertical-align: top; text-align: left; }
  84. th { background: #eaf0f7; font-weight: 600; white-space: nowrap; }
  85. tr { break-inside: avoid; }
  86. thead { display: table-header-group; }
  87. td code { font-size: 7.9pt; }
  88. .nowrap { white-space: nowrap; }
  89. .note {
  90. border-left: 3px solid var(--warn);
  91. background: var(--warn-bg);
  92. padding: 4px 9px;
  93. margin: 5px 0 7px;
  94. font-size: 8.9pt;
  95. break-inside: avoid;
  96. }
  97. .note b { color: var(--warn); }
  98. .cols2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
  99. .check { list-style: none; padding-left: 0; }
  100. .check li { padding-left: 16px; text-indent: -16px; margin: 2px 0; }
  101. .check li::before { content: "\2610\00a0"; color: var(--muted); }
  102. .small { font-size: 8.2pt; color: var(--muted); }
  103. .pdf-out { break-inside: avoid; }
  104. </style>
  105. </head>
  106. <body>
  107. <header class="head">
  108. <div class="kicker">商户服务端对接文档</div>
  109. <h1>聚合支付 开放接口</h1>
  110. <div class="meta">版本 1.0 · 2026-10-21 · 5 个接口 · 请求签名 HMAC-SHA256</div>
  111. </header>
  112. <div class="quick">
  113. <div><span>接口根地址</span><code>https://qcsj88888.com/api/v1</code></div>
  114. <div><span>请求方式</span><code>POST</code>,<code>application/json</code></div>
  115. <div><span>金额单位</span><code>分</code>,整数(<code>100</code> = 1.00 元)</div>
  116. <div><span>成功标志</span>响应体 <code>code = 0</code></div>
  117. </div>
  118. <h2>1 请求约定</h2>
  119. <p>所有接口用 POST 提交 JSON,响应也是 JSON。每个请求带三个请求头:</p>
  120. <table>
  121. <thead><tr><th style="width:30%">请求头</th><th>值</th></tr></thead>
  122. <tr><td><code>Content-Type</code></td><td><code>application/json</code></td></tr>
  123. <tr><td><code>Authorization</code></td><td><code>ApiKey &lt;你的 api_key&gt;</code>,注意 <code>ApiKey</code> 后面有一个空格</td></tr>
  124. <tr><td><code>Signature</code></td><td>对请求体算出的 HMAC-SHA256,64 位小写十六进制,算法见 §2</td></tr>
  125. </table>
  126. <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>
  127. <table>
  128. <thead><tr><th style="width:34%">用途</th><th>方法 + 路径(拼在根地址后面)</th></tr></thead>
  129. <tr><td>统一下单</td><td><code>POST /payment/openapi/unified/order/create</code></td></tr>
  130. <tr><td>查询订单</td><td><code>POST /payment/openapi/unified/order/query</code></td></tr>
  131. <tr><td>发起退款</td><td><code>POST /payment/openapi/unified/refund</code></td></tr>
  132. <tr><td>查询退款</td><td><code>POST /payment/openapi/unified/refund/query</code></td></tr>
  133. <tr><td>获取渠道用户 ID(仅 JSAPI / 小程序)</td><td><code>POST /payment/openapi/unified/channel-user/jump-url</code></td></tr>
  134. </table>
  135. <p><b>商户号 <code>mch_no</code>:</b>租户只有一个商户号时不传。有多个商户号时,<b>下单</b>和<b>获取渠道用户 ID</b> 必须传;查单、退款、查退款按单号寻址,不必传。</p>
  136. <h2>2 签名</h2>
  137. <p>签名只算请求体的顶层字段,与 URL、请求头、时间戳无关。</p>
  138. <ol>
  139. <li>取请求体的顶层字段。</li>
  140. <li>去掉 <code>sign</code>、<code>null</code>、空字符串、空对象 <code>{}</code>、空数组 <code>[]</code>。</li>
  141. <li>按字段名 ASCII 升序排列。</li>
  142. <li>每个值先 URL 编码(RFC 3986:空格 <code>%20</code>、星号 <code>%2A</code>、波浪号保留)。对象和数组先按键排序输出紧凑 JSON(无空格、中文不转义),再整体编码。</li>
  143. <li>用 <code>&amp;</code> 拼成 <code>k=v&amp;k=v</code>。</li>
  144. <li>以 <code>api_secret</code> 为密钥做 HMAC-SHA256,取小写十六进制,放入 <code>Signature</code> 头。</li>
  145. </ol>
  146. <pre><code>from urllib.parse import quote
  147. import hashlib, hmac, json
  148. def sign(body: dict, api_secret: str) -&gt; str:
  149. parts = []
  150. for key in sorted(body):
  151. value = body[key]
  152. if key == "sign" or value is None or value == "" or value == {} or value == []:
  153. continue
  154. if isinstance(value, (dict, list)):
  155. text = json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
  156. elif isinstance(value, bool):
  157. text = "true" if value else "false"
  158. else:
  159. text = str(value)
  160. parts.append(key + "=" + quote(text, safe=""))
  161. raw = "&amp;".join(parts)
  162. return hmac.new(api_secret.encode(), raw.encode(), hashlib.sha256).hexdigest()
  163. </code></pre>
  164. <div class="note"><b>密钥只在服务端使用。</b>不要把 <code>api_secret</code> 写进浏览器、App 或前端仓库。签名算的是你实际发出的那个请求体,改了任何字段都要重算。</div>
  165. <h2>3 下单</h2>
  166. <h3>3.1 必填参数</h3>
  167. <table>
  168. <thead><tr><th style="width:18%">字段</th><th style="width:11%">类型</th><th>说明</th></tr></thead>
  169. <tr><td><code>out_trade_no</code></td><td>string(64)</td><td>你的订单号,<b>幂等键</b>。同一租户内唯一。</td></tr>
  170. <tr><td><code>amount</code></td><td>integer</td><td>金额,单位<b>分</b>,必须大于 0。</td></tr>
  171. <tr><td><code>way_code</code></td><td>string(16)</td><td>支付方式,见 §4。</td></tr>
  172. <tr><td><code>subject</code></td><td>string(128)</td><td>商品标题。</td></tr>
  173. <tr><td><code>mch_no</code></td><td>string(64)</td><td>多商户时必传;单商户可空。</td></tr>
  174. </table>
  175. <h3>3.2 可选参数</h3>
  176. <table>
  177. <thead><tr><th style="width:18%">字段</th><th style="width:11%">类型</th><th>说明</th></tr></thead>
  178. <tr><td><code>body</code></td><td>string(512)</td><td>商品描述。不传时用 <code>subject</code>。</td></tr>
  179. <tr><td><code>notify_url</code></td><td>string(512)</td><td>异步通知地址,必须是公网可访问的 http/https 地址。见 §10。</td></tr>
  180. <tr><td><code>return_url</code></td><td>string(512)</td><td>支付完成后浏览器跳回的地址(部分支付方式使用)。</td></tr>
  181. <tr><td><code>expired_time</code></td><td>integer</td><td>订单有效期,单位秒,默认 7200(2 小时)。</td></tr>
  182. <tr><td><code>client_ip</code></td><td>string(64)</td><td>用户终端 IP。不传时取请求来源 IP。</td></tr>
  183. <tr><td><code>ext_param</code></td><td>string(512)</td><td>透传参数,上游原样返回。</td></tr>
  184. <tr><td><code>channel_extra</code></td><td>object</td><td>支付方式附加参数,见 §4。</td></tr>
  185. </table>
  186. <h3>3.3 完整调用示例</h3>
  187. <pre><code>import requests, uuid
  188. BASE = "https://qcsj88888.com/api/v1"
  189. API_KEY = "你的 api_key"
  190. API_SECRET = "你的 api_secret"
  191. def post(path: str, body: dict) -&gt; dict:
  192. headers = {
  193. "Content-Type": "application/json",
  194. "Authorization": "ApiKey " + API_KEY,
  195. "Signature": sign(body, API_SECRET), # sign() 见 §2
  196. }
  197. resp = requests.post(BASE + path, json=body, headers=headers, timeout=15)
  198. return resp.json()
  199. body = {
  200. "out_trade_no": "T" + uuid.uuid4().hex[:16],
  201. "amount": 100, # 分
  202. "way_code": "QR_CASHIER",
  203. "subject": "测试商品",
  204. "notify_url": "https://example.com/pay/notify",
  205. }
  206. print(post("/payment/openapi/unified/order/create", body))
  207. </code></pre>
  208. <h3>3.4 成功响应</h3>
  209. <pre><code>{
  210. "code": 0,
  211. "msg": "成功",
  212. "data": {
  213. "order_no": "UP1960000000000000001",
  214. "out_trade_no": "T20261020001",
  215. "way_code": "QR_CASHIER",
  216. "amount": 100,
  217. "refundable_amount": 100,
  218. "trade_status": "0",
  219. "pay_data_type": "codeUrl",
  220. "pay_data": "https://qr.example.com/xxx",
  221. "created_time": "2026-10-20T12:00:00+08:00"
  222. }
  223. }
  224. </code></pre>
  225. <p><code>order_no</code> 是平台订单号。<code>out_trade_no</code> 是你的单号,后续查单、退款都用它。把 <code>pay_data</code> 交给用户完成支付,渲染方式见 §4.2。</p>
  226. <h2>4 支付方式与 pay_data</h2>
  227. <h3>4.1 支持的 way_code</h3>
  228. <table>
  229. <thead><tr><th style="width:15%">way_code</th><th style="width:34%">场景</th><th>channel_extra</th></tr></thead>
  230. <tr><td><code>QR_CASHIER</code></td><td>聚合扫码,用户扫你</td><td>可选 <code>payDataType</code>:<code>codeUrl</code> / <code>codeImgUrl</code></td></tr>
  231. <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>
  232. <tr><td><code>ALI_JSAPI</code></td><td>支付宝生活号</td><td>必传 <code>{"buyerUserId":"..."}</code></td></tr>
  233. <tr><td><code>WX_JSAPI</code></td><td>微信公众号</td><td>必传 <code>{"openid":"..."}</code>;服务商模式加 <code>wxAppId</code></td></tr>
  234. <tr><td><code>WX_LITE</code></td><td>微信小程序</td><td>必传 <code>{"openid":"..."}</code>;服务商模式加 <code>wxAppId</code></td></tr>
  235. <tr><td><code>YSF_JSAPI</code></td><td>云闪付 JSAPI</td><td>可选 <code>{"openid":"..."}</code></td></tr>
  236. <tr><td><code>ALI_APP</code> <code>WX_APP</code></td><td>原生 APP 支付(支付宝 / 微信)</td><td>不用传</td></tr>
  237. <tr><td><code>ALI_WAP</code></td><td>支付宝手机网站</td><td>可选 <code>payDataType</code>:<code>form</code> / <code>codeImgUrl</code> / <code>payUrl</code></td></tr>
  238. <tr><td><code>ALI_PC</code></td><td>支付宝电脑网站</td><td>可选 <code>payDataType</code>:<code>form</code> / <code>payUrl</code></td></tr>
  239. <tr><td><code>ALI_QR</code></td><td>支付宝二维码</td><td>可选 <code>payDataType</code>:<code>codeUrl</code> / <code>codeImgUrl</code></td></tr>
  240. <tr><td><code>WX_NATIVE</code></td><td>微信扫码</td><td>可选 <code>payDataType</code>:<code>codeUrl</code> / <code>codeImgUrl</code></td></tr>
  241. <tr><td><code>WX_H5</code></td><td>微信 H5</td><td>可不传;已拿到 <code>openid</code> 时可传</td></tr>
  242. </table>
  243. <p><code>way_code</code> 大小写敏感。条码类的付款码可写 <code>auth_code</code>,也可写 <code>authCode</code>。付款码、<code>openid</code>、<code>buyerUserId</code> 只用于本次请求:平台不落库、不写日志。</p>
  244. <h3>4.2 pay_data 怎么用</h3>
  245. <table>
  246. <thead><tr><th style="width:16%">pay_data_type</th><th style="width:30%">pay_data 内容</th><th>用法</th></tr></thead>
  247. <tr><td><code>codeUrl</code></td><td>支付链接</td><td>生成二维码给用户扫</td></tr>
  248. <tr><td><code>codeImgUrl</code></td><td>二维码图片 URL</td><td>放进 <code>&lt;img src&gt;</code></td></tr>
  249. <tr><td><code>payUrl</code></td><td>支付页链接</td><td>浏览器跳转</td></tr>
  250. <tr><td><code>form</code></td><td>HTML 表单字符串</td><td>作为独立页面提交;不要插进现有页面</td></tr>
  251. <tr><td><code>wxapp</code></td><td>JSON 字符串,微信支付参数</td><td><code>JSON.parse</code> 后传给 <code>WeixinJSBridge.invoke('getBrandWCPayRequest', ...)</code></td></tr>
  252. </table>
  253. <div class="note"><b>只渲染 http/https 地址。</b>把 <code>pay_data</code> 当数据,不要当代码执行。<code>form</code> 是完整表单 HTML,插入页面会破坏现有 DOM。</div>
  254. <h2>5 幂等与失败重试</h2>
  255. <ul>
  256. <li><b>同一 <code>out_trade_no</code></b>,金额、<code>way_code</code>、<code>subject</code> 全部相同:返回原订单,<code>code=0</code>,不会重复扣款。</li>
  257. <li><b>同一 <code>out_trade_no</code></b>,以上任一项不同:返回 <code>code=409</code>「out_trade_no 已存在且参数不一致」。换单号再下。</li>
  258. <li><b><code>code=500</code> 且 msg 以「本地订单已保留」开头</b>:上游调用失败或超时,这笔<b>可能已经成功</b>。不要换单号重下,先查单。确认未支付且没有可用的 <code>pay_data</code> 后,用<b>新的</b> <code>out_trade_no</code> 重新下单。</li>
  259. <li>退款同理:<code>out_refund_no</code> 是幂等键,同一单号参数不同返回 <code>code=409</code>。</li>
  260. <li>调用前先把 <code>out_trade_no</code> 持久化。所有网络超时、连接中断,一律先查单再决定。</li>
  261. </ul>
  262. <h2>6 查询订单</h2>
  263. <pre><code>{"out_trade_no": "T20261020001"}</code></pre>
  264. <p>也可以传 <code>{"order_no": "UP1960000000000000001"}</code>。两个都传时以 <code>order_no</code> 为准。响应的 <code>data</code> 与下单相同。</p>
  265. <table>
  266. <thead><tr><th style="width:14%">trade_status</th><th>含义</th></tr></thead>
  267. <tr><td><code>0</code></td><td>待支付</td></tr>
  268. <tr><td><code>1</code></td><td>支付中</td></tr>
  269. <tr><td><code>2</code></td><td>支付成功(之后可能变成 <code>5</code> 已退款)</td></tr>
  270. <tr><td><code>3</code></td><td>支付失败</td></tr>
  271. <tr><td><code>4</code></td><td>已撤销</td></tr>
  272. <tr><td><code>5</code></td><td>已退款</td></tr>
  273. <tr><td><code>6</code></td><td>已关闭</td></tr>
  274. </table>
  275. <p><code>3</code> / <code>4</code> / <code>5</code> / <code>6</code> 是终态。建议每 3 秒查一次,到终态停止。支付结果以查单或异步通知中的 <code>trade_status</code> 为准。</p>
  276. <h2>7 发起退款</h2>
  277. <pre><code>{
  278. "out_trade_no": "T20261020001",
  279. "out_refund_no": "R20261020001",
  280. "refund_amount": 100,
  281. "refund_reason": "测试退款",
  282. "notify_url": "https://example.com/refund/notify"
  283. }</code></pre>
  284. <p>定位订单用 <code>order_no</code> 或 <code>out_trade_no</code>,至少传一个,都传时以 <code>order_no</code> 为准。<code>mch_no</code> 可选,传了必须与订单一致。</p>
  285. <table>
  286. <thead><tr><th style="width:18%">字段</th><th style="width:11%">类型</th><th>说明</th></tr></thead>
  287. <tr><td><code>out_refund_no</code></td><td>string(64)</td><td>你的退款单号,<b>幂等键</b>。</td></tr>
  288. <tr><td><code>refund_amount</code></td><td>integer</td><td>退款金额,分。</td></tr>
  289. <tr><td><code>refund_reason</code></td><td>string(64)</td><td>退款原因,必填。</td></tr>
  290. <tr><td><code>notify_url</code></td><td>string(512)</td><td>本笔退款的通知地址,可选。</td></tr>
  291. </table>
  292. <ul>
  293. <li>只有 <code>trade_status=2</code> 的订单能退款。</li>
  294. <li><code>refund_amount</code> 不能超过 <code>refundable_amount</code>。在途退款也占用额度。</li>
  295. <li>同一订单可以多次部分退款,每次换一个 <code>out_refund_no</code>。</li>
  296. </ul>
  297. <pre><code>{
  298. "code": 0,
  299. "msg": "成功",
  300. "data": {
  301. "refund_no": "UR1960000000000000001",
  302. "out_refund_no": "R20261020001",
  303. "order_no": "UP1960000000000000001",
  304. "mch_no": "M202610200001",
  305. "amount": 100,
  306. "state": "1",
  307. "created_time": "2026-10-20T12:05:00+08:00"
  308. }
  309. }
  310. </code></pre>
  311. <table>
  312. <thead><tr><th style="width:12%">state</th><th>含义</th></tr></thead>
  313. <tr><td><code>0</code></td><td>退款生成</td></tr>
  314. <tr><td><code>1</code></td><td>退款中</td></tr>
  315. <tr><td><code>2</code></td><td>退款成功</td></tr>
  316. <tr><td><code>3</code></td><td>退款失败</td></tr>
  317. <tr><td><code>4</code></td><td>退款关闭</td></tr>
  318. </table>
  319. <p><code>2</code> / <code>3</code> / <code>4</code> 是终态。</p>
  320. <h2>8 查询退款</h2>
  321. <pre><code>{"out_refund_no": "R20261020001"}</code></pre>
  322. <p>也可以传 <code>{"refund_no": "UR1960000000000000001"}</code>。两个都传时以 <code>refund_no</code> 为准。响应与退款相同。不要只等通知:主动查到 <code>state=2</code> 再确认退款成功。</p>
  323. <h2>9 获取渠道用户 ID(JSAPI / 小程序)</h2>
  324. <p><code>WX_JSAPI</code>、<code>WX_LITE</code>、<code>ALI_JSAPI</code> 需要先拿渠道用户 ID。<code>YSF_JSAPI</code> 可选。上游签名要用 <code>api_secret</code>,所以这一步必须由服务端换取 URL,浏览器不接触密钥。</p>
  325. <pre><code>{
  326. "mch_no": "M202610200001",
  327. "redirect_url": "https://example.com/channel/callback",
  328. "ext_param": "order=1"
  329. }</code></pre>
  330. <pre><code>{"code": 0, "msg": "成功", "data": {"url": "https://...签名后的上游地址"}}</code></pre>
  331. <p>把用户浏览器跳到返回的 <code>url</code>。授权完成后,<code>redirect_url</code> 会带上 <code>channelUserId</code>(微信还会带 <code>channelAppId</code>):</p>
  332. <ul>
  333. <li>微信:把 <code>channelUserId</code> 作为 <code>openid</code> 放进下单的 <code>channel_extra</code>;服务商模式再把 <code>channelAppId</code> 作为 <code>wxAppId</code>。</li>
  334. <li>支付宝:把 <code>channelUserId</code> 作为 <code>buyerUserId</code> 放进下单的 <code>channel_extra</code>。</li>
  335. </ul>
  336. <p><code>if_code</code> 不用传,平台固定使用 <code>fuioupay</code>。</p>
  337. <h2>10 异步通知</h2>
  338. <p>平台用 <b>form POST</b>(<code>application/x-www-form-urlencoded</code>)推送,固定三个字段:</p>
  339. <table>
  340. <thead><tr><th style="width:18%">字段</th><th>说明</th></tr></thead>
  341. <tr><td><code>notify_id</code></td><td>事件 ID。同一事件重试时不变,用它去重。</td></tr>
  342. <tr><td><code>timestamp</code></td><td>毫秒时间戳。</td></tr>
  343. <tr><td><code>content</code></td><td>JSON 字符串,结构见下。</td></tr>
  344. </table>
  345. <p>支付事件(<code>event=ORDER</code>):</p>
  346. <pre class="long"><code>{
  347. "event": "ORDER",
  348. "order": {
  349. "order_no": "UP1960000000000000001",
  350. "out_trade_no": "T20261020001",
  351. "mch_no": "M202610200001",
  352. "way_code": "QR_CASHIER",
  353. "amount": 100,
  354. "refunded_amount": 0,
  355. "refundable_amount": 100,
  356. "trade_status": "2",
  357. "upstream_pay_order_id": "19522112587029827620865",
  358. "upstream_channel_order_no": "4200001234202610200000000000",
  359. "pay_time": "2026-10-20T12:00:00+08:00",
  360. "err_code": null,
  361. "err_msg": null
  362. },
  363. "upstream": {}
  364. }
  365. </code></pre>
  366. <p>退款事件(<code>event=REFUND</code>):</p>
  367. <pre class="long"><code>{
  368. "event": "REFUND",
  369. "refund": {
  370. "refund_no": "UR1960000000000000001",
  371. "out_refund_no": "R20261020001",
  372. "order_no": "UP1960000000000000001",
  373. "amount": 100,
  374. "state": "2",
  375. "upstream_refund_order_id": "...",
  376. "success_time": "2026-10-20T12:06:00+08:00",
  377. "err_code": null,
  378. "err_msg": null
  379. },
  380. "order": { "...": "订单当前快照,字段同 ORDER 事件" },
  381. "upstream": {}
  382. }
  383. </code></pre>
  384. <p><code>upstream</code> 是上游回调字段的白名单投影(<code>payOrderId</code>、<code>state</code>、<code>channelOrderNo</code> 等),没有快照时是空对象。</p>
  385. <h3>10.1 推送时机</h3>
  386. <table>
  387. <thead><tr><th style="width:42%">事件</th><th>什么时候推</th></tr></thead>
  388. <tr><td><code>ORDER</code></td><td>订单状态变成 <code>2</code> / <code>3</code> / <code>4</code> / <code>6</code></td></tr>
  389. <tr><td><code>ORDER</code> + <code>REFUND</code></td><td>全额退款导致订单变成 <code>5</code>,两条都推</td></tr>
  390. <tr><td><code>REFUND</code></td><td>退款状态变成 <code>2</code> / <code>3</code> / <code>4</code></td></tr>
  391. <tr><td>不推</td><td>订单 <code>0</code> / <code>1</code>、退款 <code>0</code> / <code>1</code></td></tr>
  392. <tr><td>只推 <code>REFUND</code></td><td>部分退款成功、订单仍是 <code>2</code>;新的 <code>refunded_amount</code> 从订单快照读</td></tr>
  393. </table>
  394. <h3>10.2 接收规则</h3>
  395. <ul>
  396. <li>返回任意 <b>2xx</b> 即视为接收成功,响应体不限。</li>
  397. <li>失败后每分钟重试一次,<b>最多投递 3 次</b>(含首次)。</li>
  398. <li>用 <code>notify_id</code> 去重。重复通知直接返回 2xx。</li>
  399. <li>回调<b>不带签名</b>。不放心时以查单或查退款的结果为准,不要只凭回调发货。</li>
  400. <li><code>notify_url</code> 必须公网可达,平台会拒绝 <code>localhost</code>、<code>127.0.0.1</code>、内网地址。</li>
  401. </ul>
  402. <h2>11 错误码与排查</h2>
  403. <table>
  404. <thead><tr><th style="width:12%">HTTP</th><th style="width:12%">code</th><th style="width:30%">含义</th><th>处理</th></tr></thead>
  405. <tr><td>200</td><td><code>0</code></td><td>成功</td><td>处理 <code>data</code></td></tr>
  406. <tr><td>200</td><td><code>400</code></td><td>业务参数不合法</td><td>看 <code>msg</code></td></tr>
  407. <tr><td>200</td><td><code>404</code></td><td>订单或退款单不存在</td><td>核对单号;确认是同一租户</td></tr>
  408. <tr><td>200</td><td><code>409</code></td><td>幂等键已存在且参数不一致</td><td>换单号,或先查询确认原单</td></tr>
  409. <tr><td>200</td><td><code>500</code></td><td>上游失败或超时</td><td>不要换单号,先查单</td></tr>
  410. <tr><td>400</td><td><code>400</code></td><td>JSON 格式错误或字段校验失败</td><td>看 <code>msg</code> 里的字段名</td></tr>
  411. <tr><td>401</td><td><code>10401</code></td><td>认证失败</td><td>检查 <code>ApiKey </code> 前缀、key、签名</td></tr>
  412. <tr><td>404</td><td><code>404</code></td><td>路径不存在</td><td>检查 URL 拼写</td></tr>
  413. <tr><td>500</td><td><code>500</code></td><td>服务端异常</td><td>稍后重试;持续出现联系平台</td></tr>
  414. </table>
  415. <h3>11.1 常见问题</h3>
  416. <ul>
  417. <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>
  418. <li><b>金额差 100 倍:</b>金额单位是分。<code>1.00 元</code> 传 <code>100</code>。请求体字段全部用 snake_case。</li>
  419. <li><b>收不到通知:</b><code>notify_url</code> 要公网可达,不能是 <code>localhost</code> 或内网 IP;返回非 2xx 会重试,3 次后停止。</li>
  420. </ul>
  421. <h2>12 联调清单</h2>
  422. <ul class="check">
  423. <li>用 1 元(<code>100</code> 分)跑通「下单 → 支付 → 查单 → 全额退款 → 查退款」,确认所有金额都是<b>分</b>。</li>
  424. <li><code>notify_url</code> 公网可达,返回 2xx;重复通知按 <code>notify_id</code> 去重。</li>
  425. <li>同一 <code>out_trade_no</code> 原样重放返回同一笔订单;改金额重放返回 <code>code=409</code>。</li>
  426. <li>按业务选对 <code>way_code</code>;条码类传 <code>channel_extra.auth_code</code>;JSAPI 类先拿 <code>openid</code>。</li>
  427. <li><code>pay_data</code> 只按 http/https 渲染;<code>form</code> 不插入现有页面。</li>
  428. <li>退款后主动查 <code>refund/query</code>,确认 <code>state=2</code>。</li>
  429. <li>把 <code>out_trade_no</code>、<code>out_refund_no</code> 持久化,接口超时不换号,先查单。</li>
  430. </ul>
  431. <p class="small" style="margin-top:8px; border-top:1px solid var(--line); padding-top:5px;">
  432. 聚合支付开放接口 v1 · 2026-10-21 · 接口如有变更以平台通知为准
  433. </p>
  434. </body>
  435. </html>