<?php
namespace
fengkui\Pay;
use
Exception;
use
RuntimeException;
use
fengkui\Supports\Http;
class
Wechat
{
const
AUTH_TAG_LENGTH_BYTE = 16;
private
static
$certificatesUrl
= 'https:
private
static
$transactionsUrl
= 'https:
private
static
$refundUrl
= 'https:
private
static
$authorizeUrl
= 'https:
private
static
$accessTokenUrl
= 'https:
private
static
$config
=
array
(
'xcxid' => '',
'appid' => '',
'mchid' => '',
'key' => '',
'appsecret' => '',
'notify_url' => '',
'redirect_url' => '',
'serial_no' => '',
'cert_client' => './cert/apiclient_cert.pem',
'cert_key' => './cert/apiclient_key.pem',
'public_key' => './cert/public_key.pem',
);
public
function
__construct(
$config
=NULL,
$referer
=NULL){
$config
&& self::
$config
=
array_merge
(self::
$config
,
$config
);
}
public
static
function
unifiedOrder(
$order
,
$type
=false)
{
$config
=
array_filter
(self::
$config
);
$params
=
array
(
'appid' =>
$type
?
$config
['xcxid'] :
$config
['appid'],
'mchid' =>
$config
['mchid'],
'description' =>
$order
['body'],
'out_trade_no' => (string)
$order
['order_sn'],
'notify_url' =>
$config
['notify_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
;
}
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
;
}
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;
}
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'] . ':
$params
= ['appid' =>
$config
['appid']];
if
(
empty
(
$code
)) {
$params
['redirect_uri'] =
$redirectUri
;
$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
);
$result
= json_decode(
$response
, true);
$order
['openid'] =
$result
['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
;
}
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'],
'timeStamp' => (string)time(),
'nonceStr' => self::get_rand_str(32, 0, 1),
'prepayid' =>
$result
['prepay_id'],
);
$data
['paySign'] = self::makeSign(
$data
);
$data
['partnerid'] =
$config
['mchid'];
$data
['package'] = 'Sign=WXPay';
return
$data
;
}
else
{
return
$log
?
$result
: false;
}
}
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;
}
}
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'],
'timeStamp' => (string)time(),
'nonceStr' => self::get_rand_str(32, 0, 1),
'package' => 'prepay_id='.
$result
['prepay_id'],
);
$data
['paySign'] = self::makeSign(
$data
);
$data
['signType'] = 'RSA';
return
$data
;
}
else
{
return
$log
?
$result
: false;
}
}
public
static
function
scan(
$order
=[],
$log
=false)
{
if
(
empty
(
$order
['order_sn']) ||
empty
(
$order
['total_amount']) ||
empty
(
$order
['body'])){
die
(
"订单数组信息缺失!"
);
}
$order
['type'] = 'native';
$result
= self::unifiedOrder(
$order
);
if
(!
empty
(
$result
['code_url'])) {
return
urldecode(
$result
['code_url']);
}
else
{
return
$log
?
$result
: false;
}
}
public
static
function
notify(
$server
= [],
$response
= [])
{
$config
= self::
$config
;
$server
=
$server
??
$_SERVER
;
$response
=
$response
??
file_get_contents
('php:
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);
}
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
;
}
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
;
}
public
static
function
success()
{
$str
= ['code'=>'SUCCESS', 'message'=>'成功'];
die
(json_encode(
$str
, JSON_UNESCAPED_UNICODE));
}
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_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),
'data' => (
strtolower
(
$method
) == 'post' ? json_encode(
$data
, JSON_UNESCAPED_UNICODE) :
$data
),
];
$sign
= self::makeSign(
$body
);
$schema
= 'WECHATPAY2-SHA256-RSA2048';
$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:*
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
;
}
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
));
}
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
'';
}
}
$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
;
}
}
}
public
static
function
decryptToString(
$associatedData
,
$nonceStr
,
$ciphertext
)
{
$config
= self::
$config
;
$ciphertext
=
base64_decode
(
$ciphertext
);
if
(
strlen
(
$ciphertext
) <= self::AUTH_TAG_LENGTH_BYTE) {
return
false;
}
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']);
}
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']);
}
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');
}
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
;
}
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
'';
}
}