PHP 实现微信支付 JSAPI

目录

一、首先我们来填个坑

1:支付验签失败

二、代码示例

1.请求参数配置

2.统一下单API

3.MakeSign 签名

4.ToXml 数组参数转xml

5.postXmlCurl 发送请求

6.FromXml 结果xml参数转数组

总结


一、首先我们来填个坑

1:支付验签失败

这个问题折磨了我两天,官方文档比较含糊不清。各种百度下来的方法试过之后也不尽人意,最后发现问题是没有二次签名

二次签名需要参数(代码会展示在哪里二次签名):

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                                                                                                                           
		];
		$res = $this->unifiedOrder($oInput);     // 这里我调用的统一下单
		return $res;                             // 返给前端带APPID等参数给前端去调用支付

2.微信服务层

	const SSLCERT_PATH = 'Cert证书路径';
    const SSLKEY_PATH  = 'Key证书路径';
    const MCHID        = '商户号';
    const APPID        = 'APPID';
    const KEY          = '商户支付密钥(必须配置,登录商户平台自行设置)';


    /*微信JSAPI统一下单*/
    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下的唯一标识

			'appid' 			=> self::APPID, 			        // 顶部定义
			'mch_id' 			=> self::MCHID, 			        // 顶部定义
			'spbill_create_ip' 	=> $_SERVER['REMOTE_ADDR'], 		// 终端ip
			'nonce_str' 		=> $this->getNonceStr(), 			// 随机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;                  // 直接返回
	}




	/**
	 * 生成签名
	 * @param bool $needSignType  是否需要补signtype
	 * @return 签名,本函数不覆盖sign成员变量,如要设置签名需要调用SetSign方法赋值
	 */
	public function MakeSign($values, $needSignType = true)
	{
		if ($needSignType) {
			$sSignType = 'HMAC-SHA256'; // 可以在文档开头用枚举定义: 所有签名类型必须一致
		}

		$sKey = self: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;
	}

    
    /*
     * 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;
	}


	/**
	 * 以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 	= self::SSLCERT_PATH ;    // Cert证书路径:文档顶部自定义参数名
			$sslKeyPath 	= self::SSLKEY_PATH  ;    // Key证书路径:文档顶部自定义参数名
			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");
		}
	}


	/**
	 * 将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;
	}


	/**
	 * 
	 * 产生随机字符串,不长于32位
	 * @param int $length
	 * @return 产生的随机字符串
	 */
	public function getNonceStr($length = 32)
	{
		$chars = "abcdefghijklmnopqrstuvwxyz0123456789";
		$str = "";
		for ($i = 0; $i < $length; $i++) {
			$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
		}

		return $str;
	}


	/**
	 * 格式化参数格式化成url参数
	 */
	public function ToUrlParams($urlObj)
	{
		$buff = "";
		foreach ($urlObj as $k => $v) {
			if ($k != "sign" && $v != "" && !is_array($v)) {
				$buff .= $k . "=" . $v . "&";
			}
		}

		$buff = trim($buff, "&");
		return $buff;
	}


  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值