分润结算

接入 Ping++ 分润结算接口,仅需要 Ping++ Server SDK 即可。服务器端需要做的就是向 Ping++ 请求创建用户结算对象,确认或取消分润结算,并且监听和获取 Webhooks 通知。主要分为以下几步:

  1. 设置 API-Key
  2. SDK 验证签名设置
  3. 从服务端发起创建分润结算请求,获取 Royalty Settlement 对象
  4. 确认或取消用户结算
  5. 接收 Webhooks 通知
  6. 验证 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 开关。

第三步:从服务端发起创建分润结算请求,获取 Royalty Settlement 对象

调用 Ping++ Server SDK 发起创建分润结算请求。

$royalty_settlement = \Pingpp\RoyaltySettlement::create(['payer_app' = > APP_ID, 'method' = > 'alipay', //分润的方式,余额 balance 或渠道名称,例如 alipay
'recipient_app' = > APP_ID, 'created' = > ['gt' = > 1489826451, 'lt' = > 1492418451, ], 'source_user' = > 'user_002',
//'source_no' => '',
'min_amount' = > 1, 'metadata' = > ['key' = > 'value'], 'is_preview' = > true, //是否预览,选择预览不会真实创建分润结算对象,也不会修改分润对象的状态
]);
echo $royalty_settlement;
exit;
@Test
public void testRoyaltySettlementCreate() throws RateLimitException, APIException, ChannelException, InvalidRequestException, APIConnectionException, AuthenticationException {
Map < String, Object > params = new HashMap < > ();
params.put("payer_app", PingppAccountTestData.getAppID()); // 分润发起方所在应用的 id, 必传
params.put("method", "alipay"); // 分润的方式,余额 balance 或渠道名称,例如 alipay, 必传
params.put("recipient_app", PingppAccountTestData.getAppID()); // 分润接收方的应用 id,即分润用户关联的应用 id。可选
params.put("is_preview", false); // 是否预览,选择预览不会真实创建分润结算对象,也不会修改分润对象的状态, 可选
// 创建 royalty_settlement 方法
// 参数: params
RoyaltySettlement obj = RoyaltySettlement.create(params);
assertEquals("object should be royalty_settlement", "royalty_settlement", obj.getObject());
}
pingpp.royaltySettlements.create({
'payer_app': APP_ID,
// 分润发起方所在应用的 id, 必传
'method': 'unionpay',
// 分润的方式,余额 balance 或渠道名称,例如 alipay, 必传
'recipient_app': APP_ID,
// 分润接收方的应用 id,即分润用户关联的应用 id。可选
'is_preview': false,
// 是否预览,选择预览不会真实创建分润结算对象,也不会修改分润对象的状态, 可选
'created': {
'gt': 1488211200,
'lte': 1488297600
}
}, function(err, data) {
// YOUR CODE
if (err != null) {
console.log(err);
}
});
# api_key获取方式:登录 [Dashboard](https: //dashboard2.pingxx.com)->点击管理平台右上角公司名称->企业面板->开发参数->基础密钥->Test Secret Key 和 Live Secret Key
pingpp.api_key = 'sk_test_ibbTe5jLGCi5rzfH4OqPW9KC'
app_id = 'app_1Gqj58ynP0mHeX1q'
#设置API Key
pingpp.api_key = api_key # app_id支持全局配置
pingpp.app_id = app_id
pingpp.private_key_path = os.path.join(
os.path.dirname(os.getcwd()), 'your_rsa_private_key.pem')
try :#创建分润结算对象
params = {
"payer_app": pingpp.app_id,
"method": "unionpay",
"recipient_app": pingpp.app_id,
"created": {
"gt": 1488211200,
"lte": 1488297600
},
'is_preview': True #是否预览,选择预览不会真实创建分润结算对象,也不会修改分润对象的状态
}
royalty_settlement = pingpp.RoyaltySettlement.create( * * params)
print royalty_settlement
except Exception as e: print(e.http_body)
should "return a royalty_settlement object when passed correct params"
do
params = {
#分润发起方所在的App ID required | string: payer_app = > get_payer_app,
#分润的方式required | string,
null;余额balance或渠道名称 [alipay, wx_pub, unionpay, ...]: method = > "alipay",
#分润创建时间optional | hash: created = > {: gte = > 1504875000,
: lt = > 1504885000
},
}
begin
o = Pingpp::RoyaltySettlement.create(params)
assert o.kind_of ? (Pingpp::RoyaltySettlement)
rescue = > e
assert e.kind_of ? (Pingpp::InvalidRequestError)
assert e.message.include ? ("无有效")
end
end
func(c * RoyaltySettlementDemo) New()( * pingpp.RoyaltySettlement, error) {
params: = & pingpp.RoyaltySettlementCreateParams {
PayerApp: c.demoAppID,
Method: "alipay",
RecipientApp: c.demoAppID,
Created: pingpp.Created {
GT: 1489826451,
LT: 1492418451,
},
SourceUser: "user_002",
MinAmount: 1,
Metadata: map[string] interface {} {
"key": "value",
},
}
return royaltySettlement.New(params)
}
public static void Example() {
var createParam = new Dictionary < string,
object > {
{
"payer_app", "app_LibTW1n1SOq9Pin1"
}, //分润发起方所在应用的 id
{
"method", "alipay"
}, //分润的方式,余额 balance 或渠道名称,例如 alipay
{
"recipient_app", "app_1Gqj58ynP0mHeX1q"
}, //分润接收方的应用 id,即分润用户关联的应用 id。
{
"created", new Dictionary < string, object > {
{
"gt", 1488211200
}, {
"lte", 1488297600
}
}
},
//{"source_user", "user_001"}, //按分润用户
//{"source_no", "2017080088888"}, // 关联对象的商户订单号
//{"min_amount", 1000}, //最小分润金额,汇总后金额小于最小分润金额时不会进入分润
{
"metadata", new Dictionary < string, object > {}
}, //Metadata
{
"is_preview", false
}, //是否预览,选择预览不会真实创建分润结算对象,也不会修改分润对象的状态
};
Console.WriteLine("**** 创建royalty_settlement 对象 ****");
Console.WriteLine(RoyaltySettlement.Create(createParam));
Console.WriteLine();
}

