目前只使用到jsapi支付。其他支付尚未尝试,如有bug请自己调试。
在composer.json 添加包信息 然后执行composer update
"guzzlehttp/guzzle": "^7.4",
"guzzlehttp/uri-template": "^1.0",
"wechatpay/wechatpay": "^1.4"
composter手册
/* *
* 功能:微信v3支付
* 修改日期:2021-12-21
*/
namespace plugins\wxapp\service;
use think\Cache;
use WeChatPay\Builder;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Formatter;
use WeChatPay\Util\PemUtil;
use think\DB;
class WxpayService
{
public $merchantId = '';
public $merchantPrivateKeyFilePath = '';
public $merchantPrivateKeyInstance = '';
public $merchantCertificateSerial = '';
public $platformCertificateFilePath = '';
public $platformPublicKeyInstance = '';
public $platformCertificateSerial = '';
public $appid = '';
public $secret = '';
public $paySecret = '';
public $v3Key = '';
public $payTest = 0;
public $instance;
const KEY_LENGTH_BYTE = 32;
const AUTH_TAG_LENGTH_BYTE = 16;
function __construct()
{
$config = $this->getConfig();
// $this->appid = 'wxd03860b2b3xxx';
$this->appid = $config['appid'];
// 商户号
// $this->merchantId = '161641xxx';
$this->merchantId = $config['merchantId'];
// 从本地文件中加载商户API私钥,商户API私钥会用来生成请求的签名
// $this->merchantPrivateKeyFilePath = 'file://'.ROOT_PATH.'public/apiclient_key.pem';
$this->merchantPrivateKeyFilePath = trim($config['merchantPrivateKeyFilePath']); // 可以直接放密钥内容
$Rsa = new Rsa();
$this->merchantPrivateKeyInstance = $Rsa::from($this->merchantPrivateKeyFilePath, $Rsa::KEY_TYPE_PRIVATE);
// 商户API证书序列号
// $this->merchantCertificateSerial = '542A7C2F14E8F87B52DB3Fxxxxxxx';
$this->merchantCertificateSerial = $config['merchantCertificateSerial'];
// 公众号secret
// $this->secret = '6498b41d0ee84cf7749xxxxxx';
$this->secret = $config['secret'];
// 商户secret
// $this->paySecret = '825923d02c1649fef61xxxxxx';
$this->paySecret = $config['paySecret'];
// v3密钥
// $this->v3Key = 'wVrkTp8HCYWg6OZ0Ic5Pzfxxxxx';
$this->v3Key = $config['v3Key'];
// 是否测试支付
// $this->payTest = 1;
$this->payTest = $config['payTest'];
// dump($config);
// 使用 bin/CertificateDownloader.php 生成的参数,并非生成私钥时生成的 apiclient_cert.pem
// 命令 php ./CertificateDownloader.php -k=wVrkTp8HCYWg6OZ0xxxzxxx -m=16164xxxx -f=/mnt/www/wwwroot/path_com/Zerg/public/apiclient_key.pem -s=542A7C2F14E8F87B52DB3F6Axxx -o=/mnt/www/wwwroot/path_com/Zerg/public/wx_cert.pem
// 对应参数请查看 https://github.com/wechatpay-apiv3/wechatpay-php/blob/main/bin/README.md
// 从本地文件中加载微信支付平台证书,用来验证微信支付应答的签名
// $this->platformCertificateFilePath = 'file://'.ROOT_PATH.'public/apiclient_cert.pem';
$this->platformCertificateFilePath = trim($config['platformCertificateFilePath']);// 可以直接放密钥内容
$this->platformPublicKeyInstance = Rsa::from($this->platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
// 获取微信支付平台证书序列号
$this->platformCertificateSerial = PemUtil::parseCertificateSerialNo($this->platformCertificateFilePath);
// 构造一个 APIv3 客户端实例
$this->instance = Builder::factory([
'mchid' => $this->merchantId,
'serial' => $this->merchantCertificateSerial,
'privateKey' => $this->merchantPrivateKeyInstance,
'certs' => [
$this->platformCertificateSerial => $this->platformPublicKeyInstance,
],
]);
}
/**
* 统一下单
* @param $params
* WIDtotal_amount 金额
* WIDsubject 描述
* WIDout_trade_no 订单号
* openid 用户openid(trade_type=jsapi 需要 openid)
* trade_type 支付方式
* notifyUrl 回调地址
* @return false
*/
public function doPay($params)
{
// 手册
// https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_6_3.shtml
// https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter4_1_1.shtml
//注意: 提醒前端请查看 v3的手册 。签名方式不一样
$trade_type = $params['trade_type'] ?? 'h5';
$amount = (int)(bcmul("{$params['WIDtotal_amount']}","100")); //元转分
// dump($amount);
// dump($this->payTest);
$requestData = [
'json' => [ // JSON请求体
'appid' => $this->appid,
'mchid' => $this->merchantId,
'description' => $params['WIDsubject'],
'out_trade_no' => $params['WIDout_trade_no'],
'notify_url' => $params['notifyUrl'], // 回调地址
'amount' => [
'total' => !$this->payTest ? $amount : 1,
'currency' => 'CNY'
],
'scene_info' => [
'payer_client_ip' => get_client_ip()
],
'attach' => $params['attach'] ?? '' //附加数据
],
'headers' => ['Accept' => 'application/json']
];
// dump($requestData);exit;
if ($trade_type == 'jsapi') {
if (!isset($params['openid']) || empty($params['openid'])) {
return ['code' => 0, 'message' => 'Openid不能为空'];
}
$requestData['json']['payer'] = [
'openid' => $params['openid']
];
}
if ($trade_type == 'h5') {
$requestData['json']['scene_info']['h5_info']['type'] = "Wap";
}
try{
$resp = $this->instance
->chain("v3/pay/transactions/{$trade_type}")
->post($requestData);
// echo $resp->getStatusCode(), PHP_EOL;
// echo $resp->getBody(), PHP_EOL;
$content = $resp->getBody();
$data = json_decode($content, true);
$ret = $this->getTypeInfo($trade_type,$data);
$info['code'] = $resp->getStatusCode();
$info['data'] = $ret;
// echo $resp->getBody(), PHP_EOL;
return $info;
} catch (\Exception $e) {
// 进行错误处理
// echo $e->getMessage(), PHP_EOL;
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$r = $e->getResponse();
$code = $r->getStatusCode();
// echo $r->getReasonPhrase(), PHP_EOL;
// echo $r->getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
$data = json_decode($r->getBody(),true);
if(isset($data['prepay_id'])){
$info['code'] = 200;
$ret = $this->getTypeInfo($trade_type,$data);
$info['data'] = $ret;
return $info;
}
// echo $r->getReasonPhrase(), PHP_EOL;
// echo $r->getBody(), PHP_EOL, PHP_EOL, PHP_EOL;
}
// echo $e->getTraceAsString(), PHP_EOL;
return false;
}
}
/**
* 获得各支付类型的返回值
* @param $trade_type
* @param $data
* @return array|array[]
*/
public function getTypeInfo($trade_type,$data){
switch ($trade_type) {
// APP支付
case 'app':
$ret = ['orderInfo' => $this->getOrderInfo($data['prepay_id'])];
break;
// 公众号支付
case 'jsapi':
$ret = ['jsApiInfo' => $this->getJsApiInfo($data['prepay_id'])];
break;
// h5支付
case 'h5':
$ret = ['h5_url' => $data['h5_url']];
break;
// 扫码支付
case 'native':
$ret = ['code_url' => $data['code_url']];
break;
default:
break;
}
return $ret;
}
/**
* jsapi签名
* @param $prepay_id
* @return array
*/
public function getJsApiInfo($prepay_id){
$info = [
"appId"=>$this->appid, //公众号名称,由商户传入
"timeStamp"=>time(), //时间戳,自1970年以来的秒数
"nonceStr"=>$this->getNonceStr(16), //随机串
"package"=>"prepay_id=$prepay_id",
];
$paySign = $this->RSAsign($info,$this->merchantPrivateKeyFilePath);
return $paySign;
}
/**
* 微信md5签名
* @param $para mixed 带签名参数数组
* @param $wx_key string wxkey
*/
public static function MD5sign($array, $wx_key) {
ksort($array);
$stringA = urldecode(http_build_query($array));
$stringSignTemp="$stringA&key=".$wx_key;
return strtoupper(md5($stringSignTemp));
}
/**
* 微信rsa签名
* @param $params
* @param $merchantPrivateKeyInstance
* @return array
*/
public static function RSAsign($params,$merchantPrivateKeyInstance) {
$params += ['paySign' => Rsa::sign(
Formatter::joinedByLineFeed(...array_values($params)),
$merchantPrivateKeyInstance
), 'signType' => 'RSA'];
return $params;
}
public function getOrderInfo($prepay_id)
{
$nonceStr = $this->getNonceStr(16);
$package = "Sign=WXPay";
$timestamp = time();
$paySign = $this->paySign($this->appid, $timestamp, $nonceStr, $package);
return [
'appid' => $this->appid,
'noncestr' => $nonceStr,
'package' => $package,
'partnerid' => $this->merchantId,
'prepayid' => $prepay_id,
'timestamp' => $timestamp,
'sign' => $paySign,
];
}
/**
* 随机字符串
* @param int $length
* @return string
*/
public static function getNonceStr($length = 16)
{
$chars = "abcdefghijklmnopqrstuvwxyz0123456789";
$str ="";
for ( $i = 0; $i < $length; $i++ ) {
$str .= substr($chars, mt_rand(0, strlen($chars)-1), 1);
}
return $str;
}
/**
* 获取AccessToken
* @param $code
* @return mixed|string
*/
public function getAccessToken($code,$user_id){
$accessToken = Cache::get('accessToken:'.$user_id);
if(!isset($accessToken['errcode'])){
if($accessToken) return $accessToken;
}
$url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=$this->appid&secret=$this->secret&code=$code&grant_type=authorization_code";
$ch = curl_init();
//3.设置参数
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
//4.调用接口
$res = curl_exec($ch);
// if(curl_errno($ch)){
// var_dump(curl_error($ch));
// }
//5.关闭curl
curl_close($ch);
$arr = json_decode($res, true);
if(isset($arr['errcode'])){
// var_dump($arr);
return false;
}
Cache::set('accessToken:'.$user_id,$arr,'7200');
return $arr;
}
// 回调
/**
* 回调解码
* @param $post_data
* @return mixed|void
*/
public function notify($post_data)
{
// https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter4_1_5.shtml 手册
// Db::name('test_log')->insert(['create_time'=>time(),'name'=>'微信支付v3回调','log'=>json_encode($content,256)]);
// $str = '{"id":"8a8eb699-67c0-542e-933e-4dfb36f0919f","create_time":"2021-12-21T14:34:26+08:00","resource_type":"encrypt-resource","event_type":"TRANSACTION.SUCCESS","summary":"支付成功","resource":{"original_type":"transaction","algorithm":"AEAD_AES_256_GCM","ciphertext":"WAtQqUDtf\/QvlqLwxg0kqAx4j4YKFlSiMEgAQvXe87Iarl4LT\/SebdO\/2vCFBK7LNWoEiW6opFkk7Wl+lhlKLYPR+7xIgFVFDqqkNPemtJMeJH+yF5BFCiiGOATDJTThXh81qFqUk0GT3FHnzjlmUN4+t3jlTtr9Rrlm5YZ5lsvv4IAiOJJR5JrUABbapxvykFHAtVWi004\/wuTuSHYySRCB7mbCZybjnOrzKJxi0p1zZ553FKxk83jwk14xA\/ijBRqr\/9hbFSfxUm9xna3BfCocg1dFsRgTUvfmIEvVG7J1se8StD5O820wjtZCri5aE3rTqumIMmgTqKvUtCqfZ5dahzoSe0iR6nBDNNP01Tb0PH2QyriIomivlvMDoe3PI93ZsPD3vgO8Tmb6LxbHArkr0Xnn4fkXaJwTwrPDb2IcN8dr3c82AiuZ6u8M8x1c+JOl83bDcVtE3rPGVMwgyqg\/Ee8D5mFK8DgZ4ohzf2WWX7+itaLSLZPY\/VAlzt0T47Xc1QC6giF+qRnn0DAYVTxkd9F93QdLSrt9NArZlx+01Cp5sGeMsH8tdwyC\/P7VNdw=","associated_data":"transaction","nonce":"TQ68zkQ9LvpD"},"_plugin":"wxapp","_controller":"notify","_action":"notify"}';
// $post_data = json_decode($str,true);
if ($post_data['event_type'] == 'TRANSACTION.SUCCESS') {
$nonceStr = $post_data['resource']['nonce'];
$associatedData = $post_data['resource']['associated_data'];
$ciphertext = $post_data['resource']['ciphertext'];
$ciphertext = base64_decode($ciphertext);
if (strlen($ciphertext) <= 12) {
return;
}
$result = $this->wechartDecrypt($ciphertext,$associatedData,$nonceStr);
return $result;
// array(12) {
// ["mchid"] => string(10) "1616410845"
// ["appid"] => string(18) "wxd03860xxxx"
// ["out_trade_no"] => string(19) "202112211xxxx"
// ["transaction_id"] => string(28) "42000013052xxxx27428"
// ["trade_type"] => string(5) "JSAPI"
// ["trade_state"] => string(7) "SUCCESS"
// ["trade_state_desc"] => string(12) "支付成功"
// ["bank_type"] => string(6) "OTHERS"
// ["attach"] => string(0) ""
// ["success_time"] => string(25) "2021-12-21T14:34:26+08:00"
// ["payer"] => array(1) {
// ["openid"] => string(28) "ohH0s5g9GVDZHrPFTRpipxxxx"
// }
// ["amount"] => array(4) {
// ["total"] => int(1)
// ["payer_total"] => int(1)
// ["currency"] => string(3) "CNY"
// ["payer_currency"] => string(3) "CNY"
// }
// }
}
return ;
}
/**
* 微信回调解密
* @param $ciphertext
* @param $associatedData
* @param $nonceStr
* @return mixed|string
*/
public function wechartDecrypt($ciphertext,$associatedData, $nonceStr) {
$key = $this->v3Key;
// ext-sodium (default installed on >= PHP 7.2)
if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()) {
$str = \ sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $key);
return json_decode($str,true);
}
// 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()) {
$str = \ Sodium\ crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $key);
return json_decode($str,true);
}
// 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);
$str = \ openssl_decrypt($ctext, 'aes-256-gcm', $key, \OPENSSL_RAW_DATA, $nonceStr,
$authTag, $associatedData);
return json_decode($str,true);
}
return '';
throw new\ RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php');
}
/**
* 获取配置
* @return mixed
*/
public function getConfig(){
$payconfig = DB::name('plugin')->where('id',8)->value('config');
return json_decode($payconfig,true);
}
}
?>