C#开发微信门户及应用(39)--使用微信JSSDK实现签到的功能

随着微信开逐步开放更多JSSDK的接口,我们可以利用自定义网页的方式来调用更多微信的接口,实现我们更加丰富的界面功能和效果,例如我们可以在页面中调用各种手机的硬件来获取信息,如摄像头拍照,GPS信息、扫描二维码等等,本篇介绍如何利用这些JSSDK接口实现签到的功能,其中签到需要报送地理坐标和地址,调用摄像头实时拍照,以及获取当前用户的相关信息等等。

1、JSSDK的说明

微信JS-SDK是微信公众平台面向网页开发者提供的基于微信内的网页开发工具包。通过使用微信JS-SDK,网页开发者可借助微信高效地使用拍照、选图、语音、位置等手机系统的能力,同时可以直接使用微信分享、扫一扫、卡券、支付等微信特有的能力,为微信用户提供更优质的网页体验。
目前JSSDK支持的接口分类包括下面几类:基础接口、分享接口、图像接口、音频接口、智能接口、设备信息、地理位置、摇一摇周边、界面操作、微信扫一扫、微信小店、微信卡券、微信支付,随着微信功能的全部整合,估计更多的接口会陆续开放出来。
在微信的后台进入【开发者文档】模块,我们可以看到对应的JSSDK的功能分类和介绍,如下所示。


从右侧我们可以详细看到各个接口的使用说明,基本上JSSDK的使用方法都类似,因此调试通过并掌握其中一两个,其他的也就依葫芦画瓢,照着做就可以了。
1)JSSDK使用步骤
步骤一:绑定域名
先登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。如下所示,在公众平台进行设置。

备注:登录后可在“开发者中心”查看对应的接口权限。

步骤二:引入JS文件
在需要调用JS接口的页面引入如下JS文件,(支持https):http://res.wx.qq.com/open/js/jweixin-1.0.0.js
如需使用摇一摇周边功能,请引入 http://res.wx.qq.com/open/js/jweixin-1.1.0.js
备注:支持使用 AMD/CMD 标准模块加载方法加载
当然,我们一般编辑页面,为了方便实现更多的效果,可能还会引入其他JS,如JQuery的类库等等。还有,我们还可以基于WeUI的jquery-weui类库,实现更加丰富的功能,如下是我们案例代码里面的JS引用。

<script src="~/Content/wechat/jquery-weui/lib/jquery-2.1.4.js"></script>
<script src="~/Content/wechat/jquery-weui/js/jquery-weui.js"></script>
<script type="text/javascript" src="http://res.wx.qq.com/open/js/jweixin-1.1.0.js"></script>

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

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

});

以上的配置就是JSSDK的核心所在,里面需要配置好对应的appid,还有timestamp,nonceStr这些都没有特别之处,最值得注意的是signature的实现机制,这样我们在后台生成好对应的值,赋给JS页面就可以了,这样也是最为安全的做法。

如下代码是我们实际项目里面,在Asp.net的视图页面里面的HTML代码,如下所示。

<script language="javascript">
var appid = '@ViewBag.appid';
var noncestr = '@ViewBag.noncestr';
var signature = '@ViewBag.signature';
var timestamp = '@ViewBag.timestamp';

    wx.config({
        debug: false,
        appId: appid, // 必填,公众号的唯一标识
        timestamp: timestamp, // 必填,生成签名的时间戳
        nonceStr: noncestr, // 必填,生成签名的随机串
        signature: signature, // 必填,签名,见附录1
        jsApiList: [
           'checkJsApi',
           'onMenuShareTimeline',
           'onMenuShareAppMessage',
           'onMenuShareQQ',
           'onMenuShareWeibo',
           'onMenuShareQZone',
           'hideMenuItems',
           'showMenuItems',
           'hideAllNonBaseMenuItem',
           'showAllNonBaseMenuItem',
           'translateVoice',
           'startRecord',
           'stopRecord',
           'onVoiceRecordEnd',
           'playVoice',
           'onVoicePlayEnd',
           'pauseVoice',
           'stopVoice',
           'uploadVoice',
           'downloadVoice',
           'chooseImage',
           'previewImage',
           'uploadImage',
           'downloadImage',
           'getNetworkType',
           'openLocation',
           'getLocation',
           'hideOptionMenu',
           'showOptionMenu',
           'closeWindow',
           'scanQRCode',
           'chooseWXPay',
           'openProductSpecificView',
           'addCard',
           'chooseCard',
           'openCard'
        ]
    });

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

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

