Guide
接入须知
| 项目 | 说明 |
|---|---|
| 网关地址 | https://当前站点域名(下文 {BASE} 代指) |
| 请求方式 | HTTP POST,请求体 JSON(UTF-8) |
| 请求头 | Content-Type: application/json · X-App-Key: {商户AppKey} |
| 金额单位 | 分(10.00 元传 1000) |
| 时间格式 | RFC3339(UTC,带 Z 后缀) |
| 统一应答 | {"code":0,"message":"ok","data":{…}},code=0 成功,非 0 时 message 为失败原因 |
已在用彩虹易支付 Pro?下载官方插件上传到易支付站点
plugins 目录,即可把本平台作为上游支付通道,见下方「易支付插件」章节。Guide
签名规则
- 取除
sign外所有值非空的参数; - 按参数名 ASCII 升序排序,拼接为
k1=v1&k2=v2…; - 末尾拼接
&key={AppSecret}; - MD5 取 32 位大写十六进制即为
sign。
raw = "amount=1000&merchantOrderNo=NO123&subject=测试" + "&key=" + secret sign = MD5(raw).大写
API
提交支付
POST{BASE}/api/gateway/orders
请求参数
| 参数 | 必填 | 类型 | 说明 |
|---|---|---|---|
merchantOrderNo | 必填 | string | 商户系统内唯一订单号 |
amount | 必填 | int | 支付金额,单位分(10.00 元传 1000) |
subject | 必填 | string | 商品标题 |
notifyUrl | 必填 | string | 异步通知地址,必须 https 公网地址 |
payMethod | 可选 | string | alipay / wxpay / qqpay;留空由收银台选择 |
channelCode | 可选 | string | 指定支付通道编码,通常留空由平台路由 |
returnUrl | 可选 | string | 支付完成回跳地址(仅展示用) |
expireMinutes | 可选 | int | 订单有效期(分钟),默认 5 |
extra | 可选 | string | 附加参数 JSON,可传 openid / buyerId / payer |
sign | 必填 | string | 按签名规则计算 |
请求示例
POST {BASE}/api/gateway/orders
Content-Type: application/json
X-App-Key: 1001
{
"merchantOrderNo": "NO1756512345",
"amount": 1000,
"subject": "测试商品",
"notifyUrl": "https://商户域名/notify",
"payMethod": "alipay",
"sign": "A1B2C3..."
}返回示例
{
"code": 0,
"message": "ok",
"data": {
"orderNo": "P1756512345678",
"merchantOrderNo": "NO1756512345",
"payUrl": "https://...",
"payType": "page",
"cashierUrl": "https://平台域名/pay/P1756512345678"
}
}幂等:同一
merchantOrderNo 原单待支付时返回原单;已终态则报「merchantOrderNo 已存在」。推荐跳转 cashierUrl 聚合收银台。API
查询订单
POST{BASE}/api/gateway/orders/query
| 参数 | 必填 | 类型 | 说明 |
|---|---|---|---|
orderNo | 二选一 | string | 平台订单号 |
merchantOrderNo | 二选一 | string | 商户订单号 |
sign | 必填 | string | 按签名规则计算 |
返回示例
{
"code": 0,
"data": {
"orderNo": "P1756512345678", "merchantOrderNo": "NO1756512345",
"amount": 1000, "fee": 0, "actualAmount": 1000, "refundedAmount": 0,
"status": "SUCCESS",
"channelCode": "...", "payMethod": "alipay",
"createdAt": "2026-08-30T02:00:00Z",
"paidAt": "2026-08-30T02:01:00Z"
}
}查询待支付订单时会同步向上游查证一次,上游已支付会自动补单并返回最新状态。
API
退款
POST{BASE}/api/gateway/refunds
| 参数 | 必填 | 类型 | 说明 |
|---|---|---|---|
orderNo / merchantOrderNo | 二选一 | string | 目标订单 |
refundAmount | 必填 | int | 退款金额(分);支持部分退款,累计不可超原单金额 |
reason | 可选 | string | 退款原因 |
sign | 必填 | string | 按签名规则计算 |
仅 SUCCESS / PARTIAL_REFUND 状态可退,返回 data.refundNo(R 开头)。
Callback
异步通知
支付完成后平台向 notifyUrl 发起 POST(Content-Type: application/json):
{
"orderNo": "P1756512345678",
"merchantOrderNo": "NO1756512345",
"amount": "1000",
"status": "SUCCESS",
"channelCode": "...",
"payTime": "2026-08-30T02:01:00Z",
"sign": "..."
}| 步骤 | 说明 |
|---|---|
| 验签 | 剔除 sign 后按签名规则重算比对;一致且 status=SUCCESS 后再发货 |
| 应答 | HTTP 200,完整纯文本 SUCCESS(兼容 OK),忽略大小写及首尾空白。不要返回 JSON、HTML 或包含其他文字的应答;响应读取失败或超过 4096 字节均不算成功。 |
| 重试 | 商户未正确应答时,每轮最多尝试 3 次(含首次);无持久化自动补发计划。商户必须按订单号幂等处理。 |
| 回跳 | returnUrl 为浏览器跳转,仅展示用,不可作为发货依据 |
Reference
订单状态
| 状态 | 含义 |
|---|---|
PENDING | 待支付 |
SUCCESS | 已支付 |
FAILED | 支付失败 |
CLOSED | 已关闭 |
REFUNDING | 退款中 |
REFUNDED | 已全额退款 |
PARTIAL_REFUND | 部分退款 |
Plugin
易支付插件
正在使用彩虹易支付 Pro?无需改造现有系统:下载本平台官方支付插件,上传到你易支付网站的 plugins 目录,即可把本平台添加为上游支付通道。
1. 下载插件压缩包(下方「文档下载」卡片),解压得到 fourpay 文件夹 2. 将 fourpay 文件夹上传到易支付网站根目录的 plugins/ 下 3. 易支付后台 → 支付接口 → 支付插件 → 刷新插件列表 → 启用「聚合支付云」 4. 编辑通道,填写:网关地址 / 商户AppKey / 商户AppSecret 5. 保存后在支付方式中勾选 支付宝 / 微信 / QQ 钱包
要求:易支付站点需 HTTPS(平台异步通知要求 https 地址);支持支付方式 alipay / wxpay / qqpay;支持后台发起退款。
SDK
示例代码
<?php
$base = 'https://平台域名'; $appKey = '商户AppKey'; $secret = '商户AppSecret';
function sign_json(array $p, string $secret): string {
unset($p['sign']); ksort($p); $pairs = [];
foreach ($p as $k => $v) { if ($v !== '' && $v !== null) $pairs[] = "$k=$v"; }
return strtoupper(md5(implode('&', $pairs) . '&key=' . $secret));
}
$order = ['merchantOrderNo' => 'NO'.time(), 'amount' => 1000, 'subject' => '测试商品',
'notifyUrl' => 'https://商户域名/notify', 'payMethod' => 'alipay'];
$order['sign'] = sign_json($order, $secret);
$ch = curl_init($base.'/api/gateway/orders');
curl_setopt_array($ch, [CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-App-Key: '.$appKey],
CURLOPT_POSTFIELDS => json_encode($order), CURLOPT_RETURNTRANSFER => true]);
$resp = json_decode(curl_exec($ch), true);
if ($resp['code'] === 0) header('Location: '.$resp['data']['cashierUrl']);
// 回调验签(notify.php):
// $input = json_decode(file_get_contents('php://input'), true);
// $sign = $input['sign']; unset($input['sign']);
// if (sign_json($input, $secret) === $sign && $input['status'] === 'SUCCESS') { /* 发货 */ echo 'SUCCESS'; }import hashlib, json, time, urllib.request
BASE, APPKEY, SECRET = 'https://平台域名', '商户AppKey', '商户AppSecret'
def sign(p):
items = sorted((k, str(v)) for k, v in p.items() if k != 'sign' and str(v) != '')
return hashlib.md5(('&'.join(f'{k}={v}' for k, v in items) + '&key=' + SECRET).encode()).hexdigest().upper()
p = {'merchantOrderNo': 'NO%d' % time.time(), 'amount': 1000, 'subject': '测试商品',
'notifyUrl': 'https://商户域名/notify', 'payMethod': 'alipay'}
p['sign'] = sign(p)
req = urllib.request.Request(BASE + '/api/gateway/orders', data=json.dumps(p).encode(),
headers={'Content-Type': 'application/json', 'X-App-Key': APPKEY})
print(json.load(urllib.request.urlopen(req))['data']['cashierUrl'])
# 回调验签:同一 sign() 重算比对,status==SUCCESS 后发货并响应 SUCCESSconst crypto = require('crypto');
const BASE = 'https://平台域名', APPKEY = '商户AppKey', SECRET = '商户AppSecret';
const sign = p => {
const raw = Object.keys(p).filter(k => p[k] !== '' && p[k] != null && k !== 'sign')
.sort().map(k => k + '=' + p[k]).join('&') + '&key=' + SECRET;
return crypto.createHash('md5').update(raw).digest('hex').toUpperCase();
};
const p = { merchantOrderNo: 'NO' + Date.now(), amount: 1000, subject: '测试商品',
notifyUrl: 'https://商户域名/notify', payMethod: 'alipay' };
p.sign = sign(p);
const r = await fetch(BASE + '/api/gateway/orders', {
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-App-Key': APPKEY },
body: JSON.stringify(p) });
const j = await r.json();
console.log(j.data.cashierUrl);
// 回调验签(Express):app.post('/notify', express.json(), (req, res) => {
// const { sign: s, ...p } = req.body;
// if (sign(p) === s && p.status === 'SUCCESS') { /* 发货 */ res.send('SUCCESS'); }
// });import java.net.URI; import java.net.http.*; import java.nio.charset.StandardCharsets;
import java.security.MessageDigest; import java.util.*;
public class PayDemo {
static final String BASE = "https://平台域名", APPKEY = "商户AppKey", SECRET = "商户AppSecret";
static String sign(Map<String, String> p) throws Exception {
TreeMap<String, String> t = new TreeMap<>(p); t.remove("sign");
StringBuilder sb = new StringBuilder();
t.forEach((k, v) -> { if (v != null && !v.isEmpty()) sb.append(k).append('=').append(v).append('&'); });
sb.append("key=").append(SECRET);
byte[] d = MessageDigest.getInstance("MD5").digest(sb.toString().getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (byte b : d) hex.append(String.format("%02X", b));
return hex.toString();
}
public static void main(String[] args) throws Exception {
Map<String, String> p = new LinkedHashMap<>();
p.put("merchantOrderNo", "NO" + System.currentTimeMillis());
p.put("amount", "1000"); p.put("subject", "测试商品");
p.put("notifyUrl", "https://商户域名/notify"); p.put("payMethod", "alipay");
p.put("sign", sign(p));
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/api/gateway/orders"))
.header("Content-Type", "application/json").header("X-App-Key", APPKEY)
.POST(HttpRequest.BodyPublishers.ofString(new JSONObject(p).toString())).build();
System.out.println(HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString()).body());
}
}func sign(p map[string]string, secret string) string {
keys := []string{}
for k, v := range p { if v != "" && k != "sign" { keys = append(keys, k) } }
sort.Strings(keys)
parts := []string{}
for _, k := range keys { parts = append(parts, k+"="+p[k]) }
sum := md5.Sum([]byte(strings.Join(parts, "&") + "&key=" + secret))
return strings.ToUpper(hex.EncodeToString(sum[:]))
}
// 下单:POST JSON 到 /api/gateway/orders,头带 X-App-Key
// 回调验签:同一 sign() 重算比对,status==SUCCESS 后发货并响应 SUCCESSpublic static string Sign(Dictionary<string, string> p, string secret)
{
var raw = string.Join("&", p
.Where(kv => kv.Key != "sign" && !string.IsNullOrEmpty(kv.Value))
.OrderBy(kv => kv.Key, StringComparer.Ordinal)
.Select(kv => kv.Key + "=" + kv.Value)) + "&key=" + secret;
using var md5 = MD5.Create();
return Convert.ToHexString(md5.ComputeHash(Encoding.UTF8.GetBytes(raw)));
}
// 下单:POST JSON 到 /api/gateway/orders,头带 X-App-Key(.NET 6+,无第三方依赖)
// 回调验签:同一 Sign() 重算比对,status==SUCCESS 后发货并响应 SUCCESSrequire 'digest'
def sign(p, secret)
raw = p.reject { |k, v| k.to_s == 'sign' || v.nil? || v.to_s.empty? }
.sort_by { |k, _| k.to_s }
.map { |k, v| "#{k}=#{v}" }.join('&') + '&key=' + secret
Digest::MD5.hexdigest(raw).upcase
end
# 下单:POST JSON 到 /api/gateway/orders,头带 X-App-Key(仅标准库)
# 回调验签:同一 sign() 重算比对,status==SUCCESS 后发货并响应 'SUCCESS'Download
文档下载
📄平台 API 完整文档(Markdown)单文件交付给开发或 AI 阅读:参数表、签名、示例、FAQ→ 🧩易支付 Pro 支付插件(xypay)上传到易支付站点 plugins 目录,把本平台添加为上游支付通道→FAQ
常见问题
| 现象 | 原因与处理 |
|---|---|
| 提示签名错误 | 检查空值是否剔除、是否按 ASCII 升序、是否以 &key= 结尾、结果是否大写 |
| notifyUrl 无效 / 回调 301 | 回调地址必须为最终 https 地址;http 会被强制升级导致 301 |
| 应答 200 但仍显示回调失败 | 应答体须完整匹配纯文本 SUCCESS 或兼容的 OK,不能返回 JSON、HTML、NOT SUCCESS 等内容。易支付兼容回调仅接受纯文本 success。 |
| merchantOrderNo 已存在 | 更换商户订单号,或改用查询接口 |
| 无可用支付通道 | 检查通道状态为启用、插件配置完整 |
| 风控拦截 | 检查单笔/单日限额、IP 黑名单与下单频率(系统设置 → 风控管理) |