微信支付

前言

关于微信支付的教程,网上资源也是铺天盖地,知道了其中的原理,就能发现方法都是大同小异。微信支付 SDK 没有命名空间,那么要想将 SDK 放入到现有框架中,就需要修改一些代码。本文将演示 ThinkPHP 5.1 框架下引入微信支付。

接口申请

使用微信支付需要先申请接口权限。目前,微信公众平台仅支持认证的服务号以及认证的政府与媒体类订阅号申请支付权限。在微信支付模块按照要求依次填写资料、验证账号并签署协议。当申请完成之后,将收到微信发过来的成功通过微信支付商户资料审核的邮件,其中包含微信支付商户号及微信商户平台的登录账号及密码,至此,微信支付就申请成功了。

配置微信支付

正式开发微信支付程序之前,需要配置微信支付目录。在微信公众平台后台的“微信支付”功能中,可以找到“开发配置”模块:

在上述配置中,公众号支付的支付授权目录,是指最终发起 JSAPI 支付的页面的目录。扫码支付的回调 URL,是用于接收扫码支付时的请求信息。而刷卡支付不需要在后台配置,可以直接调用接口发起请求。上述地址需要与实际的支付程序正确对应。

设置API密钥

API 密钥是微信支付中用于签名校验身份的重要参数。商户号登录微信商户平台之后,在“帐户中心”标签中找到“API安全”,然后找到“API密钥”,其中可以看到“设置密钥”按钮。

API密钥是一个32位由数字和英文大小写字母的组合成的字符串,需要自己预先生成再填入进去。建议使用随机字符生成器生成,这样不易被别人猜出或暴力破解。

下载 SDK 并引入

在下载之前,我们先在项目中新建一个配置文件,专门用于微信支付的参数配置。在 /config 目录下新建 weixinpay.php,内容如下:

<?php

// +----------------------------------------------------------------------
// | 微信支付配置
// +----------------------------------------------------------------------
return [
    'name'        => '支付系统',
    'appid'        => 'appid',
    'appsecret'       => 'appsecret',
    'key'      => '支付密钥',
    'merchantid' => '商户号',
    'notify_url'   => '回调地址',
    'sign_type'       => 'HMAC-SHA256',
];

完成后,在微信官方网站下载 SDK 与 DEMO ,解压后保留 example 和 lib 目录,前一个是 demo,后一个是核心 SDK。example 目录中,已经包含了各种支付的使用 demo:

  • 将 phpqrcode 目录放入 /extend 下。
  • 在 /extend 目录下新建 wxpay 目录。
  • 把 SDK 的 lib 目录复制过来,并在 wxpay 下新建 cert 和 log 目录,分别用于存放证书和生成日志。
  • 把 example 中的 log.php 复制到 wxpay 下。

关于对 demo 文件的修改,只是稍作修改,目的是为了改造成适合框架使用的形式。例如我要开发 JSAPI 支付,那么可以整合 example 目录中的 jsapi.php 和 WxPay.JsApiPay.php 以及 notify.php,下面代码中是整合好的 Native 支付、JSAPI 支付和刷卡支付类,以及他们的回调,最后四个文件它们的名称都带“.”符号,分别修改它们的名称为 WxPayConfig.php、JsApiPay.php、MicroPay.php、NativePay.php,另外将 native_notify.php 和 notify.php 分别重命名为 NativeNotifyCallBack.php 和 PayNotifyCallBack.php,最后将它们几个都放入到 /extend/wxpay 中。

修改后文件如下:

下面将修改后的六个文件分别放出来:

WxPayConfig.php:

<?php
namespace wxpay;

use think\Db;
use think\facade\Config;
require_once dirname(__FILE__)."/lib/WxPay.Config.Interface.php";

class WxPayConfig extends \WxPayConfigInterface
{
	//=======【基本信息设置】=====================================
	/**
	 * TODO: 修改这里配置为您自己申请的商户信息
	 * 微信公众号信息配置
	 *
	 * APPID:绑定支付的APPID(必须配置,开户邮件中可查看)
	 *
	 * MCHID:商户号(必须配置,开户邮件中可查看)
	 *
	 */

    protected $wxData;

    public function __construct(){
        $wxData = Config::get('weixinpay.');
        $this->wxData = $wxData;
    }

	public function GetAppId()
	{
        return $this->wxData['appid'];
	}
	public function GetMerchantId()
	{
        return $this->wxData['merchantid'];
	}

	//=======【支付相关配置:支付成功回调地址/签名方式】===================================
	/**
	* TODO:支付回调url
	* 签名和验证签名方式, 支持md5和sha256方式
	**/
	public function GetNotifyUrl()
	{
        return $this->wxData['notify_url'];
	}
	public function GetSignType()
	{
        return $this->wxData['sign_type'];
	}

	//=======【curl代理设置】===================================
	/**
	 * TODO:这里设置代理机器,只有需要代理的时候才设置,不需要代理,请设置为0.0.0.0和0
	 * 本例程通过curl使用HTTP POST方法,此处可修改代理服务器,
	 * 默认CURL_PROXY_HOST=0.0.0.0和CURL_PROXY_PORT=0,此时不开启代理(如有需要才设置)
	 * @var unknown_type
	 */
	public function GetProxy(&$proxyHost, &$proxyPort)
	{
		$proxyHost = "0.0.0.0";
		$proxyPort = 0;
	}


	//=======【上报信息配置】===================================
	/**
	 * TODO:接口调用上报等级,默认紧错误上报(注意:上报超时间为【1s】,上报无论成败【永不抛出异常】,
	 * 不会影响接口调用流程),开启上报之后,方便微信监控请求调用的质量,建议至少
	 * 开启错误上报。
	 * 上报等级,0.关闭上报; 1.仅错误出错上报; 2.全量上报
	 * @var int
	 */
	public function GetReportLevenl()
	{
		return 1;
	}


