JSAPI下单
商户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易会话标识后再按Native、JSAPI、APP等不同场景生成交易串调起支付。
文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml
接口说明
适用对象: 直连商户
请求URL:https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi
请求方式:POST
public function jsApiPlaceOrder()
{
$params = \request()->all();//请求参数
/**
* 应用ID appid
* 直连商户号 mchid
* 商品描述 description
* 商户订单号 out_trade_no
* 交易结束时间 time_expire
* 附加数据 attach
* 通知地址 notify_url
* 总金额 total
* 货币类型 currency CNY:人民币,境内商户号仅支持人民币。
* 用户标识 openid
*/
$data_arr['mchid'] = '直连商户号';
$data_arr['out_trade_no'] = '商户订单号';
$data_arr['appid'] = '应用ID';
$data_arr['description'] = '商品描述';
$data_arr['notify_url'] = '通知地址';
$data_arr['attach'] = '附加数据';
$data_arr['amount'] = [
'total' => '总金额',
'currency' => '货币类型',
];
$data_arr['payer'] = [
// 通过前端传入的jscode换取openid,$get_config 包含了appid,密钥等信息的数组
'openid' => $this->GetOpenidFromMp($params['code'], $get_config) // 用户标识
];
$url_parts = parse_url('https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi');
$canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
$timestamp = time();
$nonce = $this->getNonceStr();
$body = json_encode($data_arr);
$schema = 'WECHATPAY2-SHA256-RSA2048';
$sign = $this->getWxRSA256Sign([
'POST'
, $canonical_url
, $timestamp
, $nonce
, $body
], '商户密钥');
$token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
'直连商户号', $nonce, $timestamp, '69B99084AF6C21B0D57C152BFE804851327CAAF3', $sign);
$re = $this->httpRequest(
'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi'
, 'POST'
, $body
, [
'Authorization:' . $schema . ' ' . $token
, 'Accept: application/json'
, 'Content-Type: application/json'
, 'User-Agent: '.\request()->header('user-agent', '')
]
);
$re = json_decode($re,true);
if(!empty($re) && isset($re['prepay_id'])) {
$args['timeStamp'] = (string) time();
$args['nonceStr'] = $this->getNonceStr();
$args['package'] = 'prepay_id='.$re['prepay_id'];
$args['signType'] = 'RSA';
$args['paySign'] = $this->getWxRSA256Sign([
'应用ID'
, $args['timeStamp']
, $args['nonceStr']
, $args['package']
], '商户密钥');
if($args['paySign']) {
// 将生成的参数返回给前端,给它调用wx.requestPayment(OBJECT)发起微信支付
return ['code' => 0, 'msg' => 'success', 'data' => $args];
} else {
Log::channel(\request('game'))->info(__CLASS__.';'.__FUNCTION__.";小程序调起支付的签名计算异常", [$args, $get_config]);
return ['code' => 1, 'msg' => '支付签名计算异常'];
}
} else {
Log::channel(\request('game'))->info(__CLASS__.';'.__FUNCTION__.";prepay_id 结果异常", [$params, $re]);
return ['code' => 1, 'msg' => '调起支付异常'];
}
}
/**
* 小程序调起支付的签名计算
* @param array $sign_arr
* @param string $apiclient_key
* @return string
*/
private function getWxRSA256Sign(array $sign_arr, string $apiclient_key) :string
{
$message = '';
foreach ($sign_arr as $value) $message .= $value."\n";
$pri_key = "-----BEGIN RSA PRIVATE KEY-----\n" .
wordwrap($apiclient_key, 64, "\n", true) .
"\n-----END RSA PRIVATE KEY-----";
$pi_key =openssl_pkey_get_private($pri_key);
openssl_sign($message, $raw_sign, $pi_key, 'sha256WithRSAEncryption');
return base64_encode($raw_sign);
}
private function httpRequest($url,$method,$param,$headers,$post_file=false)
{
$oCurl = curl_init();
if(stripos($url,"https://")!==FALSE){
curl_setopt($oCurl, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($oCurl, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($oCurl, CURLOPT_SSLVERSION, 1); //CURL_SSLVERSION_TLSv1
}
if (PHP_VERSION_ID >= 50500 && class_exists('\CURLFile')) {
$is_curlFile = true;
} else {
$is_curlFile = false;
if (defined('CURLOPT_SAFE_UPLOAD')) {
curl_setopt($oCurl, CURLOPT_SAFE_UPLOAD, false);
}
}
if (is_string($param)) {
$strPOST = $param;
} elseif($post_file) {
if($is_curlFile) {
foreach ($param as $key => $val) {
if (substr($val, 0, 1) == '@') {
$param[$key] = new \CURLFile(realpath(substr($val,1)));
}
}
}
$strPOST = $param;
} else {
$aPOST = array();
foreach($param as $key=>$val){
$aPOST[] = $key."=".urlencode($val);
}
$strPOST = join("&", $aPOST);
}
curl_setopt($oCurl, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($oCurl, CURLOPT_URL, $url);
curl_setopt($oCurl, CURLOPT_RETURNTRANSFER, 1 );
curl_setopt($oCurl, CURLOPT_POST,true);
curl_setopt($oCurl, CURLOPT_POSTFIELDS,$strPOST);
curl_setopt($oCurl, CURLOPT_HTTPHEADER, $headers);
$sContent = curl_exec($oCurl);
$aStatus = curl_getinfo($oCurl);
$errno = curl_errno($oCurl);
if($errno) {
Log::error("curl异常:", [
'url' => $url,
'params' => $param,
'errno' => $errno,
]);
}
curl_close($oCurl);
if(intval($aStatus["http_code"])==200){
return $sContent;
}else{
Log::error("curl 不为200:", [
'url' => $url,
'params' => $param,
'errno' => $errno,
'rtn' => $sContent,
]);
return false;
}
}
/**
* 随机字符串,不长于32位。
* @param int $length
* @return string
*/
private function getNonceStr(int $length = 32) :string
{
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
$str = '';
for ($i = 0; $i<$length; $i++) {
$str .=substr($chars, mt_rand(0, strlen($chars) - 1), 1);
}
return $str;
}
getOpenidFromMp()
方法
private function getOpenidFromMp($code, $get_config)
{
$url = $this->__CreateOauthUrlForOpenid($code, $get_config);
//初始化curl
$ch = curl_init();
$curlVersion = curl_version();
$ua = "WXPaySDK/3.0.9 (".PHP_OS.") PHP/".PHP_VERSION." CURL/".$curlVersion['version']." "
.'1481215812';
//设置超时
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER,FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST,FALSE);
curl_setopt($ch, CURLOPT_USERAGENT, $ua);
curl_setopt($ch, CURLOPT_HEADER, FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
$proxyHost = "0.0.0.0";
$proxyPort = 0;
if($proxyHost != "0.0.0.0" && $proxyPort != 0){
curl_setopt($ch,CURLOPT_PROXY, $proxyHost);
curl_setopt($ch,CURLOPT_PROXYPORT, $proxyPort);
}
//运行curl,结果以jason形式返回
$res = curl_exec($ch);
if (curl_errno($ch)) {
Log::error("curl异常 MiniAppAutoReply GetOpenidFromMp:", [
'url' => $url,
'params' => $code,
'errno' => curl_errno($ch),
]);
}
curl_close($ch);
//取出openid
$data = json_decode($res,true);
if (isset($data['openid'])) {
$openid = $data['openid'];
} else {
Log::info(__CLASS__.';'.__FUNCTION__.";请求获取openid 出错,结果为:",[ $data ]);
$openid = '';
}
return $openid;
}
构造获取open和access_toke的url地址 方法__CreateOauthUrlForOpenid()
/**
* 构造获取open和access_toke的url地址
* @param $code
* @param array $get_conf
* @return string 请求的url
*/
private function __CreateOauthUrlForOpenid($code, $get_conf)
{
$urlObj["appid"] = $get_conf['zbAppId'];//应用ID
$urlObj["secret"] = $get_conf['zbAppSecret'];//商户密钥
$urlObj["js_code"] = $code;
$urlObj["grant_type"] = "authorization_code";
$bizString = $this->ToUrlParams($urlObj);
//return "https://api.weixin.qq.com/sns/oauth2/access_token?".$bizString;
return "https://api.weixin.qq.com/sns/jscode2session?".$bizString;
}
拼接签名字符串方法 ToUrlParams()
/**
* 拼接签名字符串
* @param array $urlObj
* @return string 返回已经拼接好的字符串
*/
private function ToUrlParams($urlObj)
{
$buff = "";
foreach ($urlObj as $k => $v)
{
if($k != "sign"){
$buff .= $k . "=" . $v . "&";
}
}
$buff = trim($buff, "&");
return $buff;
}
调用wx.requestPayment(OBJECT)发起微信支付成功后,支付回调处理
public function payCallBack()
{
try {
$params = \request()->all();//请求参数
/**
* 微信小程序回调文档 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_5.shtml
* 微信小程序APIv3支付回调解密 https://blog.csdn.net/qq_16746785/article/details/120260761
*
* 支付成功结果通知
* {
"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": ""
}
}
*/
if (isset($params['summary']) && $params['summary'] == '支付成功') {
$nonce_str = $params['resource']['nonce'];
$associated_data = $params['resource']['associated_data'];
$cipher_text = base64_decode($params['resource']['ciphertext']);
if (strlen($cipher_text) <= 12) {
Log::warning(__CLASS__ .';' . __FUNCTION__ . ";微信小程序APIv3支付回调解密失败 cipher_text <= 12");
}
$get_config = $this->config();
$key = $get_config['zbApiV3Key'];//商户平台设置的api v3 密钥
// sodium_crypto_aead_aes256gcm_decrypt ( string $ciphertext , string $ad , string $nonce , string $key ) 这个函数。
//这个函数调用的时候会报错,那是因为使用这个函数需要开启 libsodium 扩展才能使用。需要先自行安装下这个扩展
$result = json_decode( sodium_crypto_aead_aes256gcm_decrypt($cipher_text, $associated_data, $nonce_str, $key), true );
// $dd = [
// 'appid' => $result['appid'],//应用ID
// 'mchid' => $result['mchid'],//商户号
// 'out_trade_no' => $result['out_trade_no'],//商户订单号
// 'transaction_id' => $result['transaction_id'],//微信支付订单号
// /**
// * 交易类型,枚举值:
// JSAPI:公众号支付
// NATIVE:扫码支付
// APP:APP支付
// MICROPAY:付款码支付
// MWEB:H5支付
// FACEPAY:刷脸支付
// 示例值:MICROPAY
// */
// 'trade_type' => $result['trade_type'],
// 'openid' => $result['payer']['openid'],//payer 支付者信息/openid 用户标识
// 'payer_total' => $result['amount']['payer_total'],//amount 订单金额信息/payer_total 用户支付金额,单位为分
// /**
// * 交易状态,枚举值:
// SUCCESS:支付成功
// REFUND:转入退款
// NOTPAY:未支付
// CLOSED:已关闭
// REVOKED:已撤销(付款码支付)
// USERPAYING:用户支付中(付款码支付)
// PAYERROR:支付失败(其他原因,如银行返回失败)
// */
// 'trade_state' => $result['trade_state'],//交易状态
// ];
//Log::debug(__CLASS__ .';' . __FUNCTION__ . ";微信小程序APIv3支付回调解密 resource对象 结果", [$result]);
}
if($result['trade_state'] == 'SUCCESS') {
// TODO 操作发货,更新订单信息等等。。
} else {
Log::warning(__CLASS__ .';' . __FUNCTION__ . ";支付通知API 回调结果不是成功的 trade_state:".$result['trade_state']);
}
} catch (Exception $e) {
Log::warning(__CLASS__ .';' . __FUNCTION__ . ";发货异常:" . $e->getMessage() . ";所在行数:" . $e->getLine());
}
return ['code' => 'SUCCESS', 'msg' => '成功'];
}