法币代收异步通知

# 代收异步通知

本接口由商户提供,当代收成功平台请求该接口 接收到HTTP请求时,请响应httpcode=200(HTTP 响应状态码) ,否则会在5小时内重复发送16次通知.

⚠️⚠️注意:用户支付有可能会只支付部分金额,需校对“实际支付金额”⚠️⚠️

# 最佳实践

  1. 对于支付成功的判定,orderStatus是1
  2. 对于部分支付的判定:orderStatus是3。请商户根据自身业务逻辑处理
  3. 对于退款的判定:orderStatus是2。这种情况一般是出现在用户投诉以及银行风控退回,请商户做好相关记录并根据自身业务逻辑进行处理(收到此回调状态之前会先收到回调成功)
  4. 若是商户业务逻辑上处理失败,请将响应的HTTP状态码改为400或者500(非200)
  5. Cheezeepay可能会多次发送相同状态的回调,请商户做好相关兼容处理,如商户自身业务已经处理完成,请务必返回响应HTTP状态码200
  6. 请务必校验回调IP➕验证签名
参数 类型 必填 描述 示例
merchantId string Y 商户ID CH10001165
mchOrderNo string Y 商户订单号 20240123172337
platOrderNo string Y 平台订单号 1749724564009521152
orderStatus int Y 订单状态 1-成功 2-退款 3-部分支付
payAmount string Y 订单实付金额 800
amountCurrency string Y 订单金额币种 THB
fee string Y 手续费 80
feeCurrency string Y 手续费币种 THB
gmtEnd long Y 完结时间 (时间戳:毫秒) 1706003885000
sign string Y 签名 FMZBnLfSfsWu1ZhHXBFifsexm9dGB5ZFt3RmVZ
AkU9Ck1vE5tzMZGHkOon8mfqK8yhd9E7gQuiWX
yqppWr6rlASviTinBEAjV5y3OqK0piA8bNyhGpR/W
4XPAaDKFyQ54IUbwypZwcBRJ9i5jKkTvOFh5YC+
TWFHkxfA0sK47xrixodVnHe+88hWSR3/oQdCMnN
4eGQN69IbpFILvC+PXQBfpuWTzHMPENuClm3nq
5mK/k7o5Nha9drHyJbPBO54t3Z+5L/aju7lfA9OgyV0
Ss1BHmhyugSx8gyMMSo1uDL1LYWWNmMgj8VO
BUeHNSFPK3Cio7Ko0Bh1TreV7bCa1A==

# 异步通知报文示例

{
  "merchantId":"CH10001165",
  "mchOrderNo":"20240123172337",
  "platOrderNo":"1749724564009521152",
  "orderStatus":1,
  "payAmount":"800",
  "amountCurrency":"THB",
  "fee":"80",
  "feeCurrency":"THB",
  "gmtEnd":1706003885000,
  "sign":"FMZBnLfSfsWu1ZhHXBFifsexm9dGB5ZFt3RmVZAkU9Ck1vE5tzMZGHkOon8mfqK8yhd9E7gQuiWXyqppWr6rlASviTinBEAjV5y3OqK0piA8bNyhGpR/W4XPAaDKFyQ54IUbwypZwcBRJ9i5jKkTvOFh5YC+TWFHkxfA0sK47xrixodVnHe+88hWSR3/oQdCMnN4eGQN69IbpFILvC+PXQBfpuWTzHMPENuClm3nq5mK/k7o5Nha9drHyJbPBO54t3Z+5L/aju7lfA9OgyV0Ss1BHmhyugSx8gyMMSo1uDL1LYWWNmMgj8VOBUeHNSFPK3Cio7Ko0Bh1TreV7bCa1A=="
}

# 异步通知代码实现Demo

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;

@RestController
@RequestMapping("callback")
public class TestCallbackController {

    /**
     * 平台公钥
     */
    public static String PLATFORM_PUB_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1dad35S74jfLPbHJh8P0jDHiTvkxwrtITK97ovVu19B24UdiHyHoEZgtNlS6alFQj1ULQ71d6EPh2rWCNkS2b5HGQXwDYBtwvesVQ8h4Sf3eVPTTLGw3BS7Os4vtDEN6BezMdv3sUG2N5i6JF+5H4CQTq3MD2Cx6u/Cv7oFOdFqeDT0AH+TR7uyZxn69OtkJaHHr834EUcdShJKKMQtbC11WCcut7ilDUgdvZnThiVTq7cfl8mcC9FDKcQ9bMWamScWIB5cJQdUW23Kr0c1NvZlpgPS8U5VODM4Uc4muHJPD2cJmquuJ+4AGP36rEk27lUB3h7B6JI1QGiuh1yyPDwIDAQAB";

    /**
     * 代收回调
     * @param paramMap
     * @param response
     */
    @PostMapping("payIn")
    public void payInCallback(@RequestBody Map<String, Object> paramMap, HttpServletResponse response) throws Exception{
        boolean verifyResult = CheeseTradeRSAUtil.verifySign(paramMap, PLATFORM_PUB_KEY);
        if (verifyResult) {
            //签名验证成功
            //订单通知业务处理逻辑
            String merchantId = (String)paramMap.get("merchantId");
            String mchOrderNo = (String)paramMap.get("mchOrderNo");
            String platOrderNo = (String)paramMap.get("platOrderNo");
            Integer orderStatus = (Integer)paramMap.get("orderStatus");
            String payAmount = (String)paramMap.get("payAmount");
            String amountCurrency = (String)paramMap.get("amountCurrency");
            String fee = (String)paramMap.get("fee");
            String feeCurrency = (String)paramMap.get("feeCurrency");
            Long gmtEnd = (Long)paramMap.get("gmtEnd");
        } else {
            //签名认证失败
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        }
    }

}
<?php
//此代码在PHP5.6版本已得到验证
class TestCallbackController 
{

