订单退款
接入 Ping++ 订单退款接口,服务器端需要做的是向 Ping++ 请求创建 Refund 对象,并且监听和获取 Webhooks 通知。具体步骤如下:
- 设置 API-Key
- SDK 验证签名设置
- 从服务端发起退款请求,获取 Refund 对象
- 接收 Webhooks 通知
- 验证 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 模式
注意: 一旦上传公钥至 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// 参数二: paramsOrderRefundCollection 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_refundo = 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 jsonfrom 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 endendserver = WEBrick::HTTPServer.new(:Port => 8000)server.mount '/webhooks', Webhookstrap 'INT' do server.shutdown endserver.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++ 公钥。验证签名需要以下几步:
- 从 header 取出签名字段并对其进行
base64
解码。 - 获取 Webhooks 请求的原始数据。
- 将获取到的 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_ordero = 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_ordero = 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);
注意事项
使用订单时,若出现以下异常情况,你需要根据实际情况进行退款处理:
- 订单在取消后完成了付款(例如打开支付宝控件后,主动/超时取消订单后,在控件完成付款)。此时 order 对象的 status 值为 canceled,paid 值为 true,amount_paid 等于 actual_amount。
- 用户在多个渠道同时完成了付款(例如:打开支付宝控件后,再打开微信控件,然后在双方同时支付完成)。此时 order 对象中的 amount_paid 大于 actual_amount。
下一步余额