first commit
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Stripe;
|
||||
|
||||
use Exception;
|
||||
|
||||
class StripeClient
|
||||
{
|
||||
const VERSION = '10.17.0';
|
||||
const DEFAULT_API_BASE = 'https://api.stripe.com';
|
||||
|
||||
private $api_key;
|
||||
|
||||
public function __construct($api_key)
|
||||
{
|
||||
$this->api_key = $api_key;
|
||||
}
|
||||
|
||||
public function request($method, $path, $params = null)
|
||||
{
|
||||
$url = self::DEFAULT_API_BASE . $path;
|
||||
[$httpCode, $response] = $this->curl($method, $url, $params);
|
||||
$arr = json_decode($response, true);
|
||||
if($httpCode >= 200 && $httpCode < 300 && $arr) {
|
||||
return $arr;
|
||||
}elseif(isset($arr['error'])){
|
||||
throw new Exception($arr['error']['message']);
|
||||
}else{
|
||||
throw new Exception('返回数据解析失败');
|
||||
}
|
||||
}
|
||||
|
||||
private function getHttpHeaders()
|
||||
{
|
||||
$uaString = 'Stripe/v1 PhpBindings/' . self::VERSION;
|
||||
|
||||
$curlVersion = curl_version();
|
||||
$clientInfo = [
|
||||
'httplib' => 'curl ' . $curlVersion['version'],
|
||||
'ssllib' => $curlVersion['ssl_version'],
|
||||
];
|
||||
|
||||
$langVersion = PHP_VERSION;
|
||||
$uname = function_exists('php_uname') ? php_uname() : '(disabled)';
|
||||
|
||||
$ua = [
|
||||
'bindings_version' => self::VERSION,
|
||||
'lang' => 'php',
|
||||
'lang_version' => $langVersion,
|
||||
'publisher' => 'stripe',
|
||||
'uname' => $uname,
|
||||
];
|
||||
if ($clientInfo) {
|
||||
$ua = array_merge($clientInfo, $ua);
|
||||
}
|
||||
|
||||
return [
|
||||
'X-Stripe-Client-User-Agent: ' . json_encode($ua),
|
||||
'User-Agent: ' . $uaString,
|
||||
'Authorization: Bearer ' . $this->api_key,
|
||||
];
|
||||
}
|
||||
|
||||
private function curl($method, $url, $params = null)
|
||||
{
|
||||
$httpheaders = $this->getHttpHeaders();
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_FAILONERROR, false);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheaders);
|
||||
|
||||
if ($method == 'post') {
|
||||
if(is_array($params)) $params = http_build_query($params);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
|
||||
} elseif($method == 'delete') {
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
|
||||
if($params && count($params) > 0) {
|
||||
$url .= '?' . http_build_query($params);
|
||||
}
|
||||
} elseif($method == 'get') {
|
||||
if($params && count($params) > 0) {
|
||||
$url .= '?' . http_build_query($params);
|
||||
}
|
||||
}
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if (curl_errno($ch)) {
|
||||
throw new Exception(curl_error($ch));
|
||||
}
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
curl_close($ch);
|
||||
return [$httpCode, $response];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace Stripe;
|
||||
|
||||
use Exception;
|
||||
|
||||
class Webhook
|
||||
{
|
||||
const DEFAULT_TOLERANCE = 300;
|
||||
|
||||
const EXPECTED_SCHEME = 'v1';
|
||||
|
||||
public static function constructEvent($payload, $sigHeader, $secret, $tolerance = self::DEFAULT_TOLERANCE)
|
||||
{
|
||||
if (!self::verifyHeader($payload, $sigHeader, $secret, $tolerance)) {
|
||||
throw new Exception('Invalid signature');
|
||||
}
|
||||
|
||||
$data = json_decode($payload, true);
|
||||
if (!$data) {
|
||||
throw new Exception('Invalid payload');
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public static function verifyHeader($payload, $header, $secret, $tolerance = null)
|
||||
{
|
||||
// Extract timestamp and signatures from header
|
||||
$timestamp = self::getTimestamp($header);
|
||||
$signatures = self::getSignatures($header, self::EXPECTED_SCHEME);
|
||||
if (!$timestamp || empty($signatures)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if expected signature is found in list of signatures from
|
||||
// header
|
||||
$signedPayload = "{$timestamp}.{$payload}";
|
||||
$expectedSignature = self::computeSignature($signedPayload, $secret);
|
||||
$signatureFound = false;
|
||||
foreach ($signatures as $signature) {
|
||||
if ($expectedSignature === $signature) {
|
||||
$signatureFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$signatureFound) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if timestamp is within tolerance
|
||||
if (($tolerance > 0) && (abs(time() - $timestamp) > $tolerance)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function getTimestamp($header)
|
||||
{
|
||||
$items = explode(',', $header);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$itemParts = explode('=', $item, 2);
|
||||
if ('t' === $itemParts[0]) {
|
||||
if (!is_numeric($itemParts[1])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (int) ($itemParts[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static function getSignatures($header, $scheme)
|
||||
{
|
||||
$signatures = [];
|
||||
$items = explode(',', $header);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$itemParts = explode('=', $item, 2);
|
||||
if (trim($itemParts[0]) === $scheme) {
|
||||
$signatures[] = $itemParts[1];
|
||||
}
|
||||
}
|
||||
|
||||
return $signatures;
|
||||
}
|
||||
|
||||
private static function computeSignature($payload, $secret)
|
||||
{
|
||||
return hash_hmac('sha256', $payload, $secret);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
|
||||
class stripe_plugin
|
||||
{
|
||||
static public $info = [
|
||||
'name' => 'stripe', //支付插件英文名称,需和目录名称一致,不能有重复
|
||||
'showname' => 'Stripe', //支付插件显示名称
|
||||
'author' => 'Stripe', //支付插件作者
|
||||
'link' => 'https://stripe.com/', //支付插件作者链接
|
||||
'types' => ['alipay','wxpay','bank','paypal'], //支付插件支持的支付方式,可选的有alipay,qqpay,wxpay,bank
|
||||
'inputs' => [ //支付插件要求传入的参数以及参数显示名称,可选的有appid,appkey,appsecret,appurl,appmchid
|
||||
'appid' => [
|
||||
'name' => 'API密钥',
|
||||
'type' => 'textarea',
|
||||
'note' => 'sk_live_开头的密钥',
|
||||
],
|
||||
'appkey' => [
|
||||
'name' => 'Webhook密钥',
|
||||
'type' => 'textarea',
|
||||
'note' => 'whsec_开头的密钥',
|
||||
],
|
||||
'appswitch' => [
|
||||
'name' => '支付模式',
|
||||
'type' => 'select',
|
||||
'options' => [0=>'跳转收银台',1=>'直接支付(仅限支付宝/微信)'],
|
||||
],
|
||||
'currency_code' => [
|
||||
'name' => '结算货币',
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
'CNY' => '人民币 (CNY)',
|
||||
'HKD' => '港币 (HKD)',
|
||||
'EUR' => '欧元 (EUR)',
|
||||
'USD' => '美元 (USD)',
|
||||
'AUD' => '澳元 (AUD)',
|
||||
'CAD' => '加拿大元 (CAD)',
|
||||
'GBP' => '英镑 (GBP)',
|
||||
'BRL' => '巴西雷亚尔 (BRL)',
|
||||
'CZK' => '克朗 (CZK)',
|
||||
'DKK' => '丹麦克朗(DKK)',
|
||||
'HUF' => '匈牙利福林 (HUF)',
|
||||
'INR' => '印度卢比 (INR)',
|
||||
'ILS' => '以色列新谢克尔 (ILS)',
|
||||
'JPY' => '日元 (JPY)',
|
||||
'MYR' => '马来西亚林吉特 (MYR)',
|
||||
'MXN' => '墨西哥比索 (MXN)',
|
||||
'TWD' => '新台币 (TWD)',
|
||||
'NZD' => '新西兰元 (NZD)',
|
||||
'NOK' => '挪威克朗 (NOK)',
|
||||
'PHP' => '菲律宾比索 (PHP)',
|
||||
'PLN' => '波兰兹罗提 (PLN)',
|
||||
'RUB' => '俄罗斯卢布 (RUB)',
|
||||
'SGD' => '新加坡元 (SGD)',
|
||||
'SEK' => '瑞典克朗 (SEK)',
|
||||
'CHF' => '瑞士法郎 (CHF)',
|
||||
'THB' => '泰铢 (THB)',
|
||||
],
|
||||
],
|
||||
'currency_rate' => [
|
||||
'name' => '货币汇率',
|
||||
'type' => 'input',
|
||||
'note' => '例如1元人民币兑换0.137美元(USD),则此处填0.137',
|
||||
],
|
||||
],
|
||||
'select' => null,
|
||||
'note' => '需设置WebHook地址:[siteurl]pay/webhook/[channel]/ <br/>侦听的事件,直接支付用: payment_intent.succeeded,跳转收银台用:checkout.session.completed、checkout.session.async_payment_succeeded', //支付密钥填写说明
|
||||
'bindwxmp' => false, //是否支持绑定微信公众号
|
||||
'bindwxa' => false, //是否支持绑定微信小程序
|
||||
];
|
||||
|
||||
static public function submit(){
|
||||
global $siteurl, $channel, $order, $ordername, $sitename, $conf;
|
||||
|
||||
if($channel['appswitch'] == 1 && $order['typename']=='alipay'){
|
||||
return ['type'=>'jump','url'=>'/pay/alipay/'.TRADE_NO.'/'];
|
||||
}elseif($channel['appswitch'] == 1 && $order['typename']=='wxpay'){
|
||||
return ['type'=>'jump','url'=>'/pay/wxpay/'.TRADE_NO.'/'];
|
||||
}
|
||||
|
||||
require_once PAY_ROOT.'inc/StripeClient.php';
|
||||
|
||||
if($order['typename']=='alipay'){
|
||||
$payment_method='alipay';
|
||||
}elseif($order['typename']=='wxpay'){
|
||||
$payment_method='wechat_pay';
|
||||
}elseif($order['typename']=='paypal'){
|
||||
$payment_method='paypal';
|
||||
}else{
|
||||
$payment_method='';
|
||||
}
|
||||
|
||||
if(!$channel['currency_rate']) $channel['currency_rate'] = 1;
|
||||
if(!$channel['currency_code']) $channel['currency_code'] = 'cny';
|
||||
$amount = intval(round($order['realmoney'] * $channel['currency_rate'] * 100));
|
||||
|
||||
try{
|
||||
$stripe = new Stripe\StripeClient($channel['appid']);
|
||||
$data = [
|
||||
'success_url' => $siteurl.'pay/return/'.TRADE_NO.'/',
|
||||
'cancel_url' => $siteurl.'pay/error/'.TRADE_NO.'/',
|
||||
'client_reference_id' => TRADE_NO,
|
||||
'line_items' => [[
|
||||
'price_data' => [
|
||||
'currency' => strtolower($channel['currency_code']),
|
||||
'product_data' => [
|
||||
'name' => $ordername
|
||||
],
|
||||
'unit_amount' => $amount
|
||||
],
|
||||
'quantity' => 1
|
||||
]],
|
||||
'mode' => 'payment'
|
||||
];
|
||||
if($payment_method)$data['payment_method_types'] = [$payment_method];
|
||||
if($payment_method == 'wechat_pay'){
|
||||
$data['payment_method_options']['wechat_pay']['client'] = 'web';
|
||||
}
|
||||
$result = $stripe->request('post', '/v1/checkout/sessions', $data);
|
||||
$jump_url = $result['url'];
|
||||
return ['type'=>'jump','url'=>$jump_url];
|
||||
}catch(Exception $e){
|
||||
return ['type'=>'error','msg'=>$e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
static private function paymentintent($payment_method){
|
||||
global $siteurl, $channel, $order, $ordername, $conf;
|
||||
|
||||
require_once PAY_ROOT.'inc/StripeClient.php';
|
||||
|
||||
$stripe = new Stripe\StripeClient($channel['appid']);
|
||||
|
||||
$data = ['type' => $payment_method];
|
||||
try{
|
||||
$result = $stripe->request('post', '/v1/payment_methods', $data);
|
||||
$payment_method_id = $result['id'];
|
||||
}catch(Exception $e){
|
||||
throw new Exception('创建支付方式失败:'.$e->getMessage());
|
||||
}
|
||||
|
||||
if(!$channel['currency_rate']) $channel['currency_rate'] = 1;
|
||||
if(!$channel['currency_code']) $channel['currency_code'] = 'cny';
|
||||
$amount = intval(round($order['realmoney'] * $channel['currency_rate'] * 100));
|
||||
|
||||
$data = [
|
||||
'amount' => $amount,
|
||||
'currency' => strtolower($channel['currency_code']),
|
||||
'confirm' => 'true',
|
||||
'payment_method' => $payment_method_id,
|
||||
'payment_method_types' => [$payment_method],
|
||||
'description' => $ordername,
|
||||
'metadata' => [
|
||||
'order_id' => TRADE_NO,
|
||||
],
|
||||
'return_url' => $siteurl.'pay/return/'.TRADE_NO.'/',
|
||||
];
|
||||
if($payment_method == 'wechat_pay'){
|
||||
$data['payment_method_options']['wechat_pay']['client'] = 'web';
|
||||
}
|
||||
$result = $stripe->request('post', '/v1/payment_intents', $data);
|
||||
if($payment_method == 'alipay'){
|
||||
$url = $result['next_action']['alipay_handle_redirect']['url'];
|
||||
}elseif($payment_method == 'wechat_pay'){
|
||||
$url = $result['next_action']['wechat_pay_display_qr_code']['data'];
|
||||
}else{
|
||||
$url = $result['next_action']['redirect_to_url']['url'];
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
static public function alipay(){
|
||||
try{
|
||||
$url = self::paymentintent('alipay');
|
||||
}catch(Exception $ex){
|
||||
return ['type'=>'error','msg'=>'支付宝支付下单失败!'.$ex->getMessage()];
|
||||
}
|
||||
|
||||
return ['type'=>'jump','url'=>$url];
|
||||
}
|
||||
|
||||
static public function wxpay(){
|
||||
try{
|
||||
$url = self::paymentintent('wechat_pay');
|
||||
}catch(Exception $ex){
|
||||
return ['type'=>'error','msg'=>'微信支付下单失败!'.$ex->getMessage()];
|
||||
}
|
||||
|
||||
if(checkwechat()){
|
||||
return ['type'=>'jump','url'=>$url];
|
||||
} elseif (checkmobile()) {
|
||||
return ['type'=>'qrcode','page'=>'wxpay_wap','url'=>$url];
|
||||
} else {
|
||||
return ['type'=>'qrcode','page'=>'wxpay_qrcode','url'=>$url];
|
||||
}
|
||||
}
|
||||
|
||||
//异步回调
|
||||
static public function webhook(){
|
||||
global $channel, $order, $DB;
|
||||
|
||||
require_once PAY_ROOT.'inc/Webhook.php';
|
||||
|
||||
$payload = file_get_contents('php://input');
|
||||
$data = json_decode($payload, true);
|
||||
if(!$data){
|
||||
http_response_code(400);
|
||||
return ['type'=>'html','data'=>'no data'];
|
||||
}
|
||||
|
||||
if(isset($data['data']['object']['client_reference_id'])){
|
||||
$out_trade_no = daddslashes($data['data']['object']['client_reference_id']);
|
||||
}elseif(isset($data['data']['object']['metadata']['order_id'])){
|
||||
$out_trade_no = daddslashes($data['data']['object']['metadata']['order_id']);
|
||||
}else{
|
||||
http_response_code(400);
|
||||
return ['type'=>'html','data'=>'no order_id'];
|
||||
}
|
||||
$order = $DB->getRow("SELECT * FROM pre_order WHERE trade_no='$out_trade_no' limit 1");
|
||||
if(!$order){
|
||||
http_response_code(400);
|
||||
return ['type'=>'html','data'=>'no order'];
|
||||
}
|
||||
|
||||
$channel = \lib\Channel::get($order['channel']);
|
||||
if(!$channel){
|
||||
http_response_code(400);
|
||||
return ['type'=>'html','data'=>'no channel'];
|
||||
}
|
||||
|
||||
$endpoint_secret = $channel['appkey'];
|
||||
$sig_header = $_SERVER['HTTP_STRIPE_SIGNATURE'];
|
||||
$event = null;
|
||||
|
||||
try {
|
||||
$event = \Stripe\Webhook::constructEvent($payload, $sig_header, $endpoint_secret);
|
||||
} catch(Exception $e) {
|
||||
http_response_code(400);
|
||||
return ['type'=>'html','data'=>$e->getMessage()];
|
||||
}
|
||||
|
||||
switch($event['type']){
|
||||
case 'checkout.session.completed':
|
||||
$session = $event['data']['object'];
|
||||
if ($session['payment_status'] == 'paid') {
|
||||
processNotify($order, $session['payment_intent']);
|
||||
}
|
||||
break;
|
||||
case 'checkout.session.async_payment_succeeded':
|
||||
$session = $event['data']['object'];
|
||||
processNotify($order, $session['payment_intent']);
|
||||
break;
|
||||
case 'payment_intent.succeeded':
|
||||
$session = $event['data']['object'];
|
||||
processNotify($order, $session['id']);
|
||||
break;
|
||||
}
|
||||
return ['type'=>'html','data'=>'success'];
|
||||
}
|
||||
|
||||
//同步回调
|
||||
static public function return(){
|
||||
return ['type'=>'page','page'=>'return'];
|
||||
}
|
||||
|
||||
//同步回调
|
||||
static public function error(){
|
||||
return ['type'=>'page','page'=>'error'];
|
||||
}
|
||||
|
||||
//退款
|
||||
static public function refund($order){
|
||||
global $channel;
|
||||
if(empty($order))exit();
|
||||
|
||||
require_once PAY_ROOT.'inc/StripeClient.php';
|
||||
|
||||
if(!$channel['currency_rate']) $channel['currency_rate'] = 1;
|
||||
$amount = intval(round($order['refundmoney'] * $channel['currency_rate'] * 100));
|
||||
|
||||
try{
|
||||
$stripe = new Stripe\StripeClient($channel['appid']);
|
||||
$data = [
|
||||
'payment_intent' => $order['api_trade_no'],
|
||||
'amount' => $amount,
|
||||
];
|
||||
$result = $stripe->request('post', '/v1/refunds', $data);
|
||||
return ['code'=>0, 'trade_no'=>$result['payment_intent'], 'refund_fee'=>$result['amount']/100];
|
||||
}catch(Exception $ex){
|
||||
return ['code'=>-1, 'msg'=>$ex->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user