代收回调通知接口
说明: 根据支付下单接口中回调地址(noticeUrl),进行异步通知。
请求方式: POST
Content-Type: application/json 或 application/x-www-form-urlencoded
一、回调通知参数
| 参数名称 | 参数变量名 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| 签名 | sign | String | 是 | 参数加密后算法生成,签名规则见附录 |
| 友商唯一订单号 | tradeNo | String(50) | 是 | 友商系统中的唯一订单号 |
| 友商订单金额 | orderAmount | Number | 是 | 订单总金额以元为单位,精确到小数点后两位 |
| 平台实际支付金额 | totalAmount | Number(11.2) | 是 | 客户实际支付金额 |
| 支付平台订单号 | outTradeNo | String(32) | 是 | 平台方支付平台里唯一的订单号 |
| 支付平台订单时间 | tradeTime | String(20) | 是 | 支付平台订单交易时间,格式为 yyyy-MM-dd HH:mm:ss,例如:2015-01-01 12:45:52 |
| 交易状态 | tradeStatus | Int | 是 | 1 = 交易成功,0 = 交易失败,-1 = 交易中 |
| 附加字段 | attach | String(30) | 否 | 附加参数(原样返回) |
| 第三方单号 | otherTradeNo | String(32) | 是 | 第三方平台单号 |
交易状态说明
| 状态值 | 说明 |
|---|---|
| 1 | 交易成功 |
| 0 | 交易失败 |
| -1 | 交易中 |
二、回调请求示例
json
{
"sign": "3E541783111209F709ACDA0F4BFD17EA",
"tradeNo": "ORDER20250115001",
"orderAmount": "100.00",
"totalAmount": "100.00",
"outTradeNo": "PO20250115001001",
"tradeTime": "2025-01-15 12:30:45",
"tradeStatus": 1,
"attach": "eyAiYmF...",
"otherTradeNo": "THIRD20250115001"
}三、商户响应要求
当通知方式为服务器后台异步通知时,友商系统在收到通知并处理完成后,必须打印输出包含 success 这个字符串。
如果响应内容不包含 success,支付平台系统会认为通知失败。
正确响应示例
**方式一:直接返回文字
text
success**方式二:返回 JSON
json
{
"code": 0,
"message": "success"
}四、回调签名验证
友商系统需要对回调数据进行签名验证,确保数据来源是支付平台。
验证步骤:
- 接收回调参数(排除
sign字段) - 将非空参数按参数名 ASCII 字典序排序
- 拼接成
key=value&key=value格式 - 末尾拼接
&key=商户密钥 - 对整个字符串进行 MD5 加密并转大写
- 与传入的 sign 进行对比
PHP 验证示例
php
<?php
// 获取回调参数(POST请求)
$params = $_POST; // 或 $_GET,取决于请求方式
// 保存sign后用于验证
$receivedSign = $params['sign'];
unset($params['sign']);
// 按参数名ASCII字典序排序
ksort($params);
// 拼接字符串
$stringA = '';
foreach ($params as $k => $v) {
if ($v !== '' && $v !== null) {
$stringA .= $k . '=' . $v . '&';
}
}
$stringA = rtrim($stringA, '&');
// 拼接密钥
$key = 'your_api_key_here';
$signStr = $stringA . '&key=' . $key;
$calculatedSign = strtoupper(md5($signStr));
// 验证签名
if ($receivedSign === $calculatedSign) {
// 签名验证通过
if ($params['tradeStatus'] == 1) {
// 交易成功,处理订单业务逻辑
// ...
echo 'success';
} else {
// 交易失败或处理中
echo 'success';
}
} else {
// 签名验证失败,不处理业务
echo 'fail';
}Java 验证示例
java
import java.util.*;
import java.security.MessageDigest;
@RequestMapping("/notify")
@ResponseBody
public String notify(HttpServletRequest request) {
// 获取所有参数
Map<String, String[]> parameterMap = request.getParameterMap();
Map<String, String> params = new TreeMap<>();
String receivedSign = null;
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
String key = entry.getKey();
String value = entry.getValue()[0];
if ("sign".equals(key)) {
receivedSign = value;
} else if (value != null && !value.isEmpty()) {
params.put(key, value);
}
}
// 拼接字符串 (TreeMap已自动排序)
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : params.entrySet()) {
if (entry.getValue() != null && !entry.getValue().isEmpty()) {
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
}
if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1);
// 拼接密钥并生成签名
String key = "your_api_key_here";
String signStr = sb.toString() + "&key=" + key;
String calculatedSign = md5(signStr).toUpperCase();
// 验证签名
if (receivedSign != null && receivedSign.equals(calculatedSign)) {
// 签名验证通过,处理业务逻辑
return "success";
} else {
return "fail";
}
}
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();
}五、注意事项
- 幂等性:建议友商系统对同一订单号的回调通知做幂等处理,避免重复处理
- 签名验证:必须对所有回调通知进行签名验证,防止伪造请求
- 交易状态判断:tradeStatus=1 时才标记订单为成功状态
- 返回要求:无论业务处理成功与否,都应返回
success字符串防止重复通知 - 业务处理:建议将回调通知放入异步队列处理,避免超时
