订单退款

接入 Ping++ 订单退款接口,服务器端需要做的是向 Ping++ 请求创建 Refund 对象,并且监听和获取 Webhooks 通知。具体步骤如下:

  1. 设置 API-Key
  2. SDK 验证签名设置
  3. 从服务端发起退款请求,获取 Refund 对象
  4. 接收 Webhooks 通知
  5. 验证 Webhooks 签名

第一步:设置 API-Key

Ping++ API 交易时需要设置 API-Key,Server SDK 提供了设置的方法。如果你直接使用 API ,需要在 header 中加入 Authorization,格式是 Authorization: Bearer API-Key。

\Pingpp\Pingpp::setApiKey('sk_test_ibbTe5jLGCi5rzfH4OqPW9KC');
Pingpp.apiKey = "sk_test_ibbTe5jLGCi5rzfH4OqPW9KC";
var pingpp = require('pingpp')('sk_test_ibbTe5jLGCi5rzfH4OqPW9KC');
pingpp.api_key = 'sk_test_ibbTe5jLGCi5rzfH4OqPW9KC'
Pingpp.api_key = "sk_test_ibbTe5jLGCi5rzfH4OqPW9KC"
pingpp.Key = "sk_test_ibbTe5jLGCi5rzfH4OqPW9KC"
Pingpp.Pingpp.SetApiKey("sk_test_ibbTe5jLGCi5rzfH4OqPW9KC");

第二步:SDK 验证签名设置

为了进一步增强交易请求的安全性,Ping++ 交易接口针对所有的 POST 和 PUT 请求已经新增 RSA 加密验签功能。如果使用该签名验证功能,你需要生成密钥,然后将私钥配置到你的代码中,公钥上传至 Ping++ 管理平台并启用验签开关。首先你需要本地生成 RSA 公钥和私钥,生成方法请参考:如何获取 RSA 公钥和私钥?

设置请求签名密钥

你需要在代码中设置请求签名的私钥(rsa_private_key.pem),可以读取配置私钥文件的路径或者直接定义变量。你如果通过 API 接口校验的话,需要生成 RSA 签名(SHA256)并在请求头中添加 Pingplusplus-Signature,如果使用 SDK 的话只需要配置私钥即可。

\Pingpp\Pingpp::setPrivateKeyPath(__DIR__ . '/your_rsa_private_key.pem');
Pingpp.privateKeyPath = "/path/to/your_rsa_private_key.pem";
pingpp.setPrivateKeyPath(__dirname + "/your_rsa_private_key.pem");
pingpp.private_key_path = 'your_rsa_private_key.pem'
Pingpp.private_key_path = File.dirname(__FILE__) + '/your_rsa_private_key.pem'
privateKey, err := ioutil.ReadFile("your_rsa_private_key.pem")
Pingpp.Pingpp.SetPrivateKeyPath(@"../../your_rsa_private_key.pem");

上传公钥至 Ping++ 管理平台

设置完代码中的私钥,你需要将已经生成的公钥(rsa_public_key.pem)填写到 Ping++ 管理平台上。 配置路径: 登录 Ping++ 管理平台->点击右上角公司名称->企业面板->开发参数->商户 RSA 公钥->将你的公钥复制粘贴进去并且保存->先启用 Test 模式进行测试->测试通过后启用 Live 模式

rsa_keys_setting

注意: 一旦上传公钥至 Ping++ 管理平台并启用 Live 模式,则验证签名功能即时生效,Ping++ 会立即验证你的真实线上交易验签请求。如果私钥为空或错误,则会交易失败,所以请确保测试模式正常后再启用 Live 开关。

第三步:从服务端发起退款请求,获取 Refund 对象

调用 Ping++ Server SDK 发起退款请求,请求所需参数具体可参考 API 文档