Ping++ 收到创建分润结算请求后返回给你的服务器一个 Royalty Settlement 对象,下面是 Royalty Settlement 的一个示例:

{
"id": "170302171104000011",
"object": "royalty_settlement",
"payer_app": "app_1Gqj58ynP0mHeX1q",
"amount": 1000,
"amount_canceled": 0,
"amount_failed": 0,
"amount_succeeded": 0,
"count": 10,
"count_canceled": 0,
"count_failed": 0,
"count_succeeded": 0,
"created": 1488445864,
"fee": 0,
"livemode": true,
"metadata": {},
"method": "unionpay",
"operation_url": null,
"royalty_transactions": {
"object": "list",
"has_more": true,
"url": "/v1/royalty_transactions",
"data": [{
"id": "170302171104000011",
"object": "royalty_transaction",
"amount": 15,
"status": "created",
"settle_account": "320217022818142300000901",
"source_user": "user_002",
"recipient_app": null,
"royalty_settlement": "170302171104000011"
}, {...
}, {...
}]
},
"status": "created",
"time_finished": null
}

第四步:确认或取消用户结算

服务端接收分润结算对象后,可以自行决定是否同意结算,调用 Server-SDK 的确认分润结算 API 发起请求。如果取消结算,分润结算对象变为结算已取消状态,同时分润对象会回到入账状态。

