内嵌SDK
# 快速开始
推荐使用内嵌接入
由 pingpong checkout 提供收银台⻚面,商户无需自行开发收银台⻚面,执行完结账请求,直接跳转至 pingpong checkout 收银台⻚面。
# 收银台模式系统交互流程

# 如何接入
调用收银台 v2/checkout 接口
v2/checkout 接口返回token
商户将用户⻚面重定向至收银台展示页面,传入token并初始化SDK
用户浏览器渲染PingPongPay 收银台⻚面,完成支付
- 审核通过
- 审核拒绝
若商户7天内无反馈,pingpong checkout 7天后将自动撤销该笔交易。
# 请求地址
https://{host}/v2/checkout
# 请求参数
参数必填属性说明:必填(M),可选(O),条件必填(C)。
# 请求参数签名
# 加签参数列表
参数名 | 描述 |
---|---|
clientId | PingPong商户号 |
accId | PingPong商户店铺号 |
amount | 交易金额 |
currency | 交易币种 |
cardNum | 交易卡号 |
transactionId | PingPong交易流水号 |
merchantTransactionId | 商户交易流水号 |
signType | 加签类型 |
shopperResultUrl | 商户自定义接收重定向的结果 URL |
# 获取签名参数列表
准备一个ArrayList 加签参数列表
遍历收到的请求参数列表。
每次循环都比较请求参数的参数名是否在加签参数列表
在加签参数列表的参数加入新的签名参数列表
# PHP伪代码示例:
<?php
$request = isPingPongPay($requestParams);
if(empty($request)){
throw new Error("验签失败");
}
$needSign = [];
$scope = [
"clientId",
"accId",
"amount",
"currency",
"cardNum",
"transactionId",
"merchantTransactionId",
"requestId",
"signType",
]
foreach($request as $key=>$value){
if(in_array($key,$scope)){
$needSign[$key] = $value;
}
// then if not empty $needSign you will to Generate signature string
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 签名串的生成
待签名参数列表按照字典序排序
将排序后的待签名参数列表转成url查询字符串(用'=' 进行参数名和参数值(trim 后的值)的拼接,用'&'进行多 个参数之间的拼接,即 key1=val1&key2=val2&key3=val。)
3.拼接签名密钥(secret): 签名密钥放入签名串的位置为签名串的开头,即{salt}key1=val2&key2=val2&key3=val3
- 签名串进行MD5运算后所有字符串转成大写,即可获得正确的签名
# 重要入参说明
1. merchantTransactionId 应是全局唯一,不可重复
2. amount 精确到两位小数,没有小数部分的补零。(如 25.12 或 20.00)
3. paymentType 一般为SALE 如果要对接AUTH请在对接过程告知,并在测试报告中声明,一定通过验收后才能上线,以免发生交易问题
4. 在收银台模式下 device browserInfo 可以自动抓取,可以不传
5. shipping billing goods 不能为空 eCommerce airline reCharge carRental 根据行业情况填写,这些参数影响交易成功率,请详实填写。
6. shipping billing state 美国加拿大地区必须填写二字简码,有些地区没有state,可以填写空字符串。
7. merchantUserId 必须全局唯一 没登录或者获取不到的情况请传空字符串。
# 请求示例
POST /v2/checkout HTTP/1.1
Host: {host}
Content-Type: application/json
Content-Length: 3012
{
"sign": "3F9DF2F986EFCE55919C2CA991689D4C",
"signType": "MD5",
"accId": "2018092714313010016291",
"amount": "20",
"currency": "USD",
"merchantTransactionId": "MTN193495030728",
"paymentType": "SALE",
"shopperResultUrl": "http://127.0.0.1:8010/demo/checkoutResult",
"threeDSecure": "N",
"riskInfo": {
"device": {
"orderTerminal": "01",
"fingerprintId": "e10adc3949ba59abbe56e057f20f883e"
},
"customer": {
"customerId": "UN00000001",
"firstName": "James",
"lastName": "LeBron",
"email": "demo@pingpongx.com",
"domain": "pingpongx.com",
"phone": "15988890852",
"mobile": "15988890856",
"workPhone": "15988890852",
"identificationType": "ID",
"identificationId": "330102199003070115",
"registerTime": "20191101122000",
"registerIp": "222.126.52.23",
"registerTerminal": "PC",
"registerCountry": "US",
"registerRange": "1",
"orderTime": "20191201122000",
"orderIp": "222.126.52.23",
"orderCountry": "US",
"payIp": "222.126.52.23",
"payCountry": "US",
"loginTime": "20200427122000",
"loginIp": "222.126.52.23",
"lastPayTime": "20200427122000",
"acquisitionChannel": "Seach engine",
"firstOrder": "N",
"nonMemberOrder": "N",
"preferentialOrder": "N",
"birthDate": "20000212",
"customerStatus": "EXISTING"
},
"goods": [
{
"name": "Macaron",
"description": "Colorful macaron",
"sku": "20191201331",
"averageUnitPrice": "20",
"number": "1",
"virtualProduct": "N"
}
],
"shipping": {
"firstName": "James",
"lastName": "LeBron",
"phone": "13588185079",
"email": "demo@pingpognx.com",
"street": "1986 Broad Street",
"postcode": "35222",
"city": "Birmingham",
"state": "Alabama",
"country": "US",
"lastModifierStreetTime": "20191225162010",
"lastModifierPhoneTime": "20191225162010"
},
"billing": {
"firstName": "James",
"lastName": "LeBron",
"phone": "13588185079",
"email": "demo@pingpognx.com",
"street": "1986 Broad Street",
"postcode": "35222",
"city": "Birmingham",
"state": "Alabama",
"country": "US"
},
"ecommerce": {
"freeShipping": "N",
"shippingMethod": "sea"
}
},
"notificationUrl": "http://127.0.0.1:8010/demo/callback/checkoutCallback",
"merchantUserId": "UN00000001",
"remark": "demo-checkout-normal"
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<?php
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => 'https://%7Bhost%7D/v2/checkout',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS =>'{
"sign": "3F9DF2F986EFCE55919C2CA991689D4C",
"signType": "MD5",
"accId": "2018092714313010016291",
"amount": "20",
"currency": "USD",
"merchantTransactionId": "MTN193495030728",
"paymentType": "SALE",
"shopperResultUrl": "http://127.0.0.1:8010/demo/checkoutResult",
"threeDSecure": "N",
"riskInfo": {
"device": {
"orderTerminal": "01",
"fingerprintId": "e10adc3949ba59abbe56e057f20f883e"
},
"customer": {
"customerId": "UN00000001",
"firstName": "James",
"lastName": "LeBron",
"email": "demo@pingpongx.com",
"domain": "pingpongx.com",
"phone": "15988890852",
"mobile": "15988890856",
"workPhone": "15988890852",
"identificationType": "ID",
"identificationId": "330102199003070115",
"registerTime": "20191101122000",
"registerIp": "222.126.52.23",
"registerTerminal": "PC",
"registerCountry": "US",
"registerRange": "1",
"orderTime": "20191201122000",
"orderIp": "222.126.52.23",
"orderCountry": "US",
"payIp": "222.126.52.23",
"payCountry": "US",
"loginTime": "20200427122000",
"loginIp": "222.126.52.23",
"lastPayTime": "20200427122000",
"acquisitionChannel": "Seach engine",
"firstOrder": "N",
"nonMemberOrder": "N",
"preferentialOrder": "N",
"birthDate": "20000212",
"customerStatus": "EXISTING"
},
"goods": [
{
"name": "Macaron",
"description": "Colorful macaron",
"sku": "20191201331",
"averageUnitPrice": "20",
"number": "1",
"virtualProduct": "N"
}
],
"shipping": {
"firstName": "James",
"lastName": "LeBron",
"phone": "13588185079",
"email": "demo@pingpognx.com",
"street": "1986 Broad Street",
"postcode": "35222",
"city": "Birmingham",
"state": "Alabama",
"country": "US",
"lastModifierStreetTime": "20191225162010",
"lastModifierPhoneTime": "20191225162010"
},
"billing": {
"firstName": "James",
"lastName": "LeBron",
"phone": "13588185079",
"email": "demo@pingpognx.com",
"street": "1986 Broad Street",
"postcode": "35222",
"city": "Birmingham",
"state": "Alabama",
"country": "US"
},
"ecommerce": {
"freeShipping": "N",
"shippingMethod": "sea"
}
},
"notificationUrl": "http://127.0.0.1:8010/demo/callback/checkoutCallback",
"merchantUserId": "UN00000001",
"remark": "demo-checkout-normal"
}',
CURLOPT_HTTPHEADER => array(
'Content-Type: application/json'
),
));
$response = curl_exec($curl);
curl_close($curl);
echo $response;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
MediaType mediaType = MediaType.parse("application/json");
RequestBody body = RequestBody.create(mediaType, "{\n \"sign\": \"3F9DF2F986EFCE55919C2CA991689D4C\",\n \"signType\": \"MD5\",\n \"accId\": \"2018092714313010016291\",\n \"amount\": \"20\",\n \"currency\": \"USD\",\n \"merchantTransactionId\": \"MTN193495030728\",\n \"paymentType\": \"SALE\",\n \"shopperResultUrl\": \"http://127.0.0.1:8010/demo/checkoutResult\",\n \"threeDSecure\": \"N\",\n \"riskInfo\": {\n \"device\": {\n \"orderTerminal\": \"01\",\n \"fingerprintId\": \"e10adc3949ba59abbe56e057f20f883e\"\n },\n \"customer\": {\n \"customerId\": \"UN00000001\",\n \"firstName\": \"James\",\n \"lastName\": \"LeBron\",\n \"email\": \"demo@pingpongx.com\",\n \"domain\": \"pingpongx.com\",\n \"phone\": \"15988890852\",\n \"mobile\": \"15988890856\",\n \"workPhone\": \"15988890852\",\n \"identificationType\": \"ID\",\n \"identificationId\": \"330102199003070115\",\n \"registerTime\": \"20191101122000\",\n \"registerIp\": \"222.126.52.23\",\n \"registerTerminal\": \"PC\",\n \"registerCountry\": \"US\",\n \"registerRange\": \"1\",\n \"orderTime\": \"20191201122000\",\n \"orderIp\": \"222.126.52.23\",\n \"orderCountry\": \"US\",\n \"payIp\": \"222.126.52.23\",\n \"payCountry\": \"US\",\n \"loginTime\": \"20200427122000\",\n \"loginIp\": \"222.126.52.23\",\n \"lastPayTime\": \"20200427122000\",\n \"acquisitionChannel\": \"Seach engine\",\n \"firstOrder\": \"N\",\n \"nonMemberOrder\": \"N\",\n \"preferentialOrder\": \"N\",\n \"birthDate\": \"20000212\",\n \"customerStatus\": \"EXISTING\"\n },\n \"goods\": [\n {\n \"name\": \"Macaron\",\n \"description\": \"Colorful macaron\",\n \"sku\": \"20191201331\",\n \"averageUnitPrice\": \"20\",\n \"number\": \"1\",\n \"virtualProduct\": \"N\"\n }\n ],\n \"shipping\": {\n \"firstName\": \"James\",\n \"lastName\": \"LeBron\",\n \"phone\": \"13588185079\",\n \"email\": \"demo@pingpognx.com\",\n \"street\": \"1986 Broad Street\",\n \"postcode\": \"35222\",\n \"city\": \"Birmingham\",\n \"state\": \"Alabama\",\n \"country\": \"US\",\n \"lastModifierStreetTime\": \"20191225162010\",\n \"lastModifierPhoneTime\": \"20191225162010\"\n },\n \"billing\": {\n \"firstName\": \"James\",\n \"lastName\": \"LeBron\",\n \"phone\": \"13588185079\",\n \"email\": \"demo@pingpognx.com\",\n \"street\": \"1986 Broad Street\",\n \"postcode\": \"35222\",\n \"city\": \"Birmingham\",\n \"state\": \"Alabama\",\n \"country\": \"US\"\n },\n \"ecommerce\": {\n \"freeShipping\": \"N\",\n \"shippingMethod\": \"sea\"\n }\n },\n \"notificationUrl\": \"http://127.0.0.1:8010/demo/callback/checkoutCallback\",\n \"merchantUserId\": \"UN00000001\",\n \"remark\": \"demo-checkout-normal\"\n}");
Request request = new Request.Builder()
.url("https://{host}/v2/checkout")
.method("POST", body)
.addHeader("Content-Type", "application/json")
.build();
Response response = client.newCall(request).execute();
2
3
4
5
6
7
8
9
10
// Make sure to add code blocks to your code group
# 签名串示例
signContent:accId=2018092714313010016291&amount=20¤cy=USD&merchantTransactionId=MTN193495030728¬ificationUrl=http://127.0.0.1:8010/demo/callback/checkoutCallback&shopperResultUrl=http://127.0.0.1:8010/demo/checkoutResult&signType=MD5
# 响应参数
参数字段 | 参数属性 | 参数说明 |
---|---|---|
clientId | M | PingPong 商户商户号 |
accId | M | PingPong 商户店铺编号 |
merchantTransactionId | M | 商户网站的的交易流水号 |
code | M | 结果状态码 |
description | M | 结果描述 |
token | M | 收银台模式下,本次结账请求的唯一标示 |
paymentUrl | M | PingPong 支付收银台地址 |
innerJsUrl | M | PingPong 内嵌 JS 文件内容地址 |
signType | M | 签名规约,支持 MD5、SHA256,具体⻅本文“签名规约” 一栏 |
sign | M | 签名内容,具体⻅本文“签名规约”一栏 |
remark | O | 商户扩展字段 |
paymentHtml | O | 预留字段 |
# 响应示例
{
"accId": "2018092714313010016291",
"clientId": "2018092714313010016",
"code": "001000",
"description": "Successful request",
"innerJsUrl": "https://pay-cdn.pingpongx.com/production/static/sdk/ppPay.min.js?token=vr_YVR8u7rn7C1gG97DOg5W0OxzazxNYIE56ShWjA4lJrY4wchTnb47oNmp-9ubP",
"merchantTransactionId": "MTN193495030728",
"paymentUrl": "https://sandbox-pay-checkout.pingpongx.com/index.html?token=vr_YVR8u7rn7C1gG97DOg5W0OxzazxNYIE56ShWjA4lJrY4wchTnb47oNmp-9ubP",
"sign": "06D1C606847FD77CC70AAEB94A2A40D6",
"signType": "MD5",
"token": "vr_YVR8u7rn7C1gG97DOg5W0OxzazxNYIE56ShWjA4lJrY4wchTnb47oNmp-9ubP"
}
2
3
4
5
6
7
8
9
10
11
12
# 响应加签串示例
signContent:accId=2018092714313010016291&clientId=2018092714313010016&code=001000&description=Successful request&innerJsUrl=https://pay-cdn.pingpongx.com/production/static/sdk/ppPay.min.js?token=vr_YVR8u7rn7C1gG97DOg5W0OxzazxNYIE56ShWjA4lJrY4wchTnb47oNmp-9ubP&merchantTransactionId=MTN193495030728&paymentUrl=https://sandbox-pay-checkout.pingpongx.com/index.html?token=vr_YVR8u7rn7C1gG97DOg5W0OxzazxNYIE56ShWjA4lJrY4wchTnb47oNmp-9ubP&signType=MD5&token=vr_YVR8u7rn7C1gG97DOg5W0OxzazxNYIE56ShWjA4lJrY4wchTnb47oNmp-9ubP
# 唤起收银台
# 引入JS-SDK
复制以下代码,通过CDN地址引入PingPongPay JS-SDK
<script src="https://pay-cdn.pingpongx.com/production/static/sdk/1.2.0/ppPay.min.js"></script>
# 初始化并SDK并渲染收银台
# 初始化SDK示例代码
<script>
window.onload = function() {// dom 加载完毕后初始化
var client = new ppPay({
lang: 'zh', // 语言 - 中文(zh)和英文(en) (可选参数) root: "#ufo",
// 支付界面绑定的 div id (可选参数)(如果没有 root,收银台会自动插入 dom 到 body 中) manul: false, // 手动模式。 开启后可使用 client.actionPayment()方法调用支付 located: true, // 是否显示 located(默认为 true)
showPrice: true,// 是否展示价格
bill: true, // 是否显示账单名 (默认为 true)
mode: 'test', // 本地测试模式 sandbox test build (沙箱,测试,线上) (必填参数)、 menu: false, // 是否开启菜单侧边栏(如果接入了本地化,可通过开启次选项,显示多
个支付方式,默认 false)
base: { // 弹框样式配置
// price
priceBgColor: "#fff", priceFontColor: "#1FA0E8", priceFontSize: "32px",
// 框体样式
width: '500px', // 框体宽度
height: '800px', // 框体高度
fontSize: '14px', // 整体字体大小
backgroundColor: '#fff', // 整体弹框背景色
borderRadius: '0px',
borderWidth: '1px',
borderColor: 'transparent', // 弹边框颜色 transparent 为透明,支持 rgba 和 hex maskBackgroundColor: 'rgba(0,0,0,.5)',
// 全屏展示时背景颜色和透明度 rgba 格式
maskzIndex: '100',
// 全屏展示时的层级(如果有 div 没有被背景覆盖,可调高次数值)
// loading 动画
loadingColor: '#20a0e8', // 加载动画颜色
loadingBackgroundColor: 'rgba(255,255,255,0.8)', // 动画背景色,rgba 格式 //bill 定制 css
billPadding: '0px',
billColor: 'rgba(0, 0, 0, 0.4)',
billFontSize: '14px',
//located 定制 css
locatedPadding: '20px 0',
locatedColor: "#999999",
locatedBgColor: '#eee',
locatedFontSize: '14px',
// 弹框头部
showHeader: true, // 是否展示头部
showHeaderLabel: true, // 是否展示头部字体 headerLabelFont: "Credit Card", // 自定义头部字体内容 headerColor: '#333333', // 头部字体颜色
headerSize: '16px', // 头部字体大小 headerBackgroundColor: '#fff', // 头部背景色 headerPadding: '20px',
headerfontWeight: '400', // 粗体或细体
// 按钮 button
btnSize: '100%', // 按钮宽度百分比或者 px btnColor: '#fff', // 按钮字体颜色 btnFontSize: '14px', // 按钮字体大小
btnPaddingX: '20px', // 字体与宽体左右间距 btnPaddingY: "10px", // 顶部和底部间距 btnBackgroundColor: '#1fa0e8', // 按钮背景色 btnBorderRadius: '4px', // 按钮圆⻆ btnBorderColor: '#1fa0e8',
// 可单独配置 btn 边框颜色覆盖 btnBorder 中设置的颜色
btnMarginTop: '0px', // button 离顶部距离 }
})
// 请求 checkout 接口获取 token var sdkConfig = {
//token: token // token 为请求后台获取到的支付凭证 }
// 拿到 token 后传入
client.createPayment(sdkConfig) )
})
// manul 模式
// 开启 manul 模式 意味着 收银台不会生成支付按钮,支付需要通过实例中的
actionPayment 方 法进行手动支付 // 使用方法
function pay() { client.actionPayment()
}
//在点击事件中执行 pay()方法即可支付// 需要将此方法绑定到你自定义的按钮上去 用户点击
按钮后触发方法 //FAQ
//使用时请查看 ppPay 是否是最新版本
//可在浏览器 console 中输入 ppPay.version 查看 (最新版本为:1.2.0) </script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# PHP调用示例代码
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
#ufo-container{
width: 545px;
min-height: 100vh;
margin: 0 auto;
}
</style>
</head>
<body>
<div id="ufo-container"
style="z-index: 999;">
</body>
</html>
<?php
class demo
{
protected string $accId = '2018092714313010016291';
protected string $salt = 'F78BC96A55548B2319EE68E0';
protected string $signType = 'MD5';
protected string $gateway = 'https://sandbox-acquirer-payment.pingpongx.com';
/**
* @throws Exception
*/
public function checkout()
{
$data = [
"accId" => $this->accId,
"amount" => "2",
"currency" => "USD",
"merchantTransactionId" => rand() . date("YmdHiss"),
"paymentType" => "SALE",
"shopperResultUrl" => "www.baidu.com",
"shopperCancelUrl" => "www.baidu.com",
"notificationUrl" => "www.baidu.com",
"signType" => $this->signType,
"sign" => "",
"language" => "en",
"threeDSecure" => "N",
"riskInfo" => [
"customer" => [
"customerId" => null,
"firstName" => "Patricia",
"lastName" => "Stryker",
"email" => "123@test.com",
"phone" => "18989541256",
"registerTime" => date("y-m-d"),
"registerIp" => $_SERVER['REMOTE_ADDR'],
"registerTerminal" => 'PC',
"registerRange" => '1',
"orderTime" => date("y-m-d H"),
"orderIp" => $_SERVER['REMOTE_ADDR'],
"payIp" => $_SERVER['REMOTE_ADDR'],
]
],
"goods" => [
"name" => "测试商品",
"description" => "这是测试商品",
"sku" => "白色12GB测试商品",
"averageUnitPrice" => "2.45",
"number" => "2",
"imgUrl" => "www.baidu.com/img/test.img",
"virtualProduct" => "N",
],
"shipping" => [
"firstName" => "Patricia",
"lastName" => "Stryker",
"phone" => "18989841256",
"email" => "123@test.com",
"street" => "3630 Wolf Pen Road",
"postcode" => "94612",
"city" => "Oakland",
"state" => "CA",
"country" => "US",
"lastModifierStreetTime" => date(""),
"lastModifierPhoneTime" => date(""),
],
"billing" => [
"firstName" => "Patricia",
"lastName" => "Stryker",
"street" => "3630 Wolf Pen Road",
"postcode" => "94612",
"city" => "Oakland",
"state" => "CA",
"country" => "US",
],
"eCommerce" => [
'freeShipping' => "",
'shippingMethod' => "",
]
];
$signData = self::getRequestSignatureScopeWhenRequest($data);
// var_dump($signData);
$sign = $this->getSign($signData, $this->salt);
$data['sign'] = $sign;
$requestUrl = $this->gateway . "/v2/checkout/";
$response = $this->request($requestUrl, $data);
// var_dump($response);
$this->getCheckoutBar($response['token']);
}
/**
* 返回签名参数范围
* @param array $params
* @return string[]
*/
public static function getRequestSignatureScopeWhenRequest(array $params): array
{
$scope = [
'clientId',
'accId',
'transactionId',
'merchantTransactionId',
'amount',
'currency',
'notificationUrl',
'shopperResultUrl',
'signType',
];
$inScope = [];
foreach ($params as $key => $param) {
if (in_array($key, $scope, true)) {
$inScope[$key] = $param;
}
}
return $inScope;
}
/**
* @param array $data
* @param string $salt
* @param int $isLower
* @return string
* @throws Exception
*/
public function getSign(array $data, string $salt, int $isLower = 0): string
{
ksort($data);
$signStr = $salt;
foreach ($data as $key => $value) {
$signStr .= "{$key}=$value&";
}
$signStr = rtrim($signStr, '&');
// var_dump("signStr:",$signStr);
$sign = md5($signStr);
if ($isLower === 0) {
return $sign;
}
if ($isLower === 1) {
return strtolower($sign);
}
if ($isLower === 2) {
return strtoupper($sign);
}
throw new Exception('param isLower error');
}
/**
* @param string $url
* @param array $data
* @return array
* @throws Exception
*/
public function request(string $url, array $data): array
{
$param = json_encode($data, JSON_UNESCAPED_UNICODE);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.52 Safari/537.17');
curl_setopt($ch, CURLOPT_POSTFIELDS, $param);//send values
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 50); //timeout in seconds
//curl_setopt($ch,CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
// Set HTTP Header for POST request
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen($param)]);
$responseStr = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpCode >= 299) {
throw new Exception("request filed httpCode {$httpCode}");
}
if (empty($responseStr)) {
throw new Exception('empty response');
}
$responseArr = json_decode($responseStr, true);
if (empty($responseArr)) {
throw new Exception('response is not json');
}
return $responseArr;
}
public function rand()
{
$strs = "QWERTYUIOPASDFGHJKLZXCVBNM";
$name = substr(str_shuffle($strs), mt_rand(0, strlen($strs) - 11), 4);
return $name;
}
public function getCheckoutBar(string $token)
{
$js = '<div id="ufo" style="width: 100%;height: 100%"></div><div><script src="https://pay-cdn.pingpongx.com/production/static/sdk/1.2.0/ppPay.min.js"> </script>';
$js .= "<script>
window.onload = function() {
let client = new ppPay({
lang: 'en',
root: '#ufo',
manul: false,
located: true,
bill: true,
mode: 'sandbox', // 本地测试模式 sandbox test build (沙箱,测试,线上) (必填参数)、
base: { // 弹框样式配置 (可选参数)
width: '100%', // 弹框宽度
height: '100%', // 弹框⾼度 (当⾼度不指定时 loading可能会失效)
formColor: '#606266', // 表单输入框文字颜色
formInputSize: '14px', // 表单输入框文字大小
formInputHeight: '36px', // 表单高度
formInputLineHeight: '34px', // 表单line height
formInputMarginTop: '0px', // input 与顶部距离
formBorderColor: '#E4E7ED', // 表单边框颜色[color,none,0]
formBorderRadius: '4px', // 表单框体圆角(50%为半圆)
formBackgroundColor: '#fbfbfb', // 表单框体颜色
formInputBackgroundColor: '#fff', // 表单input框体颜色
showHeader: true,
showHeaderLabel: true
}
})
// request PingPong api for token
let token = '" . $token . "';
let sdkConfig = {
token: token
}
// 拿到token后传⼊
client.createPayment(sdkConfig)
setPPPayPropWin();
function setPPPayPropWin(){
let winWidth = window.innerWidth|| document.documentElement.clientWidth|| document.body.clientWidth;
let winHeight = window.innerHeight|| document.documentElement.clientHeight|| document.body.clientHeight;
let ufoContainer = document.getElementById('ufo-container');
ufoContainer.style.height = (winHeight/2)+'px';
ufoContainer.style.width = (winWidth/2)+'px';
if (winWidth>= 500){
let clientW = Math.floor(winWidth/3);
clientW = clientW>=500?clientW:500;
ufoContainer.style.width = clientW+'px';
}else {
ufoContainer.style.width = winWidth+'px';
}
}
}
</script>";
$js .= '</div>';
$js .= '</div>';
echo $js;
}
}
(new demo())->checkout();
?>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
如上所示,sdk初始化条件为:
1. 必须从v2/checkout接口中获取到token 传入其中。
2. 必须有一个根元素(ufo-container)来确定收银台渲染的位置。
1. 参数mode必须正确填写,开发测试sandbox,上线之后使用build,应根据AccId自动判断,决不允许上线使用sandbox,否则交易将不会扣款,造成资损
2. lang 应该根据浏览器语言首选项自适应,获取HTTP header的Accept-Language来做映射
3. 收银台应该正常展示,不能有遮挡
4. located,bill默认强制展示,否则影响交易
5. 收银台不支持无痕模式
# 完成付款
正确如上设置之后,应正常渲染出收银台,正确输入卡号,过期时间和CVV信息,即可完成交易。
沙箱模式下的收银台如下所示

