订单异步通知
接口说明
异步通知回调接口是数汇科技支付系统在订单支付成功后,主动向商户服务器发送的支付结果通知。商户通过该接口可以实时获取订单支付状态,确保业务处理的及时性和准确性。
通知方式
POST
通知地址
商户下单时提供的
notifyUrl 地址数据格式
application/x-www-form-urlencoded 或 application/json
重要说明:异步通知是确保支付结果准确性的重要机制,商户必须正确处理该回调接口。
异步通知流程图
通知参数
数汇科技系统会以 POST 方式向商户在下单时提供的 notifyUrl 地址发送支付结果通知。
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
userId | String | Y | 由我司分配的唯一用户编号(代理通用) |
orderNo | String | Y | 商户订单编号 |
transactionNo | String | Y | 系统订单编号(平台订单号) |
amount | String | Y | 订单金额(元) |
sign | String | Y | 签名(签名方式同下单时一致) |
通知示例
Form Data
{
"userId": "your_open_userid",
"orderNo": "ORDER20250115001",
"transactionNo": "YSF202501151030001234567890",
"amount": "100.00",
"sign": "calculated_sign_value"
}
返回要求
重要要求:收到通知后需要返回大写
SUCCESS
商户服务器收到异步通知后,必须返回字符串 SUCCESS(大写),表示已成功接收通知。
| 返回示例 | 是否正确 | 说明 |
|---|---|---|
SUCCESS |
✓ 正确 | 正确返回,大写 |
success |
✗ 错误 | 错误:小写 |
OK |
✗ 错误 | 错误:错误字符串 |
{"code": "success"} |
✗ 错误 | 错误:JSON 格式 |
重试机制
重试规则:如未返回 SUCCESS,系统将按以下频率进行回调,最多回调 15 次。
| 回调次数 | 重试间隔 | 说明 |
|---|---|---|
| 首次回调失败后 | 5 秒 | 快速重试 |
| 第二次 | 30 秒 | 短期重试 |
| 第三次 | 60 秒 | 中期重试 |
| 第四次 | 10 分钟 | 长期重试开始 |
| 第五次及后续 | 1 小时 | 定期重试直到 15 次 |
签名验证
收到回调参数后,需要验证签名以确保请求来源的合法性。签名方式与下单时保持一致(RSA2 / MD5 / SM2)。
本页为接口介绍,仅作示例参考。各签名方式的完整实现请查看「通用规则 - 签名规则」页面。
验签流程
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 获取回调数据 | 从请求 Body 中获取原始数据(支持 JSON 或 URL 编码格式) |
| 2 | 解析参数 | 根据数据格式解析为关联数组 / 对象,包含 userId、orderNo、sign 等字段 |
| 3 | 提取并移除 sign | 保存 sign 参数值,然后从参数中移除 sign 字段 |
| 4 | 构建验签字符串 | 将剩余参数按字母排序后转为 JSON 字符串(不转义中文、不转义斜杠) |
| 5 | 验证签名 | 使用对应签名算法(RSA2 / MD5 / SM2)验证签名 |
| 6 | 处理业务逻辑 | 验签成功后更新订单状态,返回 SUCCESS |
代码示例
PHP
<?php
/** 异步回调处理示例 */
public function notify()
{
// 获取回调参数
$param = [
'userId' => $_POST['userId'],
'orderNo' => $_POST['orderNo'],
'transactionNo' => $_POST['transactionNo'],
'amount' => $_POST['amount'],
];
// 对参数进行排序
ksort($param);
// 验证签名(根据签名方式选择 RSA2 或 MD5)
$sign = $_POST['sign'];
$verifyResult = $this->verifySign($param, $sign);
if ($verifyResult && !empty($sign)) {
// 签名验证成功
$outTradeNo = $_POST['orderNo'];
// 幂等判断:检查订单是否已处理
$order = $this->orderModel->where('orderNo', $outTradeNo)->find();
if ($order && $order['status'] == 1) {
// 订单已处理,直接返回成功
return 'SUCCESS';
}
// 更新订单状态
$this->orderModel->where('orderNo', $outTradeNo)->update([
'status' => 1,
'transactionNo' => $_POST['transactionNo'],
'pay_time' => date('Y-m-d H:i:s')
]);
return 'SUCCESS'; // 必须大写
} else {
return 'sign fail';
}
}
/** MD5 验签方法 */
private function md5Verify($data, $sign, $md5Key)
{
ksort($data);
$str = '';
foreach ($data as $k => $v) {
$str .= "&{$k}={$v}";
}
$str = substr($str, 1);
return md5($str . $md5Key) === $sign;
}
/** RSA2 验签方法 */
private function rsaVerify($data, $sign, $publicKey)
{
ksort($data);
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$res = "-----BEGIN PUBLIC KEY-----\n" .
wordwrap($publicKey, 64, "\n", true) .
"\n-----END PUBLIC KEY-----";
$pubkeyid = openssl_pkey_get_public($res);
return openssl_verify($json, base64_decode($sign), $pubkeyid, OPENSSL_ALGO_SHA256) === 1;
}
Java
import java.util.*;
import javax.servlet.http.HttpServletRequest;
/** 异步通知处理示例 */
@PostMapping("/notify")
public String handleNotify(HttpServletRequest request) {
try {
// 获取回调参数
Map<String, String> params = new TreeMap<>();
params.put("userId", request.getParameter("userId"));
params.put("orderNo", request.getParameter("orderNo"));
params.put("transactionNo", request.getParameter("transactionNo"));
params.put("amount", request.getParameter("amount"));
String sign = request.getParameter("sign");
// 验证签名
boolean verifyResult = verifySign(params, sign);
if (verifyResult) {
String outTradeNo = params.get("orderNo");
// 幂等判断
Order order = orderService.findByOutTradeNo(outTradeNo);
if (order != null && order.getStatus() == 1) {
return "SUCCESS";
}
// 更新订单状态
orderService.updatePayStatus(outTradeNo,
params.get("transactionNo"),
params.get("amount"));
return "SUCCESS";
} else {
return "sign fail";
}
} catch (Exception e) {
log.error("处理异步通知异常", e);
return "error";
}
}
/** MD5验签 */
private boolean verifySign(Map<String, String> params, String sign) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : params.entrySet()) {
if (sb.length() > 0) sb.append("&");
sb.append(entry.getKey()).append("=").append(entry.getValue());
}
sb.append(md5Key);
String calculatedSign = DigestUtils.md5Hex(sb.toString());
return calculatedSign.equals(sign);
}
Python (Flask)
from flask import Flask, request
import hashlib
app = Flask(__name__)
@app.route('/notify', methods=['POST'])
def handle_notify():
"""异步通知处理"""
try:
# 获取回调参数
params = {
'userId': request.form.get('userId'),
'orderNo': request.form.get('orderNo'),
'transactionNo': request.form.get('transactionNo'),
'amount': request.form.get('amount'),
}
sign = request.form.get('sign')
# 验证签名
if verify_sign(params, sign):
orderNo = params['orderNo']
# 幂等判断
order = Order.query.filter_by(orderNo=orderNo).first()
if order and order.status == 1:
return 'SUCCESS'
# 更新订单状态
order.status = 1
order.transactionNo = params['transactionNo']
order.pay_time = datetime.now()
db.session.commit()
return 'SUCCESS'
else:
return 'sign fail'
except Exception as e:
print(f"处理异步通知异常: {e}")
return 'error'
def verify_sign(params, sign):
"""MD5验签"""
md5_key = 'your_md5_key'
sorted_params = sorted(params.items())
sign_str = '&'.join([f'{k}={v}' for k, v in sorted_params])
sign_str += md5_key
calculated_sign = hashlib.md5(sign_str.encode()).hexdigest()
return calculated_sign == sign
if __name__ == '__main__':
app.run(debug=True)
注意事项
重要提醒:
- 商户必须在收到通知后验证签名,确保通知来源的合法性
- 商户处理完业务后必须返回大写 SUCCESS,否则系统会持续重试
- 通知可能会因网络等原因延迟或重复,商户需做好幂等处理
- 不能依赖通知的顺序,应以支付平台返回的支付状态为准
幂等性处理:
- 同一订单可能收到多次通知,商户需根据订单号做幂等判断
- 建议在处理前先检查订单状态,避免重复处理
- 使用数据库事务或分布式锁确保数据一致性
最佳实践:
- 先验签再处理业务,验签失败直接返回错误
- 验签成功后立即返回 SUCCESS,业务处理异步进行
- 记录完整的通知日志,便于问题排查
- 建议配合订单查询接口,主动查询支付结果作为补充