为了阅读不累,我们仍然以故事的形式展开,本次我们服务的客户是一个手机充值店老板,他有一个公众服务号,我们要为其实现微信浏览器内支付功能。
客户给了我们一个原型图,是下面的样子。
需求并不复杂
- 点击支付按钮直接弹出如图二的支付页面,注意:不进行页面跳转。
- 输入密码支付成功。跳转到图三的成功支付页面。
准确的说就两个页面。
关键点普及
首先我对微信支付是有大概了解的(官方文档),为了将每个场景统一,微信支付提出了一个叫做 预支付交易单 的概念。
商户系统先调用接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易回话标识后再按扫码、JSAPI、APP等不同场景生成交易串调起支付。
而客户要求的通过微信浏览器这种场景发起的支付则属于JSAPI支付模式,如果你对各种场景下对应的支付模式不清楚,可以看下下面的表格,这将对我们以后开发各种微信支付很有好处。
JSAPI--公众号支付、NATIVE--原生扫码支付、APP--APP支付
使用场景 | 支付模式 |
---|---|
PC网站 | NATIVE |
微信浏览器 | JSAPI |
门店扫码 | NATIVE / JSAPI |
手机应用APP | APP |
一些配置工作
想让JSAPI支付类型的场景生效,我们需要对公众号进行一些设置。
首先进入公众号的微信支付页面设置公众号授权目录
注意:发起支付请求的链接地址,都必须在支付授权目录之下。
其次我们需要4个配置参数(AppID、AppSecret、merchant_id、key),merchant_id是你的商户号、key是你的支付key(在支付平台可以找到)。
我准备好了这些,现在开始编码~
开始啦
我设计了一个控制器 ChargeController
namespace app\modules\wechat\controllers;
use Yii;
use yii\web\Controller;
class ChargeController extends Controller {
/**
* 第一个页面
*/
public function actionIndex(){
return $this->render('index');
}
}
对应视图
// http://abc.com/wechat/charge-index.html
<h1>¥49.95</h1>
<div>
<button>点击支付</button>
</div>
现在要实现点击button后调出微信支付,对此我不惧怕,因为微信官方已经提供了相应文档 - 微信内H5调起支付,说白了就是一组能被微信浏览器识别的js代码,我们将含有AppId及预支付交易回话标识等传进去后微信支付就蹦出来了。
而这个过程一般是在页面加载过程中这些特殊的js代码就跟着添加了,这显然和当前需求有出入,我们要做的是点击支付后才调用js代码。
好,那就用ajax来实现它。
看来我要做3步事情,点击按钮后
- 通过ajax在服务器上生成一个类似充值订单的记录(状态为未支付),同时将一组已经拥有了正确参数的js代码返给浏览器。
- ajax接收了js代码,并且加载,支付弹出等等等等。
- 服务器端要有一个actionNotify方法接收微信服务器的异步通知,将上面的订单设置为已支付。
我们说做就做,开始重写视图
// charge/index.php
<h1>¥49.95</h1>
<div id="wxJs"></div>
<div>
<button id="payBtn">点击支付</button>
</div>
<script type="text/javascript">
$('#payBtn').click(function(){
var url = "/index.php?wechat/charge/pay";
$.getJSON(url,{},function(d){
if(d.done == true){
$('#wxJs').html(d.data);
}else{
}
});
});
</script>
wxJs就是用来存放服务器返回的那个可以调起微信支付的js代码,如果你还看不懂,那么阿北给你一个看图说话版再。
至关重要的pay
通过上面的编写我们实现了不跳转页面弹出微信支付布局,现在我们来编写这个至关重要的 actionPay 函数。
// ChargeController
use EasyWeChat\Foundation\Application;
use EasyWeChat\Payment\Order;
...
/**
* 该函数被前台的button触发
**/
public function actionPay(){
$charge = new Charge();
// 刷刷刷一堆代码,就生成了未付款订单。
// 通过EasyWechat来调用
$config = Yii::$app->params['WECHAT'];
$wxApp = new Application($config);
$payment = $wxApp->payment;
$notifyUrl = Yii::$app->request->getHostInfo() . Url::to(['/wechat/charge/notify']);
$attributes = [
'trade_type'=>Order::JSAPI,
'body'=>"商品描述",
'detail'=>"商品详情",
'out_trade_no'=>$charge->number,
'total_fee'=>$charge->money*100,
'notify_url'=>$notifyUrl,
'openid'=>$this->wxLogin->open_id
];
$order = new Order($attributes);
$result = $payment->prepare($order);
if ($result->return_code == 'SUCCESS' && $result->result_code == 'SUCCESS'){
$prepayId = $result->prepay_id;
$json = $payment->configForPayment($prepayId);
$html = $this->renderPartial('_wxpay',[
'json'=>$json,
'charge'=>$charge
]);
echo Json::encode(['done'=>true,'data'=>$html]);
}
}
...
EasyWechat对微信支付进行了很好的封装,我对代码里关键点进行说明
- $payment 是EasyWechat对微信支付的封装对象。
- $attributes 是我们要传递给微信服务器的订单信息,用来获取 预支付交易会话标识prepayId的。
- $payment->prepare是具体和微信服务器获取prepayId的功能实现
- $json 是$payment->configForPayment接收$prepayId后生成的json字符串,这个字符串传给特殊js代码就能调其微信支付。
关于EasyWechat对微信支付更多封装信息情况文档 https://easywechat.org/zh-cn/docs/payment.html
我想你也看到了下面的代码
$html = $this->renderPartial('_wxpay',[
'json'=>$json,
'charge'=>$charge
]);
_wxpay视图就是那种特殊js代码模板,我使用renderPartial函数得到传递完$json的js代码,而renderPartial的作用是不加载任何布局文件并且将其返回给变量 $html
再看一眼_wxpay的实现
use yii\helpers\Url;
<script type="text/javascript">
function jsApiCall() {
WeixinJSBridge.invoke(
'getBrandWCPayRequest',
<?= $json ?>,
function(res){
if(res.err_msg === 'get_brand_wcpay_request:ok'){
window.location.href = "<?= Url::to(['/wechat/charge/result','id'=>$charge->id]);?>";
}else if(res.err_msg === 'get_brand_wcpay_request:cancel'){
weui.alert('支付被取消');
}else if(res.err_msg === 'get_brand_wcpay_request:fail'){
weui.alert('支付失败');
}
}
);
}
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();
}
}
callpay();
</script>
请对比官方文档 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6 中的js代码,其实他们是一样的。
好的,现在$html代码传递给了微信浏览器,如图所示,微信支付框出来了。
支付后的事情
既然支付都弹出来了,那么我就输入了支付密码,成功后跳转到结果页面,看下_wxpay视图中的这段代码
支付完跳转到 ?r=wechat/charge/result,actionResult实现很简单
public function actionResult($id){
$model = Charge::findOne($id);
return $this->render('result',[
'model'=>$model
]);
}
这个action实现支付结果,那么我们的服务器如何知道用户已经支付成功了那?还是说中途放弃。
还记得前面代码中我们设置的 $notifyUrl 么?这货就是干这个的。
// $notifyUrl = Yii::$app->request->getHostInfo() . Url::to(['/wechat/charge/notify']);
public function actionNotify(){
$config = Yii::$app->params['WECHAT'];
$wxApp = new Application($config);
$payment = $wxApp->payment;
$response = $payment->handleNotify(function ($notify, $successful){
if ($successful) {
$order_arr = json_decode($notify, true);
$transactionId = $order_arr['transaction_id'];
// $order_arr就是微信异步通知给服务器的信息
//todo 我们的逻辑,将charge变为已支付
}
});
$response->send();
}
有一点要注意,微信服务器的异步通知是POST请求,而Yii2对POST默认使用了csrf验证,为了能接收到信息,请在ChargeController控制器里设置如下代码
public $enableCsrfValidation = false;
否则收不到通知哦,这点一定要记得。
到此为止我就完成了客户的需求,使用EasyWechat为我节省了大量时间,强烈推荐。