准备
需求
需求就是最简单的对接微信网页支付接口
方案心路历程
- 一开始是对接的H5,后面才发现H5支付是一定要在微信环境外才可以调用并跳转支付的
- 这是H5支付对接实操过程 下面称为
P1
- 上文
H5支付对接实操过程
中已经写了官方文档地址,这里不加以赘述 - 于是翻看了文档发现微信内部需要使用Jsapi支付方式
实操
微信商户平台配置
亦参考P1
编码
预支付
亦参考P1
/**
* 支付下单
* @param string api 支付接口 // 这里填jsapi
* @param string out_trade_no 订单id
* @param string description 详情
* @param string notify_url 异步回调api
* @param integer amount 支付金额
* @param string ip 支付用户ip
* @return boolean
*/
public function pay($data)
{
try {
$json = [
'mchid' => $this->merchantId,
'out_trade_no' => $data['out_trade_no'],
'appid' => $this->getAppid($data['systemSlug']),
'description' => $data['description'],
'notify_url' => $data['notify_url'],
'amount' => [
'total' => $data['amount'], // 订单总金额,单位为分
'currency' => 'CNY'
],
];
if (!empty($data['ip']))
$json['scene_info'] = [
"payer_client_ip" => $data['ip'],
"h5_info" => [
"type" => $data['h5_type'] ?? "Wap"
]
];
if (!empty($data['openid']))
$json['payer'] = ['openid' => $data['openid']];
// Log::info(sprintf("Wechat/Mch->pay data: %s", json_encode($data)));
$api = $data['api'];
$resp = $this->instance
->v3->pay->transactions->$api
->post(['json' => $json]);
$code = $resp->getStatusCode();
$message = $resp->getReasonPhrase();
$body = $resp->getBody();
Log::info(sprintf("Wechat/Mch->pay statusCode: %s, reasonPhrase: %s, body: %s", $code, $message, $body));
$res = [
'code' => $code,
'data' => json_decode($body, true) ?? [],
'message' => $message
];
} catch (\Exception $e) {
// 进行错误处理
Log::error("Fail to Wechat/Mch->pay: " . $e->getMessage() . "\n");
$code = 500;
if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {
$r = $e->getResponse();
Log::error("" . $r->getStatusCode() . ' ' . $r->getReasonPhrase() . "\n");
Log::error("" . $r->getBody()->getContents() . "\n\n\n");
$code = $r->getStatusCode();
}
Log::error("getTraceAsString: " . $e->getTraceAsString() . "\n");
$res = [
'code' => $code,
'data' => json_decode($body, true) ?? [],
'message' => $e->getMessage() ?? '请稍后重试'
];
}
return $res;
// 返回结果 {"code": 200, "data": {"prepay_id": "wx26112221580621e9b071c00d9e093b0000"}, "message": "OK"}
}
}
以上代码与P1
的区别在于增加了一个支付者的关键信息openid
以下是获取openid的方式,这里封装了获取openid的方式
<?php
namespace App\Services\Support\Wechat;
use Illuminate\Support\Facades\Log;
class Webview
{
public static $errcode = null;
public static $errmsg = null;
public $slug = null;
public $userInfo = null;
public function __construct($code, $slug = 'usf')
{
$this->slug = $slug;
$resp = $this->getAccessData($code);
if (!$resp || isset($resp['errcode'])) {
return;
}
foreach ($resp as $key => $value) {
$this->$key = $value;
}
}
public function getAppid()
{
return config("wechat.{$this->slug}.wx_appid");
}
// 获取openid等数据
private function getAccessData($code)
{
$url = sprintf(
config('wechat.webview.base_url') . "oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
$this->getAppid(),
config("wechat.{$this->slug}.wx_secret"),
$code
);
$resp = file_get_contents($url);
Log::info("Webview->getAccessData appid: " . $this->getAppid() . " resp: $resp");
$resp = json_decode($resp, true);
if (isset($resp['errcode'])) {
self::$errcode = $resp['errcode'];
self::$errmsg = $resp['errmsg'];
return;
}
return $resp;
}
// 获取微信信息
public function getUserInfo()
{
if ($this->userInfo) return $this->userInfo;
$url = sprintf(
config('wechat.webview.base_url') . "userinfo?access_token=%s&openid=%s&lang=zh_CN",
$this->access_token,
$this->openid
);
$resp = file_get_contents($url);
$resp = json_decode($resp, true);
Log::info($resp);
if (isset($resp['errcode'])) {
self::$errcode = $resp['errcode'];
self::$errmsg = $resp['errmsg'];
return;
}
return $this->userInfo = $resp;
}
}
$webview= new Webview();
$payOptions['openid'] = $webview($wxcode, $reqData['clientInfo']['systemSlug'])->openid ?? '';
调用openid获取是还需要获取微信返回的一个授权码,授权码获取方式对接文档
$url = sprintf(
"https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_base&state=123#wechat_redirect",
config("wechat.{$systemSlug}.wx_appid"),
urlencode($request->header('Referer') ?? '')
)
前端通过以上链接进行重定向,重定向后页面就会携带微信返回的一个参数?code=xxx
将这个参数授权码
传入后端即可
调起支付
由于jsapi支付是需要在微信内部环境下由前端调起的支付
所以以上仅仅是预支付生成prepay_id
,然后再参考JSAPI调起支付API
此API无后台接口交互,需要将列表中的数据签名
wx.chooseWXPay({
timestamp: options.timestamp, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
nonceStr: options.nonceStr, // 支付签名随机串,不长于 32 位
package: options.package, // 统一支付接口返回的prepay_id参数值,提交格式如:package: "prepay_id=wx26112221580621e9b071c00d9e093b0000")
signType: options.signType, // 微信支付V3的传入RSA,微信支付V2的传入格式与V2统一下单的签名格式保持一致
paySign: options.paySign, // 支付签名
success: function (res) {
// 支付成功后的回调函数
alert(JSON.stringify(res))
}
});
以上调起支付方式是通过JSSDK方式调用封装好的方法
这里也记录一下JSSDK
的使用方式
公司项目的使用场景是纯静态的推广页使用,所以以下记录一下原生JS的使用方式
PS: vue中使用比较方便,可通过npm install weixin-js-sdk
直接引入
由于推广页中只有一个js文件的引入,是我自己封装的一个统一下单集成的SDK,所以JSSDK
会通过js动态引入的方式
Js动态引入Js文件
// 以下代码待封装整理
let url = "./static/js/jweixin-1.6.0.js"
var script= document.createElement('script');
script.type = "text/javascript";
alert("load")
alert(script.readyState)
if (script.readyState) {
script.onreadystatechange = async function () {
if (script.readyState == "loaded" || script.readyState == "complete") {
script.onreadystatechange = null;
alert("load1")
}
}
} else {
script.onload = async function () {
// 在微信JSSDK代码加载完成后执行
alert("load2")
alert(JSON.stringify(wx))
// 配置wx环境
wx.config({
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: wxJsConfig.appId, // 必填,公众号的唯一标识
timestamp: wxJsConfig.timestamp, // 必填,生成签名的时间戳
nonceStr: wxJsConfig.nonceStr, // 必填,生成签名的随机串
signature: wxJsConfig.signature,// 必填,签名
jsApiList: ['chooseWXPay'] // 必填,需要使用的JS接口列表
});
wx.ready(function () {
alert('wx.ready')
// config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。
// 调起支付
wx.chooseWXPay({
timestamp: options.timestamp,
nonceStr: options.nonceStr,
package: options.package,
signType: options.signType,
paySign: options.paySign,
success: function (res) {
// 支付成功后的回调函数
alert(JSON.stringify(res))
}
});
});
wx.error(function (res) {
// config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
console.log(res)
});
}
}
script.src = url;
document.body.appendChild(script);
以上代码又设计两个数据集wx.config
中的wxJsConfig
,wx.chooseWXPay
中的options
这两个数据集都需要通过后端处理后返回其中paySign
则需要通过预支付中获取的prepay_id
进行二次签名
这里想吐槽一下,既然我预支付使用的是官方的SDK,那为什么预支付后不直接二次签名再一起返回前端所需数据咧,这样在使用SDK后我后端代码还需要重新进行二次签名,多封装一层,希望后面V3-SDK会做这个优化吧
二次签名 options
$timestamp = time();
$nonceStr = DataHandle::strRandom(32, 'secret'); // 这里使用的是封装好的随机数生成方法
$package = sprintf("prepay_id=%s", $res);
$message = config("wechat." . $reqData['clientInfo']['systemSlug'] . ".wx_appid") . "\n" .
$timestamp . "\n" .
$nonceStr . "\n" .
$package . "\n";
openssl_sign($message, $raw_sign, openssl_get_privatekey(file_get_contents(config('wechat.mch.ssl_key'))), 'sha256WithRSAEncryption');
$sign = base64_encode($raw_sign);
$options = [
'timestamp' => $timestamp,
'nonceStr' => $nonceStr,
'package' => $package,
'signType' => 'RSA',
'paySign' => $sign
];
附上DataHandle
部分代码
<?php
namespace App\Utils\Common;
use Illuminate\Support\Facades\Log;
class DataHandle
{
...
/**
* @description: 生成随机字符串
* @param {*} $len
* @param {*} $type [token,nums,key,secret,string]
* @return {*}
*/
public static function strRandom($len, $type = "token")
{
$nums = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
$lls = [
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k",
"l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v",
"w", "x", "y", "z"
];
$uls = [
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K",
"L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V",
"W", "X", "Y", "Z"
];
$special = [
"!", "@", "#", "$", "?", "|", "{", "/", ":", ";", "%",
"^", "&", "*", "(", ")", "-", "_", "[", "]", "}", "<",
">", "~", "+", "=", ",", "."
];
switch ($type) {
case "token":
$chars = array_merge($nums, $lls, $uls, $special);
break;
case "nums":
$chars = $nums;
break;
case "key":
$chars = array_merge($nums, $lls);
break;
case "secret":
$chars = array_merge($nums, $uls);
break;
case "string":
$chars = array_merge($nums, $lls, $uls);
break;
default:
$chars = array_merge($nums, $lls, $uls, $special);
}
$charsLen = count($chars) - 1;
shuffle($chars); //打乱数组顺序
$str = '';
for ($i = 0; $i < $len; $i++) {
$str .= $chars[mt_rand(0, $charsLen)]; //随机取出一位
}
return $str;
}
...
}
获取微信js接口调用签名 wxJsConfig
<?php
namespace App\Services;
...
use App\Utils\Common\HttpC;
use Illuminate\Support\Facades\Log;
class Wechat
{
...
// 获取微信jsapi签名
public static function getJsapiSignature($noncestr, $timestamp, $url, $slug)
{
$appid = config("wechat.{$slug}.wx_appid");
$secret = config("wechat.{$slug}.wx_secret");
$baseUrl = "https://api.weixin.qq.com/";
// PHP-http封装,目前有点过于简陋就不帖出来了,可自行百度
$res = HttpC::get(sprintf("%scgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", $baseUrl, $appid, $secret));
Log::info("cgi-bin/token: " . json_encode($res));
$res = HttpC::get(sprintf("%scgi-bin/ticket/getticket?access_token=%s&type=jsapi", $baseUrl, $res['access_token']));
Log::info("cgi-bin/ticket: " . json_encode($res));
$str = sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%s&url=%s", $res['ticket'], $noncestr, $timestamp, $url);
Log::info("str: " . $str);
$signature = sha1($str);
return $signature;
}
...
}
...
public function getWxJsapiConfig(Wechat $wechat)
$nonceStr = DataHandle::strRandom(16);
$timestamp = time();
$appid = config("wechat.{$slug}.wx_appid");
return $wxJsConfig = [
'appid' => $appid,
'nonceStr' => $nonceStr,
'timestamp' => $timestamp,
'signature' => $wechat->getJsapiSignature($nonceStr, $timestamp, $url, $slug)
];
}
做完以上步骤终于看到久违的报错信息了
微信客户端提示xxxurl未注册
微信商户号配置
微信商户号后台
附上配置教程
效果
真麻烦
后记
唤起支付的方式
测试的时候发现了一个问题:用户点击关闭支付后前端没办法及时触发支付结束这个事件
所以改成以下方式
const wxJsapiConfig = await that.getWxJsapiConfig()
const options = data.pay.data
// alert((options.timestamp).toString())
function onBridgeReady() {
WeixinJSBridge.invoke('getBrandWCPayRequest', {
"appId": wxJsapiConfig.appid, //公众号ID,由商户传入
"timeStamp": (options.timestamp).toString(), //时间戳,自1970年以来的秒数
"nonceStr": options.nonceStr, //随机串
"package": options.package,
"signType": options.signType, //微信签名方式:
"paySign": options.paySign //微信签名
},
function (res) {
// alert(JSON.stringify(res))
// 在这里可以触发支付结束事件(取消/完成)
if (res.err_msg == "get_brand_wcpay_request:ok") {
// 使用以上方式判断前端返回,微信团队郑重提示:
//res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
that.pay.config.success(res)
}
that.redirectToPayDone(data.pay.id)
});
}
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();
}