PHP JSAPI调支付API实现微信支付功能详解
一本曾经 人气:0一、首先我们来填个坑
支付验签失败
这个问题折磨了我两天,官方文档比较含糊不清。各种百度下来的方法试过之后也不尽人意,最后发现问题是没有二次签名
二次签名需要参数(代码会展示在哪里二次签名):
appId: 商户申请的公众号对应的appid(I大写)
nonceStr: 随机字符串(注意是JSAPI下单接口中返回的 nonce_str、不是重新生成)
package: 统一下单接口返回的prepay_id参数值 ,(注意格式prepay_id=wx.....)
signType: 签名类型、(官方文档)仅支持RSA。
(我的签名类型是 HMAC-SHA256 也是可以的,必须和下单使用的签名类型保持一致)
timeStamp:时间戳(这里要把 time() 转成字符串类型)
注明:使用这五个参数生成的 paySign 签名才是需要返给前端的(
)
官方文档实例要计算签名也给我整的蒙圈,最后发现直接将五个必须参数生成的签名返给前端就可以直接调取API了
二、代码示例
1.请求参数配置
$oInput = [ 'body' => '测试商品', // 商品说明 'attach' => '测试场景', // 自定义参数:可以用来做回调后场景区分 'out_trade_no' => '测试单号' . time(), // 自定义订单号 'total_fee' => 1 * 100, // 付款金额:记得*100 微信官方是以分为单位 'goods_tag' => '', // 优惠券相关参数 'notify_url' => 'http://...', // 回调通知地址 'trade_type' => 'JSAPI', // 支付方式 'openid' => $openid, // 付款用户openid // 'profit_sharing' => 'Y', // 是否分账的标识 ]; $res = $this->unifiedOrder($oInput); // 这里我调用的统一下单 return $res; // 返给前端带APPID等参数给前端去调用支付
2.统一下单API
public function unifiedOrder($inputObj, $timeOut = 6) { $url = "https://api.mch.weixin.qq.com/pay/unifiedorder"; // 首次签名参数 $oValues = [ 'body' => $inputObj['body'], // 设置商品或支付单简要描述 'attach' => $inputObj['attach'], // 设置附加数据,用于商户携带订单的自定义数据 'out_trade_no' => $inputObj['out_trade_no'], // 设置商户系统内部的订单号,transaction_id、out_trade_no二选一,如果同时存在优先级:transaction_id> out_trade_no 'total_fee' => $inputObj['total_fee'], // 设置订单总金额,只能为整数,单位:分 'time_start' => date("YmdHis"), // 设置订单生成时间 'time_expire' => date("YmdHis", time() + 600), // 设置订单失效时间 'goods_tag' => $inputObj['goods_tag'], // 设置商品标记,代金券或立减优惠功能的参数 'notify_url' => $inputObj['notify_url'], // 获取接收微信支付异步通知回调地址的值 'trade_type' => $inputObj['trade_type'], // JSAPI,NATIVE,APP 'openid' => $inputObj['openid'], // 用户在商户appid下的唯一标识 //'profit_sharing' => $inputObj['profit_sharing'], // 是否需要分账 'appid' => 'appid', // app_id:替换真实的 'mch_id' => 'mchid', // 商户号:替换真实的 'spbill_create_ip' => $_SERVER['REMOTE_ADDR'], // 终端ip 'nonce_str' => '自定义生成', // 随机32位字符串 'sign_type' => 'HMAC-SHA256', // 签名类型,自行替换 ]; // 首次签名 ksort($oValues); $oValues['sign'] = $this->MakeSign($oValues); // 调用签名 $xml = $this->ToXml($oValues); // 数字转xml类型 $response = self::postXmlCurl($xml, $url, false, $timeOut); // 请求 $result = $this->FromXml($response); // 请求结果从xml转成数组类型 // 二次签名参数 $oResult = [ 'appId' => $result['appid'], // 首次请求中的appid 'nonceStr' => $result['nonce_str'], // 首次请求中的nonce_str 'package' => 'prepay_id=' . $result['prepay_id'],// 首次请求中的prepay_id 'signType' => 'HMAC-SHA256', // 跟首次签名中的签名类型参数保持一致 'timeStamp' => (string)(time()),// 时间戳转字符串类型 ]; // 二次签名 $oResult['paySign'] = $this->MakeSign($oResult); // 调用签名 $result = json_encode($oResult); // encode数组 return $result; // 直接返回 }
3.MakeSign 签名
/** * 生成签名 * @param bool $needSignType 是否需要补signtype * @return 签名,本函数不覆盖sign成员变量,如要设置签名需要调用SetSign方法赋值 */ public function MakeSign($values, $needSignType = true) { if ($needSignType) { $sSignType = 'HMAC-SHA256'; // 可以在文档开头用枚举定义: 所有签名类型必须一致 } $sKey = 'key'; // 获取支付参数key // 签名步骤一:按字典序排序参数 ksort($values); $string = $this->ToUrlParams($values); // 签名步骤二:在string后加入KEY $string = $string . "&key=" . $sKey; // 签名步骤三:MD5加密或者HMAC-SHA256 if ($sSignType == "MD5") { $string = md5($string); } else if ($sSignType == "HMAC-SHA256") { $string = hash_hmac("sha256", $string, $sKey); } else { return "签名类型不支持!"; } // 签名步骤四:所有字符转为大写 $result = strtoupper($string); return $result; }
4.ToXml 数组参数转xml
public function ToXml($values) { if (!is_array($values) || count($values) <= 0) { return "数组数据异常!"; } $xml = "<xml>"; foreach ($values as $key => $val) { if (is_numeric($val)) { $xml .= "<" . $key . ">" . $val . "</" . $key . ">"; } else { $xml .= "<" . $key . "><![CDATA[" . $val . "]]></" . $key . ">"; } } $xml .= "</xml>"; return $xml; }
5.postXmlCurl 发送请求
/** * 以post方式提交xml到对应的接口url * * @param WxPayConfigInterface $config 配置对象 * @param string $xml 需要post的xml数据 * @param string $url url * @param bool $useCert 是否需要证书,默认不需要 * @param int $second url执行超时时间,默认30s */ private function postXmlCurl($xml, $url, $useCert = false, $second = 30) { $ch = curl_init(); $curlVersion = curl_version(); $ua = "WXPaySDK/" . self::VERSION . " (" . PHP_OS . ") PHP/" . PHP_VERSION . " CURL/" . $curlVersion['version'] . " " . $aWxpayParam['mchid']; //设置超时 curl_setopt($ch, CURLOPT_TIMEOUT, $second); $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_setopt($ch, CURLOPT_URL, $url); // curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,TRUE); // curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,2);//严格校验 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); //严格校验 curl_setopt($ch, CURLOPT_USERAGENT, $ua); // 设置header curl_setopt($ch, CURLOPT_HEADER, FALSE); // 要求结果为字符串且输出到屏幕上 curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); if ($useCert == true) { // 设置证书 // 使用证书:cert 与 key 分别属于两个.pem文件 // 证书文件请放入服务器的非web目录下 $sslCertPath = 'sslCertPath'; // 证书路径 $sslKeyPath = 'sslKeyPath'; // 证书路径 curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM'); curl_setopt($ch, CURLOPT_SSLCERT, $sslCertPath); curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM'); curl_setopt($ch, CURLOPT_SSLKEY, $sslKeyPath); } // post提交方式 curl_setopt($ch, CURLOPT_POST, TRUE); curl_setopt($ch, CURLOPT_POSTFIELDS, $xml); // 运行curl $data = curl_exec($ch); // 返回结果 if ($data) { curl_close($ch); return $data; } else { $error = curl_errno($ch); curl_close($ch); throw new WxPayException("curl出错,错误码:$error"); } }
6.FromXml 结果xml参数转数组
/** * 将xml转为array * @param string $xml * @throws WxPayException */ public function FromXml($xml) { if (!$xml) { return "xml数据异常!"; } //将XML转为array //禁止引用外部xml实体 libxml_disable_entity_loader(true); $res = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true); return $res; }
总结
注意统一下单中五个调用方法别忘了:
getNonceStr:我没贴出来,这个要自己写(0.0)
MakeSign: 这里面的key要记得替换成自己真实的参数
ToXml
postXmlCurl : 注意这里面的证书要改成自己真实的哈
FromXml
加载全部内容