支付通知
易码付平台通过支付通知接口将用户支付成功消息通知给商户
注意:
1、只有付款成功才会通知
2、同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
3、如果在所有通知频率后没有收到易码付侧回调,商户应调用查询订单接口确认订单状态。(通知频率为10秒、60秒、10分钟、30分钟、60分钟)
特别提醒:商户系统对于通知的内容一定要做签名验证,并校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。
接收成功:HTTP应答状态码需返回200或204,无需返回应答报文。
接收失败:HTTP应答状态码需返回5XX或4XX,同时需返回应答报文。
请求方式:POST
URL地址:{BASE_URL}该链接是通过下单接口中的请求参数“notify_url”来设置的。请确保回调URL是外部可正常访问的,且不能携带后缀参数,否则可能导致商户无法接收到回调通知信息。回调URL示例:“http://test.yimapay.com/payment/callback.html”
| 字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
|---|---|---|---|---|---|
| 应用ID | app_id | 是 | String | app_7ca649d7cef5deb2 | 由易码付平台生成的应用ID,全局唯一。 |
| 商户订单号 | out_trade_no | 是 | String | 1692014531737 | 商户系统内部订单号 |
| 平台订单号 | trade_no | 是 | String | YH2108142002121923822 | 易码付平台的交易流水订单号 |
| 第三方支付平台订单号 | platform_trade_no | 是 | String | 2022052113540265284652 | 第三方支付平台订单号 |
| 支付渠道 | channel | 是 | String | wx_qr | 支付使用的第三方支付渠道,详情参考 支付渠道属性值。 |
| 订单金额 | amount | 是 | Int | 100 | 订单金额,单位为分 |
| 付款时间 | pay_time | 是 | String | 2021-08-14 20:03:02 | 用户付款的时间,格式yyyy-MM-dd HH:mm:ss |
| 商品描述 | description | 是 | String | 测试商品 | 商品描述,异步通知收到此参数时,需要先解码,再验签 使用UrlEncode(UTF-8)解码,可参考编码/解码工具 |
| 附加参数 | attach | 是 | String | {"userId":888,"level":3}(编码后:%7B%22userId%22%3A888%2C%22level%22%3A3%7D) | 附加数据,JSON格式,在查询订单和支付通知中原样返回,可作为自定义参数使用。 异步通知收到此参数时,需要先解码,再验签 使用UrlEncode(UTF-8)解码,可参考编码/解码工具 |
| 签名串 | sign | 是 | String | aa4bf6d01803310d4d99c9da59dd1f29 | 待签名字符串进行MD5加密得出的32位签名值,参考签名规则 |
| 支付状态 | trade_state | 是 | String | success | 支付状态 success:支付成功 closed:订单关闭 wait:等待付款 |
| 超时时长 | time_expire | 是 | String | 60 | 订单超时时长,单位为分钟 |
| 订单创建时间 | create_time | 是 | String | 2021-07-19 00:09:50 | 订单创建时间 |
| 客户端IP | client_ip | 是 | String | 127.0.0.1 | 发起支付请求客户端的 IP地址 |
举例如下:
{BASE_URL}/payment/callback.html?app_id=app_7ca649d7cef5deb2&out_trade_no=1692014531737&trade_no=YMF2108142002121923822&channel=wx_qr&amount=2&pay_time=2021-08-14 20:03:02&description=%E6%B5%8B%E8%AF%95%E5%95%86%E5%93%81&attach=%7B%22custom%22%3A2%2C%22platform%22%3A%22%E6%98%93%E6%83%A0%22%7D&trade_state=success&time_expire=60&create_time=2021-08-14 20:01:02&client_ip=127.0.0.1&sign=aa4bf6d01803310d4d99c9da59dd1f29
| 字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
|---|---|---|---|---|---|
| 状态码 | code | 是 | String | FAIL | 业务状态码,SUCCESS or FAIL |
| 返回信息 | message | 是 | String | 签名错误 | 返回信息,如非空,为错误原因。 |
举例如下:
接收成功:HTTP应答状态码需返回200或204,无需返回应答报文。
接收失败:HTTP应答状态码需返回5XX或4XX,同时需返回应答报文,格式如下:
{
"code": "FAIL",
"message": "签名失败"
}
# 支付通知 - 商户端接收回调示例(非主动请求)
# 平台会以POST方式向商户的 notify_url 发送支付结果通知
# 商户需返回字符串 "success" 表示接收成功
# 验签方式与以下接口示例一致,
# 请参考下方 PHP / Python / Java 等语言中的验签代码
<?php
// 支付通知 - 接收平台回调并验签
$apiKey = '{YOUR_API_KEY}';
$params = $_POST;
$userSign = $params['sign'];
unset($params['sign']);
// 验签:参数按key ASCII排序,拼接后追加key,MD5大写
ksort($params);
$signStr = urldecode(http_build_query($params)) . '&key=' . $apiKey;
$calcSign = strtoupper(md5($signStr));
if ($calcSign === $userSign) {
// 验签成功,处理业务逻辑
$trade_no = $params['trade_no']; // 平台订单号
$out_trade_no= $params['out_trade_no']; // 商户订单号
$trade_state = $params['trade_state']; // 订单状态
// TODO: 更新订单状态...
echo 'success';
} else {
echo 'fail';
}
import hashlib
from flask import Flask, request
app = Flask(__name__)
api_key = '{YOUR_API_KEY}'
@app.route('/notify', methods=['POST'])
def pay_notify():
# 支付通知 - 接收平台回调并验签
params = dict(request.form)
user_sign = params.pop('sign', '')
sorted_keys = sorted(params.keys())
sign_str = '&'.join(f'{k}={params[k]}' for k in sorted_keys) + '&key=' + api_key
calc_sign = hashlib.md5(sign_str.encode()).hexdigest().upper()
if calc_sign == user_sign:
trade_no = params.get('trade_no')
out_trade_no = params.get('out_trade_no')
# TODO: 更新订单状态...
return 'success'
return 'fail'
import javax.servlet.http.*;
import java.security.MessageDigest;
import java.util.*;
// 支付通知 - 接收平台回调并验签(Servlet示例)
public class PayNotifyServlet extends HttpServlet {
private static final String API_KEY = "{YOUR_API_KEY}";
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws Exception {
Map<String, String> params = new TreeMap<>();
String userSign = "";
for (Map.Entry<String, String[]> e : req.getParameterMap().entrySet()) {
if ("sign".equals(e.getKey())) { userSign = e.getValue()[0]; continue; }
params.put(e.getKey(), e.getValue()[0]);
}
// 验签
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> e : params.entrySet())
sb.append(e.getKey()).append("=").append(e.getValue()).append("&");
sb.append("key=").append(API_KEY);
String calcSign = md5(sb.toString()).toUpperCase();
if (calcSign.equals(userSign)) {
String tradeNo = params.get("trade_no");
String outTradeNo = params.get("out_trade_no");
// TODO: 更新订单状态...
resp.getWriter().write("success");
} else {
resp.getWriter().write("fail");
}
}
static String md5(String s) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(s.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte b : digest) sb.append(String.format("%02x", b));
return sb.toString();
}
}
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: true }));
const apiKey = '{YOUR_API_KEY}';
// 支付通知 - 接收平台回调并验签
app.post('/notify', (req, res) => {
const params = { ...req.body };
const userSign = params.sign;
delete params.sign;
const sortedKeys = Object.keys(params).sort();
const signStr = sortedKeys.map(k => `=`).join('&') + '&key=' + apiKey;
const calcSign = crypto.createHash('md5').update(signStr).digest('hex').toUpperCase();
if (calcSign === userSign) {
const { trade_no, out_trade_no } = params;
// TODO: 更新订单状态...
res.send('success');
} else {
res.send('fail');
}
});
app.listen(3000);
using Microsoft.AspNetCore.Mvc;
using System.Security.Cryptography;
using System.Text;
// 支付通知 - 接收平台回调并验签(ASP.NET Core示例)
[HttpPost("notify")]
public IActionResult PayNotify([FromForm] Dictionary<string, string> form) {
var apiKey = "{YOUR_API_KEY}";
var userSign = form["sign"];
form.Remove("sign");
var sorted = form.OrderBy(x => x.Key, StringComparer.Ordinal).ToList();
var signStr = string.Join("&", sorted.Select(p => $"{p.Key}={p.Value}")) + "&key=" + apiKey;
using var md5 = MD5.Create();
var calcSign = BitConverter.ToString(md5.ComputeHash(Encoding.UTF8.GetBytes(signStr))).Replace("-","").ToUpper();
if (calcSign == userSign) {
var tradeNo = form["trade_no"];
var outTradeNo = form["out_trade_no"];
// TODO: 更新订单状态...
return Content("success");
}
return Content("fail");
}
package main
import (
"crypto/md5"
"fmt"
"net/http"
"sort"
"strings"
)
// 支付通知 - 接收平台回调并验签(Go示例)
func payNotifyHandler(w http.ResponseWriter, r *http.Request) {
apiKey := "{YOUR_API_KEY}"
r.ParseForm()
params := r.PostForm
userSign := params.Get("sign")
params.Del("sign")
keys := make([]string, 0)
for k := range params { keys = append(keys, k) }
sort.Strings(keys)
var parts []string
for _, k := range keys { parts = append(parts, k+"="+params.Get(k)) }
signStr := strings.Join(parts, "&") + "&key=" + apiKey
calcSign := fmt.Sprintf("%X", md5.Sum([]byte(signStr)))
if calcSign == userSign {
tradeNo := params.Get("trade_no")
outTradeNo := params.Get("out_trade_no")
_ = tradeNo
_ = outTradeNo
// TODO: 更新订单状态...
w.Write([]byte("success"))
} else {
w.Write([]byte("fail"))
}
}