Skip to content

代收回调通知接口

说明: 当代收订单支付成功后,系统会向商户下单时传入的 noticeUrl 异步发送回调通知。

请求方式: POST

Content-Type: application/json

由系统主动发起,商户需要提供接收回调的 HTTP 接口。


一、回调通知参数

系统会以 JSON 格式向商户的 noticeUrl 发送以下参数:

参数名称参数变量名类型必含说明
商户订单号tradeNoString商户下单时传入的订单号
订单金额orderAmountString订单金额(元),保留两位小数
实际支付金额totalAmountString实际支付金额(元),与 orderAmount 相同
平台订单号outTradeNoString平台系统生成的订单号
第三方流水号otherTradeNoString第三方支付渠道的交易流水号
交易时间tradeTimeString格式 yyyy-MM-dd HH:mm:ss,例如:2025-01-15 12:30:45
交易状态tradeStatusString"1" = 支付成功
附加字段attachString商户下单时传入的附加数据(原样返回,未传入时为空字符串)
MD5签名signStringMD5签名值(32位大写),签名规则见下文
RSA签名rsaSignStringRSA签名值(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,系统会认为通知失败并触发重试

正确响应示例

方式一:直接返回文字(推荐)

text
success

方式二:返回 JSON

json
{
  "code": 0,
  "message": "success"
}

四、重试机制

如果商户未返回 success(包括网络异常、超时、返回其他内容等情况),系统会按照以下策略进行重试:

重试次数距首次通知的间隔说明
第1次(首次)0订单支付成功后立即发送
第2次1 分钟-
第3次5 分钟-
第4次10 分钟-
第5次30 分钟-
第6次1 小时最后一次重试

最多重试 6 次。达到上限后不再重试,商户可通过查询订单接口主动获取订单状态。

超时处理

如果系统发送通知时发生连接超时(connect_time_out),会立即再重试一次。若仍失败则计入重试次数。


五、回调签名验证

商户系统必须对回调数据进行签名验证,确保数据来源是支付平台。

系统提供两种签名方式,商户可任选其一进行验签(建议使用 RSA 签名):

签名字段算法说明
signMD5(32位大写)传统签名方式,需使用商户密钥
rsaSignSHA256WithRSA(Base64)RSA签名,需使用服务端RSA公钥验签

5.1 MD5 签名验证(sign 字段)

签名类型为 MD5(32位大写),生成步骤如下:

步骤 1:参数排序

将回调参数中signrsaSign的所有非空参数,按参数名 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签名示例

假设回调参数为:

json
{
  "tradeNo": "ORDER20250115001",
  "orderAmount": "100.00",
  "totalAmount": "100.00",
  "outTradeNo": "PO20250115001001",
  "otherTradeNo": "THIRD20250115001",
  "tradeTime": "2025-01-15 12:30:45",
  "tradeStatus": "1",
  "attach": "",
  "sign": "...",
  "rsaSign": "..."
}
  1. 排序后拼接(attach 为空被过滤,sign/rsaSign 不参与):
orderAmount=100.00&otherTradeNo=THIRD20250115001&outTradeNo=PO20250115001001&tradeNo=ORDER20250115001&tradeStatus=1&tradeTime=2025-01-15 12:30:45&totalAmount=100.00
  1. 拼接密钥后 MD5 并转大写得到 sign

5.2 RSA 签名验证(rsaSign 字段,推荐)

签名类型为 SHA256WithRSA,使用服务端 回调签名私钥 签名,商户使用回调验签公钥验签。

回调验签公钥

回调验签公钥在管理后台「友商管理 → RSA密钥管理」页面生成时显示,仅显示一次,请妥善保管。 此公钥与下单签名私钥是不同的密钥对,请勿混用。

验签步骤

  1. 将回调参数中signrsaSign的所有非空参数,按参数名 ASCII 字典序升序排序
  2. key=value 形式用 & 连接(空值字段过滤)
  3. 使用服务端 RSA 公钥,对拼接后的字符串进行 SHA256WithRSA 验签
  4. 将验签结果与 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
<?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 双签名)

java
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();
    }
}

六、注意事项

  1. 幂等性:建议商户系统对同一 outTradeNo 的回调通知做幂等处理,避免重复处理(系统可能因网络重试发送多次)
  2. 签名验证必须对所有回调通知进行签名验证,防止伪造请求
  3. 交易状态判断tradeStatus"1" 时才标记订单为成功状态
  4. 返回要求:无论业务处理成功与否,只要收到通知都应返回 success 字符串,否则会触发最多 6 次的重试
  5. 回调格式:代收回调为 JSON 格式Content-Type: application/json
  6. 异步处理:建议商户将回调通知放入异步队列处理,避免阻塞导致响应超时
  7. 主动查询:如长时间未收到回调,可通过 查询订单接口 主动查询订单状态
  8. 空值过滤:验签时需过滤掉空值字段,与服务端保持一致