代收回调通知接口
说明: 当代收订单支付成功后,系统会向商户下单时传入的 noticeUrl 异步发送回调通知。
请求方式: POST
Content-Type: application/json
由系统主动发起,商户需要提供接收回调的 HTTP 接口。
一、回调通知参数
系统会以 JSON 格式向商户的 noticeUrl 发送以下参数:
| 参数名称 | 参数变量名 | 类型 | 必含 | 说明 |
|---|---|---|---|---|
| 商户订单号 | tradeNo | String | 是 | 商户下单时传入的订单号 |
| 订单金额 | orderAmount | String | 是 | 订单金额(元),保留两位小数 |
| 实际支付金额 | totalAmount | String | 是 | 实际支付金额(元),与 orderAmount 相同 |
| 平台订单号 | outTradeNo | String | 是 | 平台系统生成的订单号 |
| 第三方流水号 | otherTradeNo | String | 是 | 第三方支付渠道的交易流水号 |
| 交易时间 | tradeTime | String | 是 | 格式 yyyy-MM-dd HH:mm:ss,例如:2025-01-15 12:30:45 |
| 交易状态 | tradeStatus | String | 是 | "1" = 支付成功 |
| 附加字段 | attach | String | 否 | 商户下单时传入的附加数据(原样返回,未传入时为空字符串) |
| MD5签名 | sign | String | 是 | MD5签名值(32位大写),签名规则见下文 |
| RSA签名 | rsaSign | String | 否 | RSA签名值(Base64编码,SHA256WithRSA),商户可用服务端RSA公钥验签 |
交易状态说明
| 状态值 | 说明 |
|---|---|
| 1 | 支付成功 |
说明
代收订单仅在支付成功时才会发送回调通知。支付中、待支付状态不会发送回调。
二、回调请求示例
系统发送的 HTTP 请求:
POST {商户配置的noticeUrl} HTTP/1.1
Content-Type: application/json
Charset: UTF-8
{
"tradeNo": "ORDER20250115001",
"orderAmount": "100.00",
"totalAmount": "100.00",
"outTradeNo": "PO20250115001001",
"otherTradeNo": "THIRD20250115001",
"tradeTime": "2025-01-15 12:30:45",
"tradeStatus": "1",
"attach": "",
"sign": "3E541783111209F709ACDA0F4BFD17EA"
}三、商户响应要求
商户系统收到回调通知并处理完成后,必须在 HTTP Response Body 中返回包含 success 字符串的内容。
- 系统在判断返回值时,会先做
trim()和toLowerCase()处理,因此返回值前后空格和大小写不敏感 - 如果响应内容不包含
success,系统会认为通知失败并触发重试
正确响应示例
方式一:直接返回文字(推荐)
success方式二:返回 JSON
{
"code": 0,
"message": "success"
}四、重试机制
如果商户未返回 success(包括网络异常、超时、返回其他内容等情况),系统会按照以下策略进行重试:
| 重试次数 | 距首次通知的间隔 | 说明 |
|---|---|---|
| 第1次(首次) | 0 | 订单支付成功后立即发送 |
| 第2次 | 1 分钟 | - |
| 第3次 | 5 分钟 | - |
| 第4次 | 10 分钟 | - |
| 第5次 | 30 分钟 | - |
| 第6次 | 1 小时 | 最后一次重试 |
最多重试 6 次。达到上限后不再重试,商户可通过查询订单接口主动获取订单状态。
超时处理
如果系统发送通知时发生连接超时(connect_time_out),会立即再重试一次。若仍失败则计入重试次数。
五、回调签名验证
商户系统必须对回调数据进行签名验证,确保数据来源是支付平台。
系统提供两种签名方式,商户可任选其一进行验签(建议使用 RSA 签名):
| 签名字段 | 算法 | 说明 |
|---|---|---|
sign | MD5(32位大写) | 传统签名方式,需使用商户密钥 |
rsaSign | SHA256WithRSA(Base64) | RSA签名,需使用服务端RSA公钥验签 |
5.1 MD5 签名验证(sign 字段)
签名类型为 MD5(32位大写),生成步骤如下:
步骤 1:参数排序
将回调参数中除 sign 和 rsaSign 外的所有非空参数,按参数名 ASCII 字典序升序排序。
空值过滤
值为 null、空字符串 ""、"null" 的字段不参与签名。例如 attach 为空字符串时不会出现在签名串中。
步骤 2:拼接参数串
将排序后的参数以 key=value 形式用 & 连接:
key1=value1&key2=value2&key3=value3...步骤 3:计算密钥
key = MD5( alliessCode + ":" + alliesSecret )其中:
alliesCode= 友商编号(即 X-Allies-No)alliesSecret= 商户密钥(平台分配)- 输出为 32 位小写 hex 字符串
步骤 4:生成签名
将步骤2的结果与密钥拼接,进行 MD5 加密后转大写:
signTemp = 参数串 + "&key=" + key
sign = MD5(signTemp, "UTF-8").toUpperCase()MD5签名示例
假设回调参数为:
{
"tradeNo": "ORDER20250115001",
"orderAmount": "100.00",
"totalAmount": "100.00",
"outTradeNo": "PO20250115001001",
"otherTradeNo": "THIRD20250115001",
"tradeTime": "2025-01-15 12:30:45",
"tradeStatus": "1",
"attach": "",
"sign": "...",
"rsaSign": "..."
}- 排序后拼接(
attach为空被过滤,sign/rsaSign不参与):
orderAmount=100.00&otherTradeNo=THIRD20250115001&outTradeNo=PO20250115001001&tradeNo=ORDER20250115001&tradeStatus=1&tradeTime=2025-01-15 12:30:45&totalAmount=100.00- 拼接密钥后 MD5 并转大写得到
sign。
5.2 RSA 签名验证(rsaSign 字段,推荐)
签名类型为 SHA256WithRSA,使用服务端 回调签名私钥 签名,商户使用回调验签公钥验签。
回调验签公钥
回调验签公钥在管理后台「友商管理 → RSA密钥管理」页面生成时显示,仅显示一次,请妥善保管。 此公钥与下单签名私钥是不同的密钥对,请勿混用。
验签步骤
- 将回调参数中除
sign和rsaSign外的所有非空参数,按参数名 ASCII 字典序升序排序 - 以
key=value形式用&连接(空值字段过滤) - 使用服务端 RSA 公钥,对拼接后的字符串进行 SHA256WithRSA 验签
- 将验签结果与
rsaSign(Base64 解码后)比较
RSA验签示例
待签名内容(同 MD5 的参数串,但不拼接密钥):
orderAmount=100.00&otherTradeNo=THIRD20250115001&outTradeNo=PO20250115001001&tradeNo=ORDER20250115001&tradeStatus=1&tradeTime=2025-01-15 12:30:45&totalAmount=100.00用服务端 RSA 公钥 + SHA256WithRSA 算法验签即可。
PHP 验证示例(MD5 + RSA 双签名)
<?php
// 获取回调参数(JSON 格式)
$json = file_get_contents('php://input');
$params = json_decode($json, true);
if (!$params) {
echo 'fail';
exit;
}
// 保存签名
$receivedSign = $params['sign'];
$receivedRsaSign = isset($params['rsaSign']) ? $params['rsaSign'] : '';
unset($params['sign']);
unset($params['rsaSign']);
// 按参数名 ASCII 字典序排序
ksort($params);
// 拼接字符串(过滤空值)
$stringA = '';
foreach ($params as $k => $v) {
if ($v !== '' && $v !== null && $v !== 'null') {
$stringA .= $k . '=' . $v . '&';
}
}
$stringA = rtrim($stringA, '&');
// ========== 方式一:MD5 验签 ==========
$alliesCode = 'IG20250911001';
$alliesSecret = 'your_allies_secret';
$key = md5($alliesCode . ':' . $alliesSecret);
$signStr = $stringA . '&key=' . $key;
$calculatedSign = strtoupper(md5($signStr));
$md5Verified = ($receivedSign === $calculatedSign);
// ========== 方式二:RSA 验签(推荐) ==========
$rsaVerified = false;
if (!empty($receivedRsaSign)) {
$serverPublicKey = 'your_server_rsa_public_key';
$pubKeyId = openssl_pkey_get_public($serverPublicKey);
$rsaVerified = openssl_verify($stringA, base64_decode($receivedRsaSign), $pubKeyId, OPENSSL_ALGO_SHA256) === 1;
openssl_free_key($pubKeyId);
}
// 任一签名验证通过即可
if ($md5Verified || $rsaVerified) {
if ($params['tradeStatus'] === '1') {
// 支付成功,处理订单业务逻辑
// ... 更新订单状态、发货等
}
echo 'success';
} else {
echo 'fail';
}Java 验证示例(MD5 + RSA 双签名)
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.util.*;
import java.security.*;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
@RestController
@RequestMapping("/notify")
public class PayNotifyController {
@PostMapping("/pay")
public String notify(HttpServletRequest request) {
try {
// 读取请求体(JSON 格式)
BufferedReader reader = request.getReader();
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
// 解析 JSON 参数
Map<String, Object> paramMap = new com.alibaba.fastjson.JSON().parseObject(sb.toString(), Map.class);
// 提取并移除签名
String receivedSign = (String) paramMap.remove("sign");
String receivedRsaSign = (String) paramMap.remove("rsaSign");
// 按参数名 ASCII 字典序排序(使用 TreeMap)
Map<String, Object> sortedParams = new TreeMap<>(paramMap);
// 拼接字符串(过滤空值)
StringBuilder signStr = new StringBuilder();
for (Map.Entry<String, Object> entry : sortedParams.entrySet()) {
Object value = entry.getValue();
if (value != null && !value.toString().isEmpty() && !"null".equals(value.toString())) {
signStr.append(entry.getKey()).append("=").append(value).append("&");
}
}
if (signStr.length() > 0) {
signStr.deleteCharAt(signStr.length() - 1);
}
// ========== 方式一:MD5 验签 ==========
String alliesCode = "IG20250911001";
String alliesSecret = "your_allies_secret";
String key = md5(alliesCode + ":" + alliesSecret);
String calculatedSign = md5(signStr.toString() + "&key=" + key).toUpperCase();
boolean md5Verified = receivedSign != null && receivedSign.equals(calculatedSign);
// ========== 方式二:RSA 验签(推荐) ==========
boolean rsaVerified = false;
if (receivedRsaSign != null && !receivedRsaSign.isEmpty()) {
rsaVerified = verifyRsaSign(signStr.toString(), receivedRsaSign, SERVER_RSA_PUBLIC_KEY);
}
// 任一签名验证通过即可
if (md5Verified || rsaVerified) {
String tradeStatus = paramMap.get("tradeStatus").toString();
if ("1".equals(tradeStatus)) {
// 支付成功,处理订单业务逻辑
}
return "success";
} else {
return "fail";
}
} catch (Exception e) {
e.printStackTrace();
return "fail";
}
}
/** RSA SHA256WithRSA 验签 */
private static boolean verifyRsaSign(String content, String sign, String publicKeyStr) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(publicKeyStr);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey pubKey = keyFactory.generatePublic(keySpec);
Signature signature = Signature.getInstance("SHA256WithRSA");
signature.initVerify(pubKey);
signature.update(content.getBytes("UTF-8"));
return signature.verify(Base64.getDecoder().decode(sign));
}
private static String md5(String input) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(input.getBytes("UTF-8"));
StringBuilder hexString = new StringBuilder();
for (byte b : digest) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
}
}六、注意事项
- 幂等性:建议商户系统对同一
outTradeNo的回调通知做幂等处理,避免重复处理(系统可能因网络重试发送多次) - 签名验证:必须对所有回调通知进行签名验证,防止伪造请求
- 交易状态判断:
tradeStatus为"1"时才标记订单为成功状态 - 返回要求:无论业务处理成功与否,只要收到通知都应返回
success字符串,否则会触发最多 6 次的重试 - 回调格式:代收回调为 JSON 格式(
Content-Type: application/json) - 异步处理:建议商户将回调通知放入异步队列处理,避免阻塞导致响应超时
- 主动查询:如长时间未收到回调,可通过 查询订单接口 主动查询订单状态
- 空值过滤:验签时需过滤掉空值字段,与服务端保持一致
