XD-OP-02 · DOC

接入指南

从商户进件、申请开放平台应用到拿到接入地址与调用凭证,再到事件通知与域名归属配置——按这条顺序走,不需要先写代码就能把接入前置办齐。

小豆支付开放平台是面向已有自有系统的商家的支付接入服务:通过服务器对服务器的接口,把支付宝与微信收款接进商家自己的系统,并以签名事件通知回传支付状态;资金由持牌支付公司收付结算。个人也能申请:主体类型选「个人」,身份证 + 本人银行卡即可完成商户进件。央行持牌机构清算,安全可靠的个人和企业支付接口。

本篇讲「怎么接得对」:请求怎么签、幂等怎么做、状态怎么判、收银与交付的边界在哪。字段清单见 API 参考。

1. 域名与 Base URL

  • 商家 API 的 Base URL 只在登录后的门户「应用凭证」页下发(同一页还有 appId、kid 与指纹);文档与示例一律写作 <BASE_URL>,官方资料里不印 host,也不要在公开渠道互相转抄。取不到时请联系对接人。
  • 不要在服务端代码里硬编码 host/IP/端口,也不要在前端拼接——把 Base URL 当配置项管理,环境切换时只改配置。
  • 平台收银页地址永远使用下发响应里的 payUrl 原值,商家不得自行拼接收银地址。

2. 域名归属验证(可选,建议完成)

平台不强制核验域名归属:申报域名由你自己填写,不做归属验证也能开通应用并收款。建议顺手完成一次核验——核验通过后该域名在平台侧标记为「已验证」,域名被他人冒用申报时归属更明确,处理争议时更有依据;核验也是免费的,两个形态二选一,整条流程在门户完成。

形态你需要做什么适用情形
DNS TXT 记录(推荐)在域名的解析服务商处添加一条 TXT 记录你能改域名解析(绝大多数情况)
网站回源文件在网站根目录放一个验证文件改不了解析、但能改站点内容

2.1 DNS TXT 记录形态

门户签发的指引里会给出两个值:记录名与记录值。

  • 记录名(完整):_xiaodou-open-api.<你申报的域名>,例如申报 pay.example.com 时是 _xiaodou-open-api.pay.example.com。
  • 记录值:门户上那一串验证码(xdch_ 开头),逐字符复制。

注意解析商的「主机记录」栏:国内多数解析服务商(DNSPod 等)要求这一栏只填主域名之前的部分,不是完整记录名。同一例子(申报 pay.example.com)在这些服务商里应填的是 _xiaodou-open-api;若你把完整记录名填进去,实际生成的记录会变成 _xiaodou-open-api.pay.example.com.pay.example.com,验证永远不通过。门户指引里会按你申报的域名直接给出这一栏该填的值,照抄即可。