这个ready的接口,也就是在页面顺利加载完毕后的处理内容了,一般我们需要做很多操作,都是需要在页面加载完毕后才能调用相关的对象进行赋值、处理等操作。

例如我们在页面ready后,获取对应的GPS坐标等操作,可以用下面的JS代码实现。

wx.ready(function () {
    wx.getLocation({
        type: 'wgs84', // 默认为wgs84的gps坐标,如果要返回直接给openLocation用的火星坐标,可传入'gcj02'
        success: function (res) {
            var latitude = res.latitude; // 纬度,浮点数,范围为90 ~ -90
            var longitude = res.longitude; // 经度,浮点数,范围为180 ~ -180。
            var speed = res.speed; // 速度,以米/每秒计
            var accuracy = res.accuracy; // 位置精度
            $("#lblLoacation").text(latitude + "," + longitude);
        }
    });
});

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

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

这个error接口也就是用来处理异常信息的,一般情况下可以在这里提示用户出现的错误。

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

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

步骤2. 对string1进行sha1签名,得到signature:

0f9de62fce790f9a083d5c99e95740ceb90c27ed

注意事项
1.签名用的noncestr和timestamp必须与wx.config中的nonceStr和timestamp相同。
2.签名用的url必须是调用JS接口页面的完整URL。
3.出于安全考虑,开发者必须在服务器端实现签名的逻辑。
如出现invalid signature 等错误详见附录5常见错误及解决办法。
以上就是JSSDK总体的使用流程,虽然看起来比较抽象,但是基本上也就是这些步骤了。

上面的过程是具体的参数处理逻辑,我们要对应到C#代码的签名实现,需要对几个变量进行处理,下面是对应的生成noncestr、timestamp、以及签名等操作的代码。

/// <summary>
/// 生成时间戳,标准北京时间,时区为东八区,自1970年1月1日 0点0分0秒以来的秒数
/// </summary>
/// <returns>时间戳</returns>
private static string GetTimeStamp()
{
    TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
    return Convert.ToInt64(ts.TotalSeconds).ToString();
}

/// <summary>
/// 生成随机串,随机串包含字母或数字
/// </summary>
/// <returns>随机串</returns>
private static string GetNonceStr()
{
    return Guid.NewGuid().ToString().Replace("-", "");
}

还有我们要实现JSSDK签名的处理,必须先根据几个变量,构建好URL字符串,具体的处理过程,我们可以把它们逐一放在一个Hashtable里面,如下代码所示。

/// <summary>
/// 获取JSSDK所需要的参数信息,返回Hashtable结合
/// </summary>
/// <param name="appId">微信AppID</param>
/// <param name="jsTicket">根据Token获取到的JSSDK ticket</param>
/// <param name="url">页面URL</param>
/// <returns></returns>
public static Hashtable GetParameters(string appId, string jsTicket, string url)
{
    string timestamp = GetTimeStamp();
    string nonceStr = GetNonceStr();

    // 这里参数的顺序要按照 key 值 ASCII 码升序排序  
    string rawstring = "jsapi_ticket=" + jsTicket + "&noncestr=" + nonceStr + "&timestamp=" + timestamp + "&url=" + url + "";

    string signature = GetSignature(rawstring);
    Hashtable signPackage = new Hashtable();
    signPackage.Add("appid", appId);
    signPackage.Add("noncestr", nonceStr);
    signPackage.Add("timestamp", timestamp);
    signPackage.Add("url", url);
    signPackage.Add("signature", signature);
    signPackage.Add("jsapi_ticket", jsTicket);
    signPackage.Add("rawstring", rawstring);

    return signPackage;
}

