Small program WeChat payment development guide: https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_8_2.shtml
Flowchart:
<?php
namespace app\api\controller;
use \think\Cache;
use \think\Controller;
use think\Loader;
use think\Db;
use \think\Cookie;
use \think\Session;
use \think\Request;
use app\api\controller\Base;
// class Pay extends Base
class Pay extends Controller
{
// private $apiV3key =config('wx_key');
//微信解密https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_2.shtml
private $aesKey;
const KEY_LENGTH_BYTE = 32;
const AUTH_TAG_LENGTH_BYTE = 16;
public function _empty()
{
//把所有城市的操作解析到city方法
echo "迷路了!";
}
//微信支付调用接口,可根据情况调整
//所需字段 $price $cost_type $product_id
//产品id
public function payPrice()
{
//随机生产的订单号,可根据情况资讯修改
$out_trade_no=time().session("userinfo.id");
if(empty($out_trade_no)) return json(['code'=>0,'msg'=>'缺少参数order_sn']);
//价格
$price=(float)input('post.price');
$product_id=(string)input('post.product_id');
//JSAPI下单官方说明,请求参数,返回参数
//https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml
// 官方提供网址
$url = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
$urlarr = parse_url($url); //拆解为:[scheme=>https,host=>api.mch.weixin.qq.com,path=>/v3/pay/transactions/native]
$time = (string)time(); //时间戳
$noncestr = $this->getNonceStr();
$appid = config('appid');//appID
$mchid = config('mch_id');//商户ID
$xlid = config('xlid');//证书序列号
$data = array();
$data['appid'] = $appid;
$data['mchid'] = $mchid;
$data['description'] = '商品描述'
$data['out_trade_no'] = $out_trade_no;//订单编号,订单号在微信支付里是唯一的
$data['notify_url'] = config('notify_url');//异步接收微信支付结果通知的回调地址,需根据自己的情况修改回调接口,也可以为空
$data['amount']['total'] = $price*100;//金额 单位 分
$data['scene_info']['payer_client_ip'] = $_SERVER["REMOTE_ADDR"];;//场景ip
//用户在直连商户appid下的唯一标识openid
$data['payer']['openid']=session("userinfo.open_id"); //openid
//var_dump($data);
// var_dump(session('userinfo'));
// exit();
//json加密成json对象
$data = json_encode($data);
$http_method = 'POST';
//微信支付API v3通过验证签名来保证请求的真实性和数据的完整性,所有向微信支付发送请求之前都要先进行签名,所有微信支付返回的信息都要进行验签,涉及都 php rsa 加密解密,参考地址:https://blog.csdn.net/wkj001/article/details/129093672?spm=1001.2014.3001.5501
//微信支付签名生成:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml
//微信签名,包含了$data数据、微信指定地址、随机数和时间
$key = $this->getSign($http_method,$data,$urlarr['path'],$noncestr,$time);
// echo $key;
// exit();
$token = sprintf('mchid="%s",serial_no="%s",nonce_str="%s",timestamp="%d",signature="%s"',$mchid,$xlid,$noncestr,$time,$key);
//微信支付商户API v3要求请求通过HTTP Authorization头来传递签名。 Authorization由认证类型和签名信息两个部分组成。
//头部信息
$header = array(
'Accept: application/json',
'Content-Type: application/json',
'User-Agent:*/*',
'Authorization: WECHATPAY2-SHA256-RSA2048 '.$token
);
// var_dump($data);
// exit();
//向微信接口地址提交json格式的$data和header的头部信息,得到返回值
$res = $this->curl_post_https($url,$data,$header);
// var_dump($res);
// exit();
$prepay_id=json_decode($res,true)['prepay_id'];
//通过JSAPI下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的前端JS方法调起公众号支付
//计算调起支付的签名
$paySign=$this->getWechartSign($appid,$time,$noncestr,'prepay_id='.$prepay_id);
$payData=[
'timeStamp'=>$time,
'nonceStr'=>$noncestr,
'package'=>'prepay_id='.$prepay_id,
'signType'=>'RSA',
'paySign'=>$paySign
];
return json(['code'=>1,'msg'=>'ok','data'=>$payData]);
}
//获取随机字符串
public function getNonceStr(){
$strs="QWERTYUIOPASDFGHJKLZXCVBNM1234567890";
$name=substr(str_shuffle($strs),mt_rand(0,strlen($strs)-11),32);
return $name;
}
//微信支付签名
function getSign($method,$data=array(),$url,$randstr,$time){
$str = $method."\n".$url."\n".$time."\n".$randstr."\n".$data."\n";
$key = file_get_contents('../cert/apiclient_key.pem');//在商户平台下载的秘钥,读取到变量
$str = $this->getSha256WithRSA($str,$key);
return $str;
}
//调起支付的签名
function getWechartSign($appid,$timeStamp,$noncestr,$prepay_id){
//JSAPI调起支付的参数需要按照签名规则进行签名加密:
//https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_4.shtml#menu1
$str = $appid."\n".$timeStamp."\n".$noncestr."\n".$prepay_id."\n";
$key = file_get_contents('../cert/apiclient_key.pem');//在商户平台下载的秘钥,读取到变量
$str = $this->getSha256WithRSA($str,$key);
return $str;
}
//加密
public function getSha256WithRSA($content, $privateKey){
$binary_signature = "";
$algo = "SHA256";
openssl_sign($content, $binary_signature, $privateKey, $algo);
$sign = base64_encode($binary_signature);
return $sign;
}
/* PHP CURL HTTPS POST */
function curl_post_https($url,$data,$header){
// 模拟提交数据函数
$curl = curl_init(); // 启动一个CURL会话
curl_setopt($curl, CURLOPT_URL, $url); // 要访问的地址
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); // 对认证证书来源的检查
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0); // 从证书中检查SSL加密算法是否存在,如果出错则修改为0,默认为1
curl_setopt($curl, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']); // 模拟用户使用的浏览器
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); // 使用自动跳转
curl_setopt($curl, CURLOPT_AUTOREFERER, 1); // 自动设置Referer
curl_setopt($curl, CURLOPT_POST, 1); // 发送一个常规的Post请求
curl_setopt($curl, CURLOPT_POSTFIELDS, $data); // Post提交的数据包
curl_setopt($curl, CURLOPT_TIMEOUT, 30); // 设置超时限制防止死循环
curl_setopt($curl, CURLOPT_HEADER, 0); // 显示返回的Header区域内容
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); // 获取的信息以文件流的形式返回
curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
$tmpInfo = curl_exec($curl); // 执行操作
if (curl_errno($curl)) {
echo 'Errno'.curl_error($curl);//捕抓异常
}
curl_close($curl); // 关闭CURL会话
return $tmpInfo; // 返回数据,json格式
}
/* PHP CURL HTTPS POST */
function curl_get_https($url,$header){
// 模拟提交数据函数
$curl = curl_init(); // 启动一个CURL会话
curl_setopt($curl, CURLOPT_URL, $url); // 要访问的地址
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); // 对认证证书来源的检查
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0); // 从证书中检查SSL加密算法是否存在,如果出错则修改为0,默认为1
curl_setopt($curl, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']); // 模拟用户使用的浏览器
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); // 使用自动跳转
curl_setopt($curl, CURLOPT_AUTOREFERER, 1); // 自动设置Referer
curl_setopt($curl, CURLOPT_TIMEOUT, 30); // 设置超时限制防止死循环
curl_setopt($curl, CURLOPT_HEADER, 0); // 显示返回的Header区域内容
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); // 获取的信息以文件流的形式返回
curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
$tmpInfo = curl_exec($curl); // 执行操作
if (curl_errno($curl)) {
echo 'Errno'.curl_error($curl);//捕抓异常
}
curl_close($curl); // 关闭CURL会话
return $tmpInfo; // 返回数据,json格式
}
//什么是平台证书
//https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_1.shtml#part-2
//获取微信支付平台证书序列,解密后是平台公钥 回调验证签名要用到
//如何获取平台证书 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/wechatpay5_1.shtml
public function get_cert()
{
$url = 'https://api.mch.weixin.qq.com/v3/certificates';
$url_parts = parse_url($url);
$http_method = 'GET';
$timestamp = (string)time();
$appid = config('appid');//appID
$mchid = config('mch_id');//商户ID
$xlid = config('xlid');//证书序列号
// $nonce = md5(time() . $out_trade_no);
$noncestr = $this->getNonceStr();//生成随机字符串
$body = '';
$canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
//给微信支付发送请求前,先签名,包含了$data数据、微信指定地址、随机数和时间
$key = $this->getSign($http_method,$body,$canonical_url,$noncestr,$timestamp);
//$sign = $this->wechat_sign($message);//签名 和统一下单 调起支付签名一样
$token = sprintf('mchid="%s",serial_no="%s",nonce_str="%s",timestamp="%d",signature="%s"',$mchid,$xlid,$noncestr,$timestamp,$key);
//头部信息
$header = array(
'Accept: application/json',
'Content-Type: application/json',
'User-Agent:*/*',
'Authorization: WECHATPAY2-SHA256-RSA2048 '.$token
);
//GET 获取平台证书列表
$res = $this->curl_get_https($url,$header);
//$res=json_decode($res,true);
// var_dump($res);
// exit();
// //$rs = $this->wechat_req($nonce, $timestamp, $sign, $url, $http_method, '');
// //保存
// // $path = APPPATH . '/Wechat/wxp_cert.pem';//平台证书序列
// // file_put_contents($path, json_encode($rs));
return $res;
}
//微信支付成功后的回调信息
public function wx_notify(){
//file_put_contents("hd.txt", '微信回调');
//https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_5.shtml
//支付成功结果通知数据回调
$header =Request::instance()->header();
$response_body = file_get_contents('php://input');
// // file_put_contents("head.txt", $header);
// // file_put_contents("body.txt", $response_body);
//微信签名验证
//微信支付发来的信息要进行验签
//https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml
$res=$this->verify_wechatnotify($header, $response_body);
file_put_contents("yq.txt", $res);
if(!$res){
return json(['code'=>0,'msg'=>'签名验证失败']);
exit();
}
//验签通过后,进行内容解密
$data = file_get_contents('php://input');
//json解密成数组
$data = json_decode($data, true);
//$data['resource'] 通知数据
//随机串
$nonceStr = $data['resource']['nonce'];
//附加数据
$associatedData = $data['resource']['associated_data'];
//数据密文Base64编码
$ciphertext = $data['resource']['ciphertext'];
$ciphertext = base64_decode($ciphertext);
//php>7.1,为了使用这个扩展,你必须将extension=php_sodium.dll添加到php.ini
if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()) {
//$APIv3_KEY就是在商户平台后端设置是APIv3秘钥
$orderData = \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, config('wx_key'));
file_put_contents("jieguo.txt", $orderData.PHP_EOL,FILE_APPEND);
$orderData = json_decode($orderData, true);
// $str12=print_r($orderData,true);
// file_put_contents("yewu12.txt", $str12.PHP_EOL,FILE_APPEND);
if ($orderData['trade_state']=='SUCCESS'){
// file_put_contents("SUCCESS.txt", $orderData['attach'].PHP_EOL,FILE_APPEND);
$order_sn=$orderData['out_trade_no']; //商户订单号
$transaction_id=$orderData['transaction_id']; //微信订单号
$open_id=$orderData['payer']['openid'];
$str13=print_r($orderData,true);
file_put_contents("yewu13.txt", $str13.PHP_EOL,FILE_APPEND);
// file_put_contents("test.txt", $orderData['attach'].PHP_EOL,FILE_APPEND);
file_put_contents("yewu14.txt", (string)$orderData['attach']);
//如果附加数据不为空
$attach=explode("*",(string)$orderData['attach']);
$cost_type=$attach['0']?$attach['0']:'';
$product_id=$attach['1']?$attach['1']:'';
file_put_contents("cost_type.txt", $cost_type.PHP_EOL,FILE_APPEND);
file_put_contents("product_id.txt", $product_id.PHP_EOL,FILE_APPEND);
// 启动事务
Db::startTrans();
try{
/*业务处理*/
$str1=print_r($orderData,true);
file_put_contents("yewu.txt", $str1.PHP_EOL,FILE_APPEND);
//业务1
$re['t1']=Db::name('user')->insertGetId($id);
//业务2
$re['t2']=Db::name('product')->insertGetId($id);
//业务3
$re['t2']=Db::name('order')->insertGetId($id);
//任意一个表写入失败都会抛出异常:
if (in_array('0', $re)) {
throw new Exception('意不意外?');
}
//最后提交事务
Db::commit();
//应答微信支付已处理该订单的通知
return json(['code' => 'SUCCESS', 'message' =>'ok']);
} catch (\Exception $e) {
// 回滚事务
Db::rollback();
return json(['code' => 'ERROR', 'message' =>'no']);
}
}
}
}
//微信验证签名
public function verify_wechatnotify($header, $response_body)
// public function verify_wechatnotify()
{
// var_dump( $header);
// exit();
//$header 回调头部信息 //$response_body回调主体
//获取平台证书
$plat = $this->get_cert();
//平台证书返回为json格式,json_decode转为数组格式
$plat=json_decode($plat,true);
// $string = print_r($plat, true);
// file_put_contents("plat1.txt", $string);
// $string2 = print_r($header, true);
// file_put_contents("header1.txt", $string2);
//serial_no平台证书序列号 //跟商户证书序列号不一样 不要搞混了
$serial_no = $plat['data'][0]['serial_no'];
//验证回调头部信息中的平台证书序列号跟平台证书序列号是否一致
if ($header['wechatpay-serial'] != $serial_no) {
return $data=['code'=>'0','msg'=> '平台证书序列号不一致,签名验证不通过'];
}
//解密平台证书序列获取公钥
// var_dump($plat);
// exit();
$associatedData = $plat['data'][0]['encrypt_certificate']['associated_data'];
$nonceStr = $plat['data'][0]['encrypt_certificate']['nonce'];
$ciphertext = $plat['data'][0]['encrypt_certificate']['ciphertext'];
//官方解密文档 https://wechatpay-api.gitbook.io/wechatpay-api-v3/qian-ming-zhi-nan-1/zheng-shu-he-hui-tiao-bao-wen-jie-mi
//平台证书解密后即获取公钥
$plat_cert_decrtpt = $this->decryptToString($associatedData, $nonceStr, $ciphertext);
// var_dump($plat_cert_decrtpt);
// exit();
//这个解密方法看官方文档https://wechatpay-api.gitbook.io/wechatpay-api-v3/qian-ming-zhi-nan-1/zheng-shu-he-hui-tiao-bao-wen-jie-mi
//$pub_path = 'XX/Wechat/wxp_pub.pem';
// file_put_contents($pub_path, $plat_cert_decrtpt);//平台公钥 //保存平台公钥 回调验签要用
//$plat_cert_decrtpt = file_get_contents($pub_path);
//提取平台公钥
$pub_key = openssl_pkey_get_public($plat_cert_decrtpt);
// var_dump($pub_key);
// exit();
//组装验签名串【未加密数据】
$message = $header['wechatpay-timestamp'] . "\n" . $header['wechatpay-nonce'] . "\n" . $response_body . "\n";
//对 Wechatpay-Signature的字段值使用Base64进行解码,得到应答签名。【加密数据】
$Signature = $header['wechatpay-signature'];
$sign = base64_decode($Signature);
//进行验证签名:使用微信支付平台公钥对验签名串和签名进行SHA256 with RSA签名验证
$ok = openssl_verify($message, $sign, $pub_key, OPENSSL_ALGO_SHA256);//回调验签
if ($ok == 1) {
// return $this->error('1', '回调签名验证成功');
return true;
} else {
return false;
}
}
//微信支付解密字符串
//https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_2.shtml
public function decryptToString($associatedData, $nonceStr, $ciphertext,$aesKey = '') {
if (empty($aesKey)) {
$this-> aesKey =config('wx_key'); //微信商户平台 api安全中设置获取
}else{
$this-> aesKey=$aesKey;
}
$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');
}
}