	//=======【商户密钥信息-需要业务方继承】===================================
	/*
	 * KEY:商户支付密钥,参考开户邮件设置(必须配置,登录商户平台自行设置), 请妥善保管, 避免密钥泄露
	 * 设置地址:https://pay.weixin.qq.com/index.php/account/api_cert
	 *
	 * APPSECRET:公众帐号secert(仅JSAPI支付的时候需要配置, 登录公众平台,进入开发者中心可设置), 请妥善保管, 避免密钥泄露
	 * 获取地址:https://mp.weixin.qq.com/advanced/advanced?action=dev&t=advanced/dev&token=2005451881&lang=zh_CN
	 * @var string
	 */
	public function GetKey()
	{
        return $this->wxData['key'];
	}
	public function GetAppSecret()
	{
        return $this->wxData['appsecret'];
	}


	//=======【证书路径设置-需要业务方继承】=====================================
	/**
	 * TODO:设置商户证书路径
	 * 证书路径,注意应该填写绝对路径(仅退款、撤销订单时需要,可登录商户平台下载,
	 * API证书下载地址:https://pay.weixin.qq.com/index.php/account/api_cert,下载之前需要安装商户操作证书)
	 * 注意:
	 * 1.证书文件不能放在web服务器虚拟目录,应放在有访问权限控制的目录中,防止被他人下载;
	 * 2.建议将证书文件名改为复杂且不容易猜测的文件名;
	 * 3.商户服务器要做好病毒和木马防护工作,不被非法侵入者窃取证书文件。
	 * @var path
	 */
	public function GetSSLCertPath(&$sslCertPath, &$sslKeyPath)
	{
        $sslCertPath = dirname(__FILE__).'/./cert/apiclient_cert.pem';
        $sslKeyPath  = dirname(__FILE__).'/./cert/apiclient_key.pem';
	}
}

NativePay.php:

<?php
namespace wxpay;

require_once dirname(__FILE__)."/lib/WxPay.Api.php";
require_once dirname(__FILE__).'/log.php';
use wxpay\WxPayConfig;

/**
 * 扫码支付实现类
 */
class NativePay
{
	/**
	 * 生成扫描支付URL,模式一
	 * @param BizPayUrlInput $bizUrlInfo
	 */
	public function GetPrePayUrl($productId)
	{
		$biz = new \WxPayBizPayUrl();
		$biz->SetProduct_id($productId);
		try{
			$config = new WxPayConfig();
			$values = \WxpayApi::bizpayurl($config, $biz);
		} catch(Exception $e) {
			Log::ERROR(json_encode($e));
		}
		$url = "weixin://wxpay/bizpayurl?" . $this->ToUrlParams($values);
		return $url;
	}

	/**
	 * 参数数组转换为url参数
	 * @param array $urlObj
	 */
	private function ToUrlParams($urlObj)
	{
		$buff = "";
		foreach ($urlObj as $k => $v)
		{
			$buff .= $k . "=" . $v . "&";
		}

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

	/**
	 * 生成直接支付url,支付url有效期为2小时,模式二
	 * @param UnifiedOrderInput $input
	 */
	public function GetPayUrl($input)
	{
		if($input->GetTrade_type() == "NATIVE")
		{
			try{
				$config = new WxPayConfig();
				$result = \WxPayApi::unifiedOrder($config, $input);
				return $result;
			} catch(Exception $e) {
				Log::ERROR(json_encode($e));
			}
		}
		return false;
	}
}

NativeNotifyCallBack.php:

<?php

require_once dirname(__FILE__)."/lib/WxPay.Api.php";
require_once dirname(__FILE__)."/lib/WxPay.Notify.php";
require_once dirname(__FILE__).'/log.php';
use think\Db;
use wxpay\WxPayConfig;

//初始化日志
$logHandler= new \CLogFileHandler("../extend/logs/".date('Y-m-d').'.log');
$log = \Log::Init($logHandler, 15);

class NativeNotifyCallBack extends \WxPayNotify
{
	public function unifiedorder($openId, $product_id)
	{
		//统一下单
		$config = new WxPayConfig();
		$input = new \WxPayUnifiedOrder();

        $order = Db::name('order')->where(['id'=>$product_id])->find();

        $input->SetBody($order['title']);
        $input->SetAttach($order['title']);
        $input->SetOut_trade_no($config->GetMerchantId().date("YmdHis"));
        $input->SetTotal_fee($order['money']*100);
        $input->SetTime_start(date("YmdHis"));
        $input->SetTime_expire(date("YmdHis", time() + 600));
        $input->SetGoods_tag($order['goods_name']);
        $input->SetNotify_url("http://zhifu.acier.cn/pay/notify/native");
        $input->SetTrade_type("NATIVE");
        $input->SetOpenid($openId);
        $input->SetProduct_id($product_id);
		try {
			$result = \WxPayApi::unifiedOrder($config, $input);
			Log::DEBUG("unifiedorder:" . json_encode($result));
		} catch(Exception $e) {
			Log::ERROR(json_encode($e));
		}
		return $result;
	}

	/**
	*
	* 回包前的回调方法
	* 业务可以继承该方法,打印日志方便定位
	* @param string $xmlData 返回的xml参数
	*
	**/
	public function LogAfterProcess($xmlData)
	{
		\Log::DEBUG("call back, return xml:" . $xmlData);
		return;
	}

	/**
	 * @param WxPayNotifyResults $objData 回调解释出的参数
	 * @param WxPayConfigInterface $config
	 * @param string $msg 如果回调处理失败,可以将错误信息输出到该方法
	 * @return true回调出来完成不需要继续回调,false回调处理未完成需要继续回调
	 */
	public function NotifyProcess($objData, $config, &$msg)
	{
		$data = $objData->GetValues();
		//echo "处理回调";
		\Log::DEBUG("call back:" . json_encode($data));
		//TODO 1、进行参数校验
		if(!array_key_exists("openid", $data) ||
			!array_key_exists("product_id", $data))
		{
			$msg = "回调数据异常";
			\Log::DEBUG($msg  . json_encode($data));
			return false;
		}

		//TODO 2、进行签名验证
		try {
			$checkResult = $objData->CheckSign($config);
			if($checkResult == false){
				//签名错误
				\Log::ERROR("签名错误...");
				return false;
			}
		} catch(Exception $e) {
			\Log::ERROR(json_encode($e));
		}

		$openid = $data["openid"];
		$product_id = $data["product_id"];

		//TODO 3、处理业务逻辑
		//统一下单
		$result = $this->unifiedorder($openid, $product_id);
		if(!array_key_exists("appid", $result) ||
			 !array_key_exists("mch_id", $result) ||
			 !array_key_exists("prepay_id", $result))
		{
		 	$msg = "统一下单失败";
		 	\Log::DEBUG($msg  . json_encode($data));
		 	return false;
		 }

        //自行添加的逻辑处理
        $out_trade_no = $data['out_trade_no'];//商户订单号
        $money = $data['total_fee']/100;      //交易金额
        $order = Db::name('order')->where(['id'=>$product_id])->find();
        if(!empty($order) && $order['pay_status']==0){
            Db::name('order')->where(['id'=>$product_id])->update(['pay_status'=>1,'transaction_id'=>$data['transaction_id'],'openid'=>$openid,'out_trade_no'=>$out_trade_no]]);
		  	//发送通知 TODO
		}