我们注意到URL参数的字符串组合:

string rawstring = "jsapi_ticket=" + jsTicket + "&noncestr=" + nonceStr + "&timestamp=" + timestamp + "&url=" + url + "";

这里我们拼接好URL参数后,就需要使用签名的规则来实现签名的处理了,签名的代码如下所示,注释代码和上面代码等同。

/// <summary>
/// 使用SHA1哈希加密算法生成签名
/// </summary>
/// <param name="rawstring">待处理的字符串</param>
/// <returns></returns>
private static string GetSignature(string rawstring)
{
    return FormsAuthentication.HashPasswordForStoringInConfigFile(rawstring, "SHA1").ToLower();

    ////下面和上面代码等价
    //SHA1 sha1 = new SHA1CryptoServiceProvider();
    //byte[] bytes_sha1_in = System.Text.UTF8Encoding.Default.GetBytes(rawstring);
    //byte[] bytes_sha1_out = sha1.ComputeHash(bytes_sha1_in);
    //string signature = BitConverter.ToString(bytes_sha1_out);
    //signature = signature.Replace("-", "").ToLower();
    //return signature;
}

这样我们有了对应的值后,我们就可以把它们的参数全部放在集合里面了供使用。

/// <summary>
/// 获取用于JS-SDK的相关参数列表(该方法对accessToken和JSTicket都进行了指定时间的缓存处理,多次调用不会重复生成)
/// 集合里面包括jsapi_ticket、noncestr、timestamp、url、signature、appid、rawstring
/// </summary>
/// <param name="appid">应用ID</param>
/// <param name="appSecret">开发者凭据</param>
/// <param name="url">页面URL</param>
/// <returns></returns>
public Hashtable GetJSAPI_Parameters(string appid, string appSecret, string url)
{
    string accessToken = GetAccessToken(appid, appSecret);
    string jsTicket = GetJSAPI_Ticket(accessToken);

    return JSSDKHelper.GetParameters(appid, jsTicket, url);
}

下面我们通过具体的代码案例来介绍使用的过程。

2、签到功能的实现处理

其实签到,都可以在微信公众号和企业号实现,微信的企业号可能实现更佳一些,不过他们使用JSSDK的接口操作是一样的,我们可以拓展过去就可以了。这里介绍微信公众号JSSDK实现签到的功能处理。
签到的功能,我们希望记录用户的GPS位置信息,还有就是利用拍照功能,拍一个照片同时上传到服务器,这样我们就可以实现整个业务效果了。
首先我们来设计签到的界面,代码及效果如下所示。



界面预览效果如下所示:



我们来看看微信JSSDK里面对于【获取地理位置接口】的说明:
wx.getLocation({
    type: 'wgs84', // 默认为wgs84的gps坐标,如果要返回直接给openLocation用的火星坐标,可传入'gcj02'
    success: function (res) {
        var latitude = res.latitude; // 纬度,浮点数,范围为90 ~ -90
        var longitude = res.longitude; // 经度,浮点数,范围为180 ~ -180。
        var speed = res.speed; // 速度,以米/每秒计
        var accuracy = res.accuracy; // 位置精度
    }
});

以及图形接口里面【拍照或从手机相册中选图接口】的说明:

wx.chooseImage({
    count: 1, // 默认9
    sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
    sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
    success: function (res) {
        var localIds = res.localIds; // 返回选定照片的本地ID列表,localId可以作为img标签的src属性显示图片
    }
});

上传图片到微信服务器接口如下所示。

wx.uploadImage({
    localId: '', // 需要上传的图片的本地ID,由chooseImage接口获得
    isShowProgressTips: 1, // 默认为1,显示进度提示
    success: function (res) {
        var serverId = res.serverId; // 返回图片的服务器端ID
    }
});

备注:上传图片有效期3天,可用微信多媒体接口下载图片到自己的服务器,此处获得的 serverId 即 media_id。

根据这几个接口,我们来对它们进行包装,以实现我们的业务需求。根据我们的需要,我们对JSSDK接口进行了调用,如下所示。