$order_id = '2011708070000007521';
$orre = \Pingpp\OrderRefund::create($order_id,
[
'charge' => 'ch_eDaTe9OG0qPOz14S4KWH80eP', //在该订单中,需要退款的 charge 对象 ID,默认全部退款。
'charge_amount' => 10, //charge 退款金额,默认为全额退款。必须和 charge 参数同时使用。单位:分。
'description' => 'Your description', //退款附加说明。
'metadata' => [],
'refund_mode' => 'to_source', //退款模式。原路退回:to_source,退至余额:to_balance。默认为原路返回。
'royalty_users' => [ //退分润的用户列表
[
'user' => 'test_user_001',
'amount_refunded' => 10,
],
[
'user' => 'test_user_002',
'amount_refunded' => 10,
],
],
]
);
Map<String, Object> params = new HashMap<String, Object>();
params.put("description", "Order refund test."); // 必传
params.put("refund_mode", "to_source");
// 创建 order 退款方法
// 参数一: orderId
// 参数二: params
OrderRefundCollection objs = OrderRefund.create("2001708220000281981", params);
pingpp.orders.createRefund(
"2001708280000128761", // ORDER ID
{
"description":"test-refund"
},
function(err, order) {
if (err!=null){
console.log("pingpp.orders.creatRefund fail:",err)
}
// YOUR CODE
}
);
params = {
"description": "Your description",
"refund_mode": "to_source",
"royalty_users": [
{
"user": "test_user_002",
"amount_refunded": 1,
},
{
"user": "test_user_003",
"amount_refunded": 1,
}
]
}
order_refund = pingpp.OrderRefunds.create(order_id="2001708150000313781", **params)
order_id, charge_id = order_and_charge_to_refund
o = Pingpp::Order.refund(
order_id,
{
# optional. 要退款的 Charge ID,不填则将 Order 包含的所有 Charge 都退款。
:charge => charge_id,
# optional. 退款金额,不填则退剩余可退金额。
:charge_amount => 1,
# required.
:description => '退款信息',
# optional. 退款模式。原路退回:to_source,退至余额:to_balance。默认为原路退回。
:refund_mode => 'to_source',
# optional. 退款资金来源。unsettled_funds:使用未结算资金退款;recharge_funds:使用可用余额退款。
# 该参数仅适用于所有微信渠道,包括 wx、wx_pub、wx_pub_qr、wx_lite、wx_wap 五个渠道。
# :funding_source => 'unsettled_funds',
# optional. 退分润的用户列表,默认分润全退,不是分润全退时,需要填写所有分润的用户。
# :royalty_users => [
# { :user => '', :amount_refunded => 10 }
# ],
}
)
func (c *OrderRefundDemo) New() (*pingpp.RefundList, error) {
params := &pingpp.OrderRefundParams{
Description: "Go SDK Test",
}
return orderRefund.New("2011609290000001291", params)
}
var reParams = new Dictionary<string, object>
{
{"description", "Refund Reason"},
{"refund_mode", "to_source"}, //退款模式。原路退回:to_source,退至余额:to_balance。默认为原路返回。
//{"royalty_users", new List<Dictionary<string,object>>{ //退分润的用户列表,默认为 [],不分润
// new Dictionary<string,object>{
// {"user", "user_001"},
// {"amount_refunded",1}
// },
// new Dictionary<string,object>{
// {"user", "user_002"},
// {"amount_refunded",1}
// }
// }},
};
var re = OrderRefund.Create(orId, reParams);

Ping++ 收到订单退款请求后返回给你的服务器一个 Refund 对象,下面是 Refund 对象的一个示例:

{
"object": "list",
"url": "/v1/orders/2001608270000004421/order_refunds",
"has_more": false,
"data": [
{
"id": "re_y1u944PmfnrTHyvnL0nD0iD1",
"object": "refund",
"order_no": "y1u944PmfnrTHyvnL0nD0iD1",
"amount": 800,
"created": 1499930518,
"succeed": true,
"status": "succeeded",
"time_succeed": 1499930518,
"description": "Refund Description",
"failure_code": null,
"failure_msg": null,
"metadata": {},
"charge": "ch_8SCSCCn90ir1bb54m5fjbnX5",
"charge_order_no": "2017071102122327",
"transaction_no": "2004450349201512090096425284",
"extra": {}
}
]
}