		$this->SetData("appid", $result["appid"]);
		$this->SetData("mch_id", $result["mch_id"]);
		$this->SetData("nonce_str", \WxPayApi::getNonceStr());
		$this->SetData("prepay_id", $result["prepay_id"]);
		$this->SetData("result_code", "SUCCESS");
		$this->SetData("err_code_des", "OK");
		return true;
	}
}

JsApiPay.php:

<?php
namespace wxpay;

require_once dirname(__FILE__)."/./lib/WxPay.Api.php";
use wxpay\WxPayConfig;

/**
 *
 * JSAPI支付实现类
 * 该类实现了从微信公众平台获取code、通过code获取openid和access_token、
 * 生成jsapi支付js接口所需的参数、生成获取共享收货地址所需的参数
 *
 * 该类是微信支付提供的样例程序,商户可根据自己的需求修改,或者使用lib中的api自行开发
 *
 * @author widy
 *
 */
class JsApiPay
{
	/**
	 *
	 * 网页授权接口微信服务器返回的数据,返回样例如下
	 * {
	 *  "access_token":"ACCESS_TOKEN",
	 *  "expires_in":7200,
	 *  "refresh_token":"REFRESH_TOKEN",
	 *  "openid":"OPENID",
	 *  "scope":"SCOPE",
	 *  "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
	 * }
	 * 其中access_token可用于获取共享收货地址
	 * openid是微信支付jsapi支付接口必须的参数
	 * @var array
	 */
	public $data = null;

	/**
	 *
	 * 通过跳转获取用户的openid,跳转流程如下:
	 * 1、设置自己需要调回的url及其其他参数,跳转到微信服务器https://open.weixin.qq.com/connect/oauth2/authorize
	 * 2、微信服务处理完成之后会跳转回用户redirect_uri地址,此时会带上一些参数,如:code
	 *
	 * @return 用户的openid
	 */
	public function GetOpenid()
	{
		//通过code获得openid
		if (!isset($_GET['code'])){
			//触发微信返回code码
			$baseUrl = urlencode('http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'].$_SERVER['QUERY_STRING']);
			$url = $this->_CreateOauthUrlForCode($baseUrl);
			Header("Location: $url");
			exit();
		} else {
			//获取code码,以获取openid
		    $code = $_GET['code'];
			$openid = $this->getOpenidFromMp($code);
			return $openid;
		}
	}

	/**
	 *
	 * 获取jsapi支付的参数
	 * @param array $UnifiedOrderResult 统一支付接口返回的数据
	 * @throws WxPayException
	 *
	 * @return json数据,可直接填入js函数作为参数
	 */
	public function GetJsApiParameters($UnifiedOrderResult)
	{
		if(!array_key_exists("appid", $UnifiedOrderResult)
		|| !array_key_exists("prepay_id", $UnifiedOrderResult)
		|| $UnifiedOrderResult['prepay_id'] == "")
		{
			throw new \WxPayException("参数错误");
		}

		$jsapi = new \WxPayJsApiPay();
		$jsapi->SetAppid($UnifiedOrderResult["appid"]);
		$timeStamp = time();
		$jsapi->SetTimeStamp("$timeStamp");
		$jsapi->SetNonceStr(\WxPayApi::getNonceStr());
		$jsapi->SetPackage("prepay_id=" . $UnifiedOrderResult['prepay_id']);

		$config = new WxPayConfig();
		$jsapi->SetPaySign($jsapi->MakeSign($config));
		$parameters = json_encode($jsapi->GetValues());
		return $parameters;
	}

	/**
	 *
	 * 通过code从工作平台获取openid机器access_token
	 * @param string $code 微信跳转回来带上的code
	 *
	 * @return openid
	 */
	public function GetOpenidFromMp($code)
	{
		$url = $this->__CreateOauthUrlForOpenid($code);

		//初始化curl
		$ch = curl_init();
		$curlVersion = curl_version();
		$config = new WxPayConfig();
		$ua = "WXPaySDK/3.0.9 (".PHP_OS.") PHP/".PHP_VERSION." CURL/".$curlVersion['version']." "
		.$config->GetMerchantId();

		//设置超时
		curl_setopt($ch, CURLOPT_TIMEOUT, $this->curl_timeout);
		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;
		$config->GetProxy($proxyHost, $proxyPort);
		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);
		curl_close($ch);
		//取出openid
		$data = json_decode($res,true);
		$this->data = $data;
		$openid = $data['openid'];
		return $openid;
	}

	/**
	 *
	 * 拼接签名字符串
	 * @param array $urlObj
	 *
	 * @return 返回已经拼接好的字符串
	 */
	private function ToUrlParams($urlObj)
	{
		$buff = "";
		foreach ($urlObj as $k => $v)
		{
			if($k != "sign"){
				$buff .= $k . "=" . $v . "&";
			}
		}

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

	/**
	 *
	 * 获取地址js参数
	 *
	 * @return 获取共享收货地址js函数需要的参数,json格式可以直接做参数使用
	 */
	public function GetEditAddressParameters()
	{
		$config = new WxPayConfig();
		$getData = $this->data;
		$data = array();
		$data["appid"] = $config->GetAppId();
		$data["url"] = "http://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
		$time = time();
		$data["timestamp"] = "$time";
		$data["noncestr"] = \WxPayApi::getNonceStr();
		$data["accesstoken"] = $getData["access_token"];
		ksort($data);
		$params = $this->ToUrlParams($data);
		$addrSign = sha1($params);

		$afterData = array(
			"addrSign" => $addrSign,
			"signType" => "sha1",
			"scope" => "jsapi_address",
			"appId" => $config->GetAppId(),
			"timeStamp" => $data["timestamp"],
			"nonceStr" => $data["noncestr"]
		);
		$parameters = json_encode($afterData);
		return $parameters;
	}

	/**
	 *
	 * 构造获取code的url连接
	 * @param string $redirectUrl 微信服务器回跳的url,需要url编码
	 *
	 * @return 返回构造好的url
	 */
	private function _CreateOauthUrlForCode($redirectUrl)
	{
		$config = new WxPayConfig();
		$urlObj["appid"] = $config->GetAppId();
		$urlObj["redirect_uri"] = "$redirectUrl";
		$urlObj["response_type"] = "code";
		$urlObj["scope"] = "snsapi_base";
		$urlObj["state"] = "STATE"."#wechat_redirect";
		$bizString = $this->ToUrlParams($urlObj);
		return "https://open.weixin.qq.com/connect/oauth2/authorize?".$bizString;
	}

	/**
	 *
	 * 构造获取open和access_toke的url地址
	 * @param string $code,微信跳转带回的code
	 *
	 * @return 请求的url
	 */
	private function __CreateOauthUrlForOpenid($code)
	{
		$config = new WxPayConfig();
		$urlObj["appid"] = $config->GetAppId();
		$urlObj["secret"] = $config->GetAppSecret();
		$urlObj["code"] = $code;
		$urlObj["grant_type"] = "authorization_code";
		$bizString = $this->ToUrlParams($urlObj);
		return "https://api.weixin.qq.com/sns/oauth2/access_token?".$bizString;
	}
}

