背景
事先准备工作
- 申请一个小程序,并开通微信支付,详细见:微信小程序支付业务说明
- 仔细查阅微信支付官方文档,详细见: 微信支付开发者文档
- 仔细阅读 微信支付统一下单接口
- 仔细阅读 支付结果通知接口
- 整理并在商户平台设置好相应的回掉地址,比如http://test.dev.com/wechat/pa...
- 服务端编写两个接口
1) 微信预支付接口,http://test.dev.com/wechat/pr... , 以商品订单为例,此接口接受两个参数,goods_id 和 uid ,goods_id表示商品编号,uid表示用户编号,返回参数如下
{
errcode: 200,
msg: "SUCCESS",
data: {
status: 1, //状态,为1表示成功,其他的表示失败
result: "success",
data: {
appId: "xxx", //小程序的appid
timeStamp: 1545909092, //时间戳
nonceStr: "vuryhptlafvpee92pxhji6zs5jl2n0gu", //随机串
package: "prepay_id=wx27191130962951f060bfa1323531879649", //支付的包参数
signType: "MD5", //签名方式
paySign: "B04272BB9BBDB1F52863D3B0EF580BE8" //支付签名
}
}
}
2) 微信支付回调接口,http://test.dev.com/wechat/pa... ,此接口最好是get和post都设置,因为 微信在进行回调的时候会以post的形式进行请求
5.建表
1) 商品订单表(shop_goods_order),其中重要的字段有out_trade_no,out_trade_no传递给微信支付的支付订单号,也是我们自己的系统与微信对接的订单唯一标识;bill_no表示微信支付的交易订单号,这个字段只有在订单支付成功之后进行更新,该字段也是查询位置支付订单的唯一标识,详细的表结构如下
CREATE TABLE `shop_goods_order` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`uid` int(10) DEFAULT '0' COMMENT '用户编号',
`goods_id` int(10) DEFAULT '0' COMMENT '商品编号',
`out_trade_no` varchar(30) DEFAULT '' COMMENT '订单序列号',
`bill_no` varchar(30) DEFAULT '' COMMENT '支付方返回的交易订单号',
`paid_money` int(10) DEFAULT '0' COMMENT '支付的金额',
`paid_integral` int(10) DEFAULT '0' COMMENT '支付的健康币',
`paid_type` varchar(15) DEFAULT 'WXPAY' COMMENT '支付类型,有WXPAY和INTEGRAL等值',
`paid_status` varchar(10) DEFAULT 'CHECKED' COMMENT '支付状态,CHECKED表示初始状态,SUCC表示支付成功,FAILED表示支付失败,REFUND表示已退款',
`add_time` int(10) DEFAULT '0' COMMENT '添加时间',
`paid_time` int(10) DEFAULT '0' COMMENT '支付时间',
`update_time` int(10) DEFAULT '0' COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8;
2) 商品信息表(shop_goods_info),字段如下
CREATE TABLE `shop_goods_info` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(100) DEFAULT '' COMMENT '商品名称',
`note` varchar(300) DEFAULT '' COMMENT '商品描述',
`market_price` int(10) DEFAULT '0' COMMENT '原价',
`sale_price` int(10) DEFAULT '0' COMMENT '售价',
`integral` int(8) DEFAULT '0' COMMENT '健康币',
`main_thumbnail` varchar(40) DEFAULT '' COMMENT '主图',
`thumbnail1` varchar(40) DEFAULT '' COMMENT '缩略图1',
`thumbnail2` varchar(40) DEFAULT '' COMMENT '缩略图2',
`thumbnail3` varchar(40) DEFAULT '' COMMENT '缩略图3',
`thumbnail4` varchar(40) DEFAULT '' COMMENT '缩略图4',
`thumbnail5` varchar(40) DEFAULT '' COMMENT '缩略图5',
`content` text COMMENT '详细介绍',
`add_time` int(10) DEFAULT '0' COMMENT '添加时间',
`update_time` int(10) DEFAULT '0' COMMENT '更新时间',
`is_online` tinyint(1) DEFAULT '1' COMMENT '商品是否上线',
`sort` int(4) DEFAULT '0' COMMENT '排序值,越大越靠前',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
实现步骤
业务实现时序图
实现步骤说明
客户端
- 客户端调用 微信预支付接口 获取对应的微信支付参数
- 获取基础的支付参数后,调用wx.requetPayment接口调起微信支付
- 用户输入密码完成支付
服务端
- 客户端在发起支付前先往商品订单表里面创建一条订单,并生成对应的out_trade_no参数
- 调用微信支付的统一下单接口https://api.mch.weixin.qq.com...,向微信发起支付订单请求,统一下单接口文档地址,见微信支付统一下单接口
- 支付请求结束后微信将支付结果返回给 微信支付回调接口
- 若支付成功,服务端将订单的paid_status字段设置succ ,并将bill_no、paid_time、update_time更新,bill_no的值为微信支付的transaction_id;若支付失败,将paid_status字段更新为failed,并更新update_time字段
关键代码
客户端
发起微信支付
wxPay:function () {
var that = this
var params = {
goods_id: that.data.goods_id,
uid: that.data.uid,
paid_type: 'WXPAY'
}
var param = JSON.stringify(params)
console.log(param)
param = app.Encrypt(param)
var url = app.data.API_DOMAIN + "/wechat/prepay?param=" + param
wx.showModal({
title: '提示',
content: '确定要微信支付购买此系列课吗?',
success(res) {
if (res.confirm) {
if (that.data.iswxpay == 0) {
that.setData({
iswxpay: 1
})
app.httpRequest(that.data.uid, url, function (response) {
var payinfo = response.data.data.data
wx.requestPayment({
timeStamp: payinfo.timeStamp.toString(),
nonceStr: payinfo.nonceStr,
package: payinfo.package,
signType: 'MD5',
paySign: payinfo.paySign,
success(res) {
wx.showToast({
title: '购买成功',
icon: 'success'
})
that.setData({
is_paid: 1
})
that.getSeminarInfo(that.data.sid, that.data.uid)
},
fail(res) {
that.setData({
iswxpay: 0
})
wx.showToast({
title: '购买失败',
icon: 'none'
})
}
})
console.log(response.data.data.data)
}, function (f_res) { }, function (f_res) { })
}
} else {
that.setData({
iswxpay: 0
})
console.log('取消微信支付')
}
}
})
},
服务端
预支付接口关键代码
1、入口方法:orderPay
/**
* 微信支付的获取支付参数的接口
* 1.先要用户编号和支付方式获取对应的订单,如果存在则取存在的,若不存在则创建,一种支付类型的订单值存在一条记录
* 2.创建订单后根据out_trade_no来调用微信支付的统一下单接口得到微信支付的支付参数
* 3.将参数返回给前端进行支付
* 4.支付成功之后进行回掉
*/
public function orderPay($uid, $goodsId, $paidType){
$result = [];
$lockKey = BusinessHelper::getPayOrderLockRedisKey();
//枷锁是为了防止并发
$this->doWithLock(function()use(&$result,$uid,$goodsId,$paidType){
error_log('$paidType ================>'.$paidType);
switch ($paidType){
case Constant::PAID_TYPE_MIXED :
error_log('doIntegralPay ================>');
$result = $this->doMixedPay($uid,$goodsId,$paidType);
error_log('integral pay result ================>'.json_encode($result));
break;
case Constant::PAID_TYPE_WXPAY :
$result = $this->doWxaPay($uid,$goodsId,$paidType);
error_log('wx pay result ================>'.json_encode($result));
break;
}
},$lockKey,5);
error_log('result ================>'.json_encode($result));
return $result;
}
2、微信核心支付方法:doWxaPay
/**
* 通过小程序支付的逻辑
* @param $uid 用户编号
* @param $goodsId 系列课编号
* @param $paidType 支付类型,有INTEGRAL和WXPAY两种
* @return array
*/
public function doWxaPay($uid, $goodsId, $paidType){
$goodsInfo = ShopGoodsInfoService::getById($goodsId);
if(!$goodsInfo){
return [
'status' => -1,
'result' => '商品已经下架或者不存在'
];
}
$config = BusinessHelper::getWechatPayConfig();
$payHelper = new WechatPayHelper($config);
$payContent = $this->getWxaPrepayContent($uid,$paidType,$goodsId);
$params = $payHelper->prepay($payContent);
error_log('param ==============>'.json_encode($params));
return $params;
}
3、创建订单方法:createOrder
这个方法是为了建立订单,为了保证表示每一次支付都建立一个订单,我这边做两重的订单复用,先根据订单状态去查询是否有待支付的订单,如果有在判断这个订单的差功能键时间是否已经超过7天,如果超过七天则另外创建新的订单,尽最大的进行数据复用
/**
* 创建和验证订单,接口方法
* @param $uid 用户编号
* @param $paidType 支付类型
* @param $goodsId 系列课编号
* @return array
*/
protected function createOrder($uid, $paidType, $goodsId){
$existOrder = $this->getUserGoodsOrderWithPaidType($uid,$paidType,$goodsId);
if(!$existOrder){
return $this->generateOrder($uid,$paidType,$goodsId);
}
//验证7天之类订单有效
$createTime = date('Y-m-d',$existOrder['add_time']);
$today = date('Y-m-d');
$diff = TimeHelper::getDiffBetweenTwoDays($today,$createTime);
if($diff > 7){
return $this->generateOrder($uid,$paidType,$goodsId);
}
return $existOrder;
}
4、订单查重方法:getUserGoodsOrderWithPaidType
/**
* 根据支付类型获取用户对应的商品的订单
*/
public function getUserGoodsOrderWithPaidType($uid, $paidType, $goodsId){
$order = BeanHelper::convertStdClsToArr(
ShopGoodsOrder::where('uid', $uid)
->where('goods_id',$goodsId)
->where('paid_type',$paidType)
->whereIn('paid_status',[Constant::PAID_STATUS_CHECKED])
->orderBy('add_time','desc')
->first()
);
return $order;
}
5、生成订单方法:
/**
* 生成订单,辅助方法
* @param $uid 用户编号
* @param $paidType 支付类型
* @param $goodsId 系列课编号
* @return array
*/
public function generateOrder($uid, $paidType, $goodsId){
$goodsInfo = ShopGoodsInfoService::getById($goodsId);
$priceKey = $paidType == Constant::PAID_TYPE_WXPAY ? 'market_price' : 'sale_price';
$price = formatArrValue($goodsInfo,$priceKey,0);
$integral = $paidType == Constant::PAID_TYPE_WXPAY ? 0 : formatArrValue($goodsInfo,'integral',0);
$baseMeasureUnit = 100;
$insertOrderData = [
'uid' => $uid,
'goods_id' => $goodsId,
'out_trade_no' => BusinessHelper::generateOutTradeNo(Constant::PAID_SCENE_SHOP_GOODS_ORDER),
'paid_money' => $price * $baseMeasureUnit,
'paid_integral' => $integral,
'paid_type' => $paidType,
'paid_status' => Constant::PAID_STATUS_CHECKED,
'add_time' => time(),
'update_time' => time(),
];
$existOrder = BeanHelper::convertStdClsToArr($this->store($insertOrderData));
return $existOrder;
}
6、生成outTradeNo方法
这个方法中的getPaidSceneMapping方法返回的是一个数组,out_trade_no方法有3个部分组成,分别是当前时间,场景值(这个是为了保证不同的支付场景对应的不同的业务代码)以及10位随机数字组成
/**
* 生成第三方支付的外部订单号
*/
public static function generateOutTradeNo($paidScene = Constant::PAID_SCENE_SEMINAR_ORDER){
$prefix = date('YmdHis');
$paidSceneMap = self::getPaidSceneMapping();
$scene = formatArrValue($paidSceneMap,$paidScene,'0001');
$suffix = generateRandomNum(10);
return $prefix.$scene.$suffix;
}
/**
* 获取支付场景的map,这个是为了区分不同的支付场景时候更新不同的业务字段,为了拓展进行的预留
*/
public static function getPaidSceneMapping(){
return [
Constant::PAID_SCENE_SEMINAR_ORDER => '0001',
Constant::PAID_SCENE_SHOP_GOODS_ORDER => '0002'
];
}
支付回调接口关键代码
入口方法:payNotify
/**
* 支付的回掉
*/
public function payNotify(Request $request){
error_log('notify request param ========>');
$config = BusinessHelper::getWechatPayConfig();
$helper = new WechatPayHelper($config);
$result = $helper->notify($request);
return $result;
}
微信支付帮助类
<?php
namespace App\Http\Helper\Pay;
use App\Http\Helper\Jz\BusinessHelper;
use App\Http\Helper\Jz\Constant;
use App\Http\Helper\LogHelper;
use SeminarOrderService;
use ShopGoodsOrderService;
/**
* Created by PhpStorm.
* User: Auser
* Date: 2018/12/17
* Time: 15:41
*/
class WechatPayHelper {
public $config;
public function __construct($config)
{
$this->config = $config;
}
/**
* 预支付请求接口(POST)
* 返回json的数据
*/
public function prepay($payContent)
{
$config = $this->config;
$unifiedorder = [
'appid' =>$config['appid'],
'mch_id' =>$config['mchid'],
'nonce_str' =>self::getNonceStr(),
'body' =>$payContent['body'],
'out_trade_no' =>$payContent['out_trade_no'],
'total_fee' =>$payContent['fee'],
'spbill_create_ip'=>$_SERVER['REMOTE_ADDR'],
'notify_url' =>$config['notify_url'],
'trade_type' =>'JSAPI',
'openid' =>$payContent['openid']
];
error_log('config ===============>'.json_encode($config));
$unifiedorder['sign'] = $this->makeSign($unifiedorder);
error_log('unifine order param ===============>'.json_encode($unifiedorder));
//请求数据
$xmldata = $this->array2xml($unifiedorder);
$url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
$res = $this->request($url, $xmldata);
if(!$res){
return $this->errorResult("Can't connect the server");
}
$content = $this->xml2array($res);
error_log('unifine order result ===============>'.json_encode($content));
if(strval($content['result_code']) == 'FAIL'){
return $this->errorResult(strval($content['return_msg']));
}
if(strval($content['return_code']) == 'FAIL'){
return $this->errorResult(strval($content['return_msg']));
}
//拼接小程序的接口数据
$resData = [
'appId' => strval($content['appid']),
'timeStamp' => time(),
'nonceStr' => $this->getNonceStr(),
'package' => 'prepay_id='.strval($content['prepay_id']),
'signType' => 'MD5'
];
//加密签名
$resData['paySign'] = $this->makeSign($resData);
return $this->successResult($resData);
}
/**
* @return array|bool
* 微信支付回调验证
* 返回数据
*/
public function notify(){
//$xml = $GLOBALS['HTTP_RAW_POST_DATA'];
error_log("wechat pay notify message ============>");
$xml = file_get_contents('php://input');
//将服务器返回的XML数据转化为数组
$data = $this->xml2array($xml);
// 保存微信服务器返回的签名sign
$dataSign = $data['sign'];
// sign不参与签名算法
unset($data['sign']);
$sign = $this->makeSign($data);
// 判断签名是否正确 判断支付状态
$result = false;
error_log("return data ============>".json_encode($data));
//验证订单是否已经支付,调用订单查询接口
$isPayment = $this->verifyPament($data);
error_log("isPayment ============>".$isPayment);
if($isPayment && ($data['return_code']=='SUCCESS') && ($data['result_code']=='SUCCESS')) {
error_log("isPayment success============>");
$outTradeNo = $data['out_trade_no'];
$concurrentTime = 30;
$lockKey = getCacheKey('redis_key.cache_key.zset_list.lock') . $outTradeNo;
//采用并发锁控制并发
SeminarOrderService::doWithLock(function()use(&$result , $data){
$result = $data;
$this->setPaidSuccess($data);
},$lockKey,$concurrentTime);
}else{
error_log("isPayment failed============>");
$this->setPaidFail($data);
}
// 返回状态给微信服务器
if($result){
$str='<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
}else {
$str='<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名失败]]></return_msg></xml>';
}
return $str;
}
/**
* 支付成功
*/
public function setPaidSuccess($data){
error_log('current paid data =============>'.json_encode($data));
$paidType = substr($data['out_trade_no'], 14, 4);
error_log('current paid type is =============>'.$paidType);
switch ($paidType){
case '0001' :
SeminarOrderService::setOrderPaid($data);
break;
case '0002':
ShopGoodsOrderService::setOrderPaid($data);
break;
}
}
/**
* 支付失败
*/
public function setPaidFail($data){
$paidType = intval(substr($data['out_trade_no'], 14, 4));
LogHelper::info('current paid type is =============>'.$paidType);
switch ($paidType){
case '0001' :
SeminarOrderService::setOrderPaidFailed($data);
break;
case '0002':
ShopGoodsOrderService::setOrderPaidFailed($data);
break;
}
}
/**
* 验证支付的问题
*/
public function verifyPament($wxPayResp){
error_log("verify paymnent method=======>".json_encode($wxPayResp));
$url = "https://api.mch.weixin.qq.com/pay/orderquery";
//检测必填参数
if(!$wxPayResp['transaction_id'] && !$wxPayResp['out_trade_no']) {
error_log("订单查询接口中,out_trade_no、transaction_id至少填一个!");
return false;
}
error_log("开始查询==============》接口");
$config = BusinessHelper::getWechatPayConfig();
error_log("post config ==============》".json_encode($config));
error_log("transaction is===============>".$wxPayResp['transaction_id']);
error_log("appid is===============>".$config['appid']);
error_log("transaction is===============>".$config['mchid']);
error_log("nonce_string is===============>".$this->getNonceStr());
$params = [
'appid' => $config['appid'],
'mch_id' => $config['mchid'],
'nonce_str' => $this->getNonceStr(),
'transaction_id' => $wxPayResp['transaction_id']
];
error_log("post PARAM without sign==============》");
$params['sign'] = $this->makeSign($params);
error_log("post PARAM0 with sign ==============》");
$xmlData = $this->array2xml($params);
$response = $this->request($url,$xmlData);
if(!$response){
error_log("接口请求错误:");
return false;
}
$result = $this->xml2array($response);
error_log("查询订单接口返回结果:".json_encode($result));
if(array_key_exists("return_code", $result)
&& array_key_exists("trade_state", $result)
&& $result["return_code"] == "SUCCESS"
&& $result["trade_state"] == "SUCCESS"){
return true;
}
return false;
}
//---------------------------------------------------------------用到的函数------------------------------------------------------------
/**
* 错误返回提示
* @param string $errMsg 错误信息
* @param string $status 错误码
* @return array json的数据
*/
protected function errorResult($errMsg = 'error', $status = Constant::PAID_RESULT_FAILED)
{
return [
'status'=>$status,
'result'=>'fail',
'data'=>$errMsg
];
}
/**
* 正确返回
* @param array $data 要返回的数组
* @return array json的数据
*/
protected function successResult($data=[]){
return [
'status'=> Constant::PAID_RESULT_SUCCESS,
'result'=>'success',
'data'=>$data
];
}
/**
* 将一个数组转换为 XML 结构的字符串
* @param array $arr 要转换的数组
* @param int $level 节点层级, 1 为 Root.
* @return string XML 结构的字符串
*/
protected function array2xml($arr, $level = 1){
$s = $level == 1 ? "<xml>" : '';
foreach($arr as $tagname => $value) {
if (is_numeric($tagname)) {
$tagname = $value['TagName'];
unset($value['TagName']);
}
if(!is_array($value)) {
$s .= "<{$tagname}>".(!is_numeric($value) ? '<![CDATA[' : '').$value.(!is_numeric($value) ? ']]>' : '')."</{$tagname}>";
}else {
$s .= "<{$tagname}>" . $this->array2xml($value, $level + 1)."</{$tagname}>";
}
}
$s = preg_replace("/([\x01-\x08\x0b-\x0c\x0e-\x1f])+/", ' ', $s);
return $level == 1 ? $s."</xml>" : $s;
}
/**
* 将xml转为array
* @param string $xml xml字符串
* @return array 转换得到的数组
*/
protected function xml2array($xml)
{
//禁止引用外部xml实体
libxml_disable_entity_loader(true);
$result= json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
return $result;
}
/**
*
* 产生随机字符串,不长于32位
* @param int $length
* @return 产生的随机字符串
*/
protected 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;
}
/**
* 生成签名
* @return 签名
*/
protected function makeSign($data){
//获取微信支付秘钥
$key = $this->config['mch_secret'];
//去空
$data = array_filter($data);
//签名步骤一:按字典序排序参数
ksort($data);
$signParam = http_build_query($data);
$signParam = urldecode($signParam);
//签名步骤二:在string后加入KEY
$signContent = $signParam."&key=".$key;
//签名步骤三:MD5加密
$sign = md5($signContent);
// 签名步骤四:所有字符转为大写
$result=strtoupper($sign);
return $result;
}
/**
* 微信支付发起请求
*/
protected function request($url, $xmldata, $second=30, $aHeader=array()){
$ch = curl_init();
//超时时间
curl_setopt($ch,CURLOPT_TIMEOUT,$second);
curl_setopt($ch,CURLOPT_RETURNTRANSFER, 1);
//这里设置代理,如果有的话
//curl_setopt($ch,CURLOPT_PROXY, '10.206.30.98');
//curl_setopt($ch,CURLOPT_PROXYPORT, 8080);
curl_setopt($ch,CURLOPT_URL,$url);
curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,false);
curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,false);
if( count($aHeader) >= 1 ){
curl_setopt($ch, CURLOPT_HTTPHEADER, $aHeader);
}
curl_setopt($ch,CURLOPT_POST, 1);
curl_setopt($ch,CURLOPT_POSTFIELDS,$xmldata);
$data = curl_exec($ch);
if($data){
curl_close($ch);
return $data;
}
else {
$error = curl_errno($ch);
echo "call faild, errorCode:$error\n";
curl_close($ch);
return false;
}
}
}
踩坑点
1、支付回调接口http://test.dev.com/wechat/pa... 一定要设置成get、post都能访问,我当初只设置了get请求可以访问,浪费了好多时间进行排查,而微信回调的数据基本都是以post形式进行调用的