PHP 微信支付v3官方SDK JSAPI版(详细)

程序框架:ThinkPHP 5.1

PHP版本:7.4.33

官方SDK文档:https://pay.weixin.qq.com/doc/v3/partner/4012083112

使用 Composer 安装最新版本微信支付SDK:

composer require wechatpay/wechatpay

官方文档中还提到了Guzzle扩展

使用 Composer 安装最新版本 Guzzle 7:

扩展地址:https://packagist.org/packages/guzzlehttp/guzzle

composer require guzzlehttp/guzzle

程序项目中微信支付配置文件,根据项目你们自行创建,我这里用的是ThinkPHP 5.1版本

创建 config/applets.php 文件

注意:这里的公、私钥证书路径,必须带有 ‘file://’,否则会报错

<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Author: 自然 <c1516k@gmail.com>
// +----------------------------------------------------------------------

// +----------------------------------------------------------------------
// | 微信小程序设置
// +----------------------------------------------------------------------

return [
	// 小程序appid
	'appId'	=>	'',
	// 小程序appsecret
	'appSecret'	=>	'',
	
	//------- 微信支付 微信商户平台 账户中心 > API安全 里获取 -------
 	// 商户号
	'mchId'	=> '',
	// API密钥 v2版本,v3版本用不到可忽略
	'apiKey' => '',
	//「商户API私钥」路径
	'merchantPrivateKeyFilePath' => 'file://' . ROOT_PATH . 'data' . DIRECTORY_SEPARATOR . 'wxpay_cert' . DIRECTORY_SEPARATOR. 'apiclient_key.pem',
	//「商户API证书」序列号
	'merchantCertificateSerial' => '',
	//「微信支付公钥」路径
	'platformPublicKeyFilePath' => 'file://' . ROOT_PATH . 'data' . DIRECTORY_SEPARATOR . 'wxpay_cert' . DIRECTORY_SEPARATOR . 'pub_key.pem',
	//「微信支付公钥ID」
	'platformPublicKeyId' => '',
	// 微信支付回调密钥
	'payCallbackPrivateKey'	=> ''
];

创建客户端实例:library/WxPay.php,根据项目你们自定义

<?php
/*
* 微信支付 v3 操作
*/
namespace app\api\library;

use WeChatPay\Builder;
use WeChatPay\Crypto\Rsa;

class WxPay
{	
	function initialize()
	{
		
	}
	
    //微信支付配置获取
	static public function getConfig()
	{
		$data = config('applets.');
		//私钥解析
		if ( !empty($data['merchantPrivateKeyFilePath']) ) {
			$data['merchantPrivateKeyInstance'] = Rsa::from($data['merchantPrivateKeyFilePath'], Rsa::KEY_TYPE_PRIVATE);
		}
		//公钥解析
		if ( !empty($data['platformPublicKeyFilePath']) ) {
			$data['twoPlatformPublicKeyInstance'] = Rsa::from($data['platformPublicKeyFilePath'], Rsa::KEY_TYPE_PUBLIC);
		}
		
		return $data;
	}

	// APIv3 客户端实例
	static public function getWxpayInstance()
	{
		$config = self::getConfig();
		$instance = Builder::factory([
			'mchid' 		=> $config['mchId'],
			'serial' 		=> $config['merchantCertificateSerial'],
			'privateKey' 	=> $config['merchantPrivateKeyInstance'],
			'certs' 		=> [
				$config['platformPublicKeyId'] => $config['twoPlatformPublicKeyInstance'],
			],
		]);
		return $instance;
	}
}

创建微信支付控制器:controller/Wxpay.php

<?php
// +----------------------------------------------------------------------
// | 微信支付 v3
// +----------------------------------------------------------------------
namespace app\api\controller;

use think\Db;
use util\AesUtil;
use app\common\controller\Api;
use app\api\library\WxPay AS WxPayLibrary;

class Wxpay extends Api
{
	private $requestUrl = 'https://api.mch.weixin.qq.com/';
	public $config = [];
	//日志目录
	public $logs_path = ROOT_PATH . 'data' . DIRECTORY_SEPARATOR . 'wxpay_logs' . DIRECTORY_SEPARATOR;
	
	protected function initialize()
    {
        parent::initialize();
		$this->WxPayLibrary = new WxPayLibrary;
		$this->get_config();
    }
	