PayNotifyCallBack.php:

<?php
namespace wxpay;

use think\Db;
use wxpay\WxPayConfig;
require_once dirname(__FILE__)."/./lib/WxPay.Api.php";
require_once dirname(__FILE__)."/./lib/WxPay.Notify.php";
require_once dirname(__FILE__).'/log.php';

//初始化日志
$logHandler= new \CLogFileHandler(EXTEND_PATH."wxpay/logs/".date('Y-m-d').'.log');
$log = \Log::Init($logHandler, 15);

class PayNotifyCallBack extends \WxPayNotify
{
    //查询订单
    public function Queryorder($transaction_id)
    {
        $input = new \WxPayOrderQuery();
        $input->SetTransaction_id($transaction_id);

        $config = new WxPayConfig();
        $result = \WxPayApi::orderQuery($config, $input);
        \Log::DEBUG("query:" . json_encode($result));
        if(array_key_exists("return_code", $result)
            && array_key_exists("result_code", $result)
            && $result["return_code"] == "SUCCESS"
            && $result["result_code"] == "SUCCESS")
        {
            return true;
        }
        return false;
    }

    /**
     *
     * 回包前的回调方法
     * 业务可以继承该方法,打印日志方便定位
     * @param string $xmlData 返回的xml参数
     *
     **/
    public function LogAfterProcess($xmlData)
    {
        \Log::DEBUG("call back, return xml:" . $xmlData);
        return;
    }

    //重写回调处理函数
    /**
     * @param WxPayNotifyResults $data 回调解释出的参数
     * @param WxPayConfigInterface $config
     * @param string $msg 如果回调处理失败,可以将错误信息输出到该方法
     * @return true回调出来完成不需要继续回调,false回调处理未完成需要继续回调
     */
    public function NotifyProcess($objData, $config, &$msg)
    {
        $data = $objData->GetValues();
        //TODO 1、进行参数校验
        if(!array_key_exists("return_code", $data)
            ||(array_key_exists("return_code", $data) && $data['return_code'] != "SUCCESS")) {
            //TODO失败,不是支付成功的通知
            //如果有需要可以做失败时候的一些清理处理,并且做一些监控
            $msg = "异常异常";
            return false;
        }
        if(!array_key_exists("transaction_id", $data)){
            $msg = "输入参数不正确";
            return false;
        }

        //TODO 2、进行签名验证
        try {
            $checkResult = $objData->CheckSign($config);
            if($checkResult == false){
                //签名错误
                \Log::ERROR("签名错误...");
                return false;
            }
        } catch(Exception $e) {
            \Log::ERROR(json_encode($e));
        }

        //TODO 3、处理业务逻辑
        \Log::DEBUG("call back:" . json_encode($data));
        $notfiyOutput = array();


        //查询订单,判断订单真实性
        if(!$this->Queryorder($data["transaction_id"])){
            $msg = "订单查询失败";
            return false;
        }

        //自行添加的逻辑处理
        $out_trade_no = $data['out_trade_no'];//商户订单号
        $money = $data['total_fee']/100;      //交易金额
        $order = Db::name('order')->where(['out_trade_no'=>$out_trade_no])->find();
        if(!empty($order) && $order['pay_status']==0){
            Db::name('order')->where(['out_trade_no'=>$out_trade_no])->update(['pay_status'=>1,'transaction_id'=>$data['transaction_id']]);
            //发送通知 TODO
        }
        return true;
    }
}

MicroPay.php:

