微信网页开发和支付详解

微信网页开发和微信JS-SDK介绍 微信JS-SDK使用步骤 步骤一:公众号绑定安全域名 步骤二:在前端网页代码中引入JS-SDK文件 步骤三:通过config接口注入权限验证配置 access_token 用途 access_token获取方式 中控管理access_token防冲突 通过access_token来获取jsapi_ticket 生成JS-SDK权限验证的签名 步骤0 步骤1 步骤2 步骤四:通过ready接口处理成功验证 步骤五:通过error接口处理失败验证 接口调用说明 JS-SDK微信支付 步骤1:微信支付配置 步骤2:微信支付业务流程时序图 步骤3:统一下单 步骤4:支付结果通知 步骤5:查询订单

微信网页开发和微信JS-SDK介绍

微信JS-SDK是微信公众平台面向网页开发者提供的基于微信内的网页开发工具包。

如果你的网页只是展示,那可能用不到JS-SDK。

如果你想用一些微信上的功能,例如“微信扫一扫”,这个时候你不得不用了。

通过使用微信JS-SDK,网页开发者可借助微信高效地使用拍照、选图、语音、位置等手机系统的能力,同时可以直接使用微信分享、扫一扫、卡券、支付等微信特有的能力,为微信用户提供更优质的网页体验。

直观的看下接入JS-SDK后都有哪些接口可以用:

*.png

也可以直接看JS-SDK的demo

微信JS-SDK使用步骤

步骤一:公众号绑定安全域名

微信网页端接入,必须先配置好安全域名:

*.png

安全起见,微信的JS-SDK不是是随便哪个域名都能用的,必须得绑定公众号和域名。

步骤二:在前端网页代码中引入JS-SDK文件

在需要调用JS接口的页面引入如下JS文件,(支持https):http://res.wx.qq.com/open/js/jweixin-1.6.0.js

如需进一步提升服务稳定性,当上述资源不可访问时,可改访问:http://res2.wx.qq.com/open/js/jweixin-1.6.0.js (支持https)。

备注:支持使用 AMD/CMD 标准模块加载方法加载

步骤三:通过config接口注入权限验证配置

所有需要使用JS-SDK的页面必须先注入配置信息,否则将无法调用(同一个url仅需调用一次,对于变化url的SPA的web app可在每次url变化时进行调用,目前Android微信客户端不支持pushState的H5新特性,所以使用pushState来实现web app的页面会导致签名失败,此问题会在Android6.2中修复)。

这里的目的是:初始化JS-SDK的配置信息,之后调用其他接口,就知道是哪个公众号的哪个用户调的了。

在你的JS代码中增加如下代码:

wx.config({
  debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
  appId: '', // 必填,公众号的唯一标识
  timestamp: , // 必填,生成签名的时间戳
  nonceStr: '', // 必填,生成签名的随机串
  signature: '',// 必填,签名
  jsApiList: [] // 必填,需要使用的JS接口列表
});

timestamp、nonceStr和signature由后台代码生成后返回给前端,生成后再调用wx.config。

生成签名signature比较麻烦,它需要jsapi_ticket。jsapi_ticket的获取又需要用到access_token。这里的access_token是公众号的access_token,而不是微信网页授权access_token。

ps:网页授权access_token的应用场景是:如果用户在微信客户端中访问第三方网页,公众号可以通过微信网页授权机制,来获取用户基本信息,进而实现业务逻辑。如下图:

*.png

而我们这里的access_token 是公众号的access_token。

access_token

用途

access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。

比如上面提到的jsapi_ticket需要用到;

比如获取用户信息接口:

*.png

比如获取公众号的素材接口:

*.png
access_token获取方式

公众号和小程序均可以使用AppID和AppSecret调用本接口:

https请求方式: GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET

来获取access_token。AppID和AppSecret可在“微信公众平台-开发-基本配置”页中获得(需要已经成为开发者,且帐号没有异常状态)。调用接口时,请登录“微信公众平台-开发-基本配置”提前将服务器IP地址添加到IP白名单中,点击查看设置方法,否则将无法调用成功。小程序无需配置IP白名单。

返回结果:

*.png
中控管理access_token防冲突

1、建议公众号开发者使用中控服务器统一获取和刷新access_token,其他业务逻辑服务器所使用的access_token均来自于该中控服务器,不应该各自去刷新,否则容易造成冲突,导致access_token覆盖而影响业务;

