纯手动打造微信H5网页内唤起微信完成支付,

底层操作类库

<?php
//微信H5支付类
//By:sevstudio

class WeiXinH5{
	const _APPID = 'wx6910e78018a20000'; //公众账号ID
	const _MCHID = '1502480000'; //商户号
	const _KEY = 'FIvAYI2CkLvaJ2Ne3qYYriRLHx180000';//商户key
	
	const _NOTIFY_URL = 'http://www.xxxx.net/m_weixin_notify.php'; //异步通知URL
	const _WAP_URL = 'http://www.xxxx.net';
	const _WAP_NAME = '名字';
	
	//发起支付,返回唤起微信支付的URL,失败返回null
	public function toPay($order_no,$total_fee,$body = 'test'){
		$arr = array(
			'appid'				=> self::_APPID,
			'mch_id'			=> self::_MCHID,
			'nonce_str'			=> $this->getNonceStr(),
			'body'				=> $body,
			'out_trade_no'		=> $order_no,
			'total_fee'			=> $total_fee, //单位:分
			'spbill_create_ip'	=> $this->get_client_ip(),
			'notify_url'		=> self::_NOTIFY_URL,
			'trade_type'		=> 'MWEB',
			'scene_info'		=>'{"h5_info": {"type":"Wap","wap_url": "'.self::_WAP_URL.'","wap_name": "'.self::_WAP_NAME.'"}}',
		);
		$info = $this->zidianxu($arr);//字典序
		$info['sign'] = $this->md5sign($info);//签名
		
		$xml = $this->ToXml($info); 
		$response = $this->curl_post("https://api.mch.weixin.qq.com/pay/unifiedorder",$xml);
		//将微信返回的XML 转换成数组
		//$objectxml = (array)simplexml_load_string($response, 'SimpleXMLElement', LIBXML_NOCDATA); 
		$objectxml = $this->FromXml($response);
		if(!$objectxml || empty($objectxml['return_code']) || empty($objectxml['return_msg'])){
			throw new Exception('通信失败');
		}
		if(empty($objectxml['mweb_url'])){
			throw new Exception($objectxml['return_msg']);
		}
		return $objectxml['mweb_url']; //该URL可唤起微信支付窗口
	}
	//获取异步通知,完成基本信息校验
	public function notify(){
		if (!isset($GLOBALS['HTTP_RAW_POST_DATA'])) {
			# 如果没有数据,直接返回失败
			throw new Exception('没有数据来源');
		}
		$xml = $GLOBALS['HTTP_RAW_POST_DATA'];
		$result = $this->FromXml($xml);
		if(!$result || !is_array($result) || count($result) <1){
			throw new Exception('数据转换失败');
		}
		if(empty($result['return_code']) || $result['return_code'] != 'SUCCESS'){
			throw new Exception('通信标识失败');
		}
		//验证签名
		$tmp = array();
		foreach($result as $k=>$v){
			if(!is_array($v) && $v != '' && $k != 'sign'){
				$tmp[$k] = $v; 
			}
		}
		ksort($tmp);
		$tmp_sign = $this->md5sign($tmp);
		if($tmp_sign != $result['sign']){
			throw new Exception('签名校验失败');
		}
		//基本信息校验
		if($result['mch_id'] != self::_MCHID){
			throw new Exception('商户ID校验失败');
		}
		return $result;
	}
	//告知微信可以停止异步通知了
	public function stopNotify(){
		$res = array(
			'return_code'	=> 'SUCCESS',
			'return_msg'	=> 'OK',
		);
		$txt = $this->ToXml($res);
		header("Content-type:text/xml;charset=utf-8");
		echo $res;
		exit();
	}
	//查询订单支付结果
	public function query($order_no){
		$arr = array(
			'appid'			=>self::_APPID,
			'mch_id'		=>self::_MCHID,
			'out_trade_no'	=>$order_no,
			'nonce_str'		=>$this->getNonceStr()
		);
		$info = $this->zidianxu($arr);//字典序
		$info['sign'] = $this->md5sign($info);//签名
		
		$xml = $this->ToXml($info); 
		$response = $this->curl_post("https://api.mch.weixin.qq.com/pay/orderquery",$xml);
		$result = $this->FromXml($response);
		
		if(!$result || !is_array($result)){
			throw new Exception('拉取支付结果失败');
		}		
		if(empty($result['return_code']) || empty($result['return_msg'])){
			throw new Exception('拉取支付结果通信失败');
		}
		if($result['return_code'] != 'SUCCESS'){
			throw new Exception($result['return_msg']);
		}
		//验证签名
		$tmp = array();
		foreach($result as $k=>$v){
			if(!is_array($v) && $v != '' && $k != 'sign'){
				$tmp[$k] = $v; 
			}
		}
		ksort($tmp);

		$tmp_sign = $this->md5sign($tmp);
		if($tmp_sign != $result['sign']){
			throw new Exception('签名验证失败');
		}
		
		return $result;
	}
	//获取随机字符串
	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;
	}
	//数组转xml格式
	public function ToXml($data){
		if(!is_array($data) || count($data) <= 0){
			throw new WxPayException("数组数据异常!");
		}
		$xml = "<xml>";
		foreach ($data as $key=>$val){
			if (is_numeric($val)){
				$xml.="<".$key.">".$val."</".$key.">";
			}else{
				$xml.="<".$key."><![CDATA[".$val."]]></".$key.">";
			}
		}
		$xml.="</xml>";
		return $xml; 
	}
	/**
     * 将xml转为array
     * @param string $xml
     */
	public function FromXml($xml)
	{	
		if(!$xml){
			return null;
		}
        //将XML转为array
        //禁止引用外部xml实体
        libxml_disable_entity_loader(true);
        return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
	}
	//数组排序
	public function zidianxu($arr){
		/*
		sort() - 以升序对数组排序
		rsort() - 以降序对数组排序
		asort() - 根据值,以升序对关联数组进行排序
		ksort() - 根据键,以升序对关联数组进行排序
		arsort() - 根据值,以降序对关联数组进行排序
		krsort() - 根据键,以降序对关联数组进行排序
		*/
		ksort($arr);
		return $arr;
	}
	//获取客户端IP
	public function get_client_ip(){
		if(isset($_SERVER)){
			if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])){
				$realip = $_SERVER['HTTP_X_FORWARDED_FOR'];
			}elseif(isset($_SERVER['HTTP_CLIENT_IP'])){
				$realip = $_SERVER['HTTP_CLIENT_IP'];
			}else{
				$realip = $_SERVER['REMOTE_ADDR'];
			}
		}else{
			if(getenv("HTTP_X_FORWARDED_FOR")){
				$realip = getenv( "HTTP_X_FORWARDED_FOR");
			}elseif (getenv("HTTP_CLIENT_IP")){
				$realip = getenv("HTTP_CLIENT_IP");
			}else{
				$realip = getenv("REMOTE_ADDR");
			}
		}
		$arr = explode(",",trim($realip));
		foreach($arr as $o){
			if(preg_match("/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/",$o)){
				return $o;
			}
		}
		return "unknown";
	}
	//获取MD5签名
	public function md5sign($info){
		$str = '';
		foreach($info as $k=>$v){
			$str.= $str == '' ? "$k=$v":"&"."$k=$v";
		}
		$str.="&key=".self::_KEY;
		return strtoupper(md5($str));
	}
	public function curl_post($postUrl,$data,$header = ""){
		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL,$postUrl);
		curl_setopt($ch, CURLOPT_POST, 1);
		curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
		curl_setopt($ch, CURLOPT_HEADER, 0);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
		if(substr($postUrl,0,5) == "https"){
			curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
			curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
		}
		if(is_array($header) && count($header)>0){
			curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
		}
		$response = curl_exec($ch);
		curl_close($ch);
		return $response;
	}
}

	
	