GetOut_trade_no();

        //②、接口调用成功,明确返回调用失败
        if($result["return_code"] == "SUCCESS" &&
            $result["result_code"] == "FAIL" &&
            $result["err_code"] != "USERPAYING" &&
            $result["err_code"] != "SYSTEMERROR")
        {
            return false;
        }

        //③、确认支付是否成功
        $queryTimes = 10;
        while($queryTimes > 0)
        {
            $succResult = 0;
            $queryResult = $this->query($out_trade_no, $succResult);
            //如果需要等待1s后继续
            if($succResult == 2){
                sleep(2);
                continue;
            } else if($succResult == 1){//查询成功
                return $queryResult;
            } else {//订单交易失败
                break;
            }
        }

        //④、次确认失败,则撤销订单
        if(!$this->cancel($out_trade_no))
        {
            throw new WxpayException("撤销单失败!");
        }

        return false;
    }

    /**
     *
     * 查询订单情况
     * @param string $out_trade_no  商户订单号
     * @param int $succCode         查询订单结果
     * @return 0 订单不成功,1表示订单成功,2表示继续等待
     */
    public function query($out_trade_no, &$succCode)
    {
        $queryOrderInput = new \WxPayOrderQuery();
        $queryOrderInput->SetOut_trade_no($out_trade_no);
        $config = new WxPayConfig();
        try{
            $result = \WxPayApi::orderQuery($config, $queryOrderInput);
        } catch(Exception $e) {
            Log::ERROR(json_encode($e));
        }
        if($result["return_code"] == "SUCCESS"
            && $result["result_code"] == "SUCCESS")
        {
            //支付成功
            if($result["trade_state"] == "SUCCESS"){
                $succCode = 1;
                return $result;
            }
            //用户支付中
            else if($result["trade_state"] == "USERPAYING"){
                $succCode = 2;
                return false;
            }
        }

        //如果返回错误码为“此交易订单号不存在”则直接认定失败
        if($result["err_code"] == "ORDERNOTEXIST")
        {
            $succCode = 0;
        } else{
            //如果是系统错误,则后续继续
            $succCode = 2;
        }
        return false;
    }

    /**
     *
     * 撤销订单,如果失败会重复调用10次
     * @param string $out_trade_no
     * @param 调用深度 $depth
     */
    public function cancel($out_trade_no, $depth = 0)
    {
        try {
            if($depth > 10){
                return false;
            }

            $clostOrder = new \WxPayReverse();
            $clostOrder->SetOut_trade_no($out_trade_no);

            $config = new WxPayConfig();
            $result = \WxPayApi::reverse($config, $clostOrder);


            //接口调用失败
            if($result["return_code"] != "SUCCESS"){
                return false;
            }

            //如果结果为success且不需要重新调用撤销,则表示撤销成功
            if($result["result_code"] != "SUCCESS"
                && $result["recall"] == "N"){
                return true;
            } else if($result["recall"] == "Y") {
                return $this->cancel($out_trade_no, ++$depth);
            }
        } catch(Exception $e) {
            Log::ERROR(json_encode($e));
        }
        return false;
    }
}

开始使用

SDK 部署到框架中的工作做完了,现在开始进行调用。

在 /application 下新建 pay 模块,模块下新增三个控制器,用于支付操作、支付回调和扫码支付需使用的生成二维码,分别是 Weixin.php、Notify.php、Qrcode.php,代码入下:

Weixin.php:

<?php
namespace app\pay\controller;

use think\Controller;
use think\Db;
use wxpay\JsApiPay;
use wxpay\WxPayConfig;
use wxpay\MicroPay;
use wxpay\NativePay;
require_once "../extend/wxpay/lib/WxPay.Api.php";
require_once "../extend/wxpay/log.php";

class Weixin extends Controller{

    public function index(){
        return $this->fetch();
    }

    public function jsapi(){
        $tools = new JsApiPay();
        //$openId = $tools->GetOpenid();
        $openId = "oM8go0abYYFFlJQ-Dkg-dNF2EhZg";
        $orderData = [
            'out_trade_no' => date("YmdHis").time().rand(10000000,99999999),
            'money'        => 0.01,
            'title'        => '支付标题',
            'addtime'      => time(),
            'goods_name'   => '商品名称',
            'pay_type'     => 'JSAPI',
            'openid'       => $openId
        ];
        Db::name('order')->insertGetId($orderData);
        $input = new \WxPayUnifiedOrder();
        $input->SetBody($orderData['title']);
        $input->SetAttach($orderData['title']);
        $input->SetOut_trade_no($orderData['out_trade_no']);
        $input->SetTotal_fee($orderData['money']*100);
        $input->SetTime_start(date("YmdHis"));
        $input->SetTime_expire(date("YmdHis", time() + 600));
        $input->SetGoods_tag($orderData['goods_name']);
        $input->SetTrade_type("JSAPI");
        $input->SetOpenid($openId);
        $config = new WxPayConfig();
        $order = \WxPayApi::unifiedOrder($config, $input);
        $jsApiParameters = $tools->GetJsApiParameters($order);
//        dump($jsApiParameters);
        //获取共享收货地址
        $editAddress = $tools->GetEditAddressParameters();
//        dump($editAddress);die;
        return $this->fetch('',['jsApiParameters'=>$jsApiParameters,'editAddress'=>$editAddress]);
    }

    //扫描支付【商户平台配置回调地址】
    public function native(){
        $notify = new NativePay();
        $input = new \WxPayUnifiedOrder();
        $orderData = [
            'out_trade_no' => date("YmdHis").time().rand(10000000,99999999),
            'money'        => 0.01,
            'title'        => '支付标题',
            'addtime'      => time(),
            'goods_name'   => '商品名称',
            'pay_type'     => 'NATIVE'
        ];
        $order_id = Db::name('order')->insertGetId($orderData);
        $input->SetBody($orderData['title']);
        $input->SetAttach($orderData['title']);
        $input->SetOut_trade_no($orderData['out_trade_no']);
        $input->SetTotal_fee($orderData['money']*100);
        $input->SetTime_start(date("YmdHis"));
        $input->SetTime_expire(date("YmdHis", time() + 600));
        $input->SetGoods_tag($orderData['goods_name']);
        $input->SetNotify_url("http://zhifu.acier.cn/pay/notify/native");
        $input->SetTrade_type("NATIVE");
        $input->SetProduct_id($order_id);
        $result = $notify->GetPayUrl($input); //统一下单
//        $url = $result["code_url"];
        $url = urlencode($result["code_url"]);
//        dump($url);die;
        return $this->fetch('',['url'=>urlencode($url)]);
    }

