- 1. 部署后端服务代码
- 2. 绑定域名
- 3. 登录微信公众号后台,设置服务器配置
- 4. 后台代码配置
- 5. 开发获取jsSDK配置的接口
- 6. 登录微信公众号管理后台设置各种域名
- 7. 开发获取用户open_id的接口
- 8. 开发获取支付配置的接口
- 9. 开发微信支付前端页面
- 10. 处理微信支付成功的回调
本文总结了项目中开发微信公众号支付功能的核心流程,主要包括:
- 开发微信支付的各种账号、域名的配置;
- 获取微信用户信息(open_id);
- 下发微信jsSDK配置的接口;
- 下发微信支付配置的接口;
- 一个js调用微信支付的前端页面demo;
- 处理微信支付结果回调;
1. 部署后端服务代码
登录服务器(比如阿里云ecs):
- 拉代码
- 新增nginx vhost配置
mobile.conf
server { listen 80; server_name mobile.demo.com; // ... 详略
- 绑定host:
0.0.0.0 mobile.demo.com
- reload nginx:
nginx -s reload
2. 绑定域名
demo.com A记录:
mobile 106.14.**.**
3. 登录微信公众号后台,设置服务器配置
url: http://mobile.demo.com/wechat/callback // 主要用于处理微信事件回调,比如用户关注、发消息等,本例用不到
token: ***
EncodingAESKey : 随机
4. 后台代码配置
后台项目使用Phalcon框架实现,以下为config.ini文件的配置。 其他语言、框架的配置类似,只要能够被后端代码读取就行,这里只是用来说明需要哪些配置项。
(1)配置文件
[wechat]
DEBUG = ; 为true时,可以打印一些log
WECHAT_APPID = ; 微信公众号的APPID
WECHAT_SECRET = ; 微信公众号的SECRET
WECHAT_TOKEN = ; 微信公众号的TOKEN
WECHAT_AES_KEY = ; 微信公众号的AES_KEY
MERCHAT_ID = ; 商户平台ID
MERCHAT_KEY = ; 商户平台API密钥
CERT_PATH = ; 商户平台apiclient_cert.pem证书位置(绝对路径)
KEY_PATH = ; 商户平台apiclient_key.pem证书位置(绝对路径)
NOTIFY_URL = ; 默认的支付成功回调URL,可以在实际调用时覆盖
(2)注册DI服务
后台项目使用Phalcon框架实现,以下为\Phalcon\Mvc\Micro的DI配置,其他语言、框架的配置类似。
<?php
// ...
$di->setShared('wechat', function () use ($di) {
$options = [
'debug' => $di->get('config')->wechat['DEBUG'],
'app_id' => $di->get('config')->wechat['WECHAT_APPID'],
'secret' => $di->get('config')->wechat['WECHAT_SECRET'],
'token' => $di->get('config')->wechat['WECHAT_TOKEN'],
// 'aes_key' => null, // 可选
'log' => [
'level' => 'debug',
'file' => DIR_LOG . '/easywechat.log',
],
'oauth' => [
'scopes' => ['snsapi_userinfo'], // 公众平台网页授权获取用户信息
'callback' => '/wechat/authcallback', // 获取用户信息后的回调页面
],
'payment' => [
'merchant_id' => $di->get('config')->wechat['MERCHAT_ID'],
'key' => $di->get('config')->wechat['MERCHAT_KEY'],
'cert_path' => $di->get('config')->wechat['CERT_PATH'],
'key_path' => $di->get('config')->wechat['KEY_PATH'],
'notify_url' => $di->get('config')->wechat['NOTIFY_URL'],
],
];
$wechatApplication = new Application($options);
return $wechatApplication;
});
5. 开发获取jsSDK配置的接口
使用的easyWechat开源库,在phalcon DI中注册实例为wechat(读取配置,返回实例对象,详细代码略)。
<?php
// ...
public function jsSdkAction()
{
$this->wechat->js->setUrl(urldecode($this->input['url']));
// getBrandWCPayRequest为需要调用的wx js接口
return $this->responseData($this->wechat->js->config(
$apiArray = ['getBrandWCPayRequest',]
));
}
测试: http://mobile.demo.com/wechat/jsSdk 返回:
{
"success": true,
"data": "{\"debug\":false,\"beta\":false,\"appId\":\"wx2dbfcf8d32f00e22\",\"nonceStr\":\"H1Tz1QW9xi\",\"timestamp\":1491669904,\"url\":\"http:\\/\\/mobile.demo.com\\/wechat\\/jsSdk\",\"signature\":\"250eb980e3326bc502384193a224af969965cfbf\",\"jsApiList\":[\"getBrandWCPayRequest\"]}",
"code": 1,
"message": "成功"
}
猜测前端拿到数据后,通过wxJsBridge唤起微信支付时,微信会通过nonceStr加其他数据生成签名后与signature进行比较来判断真实性。
6. 登录微信公众号管理后台设置各种域名
- 设置
js接口安全域名
mobile.demo.com - 设置
业务域名
mobile.demo.com // 这样微信在访问该域名时就不会弹出警告信息,影响排版 授权回调页面域名
mobile.demo.com // 用户在网页授权页同意授权给公众号后,微信会将授权数据传给一个回调页面,回调页面需在此域名下,以确保安全可靠。- 公众号支付-
支付授权目录
http://mobile.demo.com/
7. 开发获取用户open_id的接口
因为调用微信支付统一下单接口需要open_id,所以需要获取用户的open_id。大体步骤如下:
(1)开发获取用户信息的中间页路由
<?php
// ...
// wechat/authpage
public function authpageAction()
{
/*
用户将实际需要访问的页面的路由作为参数来访问该路由
比如需要访问http://mobile.demo.com/jsWxPayDemo.html,而该页面需要用到open_id,那么实际的访问地址为:
http://mobile.demo.com/wechat/authpage?redirect=jsWxPayDemo.html
*/
$page = $this->input[self::REDIRECT_PAGE];
if (empty($page)) {
$page = '';
}
// 判断open_id是否已存在,只有在不存在的情况下才需要获取
$openId = $this->cookieData['open_id'];
if (!empty($openId)) {
// open_id已存在,则直接跳转相应的页面
$this->redirectPage($page);
}
else {
// open_id不存在,需要走获取open_id的流程:
// 将当期用户需要访问的页面路由存入session中;
$this->session->set(self::REDIRECT_PAGE, $page);
// 重定向至微信OAuth认证页面
return $this->wechat->oauth->redirect()->send();
}
}
(2)处理微信获取用户信息后的回调
<?php
// ...
// wechat/authcallback
public function authcallbackAction()
{
$openId = '';
try {
$oauth = $this->wechat->oauth;
$user = $oauth->user();
$openId = $user->getId();
} catch (\Exception $e) {
$this->log->error('openId获取失败!');
$this->log->error(json_encode($e));
}
// 将获取到的微信用户信息存入cookie中
CommonBusiness::instance()->updateCookies(CookieKeys::COOKIES_USER_INFO, [
'open_id' => $openId,
]);
// 从session中获取用户的实际访问路由
$page = $this->session->get(self::REDIRECT_PAGE);
// 重定向至用户路由
$this->redirectPage($page);
}
8. 开发获取支付配置的接口
本例是通过微信支付充值平台点数。
<?php
// ...
// wechat/chargePayConfig
public function chargePayConfigAction()
{
// 判断登录态 & 获取当前用户信息
$userInfo = UserBusiness::instance()->getCurrentUser();
if(empty($userInfo)){
return $this->responseError('请登录!');
}
// 获取充值点数
$chance = intval($this->input['chance']);
if($chance < 1){
return $this->responseError('充值点数非法!');
}
// 新建充值记录信息(充值记录类似订单)
$chargeInfo = new ChanceChargeInfo();
$chargeInfo->chance = $this->input['chance'];
$chargeInfo->userId = $userInfo->id;
$chargeInfo->paidAmount = $chance * 1; // 1个充值点数 = 1分钱人民币
$chargeInfo->status = ChanceChargeStatus::UNPAID; // 充值记录的状态为待付款
$service = new ChanceChargeInfoService();
$ret = $service->add($chargeInfo);
if(!$ret){
return $this->responseError('充值记录创建失败!');
}
// 获取本次充值记录的微信支付配置(最终通过调用微信支付统一下单接口得到)
$ret = WechatBusiness::instance()->getChargePayConfig($chargeInfo);
if(false === $ret){
return $this->responseError('充值订单创建失败!');
}
return $this->responseData($ret);
}
实际获取微信支付配置的代码如下:
<?php
// ...
public function getChargePayConfig(ChanceChargeInfo $info)
{
// 设置参数
$attributes = [
'trade_type' => 'JSAPI', // 通过jsSDK拉起微信支付
'body' => '充值测试',
'detail' => '充值支付详情',
'out_trade_no' => $info->id, // 重要!用于支付成功后回调时判断是哪个订单
'total_fee' => $info->paidAmount, // 单位:分
'notify_url' => $this->config->env['host'].'/callback/chargePaid', // 重要!用于告诉微信,支付成功后的回调
'openid' => $this->cookieData['open_id'], // 用户的open_id
];
// 创建微信支付订单
$order = new Order($attributes);
// 获取支付配置
$ret = $this->wechat->payment->prepare($order);
if ($ret->return_code == 'SUCCESS' && $ret->result_code == 'SUCCESS') {
return $this->wechat->payment->configForJSSDKPayment($ret->prepay_id);
} else {
return FALSE;
}
}
9. 开发微信支付前端页面
本节参考微信官方文档写了一个最基本的微信支付前端页面:/jsWxPayDemo.html
因为微信支付需要获取微信用户信息(open_id),所以不能直接跳转相应的支付页面,而是应该先跳转获取微信用户信息的中间页,根据前文,实际的跳转地址为:http://mobile.demo.com/wechat/authpage?redirect=jsWxPayDemo.html,可以将该地址配置为微信公众号的菜单。
大体的工作流程分为两步:
- (1)调用jsSDK配置下发接口,获取到微信jsSDK配置后,配置wx对象;
- (2)提供一个触发支付的方式(点击按钮),在事件触发后,调用下发微信支付配置的接口,用获取到的数据拉起微信支付; 全文如下:
<html>
<head>
<script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script type="text/javascript" src="http://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
</head>
<body>
<script>
$(document).ready(function () {
console.log(wx);
var wxPayConfig = null;
var wxJsConfig = null;
// ====================== jsSDK Config ======================
//var currUrl = 'http://mobile.bz.com/jsWxPayDemo.html';
var currUrl = 'http://mobile.demo.com/jsWxPayDemo.html';
// 先请求后端接口,获取jsSDK的配置信息
// url为当前页面的url,在使用了react router的情况下,可能是根url,而不是前端的url
$.post("/wechat/jsSdkConfig",
{url: currUrl},
function (ret, status) {
// todo 错误处理
wxJsConfig = JSON.parse(ret.data);
// debug
alert(JSON.stringify(wxJsConfig));
// 配置wx对象,这样才能使用wxJsBridge的接口,包括支付、定位、扫码等等
wx.config({
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: wxJsConfig.appId, // 必填,公众号的唯一标识
timestamp: wxJsConfig.timestamp, // 必填,生成签名的时间戳
nonceStr: wxJsConfig.nonceStr, // 必填,生成签名的随机串
signature: wxJsConfig.signature,// 必填,签名,见附录1
jsApiList: wxJsConfig.jsApiList // 必填,需要使用的JS接口列表,所有JS接口列表见附录2
});
}
);
// config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。
wx.ready(function () {
alert('config ready!');
// todo
});
// config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
wx.error(function (res) {
alert('config error!');
// todo
});
$("#btn").click(function () {
// ====================== wxpay Config ======================
// 获取支付配置(后台会创建预支付订单,微信支付成功后回调处理订单状态)
$.post("/wechat/chargePayConfig",
{chance: 1},
function (ret, status) {
// todo 错误处理
wxPayConfig = ret.data;
// debug
alert(JSON.stringify(wxPayConfig));
// 发起支付
wx.chooseWXPay({
timestamp: wxPayConfig.timestamp, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
nonceStr: wxPayConfig.nonceStr, // 支付签名随机串,不长于 32 位
package: wxPayConfig.package, // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=***)
signType: wxPayConfig.signType, // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
paySign: wxPayConfig.paySign, // 支付签名
success: function (res) {
// 支付成功后的回调函数,比如展示支付成功
// todo
alert('支付成功!');
}
});
}
);
});
});
</script>
<br/><br/><br/>
<button id="btn">支付测试</button>
</body>
</html>
10. 处理微信支付成功的回调
<?php
// ...
// callback/chargePaid
public function chargePaidAction()
{
// $this->log->info($this->request->get());
$response = ChanceBusiness::instance()->paidCallback();
$response->send();
}
实际的处理逻辑:
<?php
// ...
public function paidCallback()
{
$response = $this->wechat->payment->handleNotify(function ($notify, $successful) {
// 获取充值订单
$chanceChargeInfoService = new ChanceChargeInfoService();
$chanceChargeInfo = $chanceChargeInfoService->findById($notify->out_trade_no);
if (!$chanceChargeInfo) {
return FALSE; // 通知微信,充值订单不存在
}
// 如果已支付,或者已取消,则跳过(因为可能多次通知)
if ($chanceChargeInfo->status != ChanceChargeStatus::UNPAID) {
return TRUE;
}
// 更新用户充值信息
$chanceInfoService = new ChanceInfoService();
$chanceInfo = new ChanceInfo();
$chanceInfo->userId = $chanceChargeInfo->userId;
$chanceInfo = $chanceInfoService->findUnique($chanceInfo);
if (empty($chanceInfo)) {
// 出错
if (FALSE === $chanceInfo) {
return FALSE;
}
// 尚无记录,新建一条
$chanceInfo = new ChanceInfo();
$chanceInfo->userId = $chanceChargeInfo->userId;
$chanceInfo->chance = 0;
$chanceInfo = $chanceInfoService->add($chanceInfo);
if (!$chanceInfo) {
return FALSE;
}
}
// 使用事务更新相关订单
$transactionConn = TransactionManager::start($chanceChargeInfoService, $chanceInfoService);
if ($successful) { // 支付成功
$chanceChargeInfo->status = ChanceChargeStatus::PAID;
$chanceChargeInfo->transactionId = $notify->transaction_id;
$chanceChargeInfo->remark = json_encode($notify);
$chanceInfo->chance += $chanceChargeInfo->chance;
} else { // 支付失败
$chanceChargeInfo->status = ChanceChargeStatus::CANCELED;
$chanceChargeInfo->remark = '支付失败';
}
// 修改充值订单状态
if (!$chanceChargeInfoService->update($chanceChargeInfo)) {
TransactionManager::rollback($transactionConn);
$this->log->error('充值记录付款回调失败: ' . json_encode($chanceChargeInfo));
return FALSE;
}
// 修改用户当前点数
if (!$chanceInfoService->update($chanceInfo)) {
TransactionManager::rollback($transactionConn);
$this->log->error('用户充值点数更新失败: ' . json_encode($chanceInfo));
return FALSE;
}
TransactionManager::commit($transactionConn);
return TRUE;
});
return $response;
}
$notify的内容:
{
"appid": "wx2dbfcf8d32f00e22",
"bank_type": "CFT",
"cash_fee": "1",
"fee_type": "CNY",
"is_subscribe": "Y",
"mch_id": "**",
"nonce_str": "58e9dd0b356e4",
"openid": "omSPewKRJuP0oIpJFVGdB-ntnOJo",
"out_trade_no": "60",
"result_code": "SUCCESS",
"return_code": "SUCCESS",
"sign": "7A5B833C1645D0A7BBBD1E84F7950FB0",
"time_end": "20170409150452",
"total_fee": "1",
"trade_type": "JSAPI",
"transaction_id": "4008012001201704096413270224"
}