本系统向签约渠道提供 H5 实物电商的下单、支付、退款、查询能力。支付产品为支付宝手机网站支付(alipay.trade.wap.pay)。
sku_id,只传 target_amount(应付金额,单位:元)。实付金额即 target_amount,平台自动匹配一个售价最接近的在售商品,仅用于订单展示与支付宝商品说明(数量按 1 件记录);不收运费、不扣库存、不校验商品授权范围,匹配不到商品也照常下单;sku_id 指定商品,实付金额 = 商品售价 × quantity + 商户后台配置的运费,校验商品归属/授权并真实扣减库存;app_secret 只允许保存在渠道服务端,严禁下发 App/H5/小程序前端。
除异步通知由我方发起外,所有请求均需签名。请求格式 application/x-www-form-urlencoded 或 JSON。
sign、sign_type 及值为空的参数;key=value 用 & 拼接;APP_SECRET(不带 &key= 前缀,与彩虹易支付一致);sign 字段。target_amount 等)。任何新传入的参数,都必须同步加入签名计算,否则验签失败、下单不成功。
示例待签名串:
attach=test&partner_request_no=REQ20240520001&pid=DEMO20240001&target_amount=50.00&key=渠道密钥
POST /api/partner/order/create
| 参数 | 必填 | 说明 |
|---|---|---|
| pid | 是 | 平台签发的渠道商户ID |
| partner_request_no | 是 | 渠道侧唯一请求号,最长64;重复请求返回原订单(幂等) |
| partner_user_id | 是 | 终端用户标识,用于风控溯源 |
| sku_id | 否 | 商品ID;传了则走方式B,按商品售价×数量+运费计价 |
| target_amount | 否 | 应付金额(元),不传 sku_id 时必传;实付即该金额,平台仅按金额就近匹配商品用于展示,不收运费、不扣库存 |
| quantity | 否 | 数量,默认1 |
| receiver_name | 否 | 收货人姓名(可选项,可下单时传,也可支付后由平台后台补录) |
| receiver_phone | 否 | 收货人手机号(可选项) |
| receiver_address | 否 | 收货地址(可选项,可下单时传,也可支付后由平台后台补录) |
| notify_url | 否 | 支付/退款结果异步通知地址,不传则用后台配置的默认地址 |
| return_url | 否 | 支付完成后浏览器跳转地址 |
| attach | 否 | 透传字段,原样返回/通知,最长255 |
| user_ip | 否 | 终端用户IP,用于风控与限流 |
| sign | 是 | MD5签名 |
| 字段 | 说明 |
|---|---|
| order_no | 平台订单号(同时作为支付宝 out_trade_no) |
| cashier_url | 平台收银台地址(展示商品/运费明细,推荐跳转此地址) |
| pay_url | 支付宝官方H5收银台直跳链接(可直接302) |
| product_amount / shipping_fee / pay_amount | 商品金额 / 运费 / 应付总额 |
| order_status | PENDING_PAYMENT / PAID / CLOSED / REFUNDED |
POST /api/partner/order/query
入参:pid、sign,以及 partner_request_no 与 order_no 二选一。
返回订单状态、商品/运费/总额、支付宝交易号、支付时间、退款状态与累计退款额、物流公司/单号、履约状态。建议异步通知与主动轮询并用。
POST /api/partner/order/refund(原路退回,需平台后台审核后执行)
| 参数 | 必填 | 说明 |
|---|---|---|
| pid / sign | 是 | 同通用规则 |
| partner_refund_no | 是 | 渠道退款请求号,唯一,幂等 |
| order_no 或 partner_request_no | 是 | 定位原订单 |
| refund_amount | 是 | 本次退款金额,支持部分退款;不得超过订单可退余额 |
| reason | 否 | 退款原因 |
提交后返回 refund_no 与 refund_status=PENDING;审核结果通过异步通知下发,也可按退款单号在平台后台跟进。
POST /api/partner/product/list,入参 pid、sign。
返回 list: [{ sku_id, name, price, stock_quantity }]。分销渠道返回被授权的平台商品;独立收款商户返回归属本商户的商品。
POST /api/partner/order/list
入参:pid、sign、可选 page(默认1)、page_size(默认20,最大50)。返回分页订单与费用/退款/物流字段。
POST /api/partner/config/notify-url,入参 pid、notify_url、sign。保存后未在下单时传 notify_url 的订单使用该地址。
平台以 application/x-www-form-urlencoded POST 到渠道 notify_url,参数同样按 MD5 规则签名,渠道侧必须验签。
支付成功 trade_status=TRADE_SUCCESS:含 pid、order_no、partner_request_no、total_amount、alipay_trade_no、attach、timestamp、sign。
退款结果:trade_status=REFUND_SUCCESS / REFUND_REJECTED,含 refund_no、partner_refund_no、refund_amount、result_msg、sign。
渠道处理成功须响应纯文本 success;否则平台会重试。
| msg(示例) | 含义/处理 |
|---|---|
| 签名校验失败 | 检查排序、拼接、密钥是否一致,编码是否 UTF-8 |
| 来源IP不在白名单 | 渠道服务端出口IP需在后台白名单内(未配置则不限制) |
| 该渠道无权销售此商品 / 该商品不属于当前商户 | sku_id 不在授权目录或不属于该商户,先调商品列表接口 |
| 商品已下架 / 商品库存不足 | 更换 sku_id 或联系平台补货 |
| 操作过于频繁 | 同 pid/IP/用户 60秒内超过后台配置上限(默认5笔) |
| 退款金额超过可退余额 | 核对已累计退款金额后重提 |
<?php
function sign(array $params, string $secret): string {
unset($params['sign'], $params['sign_type']);
$params = array_filter($params, fn($v) => $v !== null && $v !== '');
ksort($params); // ASCII 升序
$str = urldecode(http_build_query($params)); // k=v&k2=v2
return md5($str . $secret);
}
$params = [
'pid' => 'DEMO20240001',
'partner_request_no' => 'REQ' . date('YmdHis'),
'partner_user_id' => 'U10086',
// 方式A:不指定商品,由平台按金额匹配——target_amount 必须参与签名
'target_amount' => '50.00',
// 方式B:也可改用 'sku_id' => '1' 精确指定商品
'quantity' => '1',
'attach' => 'campaign_a',
];
$params['sign'] = sign($params, '渠道密钥');
$ch = curl_init('https://你的域名/api/partner/order/create');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($params),
CURLOPT_RETURNTRANSFER => true,
]);
$res = json_decode(curl_exec($ch), true);
header('Location: ' . $res['data']['cashier_url']); // 或 pay_url
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.*;
public class PartnerSign {
static String sign(Map<String,String> p, String secret) throws Exception {
var keys = new TreeSet<>(p.keySet()); // ASCII 升序
var sb = new StringBuilder();
for (String k : keys) {
if (k.equals("sign") || k.equals("sign_type")) continue;
String v = p.get(k);
if (v == null || v.isEmpty()) continue;
if (sb.length() > 0) sb.append('&');
sb.append(k).append('=').append(v); // 值不做URLEncode
}
sb.append(secret);
var md = MessageDigest.getInstance("MD5");
byte[] b = md.digest(sb.toString().getBytes(StandardCharsets.UTF_8));
var hex = new StringBuilder();
for (byte x : b) hex.append(String.format("%02x", x));
return hex.toString();
}
}
import hashlib, time, requests
def sign(params: dict, secret: str) -> str:
items = [(k, v) for k, v in params.items()
if k not in ("sign", "sign_type") and v not in (None, "")]
items.sort(key=lambda x: x[0]) # ASCII 升序
raw = "&".join(f"{k}={v}" for k, v in items)
return hashlib.md5((raw + secret).encode("utf-8")).hexdigest()
params = {
"pid": "DEMO20240001",
"partner_request_no": f"REQ{int(time.time())}",
"partner_user_id": "U10086",
# 不指定商品,平台按金额匹配最接近的商品;target_amount 必须参与签名
"target_amount": "50.00",
}
params["sign"] = sign(params, "渠道密钥")
r = requests.post("https://你的域名/api/partner/order/create", data=params, timeout=10)
print(r.json()["data"]["cashier_url"])
const crypto = require('crypto');
function verifyNotify(params, secret) {
const given = params.sign;
const items = Object.keys(params)
.filter(k => !['sign', 'sign_type'].includes(k) && params[k] !== '')
.sort().map(k => `${k}=${params[k]}`);
const expected = crypto.createHash('md5')
.update(items.join('&') + secret).digest('hex');
return given === expected;
}
// 验签通过且 trade_status === 'TRADE_SUCCESS' 后,响应字符串 success
pid / user_ip / partner_user_id 每 60 秒最多 5 笔(以后台配置为准)。
数据留存:订单、支付流水、回调原文、物流记录保留不少于 3 年。