	/*
	 * 发起 JSAPI小程序支付
	 * 文档:https://pay.weixin.qq.com/doc/v3/partner/4012738519
	 */
	public function get_jsapi()
	{
		$order_sn = $this->request->post('order_sn/s', null);
		if (!$order_sn) {
			$this->error('支付失败');
		}
		//订单
		$order_info = Db::name('order')->where(['order_sn'=>$order_sn])->find();
		//非待支付订单
		if ($order_info['status'] != 2) {
			$this->error('支付失败');
		}
		//订单金额,单位为分
		$order_amount = (float) $order_info['order_amount'];
		//下单数据
		$data = [
			'appid'	=> $this->config['appId'],
			'mchid'	=> $this->config['mchId'],
			'description'	=> '订单标题【自定义】',
			'out_trade_no'	=> $order_sn,
			'time_expire'	=> getTimestampRFC3339($order_info['endtime']),
			'notify_url'	=> SITE_URL . '/api/wxpay/callback',
			'amount'		=> [
				'total'		=> ($order_amount * 100),
				'currency' 	=> 'CNY'
			],
			'payer'			=> [
				'openid'	=> $this->openid
			]
		];
		$timestamp     = time();
		$nonceStr      = self::createNonceStr();
		$apiUrl		   = 'v3/pay/transactions/jsapi';
		$url           = $this->requestUrl.$apiUrl;
        $url_parts     = parse_url($url);
        $canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
		//支付
		$instance = WxPayLibrary::getWxpayInstance();
		$resp = $instance -> chain($apiUrl) -> post(['json' => $data,
			'headers' => [
				'Authorization'	   => $this->getAuthorizationSign($nonceStr, $timestamp, $canonical_url, json_encode($data)),
				'Accept'		   => 'application/json',
				'Content-Type'	   => 'application/json',
				'User-Agent'	   => 'application/json'
			],
		]);
		$res = json_decode((string) $resp->getBody(), true);
		if ( isset($res['prepay_id']) )
		{
			//success
			$data = $this->get_request_param($timestamp, $nonceStr, $res['prepay_id']);
			$this->success('拉起支付成功', $data);
		}
		else
		{
			$this->error('支付失败');
		}
	}
	
	/*
	 * 支付回调 
	 * 文档:https://pay.weixin.qq.com/doc/v3/partner/4012085146
	 */
	public function callback()
	{
		$getCallBackData = file_get_contents('php://input');
		//将接收到的数据存入callBack.json文件中
		file_put_contents($this->logs_path.'callBack.json', $getCallBackData."\n\r",FILE_APPEND);
        //将回调密钥赋值给 解密类,回调密钥在“微信商户平台 > 账户中心 > API安全”
		$getData = new AesUtil($this->config['payCallbackPrivateKey']);
		//将变量由json类型数据转换为数组
		$disposeReturnData = json_decode($getCallBackData, true);
		//获取associated_data数据,附加数据
		$associatedData = $disposeReturnData['resource']['associated_data'];
		 //获取nonce数据,加密使用的随机串
		$nonceStr = $disposeReturnData['resource']['nonce'];
		//获取ciphertext数据,base64编码后的数据密文
		$ciphertext = $disposeReturnData['resource']['ciphertext'];
		//解密得到数组
		$result = $getData->decryptToString($associatedData, $nonceStr, $ciphertext);
		$data = json_decode($result, true);
		//记录回调解密数组
		file_put_contents($this->logs_path.'callBackData.json', $result."\n\r",FILE_APPEND);
		
		//支付成功
		if ( $data['trade_state'] === 'SUCCESS' )
		{
			//处理回调业务逻辑
			
		}
		else
		{
			// 支付异常
			return "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>";
		}
	}
	
	//获取配置
	private function get_config()
	{
		$config = WxPayLibrary::getConfig();
		return $this->config = $config;
	}
	
	//发起支付参数
	private function get_request_param($timeStamp, $nonceStr, $prepay_id)
	{
		$data = [
			'appId'		=> $this->config['appId'],
			'timeStamp'	=> $timeStamp,
			'nonceStr'	=> $nonceStr,
			'package'	=> 'prepay_id='.$prepay_id,
			'signType'	=> 'RSA'
		];
		$data['paySign'] = $this->getSign($data['appId'], $data['timeStamp'], $data['nonceStr'], $data['package']);
		return $data;
	}
	
	//随机字符串
	static function createNonceStr($length = 16)
    {
        $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
        $str = '';
        for ($i = 0; $i < $length; $i++) {
            $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
        }
        return $str;
    }
	
	//微信支付签名
	private function getSign($appid = null, $time = null, $randStr = null, $prepay_id = null)
	{
		$str = $appid . "\n" . $time . "\n" . $randStr . "\n" . $prepay_id . "\n";
		$privateKeyStr = file_get_contents( str_replace('file://', '', $this->config['merchantPrivateKeyFilePath']) );
		$data = getSha256WithRSA($str, $privateKeyStr);
		return $data;
	}
	