    /**
     * 平台公钥
     */
    const PLATFORM_PUB_KEY = '-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyVqixSZsLJ75yL7r1SwFn0QsOrDAIv4JU6PCIyLwH5Y1m7QY3nm5k+D4VX9qNvTq9R6CT9fPuXH8AVfx7iDCahHA0j//SiTUNlRqgXZVwrhYoytW4PqSl8+QkMimS6Mm9zPC8JsyaUrv0DfIfVxLPZ42JMrKo6+/neIORTduhyO8VYcfNbFAa+fzKl1B0dmXyvat9BmMDYexKPtqKb3WkXKzAERPSizdbib6GvpV6boEnEDlyLT32rtWNEb26NWvmyiaQjBb2J32sDHSPga5kOT8L1dyi3YA4T2JtHMCAMyw9hPz7tjtl1XwSBsJ3h4q9Zra6G2K+NfPxQsBIFVgtwIDAQAB
-----END PUBLIC KEY-----';

    /**
     * 代收回调
     * @param paramMap
     * @param response
     */
    public function payInCallback($paramMap) {
        $Sign = $paramMap["sign"];
        unset($paramMap['sign']); // 移除签名键
        // 获取 map 的所有 key,并排序
        $keys = array_keys($paramMap);
        sort($keys);
        // 遍历 key 列表,获取对应的 value,并拼接成字符串
        $str = '';
        foreach ($keys as $key) {
            $str .=  $key . '=' . $paramMap[$key] . '&';
        }
        $str = rtrim($str, '&');
        echo $str;

        $verifyResult = verifySign($str,$Sign ,self::PLATFORM_PUB_KEY);
        if ($verifyResult) {
             //签名验证成功
            //订单通知业务处理逻辑
            $merchantId = $paramMap["merchantId"];
            $mchOrderNo = $paramMap["mchOrderNo"];
            $platOrderNo = $paramMap["platOrderNo"];
            $orderStatus = $paramMap["orderStatus"];
            $payAmount = $paramMap["payAmount"];
            $amountCurrency = $paramMap["amountCurrency"];
            $fee = $paramMap["fee"];
            $feeCurrency = $paramMap["feeCurrency"];
            $gmtEnd = $paramMap["gmtEnd"];
            //业务处理逻辑完成后必须返回给我们HTTP200的响应
        } else {
             //签名认证失败
            http_response_code(400);
        }
    }

}
//校验签名
function verifySign($data, $sign, $pubKey){
    $sign = base64_decode($sign);
    $key = openssl_pkey_get_public($pubKey);
    $result = openssl_verify($data, $sign, $key, OPENSSL_ALGO_SHA256) === 1;
    return $result;
}

?>

const express = require('express');
const bodyParser = require('body-parser');
//引入的签名工具类(CheeseTradeRSAUtil)的函数,签名工具类可以参照"签名&验签算法"里的DEMO
const  {verifySign}= require('./CheeseTradeRSAUtil');

const app = express();
app.use(bodyParser.json()); // 解析 JSON 请求体

// 平台公钥
const platPublicKey = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1dad35S74jfLPbHJh8P0jDHiTvkxwrtITK97ovVu19B24UdiHyHoEZgtNlS6alFQj1ULQ71d6EPh2rWCNkS2b5HGQXwDYBtwvesVQ8h4Sf3eVPTTLGw3BS7Os4vtDEN6BezMdv3sUG2N5i6JF+5H4CQTq3MD2Cx6u/Cv7oFOdFqeDT0AH+TR7uyZxn69OtkJaHHr834EUcdShJKKMQtbC11WCcut7ilDUgdvZnThiVTq7cfl8mcC9FDKcQ9bMWamScWIB5cJQdUW23Kr0c1NvZlpgPS8U5VODM4Uc4muHJPD2cJmquuJ+4AGP36rEk27lUB3h7B6JI1QGiuh1yyPDwIDAQAB
-----END PUBLIC KEY-----`;

// 代收回调接口
app.post('/callback/payIn', (req, res) => {
    const paramMap = req.body; // 获取请求体中的参数

    try {
        const verifyResult = verifySign(paramMap,platPublicKey);
        if (verifyResult) {
            // 签名验证成功
            const merchantId = paramMap.merchantId;
            const mchOrderNo = paramMap.mchOrderNo;
            const platOrderNo = paramMap.platOrderNo;
            const orderStatus = paramMap.orderStatus;
            const payAmount = paramMap.payAmount;
            const amountCurrency = paramMap.amountCurrency;
            const fee = paramMap.fee;
            const feeCurrency = paramMap.feeCurrency;
            const gmtEnd = paramMap.gmtEnd;

            // 订单通知业务处理逻辑
            console.log('签名验证成功,处理订单:', {
                merchantId,
                mchOrderNo,
                platOrderNo,
                orderStatus,
                payAmount,
                amountCurrency,
                fee,
                feeCurrency,
                gmtEnd,
            });

            // 返回成功响应
            res.status(200).json({ message: '处理成功' });
        } else {
            // 签名验证失败
            res.status(400).json({ message: '签名验证失败' });
        }
    } catch (error) {
        // 处理异常
        console.error('处理回调时发生错误:', error);
        res.status(500).json({ message: '服务器内部错误' });
    }
});