快速开始 5 分钟
四步跑通第一笔支付:拿凭证 → 生成签名 → 调统一下单 → 查单对账。
| 对比项 | 保护伞原生 API(主) | Jeepay 兼容层 | 易支付(EPay) 兼容层 |
|---|---|---|---|
| 下单入口 | POST /api/pay/unifiedOrder | POST /api/jeepay/pay/unifiedOrder | POST /api/epay/mapi.php · submit.php |
| 报文格式 | JSON | JSON | 表单 form(下划线字段) |
| 金额单位 | 元(decimal) | 分(整数) | 元(两位小数字符串) |
| 签名 | MD5 大写 / HMAC_SHA256 / RSA2 | MD5 大写,尾部 &key=密钥 | MD5 小写,尾部裸拼密钥 |
| 防重放 | 需 timestamp + nonce | 无 | 无 |
| 成功码 | code=200 | code=0 | code=1 |
| 商户标识 | merchantNo + appId | mchNo + appId | pid |
接入准备与凭证
对接前需要拿到的三项凭证,以及它们在三套协议(保护伞原生 / Jeepay / 易支付)里的对应关系。
| 概念 | 说明 | 保护伞原生字段 | Jeepay 字段 | 易支付字段 |
|---|---|---|---|---|
| 商户号 | 平台分配的唯一商户标识 | merchantNo | mchNo | pid |
| 应用 ID | 一个商户可有多应用;易支付场景固定用 pid 占位 | appId | appId | —(内部 = pid) |
| 应用密钥 | 签名/验签用,务必保存在服务端,切勿下发前端 | appKey | appKey | KEY |
| 签名方式 | 原生:MD5 / HMAC_SHA256 / RSA2(默认 MD5,且额外需 timestamp + nonce 防重放);Jeepay:MD5 / HMAC_SHA256 | signType | signType | 固定 MD5 |
通用约定
- 协议 / 域名:全部接口经网关 https://api.60185.com,HTTPS 访问;下单类接口匿名放行,鉴权靠签名。
- 路径规则:网关 /api/xxx 剥离一层 /api 转发到支付服务。原生访问 /api/pay/**,兼容层访问 /api/jeepay/** 与 /api/epay/**。
- 字符集:统一 UTF-8,中文商品名需正确编码,否则验签会因字节差异失败。
- 报文格式(JSON 支持):保护伞原生与 Jeepay 兼容层的请求体必须是 JSON 对象(一层 {...},不得包 {"param":...} 外层),并带 Content-Type: application/json;易支付兼容层只接受表单 application/x-www-form-urlencoded(或 GET 查询串),传 JSON 会直接 400。
- 响应格式:三套协议响应与异步通知统一为 JSON(application/json;charset=UTF-8),验签失败等业务错误也返回 HTTP 200 + JSON 错误码(仅格式错、参数缺失才会抛非 2xx)。因此判定成败要看报文里的 code,不能只看 HTTP 状态码。
- 时间格式:响应/通知内 yyyy-MM-dd HH:mm:ss。
- 金额:原生与易支付用元(原生为 decimal、易支付为字符串两位小数),Jeepay 用分(int);平台内部统一转元。
- 幂等:同一 mchOrderNo / out_trade_no 重复下单返回同一订单;商户订单号请全局唯一。
- 有效期:订单默认 2 小时失效(原生 expireMinutes / Jeepay expiredTime 秒覆盖;易支付内部固定 120 分钟,均受平台上限约束)。
- 防重放(仅原生 API):每个请求须带 timestamp(毫秒,±5 分钟内)与 nonce(窗口内唯一随机串),二者均参与签名;兼容层(Jeepay/易支付)不需要。
签名算法
三套协议都以 MD5 为主,但大小写、拼接方式、是否含防重放字段各不相同,这是对接中最容易出错的地方。
保护伞原生签名(MD5 大写 / HMAC_SHA256 / RSA2)
- 收集所有非空请求参数(含 timestamp、nonce),排除 sign、signType。
- 按参数名 ASCII 升序拼 k1=v1&k2=v2&…。
- 按 signType 计算:
· MD5:尾部拼 &key={appKey} 后取 MD5,转大写;
· HMAC_SHA256:以 appKey 为密钥对内容串做 HMAC-SHA256,大写十六进制;
· RSA2:用商户 RSA 私钥对内容串做 SHA256withRSA,结果 Base64(平台用商户公钥验签)。
// 待签串示例(注意 timestamp / nonce 也参与) amount=0.01&appId=app_xxx&bankCode=9311¤cy=CNY&mchOrderNo=ORD20260928001&merchantNo=88001&nonce=8f2c1a¬ifyUrl=https://...&payMethod=alipay&subject=test×tamp=1758960000000&key={appKey} // MD5 → sign = MD5(上面的串).toUpperCase()
Jeepay 签名(MD5 大写)
- 收集所有非空请求参数,排除 sign、signType。
- 按参数名 ASCII 升序排列,拼成 k1=v1&k2=v2&…。
- 尾部拼接 &key={appKey}。
- 对整串做 MD5,结果转大写 = sign。
// 待签串示例 amount=1&appId=app_xxx¤cy=cny&mchNo=88001&mchOrderNo=ORD20260928001¬ifyUrl=https://...&subject=test&wayCode=ALI_QR&key={appKey} // sign = MD5(上面的串).toUpperCase()
易支付签名(MD5 小写)
- 收集所有非空参数,排除 sign、sign_type 与空值。
- 按参数名 ASCII 升序拼 k1=v1&k2=v2&…。
- 尾部直接拼接密钥(没有 &key= 前缀)。
- MD5 后转小写 = sign。
// 待签串示例(注意结尾直接跟 KEY,无 &key=) money=0.01&name=test¬ify_url=https://...&out_trade_no=ORD001&pid=1001&type=alipay{KEY} // sign = MD5(上面的串).toLowerCase()
在线签名计算器 调试神器
纯前端计算,参数与密钥不会离开你的浏览器;用它先手算出一个正确签名,和你代码里的结果比对,秒定位差异。 注:原生 API 的 HMAC_SHA256 / RSA2 需在你服务端用对应库实现,本计算器仅演示 MD5;选“保护伞原生”时记得把 timestamp 与 nonce 一并填入参数。
多语言签名 / 请求示例 复制即用
以下为保护伞原生 API(POST /api/pay/unifiedOrder)的签名 + 下单代码:非空参数 ASCII 升序 → 尾随 &key=appKey → MD5 转大写;金额单位元;timestamp 为毫秒;成功码 code=200。把 APP_KEY 换成你的应用密钥即可(密钥只放服务端)。已提供 Java / PHP / Python / Node.js / Go / C++ / .NET(C#) / Lua 八种主流语言的等价实现(Lua 面向 OpenResty / ngx_lua 网关场景),签名规则完全一致。
// 仅依赖 JDK;md5() 用标准 MD5 实现(略)
Map<String, String> p = new TreeMap<>(); // TreeMap 保证 key ASCII 升序
p.put("merchantNo", "88001");
p.put("appId", "app_demo");
p.put("mchOrderNo", "ORD20260928001");
p.put("payMethod", "alipay");
p.put("bankCode", "9311"); // 二级通道编码,参与签名
p.put("amount", "0.01"); // 单位:元
p.put("currency", "CNY");
p.put("subject", "测试商品");
p.put("notifyUrl", "https://m.example.com/pay/notify");
p.put("timestamp", String.valueOf(System.currentTimeMillis())); // 毫秒
p.put("nonce", UUID.randomUUID().toString().replace("-", ""));
// 待签串:升序 k=v&k2=v2 之后尾随 &key={appKey}
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> e : p.entrySet()) {
if (sb.length() > 0) sb.append("&");
sb.append(e.getKey()).append("=").append(e.getValue());
}
String signSrc = sb + "&key=" + APP_KEY;
p.put("signType", "MD5");
p.put("sign", md5(signSrc).toUpperCase()); // MD5 转大写
// POST https://api.60185.com/api/pay/unifiedOrder Content-Type: application/json
// 响应 {"code":200,"data":{"payEntryUrl":"https://..."}} → 引导用户打开 payEntryUrl<?php
$appKey = '你的 appKey';
$p = [
'merchantNo' => '88001', 'appId' => 'app_demo',
'mchOrderNo' => 'ORD20260928001', 'payMethod' => 'alipay',
'bankCode' => '9311',
'amount' => '0.01', 'currency' => 'CNY', 'subject' => '测试商品', // 元
'notifyUrl' => 'https://m.example.com/pay/notify',
'timestamp' => (string) round(microtime(true) * 1000), // 毫秒
'nonce' => bin2hex(random_bytes(16)),
];
ksort($p); // ASCII 升序
$parts = [];
foreach ($p as $k => $v) { $parts[] = "$k=$v"; }
$signSrc = implode('&', $parts) . '&key=' . $appKey;
$p['sign'] = strtoupper(md5($signSrc)); // MD5 大写
$p['signType'] = 'MD5';
$ch = curl_init('https://api.60185.com/api/pay/unifiedOrder');
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($p, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
]);
$res = json_decode(curl_exec($ch), true);
// $res['code'] === 200 即成功;用 $res['data']['payEntryUrl'] 引导付款import time, uuid, hashlib, json, requests
APP_KEY = '你的 appKey'
p = {
'merchantNo': '88001', 'appId': 'app_demo',
'mchOrderNo': 'ORD20260928001', 'payMethod': 'alipay',
'bankCode': '9311',
'amount': '0.01', 'currency': 'CNY', 'subject': '测试商品', # 元
'notifyUrl': 'https://m.example.com/pay/notify',
'timestamp': str(int(time.time() * 1000)), # 毫秒
'nonce': uuid.uuid4().hex,
}
base = '&'.join('%s=%s' % (k, p[k]) for k in sorted(p) if p[k] != '')
sign = hashlib.md5((base + '&key=' + APP_KEY).encode('utf-8')).hexdigest().upper()
p['signType'] = 'MD5'
p['sign'] = sign
r = requests.post('https://api.60185.com/api/pay/unifiedOrder',
data=json.dumps(p, ensure_ascii=False).encode('utf-8'),
headers={'Content-Type': 'application/json'}, timeout=10)
res = r.json()
# res['code'] == 200 即成功;用 res['data']['payEntryUrl'] 引导付款const crypto = require('crypto');
const APP_KEY = '你的 appKey';
const p = {
merchantNo: '88001', appId: 'app_demo',
mchOrderNo: 'ORD20260928001', payMethod: 'alipay',
bankCode: '9311',
amount: '0.01', currency: 'CNY', subject: '测试商品', // 元
notifyUrl: 'https://m.example.com/pay/notify',
timestamp: String(Date.now()), // 毫秒
nonce: crypto.randomBytes(16).toString('hex'),
};
const base = Object.keys(p).sort()
.filter(k => p[k] !== '')
.map(k => `${k}=${p[k]}`).join('&');
const sign = crypto.createHash('md5')
.update(base + '&key=' + APP_KEY).digest('hex').toUpperCase();
p.signType = 'MD5';
p.sign = sign;
fetch('https://api.60185.com/api/pay/unifiedOrder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(p),
}).then(r => r.json()).then(res => {
// res.code === 200 即成功;用 res.data.payEntryUrl 引导付款
});// 依赖:github.com/google/uuid;md5 用标准库
package main
import (
"bytes" "crypto/md5" "encoding/hex" "encoding/json" "fmt" "net/http" "sort" "strings" "time"
"github.com/google/uuid"
)
const appKey = "你的 appKey"
func md5HexUpper(s string) string {
sum := md5.Sum([]byte(s))
return strings.ToUpper(hex.EncodeToString(sum[:]))
}
func main() {
p := map[string]string{
"merchantNo": "88001", "appId": "app_demo",
"mchOrderNo": "ORD20260928001", "payMethod": "alipay",
"bankCode": "9311",
"amount": "0.01", "currency": "CNY", "subject": "测试商品", // 单位:元
"notifyUrl": "https://m.example.com/pay/notify",
"timestamp": fmt.Sprintf("%d", time.Now().UnixMilli()), // 毫秒
"nonce": strings.ReplaceAll(uuid.NewString(), "-", ""),
}
keys := make([]string, 0, len(p))
for k, v := range p {
if v != "" { keys = append(keys, k) } // 只对非空参数签名
}
sort.Strings(keys) // ASCII 升序
parts := make([]string, 0, len(keys))
for _, k := range keys { parts = append(parts, k+"="+p[k]) }
signSrc := strings.Join(parts, "&") + "&key=" + appKey
p["signType"] = "MD5"
p["sign"] = md5HexUpper(signSrc) // MD5 转大写
body, _ := json.Marshal(p)
resp, err := http.Post("https://api.60185.com/api/pay/unifiedOrder",
"application/json", bytes.NewReader(body))
if err != nil { panic(err) }
defer resp.Body.Close()
var out struct { Code int `json:"code"`; Msg string `json:"msg"`; Data struct{ PayEntryUrl string `json:"payEntryUrl"` } `json:"data"` }
json.NewDecoder(resp.Body).Decode(&out)
// out.Code == 200 即成功;用 out.Data.PayEntryUrl 引导用户打开付款
}// 依赖:cpp-httplib(单头文件)+ OpenSSL(MD5)
// g++ main.cpp -o pay -lssl -lcrypto
#include <httplib.h>
#include <openssl/md5.h>
#include <map> #include <string> #include <vector> #include <sstream>
#include <iomanip> #include <chrono> #include <random> #include <algorithm>
static std::string md5Upper(const std::string& s) {
unsigned char d[MD5_DIGEST_LENGTH];
MD5(reinterpret_cast<const unsigned char*>(s.data()), s.size(), d); // OpenSSL 1.x;3.x 用 EVP_Digest
std::ostringstream o;
for (unsigned char c : d) o << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << (int)c;
return o.str();
}
int main() {
const std::string appKey = "你的 appKey";
std::map<std::string, std::string> p; // std::map 天然按 key ASCII 升序
p["merchantNo"] = "88001"; p["appId"] = "app_demo";
p["mchOrderNo"] = "ORD20260928001"; p["payMethod"] = "alipay";
p["bankCode"] = "9311";
p["amount"] = "0.01"; p["currency"] = "CNY"; // 单位:元
p["subject"] = "测试商品";
p["notifyUrl"] = "https://m.example.com/pay/notify";
p["timestamp"] = std::to_string( std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count() ); // 毫秒
p["nonce"] = []{ std::random_device rd; std::ostringstream s;
for (int i = 0; i < 8; ++i) s << std::hex << rd(); return s.str(); }();
std::string base; // k=v&k2=v2 升序拼接
for (auto& kv : p) { if (!base.empty()) base += "&"; base += kv.first + "=" + kv.second; }
p["sign"] = md5Upper(base + "&key=" + appKey); // MD5 转大写
p["signType"] = "MD5";
std::string jsonBody = "{"; // 手工拼 JSON(UTF-8 原文)
for (auto& kv : p) { jsonBody += "\"" + kv.first + "\":\"" + kv.second + "\","; }
jsonBody.back() = '}';
httplib::Client cli("https://api.60185.com");
auto res = cli.Post("/api/pay/unifiedOrder", jsonBody, "application/json");
// res->status == 200 且响应体 code==200 即成功;取 data.payEntryUrl 引导付款
}// .NET 6+ / C#(System.Text.Json + HttpClient,无需第三方依赖)
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
string appKey = "你的 appKey";
var p = new Dictionary<string, string>
{
["merchantNo"] = "88001", ["appId"] = "app_demo",
["mchOrderNo"] = "ORD20260928001", ["payMethod"] = "alipay",
["bankCode"] = "9311",
["amount"] = "0.01", ["currency"] = "CNY", ["subject"] = "测试商品", // 单位:元
["notifyUrl"] = "https://m.example.com/pay/notify",
["timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(), // 毫秒
["nonce"] = Guid.NewGuid().ToString("N"),
};
// 非空参数按 key ASCII 升序 → k=v&... → 尾随 &key=appKey → MD5 大写
var signSrc = string.Join("&", p.Where(kv => !string.IsNullOrEmpty(kv.Value))
.OrderBy(kv => kv.Key, StringComparer.Ordinal)
.Select(kv => $"{kv.Key}={kv.Value}")) + "&key=" + appKey;
var hash = MD5.HashData(Encoding.UTF8.GetBytes(signSrc));
p["sign"] = Convert.ToHexString(hash); // .NET 5+ 默认输出大写 hex
p["signType"] = "MD5";
var payload = JsonSerializer.SerializeToUtf8Bytes(p,
new JsonSerializerOptions { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping });
using var http = new HttpClient();
var resp = await http.PostAsync("https://api.60185.com/api/pay/unifiedOrder",
new StringContent(Encoding.UTF8.GetString(payload), Encoding.UTF8, "application/json"));
using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync());
// doc.RootElement.GetProperty("code").GetInt32() == 200 即成功;取 data.payEntryUrl 引导付款-- 依赖:luaossl(md5)+ lua-cjson + luasec/http;OpenResty 下用 resty.md5 + resty.http
local md5 = require('resty.openssl.digest') -- luaossl;OpenResty 也可用 resty.md5(见下方注释)
local cjson = require('cjson')
local http = require('http')
local appKey = '你的 appKey'
local p = {
merchantNo = '88001', appId = 'app_demo',
mchOrderNo = 'ORD20260928001', payMethod = 'alipay',
bankCode = '9311',
amount = '0.01', currency = 'CNY', subject = '测试商品', -- 单位:元
notifyUrl = 'https://m.example.com/pay/notify',
timestamp = tostring(math.floor(ngx and ngx.now() * 1000 or os.time() * 1000)), -- 毫秒
nonce = string.format('%08x%08x', math.random(0, 0xffffffff), math.random(0, 0xffffffff)),
}
-- 非空参数按 key ASCII(字节序)升序 → k=v&... → 尾随 &key=appKey → MD5 大写
local keys = {}
for k, v in pairs(p) do if v ~= nil and v ~= '' then keys[#keys + 1] = k end end
table.sort(keys) -- Lua 字符串比较即字节序,等价 ASCII 升序
local parts = {}
for _, k in ipairs(keys) do parts[#parts + 1] = k .. '=' .. tostring(p[k]) end
local signSrc = table.concat(parts, '&') .. '&key=' .. appKey
local function md5Upper(s)
local d = md5.new('md5'); d:update(s)
return (d:final():gsub('.', function(c) return string.format('%02X', c) end)) -- 大写 hex
end
p.sign = md5Upper(signSrc)
-- OpenResty 写法: local m = require('resty.md5'); m:set(signSrc); p.sign = string.upper(m:tohex())
p.signType = 'MD5'
local res, err = http.post('https://api.60185.com/api/pay/unifiedOrder', cjson.encode(p),
{ ['Content-Type'] = 'application/json' })
if not res then error(err) end
local out = cjson.decode(res) -- luaossl http 直接返回响应体字符串
-- out.code == 200 即成功;用 out.data.payEntryUrl 引导用户打开付款
-- 注:luaossl/cjson 为阻塞调用,适合普通 Lua 脚本;OpenResty 网关内请改用 resty.http + cjson主 API 保护伞原生支付接口
保护伞自己的标准支付 API,也是本平台功能最完整、推荐优先对接的接口。报文体为 JSON,金额单位元,成功码 code=200,采用 appKey 签名 + timestamp/nonce 防重放(比兼容层多一层安全校验)。下方的 Jeepay / 易支付 仅为存量商户零代码迁移的兼容适配层。
接口清单
| 接口 | 方法 | 路径 | 说明 |
|---|---|---|---|
| 统一下单 | POST | /api/pay/unifiedOrder | 创建支付订单,返回支付入口 |
| 查询订单 | GET | /api/pay/query/{orderId} | 按平台订单号查询 |
| 关闭订单 | POST | /api/pay/close/{orderId} | 关闭待支付订单 |
| 申请退款 | POST | /api/pay/refund | 对已支付订单发起退款(见 退款) |
| 查询退款 | GET | /api/pay/refund/{refundId} | 按退款单号查询 |
① Content-Type: application/json 必带;下单 / 退款不支持用 x-www-form-urlencoded 调用(表单字段能过验签,但业务层拿不到报文体,会直接请求格式报错);
② 金额等数字字段按原样字符串签名(1 就签 1,不要写成 1.00;0.01 就签 0.01);
③ 不要给签名串里传数组字段;嵌套对象(如 extra)请传已字符串化的 JSON(字符串值),否则平台拼接的待签串与你本地不一致;
④ 空字符串字段不参与签名(平台验签时自动剔除 null/空值),要么不传、要么传真实值;sign / signType 永不被签。
统一下单 POST /api/pay/unifiedOrder
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| merchantNo | string | 是 | 商户号 |
| appId | string | 是 | 应用 ID |
| mchOrderNo | string | 是 | 商户订单号,全局唯一 |
| payMethod | string | 是 | 支付方式:alipay / wxpay / bank / quick |
| amount | decimal | 是 | 金额,单位元,最小 0.01 |
| currency | string | 否 | 币种 CNY/USD/THB/VND,默认 CNY |
| bankCode | string | 必带 | 支付通道编码(二级分类),取值见下方 通道编码全表(如 9311 支付宝扫码、9211 微信扫码)。一旦携带即按 ASCII 升序纳入待签串、参与签名;强烈建议每笔都携带——平台据此做入口鉴定精确锁定运营商分配给该商户的入金账户,不携带则回退共享池权重路由(通道可能漂移) |
| subject | string | 否 | 商品标题 |
| body | string | 否 | 商品描述 |
| clientIp | string | 否 | 客户端 IP |
| deviceId | string | 否 | 设备指纹(前端 SDK 采集) |
| notifyUrl | string | 否 | 异步通知地址(强烈建议传) |
| returnUrl | string | 否 | 前端跳转地址 |
| expireMinutes | int | 否 | 订单过期分钟数(受平台上限约束) |
| extra | string | 否 | 扩展参数 JSON,通知原样返回 |
| timestamp | long | 是 | 毫秒时间戳,±5 分钟内有效,参与签名 |
| nonce | string | 是 | 随机串(≤ 64 字符),窗口内不可重复,参与签名 |
| signType | string | 是 | MD5 / HMAC_SHA256 / RSA2 |
| sign | string | 是 | 签名,见 签名算法 |
二级分类 · 支付通道编码(bankCode)全表
保护伞采用三级分类模型:一级大类(payMethod)→ 二级通道(bankCode,商户下单携带,做入口鉴定)→ 三级入金账户(运营商分配,经二级关联目录挂接)。下单时 payMethod 与 bankCode 需同族匹配(如 alipay 配 93xx);该字段携带后必须纳入待签串参与签名。标“待接入”的二级分类下单会被拒绝。
| 一级 payMethod | 二级分类 | bankCode | categoryCode | 接入状态 |
|---|---|---|---|---|
| wxpay | 微信扫码 | 9211 | WX_QR | 已接入 |
| wxpay | 微信H5 | 9212 | WX_H5 | 已接入 |
| wxpay | 微信公众号 | 9213 | WX_JSAPI | 已接入 |
| wxpay | 微信APP | 9214 | WX_APP | 已接入 |
| wxpay | 微信付款码 | 9215 | WX_BARCODE | 已接入 |
| wxpay | 微信刷脸 | 9216 | WX_FACE | 待接入 |
| wxpay | 微信转账 | 9217 | WX_TRANSFER | 待接入 |
| alipay | 支付宝扫码 | 9311 | ALI_QR | 已接入 |
| alipay | 支付宝H5 | 9312 | ALI_H5 | 已接入 |
| alipay | 支付宝PC | 9313 | ALI_PC | 已接入 |
| alipay | 支付宝APP | 9314 | ALI_APP | 已接入 |
| alipay | 支付宝条码 | 9315 | ALI_BARCODE | 已接入 |
| alipay | 支付宝刷脸 | 9316 | ALI_FACE | 待接入 |
| alipay | 支付宝转账 | 9317 | ALI_TRANSFER | 待接入 |
| qqpay | QQ钱包扫码 | 9411 | QQ_QR | 已接入 |
| qqpay | QQ钱包H5 | 9412 | QQ_H5 | 已接入 |
| qqpay | QQ钱包付款码 | 9413 | QQ_BARCODE | 已接入 |
| jdpay | 京东钱包 | 9511 | JD_QR | 已接入 |
| jdpay | 京东付款码 | 9512 | JD_FKM | 已接入 |
| unionpay | 银联扫码 | 9611 | UNION_QR | 待接入 |
| unionpay | 银联H5 | 9612 | UNION_H5 | 待接入 |
| unionpay | 银联APP | 9613 | UNION_APP | 待接入 |
| unionpay | 银联PC网银 | 9614 | UNION_WEB | 待接入 |
| paypal | PayPal | 9711 | PAYPAL_WEB | 已接入 |
| xpay | 个人支付宝固码 | 9811 | XPAY_ALI | 已接入 |
| xpay | 个人微信固码 | 9812 | XPAY_WX | 已接入 |
POST https://api.60185.com/api/pay/unifiedOrder Content-Type: application/json { "merchantNo": "88001", "appId": "app_demo", "mchOrderNo": "ORD20260928001", "payMethod": "alipay", "bankCode": "9311", // 二级通道编码,参与签名 "amount": 0.01, // 元 "currency": "CNY", "subject": "测试商品", "notifyUrl": "https://merchant.example.com/pay/notify", "returnUrl": "https://merchant.example.com/pay/return", "expireMinutes": 30, "timestamp": 1758960000000, // 毫秒 "nonce": "a1b2c3d4e5", "signType": "MD5", "sign": "5F3A...大写MD5...9C1D" }
{
"code": 200,
"msg": "success",
"data": {
"orderId": "P20260928000001", // 平台订单号
"mchOrderNo": "ORD20260928001",
"merchantNo": "88001",
"appId": "app_demo",
"payMethod": "alipay",
"amount": 0.01, // 元
"currency": "CNY",
"status": 0, // 0待支付 1支付中 2成功 3失败 4已退款 5部分退款 6已关闭
"payEntryUrl": "https://api.60185.com/risk/loading?...", // ★风控入口,必须引导用户打开
"payData": "https://qr.alipay.com/xxx", // 收银台URL/二维码内容(服务端留存)
"expireTime": "2026-09-28 17:44:19",
"traceId": "a1b2c3...",
"routingStatus": null // busy/none 仅入金账户异常时透出
}
}订单状态 status
| status | 含义 | status | 含义 |
|---|---|---|---|
| 0 | 待支付 | 3 | 支付失败 |
| 1 | 支付中 | 4 | 已退款 |
| 2 | 支付成功 | 5 | 部分退款 |
| 6 | 已关闭 | — | — |
Jeepay 兼容层接口
面向存量 Jeepay 商户的零代码迁移入口。报文体为 JSON,金额单位分,成功码 code=0。所有接口 POST,需带 sign。
统一下单 POST /api/jeepay/pay/unifiedOrder
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| mchNo | string | 是 | 商户号 |
| appId | string | 是 | 应用 ID |
| mchOrderNo | string | 是 | 商户订单号,全局唯一 |
| wayCode | string | 是 | 支付方式,见 wayCode 表(如 ALI_QR / WX_JSAPI) |
| amount | int | 是 | 金额,单位分,须 > 0 |
| currency | string | 否 | 币种,默认 cny |
| clientIp | string | 否 | 客户端 IP |
| subject | string | 否 | 商品标题 |
| body | string | 否 | 商品描述 |
| notifyUrl | string | 否 | 异步通知地址(强烈建议传) |
| returnUrl | string | 否 | 同步跳转地址 |
| expiredTime | int | 否 | 订单失效时间(秒),默认 2 小时 |
| channelId | string | 否 | 渠道用户 ID(微信 openid,JSAPI/小程序必传) |
| extParam | string | 否 | 扩展参数,通知原样返回 |
| reqStr | string | 否 | 随机串(防重放) |
| signType | string | 否 | MD5(默认)/ HMAC_SHA256 |
| sign | string | 是 | 签名,见 签名算法 |
查询订单 POST /api/jeepay/pay/query
参数:mchNo / appId 必填,payOrderId 与 mchOrderNo 至少一个,加 signType / sign。响应 data 结构同下单。
关闭订单 POST /api/jeepay/pay/close
参数同查询(mchNo/appId + payOrderId|mchOrderNo + sign),成功返回 {code:0,msg:SUCCESS}。
验签要点
- 参与签名的字段随接口不同而不同(下单含 amount/currency/subject/notifyUrl…,查单只含 mchNo/appId/payOrderId|mchOrderNo)——用哪几个字段就只签哪几个,与 计算器里输入的行保持一致。
- 响应 data 当前不下发 sign,无需对响应验签。
易支付 兼容层接口
面向存量易支付(EPay)商户的零代码迁移入口。表单格式、下划线字段、金额单位元、成功码 code=1、签名 MD5 小写。注意:本兼容层只接受表单(application/x-www-form-urlencoded)或 GET 查询串,不接受 JSON 请求体。
API 接口支付 POST /api/epay/mapi.php
| 参数 | 必填 | 说明 |
|---|---|---|
| pid | 是 | 商户 ID(= 商户号) |
| type | 否 | 支付方式 alipay / wxpay / qqpay / bank;不传走收银台 |
| out_trade_no | 是 | 商户订单号,唯一 |
| notify_url | 是 | 异步通知地址 |
| return_url | 否 | 同步跳转地址 |
| name | 否 | 商品名称 |
| money | 是 | 金额,单位元,如 0.01 |
| clientip | 否 | 用户 IP |
| param | 否 | 业务扩展参数,回调原样返回 |
| sign | 是 | MD5 小写,密钥裸拼(无 &key=) |
| sign_type | 否 | 固定 MD5,不参与签名 |
POST https://api.60185.com/api/epay/mapi.php Content-Type: application/x-www-form-urlencoded pid=1001&type=alipay&out_trade_no=ORD001¬ify_url=https://m.example.com/notify&name=test&money=0.01&sign={小写MD5}&sign_type=MD5 // 响应 { "code":1, "msg":"SUCCESS", "trade_no":"P20260928000009", "orderid":"P20260928000009", "type":"alipay", "payurl":"https://qr.alipay.com/xxx", "payDataType":"url" }
页面跳转支付 GET/POST /api/epay/submit.php
参数与 mapi.php 完全相同,用于传统“表单自动提交跳转到收银台”。商户页面构造一个 form 自动 POST 即可:
<form id="epay" method="post" action="https://api.60185.com/api/epay/submit.php">
<input name="pid" value="1001">
<input name="type" value="alipay">
<input name="out_trade_no" value="ORD001">
<input name="notify_url" value="https://m.example.com/notify">
<input name="return_url" value="https://m.example.com/return">
<input name="name" value="test">
<input name="money" value="0.01">
<input name="sign" value="{小写MD5}">
<input name="sign_type" value="MD5">
</form><script>epay.submit()</script>
查询 GET /api/epay/api.php
① 查商户信息
GET /api/epay/api.php?act=query&pid=1001&key={KEY}
// → { "code":1, "pid":1001, "key":"...", "active":1, "money":"余额" }
② 查订单
GET /api/epay/api.php?pid=1001&key={KEY}&out_trade_no=ORD001
// 或传 trade_no=P20260928000009
// → { "trade_no","out_trade_no","type","pid","addtime","endtime","name","money","status":1 }
// status: 1=已支付 0=未支付
异步通知(支付回调)
用户付款成功后,平台主动 POST 到你下单时的 notifyUrl。根据商户来源平台自动选择 保护伞原生 / Jeepay / 易支付 格式,Content-Type: application/json。
重试机制
- 最多重试 8 次,指数退避(首次约 60s,逐次 ×2);直到你返回 2xx 或耗尽。
- 因此通知会重复到达,商户端必须做幂等(同一 trade_no 只入账一次)。
POST {notifyUrl} Content-Type: application/json
{
"merchantNo": "88001",
"appId": "app_demo",
"mchOrderNo": "ORD20260928001", // 商户订单号
"orderId": "P20260928000001", // 平台订单号
"channelOrderNo": "2026...CHN",
"payMethod": "alipay",
"amount": "0.01", // 金额,单位元
"status": "SUCCESS", // 支付成功
"payTime": "2026-09-28 15:44:19",
"channelUser": "",
"signType": "MD5",
"sign": "5F3A...大写MD5...9C1D"
}
验签(保护伞原生):除 sign/signType 外的非空字段 ASCII 升序拼 k=v&…&key={appKey},MD5 大写(若商户为 HMAC_SHA256/RSA2 则按对应算法,RSA2 用平台公钥验签)。校验 status==SUCCESS 且 amount 与本地订单一致才入账。验签通过后仍须调查单接口确认状态,不得仅凭通知入账。
POST {notifyUrl} Content-Type: application/json
{
"payOrderId": "P20260928000001", // 平台订单号
"mchOrderNo": "ORD20260928001", // 商户订单号
"mchNo": "88001", "appId": "app_demo",
"wayCode": "ALI_QR",
"payAmount": "1", // 金额,单位分
"currency": "cny",
"state": "2", // 2=支付成功
"orderId": "2026...CHN", "channelOrderNo": "2026...CHN",
"subject": "测试商品", "body": "",
"paySuccTime": "2026-09-28 15:44:19", "createTime": "...",
"extParam": "",
"signType": "MD5",
"sign": "5F3A...大写MD5...9C1D"
}
验签:把除 sign/signType 外的非空字段按 ASCII 升序拼 k=v&…&key={appKey},MD5 大写 与 sign 比对;再校验 state==2 且 payAmount 与本地订单金额一致才入账。验签通过后仍须调查单接口确认状态,不得仅凭通知入账。
POST {notify_url} Content-Type: application/json
{
"trade_no": "P20260928000009", // 平台订单号
"out_trade_no": "ORD001", // 商户订单号
"type": "alipay",
"pid": "1001",
"trade_status": "TRADE_SUCCESS", // 支付成功
"money": "0.01", // 金额,单位元
"param": "",
"sign": "a1b2...小写MD5...f9e8",
"sign_type": "MD5"
}
验签(易支付):除 sign/sign_type 外的非空字段 ASCII 升序拼 k=v&… 后直接拼 KEY(无 &key=),MD5 小写。校验 trade_status==TRADE_SUCCESS 且 money 与订单一致。
回调处理四步(防伪造、防重复、防金额篡改)
退款
保护伞原生 API 与 Jeepay 兼容层均提供商户退款接口;易支付商户如需退款请联系运营方处理。
主 API 申请退款 POST /api/pay/refund
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| merchantNo | string | 是 | 商户号 |
| orderId | string | 是 | 原平台订单号 |
| mchRefundNo | string | 是 | 商户退款单号,唯一 |
| refundAmount | decimal | 是 | 退款金额(元),最小 0.01,且 ≤ 原订单可退余额 |
| reason | string | 否 | 退款原因 |
| notifyUrl | string | 否 | 退款结果通知地址 |
| timestamp / nonce | string | 是 | 防重放,均参与签名 |
| signType / sign | string | 是 | 签名 |
// 响应 R<RefundVO> { "code":200, "msg":"success", "data":{ "refundId":"R20260928000001", "orderId":"P20260928000001", "mchRefundNo":"RF001", "refundAmount":0.01, "payAmount":0.01, "status":0, // 0处理中 1成功 2失败 "channelRefundNo":"", "reason":"", "errorMsg":"" } }
查询退款:GET /api/pay/refund/{refundId}(附 merchantNo/appId/timestamp/nonce/signType/sign)。
Jeepay 申请退款 POST /api/jeepay/refund/refundOrder
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| mchNo / appId | string | 是 | 商户号 / 应用 ID |
| payOrderId | string | 二选一 | 原支付订单号 |
| mchOrderNo | string | 二选一 | 原商户订单号 |
| mchRefundNo | string | 是 | 商户退款单号,唯一 |
| refundAmount | int | 是 | 退款金额(分),须 > 0 |
| refundReason | string | 否 | 退款原因 |
| currency / clientIp / notifyUrl / extParam | string | 否 | 可选字段 |
| signType / sign | string | 是 | 签名 |
// 响应 data { "code":0, "msg":"SUCCESS", "data":{ "refundOrderId":"R20260928000001", "payOrderId":"P20260928000001", "mchRefundNo":"RF001", "refundAmount":1, "refundState":1, // 1=退款中 2=成功 3=失败 "channelRefundOrderNo":"...", "refundReason":"", "errMsg":"" } }
查询退款 POST /api/jeepay/refund/query
参数:mchNo/appId + refundOrderId|mchRefundNo 至少一个 + sign。
错误码汇总
| code | msg | 常见原因 |
|---|---|---|
| 400 | 参数错误 | 必填字段缺失/校验不通过(@Valid) |
| 1012 | IP不在白名单中 | 应用配了 IP 白名单且调用方 IP 不在其中 |
| 5001 | 应用不存在 | appId 错或不属于该商户 |
| 5002 | 应用已停用 | 应用被禁用 |
| 5006 | 商户不存在 | merchantNo 错 |
| 5007 | 商户已停用 | 商户被禁用 |
| 8001 | 订单不存在 | orderId 错或跨商户查询 |
| 8002 | 订单状态异常 | 关单/退款时状态不满足(如未支付却退款) |
| 8003 | 订单已过期 | 订单超时失效 |
| 8004 | 重复下单 | mchOrderNo 已存在 |
| 8005 | 金额异常 | 金额低于 0.01 / 超限额 / 与配置不符 |
| 8006 | 通道调用失败 | 下游支付通道异常 |
| 8007 | 退款失败 | 通道退款异常或超额 |
| 8016 | 签名验证失败 | 最常见:大小写/拼串/timestamp·nonce 未参与签名/时间戳过期/nonce 重复/字段集不一致 |
| msg | 常见原因 |
|---|---|
| 缺少必填参数(mchNo/appId/mchOrderNo/wayCode) | 下单必传字段缺失 |
| 支付金额必须大于0 | amount 为 0 或负数(单位分) |
| 商户不存在: {mchNo} | mchNo 错或未开通 |
| 商户已停用 | 商户被禁用 |
| 应用不存在或不属于该商户 | appId 错或不属于该 mchNo |
| 应用已停用 | 应用被禁用 |
| 签名验证失败 | 最常见:大小写/拼串/字段集/编码不一致 |
| payOrderId 和 mchOrderNo 至少传一个 | 查单/关单未传订单号 |
| 订单不存在 | 订单号错或不属该商户 |
| msg | 常见原因 |
|---|---|
| 缺少必填参数(pid/out_trade_no/money/notify_url) | 下单必传字段缺失 |
| 商户不存在 / 商户已停用 | pid 错或未开通/被禁 |
| 应用不存在 | pid 对应的应用未配 |
| 签名验证失败 | 最常见:用了大写/加了 &key=/空值参与签名 |
| 缺少参数 / 密钥错误 | api.php 查商户时 pid/key 不完整或错 |
| 缺少订单号参数 / 订单不存在 | 查单未传 trade_no/out_trade_no |
| 状态码 | 含义 |
|---|---|
| 200 | 请求已受理(看 body 里 code 判断业务成败) |
| 404 | 路径错(漏了 /api 前缀或拼写错) |
| 405 | 方法错(该接口只收 GET 或 POST) |
| 502 / 504 | 网关到支付服务不可达,稍后重试或联系运营方 |
支付方式 / 状态码对照
三套协议各自的支付方式取值与订单状态码对照,切换标签查看。保护伞原生为主,Jeepay / 易支付为兼容层。
| payMethod | 含义 | payMethod | 含义 |
|---|---|---|---|
| alipay | 支付宝 | wxpay | 微信 |
| qqpay | QQ钱包 | jdpay | 京东支付 |
| unionpay | 银联 | paypal | PayPal |
| xpay | 个人固码 | bank | 银行卡 |
| quick | 快捷支付 | — | — |
| status | 含义 | 退款单 status | 含义 |
|---|---|---|---|
| 0 | 待支付 | 0 | 退款处理中 |
| 1 | 支付中 | 1 | 退款成功 |
| 2 | 支付成功 | 2 | 退款失败 |
| 3 | 支付失败 | — | — |
| 4 | 已退款 | ||
| 5 | 部分退款 | ||
| 6 | 已关闭 | ||
| 7 | 已冻结 |
流程节点 flowNode:0 发起 → 1 进入风控 → 2 领取支付链接 → 3 已付款(单调递增,用于观测用户推进到哪一步)。接口成功码 code=200;支付最终结果必须以查单接口为准,异步通知仅作触发(见 异步通知)。
| wayCode | 含义 | wayCode | 含义 |
|---|---|---|---|
| ALI_BAR | 支付宝条码(被扫) | WX_BAR | 微信条码(被扫) |
| ALI_QR | 支付宝二维码(主扫) | WX_QR | 微信二维码(主扫) |
| ALI_LITE | 支付宝小程序 | WX_LITE | 微信小程序 |
| ALI_JSAPI | 支付宝生活号/JSAPI | WX_JSAPI | 微信公众号/JSAPI |
| ALI_WAP | 支付宝 WAP | WX_H5 | 微信 H5 |
| ALI_PC | 支付宝 PC 网页 | WX_APP | 微信 APP |
| ALI_APP | 支付宝 APP | YSF_BAR/QR/APP | 云闪付 |
| JD_BAR/JD_QR | 京东支付 | PAYPAL | PayPal |
| QR_CASHIER | 聚合扫码(收银台) | AUTO_BAR | 聚合条码(被扫) |
微信 JSAPI/小程序(WX_JSAPI / WX_LITE)需传 channelId=用户 openid。
| state | 含义 | refundState | 含义 |
|---|---|---|---|
| 0 | 初始/待支付 | 0 | 订单生成 |
| 1 | 支付中 | 1 | 退款中 |
| 2 | 支付成功 | 2 | 退款成功 |
| 3 | 支付失败 | 3 | 退款失败 |
| 4 | 已关闭 | 4 | 关闭 |
| 6 | 退款成功 | — | — |
| type | 含义 | 字段 | 取值 |
|---|---|---|---|
| alipay | 支付宝 | status | 1=已支付 / 0=未支付 |
| wxpay | 微信 | trade_status | TRADE_SUCCESS=成功 |
| qqpay | QQ钱包 | code | 1=成功 / -1=失败 |
| bank | 银行卡 | type | 不传走收银台 |
易支付:金额单位元(字符串),签名 MD5 小写、密钥裸拼;详见 易支付接口。
常见问题
验签总是失败?
- 先确认拼接方式:易支付末尾不能有 &key=(裸拼密钥),原生 / Jeepay 必须有。平台验签对 sign 大小写不敏感,所以若失败,真正的差异几乎总在待签字符串本身(字段集合 / &key=)。
- 只用实际发送的非空字段签名(空值易支付不签);排除 sign 与 signType/sign_type。
- 用 在线计算器 把你的参数粘进去算一个正确值,与你代码输出逐字对比。
- 中文参数确保 UTF-8;URL 参数签名时用原始值(不要提前 URL-encode)。
- JSON 报文下单时最容易错在:数字写成 1.00 而签 1(或反之)、传了空字符串、多传了未参与签名的字段、Content-Type 不是 application/json。
金额不对?
- Jeepay 传分(int):1 元 = 100;易支付传元(字符串):1 元 = "1.00"。
收不到异步通知?
- 确认 notifyUrl 公网可达、HTTPS 证书有效、返回 HTTP 200(非 2xx 会一直重试)。
- 用查单接口主动对账兜底;通知可能因重试而多次到达,必须幂等。
下单成功但 state 不是 2?
- state=1 仅表示订单已创建,真实支付结果以异步通知 state=2 或查单结果为准。
只按异步通知入账,行不行?
- 不行。任何人均能向你的公网 notifyUrl 伪造/重放通知,通知也可能丢失。必须验签后再调查单接口核实状态与金额,并做幂等;同时上定时对账任务兜底。
能同一条代码兼容两套协议吗?
- 不建议。签名与金额单位互斥;按你原系统选一套即可,平台侧运营会把你标为对应来源。