$royalty_settlement_list = \Pingpp\RoyaltySettlement::update('431170318144700001', ['status' = > 'pending' // 确认 pending, 取消 canceled
]);
echo $royalty_settlement_list;
exit;
@Test public void testRoyaltySettlementUpdate() throws RateLimitException, APIException, ChannelException, InvalidRequestException, APIConnectionException, AuthenticationException {
Map < String, Object > params = new HashMap < > ();
params.put("status", "canceled"); // 需要更新的状态 [pending, canceled]
// 更新 royalty_settlement 方法
// 参数: params
RoyaltySettlement.update("170302171104000011", params);
}
/**
* 更新分润结算对象
*/
pingpp.royaltySettlements.update('431170401120500001', // royaltySettlements ID
{
'status': 'pending'
}, // 需要更新的状态 [pending, canceled]
function(err, data) {
// YOUR CODE
if (err != null) {
console.log(err);
}
});
/**
* 取消分润结算对象
*/
pingpp.royaltySettlements.cancel('431170401120500001', // royaltySettlements ID
function(err, data) {
// YOUR CODE
if (err != null) {
console.log(err);
}
});
#更新分润结算对象 - 确认
royalty_settlement = pingpp.RoyaltySettlement.confirm(id = '430170320150300001')
print(royalty_settlement)
#更新分润结算对象 - 取消
royalty_settlement = pingpp.RoyaltySettlement.cancel(id = '430170320150300001')
print(royalty_settlement)
should "return an updated royalty_settlement object when passed a correct id"
do
begin
o = Pingpp::RoyaltySettlement.update(
existed_royalty_settlement_id, {
#更新状态required | string;
canceled: 取消结算,
pending: 确认结算: status = > 'canceled'
})
assert o.kind_of ? (Pingpp::RoyaltySettlement)
rescue = > e
assert e.kind_of ? (Pingpp::InvalidRequestError) || e.kind_of ? (Pingpp::ChannelError)
assert e.message.include ? ("状态错误")
end
end
func(c * RoyaltySettlementDemo) Update()( * pingpp.RoyaltySettlement, error) {
params: = pingpp.RoyaltySettlementUpdateParams {
Status: "pending",
//确认 pending, 取消 canceled
}
return royaltySettlement.Update(c.royaltySettlementId, params)
}
var updateParam = new Dictionary < string,
object > {
{
"status", "canceled" //确认 pending, 取消 canceled
}
};
Console.WriteLine("**** 更新royalty_settlement 对象 ****");
Console.WriteLine(RoyaltySettlement.Update("431170321101800001", updateParam));
Console.WriteLine();

第五步:接收 Webhooks 通知