第四步:接收 Webhooks 通知

当用户完成退款后 Ping++ 会给你配置在 Ping++ 管理平台的 Webhooks 通知地址主动发送订单退款结果,我们称之为 Webhooks 通知。 Webhooks 通知是以 POST 形式发送的 JSON,放在请求的 body 里,内容是 Event 对象,支付成功的事件类型为 order.refunded ,你需要监听并接收 Webhooks 通知,接收到 Webhooks 后需要返回服务器状态码 2xx 表示接收成功,否则请返回状态码 500

$event = json_decode(file_get_contents("php://input"));
// 对异步通知做处理
if (!isset($event->type)) {
header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request');
exit("fail");
}
switch ($event->type) {
case "order.succeeded":
// 开发者在此处加入对支付异步通知的处理代码
header($_SERVER['SERVER_PROTOCOL'] . ' 200 OK');
break;
case "order.order.refund.succeeded":
// 开发者在此处加入对退款异步通知的处理代码
header($_SERVER['SERVER_PROTOCOL'] . ' 200 OK');
break;
default:
header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request');
break;
}
import com.pingplusplus.model.Event;
import com.pingplusplus.model.Webhooks;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
public class ServletDemo extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("UTF8");
//获取头部所有信息
Enumeration headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = (String) headerNames.nextElement();
String value = request.getHeader(key);
System.out.println(key+" "+value);
}
// 获得 http body 内容
BufferedReader reader = request.getReader();
StringBuffer buffer = new StringBuffer();
String string;
while ((string = reader.readLine()) != null) {
buffer.append(string);
}
reader.close();
// 解析异步通知数据
Event event = Webhooks.eventParse(buffer.toString());
if ("order.succeeded".equals(event.getType())) {
response.setStatus(200);
} else if ("order.order.refund.succeeded".equals(event.getType())) {
response.setStatus(200);
} else {
response.setStatus(500);
}
}
}
var http = require('http');
http.createServer(function (req, res) {
req.setEncoding('utf8');
var postData = "";
req.addListener("data", function (chunk) {
postData += chunk;
});
req.addListener("end", function () {
var resp = function (ret, status_code) {
res.writeHead(status_code, {
"Content-Type": "text/plain; charset=utf-8"
});
res.end(ret);
}
try {
var event = JSON.parse(postData);
if (event.type === undefined) {
return resp('Event 对象中缺少 type 字段', 400);
}
switch (event.type) {
case "order.succeeded":
// 开发者在此处加入对支付异步通知的处理代码
return resp("OK", 200);
break;
case "order.refund.succeeded":
// 开发者在此处加入对退款异步通知的处理代码
return resp("OK", 200);
break;
default:
return resp("未知 Event 类型", 400);
break;
}
} catch (err) {
return resp('JSON 解析失败', 400);
}
});
}).listen(8080, "0.0.0.0");
import json
from flask import Flask, request, Response
# 使用 flask
@app.route('/webhooks', methods=['POST'])
def webhooks():
event = request.get_json()
if event['type'] == 'order.succeeded':
return Response(status=200)
elif event['type'] == 'order.refund.succeeded':
return Response(status=200)
return Response(status=500)
if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0', port=8080)
require 'webrick'
require 'json'
class Webhooks < WEBrick::HTTPServlet::AbstractServlet
def do_POST(request, response)
status = 400
response_body = '' # 可自定义
begin
event = JSON.parse(request.body)
if event['type'].nil?
response_body = 'Event 对象中缺少 type 字段'
elsif event['type'] == 'order.succeeded'
# 开发者在此处加入对支付异步通知的处理代码
status = 200
response_body = 'OK'
elsif event['type'] == 'order.refund.succeeded'
# 开发者在此处加入对退款异步通知的处理代码
status = 200
response_body = 'OK'
else
response_body = '未知 Event 类型'
end
rescue JSON::ParserError
response_body = 'JSON 解析失败'
end
response.status = status
response['Content-Type'] = 'text/plain; charset=utf-8'
response.body = response_body
end
end
server = WEBrick::HTTPServer.new(:Port => 8000)
server.mount '/webhooks', Webhooks
trap 'INT' do server.shutdown end
server.start
func webhook(w http.ResponseWriter, r *http.Request) {
if strings.ToUpper(r.Method) == "POST" {
buf := new(bytes.Buffer)
buf.ReadFrom(r.Body)
signature := r.Header.Get("x-pingplusplus-signature")
webhook, err := pingpp.ParseWebhooks(buf.Bytes())
fmt.Println(webhook.Type)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "fail")
return
}
if webhook.Type == "order.succeeded" {
// TODO your code for charge
w.WriteHeader(http.StatusOK)
} else if webhook.Type == "order.refund.succeeded" {
// TODO your code for refund
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusInternalServerError)
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Pingpp.Models;
using System.IO;
namespace Example.Example
{
public class WebhooksDemo
{
public static Event Example()
{
var data = ReadFileToString(@"../../data.txt");
var evt = Webhooks.ParseWebhook(data);
Console.WriteLine(evt);
return evt;
}
public static string ReadFileToString(string path)
{
using (var sr = new StreamReader(path))
{
return sr.ReadToEnd();
}
}
}
}

