Skip to content

代付回调通知接口

说明: 当代付订单状态发生变化(审核成功/失败、代付成功/失败)时,系统会向商户下单时传入的 noticeUrl 异步发送回调通知。

请求方式: POST

Content-Type: application/json

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


一、回调通知参数

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

参数名称参数变量名类型必含说明
商户订单号tradeNoString商户下单时传入的订单号
代付金额totalAmountString代付金额(元),保留两位小数
平台代付单号outTradeNoString平台系统生成的代付订单号
交易时间tradeTimeString格式 yyyy-MM-dd HH:mm:ss,例如:2025-01-15 12:30:45
交易状态tradeStatusString订单状态(见下表)
备注remarkString备注信息(仅在非空时才发送,如失败原因)
附加字段attachString商户下单时传入的附加数据(原样返回,未传入时为空字符串)
MD5签名signStringMD5签名值(32位大写),签名规则见下文
RSA签名rsaSignStringRSA签名值(Base64编码,SHA256WithRSA),商户可用服务端RSA公钥验签

交易状态说明

状态值说明备注
0审核中订单提交,等待审核
1审核成功审核通过,等待代付(属于成功状态)
2审核失败订单被驳回(失败状态)
3代付中正在向支付通道发起代付
4代付已转账代付成功(最终成功状态)
5暂停代付代付流程被暂停

成功状态判断

代付订单的成功状态包括 "1"(审核成功)"4"(代付已转账),两者都应视为订单成功。"4" 为最终到账成功状态。"2"(审核失败)为失败状态。


二、回调请求示例

代付成功通知

json
{
  "tradeNo": "PEER20250115001",
  "totalAmount": "1000.00",
  "outTradeNo": "DF20250115001001",
  "tradeTime": "2025-01-15 12:30:45",
  "tradeStatus": "4",
  "attach": "测试代付",
  "sign": "3E541783111209F709ACDA0F4BFD17EA"
}

代付失败通知(含备注)

json
{
  "tradeNo": "PEER20250115002",
  "totalAmount": "5000.00",
  "outTradeNo": "DF20250115001002",
  "tradeTime": "2025-01-15 12:35:20",
  "tradeStatus": "2",
  "remark": "收款账户信息错误",
  "attach": "",
  "sign": "A1B2C3D4E5F67890ABCDEF1234567890",
  "rsaSign": "xYzAbCdEfGhIjKlMnOpQrStUvWxYz9876..."
}

三、商户响应要求

商户系统收到回调通知并处理完成后,必须在 HTTP Response Body 中返回包含 success 字符串的内容。

  • 系统在判断返回值时,会先做 trim()toLowerCase() 处理,因此返回值前后空格和大小写不敏感
  • 如果响应内容不包含 success,系统会认为通知失败并触发重试

正确响应示例

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

text
success

方式二:返回 JSON

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

四、重试机制

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

重试次数距首次通知的间隔说明
第1次(首次)0订单状态变更后立即发送
第2次10 秒-
第3次30 秒-
第4次1 分钟-
第5次3 分钟最后一次重试

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

超时处理

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


五、回调签名验证

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

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

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

5.1 MD5 签名验证(sign 字段)

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

步骤 1:参数排序

将回调参数中signrsaSign的所有非空参数,按参数名 ASCII 字典序升序排序。

空值过滤

值为 null、空字符串 """null" 的字段不参与签名。例如 attach 为空字符串、remark 未发送时都不会出现在签名串中。

步骤 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签名示例

假设回调参数为(代付成功,无 remark):

json
{
  "tradeNo": "PEER20250115001",
  "totalAmount": "1000.00",
  "outTradeNo": "DF20250115001001",
  "tradeTime": "2025-01-15 12:30:45",
  "tradeStatus": "4",
  "attach": "测试代付",
  "sign": "...",
  "rsaSign": "..."
}
  1. 排序后拼接(sign/rsaSign 不参与):
attach=测试代付&outTradeNo=DF20250115001001&tradeNo=PEER20250115001&tradeStatus=4&tradeTime=2025-01-15 12:30:45&totalAmount=1000.00
  1. 拼接密钥后 MD5 并转大写得到 sign

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

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

回调验签公钥

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

验签步骤

  1. 将回调参数中signrsaSign的所有非空参数,按参数名 ASCII 字典序升序排序
  2. key=value 形式用 & 连接(空值字段过滤)
  3. 使用服务端 RSA 公钥,对拼接后的字符串进行 SHA256WithRSA 验签
  4. 将验签结果与 rsaSign(Base64 解码后)比较

RSA验签示例

待签名内容(同 MD5 的参数串,但不拼接密钥):

attach=测试代付&outTradeNo=DF20250115001001&tradeNo=PEER20250115001&tradeStatus=4&tradeTime=2025-01-15 12:30:45&totalAmount=1000.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) {
    $tradeStatus = $params['tradeStatus'];

    if ($tradeStatus == '1' || $tradeStatus == '4') {
        // 代付成功(审核成功 或 代付已转账),处理业务逻辑
    } elseif ($tradeStatus == '2') {
        // 审核失败
        $remark = isset($params['remark']) ? $params['remark'] : '';
    }

    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("/payout")
public class PayoutNotifyController {

    @PostMapping("/notify")
    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) || "4".equals(tradeStatus)) {
                    // 代付成功,处理业务逻辑
                } else if ("2".equals(tradeStatus)) {
                    // 审核失败
                    String remark = paramMap.containsKey("remark") ? paramMap.get("remark").toString() : "";
                }

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

六、与代收回调的区别

特性代收回调代付回调
触发时机支付成功时审核成功/失败、代付成功/失败时
成功状态tradeStatus = "1"tradeStatus = "1""4"
失败状态不发送回调tradeStatus = "2"
备注字段remark(失败时含失败原因)
重试次数最多 6 次最多 5 次
重试间隔1min → 5min → 10min → 30min → 1h10s → 30s → 1min → 3min
金额字段orderAmount + totalAmounttotalAmount
第三方流水号otherTradeNo

七、注意事项

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