    //订单查询
    public function orderquery(){
        if(isset($_REQUEST["transaction_id"]) && $_REQUEST["transaction_id"] != ""){
            try {
                $transaction_id = $_REQUEST["transaction_id"];
                $input = new \WxPayOrderQuery();
                $input->SetTransaction_id($transaction_id);
                $config = new WxPayConfig();
                print_r(\WxPayApi::orderQuery($config, $input));
            } catch(Exception $e) {
                \Log::ERROR(json_encode($e));
            }
            exit();
        }

        if(isset($_REQUEST["out_trade_no"]) && $_REQUEST["out_trade_no"] != ""){
            try{
                $out_trade_no = $_REQUEST["out_trade_no"];
                $input = new \WxPayOrderQuery();
                $input->SetOut_trade_no($out_trade_no);
                $config = new WxPayConfig();
                print_r(\WxPayApi::orderQuery($config, $input));
            } catch(Exception $e) {
                \Log::ERROR(json_encode($e));
            }
            exit();
        }
        return $this->fetch();
    }

    //订单退款【下载证书】
    public function refund(){

        if(isset($_REQUEST["transaction_id"]) && $_REQUEST["transaction_id"] != ""){
            try{
                $transaction_id = $_REQUEST["transaction_id"];
                $total_fee = $_REQUEST["total_fee"];
                $refund_fee = $_REQUEST["refund_fee"];
                $input = new \WxPayRefund();
                $input->SetTransaction_id($transaction_id);
                $input->SetTotal_fee($total_fee);
                $input->SetRefund_fee($refund_fee);

                $config = new WxPayConfig();
                $input->SetOut_refund_no("sdkphp".date("YmdHis"));
                $input->SetOp_user_id($config->GetMerchantId());
                print_r(\WxPayApi::refund($config, $input));
            } catch(Exception $e) {
                \Log::ERROR(json_encode($e));
            }
            exit();
        }

        if(isset($_REQUEST["out_trade_no"]) && $_REQUEST["out_trade_no"] != ""){
            try{
                $out_trade_no = $_REQUEST["out_trade_no"];
                $total_fee = $_REQUEST["total_fee"];
                $refund_fee = $_REQUEST["refund_fee"];
                $input = new \WxPayRefund();
                $input->SetOut_trade_no($out_trade_no);
                $input->SetTotal_fee($total_fee);
                $input->SetRefund_fee($refund_fee);
                $config = new WxPayConfig();
                $input->SetOut_refund_no("sdkphp".date("YmdHis"));
                $input->SetOp_user_id($config->GetMerchantId());
                print_r(\WxPayApi::refund($config, $input));
            } catch(Exception $e) {
                \Log::ERROR(json_encode($e));
            }
            exit();
        }

        return $this->fetch();
    }
    //退款查询
    public function refundquery(){

        if(isset($_REQUEST["transaction_id"]) && $_REQUEST["transaction_id"] != ""){
            try{
                $transaction_id = $_REQUEST["transaction_id"];
                $input = new \WxPayRefundQuery();
                $input->SetTransaction_id($transaction_id);
                $config = new WxPayConfig();
                print_r(\WxPayApi::refundQuery($config, $input));
            } catch(Exception $e) {
                \Log::ERROR(json_encode($e));
            }
        }

        if(isset($_REQUEST["out_trade_no"]) && $_REQUEST["out_trade_no"] != ""){
            try{
                $out_trade_no = $_REQUEST["out_trade_no"];
                $input = new \WxPayRefundQuery();
                $input->SetOut_trade_no($out_trade_no);
                $config = new WxPayConfig();
                print_r(\WxPayApi::refundQuery($config, $input));
            } catch(Exception $e) {
                \Log::ERROR(json_encode($e));
            }
            exit();
        }

        if(isset($_REQUEST["out_refund_no"]) && $_REQUEST["out_refund_no"] != ""){
            try{
                $out_refund_no = $_REQUEST["out_refund_no"];
                $input = new \WxPayRefundQuery();
                $input->SetOut_refund_no($out_refund_no);
                $config = new WxPayConfig();
                print_r(\WxPayApi::refundQuery($config, $input));
            } catch(Exception $e) {
                \Log::ERROR(json_encode($e));
            }
            exit();
        }

        if(isset($_REQUEST["refund_id"]) && $_REQUEST["refund_id"] != ""){
            try{
                $refund_id = $_REQUEST["refund_id"];
                $input = new \WxPayRefundQuery();
                $input->SetRefund_id($refund_id);
                $config = new WxPayConfig();
                print_r(\WxPayApi::refundQuery($config, $input));
            } catch(Exception $e) {
                \Log::ERROR(json_encode($e));
            }
            exit();
        }

        return $this->fetch();

    }
    //下载订单
    public function download(){

        if(isset($_REQUEST["bill_date"]) && $_REQUEST["bill_date"] != ""){

            $bill_date = $_REQUEST["bill_date"];
            $bill_type = $_REQUEST["bill_type"];
            $input = new \WxPayDownloadBill();
            $input->SetBill_date($bill_date);
            $input->SetBill_type($bill_type);
            $config = new \WxPayConfig();
            $file = \WxPayApi::downloadBill($config, $input);
            echo htmlspecialchars($file, ENT_QUOTES);
            //TODO 对账单文件处理

            exit(0);
        }

        return $this->fetch();
    }

    //刷卡支付
    public function micropay(){
        if(isset($_REQUEST["auth_code"]) && $_REQUEST["auth_code"] != ""){

            $orderData = [
                'out_trade_no' => date("YmdHis").time().rand(10000000,99999999),
                'money'        => 0.01,
                'title'        => '支付标题',
                'addtime'      => time(),
                'goods_name'   => '商品名称',
                'pay_type'     => 'MicroPay',
                'auth_code'    => $_REQUEST["auth_code"]
            ];
            Db::name('order')->insertGetId($orderData);
            try {
                $auth_code = $_REQUEST["auth_code"];
                $input = new \WxPayMicroPay();
                $input->SetAuth_code($auth_code);
                $input->SetBody($orderData['title']);
                $input->SetTotal_fee($orderData['money']*100);
                $input->SetOut_trade_no($orderData['out_trade_no']);

                $microPay = new MicroPay();
                print_r($microPay->pay($input));
            } catch(Exception $e) {
                \Log::ERROR(json_encode($e));
            }
        }
        return $this->fetch();
    }

}