以下是 Webhooks 通知地址配置的 order.refunded 对象的示例:

{
"id": "evt_401171222113755187074803",
"created": 1513913873,
"livemode": true,
"type": "order.refunded",
"data": {
"object": {
"id": "2001708140000017551",
"object": "order",
"created": 1513913809,
"livemode": true,
"paid": true,
"refunded": true,
"status": "refunded",
"app": "app_1Gqj58ynP0mHeX1q",
"uid": "0",
"available_balance": 0,
"merchant_order_no": "2017081400000006",
"amount": 1,
"actual_amount": 1,
"amount_refunded": 1,
"amount_paid": 1,
"coupon_amount": 0,
"currency": "cny",
"subject": "Your Subject",
"body": "Your Body",
"client_ip": "192.168.1.1",
"time_paid": 1513913854,
"time_expire": 1514000209,
"coupon": null,
"description": null,
"metadata": {},
"charge_essentials": {
"channel": "alipay_qr",
"transaction_no": "201712222100100888",
"failure_code": null,
"failure_msg": null,
"extra": {
"buyer_user_id": "2088902388888888",
"fund_bill_list": [{
"amount": 1,
"fundChannel": "COUPON"
}],
"buyer_account": "133****3333"
},
"credential": {}
},
"receipt_app": "app_1Gqj58ynP0mHeX1q",
"service_app": "app_1Gqj58ynP0mHeX1q",
"available_methods": [],
"charges": {
"object": "list",
"url": "/v1/charges",
"has_more": false,
"data": [{
"id": "ch_1Kyn50DyjXbHbvnv5SGK4qDK",
"object": "charge",
"created": 1513913827,
"livemode": true,
"paid": true,
"refunded": true,
"reversed": false,
"app": "app_1Gqj58ynP0mHeX1q",
"channel": "alipay_qr",
"order_no": "2017081400000006",
"client_ip": "192.168.1.1",
"amount": 1,
"amount_settle": 1,
"currency": "cny",
"subject": "Your Subject",
"body": "Your Body",
"extra": {
"buyer_user_id": "2088902388888888",
"fund_bill_list": [{
"amount": 1,
"fundChannel": "COUPON"
}],
"buyer_account": "133****3333"
},
"time_paid": 1513913854,
"time_expire": 1514000209,
"time_settle": null,
"transaction_no": "201712222100100888",
"refunds": null,
"amount_refunded": 0,
"failure_code": null,
"failure_msg": null,
"metadata": {},
"credential": {},
"description": null
}]
}
}
},
"object": "event",
"request": "iar_C8KOu5HOmD8CbzbXrPqv1y9S",
"pending_webhooks": 0
}

第五步:验证 Webhooks 签名

签名简介