然后是业务操作部分,

$act = empty($_REQUEST['act']) ? '' : trim($_REQUEST['act']);

if($act == 'topay'){
	
	$order_no = trim($_POST['order_no']);
	$total_fee = intval($_POST['total_fee']);
	$body = trim($_POST['body']);
	
	$hp = new WeiXinH5();
	$result = null;
	try{
		$result = $hp->toPay($order_no,$total_fee,$body);
	}catch(Exception $e){
		__log($e->getmessage());
		ajax('失败');
	}

	ajax($result,true);
}else if($act == 'query'){
	$order_no = trim($_POST['order_no']);
	$hp = new WeiXinH5();
	$result = null;
	try{
		$result = $hp->query($order_no);
	}catch(Exception $e){
		__log($e->getmessage());
		ajax('订单支付失败');
	}
	
	if($result['trade_state'] != 'SUCCESS'){
		ajax('订单支付失败');
	}
	
	$order = $con->find("select * from #__order where #__order.pay_no='{$result['out_trade_no']}'");
	if(empty($order)){
		ajax('订单信息不存在');
	}
	if(intval($order['status_pay']) == STATUS_PAY_YES){
		ajax('订单已经支付');
	}
	
	if(intval($order['fee']) * 100 != intval($result['total_fee'])){
		ajax('订单金额不匹配');
	}
	
	//更新订单信息
	$time_pay = strtotime($result['time_end']);//订单完成支付的时间
	//更新订单步骤,省略

	ajax('订单支付成功',true);
}


ajax('');


function ajax($info,$flag=false){
	$res = array(
		'flag'	=> $flag,
		'info'	=> $info,
	);
	echo json_encode($res); exit();
}
function __log($msg){
	file_put_contents('log.txt',$msg."\t\t".date('YmdHis').PHP_EOL,FILE_APPEND);
}


微信H5支付流程简要说明:

1、新建一个页面A,通过ajax请求上面的文件 act=topay,得到一个微信的URL地址;

2、在页面上A上弹出URL地址,同时显示中间页面如下图,即可唤起微信支付;

002020.JPG

3、若支付流程走完,则默认会返回到页面A,可以选择点击上图中的第2/3行,点第2行则通过ajax请求上面的文件 act=query去抓取支付结果,并显示支付结果。当然可以点击第3行回到步骤1;

4、设置的notify页面只有在支付流程走完后才会去通知,如果支付没走完则不会通知,故需要通过步骤2种的图片所示进行引导查询支付结果。