支付结果回调

更新时间 2024-07-24 02:58:49
收藏
我的收藏
订单支付成功/取消时,抖音开平交易系统会向开发者的服务发起请求,将支付结果通知到开发者服务端。​

使用限制​

接入接口前请先查看接入前准备是否完成,否则会出现收不到支付回调的问题​

接口说明​

    由于网络波动等原因,可能会产生重复的通知消息,接入方需要做好幂等,正确处理。
    回调可能存在延时,若实时性要求高,开发者可以通过主动请求查询订单信息,确认支付结果。
    支付回调一定要做验签处理,防止收到假通知,可参考下文验签示例代码
    在开发者服务端收到回调且处理成功后,需要按以下正常返回示例返回并且 HTTP 响应状态码设为 200,否则会认为通知失败进行重试。重试频率为 15s/30s/1m/2m/4m/8m/16m/32m/64m/128m - 总共4小时12m。​
    JS API 下单 有 payNotifyUrl 字段,如果在下单传了该字段,则会优先使用下单的 payNotifyUrl 回调地址,否则使用解决方案配置的回调地址。​
    注意: 无论是否使用 payNotifyUrl 都需要配置解决方案的支付回调地址,否则即使下单传入 payNotifyUrl 也不会生效,详见接入前准备

基本信息​

基本信息​
HTTP URL
在解决方案配置-消息通知中指定的回调地址,配置方式参考解决方案配置文档
HTTP Method
POST​

请求头​

参考 通用参数

请求参数​

参数名称​
类型​
是否必填​
描述​
示例值​
msg​
string​
是​
订单相关信息的 json 字符串​
见请求示例​
type​
string​
是​
回调类型(支付结果回调为 payment):payment(支付成功/支付取消)​
payment​
version​
string​
是​
固定值:"3.0"。回调版本,用于开发者识别回调参数的变更​
"3.0"​
msg 字段
字段名​
类型​
是否必填​
描述​
示例值​
app_id​
string​
是​
小程序 app_id​
ttcfdbb96650e33350​
out_order_no​
string​
是​
开发者系统生成的订单号,与抖音开平交易单号 order_id 唯一关联,长度 <= 64byte​
"motb52726742593307630520652"​
order_id​
string​
是​
抖音开平侧订单id,长度 <= 64byte​
"ext_order_123"​
status​
string​
是​
支付结果状态,目前有两种状态:​
"SUCCESS" (支付成功 )​
"CANCEL" (支付取消)​
"SUCCESS"​
total_amount​
int64​
是​
订单总金额,单位分支付金额为 = total_amount - discount_amount​
1000​
discount_amount​
int64​
否​
订单优惠金额,单位分,接入营销时请关注这个字段​
0​
pay_channel​
int32​
否​
支付渠道枚举(支付成功时才有):1:微信2:支付宝10:抖音支付​
1​
channel_pay_id​
string​
否​
渠道支付单号,如微信/支付宝的支付单号,长度 <= 64byte​
注:status="SUCCESS"时一定有值​
"2iu2082897r9hflquf"​
merchant_uid​
string​
否​
该笔交易的卖家商户号​
注:status="SUCCESS"时一定有值​
"1231123"​
message​
string​
否​
该笔交易取消原因,如:"USER_CANCEL":用户取消"TIME_OUT":超时取消​
"USER_CANCEL"​
event_time​
int64​
是​
用户支付成功/支付取消时间戳,单位为毫秒​
1692775192000​
user_bill_pay_id​
string​
否​
对应用户抖音账单里的"支付单号"​
注:status="SUCCESS"时一定有值​
"DPTS202312120312301231"​

请求示例​

Plain Text
复制
curl --location --request POST 'https://xxxxxxx.net/api/v2/result_callback?timestamp=1345678901234&nonce=iuy987q4htafreqw' \
--header 'Content-Type: application/json' \
--data-raw='{
"version": "3.0", //本次固定为3.0, 通过版本信息识别,用不同的结构体去解析上述关键参数
"msg": "{\"app_id\":\"tt07e371xxxxxxx\",\"status\":\"SUCCESS\",\"order_id\":\"ot7057422956397414686\",\"cp_extra\":\"xxx\",\"item_id\":\"xxxxx\",\"seller_uid\":\"xxxxxx\",\"pay_channel\":1,\"message\":\"\",\"extra\":\"{\\\"cps_info\\\":\\\"poi\\\",\\\"share_amount\\\":\\\"299\\\"}\",\"event_time\":1643185090000,\"out_order_no\":\"ext_order_no_1643185079529\",\"total_amount\":1}",
"type": "payment"
}'