Notify.php:

<?php
namespace app\pay\controller;

use think\Controller;
use wxpay\WxPayConfig;
use wxpay\PayNotifyCallBack;
use wxpay\NativeNotifyCallBack;

class Notify extends Controller {
    public function jsapi(){
        $config = new WxPayConfig();
        $notify = new PayNotifyCallBack();
        $notify->Handle($config, false);
    }

    //扫码支付回调
    public function native(){
        $config = new WxPayConfig();
        $notify = new NativeNotifyCallBack();
        $notify->Handle($config, true);
    }
}

Qrcode.php:

<?php
namespace app\pay\controller;

use think\Controller;
require_once '../extend/phpqrcode/phpqrcode.php';

class Qrcode extends Controller {
    public function index(){
        $url = urldecode(input('data'));
        if(substr($url, 0, 6) == "weixin"){
            \QRcode::png($url);
        }else{
            header('HTTP/1.1 404 Not Found');
        }
    }
}

Weixin.php 中包含了扫码支付、JSAPI 支付、刷卡支付、订单查询、订单退款、退款查询、订单下载的调用方法。

下面罗列出个各方法所对应的前端模板 HTML:

index.html:

<html>
<head>
    <meta http-equiv="content-type" content="text/html;charset=utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>微信支付样例</title>
    <style type="text/css">
        ul {
            margin-left:10px;
            margin-right:10px;
            margin-top:10px;
            padding: 0;
        }
        li {
            width: 32%;
            float: left;
            margin: 0px;
            margin-left:1%;
            padding: 0px;
            height: 100px;
            display: inline;
            line-height: 100px;
            color: #fff;
            font-size: x-large;
            word-break:break-all;
            word-wrap : break-word;
            margin-bottom: 5px;
        }
        a {
            -webkit-tap-highlight-color: rgba(0,0,0,0);
            text-decoration:none;
            color:#fff;
        }
        a:link{
            -webkit-tap-highlight-color: rgba(0,0,0,0);
            text-decoration:none;
            color:#fff;
        }
        a:visited{
            -webkit-tap-highlight-color: rgba(0,0,0,0);
            text-decoration:none;
            color:#fff;
        }
        a:hover{
            -webkit-tap-highlight-color: rgba(0,0,0,0);
            text-decoration:none;
            color:#fff;
        }
        a:active{
            -webkit-tap-highlight-color: rgba(0,0,0,0);
            text-decoration:none;
            color:#fff;
        }
    </style>
</head>
<body>
<div align="center">
    <ul>
        <li style="background-color:#FF7F24"><a href="{:url('jsapi')}">JSAPI支付</a></li>
        <li style="background-color:#698B22"><a href="{:url('micropay')}">刷卡支付</a></li>
        <li style="background-color:#8B6914"><a href="{:url('native')}">扫码支付</a></li>
        <li style="background-color:#CDCD00"><a href="{:url('orderquery')}">订单查询</a></li>
        <li style="background-color:#CD3278"><a href="{:url('refund')}">订单退款</a></li>
        <li style="background-color:#848484"><a href="{:url('refundquery')}">退款查询</a></li>
        <li style="background-color:#8EE5EE"><a href="{:url('download')}">下载订单</a></li>
    </ul>
</div>
</body>
</html>

jsapi.html:


<html>
<head>
    <meta http-equiv="content-type" content="text/html;charset=utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>微信支付样例-支付</title>
    <script type="text/javascript">
  //调用微信JS api 支付
  function jsApiCall()
  {
    WeixinJSBridge.invoke(
      'getBrandWCPayRequest',
      <?php echo $jsApiParameters; ?>,
      function(res){
        WeixinJSBridge.log(res.err_msg);
        alert(res.err_code+res.err_desc+res.err_msg);
      }
    );
  }

  function callpay()
  {
      if (typeof WeixinJSBridge == "undefined"){
        if( document.addEventListener ){
            document.addEventListener('WeixinJSBridgeReady', jsApiCall, false);
        }else if (document.attachEvent){
            document.attachEvent('WeixinJSBridgeReady', jsApiCall);
            document.attachEvent('onWeixinJSBridgeReady', jsApiCall);
        }
    }else{
        jsApiCall();
    }
  }
  </script>
  <script type="text/javascript">
  //获取共享地址
  function editAddress()
  {
      WeixinJSBridge.invoke(
      'editAddress',
      <?php echo $editAddress; ?>,
      function(res){
        var value1 = res.proviceFirstStageName;
        var value2 = res.addressCitySecondStageName;
        var value3 = res.addressCountiesThirdStageName;
        var value4 = res.addressDetailInfo;
        var tel = res.telNumber;
        alert(value1 + value2 + value3 + value4 + ":" + tel);
      }
    );
  }

  window.onload = function(){
    if (typeof WeixinJSBridge == "undefined"){
        if( document.addEventListener ){
            document.addEventListener('WeixinJSBridgeReady', editAddress, false);
        }else if (document.attachEvent){
            document.attachEvent('WeixinJSBridgeReady', editAddress);
            document.attachEvent('onWeixinJSBridgeReady', editAddress);
        }
    }else{
        editAddress();
    }
  };

  </script>
</head>
<body>
    <br/>
    <font color="#9ACD32"><b>该笔订单支付金额为<span style="color:#f00;font-size:50px">1分</span>钱</b></font><br/><br/>
    <div align="center">
        <button style="width:210px; height:50px; border-radius: 15px;background-color:#FE6714; border:0px #FE6714 solid; cursor: pointer;  color:white;  font-size:16px;" type="button" onclick="callpay()" >立即支付</button>
    </div>
</body>
</html>

native.html:


<html>
<head>
    <meta http-equiv="content-type" content="text/html;charset=utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>微信扫码支付</title>
</head>
<body>
<div style="margin-left: 10px;color:#556B2F;font-size:30px;font-weight: bolder;">扫码支付</div><br/>
<img alt="微信扫一扫支付" src="{:url('qrcode/index',array('data' => $url))}" style="width:150px;height:150px;"/>
<div style="color:#ff0000"><b>微信扫一扫支付</b></div>
</body>
</html>

