红包
接入 Ping++ 发起红包,仅需要 Ping++ Server SDK 即可。服务器端需要做的就是向 Ping++ 请求 Redenvelope 对象,并且监听和获取 Webhooks 通知,具体步骤如下:
- 设置 API-Key
- SDK 验证签名设置
- 服务端发起红包请求获取红包对象
- 接收 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 开关。
第三步:服务端发起红包请求获取红包对象
调用 Ping++ Server SDK 发起红包请求,发起请求所需参数具体可参考 API 文档,目前只支持微信现金红包。
$red = \Pingpp\RedEnvelope::create( array( 'subject' => 'Your Subject', 'body' => 'Your Body', 'amount' => 100,//订单总金额, 人民币单位:分(如订单总金额为 1 元,此处请填 100) 'order_no' => '123456789', 'currency' => 'cny', 'extra' => array( 'send_name' => 'Send Name' ), 'recipient' => 'User OpenId',//发送红包给指定用户的 open_id 'channel' => 'wx_pub',//此处 wx_pub 为公众平台的支付 'app' => array('id' => 'app_1Gqj58ynP0mHeX1q'), 'description' => 'Your Description' ) ); echo $red;
Map<String, Object> redenvelope = new HashMap<String, Object>(); redenvelope.put("amount", 100);//订单总金额, 人民币单位:分(如订单总金额为 1 元,此处请填 100) redenvelope.put("currency", "cny"); redenvelope.put("subject", "Your Subject"); redenvelope.put("body", "Your Body"); redenvelope.put("order_no", "123456789"); redenvelope.put("channel", "wx_pub");//此处 wx_pub 为公众平台的支付 redenvelope.put("recipient", "User OpenId");//发送红包给指定用户的 open_id redenvelope.put("description", "Your Description"); Map<String, String> app = new HashMap<String, String>(); app.put("id", "app_1Gqj58ynP0mHeX1q"); redenvelope.put("app", app); Map<String, String> extra = new HashMap<String, String>(); extra.put("send_name", "Send Name"); redenvelope.put("extra", extra); RedEnvelope red = RedEnvelope.create(redenvelope);
pingpp.redEnvelopes.create({ order_no: "123456789", app: { id: "app_1Gqj58ynP0mHeX1q" }, channel: "wx_pub",//此处 wx_pub 为公众平台的支付 amount: 100,//订单总金额, 人民币单位:分(如订单总金额为 1 元,此处请填 100) currency: "cny", subject: "Your Subject", body: "Your Body", extra: { send_name: "Send Name" }, recipient: "User OpenId",//发送红包给指定用户的 open_id description: "Your Description"}, function(err, redEnvelope) { // YOUR CODE});
redenvelope = pingpp.RedEnvelope.create( order_no='123456789', channel='wx_pub',//此处 wx_pub 为公众平台的支付 amount=100, #订单总金额, 人民币单位:分(如订单总金额为 1 元,此处请填 100) subject='Your Subject', body='Your Body', currency='cny', app=dict(id='app_1Gqj58ynP0mHeX1q'), extra=dict(send_name='Send Name'), recipient='User OpenId',# 发送红包给指定用户的 open_id description='Your Description')print redenvelope
red = Pingpp::RedEnvelope.create( :order_no => "123456789", :app => { :id => "app_1Gqj58ynP0mHeX1q" }, :channel => "wx_pub",# 此处 wx_pub 为公众平台的支付 :amount => 100,# 订单总金额, 人民币单位:分(如订单总金额为 1 元,此处请填 100) :currency => "cny", :subject => "Your Subject", :body => "Your Body", :extra => { :send_name => "Send Name" }, :recipient => "User OpenId",# 发送红包给指定用户的 open_id :description => "Your Description" ) puts red
extra := make(map[string]interface{})extra["send_name"] = "Send Name"redenvelopeParams := &pingpp.RedEnvelopeParams{ App: pingpp.App{Id: "app_1Gqj58ynP0mHeX1q"}, Channel: "wx_pub",//此处 wx_pub 为公众平台的支付 Order_no: "123456789", Amount: 100,//订单总金额, 人民币单位:分(如订单总金额为 1 元,此处请填 100) Currency: "cny", Recipient: "User OpenId",//发送红包给指定用户的 open_id Subject: "Your Subject", Body: "Your Body", Description: "Your Description", Extra: extra,}redEnvelope, err := redEnvelope.New(redenvelopeParams)
var redParams = new Dictionary<string, object>{ {"order_no", "123456789"}, {"amount", 100}, {"channel", "wx_pub"},//此处 wx_pub 为公众平台的支付 {"currency", "cny"}, {"subject", "Your Subject"}, {"body", "Your Body"}, {"recipient", "User OpenId"},//发送红包给指定用户的 open_id {"description", "Your Description"}, {"extra", new Dictionary<string, object> {{"send_name", "Send Name"}}}, {"app", new Dictionary<string, string> {{"id", app_1Gqj58ynP0mHeX1q}}}}; var red = RedEnvelope.Create(redParams);
第四步:接收 Webhooks 通知
当商户成功发送红包后 Ping++ 会给你配置在 Ping++ 管理平台的 Webhooks 通知地址主动发送红包发送的结果,我们称之为 Webhooks 通知。 Webhooks 通知是以 POST
形式发送的 JSON,放在请求的 body 里,内容是 Event 对象,红包发送成功的事件类型为 red_envelope.sent
,红包领取成功的事件为 red_envelope.received
。你需要监听并接收 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 "charge.succeeded": // 开发者在此处加入对支付异步通知的处理代码 header($_SERVER['SERVER_PROTOCOL'] . ' 200 OK'); break; case "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 ("charge.succeeded".equals(event.getType())) { response.setStatus(200); } else if ("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 "charge.succeeded": // 开发者在此处加入对支付异步通知的处理代码 return resp("OK", 200); break; case "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'] == 'charge.succeeded': return Response(status=200) elif event['type'] == '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'] == 'charge.succeeded' # 开发者在此处加入对支付异步通知的处理代码 status = 200 response_body = 'OK' elsif event['type'] == '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 == "charge.succeeded" { // TODO your code for charge w.WriteHeader(http.StatusOK) } else if webhook.Type == "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 通知地址配置的 red_envelope.sent
对象的示例:
{ "id": "evt_dS28Kn8qsUCBZYTgSUxCPnKA", "created": 1458793491, "livemode": true, "type": "red_envelope.sent", "data": { "object": { "id": "red_04er10TuvT4OCyLO08aLOqbD", "object": "red_envelope", "created": 1458793489, "received": null, "livemode": true, "status": "sent", "app": "app_1Gqj58ynP0mHeX1q", "channel": "wx_pub", "order_no": "123456789", "transaction_no": "0010037269201603240630694982", "amount": 100, "amount_settle": 100, "currency": "cny", "recipient": "o9zpMs7Xk9e9aJbTXgufovuWGp7c", "subject": "Your Subject", "body": "Your Body", "description": "Your Description", "failure_msg": null, "extra": { "send_name": "Send Name" }, "metadata": {} } }, "object": "event", "pending_webhooks": 0, "request": "iar_H0OifTOmTS84Xfj5mT0mvfb1"}
第五步:验证 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++ 管理平台提供详细的订单信息和 Webhooks 功能,但是如果商户本身由于某种原因导致 Webhooks 没有收到或者延缓更新时,可以主动调用红包查询接口来获得红包发送的状态。
单笔红包查询
\Pingpp\RedEnvelope::retrieve('RED_ID');
Charge ch = RedEnvelope.retrieve("RED_ID");
pingpp.RedEnvelope.retrieve( "RED_ID", function(err, RedEnvelope) { // YOUR CODE } );
ch = pingpp.RedEnvelope.retrieve('RED_ID')
Pingpp::RedEnvelope.retrieve("RED_ID")
redEnvelope, err := redEnvelope.Get("RED_ID")
var red = RedEnvelope.Retrieve("ch_id");
红包列表查询
\Pingpp\RedEnvelope::all(array('limit' => 3));
Map<String, Object> RedEnvelopeParams = new HashMap<String, Object>(); RedEnvelopeParams.put("limit", 3); RedEnvelope.all(RedEnvelopeParams);
pingpp.RedEnvelope.list({limit: 3}, function(err, RedEnvelope) { // 异步调用 });
pingpp.RedEnvelope.all(limit=3)
Pingpp::RedEnvelope.all(:limit => 3)
params := &pingpp.RedEnvelopeListParams{} params.Filters.AddFilter("limit", "", "3") //设置是不是只需要之前设置的 limit 这一个查询参数 params.Single = true i := redEnvelope.List(params) for i.Next() { c := i.RedEnvelope() }
var reds = RedEnvelope.List(new Dictionary<string, object> {{"limit", 3}});
注意事项
- 请求 Ping++ 红包接口返回的 Redenvelope 对象中的
status
是sending
状态,实际发送成功以及接收成功请根据 Webhooks 通知为准。 - 红包发送成功后但是未被用户接收的情况下,此时的 Event 类型是
red_envelope.sent
,红包已被用户接收时返回的 Event 类型是red_envelope.received
,其字段data
包含了object
字段,object
字段的值是一个 Redenvelope 对象。 - 你需要在 Ping++ 的管理平台里填写 Webhooks 通知地址,详见 Webhooks 配置说明,你的服务器需要监听这个地址并且接收 Webhooks 通知,接收到 Webhooks 通知后需给 Ping++ 返回服务器状态
2xx
。此时事件类型是red_envelope.sent
或red_envelope.received
,其字段data
包含了object
字段,object
字段的值是一个 Redenvelope 对象。 - test 模式下调用 红包查询接口才会触发 Webhooks 通知,第一次查询会触发
red_envelope.sent
事件,第二次查询会触发red_envelope.received
事件。 - 若你的服务器未正确返回
2xx
,Ping++ 服务器会在 25 小时内向你的服务器不断重发通知,最多 10 次。Webhooks 首次是即时推送,重试通知时间间隔为 5s、10s、2min、5min、10min、30min、1h、2h、6h、15h,直到你正确回复状态2xx
或者超过最大重发次数,Ping++ 将不再发送。 - 在可接受的时间范围内,如果你服务端没有收到 Webhooks 的通知,你也可以调用 Server-SDK 封装的查询方法,主动向 Ping++ 发起请求来获得订单状态,该查询结果可以作为交易结果。
下一步会员账户系统