msg 字段内容示例

JSON
复制
//支付成功回调示例
{
"app_id": "tt07e371xxxxxxx",
"out_order_no": "motb52726742593307630520652",
"order_id": "ext_order_123",
"status": "SUCCESS",
"total_amount": 1,
"discount_amount": 0,
"pay_channel": 1,
"channel_pay_id": "2iu2082897r9hflquf",
"merchant_uid": "1231123",
"message": "",
"event_time": 1692775192000,
"user_bill_pay_id": "DPTS12031230128124421",
}
//支付取消回调示例
{
"app_id": "tt07e371xxxxxxx",
"out_order_no": "motb52726742593307630520652",
"order_id": "ext_order_123",
"status": "CANCEL",
"total_amount": 1,
"discount_amount": 0,
"merchant_uid": "1231123",
"message": "",
"event_time": 1692775192000
}

响应参数​

参数名称​
类型​
描述​
示例值​
err_no​
int64​
错误码​
0​
err_tips​
string​
错误提示​
success​

正常示例​

Plain Text
复制
//正常返回响应且http状态码为200
//注意:
//正常返回时一定要保证err_no和err_tips为下面标准返回方式,不然都认为失败,将会重试
{
"err_no": 0,
"err_tips": "success"
}

异常示例​

Plain Text
复制
//异常响应或http状态码为非200,
//字节服务端会不断重试
{
"err_no": 1, //非0
"err_tips": "system error" //非success
}

验签示例代码​

JAVA
Java
复制
/**
* 为方便开发者快速接入验签机制,以下提供了解析请求数据和验签的示例代码 @see verifySignature
* 请开发者注意,该示例代码仅提供验签核心部分,
* 在验签通过且完成业务逻辑处理后,需按上文响应参数规范进行接口响应
*/
public class ReqInfo {
public String body;
public String timestamp;
public String nonce;
public String signature;
}
/**
* @param reqInfo 通过@see resolveReq方法解析出来的请求体数据
* @param publicKey 平台公钥, 注意验签应使用平台公钥,而非应用公钥
*/
public boolean verifySignature(ReqInfo reqInfo, String publicKey) throws Exception {
StringBuffer buffer = new StringBuffer();
buffer.append(reqInfo.timestamp).append("\n");
buffer.append(reqInfo.nonce).append("\n");
buffer.append(reqInfo.body).append("\n");
String message = buffer.toString();
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initVerify(string2PublicKey(publicKey)); // 注意验签时publicKey使用平台公钥而非应用公钥
sign.update(message.getBytes(StandardCharsets.UTF_8));
return sign.verify(Base64.getDecoder().decode(reqInfo.signature.getBytes(StandardCharsets.UTF_8)));
}
public ReqInfo resolveReq(HttpServletRequest req) {
log.info("receive payNotify");
String body = getRequestBodyParamsStr(req);
String timestamp = req.getHeader("Byte-Timestamp");
String nonce = req.getHeader("Byte-Nonce-Str");
String signature = req.getHeader("Byte-Signature");
log.info("body:%s, timestamp:%s, nonce:%s, signature:%s", body, timestamp, nonce, signature);
ReqInfo reqInfo = new ReqInfo();
reqInfo.body = body;
reqInfo.timestamp = timestamp;
reqInfo.nonce = nonce;
reqInfo.signature = signature;
return reqInfo;
}
public String getRequestBodyParamsStr(HttpServletRequest request) {
BufferedReader br;
String str, wholeStr = "";
try {
br = request.getReader();
while ((str = br.readLine()) != null) {
wholeStr += str;
}
wholeStr = wholeStr.replaceAll(" ", "");
log.info(wholeStr);
} catch (IOException e) {
e.printStackTrace();
log.error(String.valueOf(e));
}
return wholeStr;
}
public PublicKey string2PublicKey(String publicKey) throws Exception{
byte[] decoded = Base64.getDecoder().decode(publicKey);
return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded));
}