余额提现

接入 Ping++ 发起余额提现,仅需要 Ping++ Server SDK 即可。服务器端需要做的就是向 Ping++ 请求 withdrawals 接口,并且监听和获取 Webhooks 通知,具体步骤如下:

  1. 设置 API-Key
  2. SDK 验证签名设置
  3. 服务端发起提现请求获取 withdrawal 对象
  4. 更新 withdrawal 对象
  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 开关。

第三步:服务端发起提现请求获取 withdrawal 对象

调用 Ping++ Server SDK 发起余额提现请求,发起请求所需参数具体可参考 API 文档。通过调用该接口发起一笔用户提现申请,提现申请表示用户提现意图,不会真实发起提现行为,需要后续确认。 提现支持的渠道有:银联(unionpay)、支付宝(alipay)、微信 App(wx)、微信JSAPI(wx_pub) 、通联(allinpay) 、京东(jdpay)。

unionpay
$params = [
'user' => 'user_test_01', // 用户 ID
'amount' => 200, // 转账金额
'channel' => 'unionpay', //提现使用渠道。
'user_fee' => 10, //用户需要承担的手续费
'description' => 'Your Description', //描述。
'order_no' => time() . mt_rand(100000, 999999), //提现订单号
'extra' => array(
'account' => '6225210207078888', //必填项 收款人银行卡号或者存折号。
'name' => '张三', //必填项 收款人姓名。
'open_bank_code' => '0308', //选填项 开户银行编号,open_bank_code 和 open_bank 必须填写一个,优先推荐填写 open_bank_code。
'open_bank' => '招商银行', //选填项 开户银行,open_bank_code 和 open_bank 必须填写一个,优先推荐填写 open_bank_code。
'prov' => '上海', //选填项 省份。
'city' => '上海', //选填项 城市。
'sub_bank' => '徐家汇支行'
)
$withdrawal = \Pingpp\Withdrawal::create($params);
echo $withdrawal;
Pingpp.apiKey = "sk_test_ibbTe5jLGCi5rzfH4OqPW9KC";
Map<String, Object> params = new HashMap<String, Object>();
params.put("user", "test_user_001"); // 用户 ID, 必传
params.put("channel", "unionpay"); // 提现使用渠道。
params.put("amount", 100); // 转账金额, 必传
params.put("description", "custom description"); // 描述, 可选
params.put("order_no", "1010" + System.currentTimeMillis()); // 提现订单号, 必传
Map<String, String> extra = new HashMap<String, String>();
extra.put("name", "NAME");
extra.put("account", "6225210207078888");
params.put("extra", extra);
params.put("user_fee", 5); // 用户需要承担的手续费, 必传
params.put("settle_account", "test_user_001"); // 使用结算账户提现,不需要填写 extra 参数,同时填写时,结算账号不生效
Withdrawal withdrawal = Withdrawal.create(params); // 创建 Withdrawal 对象
var order_no = new Date().getTime().toString().substr(0, 10);
var channel = 'unionpay';
var extra = withdrawal_extra(channel);
var params = {
'user': 'test_user_001',
'amount': 20, // 金额
'channel': channel, // 渠道
'user_fee': 10, // 用户需要承担的手续费
'order_no': order_no, // 提现订单号
'description': 'test232description', // 转账描述
'extra': extra
};
request_info = {
"amount": 20000,
"user_fee": 50,
'order_no': '1234567890',
"description": "Your description",
'channel': 'unionpay',
"extra": {
"account": "622123456789",
"name": "姓名",
"open_bank_code": "0102",
"prov": "上海",
"city": "上海"
},
new_wd = pingpp.Withdrawal.create(user="user_id_001", **request_info)
print(new_wd)
params = {
:user => existed_user_id,
:amount => 10,
:user_fee => 0,
:description => "用户提现",
:channel => "unionpay",
:extra => {
# 收款账号
:account => "6201112223333",
# 姓名 conditional|string
:name => "姓名",
},
# 结算账号 ID conditional|string; 与 user 绑定的结算账号 ID
# :settle_account => "SETTLE_ACCOUNT",
# metadata optional|hash
:metadata => {
:custom_key => "custom_value",
# ...
}
}
if params[:channel] == "allinpay"
params[:order_no] = "301002#{Time.now.to_i.to_s}#{rand(999999).to_s.rjust(6, "0")}"
else
params[:order_no] = "#{Time.now.to_i.to_s}#{rand(9999).to_s.rjust(4, "0")}"
end
o = Pingpp::Withdrawal.create(
params,
{
# URL 中的 {app_id} ; 用于覆盖 Pingpp.app_id
:app => get_app_id
}
)
func (c *WithdrawalDemo) Setup(app string) {
c.demoAppID = app
c.demoChannel = "unionpay"
}
// 余额提现申请
func (c *WithdrawalDemo) New() (*pingpp.Withdrawal, error) {
params := &pingpp.WithdrawalParams{
User: "user_001",
Amount: 20000,
User_fee: 0,
Channel: c.demoChannel,
Description: "Your description",
Order_no: "20160829133002",
Extra: common.WithdrawExtra[c.demoChannel],
}
return withdrawal.New(c.demoAppID, params)
}
public static Withdrawal unionpayWithdrawal(String appId)
{
var createParams = new Dictionary<string, object>
{
{"order_no", "2016111000039"}, //提现订单号
{"amount" , 1}, //体现金额。
{"user_fee", 0}, //用户需要承担的手续费。
{"channel", "unionpay"}, //提现使用渠道。
{"user", "user_001"}, //用户 ID。
{"description", "Your description"}, //描述
{"extra", new Dictionary<string, object>{
{"account", "6214850210294648"}, //收款人银行卡号或者存折号。
{"name", "张三"}, //收款人姓名。
{"open_bank_code", "0308"}, //开户银行编号,open_bank_code 和 open_bank 必须填写一个,优先推荐填写 open_bank_code
{"open_bank", "招商银行"}, //开户银行,open_bank_code 和 open_bank 必须填写一个,优先推荐填写 open_bank_code。
{"prov", "上海"}, //省份。
{"city", "上海"}, //城市。
{"sub_bank","徐家汇支行"} // 开户支行名称。
}},
//{"settle_account", "320217081417114300000501"} //使用结算账户提现,不需要填写 channel 和 extra 相关参数,同时填写时,结算账号不生效
};
return Withdrawal.Request(appId,createParams);
}
alipay
$params = [
'user' => 'user_test_01', // 用户 ID
'amount' => 200, // 转账金额
'channel' => 'alipay', //提现使用渠道。
'user_fee' => 10, //用户需要承担的手续费
'description' => 'Your Description', //描述。
'order_no' => time() . mt_rand(100000, 999999), //提现订单号
'extra' => array(
'account' => 'user@gmail.com', //必填项 支付宝账号。
'name' => '张三' //必填项 收款人姓名。
)
$withdrawal = \Pingpp\Withdrawal::create($params);
echo $withdrawal;
Pingpp.apiKey = "sk_test_ibbTe5jLGCi5rzfH4OqPW9KC";
Map<String, Object> params = new HashMap<String, Object>();
params.put("user", "test_user_001"); // 用户 ID, 必传
params.put("channel", "alipay"); // 提现使用渠道。
params.put("amount", 100); // 转账金额, 必传
params.put("description", "custom description"); // 描述, 可选
params.put("order_no", "1010" + System.currentTimeMillis()); // 提现订单号, 必传
Map<String, String> extra = new HashMap<String, String>();
extra.put("name", "NAME");
extra.put("account", "test_user_001@gmail.com");
params.put("extra", extra);
params.put("user_fee", 5); // 用户需要承担的手续费, 必传
params.put("settle_account", "test_user_001"); // 使用结算账户提现,不需要填写 extra 参数,同时填写时,结算账号不生效
Withdrawal withdrawal = Withdrawal.create(params); // 创建 Withdrawal 对象
var order_no = new Date().getTime().toString().substr(0, 10);
var channel = 'alipay';
var extra = withdrawal_extra(channel);
var params = {
'user': 'test_user_001',
'amount': 20, // 金额
'channel': channel, // 渠道
'user_fee': 10, // 用户需要承担的手续费
'order_no': order_no, // 提现订单号
'description': 'test232description', // 转账描述
'extra': extra
};
request_info = {
"amount": 20000,
"user_fee": 50,
'order_no': '1234567890',
"description": "Your description",
'channel': 'alipay',
"extra": {
"account": "622123456789",
"name": "姓名"
},
new_wd = pingpp.Withdrawal.create(user="user_id_001", **request_info)
print(new_wd)
params = {
:user => existed_user_id,
:amount => 10,
:user_fee => 0,
:description => "用户提现",
:channel => "alipay",
:extra => {
# 收款账号
:account => "user@gmail.com",
# 姓名
:name => "姓名",
},
# 结算账号 ID ; 与 user 绑定的结算账号 ID
# :settle_account => "SETTLE_ACCOUNT",
# metadata optional|hash
:metadata => {
:custom_key => "custom_value",
# ...
}
}
if params[:channel] == "allinpay"
params[:order_no] = "301002#{Time.now.to_i.to_s}#{rand(999999).to_s.rjust(6, "0")}"
else
params[:order_no] = "#{Time.now.to_i.to_s}#{rand(9999).to_s.rjust(4, "0")}"
end
o = Pingpp::Withdrawal.create(
params,
{
# URL 中的 {app_id} ; 用于覆盖 Pingpp.app_id
:app => get_app_id
}
)
func (c *WithdrawalDemo) Setup(app string) {
c.demoAppID = app
c.demoChannel = "alipay"
}
// 余额提现申请
func (c *WithdrawalDemo) New() (*pingpp.Withdrawal, error) {
params := &pingpp.WithdrawalParams{
User: "user_001",
Amount: 20000,
User_fee: 0,
Channel: c.demoChannel,
Description: "Your description",
Order_no: "20160829133002",
Extra: common.WithdrawExtra[c.demoChannel],
}
return withdrawal.New(c.demoAppID, params)
}
public static Withdrawal unionpayWithdrawal(String appId)
{
var createParams = new Dictionary<string, object>
{
{"order_no", "2016111000039"}, //提现订单号
{"amount" , 1}, //体现金额。
{"user_fee", 0}, //用户需要承担的手续费。
{"channel", "alipay"}, //提现使用渠道。
{"user", "user_001"}, //用户 ID。
{"description", "Your description"}, //描述
{"extra", new Dictionary<string, object>{
{"account", "user@example.com"}, //收款人的支付宝账号。
{"name", "张三"}, //收款人姓名。
{"account_type", "ALIPAY_USERID"} //收款方账户类型
}},
//{"settle_account", "320217081417114300000501"} //使用结算账户提现,不需要填写 channel 和 extra 相关参数,同时填写时,结算账号不生效
};
return Withdrawal.Request(appId,createParams);
}
wx
$params = [
'user' => 'user_test_01', // 用户 ID
'amount' => 200, // 转账金额
'channel' => 'wx', //提现使用渠道。
'user_fee' => 10, //用户需要承担的手续费
'description' => 'Your Description', //描述。
'order_no' => time() . mt_rand(100000, 999999), //提现订单号
'extra' => array(
'openid' => 'open_id', //必填项 微信开放平台下对应获取的用户 open_id。
'name' => '张三'
)
$withdrawal = \Pingpp\Withdrawal::create($params);
echo $withdrawal;
Pingpp.apiKey = "sk_test_ibbTe5jLGCi5rzfH4OqPW9KC";
Map<String, Object> params = new HashMap<String, Object>();
params.put("user", "test_user_001"); // 用户 ID, 必传
params.put("channel", "wx"); // 提现使用渠道。
params.put("amount", 100); // 转账金额, 必传
params.put("description", "custom description"); // 描述, 可选
params.put("order_no", "1010" + System.currentTimeMillis()); // 提现订单号, 必传
Map<String, String> extra = new HashMap<String, String>();
extra.put("name", "NAME");
extra.put("openid", "open_id");
params.put("extra", extra);
params.put("user_fee", 5); // 用户需要承担的手续费, 必传
params.put("settle_account", "test_user_001"); // 使用结算账户提现,不需要填写 extra 参数,同时填写时,结算账号不生效
Withdrawal withdrawal = Withdrawal.create(params); // 创建 Withdrawal 对象
var order_no = new Date().getTime().toString().substr(0, 10);
var channel = 'wx';
var extra = withdrawal_extra(channel);
var params = {
'user': 'test_user_001',
'amount': 20, // 金额
'channel': channel, // 渠道
'user_fee': 10, // 用户需要承担的手续费
'order_no': order_no, // 提现订单号
'description': 'test232description', // 转账描述
'extra': extra
};
request_info = {
"amount": 20000,
"user_fee": 50,
'order_no': '1234567890',
"description": "Your description",
'channel': 'wx',
"extra": {
"openid": "open_id",
"name": "姓名"
},
new_wd = pingpp.Withdrawal.create(user="user_id_001", **request_info)
print(new_wd)
params = {
:user => existed_user_id,
:amount => 10,
:user_fee => 0,
:description => "用户提现",
:channel => "wx",
:extra => {
# 微信开放平台下对应获取的用户 open_id
:openid => "open_id",
# 姓名
:name => "姓名"
},
# 结算账号 ID ; 与 user 绑定的结算账号 ID
# :settle_account => "SETTLE_ACCOUNT",
# metadata optional|hash
:metadata => {
:custom_key => "custom_value",
# ...
}
}
if params[:channel] == "allinpay"
params[:order_no] = "301002#{Time.now.to_i.to_s}#{rand(999999).to_s.rjust(6, "0")}"
else
params[:order_no] = "#{Time.now.to_i.to_s}#{rand(9999).to_s.rjust(4, "0")}"
end
o = Pingpp::Withdrawal.create(
params,
{
# URL 中的 {app_id} ; 用于覆盖 Pingpp.app_id
:app => get_app_id
}
)
func (c *WithdrawalDemo) Setup(app string) {
c.demoAppID = app
c.demoChannel = "wx"
}
// 余额提现申请
func (c *WithdrawalDemo) New() (*pingpp.Withdrawal, error) {
params := &pingpp.WithdrawalParams{
User: "user_001",
Amount: 20000,
User_fee: 0,
Channel: c.demoChannel,
Description: "Your description",
Order_no: "20160829133002",
Extra: common.WithdrawExtra[c.demoChannel],
}
return withdrawal.New(c.demoAppID, params)
}
public static Withdrawal unionpayWithdrawal(String appId)
{
var createParams = new Dictionary<string, object>
{
{"order_no", "2016111000039"}, //提现订单号
{"amount" , 1}, //体现金额。
{"user_fee", 0}, //用户需要承担的手续费。
{"channel", "wx"}, //提现使用渠道。
{"user", "user_001"}, //用户 ID。
{"description", "Your description"}, //描述
{"extra", new Dictionary<string, object>{
{"openid", "open_id"}, //微信开放平台下对应获取的用户 open_id
{"name", "张三"} //收款人姓名。
}},
//{"settle_account", "320217081417114300000501"} //使用结算账户提现,不需要填写 channel 和 extra 相关参数,同时填写时,结算账号不生效
};
return Withdrawal.Request(appId,createParams);
}
wx_pub
$params = [
'user' => 'user_test_01', // 用户 ID
'amount' => 200, // 转账金额
'channel' => 'wx_pub', //提现使用渠道。
'user_fee' => 10, //用户需要承担的手续费
'description' => 'Your Description', //描述。
'order_no' => time() . mt_rand(100000, 999999), //提现订单号
'extra' => array(
'openid' => 'open_id', //必填项 微信公众平台下对应获取的用户 open_id。
'name' => '张三'
)
$withdrawal = \Pingpp\Withdrawal::create($params);
echo $withdrawal;
Pingpp.apiKey = "sk_test_ibbTe5jLGCi5rzfH4OqPW9KC";
Map<String, Object> params = new HashMap<String, Object>();
params.put("user", "test_user_001"); // 用户 ID, 必传
params.put("channel", "wx_pub"); // 提现使用渠道。
params.put("amount", 100); // 转账金额, 必传
params.put("description", "custom description"); // 描述, 可选
params.put("order_no", "1010" + System.currentTimeMillis()); // 提现订单号, 必传
Map<String, String> extra = new HashMap<String, String>();
extra.put("name", "NAME");
extra.put("openid", "open_id");
params.put("extra", extra);
params.put("user_fee", 5); // 用户需要承担的手续费, 必传
params.put("settle_account", "test_user_001"); // 使用结算账户提现,不需要填写 extra 参数,同时填写时,结算账号不生效
Withdrawal withdrawal = Withdrawal.create(params); // 创建 Withdrawal 对象
var order_no = new Date().getTime().toString().substr(0, 10);
var channel = 'wx_pub';
var extra = withdrawal_extra(channel);
var params = {
'user': 'test_user_001',
'amount': 20, // 金额
'channel': channel, // 渠道
'user_fee': 10, // 用户需要承担的手续费
'order_no': order_no, // 提现订单号
'description': 'test232description', // 转账描述
'extra': extra
};
request_info = {
"amount": 20000,
"user_fee": 50,
'order_no': '1234567890',
"description": "Your description",
'channel': 'wx_pub',
"extra": {
"openid": "open_id",
"name": "姓名"
},
new_wd = pingpp.Withdrawal.create(user="user_id_001", **request_info)
print(new_wd)
params = {
:user => existed_user_id,
:amount => 10,
:user_fee => 0,
:description => "用户提现",
:channel => "wx_pub",
:extra => {
# 微信公众平台下对应获取的用户 open_id
:openid => "open_id",
# 姓名
:name => "姓名"
},
# 结算账号 ID ; 与 user 绑定的结算账号 ID
# :settle_account => "SETTLE_ACCOUNT",
# metadata optional|hash
:metadata => {
:custom_key => "custom_value",
# ...
}
}
if params[:channel] == "allinpay"
params[:order_no] = "301002#{Time.now.to_i.to_s}#{rand(999999).to_s.rjust(6, "0")}"
else
params[:order_no] = "#{Time.now.to_i.to_s}#{rand(9999).to_s.rjust(4, "0")}"
end
o = Pingpp::Withdrawal.create(
params,
{
# URL 中的 {app_id} ; 用于覆盖 Pingpp.app_id
:app => get_app_id
}
)
func (c *WithdrawalDemo) Setup(app string) {
c.demoAppID = app
c.demoChannel = "wx_pub"
}
// 余额提现申请
func (c *WithdrawalDemo) New() (*pingpp.Withdrawal, error) {
params := &pingpp.WithdrawalParams{
User: "user_001",
Amount: 20000,
User_fee: 0,
Channel: c.demoChannel,
Description: "Your description",
Order_no: "20160829133002",
Extra: common.WithdrawExtra[c.demoChannel],
}
return withdrawal.New(c.demoAppID, params)
}
public static Withdrawal unionpayWithdrawal(String appId)
{
var createParams = new Dictionary<string, object>
{
{"order_no", "2016111000039"}, //提现订单号
{"amount" , 1}, //体现金额。
{"user_fee", 0}, //用户需要承担的手续费。
{"channel", "wx_pub"}, //提现使用渠道。
{"user", "user_001"}, //用户 ID。
{"description", "Your description"}, //描述
{"extra", new Dictionary<string, object>{
{"openid", "open_id"}, //微信公众平台下对应获取的用户 open_id
{"name", "张三"} //收款人姓名。
}},
//{"settle_account", "320217081417114300000501"} //使用结算账户提现,不需要填写 channel 和 extra 相关参数,同时填写时,结算账号不生效
};
return Withdrawal.Request(appId,createParams);
}
wx_lite
$params = [
'user' => 'user_test_01', // 用户 ID
'amount' => 200, // 转账金额
'channel' => 'wx_lite', //提现使用渠道。
'user_fee' => 10, //用户需要承担的手续费
'description' => 'Your Description', //描述。
'order_no' => time() . mt_rand(100000, 999999), //提现订单号
'extra' => array(
'openid' => 'open_id', //必填项 微信公众平台下对应获取的用户 open_id。
'name' => '张三'
)
$withdrawal = \Pingpp\Withdrawal::create($params);
echo $withdrawal;
Pingpp.apiKey = "sk_test_ibbTe5jLGCi5rzfH4OqPW9KC";
Map<String, Object> params = new HashMap<String, Object>();
params.put("user", "test_user_001"); // 用户 ID, 必传
params.put("channel", "wx_lite"); // 提现使用渠道。
params.put("amount", 100); // 转账金额, 必传
params.put("description", "custom description"); // 描述, 可选
params.put("order_no", "1010" + System.currentTimeMillis()); // 提现订单号, 必传
Map<String, String> extra = new HashMap<String, String>();
extra.put("name", "NAME");
extra.put("openid", "open_id");
params.put("extra", extra);
params.put("user_fee", 5); // 用户需要承担的手续费, 必传
params.put("settle_account", "test_user_001"); // 使用结算账户提现,不需要填写 extra 参数,同时填写时,结算账号不生效
Withdrawal withdrawal = Withdrawal.create(params); // 创建 Withdrawal 对象
var order_no = new Date().getTime().toString().substr(0, 10);
var channel = 'wx_lite';
var extra = withdrawal_extra(channel);
var params = {
'user': 'test_user_001',
'amount': 20, // 金额
'channel': channel, // 渠道
'user_fee': 10, // 用户需要承担的手续费
'order_no': order_no, // 提现订单号
'description': 'test232description', // 转账描述
'extra': extra
};
request_info = {
"amount": 20000,
"user_fee": 50,
'order_no': '1234567890',
"description": "Your description",
'channel': 'wx_lite',
"extra": {
"openid": "open_id",
"name": "姓名"
},
new_wd = pingpp.Withdrawal.create(user="user_id_001", **request_info)
print(new_wd)
params = {
:user => existed_user_id,
:amount => 10,
:user_fee => 0,
:description => "用户提现",
:channel => "wx_lite",
:extra => {
# 微信公众平台下对应获取的用户 open_id
:openid => "open_id",
# 姓名
:name => "姓名"
},
# 结算账号 ID ; 与 user 绑定的结算账号 ID
# :settle_account => "SETTLE_ACCOUNT",
# metadata optional|hash
:metadata => {
:custom_key => "custom_value",
# ...
}
}
if params[:channel] == "allinpay"
params[:order_no] = "301002#{Time.now.to_i.to_s}#{rand(999999).to_s.rjust(6, "0")}"
else
params[:order_no] = "#{Time.now.to_i.to_s}#{rand(9999).to_s.rjust(4, "0")}"
end
o = Pingpp::Withdrawal.create(
params,
{
# URL 中的 {app_id} ; 用于覆盖 Pingpp.app_id
:app => get_app_id
}
)
func (c *WithdrawalDemo) Setup(app string) {
c.demoAppID = app
c.demoChannel = "wx_lite"
}
// 余额提现申请
func (c *WithdrawalDemo) New() (*pingpp.Withdrawal, error) {
params := &pingpp.WithdrawalParams{
User: "user_001",
Amount: 20000,
User_fee: 0,
Channel: c.demoChannel,
Description: "Your description",
Order_no: "20160829133002",
Extra: common.WithdrawExtra[c.demoChannel],
}
return withdrawal.New(c.demoAppID, params)
}
public static Withdrawal unionpayWithdrawal(String appId)
{
var createParams = new Dictionary<string, object>
{
{"order_no", "2016111000039"}, //提现订单号
{"amount" , 1}, //体现金额。
{"user_fee", 0}, //用户需要承担的手续费。
{"channel", "wx_lite"}, //提现使用渠道。
{"user", "user_001"}, //用户 ID。
{"description", "Your description"}, //描述
{"extra", new Dictionary<string, object>{
{"openid", "open_id"}, //微信公众平台下对应获取的用户 open_id
{"name", "张三"} //收款人姓名。
}},
//{"settle_account", "320217081417114300000501"} //使用结算账户提现,不需要填写 channel 和 extra 相关参数,同时填写时,结算账号不生效
};
return Withdrawal.Request(appId,createParams);
}
allinpay
$params = [
'user' => 'user_test_01', // 用户 ID
'amount' => 200, // 转账金额
'channel' => 'allinpay', //提现使用渠道。
'user_fee' => 10, //用户需要承担的手续费
'description' => 'Your Description', //描述。
'order_no' => time() . mt_rand(100000, 999999), //提现订单号
'extra' => array(
'account' => '6214850288888888', //必填项 收款人银行卡号或者存折号。
'name' => '张三', //必填项 收款人姓名。
'open_bank_code' => '0308', //必填项 开户银行编号。
'business_code' => '09900', //选填项 业务代码,根据通联业务人员提供,不填使用通联提供默认值 09900。
'card_type' => 0, //选填项 银行卡号类型,0:银行卡;1:存折。
)
$withdrawal = \Pingpp\Withdrawal::create($params);
echo $withdrawal;
Pingpp.apiKey = "sk_test_ibbTe5jLGCi5rzfH4OqPW9KC";
Map<String, Object> params = new HashMap<String, Object>();
params.put("user", "test_user_001"); // 用户 ID, 必传
params.put("channel", "allinpay"); // 提现使用渠道。
params.put("amount", 100); // 转账金额, 必传
params.put("description", "custom description"); // 描述, 可选
params.put("order_no", "1010" + System.currentTimeMillis()); // 提现订单号, 必传
Map<String, String> extra = new HashMap<String, String>();
extra.put("account", "6214850288888888");
extra.put("name", "NAME");
extra.put("open_bank_code", "0308"); //开户银行编号。
extra.put("business_code", "09900"); //业务代码,根据通联业务人员提供,不填使用通联提供默认值 09900。
extra.put("card_type", 0); //银行卡号类型,0:银行卡;1:存折。
params.put("extra", extra);
params.put("user_fee", 5); // 用户需要承担的手续费, 必传
params.put("settle_account", "test_user_001"); // 使用结算账户提现,不需要填写 extra 参数,同时填写时,结算账号不生效
Withdrawal withdrawal = Withdrawal.create(params); // 创建 Withdrawal 对象
var order_no = new Date().getTime().toString().substr(0, 10);
var channel = 'allinpay';
var extra = withdrawal_extra(channel);
var params = {
'user': 'test_user_001',
'amount': 20, // 金额
'channel': channel, // 渠道
'user_fee': 10, // 用户需要承担的手续费
'order_no': order_no, // 提现订单号
'description': 'test232description', // 转账描述
'extra': extra
};
request_info = {
"amount": 20000,
"user_fee": 50,
'order_no': '1234567890',
"description": "Your description",
'channel': 'allinpay',
"extra": {
"account": "6214850288888888",
"name": "姓名",
"open_bank_code":"0308", #开户银行编号
"business_code": "09900", #业务代码,根据通联业务人员提供,不填使用通联提供默认值 09900。
"card_type": 0 #银行卡号类型,0:银行卡;1:存折。
},
new_wd = pingpp.Withdrawal.create(user="user_id_001", **request_info)
print(new_wd)
params = {
:user => existed_user_id,
:amount => 10,
:user_fee => 0,
:description => "用户提现",
:channel => "allinpay",
:extra => {
:account => "6214850288888888",
:name => "姓名",
:open_bank_code => "0308", #开户银行编号。
:business_code => "09900", #业务代码,根据通联业务人员提供,不填使用通联提供默认值 09900。
:card_type => 0 #银行卡号类型,0:银行卡;1:存折。
},
# 结算账号 ID ; 与 user 绑定的结算账号 ID
# :settle_account => "SETTLE_ACCOUNT",
# metadata optional|hash
:metadata => {
:custom_key => "custom_value",
# ...
}
}
if params[:channel] == "allinpay"
params[:order_no] = "301002#{Time.now.to_i.to_s}#{rand(999999).to_s.rjust(6, "0")}"
else
params[:order_no] = "#{Time.now.to_i.to_s}#{rand(9999).to_s.rjust(4, "0")}"
end
o = Pingpp::Withdrawal.create(
params,
{
# URL 中的 {app_id} ; 用于覆盖 Pingpp.app_id
:app => get_app_id
}
)
func (c *WithdrawalDemo) Setup(app string) {
c.demoAppID = app
c.demoChannel = "allinpay"
}
// 余额提现申请
func (c *WithdrawalDemo) New() (*pingpp.Withdrawal, error) {
params := &pingpp.WithdrawalParams{
User: "user_001",
Amount: 20000,
User_fee: 0,
Channel: c.demoChannel,
Description: "Your description",
Order_no: "20160829133002",
Extra: common.WithdrawExtra[c.demoChannel],
}
return withdrawal.New(c.demoAppID, params)
}
public static Withdrawal unionpayWithdrawal(String appId)
{
var createParams = new Dictionary<string, object>
{
{"order_no", "2016111000039"}, //提现订单号
{"amount" , 1}, //体现金额。
{"user_fee", 0}, //用户需要承担的手续费。
{"channel", "allinpay"}, //提现使用渠道。
{"user", "user_001"}, //用户 ID。
{"description", "Your description"}, //描述
{"extra", new Dictionary<string, object>{
{"account", "6214850288888888"}, //收款人银行卡号或者存折号。
{"name", "张三"}, // 收款人姓名。
{"open_bank_code", "0308"}, //开户银行编号。
{"business_code", "09900"}, //业务代码,根据通联业务人员提供,不填使用通联提供默认值 09900。
{"card_type", 0} //银行卡号类型,0:银行卡;1:存折。
}},
//{"settle_account", "320217081417114300000501"} //使用结算账户提现,不需要填写 channel 和 extra 相关参数,同时填写时,结算账号不生效
};
return Withdrawal.Request(appId,createParams);
}
jdpay
$params = [
'user' => 'user_test_01', // 用户 ID
'amount' => 200, // 转账金额
'channel' => 'jdpay', //提现使用渠道。
'user_fee' => 10, //用户需要承担的手续费
'description' => 'Your Description', //描述。
'order_no' => time() . mt_rand(100000, 999999), //提现订单号
'extra' => array(
'account' => '6214850288888888', //必填项 收款人银行卡号或者存折号。
'name' => '张三', //必填项 收款人姓名。
'open_bank_code' => '0308' //必填项 开户银行编号。。
)
$withdrawal = \Pingpp\Withdrawal::create($params);
echo $withdrawal;
Pingpp.apiKey = "sk_test_ibbTe5jLGCi5rzfH4OqPW9KC";
Map<String, Object> params = new HashMap<String, Object>();
params.put("user", "test_user_001"); // 用户 ID, 必传
params.put("channel", "jdpay"); // 提现使用渠道。
params.put("amount", 100); // 转账金额, 必传
params.put("description", "custom description"); // 描述, 可选
params.put("order_no", "1010" + System.currentTimeMillis()); // 提现订单号, 必传
Map<String, String> extra = new HashMap<String, String>();
extra.put("account", "6214850288888888");
extra.put("name", "NAME");
extra.put("open_bank_code", "0308"); //开户银行编号。
params.put("extra", extra);
params.put("user_fee", 5); // 用户需要承担的手续费, 必传
params.put("settle_account", "test_user_001"); // 使用结算账户提现,不需要填写 extra 参数,同时填写时,结算账号不生效
Withdrawal withdrawal = Withdrawal.create(params); // 创建 Withdrawal 对象
var order_no = new Date().getTime().toString().substr(0, 10);
var channel = 'jdpay';
var extra = withdrawal_extra(channel);
var params = {
'user': 'test_user_001',
'amount': 20, // 金额
'channel': channel, // 渠道
'user_fee': 10, // 用户需要承担的手续费
'order_no': order_no, // 提现订单号
'description': 'test232description', // 转账描述
'extra': extra
};
request_info = {
"amount": 20000,
"user_fee": 50,
'order_no': '1234567890',
"description": "Your description",
'channel': 'jdpay',
"extra": {
"account": "6214850288888888",
"name": "姓名",
"open_bank_code":"0308" #开户银行编号
},
new_wd = pingpp.Withdrawal.create(user="user_id_001", **request_info)
print(new_wd)
params = {
:user => existed_user_id,
:amount => 10,
:user_fee => 0,
:description => "用户提现",
:channel => "jdpay",
:extra => {
:account => "6214850288888888",
:name => "姓名",
:open_bank_code => "0308" #开户银行编号。
},
# 结算账号 ID ; 与 user 绑定的结算账号 ID
# :settle_account => "SETTLE_ACCOUNT",
# metadata optional|hash
:metadata => {
:custom_key => "custom_value",
# ...
}
}
if params[:channel] == "allinpay"
params[:order_no] = "301002#{Time.now.to_i.to_s}#{rand(999999).to_s.rjust(6, "0")}"
else
params[:order_no] = "#{Time.now.to_i.to_s}#{rand(9999).to_s.rjust(4, "0")}"
end
o = Pingpp::Withdrawal.create(
params,
{
# URL 中的 {app_id} ; 用于覆盖 Pingpp.app_id
:app => get_app_id
}
)
func (c *WithdrawalDemo) Setup(app string) {
c.demoAppID = app
c.demoChannel = "jdpay"
}
// 余额提现申请
func (c *WithdrawalDemo) New() (*pingpp.Withdrawal, error) {
params := &pingpp.WithdrawalParams{
User: "user_001",
Amount: 20000,
User_fee: 0,
Channel: c.demoChannel,
Description: "Your description",
Order_no: "20160829133002",
Extra: common.WithdrawExtra[c.demoChannel],
}
return withdrawal.New(c.demoAppID, params)
}
public static Withdrawal unionpayWithdrawal(String appId)
{
var createParams = new Dictionary<string, object>
{
{"order_no", "2016111000039"}, //提现订单号
{"amount" , 1}, //体现金额。
{"user_fee", 0}, //用户需要承担的手续费。
{"channel", "jdpay"}, //提现使用渠道。
{"user", "user_001"}, //用户 ID。
{"description", "Your description"}, //描述
{"extra", new Dictionary<string, object>{
{"account", "6214850288888888"}, //收款人银行卡号或者存折号。
{"name", "张三"}, // 收款人姓名。
{"open_bank_code", "0308"} //开户银行编号。
}},
//{"settle_account", "320217081417114300000501"} //使用结算账户提现,不需要填写 channel 和 extra 相关参数,同时填写时,结算账号不生效
};
return Withdrawal.Request(appId,createParams);
}

Ping++ 收到提现请求后返回给你的服务器一个 withdrwal 对象,下面是 withdrwal 的一个示例:

unionpay
{
"id": "1701611150302360654",
"object": "withdrawal",
"app": "app_1Gqj58ynP0mHeX1q",
"amount": 20000,
"asset_transaction": "",
"balance_transaction": "",
"channel": "unionpay",
"created": 1472648887,
"description": "申请提现",
"extra": {
"card_number": "6225210207073918",
"user_name": "姓名",
"open_bank_code": "0102",
"prov": "上海",
"city": "上海"
},
"failure_msg": null,
"fee": 200,
"livemode": true,
"metadata":{},
"operation_url": null,
"order_no": "20160829133002",
"source": null,
"status": "created",
"time_canceled": null,
"time_succeeded": null,
"user": "user_001",
"user_fee": 50,
"settle_account": null
}
alipay
{
"id": "1701611150302360654",
"object": "withdrawal",
"app": "app_1Gqj58ynP0mHeX1q",
"amount": 20000,
"asset_transaction": "",
"balance_transaction": "",
"channel": "alipay",
"created": 1472648887,
"description": "申请提现",
"extra": {
"account": "user@example.com",
"user_name": "姓名",
"account_type": "ALIPAY_LOGONID"
},
"failure_msg": null,
"fee": 200,
"livemode": true,
"metadata":{},
"operation_url": null,
"order_no": "20160829133002",
"source": null,
"status": "created",
"time_canceled": null,
"time_succeeded": null,
"user": "user_001",
"user_fee": 50,
"settle_account": null
}
wx
{
"id": "1701611150302360654",
"object": "withdrawal",
"app": "app_1Gqj58ynP0mHeX1q",
"amount": 20000,
"asset_transaction": "",
"balance_transaction": "",
"channel": "wx",
"created": 1472648887,
"description": "申请提现",
"extra": {
"open_id": "wxopenid",
"name": "姓名",
"force_check": false
},
"failure_msg": null,
"fee": 200,
"livemode": true,
"metadata":{},
"operation_url": null,
"order_no": "20160829133002",
"source": null,
"status": "created",
"time_canceled": null,
"time_succeeded": null,
"user": "user_001",
"user_fee": 50,
"settle_account": null
}
wx_pub
{
"id": "1701611150302360654",
"object": "withdrawal",
"app": "app_1Gqj58ynP0mHeX1q",
"amount": 20000,
"asset_transaction": "",
"balance_transaction": "",
"channel": "wx_pub",
"created": 1472648887,
"description": "申请提现",
"extra": {
"open_id": "wxopenid",
"name": "姓名",
"force_check": false
},
"failure_msg": null,
"fee": 200,
"livemode": true,
"metadata":{},
"operation_url": null,
"order_no": "20160829133002",
"source": null,
"status": "created",
"time_canceled": null,
"time_succeeded": null,
"user": "user_001",
"user_fee": 50,
"settle_account": null
}
wx_lite
{
"id": "1701611150302360654",
"object": "withdrawal",
"app": "app_1Gqj58ynP0mHeX1q",
"amount": 20000,
"asset_transaction": "",
"balance_transaction": "",
"channel": "wx_lite",
"created": 1472648887,
"description": "申请提现",
"extra": {
"open_id": "wxopenid",
"name": "姓名",
"force_check": false
},
"failure_msg": null,
"fee": 200,
"livemode": true,
"metadata":{},
"operation_url": null,
"order_no": "20160829133002",
"source": null,
"status": "created",
"time_canceled": null,
"time_succeeded": null,
"user": "user_001",
"user_fee": 50,
"settle_account": null
}
allinpay
{
"id": "1701611150302360654",
"object": "withdrawal",
"app": "app_1Gqj58ynP0mHeX1q",
"amount": 20000,
"asset_transaction": "",
"balance_transaction": "",
"channel": "allinpay",
"created": 1472648887,
"description": "申请提现",
"extra": {
"account": "6214850288888888",
"name": "姓名",
"open_bank_code": "0308" ,
"business_code":"09900",
"card_type":0
},
"failure_msg": null,
"fee": 200,
"livemode": true,
"metadata":{},
"operation_url": null,
"order_no": "20160829133002",
"source": null,
"status": "created",
"time_canceled": null,
"time_succeeded": null,
"user": "user_001",
"user_fee": 50,
"settle_account": null
}
jdpay
{
"id": "1701611150302360654",
"object": "withdrawal",
"app": "app_1Gqj58ynP0mHeX1q",
"amount": 20000,
"asset_transaction": "",
"balance_transaction": "",
"channel": "allinpay",
"created": 1472648887,
"description": "申请提现",
"extra": {
"account": "6214850288888888",
"name": "姓名",
"open_bank_code": "0308"
},
"failure_msg": null,
"fee": 200,
"livemode": true,
"metadata":{},
"operation_url": null,
"order_no": "20160829133002",
"source": null,
"status": "created",
"time_canceled": null,
"time_succeeded": null,
"user": "user_001",
"user_fee": 50,
"settle_account": null
}

第四步:更新 withdrawal 对象

调用 Ping++ Server SDK 发起余额提现请求状态更新,发起请求所需参数具体可参考 API 文档。确认提现,则更新状态时 status 传值为 pending ;取消提现申请,则更新状态时 status 传值为 canceled 。若提现成功,则 withdrawal 对象的 status 值变为 succeeded。若提现因用户原因失败(如提现账户有误),则 withdrawal 对象的 status 值变为 failed,若提现因非用户原因失败(如商户付款金额不足),则 withdrawal 对象的 status 值变为 created。

发起提现确认:

\Pingpp\Withdrawal::confirm($withdrawalId);
Map<String, Object> params = new HashMap<String, Object>();
params.put("status", "pending");
Withdrawal withdrawal = Withdrawal.update(id, params);
pingpp.withdrawals.confirm(
APP_ID, // App ID
'1701709011052380697', // 提现 ID
function(err, data) {
if (err != null){
console.log('pingpp.withdrawals.confirm fail:', err);
}
// YOUR CODE
}
);
pingpp.Withdrawal.confirm("withDrawal_0002", app=app_id, user="user_id_001")
Pingpp::Withdrawal.confirm(
existed_withdrawal_id,
{ :app => get_app_id } # App 信息
)
withdrawal.Confirm(c.demoAppID, "1701611150302360654")
Withdrawal.Confirm(appId, wd.Id)

发起提现取消:

\Pingpp\Withdrawal::cancel($withdrawalId);
Map<String, Object> params = new HashMap<String, Object>();
params.put("status", "canceled");
Withdrawal withdrawal = Withdrawal.update(id, params);
pingpp.withdrawals.cancel(
APP_ID, // App ID
'1701709011053327225', // 提现 ID
function(err, data) {
if (err != null){
console.log('pingpp.withdrawals.cancel fail:', err);
}
// YOUR CODE
}
);
pingpp.Withdrawal.cancel("withDrawal_0002", app=app_id, user="user_id_001")
o = Pingpp::Withdrawal.cancel(
existed_withdrawal_id,
{ :app => get_app_id } # App 信息
)
withdrawal.Cancel(c.demoAppID, "1701611150302360654")
Withdrawal.Cancel(appId, wd.Id)

Ping++ 收到提现状态更新请求后返回给你的服务器一个 withdrwal 对象,下面是 withdrwal 的一个示例:

{
"id": "1701611150302360654",
"object": "withdrawal",
"app": "app_1Gqj58ynP0mHeX1q",
"amount": 20000,
"asset_transaction": "",
"balance_transaction": "",
"channel": "unionpay",
"created": 1472648887,
"description": "提现确认",
"extra": {
"card_number": "6225210207073918",
"user_name": "姓名",
"open_bank_code": "0102",
"prov": "上海",
"city": "上海"
},
"failure_msg": null,
"fee": 200,
"livemode": true,
"metadata":{},
"operation_url": null,
"order_no": "20160829133002",
"source": "tr_testCGyHqD",
"status": "succeeded",
"time_canceled": null,
"time_succeeded": null,
"user": "user_001",
"user_fee": 50,
"settle_account": null
}

第五步:接收 Webhooks 通知

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

{
"created": 1505992666,
"livemode": true,
"type": "balance.withdrawal.succeeded",
"data": {
"object": {
"id": "1711111111222210555",
"object": "withdrawal",
"app": "app_xxxxxxxxxxxx",
"amount": 100,
"asset_transaction": null,
"balance_transaction": "601170921711xxxxxxx",
"channel": "wx_pub",
"created": 1505992661,
"description": "用户提现",
"extra": {
"open_id": "openid"
},
"failure_msg": null,
"fee": 0,
"livemode": true,
"metadata": {},
"operation_url": null,
"order_no": "12345678",
"source": "tr_Puxxxxxxxxxx",
"status": "succeeded",
"time_canceled": null,
"time_succeeded": 1505992666,
"user": "user_001",
"user_fee": 0,
"settle_account": null
}
},
"object": "event",
"request": "iar_xxxxxxxxxx",
"scope": "app_xxxxxxxxxx",
"acct_id": "acct_xxxxxxxxxx"
}

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

{
"created": 1505992666,
"livemode": true,
"type": "balance.withdrawal.failed",
"data": {
"object": {
"id": "1711111111222210555",
"object": "withdrawal",
"app": "app_xxxxxxxxxxxx",
"amount": 100,
"asset_transaction": null,
"balance_transaction": null,
"channel": "wx_pub",
"created": 1505992661,
"description": "用户提现",
"extra": {
"open_id": "openid"
},
"failure_msg": null,
"fee": 0,
"livemode": true,
"metadata": {},
"operation_url": null,
"order_no": "12345678",
"source": "tr_Puxxxxxxxxxx",
"status": "failed",
"time_canceled": null,
"time_succeeded": 1505992666,
"user": "user_001",
"user_fee": 0,
"settle_account": null
}
},
"object": "event",
"request": "iar_xxxxxxxxxx",
"scope": "app_xxxxxxxxxx",
"acct_id": "acct_xxxxxxxxxx"
}

第六步:验证 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++ 管理平台提供详细的提现信息和 Webhooks 功能,所以查询功能相对来说并不是那么必要。如果商户本身由于某种原因导致 Webhooks 没有收到或者延缓更新时,可以主动调用提现查询接口来获得交易的状态。

查询提现 Withdrawal 对象

\Pingpp\Withdrawal::retrieve($withdrawalId);
Withdrawal.retrieve(withdrawalId);
pingpp.withdrawals.retrieve(
APP_ID, // App ID
'1701709011052380697', // 提现 ID
function(err, data) {
if (err != null){
console.log('pingpp.withdrawals.retrieve fail:', err);
}
// YOUR CODE
}
);
pingpp.Withdrawal.retrieve("withDrawal_0002", app=app_id, user="user_id_001")
o = Pingpp::Withdrawal.retrieve(
existed_withdrawal_id,
{ :app => get_app_id } # App 信息
)
withdrawal.Get(c.demoAppID, "1701611150302360654")
Withdrawal.Retrieve(appId, withDrawal.Id);

查询提现 Withdrawal 对象列表

\Pingpp\Withdrawal::all(['per_page' => 3]);
Map<String, Object> params = new HashMap<String, Object>();
params.put("page", 1);
params.put("per_page", 3);
WithdrawalCollection withdrawals = Withdrawal.list(params);
pingpp.withdrawals.list(
APP_ID, // App ID
{ per_page: 3 },
function(err, data) {
if (err != null){
console.log('pingpp.withdrawals.retrieve fail:', err);
}
// YOUR CODE
}
);
pingpp.Withdrawal.list(user="user_id_001")
o = Pingpp::Withdrawal.list(
{ :per_page => 3, :page => 1 },
{ :app => get_app_id } # App 信息
)
params := &pingpp.PagingParams{}
params.Filters.AddFilter("page", "", "1")
params.Filters.AddFilter("per_page", "", "2")
return withdrawal.List(c.demoAppID, params)
Withdrawal.List(appId, listParams);

注意事项

  1. 你需要在 Ping++ 的管理平台里填写 Webhooks 通知地址,详见 Webhooks 配置说明,你的服务器需要监听这个地址并且接收 Webhooks 通知,接收到 Webhooks 通知后需给 Ping++ 返回服务器状态 2xx 。此时事件类型是 balance.withdrawal.succeededbalance.withdrawal.failed ,其字段 data 包含了 object 字段, object 字段的值是一个 withdrawal 对象。
  2. 若你的服务器未正确返回 2xx,Ping++ 会在 25 小时内向商户服务器最多发送 10 次 Webhooks 通知,时间间隔分别为 5s、10s、2min、5min、10min、30min、1h、2h、6h、15h,直到用户向 Ping++ 返回服务器状态 2xx 或者超过最大重发次数为止。
  3. 在可接受的时间范围内,如果你服务端没有收到 Webhooks 的通知,你也可以调用 Server-SDK 封装的查询方法,主动向 Ping++ 发起请求来获得订单状态,该查询结果可以作为交易结果。

下一步余额赠送