orderquery.html:


<html>
<head>
    <meta http-equiv="content-type" content="text/html;charset=utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1" /> 
    <title>微信支付样例-订单查询</title>
</head>
<body>  
	<form action="#" method="post">
        <div style="margin-left:2%;color:#f00">微信订单号和商户订单号选少填一个,微信订单号优先:</div><br/>
        <div style="margin-left:2%;">微信订单号:</div><br/>
        <input type="text" style="width:96%;height:35px;margin-left:2%;" name="transaction_id" /><br /><br />
        <div style="margin-left:2%;">商户订单号:</div><br/>
        <input type="text" style="width:96%;height:35px;margin-left:2%;" name="out_trade_no" /><br /><br />
		<div align="center">
			<input type="submit" value="查询" style="width:210px; height:50px; border-radius: 15px;background-color:#FE6714; border:0px #FE6714 solid; cursor: pointer;  color:white;  font-size:16px;" type="button" onclick="callpay()" />
		</div>
	</form>
</body>
</html>

refund.html:


<html>
<head>
    <meta http-equiv="content-type" content="text/html;charset=utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1" /> 
    <title>微信支付样例-退款</title>
</head>

<body>  
	<form action="#" method="post">
        <div style="margin-left:2%;color:#f00">微信订单号和商户订单号选少填一个,微信订单号优先:</div><br/>
        <div style="margin-left:2%;">微信订单号:</div><br/>
        <input type="text" style="width:96%;height:35px;margin-left:2%;" name="transaction_id" /><br /><br />
        <div style="margin-left:2%;">商户订单号:</div><br/>
        <input type="text" style="width:96%;height:35px;margin-left:2%;" name="out_trade_no" /><br /><br />
        <div style="margin-left:2%;">订单总金额(分):</div><br/>
        <input type="text" style="width:96%;height:35px;margin-left:2%;" name="total_fee" /><br /><br />
        <div style="margin-left:2%;">退款金额(分):</div><br/>
        <input type="text" style="width:96%;height:35px;margin-left:2%;" name="refund_fee" /><br /><br />
		<div align="center">
			<input type="submit" value="提交退款" style="width:210px; height:50px; border-radius: 15px;background-color:#FE6714; border:0px #FE6714 solid; cursor: pointer;  color:white;  font-size:16px;" type="button" onclick="callpay()" />
		</div>
	</form>
</body>
</html>

refundquery.html:


<html>
<head>
    <meta http-equiv="content-type" content="text/html;charset=utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1" /> 
    <title>微信支付样例-查退款单</title>
</head>

<body>  
	<form action="#" method="post">
        <div style="margin-left:2%;color:#f00">微信订单号、商户订单号、微信订单号、微信退款单号选填至少一个,微信退款单号优先:</div><br/>
        <div style="margin-left:2%;">微信订单号:</div><br/>
        <input type="text" style="width:96%;height:35px;margin-left:2%;" name="transaction_id" /><br /><br />
        <div style="margin-left:2%;">商户订单号:</div><br/>
        <input type="text" style="width:96%;height:35px;margin-left:2%;" name="out_trade_no" /><br /><br />
        <div style="margin-left:2%;">商户退款单号:</div><br/>
        <input type="text" style="width:96%;height:35px;margin-left:2%;" name="out_refund_no" /><br /><br />
        <div style="margin-left:2%;">微信退款单号:</div><br/>
        <input type="text" style="width:96%;height:35px;margin-left:2%;" name="refund_id" /><br /><br />
		<div align="center">
			<input type="submit" value="查询" style="width:210px; height:50px; border-radius: 15px;background-color:#FE6714; border:0px #FE6714 solid; cursor: pointer;  color:white;  font-size:16px;" type="button" onclick="callpay()" />
		</div>
	</form>
</body>
</html>

download.html:


<html>
<head>
    <meta http-equiv="content-type" content="text/html;charset=utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1" /> 
    <title>微信支付样例-下载订单</title>
</head>
<body>  
	<form action="#" method="post">
        <div style="margin-left:2%;">对账日期:</div><br/>
        <input type="text" style="width:96%;height:35px;margin-left:2%;" name="bill_date" /><br /><br />
        <div style="margin-left:2%;">账单类型:</div><br/>
        <select style="width:96%;height:35px;margin-left:2%;" name="bill_type">
		  <option value ="ALL">所有订单信息</option>
		  <option value ="SUCCESS">成功支付的订单</option>
		  <option value="REFUND">退款订单</option>
		  <option value="REVOKED">撤销的订单</option>
		</select><br /><br />
       	<div align="center">
			<input type="submit" value="下载订单" style="width:210px; height:50px; border-radius: 15px;background-color:#FE6714; border:0px #FE6714 solid; cursor: pointer;  color:white;  font-size:16px;" type="button" onclick="callpay()" />
		</div>
	</form>
</body>
</html>

micropay.html:


<html>
<head>
    <meta http-equiv="content-type" content="text/html;charset=utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>微信支付样例-刷卡支付</title>
</head>

<body>
	<form action="#" method="post">
        <div style="margin-left:2%;">商品描述:</div><br/>
        <input type="text" style="width:96%;height:35px;margin-left:2%;" readonly value="刷卡测试样例-支付" name="auth_code" /><br /><br />
        <div style="margin-left:2%;">支付金额:</div><br/>
        <input type="text" style="width:96%;height:35px;margin-left:2%;" readonly value="1分" name="auth_code" /><br /><br />
        <div style="margin-left:2%;">授权码:</div><br/>
        <input type="text" style="width:96%;height:35px;margin-left:2%;" name="auth_code" /><br /><br />
       	<div align="center">
			<input type="submit" value="提交刷卡" style="width:210px; height:50px; border-radius: 15px;background-color:#FE6714; border:0px #FE6714 solid; cursor: pointer;  color:white;  font-size:16px;" type="button" onclick="callpay()" />
		</div>
	</form>
</body>
</html>

上述演示代码,可进行微信支付的测试。

发表评论

发表评论

沙发空缺中,还不快抢~