不知不覺微信支付也更新了,介面版本也升級到了V3,
跟著微信的升級,將個人使用微信支付類也進行了升級,
V3微信支付文件:pay.weixin.qq.com/wiki/doc/apiv3/i...
使用方法還和之前的一樣(V2微信支付),直接傳遞引數就可使用:
新版新增了composer
安裝,便於整合框架使用(Github地址):
composer require fengkui/pay
首先把配置檔案填寫完整(細心不要填錯,否則會導致簽名錯誤):
# 微信支付配置
$wechatConfig = [
'xcxid' => '', // 小程式 appid
'appid' => '', // 微信支付 appid
'mchid' => '', // 微信支付 mch_id 商戶收款賬號
'key' => '', // 微信支付 apiV3key(儘量包含大小寫字母,否則驗籤不透過)
'appsecret' => '', // 公眾帳號 secert (公眾號支付獲取 code 和 openid 使用)
'notify_url' => '', // 接收支付狀態的連線 改成自己的回撥地址
'redirect_url' => '', // 公眾號支付,調起支付頁面
'serial_no' => '', // 證照序列號
'cert_client' => './cert/apiclient_cert.pem', // 證照(退款,紅包時使用)
'cert_key' => './cert/apiclient_key.pem', // 商戶私鑰(Api安全中下載)
'public_key' => './cert/public_key.pem', // 平臺公鑰(調動證照列表,自動生成,注意目錄讀寫許可權)
];
支付類封裝相關方法:
method | 描述 |
---|---|
js | JSAPI下單 |
app | APP支付 |
h5 | H5支付 |
scan | Navicat支付 |
xcx | 小程式支付 |
query | 查詢訂單 |
close | 關閉訂單 |
refund | 申請退款 |
notify | 支付結果通知 |
使用方法(這裡已小程式支付為示例):
<?php
require_once('./vendor/autoload.php');
$config = []; // 支付配置
$order = [
'order_sn' => time(), // 訂單編號
'total_amount' => 1, // 訂單金額(分)
'body' => '測試商品', // 商品名稱
'openid' => '', // 使用者openid
// 'type' => 'Wap',
];
$wechat = new fengkui\Pay\Wechat($config);
$re = $wechat->xcx($order);
die(json_encode($re)); // JSON化直接返回小程式客戶端
如下程式碼是封裝好的完整支付類檔案(Wechat.php),
可以根據自己需求隨意修改,詳細的使用方法後期會有文件:
<?php
/**
* @Author: [FENG] <1161634940@qq.com>
* @Date: 2019-09-06 09:50:30
* @Last Modified by: [FENG] <1161634940@qq.com>
* @Last Modified time: 2021-07-12 18:24:18
*/
namespace fengkui\Pay;
use Exception;
use RuntimeException;
use fengkui\Supports\Http;
/**
* Wechat 微信支付
* 新版(V3)介面(更新中)
*/
class Wechat
{
const AUTH_TAG_LENGTH_BYTE = 16;
// 新版相關介面
// GET 獲取平臺證照列表
private static $certificatesUrl = 'https://api.mch.weixin.qq.com/v3/certificates';
// 統一下訂單管理
private static $transactionsUrl = 'https://api.mch.weixin.qq.com/v3/pay/transactions/';
// 申請退款
private static $refundUrl = 'https://api.mch.weixin.qq.com/v3/refund/domestic/refunds';
// 靜默授權,獲取code
private static $authorizeUrl = 'https://open.weixin.qq.com/connect/oauth2/authorize';
// 透過code獲取access_token以及openid
private static $accessTokenUrl = 'https://api.weixin.qq.com/sns/oauth2/access_token';
// 支付完整配置
private static $config = array(
'xcxid' => '', // 小程式appid
'appid' => '', // 微信支付appid
'mchid' => '', // 微信支付 mch_id 商戶收款賬號
'key' => '', // 微信支付 apiV3key(儘量包含大小寫字母,否則驗籤不透過)
'appsecret' => '', // 公眾帳號 secert (公眾號支付獲取code 和 openid使用)
'notify_url' => '', // 接收支付狀態的連線 改成自己的回撥地址
'redirect_url' => '', // 公眾號支付,調起支付頁面
'serial_no' => '', // 證照序列號
'cert_client' => './cert/apiclient_cert.pem', // 證照(退款,紅包時使用)
'cert_key' => './cert/apiclient_key.pem', // 商戶私鑰(Api安全中下載)
'public_key' => './cert/public_key.pem', // 平臺公鑰(調動證照列表,自動生成,注意目錄讀寫許可權)
);
/**
* [__construct 建構函式]
* @param [type] $config [傳遞微信支付相關配置]
*/
public function __construct($config=NULL, $referer=NULL){
$config && self::$config = array_merge(self::$config, $config);
}
/**
* [unifiedOrder 統一下單]
* @param [type] $order [訂單資訊(必須包含支付所需要的引數)]
* @param boolean $type [區分是否是小程式,是則傳 true]
* @return [type] [description]
* $order = array(
* 'body' => '', // 產品描述
* 'order_sn' => '', // 訂單編號
* 'total_amount' => '', // 訂單金額(分)
* );
*/
public static function unifiedOrder($order, $type=false)
{
$config = array_filter(self::$config);
// 獲取配置項
$params = array(
'appid' => $type ? $config['xcxid'] : $config['appid'], // 由微信生成的應用ID
'mchid' => $config['mchid'], // 直連商戶的商戶號
'description' => $order['body'], // 商品描述
'out_trade_no' => (string)$order['order_sn'], // 商戶系統內部訂單號
'notify_url' => $config['notify_url'], // 通知URL必須為直接可訪問的URL
'amount' => ['total' => (int)$order['total_amount'], 'currency' => 'CNY'], // 訂單金額資訊
);
!empty($order['attach']) && $params['attach'] = $order['attach']; // 附加資料
if (!empty($order['time_expire'])) { // 訂單失效時間
preg_match('/[年\/-]/', $order['time_expire']) && $order['time_expire'] = strtotime($order['time_expire']);
$time = $order['time_expire'] > time() ? $order['time_expire'] : $order['time_expire'] + time();
$params['time_expire'] = date(DATE_ATOM, $time);
}
if (!in_array($order['type'], ['native'])) {
!empty($order['openid']) && $params['payer'] = ['openid' => $order['openid']];
$params['scene_info'] = ['payer_client_ip' => self::get_ip()];
}
if (in_array($order['type'], ['iOS', 'Android', 'Wap'])) {
$params['scene_info']['h5_info'] = ['type' => $order['type']];
$url = self::$transactionsUrl . 'h5'; // 拼接請求地址
} else {
$url = self::$transactionsUrl . strtolower($order['type']); // 拼接請求地址
}
$header = self::createAuthorization($url, $params, 'POST');
$response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE), $header);
$result = json_decode($response, true);
if (isset($result['code']) && isset($result['message'])) {
throw new \Exception("[" . $result['code'] . "] " . $result['message']);
}
return $result;
}
/**
* [query 查詢訂單]
* @param [type] $orderSn [訂單編號]
* @param boolean $type [微信支付訂單編號,是否是微信支付訂單號]
* @return [type] [description]
*/
public static function query($orderSn, $type = false)
{
$config = self::$config;
$url = self::$transactionsUrl . ($type ? 'id/' : 'out-trade-no/') . $orderSn . '?mchid=' . $config['mchid'];
$params = '';
$header = self::createAuthorization($url, $params, 'GET');
$response = Http::get($url, $params, $header);
$result = json_decode($response, true);
return $result;
}
/**
* [close 關閉訂單]
* @param [type] $orderSn [微信支付訂單編號]
* @return [type] [description]
*/
public static function close($orderSn)
{
$config = self::$config;
$url = self::$transactionsUrl . 'out-trade-no/' . $orderSn . '/close';
$params['mchid'] = $config['mchid'];
$header = self::createAuthorization($url, $params, 'POST');
$response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE), $header);
$result = json_decode($response, true);
return true;
}
/**
* [js 獲取jssdk需要用到的資料]
* @param [type] $order [訂單資訊陣列]
* @return [type] [description]
*/
public static function js($order=[]){
$config = self::$config;
if (!is_array($order) || count($order) < 3)
die("訂單陣列資訊缺失!");
if (count($order) == 4 && !empty($order['openid'])) {
$data = self::xcx($order, false, false); // 獲取支付相關資訊(獲取非小程式資訊)
return $data;
}
$code = !empty($order['code']) ? $order['code'] : ($_GET['code'] ?? '');
$redirectUri = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'] . rtrim($_SERVER['REQUEST_URI'], '/') . '/'; // 重定向地址
$params = ['appid' => $config['appid']];
// 如果沒有get引數沒有code;則重定向去獲取code;
if (empty($code)) {
$params['redirect_uri'] = $redirectUri; // 返回的url
$params['response_type'] = 'code';
$params['scope'] = 'snsapi_base';
$params['state'] = $order['order_sn']; // 獲取訂單號
$url = self::$authorizeUrl . '?'. http_build_query($params) .'#wechat_redirect';
} else {
$params['secret'] = $config['appsecret'];
$params['code'] = $code;
$params['grant_type'] = 'authorization_code';
$response = Http::get(self::$accessTokenUrl, $params); // 進行GET請求
$result = json_decode($response, true);
$order['openid'] = $result['openid']; // 獲取到的openid
$data = self::xcx($order, false, false); // 獲取支付相關資訊(獲取非小程式資訊)
if (!empty($order['code'])) {
return $data;
}
$url = $config['redirect_url'] ?? $redirectUri;
$url .= '?data=' . json_encode($data, JSON_UNESCAPED_UNICODE);
}
header('Location: '. $url);
die;
}
/**
* [app 獲取APP支付需要用到的資料]
* @param [type] $order [訂單資訊陣列]
* @return [type] [description]
*/
public static function app($order=[], $log=false)
{
if(empty($order['order_sn']) || empty($order['total_amount']) || empty($order['body'])){
die("訂單陣列資訊缺失!");
}
$order['type'] = 'app'; // 獲取訂單型別,使用者拼接請求地址
$result = self::unifiedOrder($order, true);
if (!empty($result['prepay_id'])) {
$data = array (
'appId' => self::$config['appid'], // 微信開放平臺稽核透過的移動應用appid
'timeStamp' => (string)time(),
'nonceStr' => self::get_rand_str(32, 0, 1), // 隨機32位字串
'prepayid' => $result['prepay_id'],
);
$data['paySign'] = self::makeSign($data);
$data['partnerid'] = $config['mchid'];
$data['package'] = 'Sign=WXPay';
return $data; // 資料小程式客戶端
} else {
return $log ? $result : false;
}
}
/**
* [h5 微信H5支付]
* @param [type] $order [訂單資訊陣列]
* @return [type] [description]
*/
public static function h5($order=[], $log=false)
{
if(empty($order['order_sn']) || empty($order['total_amount']) || empty($order['body']) || empty($order['type']) || !in_array(strtolower($order['type']), ['ios', 'android', 'wap'])){
die("訂單陣列資訊缺失!");
}
$result = self::unifiedOrder($order);
if (!empty($result['h5_url'])) {
return $result['h5_url']; // 返回連結讓使用者點選跳轉
} else {
return $log ? $result : false;
}
}
/**
* [xcx 獲取jssdk需要用到的資料]
* @param [type] $order [訂單資訊陣列]
* @param boolean $log [description]
* @param boolean $type [區分是否是小程式,預設 true]
* @return [type] [description]
*/
public static function xcx($order=[], $log=false, $type=true)
{
if(empty($order['order_sn']) || empty($order['total_amount']) || empty($order['body']) || empty($order['openid'])){
die("訂單陣列資訊缺失!");
}
$order['type'] = 'jsapi'; // 獲取訂單型別,使用者拼接請求地址
$config = self::$config;
$result = self::unifiedOrder($order, $type);
if (!empty($result['prepay_id'])) {
$data = array (
'appId' => $type ? $config['xcxid'] : $config['appid'], // 由微信生成的應用ID
'timeStamp' => (string)time(),
'nonceStr' => self::get_rand_str(32, 0, 1), // 隨機32位字串
'package' => 'prepay_id='.$result['prepay_id'],
);
$data['paySign'] = self::makeSign($data);
$data['signType'] = 'RSA';
return $data; // 資料小程式客戶端
} else {
return $log ? $result : false;
}
}
/**
* [scan 微信掃碼支付]
* @param [type] $order [訂單資訊陣列]
* @return [type] [description]
*/
public static function scan($order=[], $log=false)
{
if(empty($order['order_sn']) || empty($order['total_amount']) || empty($order['body'])){
die("訂單陣列資訊缺失!");
}
$order['type'] = 'native'; // Native支付
$result = self::unifiedOrder($order);
if (!empty($result['code_url'])) {
return urldecode($result['code_url']); // 返回連結讓使用者點選跳轉
} else {
return $log ? $result : false;
}
}
/**
* [notify 回撥驗證]
* @return [array] [返回陣列格式的notify資料]
*/
public static function notify($server = [], $response = [])
{
$config = self::$config;
$server = $server ?? $_SERVER;
$response = $response ?: file_get_contents('php://input', 'r');
if (empty($response) || empty($server['HTTP_WECHATPAY_SIGNATURE'])) {
return false;
}
$body = [
'timestamp' => $server['HTTP_WECHATPAY_TIMESTAMP'],
'nonce' => $server['HTTP_WECHATPAY_NONCE'],
'data' => $response,
];
// 驗證應答簽名
$verifySign = self::verifySign($body, trim($server['HTTP_WECHATPAY_SIGNATURE']), trim($server['HTTP_WECHATPAY_SERIAL']));
if (!$verifySign) {
die("簽名驗證失敗!");
}
$result = json_decode($response, true);
if (empty($result) || $result['event_type'] != 'TRANSACTION.SUCCESS' || $result['summary'] != '支付成功') {
return false;
}
// 加密資訊
$associatedData = $result['resource']['associated_data'];
$nonceStr = $result['resource']['nonce'];
$ciphertext = $result['resource']['ciphertext'];
$data = $result['resource']['ciphertext'] = self::decryptToString($associatedData, $nonceStr, $ciphertext);
return json_decode($data, true);
}
/**
* [refund 微信支付退款]
* @param [type] $order [訂單資訊]
* @param [type] $type [是否是小程式]
*/
public static function refund($order)
{
$config = self::$config;
if(empty($order['refund_sn']) || empty($order['refund_amount']) || (empty($order['order_sn']) && empty($order['transaction_id']))){
die("訂單陣列資訊缺失!");
}
$params = array(
'out_refund_no' => (string)$order['refund_sn'], // 商戶退款單號
'funds_account' => 'AVAILABLE', // 退款資金來源
'amount' => [
'refund' => $order['refund_amount'],
'currency' => 'CNY',
]
);
if (!empty($order['transaction_id'])) {
$params['transaction_id'] = $order['transaction_id'];
$orderDetail = self::query($order['transaction_id'], true);
} else {
$params['out_trade_no'] = $order['order_sn'];
$orderDetail = self::query($order['order_sn']);
}
$params['amount']['total'] = $orderDetail['amount']['total'];
!empty($order['reason']) && $params['reason'] = $order['reason'];
$url = self::$refundUrl;
$header = self::createAuthorization($url, $params, 'POST');
$response = Http::post($url, json_encode($params, JSON_UNESCAPED_UNICODE), $header);
$result = json_decode($response, true);
return $result;
}
/**
* [queryRefund 查詢退款]
* @param [type] $refundSn [退款單號]
* @return [type] [description]
*/
public static function queryRefund($refundSn, $type = false)
{
$url = self::$refundUrl . '/' . $refundSn;
$params = '';
$header = self::createAuthorization($url, $params, 'GET');
$response = Http::get($url, $params, $header);
$result = json_decode($response, true);
return $result;
}
/**
* [success 通知支付狀態]
*/
public static function success()
{
$str = ['code'=>'SUCCESS', 'message'=>'成功'];
die(json_encode($str, JSON_UNESCAPED_UNICODE));
}
/**
* [createAuthorization 獲取介面授權header頭資訊]
* @param [type] $url [請求地址]
* @param array $data [請求引數]
* @param string $method [請求方式]
* @return [type] [description]
*/
//生成v3 Authorization
protected static function createAuthorization($url, $data=[], $method='POST'){
$config = self::$config;
//商戶號
$mchid = $config['mchid'];
// 證照序列號
if (empty($config['serial_no'])) {
$certFile = @file_get_contents($config['cert_client']);
$certArr = openssl_x509_parse($publicStr);
$serial_no = $certArr['serialNumberHex'];
} else {
$serial_no = $config['serial_no'];
}
// 解析url地址
$url_parts = parse_url($url);
//生成簽名
$body = [
'method' => $method,
'url' => ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : "")),
'time' => time(), // 當前時間戳
'nonce' => self::get_rand_str(32, 0, 1), // 隨機32位字串
'data' => (strtolower($method) == 'post' ? json_encode($data, JSON_UNESCAPED_UNICODE) : $data), // POST請求時 需要 轉JSON字串
];
$sign = self::makeSign($body);
//Authorization 型別
$schema = 'WECHATPAY2-SHA256-RSA2048';
//生成token
$token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"', $mchid, $body['nonce'], $body['time'], $serial_no, $sign);
$header = [
'Content-Type:application/json',
'Accept:application/json',
'User-Agent:*/*',
'Authorization: '. $schema . ' ' . $token
];
return $header;
}
/**
* [makeSign 生成簽名]
* @param [type] $data [加密資料]
* @return [type] [description]
*/
public static function makeSign($data)
{
$config = self::$config;
if (!in_array('sha256WithRSAEncryption', \openssl_get_md_methods(true))) {
throw new \RuntimeException("當前PHP環境不支援SHA256withRSA");
}
// 拼接生成簽名所需的字串
$message = '';
foreach ($data as $value) {
$message .= $value . "\n";
}
// 商戶私鑰
$private_key = self::getPrivateKey($config['cert_key']);
// 生成簽名
openssl_sign($message, $sign, $private_key, 'sha256WithRSAEncryption');
$sign = base64_encode($sign);
return $sign;
}
/**
* [verifySign 驗證簽名]
* @param [type] $data [description]
* @param [type] $sign [description]
* @param [type] $serial [description]
* @return [type] [description]
*/
public static function verifySign($data, $sign, $serial)
{
$config = self::$config;
if (!in_array('sha256WithRSAEncryption', \openssl_get_md_methods(true))) {
throw new \RuntimeException("當前PHP環境不支援SHA256withRSA");
}
$sign = \base64_decode($sign);
// 拼接生成簽名所需的字串
$message = '';
foreach ($data as $value) {
$message .= $value . "\n";
}
// 獲取證照相關資訊
self::certificates($serial);
// 平臺公鑰
$public_key = self::getPublicKey($config['public_key']); //平臺公鑰
// 驗證簽名
$recode = \openssl_verify($message, $sign, $public_key, 'sha256WithRSAEncryption');
return $recode == 1 ? true : false;
}
//獲取私鑰
public static function getPrivateKey($filepath)
{
return openssl_pkey_get_private(file_get_contents($filepath));
}
//獲取公鑰
public static function getPublicKey($filepath)
{
return openssl_pkey_get_public(file_get_contents($filepath));
}
/**
* [certificates 獲取證照]
* @return [type] [description]
*/
public static function certificates($serial)
{
$config = self::$config;
$publicStr = @file_get_contents($config['public_key']);
if ($publicStr) { // 判斷證照是否存在
$openssl = openssl_x509_parse($publicStr);
if ($openssl['serialNumberHex'] == $serial) { // 是否是所需證照
// return self::getPublicKey($config['public_key']); //平臺公鑰
return '';
}
}
$url = self::$certificatesUrl;
$params = '';
$header = self::createAuthorization($url, $params, 'GET');
$response = Http::get($url, $params, $header);
$result = json_decode($response, true);
if (empty($result['data'])) {
throw new RuntimeException("[" . $result['code'] . "] " . $result['message']);
}
foreach ($result['data'] as $key => $certificate) {
if ($certificate['serial_no'] == $serial) {
$publicKey = self::decryptToString(
$certificate['encrypt_certificate']['associated_data'],
$certificate['encrypt_certificate']['nonce'],
$certificate['encrypt_certificate']['ciphertext']
);
file_put_contents($config['public_key'], $publicKey);
break; // 終止迴圈
}
// self::$publicKey[$certificate['serial_no']] = $publicKey;
}
// return self::getPublicKey($config['public_key']); //平臺公鑰
}
/**
* [decryptToString 證照和回撥報文解密]
* @param [type] $associatedData [附加資料包(可能為空)]
* @param [type] $nonceStr [加密使用的隨機串初始化向量]
* @param [type] $ciphertext [Base64編碼後的密文]
* @return [type] [description]
*/
public static function decryptToString($associatedData, $nonceStr, $ciphertext)
{
$config = self::$config;
$ciphertext = base64_decode($ciphertext);
if (strlen($ciphertext) <= self::AUTH_TAG_LENGTH_BYTE) {
return false;
}
// ext-sodium (default installed on >= PHP 7.2)
if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') &&
\sodium_crypto_aead_aes256gcm_is_available()) {
return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $config['key']);
}
// ext-libsodium (need install libsodium-php 1.x via pecl)
if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') &&
\Sodium\crypto_aead_aes256gcm_is_available()) {
return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $config['key']);
}
// openssl (PHP >= 7.1 support AEAD)
if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) {
$ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE);
$authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE);
return \openssl_decrypt($ctext, 'aes-256-gcm', $config['key'], \OPENSSL_RAW_DATA, $nonceStr,
$authTag, $associatedData);
}
throw new \RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安裝libsodium-php');
}
/** fengkui.net
* [get_rand_str 獲取隨機字串]
* @param integer $randLength [長度]
* @param integer $addtime [是否加入當前時間戳]
* @param integer $includenumber [是否包含數字]
* @return [type] [description]
*/
public static function get_rand_str($randLength=6, $addtime=0, $includenumber=1)
{
if ($includenumber)
$chars='abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQEST123456789';
$chars='abcdefghijklmnopqrstuvwxyz';
$len = strlen($chars);
$randStr = '';
for ($i=0; $i<$randLength; $i++){
$randStr .= $chars[rand(0, $len-1)];
}
$tokenvalue = $randStr;
$addtime && $tokenvalue = $randStr . time();
return $tokenvalue;
}
/** fengkui.net
* [get_ip 定義一個函式get_ip() 客戶端IP]
* @return [type] [description]
*/
public static function get_ip()
{
if (getenv("HTTP_CLIENT_IP"))
$ip = getenv("HTTP_CLIENT_IP");
else if(getenv("HTTP_X_FORWARDED_FOR"))
$ip = getenv("HTTP_X_FORWARDED_FOR");
else if(getenv("REMOTE_ADDR"))
$ip = getenv("REMOTE_ADDR");
else $ip = "Unknow";
if(preg_match('/^((?:(?:25[0-5]|2[0-4]\d|((1\d{2})|([1-9]?\d)))\.){3}(?:25[0-5]|2[0-4]\d|((1\d{2})|([1 -9]?\d))))$/', $ip))
return $ip;
else
return '';
}
}
本文參考文件:
1、微信支付 小程式 (v3)- PHP 完整後端程式碼
2、PHP 微信小程式 微信支付 v3
3、微信支付V3版本小程式支付 php簽名,驗籤,資料解密程式碼分享(完整方法主參考)
4、微信支付 API v3 回撥通知 簽名驗證 PHPdemo有嘛?
本作品採用《CC 協議》,轉載必須註明作者和本文連結