当对分润结算对象进行确认或取消后 Ping++ 会给你配置在 Ping++ 管理平台的 Webhooks 通知地址主动发送确认或取消结果,我们称之为 Webhooks 通知。 Webhooks 通知是以 POST 形式发送的 JSON,放在请求的 body 里,内容是 Event 对象;分润结算全部成功的事件类型为 royalty_settlement.succeeded ,分润结算部分成功的事件类型为 royalty_settlement.partially_succeeded,分润结算全部失败的事件类型为 royalty_settlement.failed,分润结算全部取消的事件类型为 royalty_settlement.canceled, 你需要监听并接收 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 "royalty_settlement.succeeded":
// 开发者在此处加入对支付异步通知的处理代码
header($_SERVER['SERVER_PROTOCOL'] . ' 200 OK');
break;
case "royalty_settlement.partially_succeeded":
// 开发者在此处加入对支付异步通知的处理代码
header($_SERVER['SERVER_PROTOCOL'] . ' 200 OK');
break;
case "royalty_settlement.failed ":
// 开发者在此处加入对退款异步通知的处理代码
header($_SERVER['SERVER_PROTOCOL'] . ' 200 OK');
break;
case "royalty_settlement.canceled":
// 开发者在此处加入对支付异步通知的处理代码
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 ("royalty_settlement.succeeded".equals(event.getType())) {
response.setStatus(200);
} else if ("royalty_settlement.partially_succeeded".equals(event.getType())) {
response.setStatus(200);
} else if{
("royalty_settlement.failed".equals(event.getType())) {
response.setStatus(200);
}else if{
("royalty_settlement.canceled".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 "royalty_settlement.succeeded":
// 开发者在此处加入对支付异步通知的处理代码
return resp("OK", 200);
break;
case "royalty_settlement.partially_succeeded":
// 开发者在此处加入对退款异步通知的处理代码
return resp("OK", 200);
break;
case "royalty_settlement.failed":
// 开发者在此处加入对退款异步通知的处理代码
return resp("OK", 200);
break;
case "royalty_settlement.canceled":
// 开发者在此处加入对退款异步通知的处理代码
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'] == 'royalty_settlement.succeeded':
return Response(status=200)
elif event['type'] == 'royalty_settlement.partially_succeeded':
return Response(status=200)
elif event['type'] == 'royalty_settlement.failed':
return Response(status=200)
elif event['type'] == 'royalty_settlement.canceled':
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'] == 'royalty_settlement.succeeded'
# 开发者在此处加入对支付异步通知的处理代码
status = 200
response_body = 'OK'
elsif event['type'] == 'royalty_settlement.partially_succeeded'
# 开发者在此处加入对退款异步通知的处理代码
status = 200
response_body = 'OK'
elsif event['type'] == 'royalty_settlement.failed'
# 开发者在此处加入对退款异步通知的处理代码
status = 200
response_body = 'OK'
elsif event['type'] == 'royalty_settlement.canceled'
# 开发者在此处加入对退款异步通知的处理代码
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 == "royalty_settlement.succeeded" {
// TODO your code for charge
w.WriteHeader(http.StatusOK)
} else if webhook.Type == "royalty_settlement.partially_succeeded" {
// TODO your code for refund
w.WriteHeader(http.StatusOK)
} else if webhook.Type == "royalty_settlement.failed" {
// TODO your code for refund
w.WriteHeader(http.StatusOK)
} else if webhook.Type == "royalty_settlement.canceled" {
// 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 通知地址配置的 royalty_settlement.succeeded 对象的示例:

{
"id": "evt_lqVSy5gbL0A68pS8YKvJzdWA",
"created": 1430915345,
"livemode": true,
"object": "event",
"data": {
"object": {
"id": "170302171104000011",
"object": "royalty_settlement",
"payer_app": "app_1Gqj58ynP0mHeX1q",
"amount": 1,
"amount_canceled": 0,
"amount_failed": 1,
"amount_succeeded": 0,
"count": 1,
"count_canceled": 0,
"count_failed": 1,
"count_succeeded": 0,
"created": 1488445864,
"fee": 0,
"livemode": true,
"metadata": {},
"method": "unionpay",
"operation_url": null,
"royalty_transactions": {
"object": "list",
"page_number": 1,
"total_page": 1,
"url": "/v1/royalty_transactions",
"data": [{
"id": "170302171104000011",
"object": "royalty_transaction",
"amount": 1,
"created": 1430915345,
"status": "failed",
"settle_account": "320217022818142300000901",
"source_user": "user_002",
"recipient_app": null,
"royalty_settlement": "170302171104000011"
}]
},
"status": "failed",
"time_finished": null
}
},
"pending_webhooks": 0,
"type": "royalty_settlement.failed",
"request": "iar_0K8m90CCeDK8PabXD00yfTq"
}

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

{
"id": "evt_401170321172440000291102",
"created": 1490088123,
"livemode": true,
"type": "royalty_settlement.partially_succeeded",
"data": {
"object": {
"id": "431170321171500001",
"object": "royalty_settlement",
"payer_app": "app_LibTW1n1SOq9Pin1",
"created": 1490087747,
"livemode": true,
"method": "alipay",
"amount": 4,
"amount_succeeded": 1,
"amount_failed": 3,
"amount_canceled": 0,
"count": 2,
"count_succeeded": 1,
"count_failed": 1,
"count_canceled": 0,
"time_finished": 1490088123,
"fee": 100,
"metadata": {
"userStr": "KTs/YDg3ZFUFONZUC10SuA=="
},
"description": null,
"operation_url": null,
"status": "partially_succeeded",
"royalty_transactions": {
"object": "list",
"url": "/v1/royalty_transactions",
"data": [{
"id": "441170321171500001",
"object": "royalty_transaction",
"amount": 3,
"status": "failed",
"settle_account": "333333032117170500001234",
"source_user": "uid@pinpula.com",
"recipient_app": null,
"royalty_settlement": "431170321171500001",
"created": 1490087905
}, {
"id": "441170321171500002",
"object": "royalty_transaction",
"amount": 1,
"status": "succeeded",
"settle_account": "333333032117170500001234",
"source_user": "uid@pinpula.com",
"recipient_app": null,
"royalty_settlement": "431170321171500001",
"created": 1490087905
}],
"has_more": false
}
}
},
"object": "event",
"request": "iar_uPynnTSmDiT4rPqn9CP8OKCS",
"pending_webhooks": 0
}

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

{
"id": "evt_lqVSy5gbL0A68pS8YKvJzdWA",
"created": 1430915345,
"livemode": true,
"object": "event",
"data": {
"object": {
"id": "170302171104000011",
"object": "royalty_settlement",
"payer_app": "app_1Gqj58ynP0mHeX1q",
"amount": 1,
"amount_canceled": 0,
"amount_failed": 1,
"amount_succeeded": 0,
"count": 1,
"count_canceled": 0,
"count_failed": 1,
"count_succeeded": 0,
"created": 1488445864,
"fee": 0,
"livemode": true,
"metadata": {},
"method": "unionpay",
"operation_url": null,
"royalty_transactions": {
"object": "list",
"page_number": 1,
"total_page": 1,
"url": "/v1/royalty_transactions",
"data": [{
"id": "170302171104000011",
"object": "royalty_transaction",
"amount": 1,
"created": 1430915345,
"status": "failed",
"settle_account": "320217022818142300000901",
"source_user": "user_002",
"recipient_app": null,
"royalty_settlement": "170302171104000011"
}]
},
"status": "failed",
"time_finished": null
}
},
"pending_webhooks": 0,
"type": "royalty_settlement.failed",
"request": "iar_0K8m90CCeDK8PabXD00yfTq"
}

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

{
"id": "evt_lqVSy5gbL0A68pS8YKvJzdWZ",
"created": 1430915345,
"livemode": true,
"object": "event",
"data": {
"object": {
"id": "170302171104000011",
"object": "royalty_settlement",
"payer_app": "app_1Gqj58ynP0mHeX1q",
"amount": 1,
"amount_canceled": 1,
"amount_failed": 0,
"amount_succeeded": 0,
"count": 1,
"count_canceled": 1,
"count_failed": 0,
"count_succeeded": 0,
"created": 1488445864,
"fee": 0,
"livemode": true,
"metadata": {},
"method": "unionpay",
"operation_url": null,
"royalty_transactions": {
"object": "list",
"page_number": 1,
"total_page": 1,
"url": "/v1/royalty_transactions",
"data": [{
"id": "170302171104000011",
"object": "royalty_transaction",
"amount": 1,
"created": 1430915345,
"status": "canceled",
"settle_account": "320217022818142300000901",
"source_user": "user_002",
"recipient_app": null,
"royalty_settlement": "170302171104000011"
}]
},
"status": "canceled",
"time_finished": null
}
},
"pending_webhooks": 0,
"type": "royalty_settlement.canceled",
"request": "iar_0K8m90CCeDK8PabXD00yfTq"
}

第六步:验证 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++ 提供分润结算查询接口可以查询分润结算以及分润结算列表。

单个分润结算查询

$royalty_settlement = \Pingpp\RoyaltySettlement::retrieve('431170318144700001');
echo $royalty_settlement;
exit;
@Test public void testRoyaltySettlementRetrieve() throws RateLimitException, APIException, ChannelException, InvalidRequestException, APIConnectionException, AuthenticationException {
// 查询单个 royalty_settlement 方法
// 参数: royalty_settlement id
RoyaltySettlement obj = RoyaltySettlement.retrieve("170302171104000011");
assertEquals("object should be royalty_settlement", "royalty_settlement", obj.getObject());
}
pingpp.royaltySettlements.retrieve('431170401120500001', // royaltySettlements ID
function(err, data) {
// YOUR CODE
if (err != null) {
console.log(err);
}
});
royalty_settlement = pingpp.RoyaltySettlement.retrieve(id = '430170320150300001')
print(royalty_settlement)
should "return an existed royalty_settlement object when passed a correct id"
do
o = Pingpp::RoyaltySettlement.retrieve(existed_royalty_settlement_id)
assert o.kind_of ? (Pingpp::RoyaltySettlement)
end
func(c * RoyaltySettlementDemo) Get()( * pingpp.RoyaltySettlement, error) {
return royaltySettlement.Get(c.royaltySettlementId)
}
Console.WriteLine("**** 查询royalty_settlement 对象 ****");
Console.WriteLine(RoyaltySettlement.Retrieve("431170321101800001"));
Console.WriteLine();

分润结算列表查询

$royalty_settlement_list = \Pingpp\RoyaltySettlement::all(['payer_app' = > APP_ID]);
echo $royalty_settlement_list;
exit;
@Test public void testRoyaltySettlementList() throws RateLimitException, APIException, ChannelException, InvalidRequestException, APIConnectionException, AuthenticationException {
Map < String, Object > params = new HashMap < String, Object > ();
params.put("payer_app", PingppAccountTestData.getAppID());
params.put("per_page", 3);
params.put("page", 1);
// 查询 royalty_settlement list 列表方法
// 参数: params
RoyaltySettlementCollection objs = RoyaltySettlement.list(params);
assertEquals("object should be list", "list", objs.getObject());
}
pingpp.royaltySettlements.list({
payer_app: APP_ID,
// payer_app 必传
page: 1,
per_page: 3
}, function(err, data) {
// YOUR CODE
if (err != null) {
console.log(err);
}
});
params = {
"page": 1,
"per_page": 10,
"payer_app": pingpp.app_id
}
royalty_settlement = pingpp.RoyaltySettlement.all( * * params)
print(royalty_settlement)
should "return a list object of royalty_settlement"
do
o = Pingpp::RoyaltySettlement.list({: per_page = > 3,
: page = > 1,
: payer_app = > get_payer_app
})
assert o.kind_of ? (Pingpp::ListObject)
if o.data.count > 0 assert o.data[0].kind_of ? (Pingpp::RoyaltySettlement) end
end
func(c * RoyaltySettlementDemo) List()( * pingpp.RoyaltySettlementList, error) {
params: = & pingpp.PagingParams {}
params.Filters.AddFilter("per_page", "", "3")
params.Filters.AddFilter("payer_app", "", "app_LibTW1n1SOq9Pin1")
return royaltySettlement.List(params)
}
var listParam = new Dictionary < string,
object > {
{
"payer_app", "app_LibTW1n1SOq9Pin1"
}
};
Console.WriteLine("**** 查询royalty_settlement 对象列表 ****");
Console.WriteLine(RoyaltySettlement.List(listParam));
Console.WriteLine();

注意事项

  1. 发起分润结算前要配置分润结算所需要的付款渠道,目前支持支付宝、微信、银联电子代付、通联代付渠道。
  2. 发起分润结算前要为需要接收分润的用户配置相应渠道的结算账户。
  3. 汇总分润对象时,需要满足汇总后对分润接收方的分润金额大于等于 0 时才可发起结算。
  4. 如果只是想查看分润结算信息,不想真实生成分润结算对象,可以使用结算预览参数进行预览。
  5. 如果分润结算失败,对应的分润对象会回到入账状态,可以重新发起分润结算。
  6. 此接口需要使用创建 Order 时收款方(receipt_app)的APIKey和私钥请求 API 接口