2、目前access_token的有效期通过返回的expire_in来传达,目前是7200秒之内的值。中控服务器需要根据这个有效时间提前去刷新新access_token。在刷新过程中,中控服务器可对外继续输出的老access_token,此时公众平台后台会保证在5分钟内,新老access_token都可用,这保证了第三方业务的平滑过渡

通过access_token来获取jsapi_ticket

用拿到的access_token 采用http GET方式请求获得jsapi_ticket(有效期7200秒,开发者必须在自己的服务全局缓存jsapi_ticket):

https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi

成功返回如下JSON:

{
  "errcode":0,
  "errmsg":"ok",
  "ticket":"bxLdikRXVbTPdHSM05e5u5sUoXNKd8-41ZO3MhKoyN5OfkWITDGgnr2fwJ0m9E8NYzWKVZvdVtaUgWvsdshFKA",
  "expires_in":7200
}

获得jsapi_ticket之后,接下来用它生成JS-SDK权限验证的签名signature。

生成JS-SDK权限验证的签名

签名生成规则如下:参与签名的字段包括noncestr(随机字符串), 有效的jsapi_ticket, timestamp(时间戳), url(当前网页的URL,不包含#及其后面部分) 。对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1。这里需要注意的是所有参数名均为小写字符。对string1作sha1加密,字段名和字段值都采用原始值,不进行URL 转义。

即signature=sha1(string1)。

步骤0

看下所需参数:

noncestr=Wm3WZYTPz0wzccnW
jsapi_ticket=sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg
timestamp=1414587457
url=http://mp.weixin.qq.com?params=value
步骤1

对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1:

jsapi_ticket=sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg&noncestr=Wm3WZYTPz0wzccnW&timestamp=1414587457&url=http://mp.weixin.qq.com?params=value

由于jsapi_ticket的ASCII码比较小,所以排在了前面。

步骤2

对string1进行sha1签名,得到signature:

0f9de62fce790f9a083d5c99e95740ceb90c27ed

至此签名生成好了,再看下我们需要的参数:

wx.config({
  debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
  appId: '', // 必填,公众号的唯一标识
  timestamp: , // 必填,生成签名的时间戳
  nonceStr: '', // 必填,生成签名的随机串
  signature: '',// 必填,签名
  jsApiList: [] // 必填,需要使用的JS接口列表
});

这里的appId我们有,签名用的noncestr和timestamp必须与wx.config中的nonceStr和timestamp相同,所以我们可以直接用,signature刚生成,jsApiList主要看我们用哪些接口就配置哪些接口。可参照:所有JS接口列表

部分代码:

// 定义签名所用的对象
        const requestParams = {
            noncestr: nonceStr,
            jsapi_ticket: jsapi_ticket,
            timestamp: timeStamp
        };

        // 步骤1. 对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1
        let paramStr = '';
        for (let key of Object.keys(requestParams).sort()) {
            paramStr += key + '=' + requestParams[key] + '&';
        }

        paramStr += 'url=' + url;

        // 步骤2. 对string1进行sha1签名,得到signature
        // sha1加密
        //creating hash object 
        let hash = crypto.createHash('sha1');
        //passing the data to be hashed
        let data = hash.update(paramStr, 'utf-8');
        //Creating the hash in the required format
        let signature = data.digest('hex');

        let res = {
            appId: appid, // 必填,公众号的唯一标识
            timestamp: timeStamp, // 必填,生成签名的时间戳
            nonceStr: nonceStr, // 必填,生成签名的随机串
            signature: signature, // 必填,签名
            jsApiList: [] // 必填,需要使用的JS接口列表
        }

注意:

签名用的url必须是调用JS接口页面的完整URL。

出于安全考虑,开发者必须在服务器端实现签名的逻辑。

如出现invalid signature 等错误详见附录5常见错误及解决办法。

步骤四:通过ready接口处理成功验证

wx.ready(function(){
  // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。
});

步骤五:通过error接口处理失败验证

wx.error(function(res){ // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。 });

接口调用说明

所有接口通过wx对象(也可使用jWeixin对象)来调用,参数是一个对象,除了每个接口本身需要传的参数之外,还有以下通用参数:

success:接口调用成功时执行的回调函数。 fail:接口调用失败时执行的回调函数。 complete:接口调用完成时执行的回调函数,无论成功或失败都会执行。 cancel:用户点击取消时的回调函数,仅部分有用户取消操作的api才会用到。 trigger: 监听Menu中的按钮点击时触发的方法,该方法仅支持Menu中的相关接口。

备注:不要尝试在trigger中使用ajax异步请求修改本次分享的内容,因为客户端分享操作是一个同步操作,这时候使用ajax的回包会还没有返回。

以上几个函数都带有一个参数,类型为对象,其中除了每个接口本身返回的数据之外,还有一个通用属性errMsg,其值格式如下:

调用成功时:"xxx:ok" ,其中xxx为调用的接口名

用户取消时:"xxx:cancel",其中xxx为调用的接口名

调用失败时:其值为具体错误信息

微信摇一摇红包和周边:https://www.zhihu.com/question/35861359

JS-SDK微信支付

微信支付

步骤1:微信支付配置

参照:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_3

设置支付目录

商户最后请求拉起微信支付收银台的页面地址我们称之为“支付目录”,例如:https://www.weixin.com/pay.php。

商户实际的支付目录必须和在微信支付商户平台设置的一致,否则会报错“当前页面的URL未注册

登录微信支付商户平台(pay.weixin.qq.com)-->产品中心-->开发配置,设置后一般5分钟内生效。

如果支付授权目录设置为顶级域名(例如:https://www.weixin.com/ ),那么只校验顶级域名,不校验后缀;

如果支付授权目录设置为多级目录,就会进行全匹配,例如设置支付授权目录为https://www.weixin.com/abc/123/,则实际请求页面目录不能为https://www.weixin.com/abc/,也不能为https://www.weixin.com/abc/123/pay/,必须为https://www.weixin.com/abc/123/

*.png 设置授权域名

开发JSAPI支付时,在统一下单接口中要求必传用户openid,而获取openid则需要您在公众平台设置获取openid的域名,只有被设置过的域名才是一个有效的获取openid的域名,否则将获取失败。具体界面如图所示:

*.png *.png

步骤2:微信支付业务流程时序图

*.png

商户系统和微信支付系统主要交互:

1、商户server调用统一下单接口请求订单,api参见公共api【统一下单API】

2、商户server接收支付通知,api参见公共api【支付结果通知API】

3、商户server查询支付结果,api参见公共api【查询订单API】

步骤3:统一下单

部分代码如下:


const requestParams = {
                appid: appid,
                mch_id:mch_id,
                nonce_str: nonceStr,
                sign_type: 'MD5',
                body: '产品付款订单',
                out_trade_no: out_trade_no,
                total_fee: total_fee,
                spbill_create_ip: spbill_create_ip,
                notify_url: 'http://www.daguanren.cc/wechat/pay/notify',
                trade_type: 'JSAPI',
                openid: openid,
                attach: '订单号为:' + out_trade_no + '的产品订单'
            };


            //2.1、用md5加密算法计算sign值
            //第一步:对参数(requestParams)按照key=value的格式,并按照参数名ASCII字典序排序如下:
            //第二步:拼接API密钥:
            let paramStr = '';
            for (let key of Object.keys(requestParams).sort()) {
                paramStr += key + '=' + requestParams[key] + '&';
            }
            //        paramStr = paramStr.substring(0,paramStr.length-1);
            paramStr += 'key=' + wechat_config.key;

            //第三步:使用sign_type中配置的方式加密,这里是MD5
            requestParams.sign = md5(paramStr).toUpperCase();
            //第四步:将json对象转换成XML
            var builder = new xml2js.Builder({
                rootName: 'xml'
            });
            var xml = builder.buildObject(requestParams);

            //2.2、向如下地址发送统一下单请求
            //https://api.mch.weixin.qq.com/pay/unifiedorder
            const orderOptions = {
                method: 'POST',
                url: 'https://api.mch.weixin.qq.com/pay/unifiedorder',
                formData: {
                    xml
                }
            };
            const orderResultXml = await rp(orderOptions);

返回结果举例:

<xml>
   <return_code><![CDATA[SUCCESS]]></return_code>
   <return_msg><![CDATA[OK]]></return_msg>
   <appid><![CDATA[wx2421b1c4370ec43b]]></appid>
   <mch_id><![CDATA[10000100]]></mch_id>
   <nonce_str><![CDATA[IITRi8Iabbblz1Jc]]></nonce_str>
   <openid><![CDATA[oUpF8uMuAJO_M2pxb1Q9zNjWeS6o]]></openid>
   <sign><![CDATA[7921E432F65EB8ED0CE9755F0E86D72F]]></sign>
   <result_code><![CDATA[SUCCESS]]></result_code>
   <prepay_id><![CDATA[wx201411101639507cbf6ffd8b0779950874]]></prepay_id>
   <trade_type><![CDATA[JSAPI]]></trade_type>
</xml>

// ——如果微信返回的sign正确,则计算paysign,返回给前端
            let timeStamp = parseInt(new Date().getTime() / 1000);
            let paySignStr = 'appId=' + appid + '&nonceStr=' + orderResultObj.xml.nonce_str[0] +
                '&package=prepay_id=' + orderResultObj.xml.prepay_id[0] + '&signType=MD5&timeStamp=' + timeStamp + '&key=' +
                key;
            let paySign = md5(paySignStr).toUpperCase();
            //——如果业务结果返回成功,将package和返回结果返回前端
            const returnResult = {
                result_code: orderResultObj.xml.result_code[0],
                nonceStr: orderResultObj.xml.nonce_str[0],
                package: 'prepay_id=' + orderResultObj.xml.prepay_id[0],
                signType: 'MD5',
                timeStamp: timeStamp,
                paySign: paySign
            }

前端发起微信支付所需的参数为:

wx.chooseWXPay({
  timestamp: 0, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
  nonceStr: '', // 支付签名随机串,不长于 32 位
  package: '', // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=\*\*\*)
  signType: '', // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'
  paySign: '', // 支付签名
  success: function (res) {
    // 支付成功后的回调函数
  }
});

步骤4:支付结果通知

成功支付后,微信服务器会往我们指定的notify_url接口发送支付结果通知。

一般的通知数据格式为xml的,例如:

<xml>
  <appid><![CDATA[wx2421b1c4370ec43b]]></appid>
  <attach><![CDATA[支付测试]]></attach>
  <bank_type><![CDATA[CFT]]></bank_type>
  <fee_type><![CDATA[CNY]]></fee_type>
  <is_subscribe><![CDATA[Y]]></is_subscribe>
  <mch_id><![CDATA[10000100]]></mch_id>
  <nonce_str><![CDATA[5d2b6c2a8db53831f7eda20af46e531c]]></nonce_str>
  <openid><![CDATA[oUpF8uMEb4qRXf22hE3X68TekukE]]></openid>
  <out_trade_no><![CDATA[1409811653]]></out_trade_no>
  <result_code><![CDATA[SUCCESS]]></result_code>
  <return_code><![CDATA[SUCCESS]]></return_code>
  <sign><![CDATA[B552ED6B279343CB493C5DD0D78AB241]]></sign>
  <time_end><![CDATA[20140903131540]]></time_end>
  <total_fee>1</total_fee>
  <coupon_fee><![CDATA[10]]></coupon_fee>
  <coupon_count><![CDATA[1]]></coupon_count>
  <coupon_type><![CDATA[CASH]]></coupon_type>
  <coupon_id><![CDATA[10000]]></coupon_id>
  <trade_type><![CDATA[JSAPI]]></trade_type>
  <transaction_id><![CDATA[1004400740201409030005092168]]></transaction_id>
</xml>

实际中,我们会将xml解析成json格式便于后续处理,数据示例如下:

# 一张优惠券
{  
   appid: 'wx831244257d43',
   attach: 'eshop',
   bank_type: 'PAB_CREDIT',
   cash_fee: '7900',
   coupon_count: '1',
   coupon_fee: '100',
   coupon_fee_0: '100',
   coupon_id_0: '15399597',
   fee_type: 'CNY',
   is_subscribe: 'Y',
   mch_id: '17842',
   nonce_str: 'zleoa6t35xt',
   openid: 'oMkjnFXFsOyzdzmSPi',
   out_trade_no: '20200826130332000657',
   result_code: 'SUCCESS',
   return_code: 'SUCCESS',
   sign: 'CFC6804EABEADD4EE8473820F0C3B6BB',
   time_end: '20200826130346',
   total_fee: '8000',
   trade_type: 'JSAPI',
   transaction_id: '4200000687202008265017514' 
}
# 两张优惠券
{ "appid": "wx831244257d43",
   "attach": "eshop",
   "bank_type": "SPDB_DEBIT",
   "cash_fee": "135724",
   "coupon_count": "2",
   "coupon_fee": "276",
   "coupon_fee_0": "168",
   "coupon_fee_1": "108",
   "coupon_id_0": "15343260249",
   "coupon_id_1": "15344206293",
   "fee_type": "CNY",
   "is_subscribe": "Y",
   "mch_id": "17842",
   "nonce_str": "r59jjd2xq9c",
   "openid": "oMkjnFXFsOyzdzmSPi-DroM",
   "out_trade_no": "20200826130332000857",
   "result_code": "SUCCESS",
   "return_code": "SUCCESS",
   "sign": "B3F13AA91F5F03FAF77E1F5537A412D3",
   "time_end": "20200826144436",
   "total_fee": "136000",
   "trade_type": "JSAPI",
   "transaction_id": "4200000703202008260568866" 
}
# 无优惠券
{  
   appid: 'wx831244257d43',
   attach: 'eshop',
   bank_type: 'OTHERS',
   cash_fee: '4',
   fee_type: 'CNY',
   is_subscribe: 'Y',
   mch_id: '17842',
   nonce_str: 'htlc0rpbsk',
   openid: 'oMkjnFXFsOyzdzmSPi',
   out_trade_no: '20200826130332000957',
   result_code: 'SUCCESS',
   return_code: 'SUCCESS',
   sign: 'F6A8A4F411493024BEB6814E4548495E',
   time_end: '20200824175952',
   total_fee: '4',
   trade_type: 'JSAPI',
   transaction_id: '4200000680202008241255457' 
}

步骤5:查询订单

具体示例:

# url
https://api.mch.weixin.qq.com/pay/orderquery

# POST DATA
<xml>
   <appid>wx7084f3353e9d7d43</appid>
   <mch_id>1482346262</mch_id>
   <nonce_str>ec2316275641faa3aacf3cc599e8730f</nonce_str>
   <transaction_id>4200000687202008265017546514</transaction_id>
   <sign>18B82473892DE4604F34A8B3ED154FF1</sign>
</xml>

# RETURN
<xml>
    <return_code>
        <![CDATA[SUCCESS]]>
    </return_code>
    <return_msg>
        <![CDATA[OK]]>
    </return_msg>
    <appid>
        <![CDATA[wx7084f3353e9d7d43]]>
    </appid>
    <mch_id>
        <![CDATA[1482346262]]>
    </mch_id>
    <nonce_str>
        <![CDATA[UZKAG2Z3TU61ujNz]]>
    </nonce_str>
    <sign>
        <![CDATA[4E104AD14E6788A437D5C3556F2CE8A2]]>
    </sign>
    <result_code>
        <![CDATA[SUCCESS]]>
    </result_code>
    <openid>
        <![CDATA[oMhDkjnFXFsOL8CtyzdzSbmSPi6A]]>
    </openid>
    <is_subscribe>
        <![CDATA[Y]]>
    </is_subscribe>
    <trade_type>
        <![CDATA[JSAPI]]>
    </trade_type>
    <bank_type>
        <![CDATA[PAB_CREDIT]]>
    </bank_type>
    <total_fee>8000</total_fee>
    <coupon_fee>100</coupon_fee>
    <fee_type>
        <![CDATA[CNY]]>
    </fee_type>
    <transaction_id>
        <![CDATA[4200000687202008265017546514]]>
    </transaction_id>
    <out_trade_no>
        <![CDATA[20200826130332000159]]>
    </out_trade_no>
    <attach>
        <![CDATA[pheshop]]>
    </attach>
    <time_end>
        <![CDATA[20200826130347]]>
    </time_end>
    <trade_state>
        <![CDATA[SUCCESS]]>
    </trade_state>
    <coupon_id_0>
        <![CDATA[15139195597]]>
    </coupon_id_0>
    <coupon_fee_0>100</coupon_fee_0>
    <coupon_count>1</coupon_count>
    <cash_fee>7900</cash_fee>
    <trade_state_desc>
        <![CDATA[支付成功]]>
    </trade_state_desc>
    <cash_fee_type>
        <![CDATA[CNY]]>
    </cash_fee_type>
</xml>

文章来源:

Author:大官人
link:https://www.daguanren.cc/post/2020-xin-wei-xin-zhi-fu.html