	//公共签名
	private function getAuthorizationSign($nonceStr, $timeStamp, $canonical_url, $data){  
			
		$key     = openssl_get_privatekey(file_get_contents( str_replace('file://', '', $this->config['merchantPrivateKeyFilePath']) ));//这里是打开商户私钥证书
		$method  = 'POST'; //请求的方式
		$message = $method . "\n" .$canonical_url. "\n" .$timeStamp . "\n" . $nonceStr . "\n" . $data . "\n"; //组合成一字符串 \n 需要每行携带,官方要求的
		openssl_sign($message, $rawSign, $key,'sha256WithRSAEncryption'); //sha256WithRSA 加密
		$sign    = base64_encode($rawSign); //收到上面加密值做base64
		return  'WECHATPAY2-SHA256-RSA2048 mchid='.$this->config['mchId'].',nonce_str="'.$nonceStr.'",timestamp="'.$timeStamp.'",serial_no='.$this->config['merchantCertificateSerial'].',signature="'.$sign.'"';//最后这一步,组合头部参数字符串,
		
	}
}

这里官方给提供了一个回调密文解密类,文档:https://pay.weixin.qq.com/doc/v3/partner/4012082320

<?php
//微信支付解密

namespace util;

class AesUtil
{
    /**
     * AES key
     *
     * @var string
     */
    private $aesKey;

    const KEY_LENGTH_BYTE = 32;
    const AUTH_TAG_LENGTH_BYTE = 16;

    /**
     * Constructor
     */
    public
    function __construct($aesKey)
    {
        if (strlen($aesKey) != self::KEY_LENGTH_BYTE) {
            throw new InvalidArgumentException('无效的ApiV3Key,长度应为32个字节');
        }
        $this->aesKey = $aesKey;
    }

    /**
     * Decrypt AEAD_AES_256_GCM ciphertext
     *
     * @param string $associatedData AES GCM additional authentication data
     * @param string $nonceStr AES GCM nonce
     * @param string $ciphertext AES GCM cipher text
     *
     * @return string|bool      Decrypted string on success or FALSE on failure
     */
    public
    function decryptToString($associatedData, $nonceStr, $ciphertext)
    {
        $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, $this->aesKey);
        }

        // 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, $this->aesKey);
        }

        // 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', $this->aesKey, \OPENSSL_RAW_DATA, $nonceStr,
                $authTag, $associatedData);
        }

        throw new \RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php');
    }
}

回调密文(解密前)

{
    "id": "EV-2018022511223320873",
    "create_time": "2015-05-20T13:29:35+08:00",
    "resource_type": "encrypt-resource",
    "event_type": "TRANSACTION.SUCCESS",
    "summary": "支付成功",
    "resource": {
        "original_type": "transaction",
        "algorithm": "AEAD_AES_256_GCM",
        "ciphertext": "",
        "associated_data": "",
        "nonce": ""
    }
}

回调密文(解密后)

{
  "sp_appid" : "wx8888888888888888",
  "sp_mchid" : "1230000109",
  "sub_appid" : "wxd678efh567hg6999",
  "sub_mchid" : "1900000109",
  "out_trade_no" : "1217752501201407033233368018",
  "transaction_id" : "1217752501201407033233368018",
  "trade_type" : "JSAPI",
  "trade_state" : "SUCCESS",
  "trade_state_desc" : "支付成功",
  "bank_type" : "CMC",
  "attach" : "自定义数据",
  "success_time" : "2018-06-08T10:34:56+08:00",
  "payer" : {
    "sp_openid" : "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o\t",
    "sub_openid" : "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o\t"
  },
  "amount" : {
    "total" : 100,
    "payer_total" : 100,
    "currency" : "CNY",
    "payer_currency" : "CNY"
  },
  "scene_info" : {
    "device_id" : "013467007045764"
  },
  "promotion_detail" : [
    {
      "coupon_id" : "109519",
      "name" : "单品惠-6",
      "scope" : "GLOBAL",
      "type" : "CASH",
      "amount" : 100,
      "stock_id" : "931386",
      "wechatpay_contribute" : 0,
      "merchant_contribute" : 0,
      "other_contribute" : 0,
      "currency" : "CNY",
      "goods_detail" : [
        {
          "goods_id" : "M1006",
          "quantity" : 1,
          "unit_price" : 100,
          "discount_amount" : 1,
          "goods_remark" : "商品备注信息"
        }
      ]
    }
  ]
}

支付控制器还涉及到两个自定义函数,我给贴到下面,自行使用

/**
 * 生成SHA256WithRSA签名
 * @param string $data 待签名数据
 * @param string $privateKey 私钥(PEM格式)
 * @return string Base64编码的签名
 */
function getSha256WithRSA($data, $privateKey)
{
    $key = openssl_pkey_get_private($privateKey);
    if (!$key) {
        throw new Exception("私钥无效");
    }

    openssl_sign($data, $signature, $key, OPENSSL_ALGO_SHA256);
    openssl_free_key($key);

    return base64_encode($signature);
}

//生成 RFC 3339 时间格式 2018-06-08T10:34:56+08:00"
function getTimestampRFC3339($timestamp)
{
	// 创建一个DateTime对象,使用时间戳初始化
	$dateTime = new DateTime();
	$dateTime->setTimestamp($timestamp);
	// 格式化为RFC 3339
	$rfc3339String = $dateTime->format(DateTime::RFC3339);
	return $rfc3339String;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值