微信Jsapi支付&实战踩坑

准备

需求

需求就是最简单的对接微信网页支付接口

方案心路历程

  1. 一开始是对接的H5,后面才发现H5支付是一定要在微信环境外才可以调用并跳转支付的
  2. 这是H5支付对接实操过程 下面称为P1
  3. 上文H5支付对接实操过程中已经写了官方文档地址,这里不加以赘述
  4. 于是翻看了文档发现微信内部需要使用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中的wxJsConfigwx.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&timestamp=%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();
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值