<script language="javascript">
var appid = '@ViewBag.appid';
var noncestr = '@ViewBag.noncestr';
var signature = '@ViewBag.signature';
var timestamp = '@ViewBag.timestamp';

    wx.config({
        debug: false,
        appId: appid, // 必填,公众号的唯一标识
        timestamp: timestamp, // 必填,生成签名的时间戳
        nonceStr: noncestr, // 必填,生成签名的随机串
        signature: signature, // 必填,签名,见附录1
        jsApiList: [
           'checkJsApi',
           'chooseImage',
           'previewImage',
           'uploadImage',
           'downloadImage',
           'getNetworkType',
           'openLocation',
           'getLocation'
        ]
    });

    wx.ready(function () {
        wx.getLocation({
            type: 'wgs84', // 默认为wgs84的gps坐标,如果要返回直接给openLocation用的火星坐标,可传入'gcj02'
            success: function (res) {
                var latitude = res.latitude; // 纬度,浮点数,范围为90 ~ -90
                var longitude = res.longitude; // 经度,浮点数,范围为180 ~ -180。
                var speed = res.speed; // 速度,以米/每秒计
                var accuracy = res.accuracy; // 位置精度
                $("#lblLoacation").text(latitude + "," + longitude);

                //解析坐标地址
                var location = latitude + "," + longitude;
                $.ajax({
                    type: 'GET',
                    url: '/JSSDKTest/GetAddress?location=' + location,
                    //async: false, //同步
                    //dataType: 'json',
                    success: function (json) {
                        $("#lblAddress").text(json);
                    },
                    error: function (xhr, status, error) {
                        $.messager.alert("提示", "操作失败" + xhr.responseText); //xhr.responseText
                    }
                });
            }
        });
        wx.getNetworkType({
            success: function (res) {
                var networkType = res.networkType; // 返回网络类型2g,3g,4g,wifi
                $("#lblNetwork").text(networkType);
            }
        });
        
        chooseImage();
    });
</script>

其中的chooseImage()是我们在页面开始的时候,让用户拍照的操作,具体JS代码如下所示。

//拍照显示
var localIds;
function chooseImage() {
    wx.chooseImage({
        count: 1, // 默认9
        sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
        sourceType: ['camera'], // 可以指定来源是相册还是相机,默认二者都有
        success: function (res) {
            localIds = res.localIds; // 返回选定照片的本地ID列表,localId可以作为img标签的src属性显示图片
            $("#imgUpload").attr("src", localIds);
        }
    });
}

但用户使用摄像头拍照后,就会返回一个res.localIds集合,因为我们拍照一个,那么可以把它直接赋值给图片对象,让它显示当前拍照的图片。

拍照完成,我们单击【签到】应该把图片和相关的坐标等信息上传到服务器的,图片首先是保存在微信服务器的,上传图片有效期3天,可用微信多媒体接口下载图片到自己的服务器,此处获得的 serverId 即 media_id。

为了实现我们自己的业务数据,我们需要把图片集相关信息存储在自己的服务器,这样才可以实现信息的保存,最后提示【签到操作成功】,具体过程如下所示。

//上传图片
var serverId;
function upload() {
    wx.uploadImage({
        localId: localIds[0],
        success: function (res) {
            serverId = res.serverId;

            //提交数据到服务器

            //提示信息
            $.toast("签到操作成功");
        },
        fail: function (res) {
            alert(JSON.stringify(res));
        }
    });
}

另外,我们为了实现单击图片控件,实现重新拍照的操作,以及签到的事件处理,我们对控件的单击处理进行了绑定,如下代码所示。

document.querySelector('#imgUpload').onclick = function () {
    chooseImage();
};

$(document).on("click", "#btnSignIn", function () {
    if (localIds == undefined || localIds== null) {
        $.toast('请先拍照', "forbidden");
        return;
    }
    //调用上传图片获得媒体ID
    upload();
});

如果对这个《C#开发微信门户及应用》系列感兴趣,可以关注我的其他文章.

推荐阅读更多精彩内容