浦云支付商户对接文档
本文档只描述当前生产系统已经实现并开放给商户的能力:大额支付宝、易安支付宝/微信、数字货币收银台、USDT-TRC20 的统一下单、订单查询和支付结果通知。
1. 系统概览
商户系统提交签名订单,浦云支付根据该商户后台已勾选授权的通道创建上游订单,并返回可跳转的支付地址。支付完成后,浦云支付向商户的 notifyUrl 推送签名通知。
payData。2 为准。2. 接入准备
- 登录商户平台:https://user.puyunpay.com/。
- 获取商户号
mchNo、应用 IDappId和应用密钥appSecret。 - 确认管理员已给该商户勾选需要的支付通道。
- 如需限制调用来源,在商户端个人中心的“API白名单”中填写商户服务器公网 IP;同一商户下所有 AppId 共用该白名单。
- 准备公网可访问的 HTTPS 异步通知地址。
- 在商户服务端实现签名、验签、订单幂等和主动查单。
clientIp 不能代替服务器真实来源 IP。3. 请求与响应规则
| 项目 | 要求 |
|---|---|
| 生产网关 | https://pay.125.mom |
| 数字货币收银台 | https://uapi.125.mom/i/{invoiceId},统一下单成功后由系统在 payData 中返回完整地址 |
| 编码 | UTF-8 |
| 下单方式 | POST,推荐 application/json;系统也支持表单参数 |
| 查询方式 | POST 或 GET;生产建议统一使用 POST JSON |
| 金额 | 整数,单位为分。例如人民币 500 元传 50000 |
| 时间 | 13位毫秒时间戳字符串,例如 1787078400000 |
| 版本 | version=1.0 |
| 签名 | signType=MD5,签名输出大写 |
公共请求参数
| 字段 | 必填 | 类型 | 说明 |
|---|---|---|---|
mchNo | 是 | String | 商户号 |
appId | 是 | String | 商户应用 ID,必须属于该商户 |
reqTime | 是 | String | 请求毫秒时间戳 |
version | 是 | String | 固定 1.0 |
signType | 是 | String | 固定 MD5 |
sign | 是 | String | 32位大写 MD5 签名 |
公共响应结构
{
"code": 0,
"msg": "SUCCESS",
"data": {},
"sign": "响应 data 的 MD5 签名"
}code=0 仅表示本次接口处理成功。是否支付成功必须继续判断 data.orderState 或查询结果中的 data.state。只有支付数据非空且状态允许支付时才打开支付链接;状态未知时先查单,勿重复下单。
4. MD5 签名算法
- 移除
sign字段。 - 排除值为
null或空字符串的字段;数字 0 不得排除。 - 按照字段名不区分大小写升序排列。
- 依次拼接为
key=value&。 - 末尾追加
key=APP_SECRET。 - 使用 UTF-8 计算 MD5,并转为大写。
固定签名样例
演示密钥:DEMO_APP_SECRET
amount=50000&appId=APP_DEMO_10001&body=浦云支付接口测试&clientIp=203.0.113.10¤cy=CNY&expiredTime=1800&extParam=user_10001&mchNo=M_DEMO_10001&mchOrderNo=ORDER202608190001¬ifyUrl=https://merchant.example.com/pay/notify&reqTime=1787078400000&returnUrl=https://merchant.example.com/pay/result&signType=MD5&subject=测试订单&version=1.0&wayCode=ALIPAY_DE&key=DEMO_APP_SECRET
签名结果:8FC1237997EC94C5DD3D3EABF2F1ECF8
响应验签和回调验签采用同一算法:仅对 data 内字段或回调字段计算,先移除收到的 sign。
5. 当前开放的支付通道
| 名称 | wayCode | 实际接口 | 说明 |
|---|---|---|---|
| 大额支付宝500–30000 | ALIPAY_DE | wfpay | 金额范围人民币500–30000元;实际限额以商户配置和上游返回为准 |
| 易安支付宝937 | YIAN_937 | yianpay937 | 需运营后台逐项开通并设置费率;实际限额和可用性以上游配置为准 |
| 易安微信938 | YIAN_938 | yianpay938 | |
| 微信双端原生100(淘宝) | FVXF_201 | fvxfpay | 19个支付方式由运营后台逐项为商户开通并分别设置费率;实际限额和可用性以上游配置为准 |
| 微信双端原生200(淘宝) | FVXF_202 | fvxfpay | |
| 微信双端10-100 | FVXF_203 | fvxfpay | |
| 美团 | FVXF_204 | fvxfpay | |
| 微信/聚合码/综合支付 | FVXF_333 | fvxfpay | |
| UID超大 | FVXF_2001 | fvxfpay | |
| UID大额 | FVXF_2002 | fvxfpay | |
| UID中额 | FVXF_2003 | fvxfpay | |
| UID小额 | FVXF_2004 | fvxfpay | |
| 金条300 | FVXF_2005 | fvxfpay | |
| 金条200 | FVXF_2006 | fvxfpay | |
| 金条100 | FVXF_2007 | fvxfpay | |
| 特价宝妈 | FVXF_2008 | fvxfpay | |
| 免输金条200 | FVXF_2102 | fvxfpay | |
| 免输金条300 | FVXF_2103 | fvxfpay | |
| 免输金条500 | FVXF_2104 | fvxfpay | |
| AA收款 | FVXF_3001 | fvxfpay | |
| 抖音固额100 | FVXF_3002 | fvxfpay | |
| 复制转账大额 | FVXF_6666 | fvxfpay | |
| 数字货币收银台 | CRYPTO_CASHIER | bitcart | 打开数字货币收银台,由当前商户已配置的钱包决定可选币种 |
| USDT-TRC20 | USDT_TRC20 | bitcart | 限定使用商户已配置的 USDT-TRC20 钱包 |
数字货币订单仍使用统一金额规则:amount / 100 作为订单价格,currency 原样转换为大写后发送到收银台。例如 amount=10000, currency=CNY 表示价格 100.00 CNY,再由收银台展示应付数字货币数量。
amount、currency 和到账核对规则。6. 统一下单
业务请求参数
| 字段 | 必填 | 类型 | 说明 |
|---|---|---|---|
mchOrderNo | 是 | String | 商户订单号;同一商户必须唯一,失败重试不要换号 |
wayCode | 是 | String | 只能使用当前商户已授权的编码 |
amount | 是 | Long | 整数,单位分,最小1分 |
currency | 是 | String | 币种代码,建议大写,例如 CNY |
clientIp | 否 | String | 最终用户公网 IP;不传时系统使用请求来源 IP |
subject | 是 | String | 商品标题 |
body | 是 | String | 商品描述 |
notifyUrl | 建议 | String | 支付结果异步通知地址;不传则不会推送回调 |
returnUrl | 否 | String | 支付完成后的浏览器跳转地址,不可代替异步通知 |
expiredTime | 否 | Integer | 订单有效期,单位秒;不传使用系统默认值 |
channelExtra | 否 | String | 当前三个通道无需传;预留为 JSON 字符串 |
extParam | 否 | String | 商户扩展数据,查询和回调原样返回 |
divisionMode | 否 | Byte | 当前通道不开放分账,建议不传或传0 |
请求示例
{
"mchNo": "M_DEMO_10001",
"appId": "APP_DEMO_10001",
"mchOrderNo": "ORDER202608190001",
"wayCode": "ALIPAY_DE",
"amount": 50000,
"currency": "CNY",
"clientIp": "203.0.113.10",
"subject": "测试订单",
"body": "浦云支付接口测试",
"notifyUrl": "https://merchant.example.com/pay/notify",
"returnUrl": "https://merchant.example.com/pay/result",
"expiredTime": 1800,
"extParam": "user_10001",
"reqTime": "1787078400000",
"version": "1.0",
"signType": "MD5",
"sign": "8FC1237997EC94C5DD3D3EABF2F1ECF8"
}成功响应
{
"code": 0,
"msg": "SUCCESS",
"data": {
"payOrderId": "P20260819000100001",
"mchOrderNo": "ORDER202608190001",
"orderState": 1,
"payDataType": "payUrl",
"payData": "https://checkout.example/pay/xxx",
"errCode": null,
"errMsg": null
},
"sign": "响应签名"
}| 字段 | 说明 |
|---|---|
payOrderId | 浦云支付订单号,建议保存 |
mchOrderNo | 原商户订单号 |
orderState | 下单时订单状态 |
payDataType | 当前通道正常返回 payUrl |
payData | 用户支付页面 URL;由浏览器跳转打开 |
errCode / errMsg | 上游错误信息;可能为空 |
7. 支付订单查询
payOrderId 和 mchOrderNo 二选一。推荐保留两者并优先使用浦云支付订单号查询。
{
"mchNo": "M_DEMO_10001",
"appId": "APP_DEMO_10001",
"payOrderId": "P20260819000100001",
"reqTime": "1787078460000",
"version": "1.0",
"signType": "MD5",
"sign": "按本文算法计算"
}查询响应 data
| 字段 | 类型 | 说明 |
|---|---|---|
payOrderId | String | 浦云支付订单号 |
mchNo / appId | String | 所属商户和应用 |
mchOrderNo | String | 商户订单号 |
ifCode | String | 实际通道:wfpay 或 bitcart |
wayCode | String | 支付方式编码 |
amount / currency | Long / String | 订单金额(分)和币种 |
state | Byte | 最终判断使用的订单状态 |
channelOrderNo | String | 上游订单或发票号 |
errCode / errMsg | String | 上游错误 |
extParam | String | 下单时的扩展参数 |
createdAt / successTime | Long | 13位毫秒时间戳 |
8. 支付结果异步通知
当订单状态变化并需要通知商户时,浦云支付向下单时的 notifyUrl 发起 HTTP POST。
| 项目 | 实际行为 |
|---|---|
| 请求方法 | POST |
| 内容类型 | application/x-www-form-urlencoded |
| 连接超时 | 20秒 |
| 成功应答 | 正文返回 SUCCESS,不区分大小写;建议严格返回大写且无空格、无JSON、无HTML |
| 重试 | 最多6次,约在0、30、60、90、120、150秒发送 |
回调字段
字段与订单查询 data 基本一致,并额外包含 reqTime 和 sign。验签时移除 sign,其余非空字段全部参与签名。
payOrderId=P20260819000100001 mchNo=M_DEMO_10001 appId=APP_DEMO_10001 mchOrderNo=ORDER202608190001 ifCode=wfpay wayCode=ALIPAY_DE amount=50000 currency=CNY state=2 extParam=user_10001 createdAt=1787078400000 successTime=1787078499000 reqTime=1787078500000 sign=回调签名
正确处理顺序
- 读取所有表单字段并取出
sign。 - 使用该应用 App Secret 验签。
- 核对
mchNo、appId、订单号、金额和币种。 - 确认
state=2,必要时调用查询接口复核。 - 用商户订单号做数据库唯一约束,在同一事务中幂等入账。
- 事务提交成功后输出纯文本
SUCCESS。
returnUrl 参数,不要仅凭页面跳转发货,不要在验签或入账失败时返回 SUCCESS。9. 订单状态
| 值 | 状态 | 商户处理 |
|---|---|---|
| 0 | 订单生成 | 等待用户支付 |
| 1 | 支付中 | 继续等待或主动查询 |
| 2 | 支付成功 | 验签、核对金额、幂等入账 |
| 3 | 支付失败 | 终止本次支付 |
| 4 | 已撤销 | 不可交付 |
| 5 | 已退款 | 按业务处理退款结果 |
| 6 | 订单关闭 | 不可继续支付 |
10. 错误码与排查
易安上游通知与商户通知:易安937/938的上游通知入口为 https://pay.125.mom/api/pay/notify/yianpay/{payOrderId}。平台全局支付网关地址必须是可公网访问的HTTPS地址,不能填写Docker内部服务名。商户下单的 notifyUrl 是平台确认成功后通知商户的另一层地址,不能混用。已创建订单在上游保存的旧通知地址不会随平台配置修改而自动改变。
易安查单与晚到通知:上游查单为顶层平铺字段,按实际返回字段验签,并核对上游商户号、订单号、金额和渠道流水。平台只有在成功状态落库后才发送商户成功通知。已关闭易安订单的有效成功回调可按条件恢复,重复回调不得重复处理;已关闭且未收到上游回调的历史订单须先核实上游支付证据,再逐单恢复,不能批量改成成功。
皇冠V9:上游明确返回“未适配到通道”时,记录失败状态及 FVXF_CHANNEL_UNAVAILABLE,需要平台核对当前账户的通道和金额条件;通信异常或支付数据缺失保持待确认,错误码分别为 FVXF_RESULT_UNKNOWN、FVXF_INCOMPLETE_RESPONSE。不要根据历史参考限额反复重下订单。
WFPay 白名单与下单异常:商户 API 来源白名单和上游代收服务器 IP 白名单是两套配置。明确的上游白名单拒绝会记录订单失败(orderState=3)及 WFPAY_AUTH_REJECTED;通信异常或未返回完整支付数据保持待确认(orderState=1),可在查单的 errCode / errMsg 中查看 WFPAY_RESULT_UNKNOWN 或 WFPAY_INCOMPLETE_RESPONSE。此时不要付款或盲目换订单号重试。HTTP 403 本身不能作为支付失败依据。
| code | 含义 | 处理建议 |
|---|---|---|
| 0 | SUCCESS | 继续检查 data 中的订单状态 |
| 10 | 系统异常 | 记录请求号和 msg,稍后使用原商户订单号重试或查单 |
| 11 | 参数有误 | 检查必填字段、类型、金额和时间戳 |
| 12 | 数据库服务异常 | 不要换订单号盲目重下,先查单 |
| 9999 | 自定义业务异常 | 以 msg 为准,常见为验签失败、通道未授权或配置不可用 |
常见问题
- 验签失败:确认排除了 sign 和空字符串,排序规则一致,中文使用 UTF-8,末尾是
key=密钥。 - 不支持的支付方式:检查 wayCode 拼写及商户后台是否已勾选。
- 重复商户订单号:先调用查单,不要创建不同订单号重复扣款。
- 支付成功但没收到回调:检查 notifyUrl 公网 HTTPS、响应耗时、防火墙和是否返回纯文本 SUCCESS。
- 一直支付中:主动查单;数字货币需要上游达到完整确认条件后才会成功。
11. 服务端代码示例
cURL 下单
curl -X POST 'https://pay.125.mom/api/pay/unifiedOrder' \
-H 'Content-Type: application/json' \
-d '{"mchNo":"M_DEMO_10001","appId":"APP_DEMO_10001","mchOrderNo":"ORDER202608190001","wayCode":"ALIPAY_DE","amount":50000,"currency":"CNY","subject":"测试订单","body":"浦云支付接口测试","notifyUrl":"https://merchant.example.com/pay/notify","reqTime":"1787078400000","version":"1.0","signType":"MD5","sign":"请由服务端计算"}'PHP 签名
<?php
function puyunSign(array $data, string $secret): string {
unset($data['sign']);
$data = array_filter($data, fn($v) => $v !== null && $v !== '');
uksort($data, 'strcasecmp');
$text = '';
foreach ($data as $key => $value) {
$text .= $key . '=' . $value . '&';
}
return strtoupper(md5($text . 'key=' . $secret));
}
$payload['sign'] = puyunSign($payload, getenv('PUYUNPAY_APP_SECRET'));Node.js 签名
import crypto from 'node:crypto';
export function puyunSign(input, secret) {
const data = { ...input };
delete data.sign;
const text = Object.entries(data)
.filter(([, value]) => value !== null && value !== '')
.sort(([a], [b]) => a.toLowerCase().localeCompare(b.toLowerCase()))
.map(([key, value]) => `${key}=${value}&`)
.join('') + `key=${secret}`;
return crypto.createHash('md5').update(text, 'utf8').digest('hex').toUpperCase();
}Python 签名
import hashlib
def puyun_sign(data: dict, secret: str) -> str:
items = [(k, v) for k, v in data.items()
if k != "sign" and v is not None and v != ""]
items.sort(key=lambda item: item[0].lower())
text = "".join(f"{k}={v}&" for k, v in items) + f"key={secret}"
return hashlib.md5(text.encode("utf-8")).hexdigest().upper()Java 签名
static String puyunSign(Map<String, Object> input, String secret) throws Exception {
List<Map.Entry<String, Object>> items = input.entrySet().stream()
.filter(e -> !"sign".equals(e.getKey()))
.filter(e -> e.getValue() != null && !"".equals(e.getValue()))
.sorted((a, b) -> a.getKey().compareToIgnoreCase(b.getKey()))
.toList();
StringBuilder text = new StringBuilder();
for (var item : items) text.append(item.getKey()).append('=')
.append(item.getValue()).append('&');
text.append("key=").append(secret);
byte[] digest = MessageDigest.getInstance("MD5")
.digest(text.toString().getBytes(StandardCharsets.UTF_8));
return HexFormat.of().withUpperCase().formatHex(digest);
}PHP 回调骨架
<?php
$params = $_POST;
$receivedSign = $params['sign'] ?? '';
$expectedSign = puyunSign($params, getenv('PUYUNPAY_APP_SECRET'));
if (!hash_equals($expectedSign, strtoupper($receivedSign))) {
http_response_code(400);
exit('INVALID SIGN');
}
if (($params['state'] ?? '') !== '2') exit('WAIT');
// 开启数据库事务;锁定 mchOrderNo;核对金额、币种;仅首次成功时入账;提交事务。
header('Content-Type: text/plain; charset=UTF-8');
echo 'SUCCESS';12. 上线检查清单
- 商户号、App ID、App Secret 来自同一应用。
- App Secret 仅保存在服务端环境变量或密钥系统。
- 如已启用API白名单,确认商户服务器的实际公网出口IP已加入白名单。
- 通道已由管理员勾选授权。
- 金额全部使用整数分,数据库保存原始请求金额;USDT 付款严格使用收银台显示的完整随机尾数金额。
- 商户订单号有唯一索引。
- 回调先验签,再核对商户、订单、金额、币种和状态。
- 重复回调不会重复加款或重复发货。
- notifyUrl 使用 HTTPS,20秒内完成处理并返回 SUCCESS。
- 下单超时先查单,不直接换订单号重下。
- 生产日志不打印 App Secret、完整签名原文或用户敏感数据。
13. USDT TRC20代付
13.1 提交代付订单
https://pay.125.mom/api/transferOrder| 字段 | 必填 | 说明 |
|---|---|---|
mchNo、appId | 是 | 商户号和应用AppId |
mchOrderNo | 是 | 商户代付单号;同一商户必须唯一 |
ifCode | 是 | 固定为 bitcart |
entryType | 是 | 固定为 USDT_TRC20 |
amount | 是 | 整数,单位分,例如100 USDT传10000 |
currency | 是 | 固定为 USD |
accountNo | 是 | 有效TRC20收款地址 |
transferDesc | 是 | 代付备注 |
notifyUrl | 否 | 成功或失败结果通知地址 |
extParam | 否 | 商户扩展参数,查询及通知原样返回 |
reqTime、version、signType、sign | 是 | 与支付接口使用相同签名规则 |
{
"mchNo":"M_DEMO_10001","appId":"APP_DEMO_10001",
"mchOrderNo":"TRANSFER202608190001","ifCode":"bitcart",
"entryType":"USDT_TRC20","amount":10000,"currency":"USD",
"accountNo":"TRC20收款地址","transferDesc":"USDT代付",
"notifyUrl":"https://merchant.example.com/transfer/notify",
"reqTime":1787078400000,"version":"1.0","signType":"MD5","sign":"服务端计算"
}同一商户订单号重复提交且关键参数一致时返回原订单;金额、地址、应用或通道等参数不一致时拒绝。状态:1代付中、2成功、3失败。
13.2 查询代付订单
https://pay.125.mom/api/transfer/query签名公共参数之外,传 mchOrderNo 或 transferId 之一。订单严格按当前 mchNo + appId 隔离。成功结果中的 channelOrderNo 为TRON交易哈希。
13.3 代付结果通知
订单被管理员确认成功或失败后,系统向下单时的 notifyUrl 发送 application/x-www-form-urlencoded POST通知,字段与查询结果一致,并增加 reqTime 和 sign。商户验签、核对订单和金额并完成幂等处理后,返回纯文本 SUCCESS。未返回SUCCESS时系统最多发送6次。
商户平台仍可在“商户中心 → 代付”提交,并在“订单中心 → 代付订单”查询;提现管理是另一项独立功能。
超管后台删除订单采用软删除;删除仍冻结余额的代付订单时,系统会在同一事务内先释放冻结余额。删除操作不改变已完成的链上交易结果。