Ping++ 的 Webhooks 通知包含了签名字段,可以使用该签名验证 Webhooks 通知的合法性。签名放置在 header 的自定义字段 x-pingplusplus-signature 中,签名用 RSA 私钥对 Webhooks 通知使用 RSA-SHA256 算法进行签名,以 base64 格式输出。

验证签名

Ping++ 在管理平台中提供了 RSA 公钥,供验证签名,该公钥具体获取路径:点击管理平台右上角公司名称->企业面板->开发参数-> Ping++ 公钥。验证签名需要以下几步:

  1. 从 header 取出签名字段并对其进行 base64 解码。
  2. 获取 Webhooks 请求的原始数据。
  3. 将获取到的 Webhooks 通知、 Ping++ 管理平台提供的 RSA 公钥、和 base64 解码后的签名三者一同放入 RSA 的签名函数中进行非对称的签名运算,来判断签名是否验证通过。 Ping++ 提供了验证签名的 Demo Demo Demo Demo Demo Demo Demo ,放在 SDK 的 example 里供参考,我们在此不再赘述。

退款查询

Ping++ 提供接口可以通过订单 ID 及退款 ID 查询订单退款对象及对象列表。

单笔订单退款查询

$order_id = '2011708070000007521';
$refund_id = 're_OW1CSS8KCS0KvfzDu5jTerrH';
$orre = \Pingpp\OrderRefund::retrieve($order_id, $refund_id);
Refund obj = OrderRefund.retrieve("2001708220000258501", "re_5GefjD14GW50qrT40Gq9KmPS");
pingpp.orders.retrieveRefund(
"2001708220000258501", // ORDER ID
"re_5GefjD14GW50qrT40Gq9KmPS", // REFUND ID
function(err, order) {
if (err!=null){
console.log("pingpp.orders.retrieveRefund fail:",err)
}
// YOUR CODE
}
);
order_refunds_detail = pingpp.OrderRefunds.retrieve(
order_id="2001708150000313781",
refund_id="re_HK0aXLnfjTy9u1aj14PCyzLG"
)
order_id, refund_id = existed_refund_id_of_order
o = Pingpp::Order.retrieve_refund(
order_id,
refund_id
)
func (c *OrderRefundDemo) Get() (*pingpp.Refund, error) {
return orderRefund.Get("2011609290000001291", "2111609290000001601")
}
OrderRefund.Retrieve(orId, "re_vjz1a1DCufb5n9erLK48SGC0");

订单退款列表查询

$order_id = '2011708090000015171';
$params = [
'page' =>1,
'per_page' => 10,
];
$orres = \Pingpp\OrderRefund::all($order_id, $params);
OrderRefundCollection objs = OrderRefund.list("2001708220000258501");
pingpp.orders.listRefunds(
"2001708220000258501", // ORDER ID
{'page': 1, 'per_page': 3},
function(err, data) {
if (err!=null){
console.log("pingpp.orders.listRefunds fail:",err)
}
// YOUR CODE
}
);
//订单列表查询
order_refunds_list = pingpp.OrderRefunds.list(order_id="2001708150000313781")
order_id, _ = existed_refund_id_of_order
o = Pingpp::Order.list_refunds(
order_id,
{ :per_page => 3, :page => 1 }
)
func (c *OrderRefundDemo) List() (*pingpp.RefundList, error) {
params := &pingpp.PagingParams{}
params.Filters.AddFilter("page", "", "1") //取第一页数据
params.Filters.AddFilter("per_page", "", "2") //每页两个Order对象
return orderRefund.List("2011609290000001291", params)
}
OrderRefund.List(orId);

注意事项

使用订单时,若出现以下异常情况,你需要根据实际情况进行退款处理:

  1. 订单在取消后完成了付款(例如打开支付宝控件后,主动/超时取消订单后,在控件完成付款)。此时 order 对象的 status 值为 canceled,paid 值为 true,amount_paid 等于 actual_amount。
  2. 用户在多个渠道同时完成了付款(例如:打开支付宝控件后,再打开微信控件,然后在双方同时支付完成)。此时 order 对象中的 amount_paid 大于 actual_amount。

下一步余额