主流程
- 微信公众号支付课程介绍
- 开发微信支付的前期准备
- 微信公众号支付流程分析
- 微信签名算法分析与实战
- 如何获取用户Openid
- 统一下单API原理分析与实现
- 构建JSAPI支付请求
- 支付通知的处理
2. 微信支付前期的准备
公众平台 https://mp.weixin.qq.com/
商户平台 https://pay.weixin.qq.com/
1. 开通微信支付的流程
2.配置授权目录和域名
3. 获取开发相关的配置信息
开发者配置信息
开发者ID APPID | 在公众平台—>开发->基本配置 可查看(无法修改) |
开发者密码 SECRET | 在公众平台 -> 开发 -> 基本配置 可设置或者重置 |
商户号 MCHID | 在商户平台 -> 产品中心 ->开发配置中可查看 |
商户支付秘钥 KEY | 在商户平台 -> 账户中心 -> API安全中设置 |
证书文件 | 在 商户平台 -> 账户中心 -> API安全 下载证书 |
3. 微信公众号支付流程分析
开发文档: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_1
微信支付流程: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_4
4. 微信签名算法分析与实战
统一下单 api: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1
源码放在 tp5中
Wxbase.php
<?php
// +----------------------------------------------------------------------
// | User: zq
// +----------------------------------------------------------------------
// | Time: 2020/3/22 2:01 下午
// +----------------------------------------------------------------------
namespace app\api\controller;
use think\Controller;
use think\Db;
use think\Request;
class Wxbase extends Controller
{
# 微信签名校验类
private $appid = 'xxx';
private $key = "xxx";
private $mch_id = 'xxx';
/*
* $arr = ['appid'=>'dfggg', 'mch_id'=>'sdgfgd', 'body'=>'88333']
*/
//解决签名中有中文的情况
public function arrToUrl($arr)
{
return urldecode(http_build_query($arr));
}
//将一个不带签名的数组设置上签名
public function setSign($arr)
{
//1. 获取签名
$sign = $this->getSign($arr);
//2. 加上签名
$arr['sign'] = $sign;
//3. 返回数组
return $arr;
}
//生成签名
public function getSign($arr)
{
//1. 对参数按照key=value的格式,并按照参数名ASCII字典序排序生成字符串
//1.1 去除空值
$arr = array_filter($arr);
//1.2 如果其中含有签名,则要去掉签名
if (isset($arr['sign'])) {
unset($arr['sign']);
}
//1.3 按照参数名ASCII字典序排序生成字符串
ksort($arr);
//1.4 生成url格式的字符串,防止里面有中文,所以单独写了一个方法
$str = $this->arrToUrl($arr);
//2. 连接商户key
$str = $str . '&key=' . $this->key;
//3. 生成sign并转成大写,并返回签名
return strtoupper(md5($str));
}
//验证签名, 验证签名接受的数组,比之前生成签名的数组多了一项(会在数组最后,这一项就是签名)
/*
* $arr = [
* ...
* 'sign' => 'C9B6CE4BD1307D74B5D98A247DA0F27B'
* ];
*/
//我们在得到签名的函数中,已经做了处理,如果有签名,把签名去掉,所以可以直接获取新的签名和数组中的最后一项签名做对比
public function checkSign($arr)
{
//1. 生成签名
$sign = $this->getSign($arr);
//2. 验证签名
if ($sign == $arr['sign']) {
return true;
} else {
return false;
}
}
}
Wxpay.php
<?php
// +----------------------------------------------------------------------
// | User: zq
// +----------------------------------------------------------------------
// | Time: 2020/3/22 2:12 下午
// +----------------------------------------------------------------------
namespace app\api\controller;
use app\api\controller\Wxbase;
use think\Db;
use think\Request;
class Wxpay extends Wxbase
{
public function __construct(Request $request = null)
{
parent::__construct($request);
}
public function testpay()
{
$arr = [
'appid' => 'dfggg',
'mch_id' => 'sdgfgd',
'body' => '88333'
];
//设置签名
$arr = $this->setSign($arr);
//验证签名
dump($this->checkSign($arr));
}
}
5. 如何获取用户Openid
- 获取用户openid
- 构建原始数据
- 加入签名
- 调用统一下单API
- 获取到prepay_id
文档: https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html#0
这里只需要openid,所以用 静默(snsapi_base)的方式即可。
如下代码,这里用了一个很巧妙的地方,就是用 code做判断,如果有则获取到openid,如果没有,先去获取code,然后跳转到自身地址,又会做 code判断,这时候有了,就直接获取openid了。
//获取opeind
//文档 https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html#0
public function getOpenId(Request $request = null)
{
//1. 如果session中存在了openid,则直接返回, 如果不存在则去获取openid
if (input('session.openid')) {
return input('session.openid');
} else {
// 2. 静默获取openid
// 2.1 用户访问一个地址先获取code (页面有跳转)
// 2.2 根据code获取到openid
if (!input('get.code')) {
//构建跳转地址 跳转
// 获取当前地址,下面的redirect_uri,就是访问下面的$url会跳转到当前地址来
$request = Request::instance();
$self_url = $request->url(True);
// 拼接微信api文档中指定的地址
$url = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=' . $this->appid . '&redirect_uri=' . $self_url . '&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect';
// 跳转
header('location:' . $url);
} else {
//调用接口获取 openid
$code_url = 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=' . $this->appid . '&secret=' . $this->key . '&code=' . input('get.code') . '&grant_type=authorization_code';
//请求这个地址,返回一个含有openid的数组
$data = http_curl($code_url);
//把openid 保存到session中
input('session.openid', $data['openid']);
//返回openid
return $data['openid'];
}
}
}
6. 统一下单API原理分析与实现
文档: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1
- 构建原始数据
- 加入签名
- 将数据转为 xml
- 发送xml格式的数据到接口地址
//调用统一下单api
public function unifiedOrder()
{
/*
* 构建原始数据
* 加入签名
* 将数据转为 xml
* 发送xml格式的数据到接口地址
*/
//1.构建原始数据
$parms = [
'appid' => $this->appid, //公众号id
'mch_id' => $this->mch_id, //商户id,
'nonce_str' => md5(time()), //随机字符串
'body' => '公众号测试', //商品描述
'out_trade_no' => '1119', //商户订单号
'total_fee' => '2', //标价金额
'spbill_create_ip' => $_SERVER['REMOTE_ADDR'], //终端IP
'notify_url' => url('notify'), //通知地址
'trade_type' => 'JSAPI', //交易类型
'product_id' => '1119', //商品ID
'openid' => $this->getOpenId()
];
//2.加入签名
$parms = $this->setSign($parms);
//3.将数据转为 xml
$xmldata = ArrToXml($parms);
//4.发送xml格式的数据到接口地址
$url = 'https://api.mch.weixin.qq.com/pay/unifiedorder'; //统一下单api地址
//5. 返回的结果如下,也是xml格式的数据
$resdata = postXml($url, $xmldata);
//6. 把xml转为数组,prepay_id就在数组中
$resdata = XmlToArr($resdata);
dump($resdata);
}
上面的 商品订单号,和商品id,都是自己定义的。特别是商品订单号,在自己的项目中,要建立数据表,先新建一个商品订单号,然后再进行下面操作。
7. 构建JSAPI支付请求
微信内H5调起支付: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6
//获取公众号支付所需要的json数据
public function getJsParms($prepay_id)
{
$params = [
'appId' => $this->appid,
'timeStamp' => '"'.time().'"',
'nonceStr' => md5(time()),
'package' => 'prepay_id=' . $prepay_id,
'signType' => 'MD5'
];
//获取签名
$params['paySign'] = $this->getSign($params);
//返回一个json数据
return json_encode($params);
}
构建JSAPI支付请求
<script>
function onBridgeReady(){
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {$json},
function(res){
if(res.err_msg == "get_brand_wcpay_request:ok" ){
// 使用以上方式判断前端返回,微信团队郑重提示:
//res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
}
});
}
if (typeof WeixinJSBridge == "undefined"){
if( document.addEventListener ){
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
}else if (document.attachEvent){
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
}else{
onBridgeReady();
}
</script>
8. 支付通知的处理
- 获取通知数据 -> 转换为数组
- 验证签名 (使用以前的方法)
- 验证业务结果(return_code 和 result_code)
- 验证订单号和金额 (out_trade_no total_fee)
- 记录日志 修改订单状态,给用户发货
#支付成功之后的通知,支付成功之后会返回到这个地址,得到的数据是xml格式的.
/*
* 1.获取通知数据 -> 转换为数组
* 2.验证签名 (使用以前的方法)
* 3.验证业务结果(return_code 和 result_code)
* 4.验证订单号和金额 (out_trade_no total_fee)
* 5.记录日志 修改订单状态,给用户发货
*/
public function notify()
{
//1.获取通知数据 -> 转换为数组
$xmlData = $this->getPost();
file_put_contents('info1.txt', $xmlData . "\n", FILE_APPEND);
$arr = XmlToArr($xmlData);
//2.验证签名
if ($this->checkSign($arr)) {
//3.验证业务结果(return_code 和 result_code)
if ($arr['return_code'] == 'SUCCESS' && $arr['result_code'] == 'SUCCESS') {
//成功
//4. 验证订单号和金额 (out_trade_no total_fee)
// 这里是通过数据库查询订单号,得到金额和返回的金额对比,再进行操作.
// 这里有一个问题,我看文档的返回数据中,貌似没有上面2个值...
//返回结果给微信服务器
$returnparams = [
'return_code' => 'SUCCESS',
'return_msg' => 'OK'
];
echo ArrToXml($returnparams);
} else {
//失败
return '支付失败';
}
} else {
//签名不成功
return '签名失败';
}
}
//获取post过来的数据
public function getPost()
{
return file_get_contents('php://input');
}