# 3DS
收银台模式下,3DS 流程由 pingpong checkout 进行了内部封装,无需商户额外接入。
# 预授权
# 什么是预授权
- 商户在持卡人消费前先冻结持卡人creditcard的余额或者额度。
- 持卡人消费结束后,商户再正式扣掉这部分资金,常用于酒店住宿、出租等行业。
- 线上交易正常预授权资金冻结期限为7天,部分发卡行是30天。
- PingPongPay默认不会自动解冻持卡人资金。
- 预授权交易7天或者30天后,如果商户在这期间没有任何操作,发卡行会自动解冻持卡人冻结的资金。
- 商户发起Auth之后必须发起在恰当时间(通常为7天)发起CAPTURE,否则交易将被超时取消
# 如何发起预授权交易
v2/checkout 接口中
交易类型:
- SALE-直接付款
- AUTH-预授权
填入paymentType= AUTH 即视为AUTH 业务
# CAPTURE
# 什么是CAPTURE
对已经预授权成功的交易,在资金冻结期限内使用预授权完成进行请款操作。
# 业务前提
针对“预授权”交易可以发起“预授权完成”操作。
# 业务限制
- 当前“预授权”交易未被判定为“预授权取消”。
- 预授权完成的金额需小于等于关联的 CAPTURE 交易。
# 如何发起
收银台模式和端到端模式都请求二次交易接口,填入paymentType=CAPTURE发起退款
# 接口地址
https://{host}/v2/payment/{transactionId}
参数详见 退款预授权
# VOID
# 什么是VOID
对已经预授权的交易,通知发卡行进行预授权撤销,预授权撤销成功后发卡行会解冻持卡人冻结的资金。
# 业务前提
针对“预授权”交易可以发起“预授权撤销”操作。
# 业务限制
- 当前“预授权”交易未被判定为“预授权完成”。
- 预授权撤销只能全额撤销。
# 如何发起
收银台模式和端到端模式都请求二次交易接口,填入paymentType=VOID发起退款
# 接口地址
https://{host}/v2/payment/{transactionId}
参数详见退款预授权