2.2 网站回源文件形态

  • 在站点根目录放置验证文件,使门户给出的地址(形如 https://<你申报的域名>/.well-known/xiaodou-open-api-challenge)返回的内容包含验证码即可通过(判据是「页面内容含该验证码」;原样只返回验证码是最稳妥的做法,可避免框架附加内容引入变量)。
  • 该地址必须满足三条:可公网访问、走 HTTPS、不跳转(不能 302 到别处,也不能先跳登录页)。

2.3 生效时间与常见失败

现象含义你要做的
提示「暂未读取到验证记录」(唯一可重试的失败)平台按当前 DNS/站点状态还没读到你的配置解析/部署生效通常需要几分钟,稍后重新点一次验证即可(同一条验证最多 5 次,每次只统计「没读到」)
提示「已读取到配置内容,但与本页验证码不一致」记录填了,但值不对(常见:复制时带了空格、换行,或填了上一次的验证码)该条验证就此作废(终态):重新签发 → 拿到新验证码 → 覆盖记录值 → 再验证。直接点「验证」只会拿到 409
提示「目标域名解析到非公网地址」域名指向了内网/保留地址同属终态:先修好解析,再重新签发验证(平台不会去访问非公网目标)
提示「多次验证未通过,该验证已关闭」尝试次数用尽(安全约束)在门户重新签发一条新的验证
提示「该验证已过期」超出门户显示的有效期(24 小时)重新签发
  • 上表的提示文案是大意——按失败原因分类,措辞以门户当场显示的为准,不要按字面去比对。
  • 只有「暂未读取到验证记录」能原地重试;其余失败都是终态,必须重新签发(且新验证码要重新覆盖到 DNS 记录或回源文件)。每次失败门户都会在该条记录旁边给出具体原因,按上表处理。
  • 申请时可以先不填域名:开通后在卖家门户的「应用配置变更」里补上你自己的域名(那里至少要填一条,但同样不需要核验);也可以申请时就填,是否核验都不影响开通与生效(域名归属不是判据)。
  • 单次申报最多 10 个域名(超出会与「格式非法」共用同一句 400 文案,请先自查数量)。
  • 域名在本平台不是运行时访问控制:平台不会因为某个域名未验证而拒绝你发起的支付或回调。

3. 鉴权总览

方向机制关键材料
商家 → 平台XD-Signature-v1 六头签名(RSA-SHA256)你的 RSA2 私钥、appId、kid
平台 → 商家(响应)XD-Response-v1 响应头签名平台公钥(固定文档页与门户双渠道发布)
平台 → 商家(事件)XD-Webhook-v1 投递头签名平台公钥
  • 六头成组提交,缺一个即 400;验签失败统一返回中性 401(code 一致,但 message 会点出时间窗越界这类原因,排障时先看它);nonce 复用是 409,不是 401。
  • 时间窗与 nonce 防重放是强校验:服务器时间必须与标准时间对齐(建议常开 NTP)。
  • 响应签名与事件签名的规范串与请求签名不同,不要复用同一段拼串逻辑 —— 见 安全红线 与 webhook 验签。

3.1 平台公钥与指纹(公示)

平台用同一把 RSA2 密钥对两类出站物签名:同步响应(XD-Response-v1)与事件投递(XD-Webhook-v1)。公钥 PEM 正文在本页公示;门户「应用凭证」页公示同一把钥的 kid 与指纹(与下方 kid/指纹逐字符一致,两处同源、可相互核对)。

项值
密钥标识(kid)xdpk-2026-09
指纹(规范化 PEM 字节的 SHA-256,十六进制小写)c828f7e47c2eef5a958aa32009f5151bda43b585c979b40bf7c795b0b48d35f4
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw9Z3B6q8jsaNu3KmswGF
Kl/8Rc3dWV2yFCq61VIbuPVEQPI6S1S2gbFeWpXz6KzrdAJxTDyMqfSDnSStoesn
t6FCMjgc0SBflQOjxsM2ahNDJO4JmJGj7Mca/nFCMrvVTy4tLxFkxMUxOcXhn4LU
QPCvpoIc7Y10xHBrqVizrHwMWNPxteWsOG4ZQUbXDrYddsceTcZ3a46cBhBUyqas
lNkWwzjaw9KmrdcXWOjDT3YCyMAXkcjLBh/7gr+sb11P4PUCjeQl2F6dDASIjZmO
EXFRSQJtXSRlna+WHWgVSFtrWBdyq+Gf+5pwjXMk1PrSWIG56QKhCXKCylNz8Kwn
TQIDAQAB
-----END PUBLIC KEY-----

用法要点:

  • 只从本页或门户取公钥;不要在未验签的响应或事件里取公钥(那等于让攻击者自己递钥匙)。
  • 验签时按响应/事件头里的 key-id 选中对应公钥;kid 与上面不一致时先到本页确认是否有新版本。
  • 指纹用于本地自检:算之前先按下面的规则规范化,再取 SHA-256(十六进制小写),与上表逐字符比对,可确认没抄错、没被中间环节替换:
  1. 把 PEM 文本里的 CRLF(\r\n)与孤立 CR(\r)统一成 LF(\n);
  2. 去掉首尾空白(含末尾换行);
  3. 对这段 UTF-8 文本字节求 SHA-256。 照抄上面的 PEM 块时,不要把文件末尾的换行一起算进去——那会得到另一个值,让你误以为「被换了钥匙」。
  • 轮换:平台更换签名密钥时,会同时发布新的 kid 与新公钥/指纹;新旧并存期内,请同时保留两把公钥并按 kid 选择。参考实现见 undefined(在公开仓与「下载接入包」内;不在「下载文档包」内)。

4. 幂等:三层,别混用

层键语义
请求级X-XD-Request-Id同 (appId, 方法, 路径, requestId) 绑定一次请求体摘要;重放原样返回首次响应;同 requestId 换请求体 → 409 冲突
业务级outTradeNo / outRefundNo同号同内容 → 幂等重放(200 + replayed=true);同号异内容 → 409 冲突
状态机订单/退款状态非法迁移或终态冲突 → 409

实践建议:

  • 重试必须复用同一个 requestId(网络超时、5xx 重试场景)。换 requestId 重发等于新请求,可能造成重复下单。注意请求体也要逐字节相同:同一个 requestId 换了请求体(哪怕只是「不传 body」变成传 {})会拿到 409 OPEN_API_REQUEST_ID_CONFLICT。
  • 「同号同内容」里的内容 = 业务摘要:channel、tradeScope、amountFen、currency、subject、description、returnUrl、attach(完整清单见 API 参考 §2.7)——改动其中任何一个再重试,都是「异内容」→ 409。payerClientIp 不在摘要内,重试时按实际情况传不会冲突。
  • outTradeNo 由你的系统生成,不要用「时间戳+随机数」当订单号后重试——重试要用同一个号。
  • 遇到 409 先查单,不要盲目换号重试。

5. 状态与终态可信边界

订单状态:CREATED → PAYING → SUCCEEDED,或收敛到 CLOSED / FAILED;另有 UNKNOWN(状态不确定,不判死)。退款状态:REQUESTED → PENDING → SUCCEEDED / FAILED,另有 UNKNOWN。

CLOSED / FAILED 不是终点(最容易写错的一条):支付机构随后确认到账时,平台会把订单提升为 SUCCEEDED 并推送 PAYMENT_SUCCEEDED——关单与到账竞态一律以支付机构为准。所以这两个状态不能建成吸收态,收到 PAYMENT_SUCCEEDED 就按到账处理。

可信边界(最容易踩的一条):

  • ❌ 收银页被打开、收银页回跳 returnUrl、用户点了「完成」——都不是支付终态。
  • ✅ 终态只认:查单接口返回的订单状态,或经验签的事件通知。

UNKNOWN 的处理纪律:UNKNOWN 表示「平台与支付机构之间的结果暂时不可知」,不是失败。遇到 UNKNOWN:不要重新下单、不要自行判失败、不要给买家提示「支付失败」;按查单/事件收敛即可(补偿策略见 退款与对账)。

6. 通道与交易范围申报

  • channel 是你要走哪条收款通道(枚举见 API 参考);通道在下单时冻结,订单创建后不可改。
  • tradeScope 是交易形态申报:双申报的应用必填,单申报应用可省略(省略即取该申报形态)。申报与实际不符会被拒(422)。
  • 通道存在但链路未放行时返回 422 OPEN_API_CHANNEL_UNAVAILABLE——这是可预期的正常拒绝,不要按故障处理,也不要重试刷量。
  • 放行粒度是「通道 × 交易形态」:微信 JSAPI 已开放(仅微信内打开收银页可调起;微信外手机浏览器与商家 App 内没有直接拉起形态);其他通道或交易形态可能尚未放行。接入前请与对接人确认你要用的那个组合是否已放行;平台侧没有自助查询面,也不要靠试探下单去确认。

7. 收银与交付的边界

  • 平台负责:收款、支付事实、状态与事件。
  • 商家负责:交付本身(卡密发放、服务开通、发货、开票、售后)。
  • 收银页在买家点「返回商家」时把浏览器 303 跳回你下单时冻结的 returnUrl——它不是自动跳转:买家不点、刷新过页面时都不会回跳;按钮由收银页本次会话首次观察到支付成功时提供,扫码支付(PC 出码 → 手机付款)不受此限(只要收银页保持打开并轮询到支付成功,按钮照常提供并可回跳)。而且这是一次导航,不是终态通知,你的落地页必须自己再查一次单或等事件。
  • 微信内(JSAPI)支付完成后微信会接管完成页:买家从微信支付记录中的「商家小票」点「返回商家」回到同一个 returnUrl(平台侧配置,无需商家开通;同样只是导航,不是终态)。
  • 不要把交付内容(卡密、下载链接、账号密码)塞进 returnUrl 的 query 里:它会被浏览器历史、日志、Referer 一路带走。交付只在你的服务端用订单状态驱动。

8. 商家自有 App / 宿主内的硬约束

如果你的买家在你自己的 App(WebView/内嵌浏览器)里打开收银链:

  • 必须用系统浏览器打开(等价「跳转外部浏览器」语义),禁止在自有 App 的 WebView 内嵌打开收银页。
  • 原因:内嵌容器的 UA 与浏览器特性会失真,平台侧按终端分派收款形态的判断会错,买家会落到一个拉不起来的页面。
  • App 内的微信支付仍然走推荐路径(把链接送进微信内打开),平台侧没有「App 内直接拉起微信支付」的形态。

9. 时间与金额

  • 金额:整数分,字段后缀 Fen;仅人民币。
  • 时间:接口返回的时间字段是 ISO-8601 UTC;payUrlExpireHint / 事件 occurredAt 是 UTC epoch 秒。
  • 判断「是否过期」以服务端返回的 expireAt 为准,不要在客户端用本地时间自算窗口。

10. 联调支持边界(重要)

平台暂不提供沙箱、测试应用与回调模拟器。可用的替代路径:

  1. 本地 demo(undefined,只在公开仓提供;不在门户的两个下载包内):loopback + mock,全链跑通,不连生产支付机构。
  2. 上线检查清单(undefined):逐条自检,上线前必须全过。
  3. 首次生产首单人工陪跑:通过官网反馈渠道提交,平台侧协助(不承诺 SLA)。

本地 demo 与任何「mock 通过」都不能替代生产首单验证:mock 只证明你的签名与解析没问题,不证明资金链通。

内容源:open-platform/docs/02-integration-guide.md · 最后更新 2026-10-17 下载本文 .md 在 GitHub 查看