wenmo8 发布的文章

在现今很多网站里面,都使用了微信开放平台的扫码登录认证处理,这样做相当于把身份认证交给较为权威的第三方进行认证,在应用网站里面可以不需要存储用户的密码了。本篇介绍如何基于微信开放平台的扫码进行网站的登陆处理。

1、开放平台的认证

要使用网站的扫码登录处理,就需要先进行微信开放平台帐号的开发者资质认证,提交相关的资料,以及交付每年300元的认证费用。

认证后,建立相关的网站应用后,就有相关的APPID和APPSecret了,这些关键的参数就可以用来获取相关的用户信息了。

网站应用的应用详情界面如下所示。

整个开放平台感觉没有多少东西,不过需要收费认证才能使用这些功能,感觉不是很爽。

我们采用的扫码登录,需要通过开放平台获取用户的信息,因此还需要设置获取用户基本信息接口的域名,否则无法获取信息,从而会导致重定向出错。

设置域名在【接口权限】【网页账号】【网页授权获取用户基本信息】的修改入口,如下图所示。

然后在弹出的对话框里面输入授权回调的域名即可。

这样设置就可以确保获取到用户信息了。

2、扫码登录的说明和具体使用

网站应用微信登录是基于
OAuth2.0协议标准
构建的微信OAuth2.0授权登录系统。

在进行微信OAuth2.在进行微信OAuth2.0授权登录接入之前,在微信开放平台注册开发者帐号,并拥有一个已审核通过的网站应用,并获得相应的AppID和AppSecret,申请微信登录且通过审核后,可开始接入流程。

微信OAuth2.0授权登录让微信用户使用微信身份安全登录第三方应用或网站,在微信用户授权登录已接入微信OAuth2.0的第三方应用后,第三方可以获取到用户的接口调用凭证(access_token),通过access_token可以进行微信开放平台授权关系接口调用,从而可实现获取微信用户基本开放信息和帮助用户实现基础开放功能等。

微信OAuth2.0授权登录目前支持authorization_code模式,适用于拥有server端的应用授权。该模式整体流程为:

1. 第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数;2. 通过code参数加上AppID和AppSecret等,通过API换取access_token;3. 通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。

获取access_token时序图:

从上图我们可以大概了解整个扫码登陆的处理过程。

3、扫码登录的各个步骤处理

1)用户身份的绑定

为了实现二维码扫码登录,我们需要在现有系统里面绑定用户的微信,这样才能在用户扫码的时候,判断用户的身份从而实现自动登录的过程。

我们可以在用户管理里面进行统一设置,也可以在常规用户登录(用户名+密码)后进行设置,这个主要看我们是否需要保留用户名密码登陆这种方式。

例如可以在用户管理里面统一绑定,也就是在创建用户的时候,让用户绑定下微信,获取微信的唯一标识。

也可以在保留用户名密码的登陆方式外,让用户登陆系统后自行进行绑定微信即可。

上面的界面,就是在一个页面里面弹出一个层,然后请求二维码显示即可,如下界面代码所示。

        <divid="divWechat"class="easyui-dialog"style="width:450px;height:350px;padding:10px 20px"closed="true"resizable="true"modal="true"iconcls="icon-setting">
            <div>
                <h4>扫描用户二维码,进行绑定</h4>
            </div>
            <divalign="center">
                <imgid="imgQRcode"alt="使用微信扫码进行绑定"style="height:200px;width:auto" />
            </div>

            <divalign="right">
                <ahref="javascript:void(0)"class="easyui-linkbutton"iconcls="icon-cancel"onclick="javascript: $('#divWechat').dialog('close')">关闭</a>
            </div>
        </div>

上面的层在打开的时候,我们使用JS来动态获取二维码进行显示,具体JS代码如下所示。

    //绑定微信登陆
    functionBindWechat() {var url = "http://www.iqidi.com/H5/BindWechat?id=@Session["UserID"]";
url
=encodeURIComponent(url);
$(
"#imgQRcode").attr("src", "/H5/QR?url=" + url);//打开绑定窗口 $("#divWechat").dialog('open').dialog('setTitle', '使用微信扫码进行绑定');
}

上面的JS只是做前端的数据请求和显示,具体的QR动作Action其实就是生成扫描二维码的过程,这个二维码其实就是采用通用的方式,来构建一个指向我们绑定账号的地址,从而实现我们绑定账号的判断,二维码的生成过程如下所示。

        /// <summary>
        ///转换二维码连接为图片格式/// </summary>
        /// <param name="url">二维码连接</param>
        /// <returns></returns>
[HttpGet]public ActionResult QR(stringurl)
{
//初始化二维码生成工具 QRCodeEncoder qrCodeEncoder = newQRCodeEncoder();
qrCodeEncoder.QRCodeEncodeMode
=QRCodeEncoder.ENCODE_MODE.BYTE;
qrCodeEncoder.QRCodeErrorCorrect
=QRCodeEncoder.ERROR_CORRECTION.M;
qrCodeEncoder.QRCodeVersion
= 0;
qrCodeEncoder.QRCodeScale
= 4;//将字符串生成二维码图片 var image =qrCodeEncoder.Encode(url, Encoding.Default);//保存为PNG到内存流 MemoryStream ms = newMemoryStream();
image.Save(ms, ImageFormat.Png);
image.Dispose();
return File(ms.ToArray(), "image/Png");
}

为了实现用户的绑定,我们需要获取当前用户的身份信息,因此需要在BindWeChat的操作里面做一个转向处理,如下接口所示。

        /// <summary>
        ///生成绑定微信的地址/// </summary>
        /// <returns></returns>
        public ActionResult BindWechat()

这个函数处理里面,我们需要重新定向处理,我们把它定向到BindAccount函数里面,方便获取用户的openid和其他必要的信息。

另外我们基于微信开放平台的应用,建立了一个和微信账号信息的联系,因此创建数据库信息如下所示。

也就是一个具体的开放平台应用对应着一个具体的微信账号,这样我们就可以充分利用配置进行处理了。

上面提到的BindAccount的处理的逻辑就是获取必要的信息,然后在数据库层面对身份信息进行验证,具体代码如下所示。

        /// <summary>
        ///绑定用户微信号/// </summary>
        /// <param name="id">账号ID</param>
        /// <returns></returns>
        publicActionResult BindAccount()
{
WebAppInfo appInfo
=GetWebApp(ConfigData.WebAppId);
AccountInfo accountInfo
=GetAccount(appInfo.AccountNo);var htResult = GetOpenIdAndUnionId(accountInfo.UniteAppId, accountInfo.UniteAppSecret);//存储openid方便使用 string openid = htResult["openid"].ToString();var unionid = htResult["unionid"].ToString();var userid = Request.QueryString["id"];var state = Request.QueryString["state"]; if (!string.IsNullOrEmpty(openid) && !string.IsNullOrEmpty(userid))
{
CommonResult result
= BLLFactory<User>.Instance.BindUser(openid, unionid, userid.ToInt32());if(result.Success)
{
returnBindSuccess();
}
else{returnBindFail();
}
}
else{throw new WeixinException("无法获取openid" + string.Format(", openid:{0}, userid:{1}", openid, userid));
}
}

在绑定的过程,我们需要考虑绑定正确账号,重复绑定其他账号,无效绑定几种情况,如果成功绑定正确账号(可多次处理结果一样),那么得到界面如下所示(这个界面的样式采用了weui的样式)。


2)用户的扫码登录处理

上面绑定了账号后,就可以通过扫码进行登录了,扫码回调的时候我们有自己的判断处理,扫码界面如下所示(我们在保留用户名密码登陆的方式外,增加了一个扫码登录的处理)。

如果是Bootstrap的界面效果

如果是EasyUI的界面效果

这个和前面的二维码显示规则差不多,不过他们的连接地址是不同的,这个地方用到了开放平台的接口,也就是我们前面提到开放平台认证的接口了。

上面的扫码登录的界面代码如下所示。

    <!--二维码扫描登陆的界面层-->
    <divid="divWechat"class="easyui-dialog"style="width:550px;height:500px;padding:10px 20px"closed="true"resizable="true"modal="true"iconcls="icon-setting">
        <divid="login_container"align="center">
        </div>

        <divalign="right">
            <ahref="javascript:void(0)"class="easyui-linkbutton"iconcls="icon-cancel"onclick="javascript: $('#divWechat').dialog('close')">关闭</a>
        </div>
    </div>

上面代码需要引入JS文件,并使用微信JSSDK的API进行显示的。

    <!--使用微信扫码进行登陆-->
    <scriptsrc="http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"></script>
    <scriptlanguage="javascript">

        functionOpenJSLogin() {varobj= newWxLogin({
id:
"login_container",
appid:
"@ViewBag.appid",
scope:
"snsapi_login",
redirect_uri:
"@ViewBag.redirect_uri",
state:
"@ViewBag.state",
style:
"black",
href:
".impowerBox .qrcode {width: 200px;}"});//打开绑定窗口 $("#divWechat").dialog('open').dialog('setTitle','使用微信扫码进行登陆');
}
</script>

这个里面的参数,如APPID就是来源我们认证后的开放平台参数。

这些信息我们在MVC控制器后面获取后绑定在ViewBag,方便界面前端的使用。

            //使用JSLogin登陆
            WebAppInfo appInfo = BLLFactory<WebApp>.Instance.FindByID(ConfigData.WebAppId);
ArgumentValidation.CheckForNullReference(appInfo,
"Web应用程序appInfo");if (appInfo != null)
{
ViewBag.appid
=appInfo.OpenAppID;
ViewBag.redirect_uri
=appInfo.LoginCallBackUrl;
ViewBag.state
=ConfigData.AuthState;
}

其中的redirect_uri是通过数据库获取的LoginCallBackUrl地址,这个地址类似如下格式:
http://www.iqidi.com/H5/callback?uid=iqidiSoftware

也就是我们在开放平台处理返回后进行的回调处理。

通过开放平台的APPID和APPSecret,我们可以获取到对应的接口调用凭证,然后根据接口凭证,以及openid,获得用户的公众平台统一的UnionID,这个标识是我们用户的唯一标识,代码如下所示。

                var result =baseApi.GetAuthToken(appid, appsecret, code);if (result != null && !string.IsNullOrEmpty(result.openid))
{
openid
=result.openid;var unionResult =baseApi.GetSnsapiUserInfo(result.access_token, result.openid);

ht.Add(
"openid", openid);
ht.Add(
"unionid", unionResult != null ? unionResult.unionid : "");
}

有了unionid我们就可以根据这个标识在我们的用户数据库里面查找对应的用户,如下代码所示。

            //开放平台的OpenID,不是公众号的OpenID,需要转换为unionid
            if (!string.IsNullOrEmpty(openid) && !string.IsNullOrEmpty(unionid))
{
UserInfo userInfo
= BLLFactory<User>.Instance.FindByUnionId(unionid);

然后判断我们去到的用户信息是否正确,如下代码所示

                if (userInfo != null)
{
CommonResult loginResult
=CheckLogin(userInfo.Name);if (!loginResult.Success)
{
LogHelper.Info(
string.Format("用户登陆不成功,{0}", loginResult.ErrorMessage));
}
//登陆成功后的重定向地址 var url = appInfo.HomeUrl; //例如:http://www.iqidi.com/Home returnRedirect(url);
}

如果不成功,那么我们定向到指定的界面即可。

            //如不成功,最后都统一提示信息
            ViewBag.Error = "获取信息失败,登陆错误";return View("LoginError");

如果我们登陆成功后,需要设置一些Session信息或者Cookie信息,那么就可以通过CheckLogin函数进行处理即可。

以上就是我们结合微信开放平台实现微信扫码登录的过程,其中整个过程就是用到了下面几个过程。

1)使用JSSDK的脚本实现扫码获取code

JS微信登录主要用途:网站希望用户在网站内就能完成登录,无需跳转到微信域下登录后再返回,提升微信登录的流畅性与成功率。 网站内嵌二维码微信登录JS实现办法:

步骤1:在页面中先引入如下JS文件(支持https):

<scriptsrc="http://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js"></script>

步骤2:在需要使用微信登录的地方实例以下JS对象:

                          var obj = newWxLogin({
id:
"login_container",
appid:
"",
scope:
"",
redirect_uri:
"",
state:
"",
style:
"",
href:
""});

2) 第二步:通过code获取access_token

通过code获取access_token

https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

3)第三步:通过access_token调用接口

获取access_token后,进行接口调用,

对于接口作用域(scope),能调用的接口有以下:

授权作用域(scope) 接口 接口说明
snsapi_base /sns/oauth2/access_token 通过code换取access_token、refresh_token和已授权scope
/sns/oauth2/refresh_token 刷新或续期access_token使用
/sns/auth 检查access_token有效性
snsapi_userinfo /sns/userinfo 获取用户个人信息

其中snsapi_base属于基础接口,若应用已拥有其它scope权限,则默认拥有snsapi_base的权限。使用snsapi_base可以让移动端网页授权绕过跳转授权登录页请求用户授权的动作,直接跳转第三方网页带上授权临时票据(code),但会使得用户已授权作用域(scope)仅为snsapi_base,从而导致无法获取到需要用户授权才允许获得的数据和基础功能。

4)获取信息在回调界面中进行登录前处理

通过上面接口,我们可以获得相应的用户身份信息,因此可以结合我们用户数据库进行用户身份的认定和处理,并设置必要的Session或者Cookie信息等,最后定位到我们的应用主界面即可。

在很多情况下,我们利用IOC控制反转可以很方便实现一些接口的适配处理,可以在需要的时候切换不同的接口实现,使用这种方式在调用的时候,只需要知道相应的接口接口,具体调用哪个实现类,可以在配置文件中动态指定,本篇主要介绍AutoFac的IOC组件的使用,用来实现微信接口处理的控制反转功能。

我们知道,实
现IOC的方式有很多,如Unity、AutoFac、Ninject、Castle Windsor、Spring.NET等等,每种IOC组件均有自己的一些特点,我在之前的实体框架随笔系列介绍过Unity的使用《
Entity Framework 实体框架的形成之旅--利用Unity对象依赖注入优化实体框架(2)
》,本来也想用这个来实现微信的接口调用处理,不过由于其版本以及一些其他问题,总是没有那么方便,最后决定使用也比较流行,应用较多的的AutoFac组件来实现。

1、微信接口的处理需求

我们在使用微信公众号实现一些业务处理的时候,往往需要根据不同的条件进行不同的接口调用。

如通过二维码扫码的结果处理,然后呈现给微信用户的相关信息,有下面两种方式。

根据用户的扫码结果,我们可以自定义自己的业务处理,然后呈现给用户,那么这里使用IOC来实现具体的业务是比较好的,我们在具体的业务实现里面,可以根据不同的条件实现所需要的复杂处理。

当然我们还可以扩展到很多的业务接口里面,如百度的地理位置解析接口、电影院信息查询、天气信息查询、交通信息查询、旅游信息查询等,还有短信、邮件发送等常规接口,都可以使用这种方式进行处理。

接口的效果展示如下所示。

这些给其他项目模块使用的时候,我们可以在配置文件里面指定具体的接口实现信息,这种可以具体指定所需的实现。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <sectionname="autofac"type="Autofac.Configuration.SectionHandler, Autofac.Configuration"/>
  </configSections>
  <autofacdefaultAssembly="WHC.Common.Handler">
    <components>
      <componenttype="WHC.Common.Handler.TestHandler, WHC.Common.Handler"service="WHC.Common.Handler.ITestHandler" />
      <componenttype="WHC.Common.Handler.QRCodeHandler, WHC.Common.Handler"service="WHC.Common.Handler.IQRCodeHandler" />
      <!--邮件短信-->
      <componenttype="WHC.Common.Handler.SmsSendHandler, WHC.Common.Handler"service="WHC.Common.Handler.ISmsHandler" />
      <componenttype="WHC.Common.Handler.MailSendHandler, WHC.Common.Handler"service="WHC.Common.Handler.IMailHandler" />
      
    </components>
  </autofac>
</configuration>

直接使用AutoFac的操作应该是比较方便,使用接口获取方式获取具体实现就可以了。

2、使用Autofac实现

为了方便使用Autofac,我们可以先在项目上的Nuget包管理,引用相关的DLL,其中包括核心的Autofac类库,以及读取配置文件的Autofac Configuration,后者为方便读取XML配置信息所必须。

引入这两个DLL就可以使用Autofac的功能了。

一般通过配置文件,初始化的Autofac组件的代码如下所示

    instance = newAutoFactory();//初始化相关的注册接口
    var builder = newContainerBuilder();//从配置文件注册相关的接口处理
    builder.RegisterModule(new ConfigurationSettingsReader("autofac", configurationFile));
container
= builder.Build();

而我们使用Autofac的接口也是很容易的,常规的使用代码如下所示。

            var handler = container.Resolve<ITestHandler>();
handler.Test(
"测试");

当然,为了方便,我们可以使用一个辅助类来简化这个接口的调用:在辅助类初始化的时候,我们从配置文件加载对应的组件接口实现,当我们需要解析具体接口的时候,就可以直接从Container容器里面胡获取了,辅助类代码如下所示。

    /// <summary>
    ///使用AutoFac的工厂类,通过配置/// </summary>
    public classAutoFactory
{
//普通局部变量 private static object syncRoot = newObject();//工厂类的单例 private static AutoFactory instance = null;//配置文件 private const string configurationFile = "autofac.config";/// <summary> ///IOC的容器,可调用来获取对应接口实例。/// </summary> public IContainer Container { get; set; }/// <summary> ///IOC容器工厂类的单例/// </summary> public staticAutoFactory Instatnce
{
get{if (instance == null)
{
lock(syncRoot)
{
if (instance == null)
{
instance
= newAutoFactory();//初始化相关的注册接口 var builder = newContainerBuilder();//从配置文件注册相关的接口处理 builder.RegisterModule(new ConfigurationSettingsReader("autofac", configurationFile));
instance.Container
=builder.Build();
}
}
}
returninstance;
}
}
/// <summary> ///测试的接口/// </summary> public voidTest()
{
var handler = AutoFactory.Instatnce.Container.Resolve<ITestHandler>();
handler.Test(
"测试");
}
}

3、外部接口实现及调用

这样我们所有的接口都定义好,并给每个定义的接口相应个实现就可以使用这个Autofac组件进行调用了。

    /// <summary>
    ///短信发送接口/// </summary>
    public interfaceISmsHandler
{
/// <summary> ///发送短信/// </summary> /// <param name="content">短信内容</param> /// <param name="mobiles">手机号码(多个号码用”,”分隔)</param> /// <param name="sendTime">预约发送时间</param> /// <returns></returns> CommonResult Send(string content, string mobiles, DateTime? sendTime = null);/// <summary> ///查询剩余条数/// </summary> /// <returns></returns> CommonResult GetLeftCount();
}
    /// <summary>
    ///邮件发送接口/// </summary>
    public interfaceIMailHandler
{
/// <summary> ///发送外部邮件(自定义邮件配置,如个人邮件)/// </summary> /// <param name="mailInfo">发送邮件信息</param> /// <param name="settingInfo">SMTP协议设置信息</param> /// <returns></returns> CommonResult Send(MailInfo mailInfo, SmtpSettingInfo settingInfo);/// <summary> ///发送外部邮件(系统配置,系统邮件)/// </summary> /// <param name="mailInfo">发送邮件信息</param> /// <returns></returns> CommonResult Send(MailInfo mailInfo);
}

例如,测试发送短信和邮件的IOC调用代码如下所示

            //使用IOC模块发送
            var sms = AutoFactory.Instatnce.Container.Resolve<ISmsHandler>();var smsTemplate = string.Format("验证码:{0}。尊敬的会员,您好,您正在注册会员,验证码2分钟内有效,感谢您的支持。", new Random().Next(100000));var result = sms.Send(smsTemplate, "18620292076");
Console.WriteLine(result.Success
? "发送短信成功" : "发送短信失败:" +result.ErrorMessage);

MailInfo info
= newMailInfo();
info.ToEmail
= "wuhuacong@163.com";
info.FromEmail
= "wuhuacong@163.com";
info.Subject
= "这是一份来自我自己的测试邮件";
info.Body
= info.Subject + ",这是内容部分。<a href='http://www.iqidi.com'>点击这里返回主页</a>";var mail = AutoFactory.Instatnce.Container.Resolve<IMailHandler>();var mailResult =mail.Send(info);
Console.WriteLine(mailResult.Success
? "发送邮件成功" : "发送邮件失败:" + mailResult.ErrorMessage);

测试后得到的结果如下:

邮件结果一样可以收到。

我们回到上面介绍的二维码扫描的业务实现效果,上面提到了,一个二维码事件可以派生出不同的接口实现,从而给不同的响应信息。

    /// <summary>
    ///扫码进行的处理/// </summary>
    public interfaceIQRCodeHandler
{
/// <summary> ///处理ScancodePush的事件/// </summary> /// <param name="info">扫描信息</param> /// <param name="accountInfo">账号信息</param> /// <returns></returns> stringHandleScancodePush(RequestEventScancodePush info, AccountInfo accountInfo);/// <summary> ///处理ScancodeWaitmsg的事件/// </summary> /// <param name="info">扫描信息</param> /// <param name="accountInfo">账号信息</param> /// <returns></returns> stringHandleScancodeWaitmsg(RequestEventScancodeWaitmsg info, AccountInfo accountInfo);
}

我们可以定义两个简单的接口处理,用来承接微信二维码扫描接口的处理操作。

这样我们在处理二维码扫描事件的时候,我们就可以把它分配到接口里面进行处理即可。

        /// <summary>
        ///扫码推事件的事件推送处理/// </summary>
        /// <param name="info">扫描信息</param>
        /// <returns></returns>
        public stringHandleEventScancodePush(RequestEventScancodePush info, AccountInfo accountInfo)
{
string result = "";var handler = AutoFactory.Instatnce.Container.Resolve<IQRCodeHandler>();if(handler != null)
{
result
=handler.HandleScancodePush(info, accountInfo);
}
returnresult;
}
/// <summary> ///扫码推事件且弹出“消息接收中”提示框的事件推送的处理/// </summary> /// <param name="info">扫描信息</param> /// <returns></returns> public stringHandleEventScancodeWaitmsg(RequestEventScancodeWaitmsg info, AccountInfo accountInfo)
{
string result = "";try{var handler = AutoFactory.Instatnce.Container.Resolve<IQRCodeHandler>();if (handler != null)
{
result
=handler.HandleScancodeWaitmsg(info, accountInfo);
}
}
catch(Exception ex)
{
LogHelper.Error(ex);
}
returnresult;
}

对于其中之一的接口处理,我们都可以把它分拆,根据扫描的事件键值Key进行不同的信息相应。

        /// <summary>
        ///扫描后,会等待事件处理结果返回给用户/// </summary>
        public stringHandleScancodeWaitmsg(RequestEventScancodeWaitmsg info, AccountInfo accountInfo)
{
ResponseText response
= newResponseText(info);
response.Content
= string.Format("您的信息为:{0},可以结合后台进行数据查询。", info.ScanCodeInfo.ScanResult);var result =response.ToXml();string devicecode = GetParam(info.ScanCodeInfo, "devicecode");//参数名为小写 if (!string.IsNullOrEmpty(devicecode))
{
switch(info.EventKey.ToLower())
{
case "device_view"://设备查看 {var deviceinfo = BLLFactory<Device>.Instance.FindByCode(devicecode);
response.Content
=ConvertDeviceInfo(deviceinfo);
result
=response.ToXml();
}
break;case "measure"://设备计量 {var deviceinfo = BLLFactory<Device>.Instance.FindByCode(devicecode);
response.Content
=ConvertMeasure(deviceinfo);
result
=response.ToXml();
}
break;case "repair"://设备报修,返回报修单号 {var content =ConvertRepaire(info, accountInfo, devicecode);
response.Content
=content;
result
=response.ToXml();
}
break;case "inventory"://设备盘点,转到盘点界面 {var content =ConvertInventory(info, accountInfo, devicecode);
response.Content
=content;
result
=response.ToXml();
}
break;case "maintain":break;case "check":break;case "device_add":break;
}
}
returnresult;
}

以上就是关于使用Autofac实现一些常规接口处理的实现,这种控制反转的方式,可以便于我们项目的开发效率,可以根据需要指定一些特定的实现处理即可,而且通过配置文件的方式加载,可以很方便的进行配置。

我们在开发微信相关的应用的时候,一般需要完善的基础模块支持,包括微信公众号,微信企业号,以及一些业务模块的支持,一般随着功能的增多,我们需要非常清晰的界定他们的关系。模块的分拆以及合并往往需要考虑的代码的重用,而且尽量做到简单而不重复。本篇随笔基于我的微信框架的各个模块的功能介绍以及他们关系的描述。

1、公众号模块的命名及相关关系

微信开发,我们首先需要利用我们的语言(这里是利用C#语言),为所有用到的API接口实现进一步的封装,方便使用,微信API模块包含的内容很多,大概可以分为下面的项目。

有了这些接口功能的封装类,只是万里长征的第一步,我们还需要围绕这些接口,以及我们的业务模块实现更多交互功能的。

我们在WHC.Weixin.Data模块里面,定义了包含公众号的消息分派处理接口,这个分派接口是对接收来自微信服务器的各种消息事件进行响应;另外该模块还包含一些常规的数据存储,如关注用户、菜单、文章内容等方面数据的存储,如下所示。

当然,这个WHC.Weixin.Data是集大成者,它需要使用WHC.Weixin.API的项目内容来做数据提交,同时也是需要使用内部的数据存储处理模块。

2、企业号模块的命名及相关关系

企业号的做法和公众号类似,也是需要对微信提供的各种API进行封装,方便我们后面的接口调用,不过企业号目前支持的功能相对公众号少一些,大概包括有基础接口、企业号应用接口、菜单管理、通讯录管理、消息管理、摇一摇周边等模块。随着企业号功能的逐步完善和加入,可能腾讯会加入更多的一些功能模块。

同样我们参考微信公众号的做法,也是建立一个数据存储管理的项目,作为微信消息事件的处理入口,同时也管理存储一些必须的数据,包括需要同步的用户、标签、部门等数据。

随着微信公众号和企业号的功能逐渐统一,很多接口的交互数据几乎是一样的,因此我们可以把公用的实体类部分作为一个独立的项目,方便公众号和企业号两个项目的共同使用,这个项目命名为
WHC.Common.Entity

它们几个项目关系如下所示。

项目目录如下所示,包括了基础模块、摇一摇红包、菜单及多媒体管理模块、消息请求模块、消息应答模块、摇一摇周边、微信支付等。

由于微信支付的接口实现,是在企业号和公众号相对比较独立的一个API接口群,因此我们可以微信支付部分独立作为一个接口实现来处理,公众号或者企业号需要的时候,包含进去使用即可。

我们把它命名为WHC.Common.API项目。

还有我在前面随笔《
C#开发微信门户及应用(42)--使用Autofac实现微信接口处理的控制反转处理
》里面介绍过的基于对外部接口和二维码扫码处理接口的封装项目,命名为
WHC.Common.Handler

整个插件业务接口包括:百度的地理位置解析接口、电影院信息查询、天气信息查询、交通信息查询、旅游信息查询等,还有短信、邮件发送等常规接口,都可以使用这种方式进行处理。接口的效果展示如下所示。

因此上面这些以WHC.Common命名的项目,基本上就是可以通用在公众号和企业号两边的项目模块了,它们包含前面介绍过的几个模块,如下所示。

当然,除了这些之外,我们做项目,一般还涉及到一些基础功能模块,如公用类库,以及附件管理、通讯录管理、权限管理模块等内容,我们可以把后者几个模块放在一起,组成基础模块。

3、微信界面项目

微信界面部分是前面模块组件的综合使用,在微信应用里面,一般需要使用80端口和微信服务器做交互,而这个同时往往也是我们项目的端口地址。

如果考虑的更好一些,我们可以采用
以Web API优先的理念来设计整个企业应用体系的。Web API作为整个纽带的核心,在整个核心层需要考虑到统一性、稳定性、以及安全性等方面因素。

这样我们不管是Web项目、Winform项目,还是移动项目,都可以通过共同的接口Web API进行接入,实现更多元化的后台管理或者是前端界面呈现了。

Web API层作为一个公共的接口层,我们就很好保证了各个界面应用层的数据一致性,如果考虑到响应式的集成处理,我们甚至可以把微信应用、APP应用、Web应用做层一套Web程序,即使为了利用各自应用的特殊性,也可以把这些应用做的很相似,这样就给用户提供了一个统一的界面表示方式,极大提高客户使用的界面体验效果,用户几乎不需要额外的界面学习,就可以熟悉整个应用体系的各个模块使用。

这样最终就回到了我前面随笔介绍过的《
Web API应用架构设计分析(1)
》、《
Web API应用架构设计分析(2)
》大平台方案了,目前我正在努力整合所有微信的接口和相关的应用在一个大的Web API平台上,逐渐整合我目前应用较为广泛的混合式开发框架的相关技术和理念。

这样全面构建的Web API作为核心层,可以在上面开发我们各种企业业务应用,实现我们一个大平台的整合和多元化的应用,甚至我们可以把部分模块外包给更加擅长的团队,我们只需要确保我们核心的Web API层安全、健壮且具有良好的扩展性即可。

在基于Bootstrap开发的项目中,鲜艳颜色的按钮,以及丰富的图表是很吸引人的特点,为了将这个特点发挥到极致,可以利用Bootstrap图标抽取到数据库里面,并在界面中进行管理和使用,这样我们可以把这些图标方便应用在各个页面部件上,如菜单模块,按钮界面,表单输入等多个场合进行使用。在前面随笔《
基于Metronic的Bootstrap开发框架经验总结(4)--Bootstrap图标的提取和利用
》中,我对如何抽取Bootstrap图标,并单独开发一个页面进行图表的管理,本随笔介绍如何在这个基础上进行优化,实现更方便的使用。

1、优化图标管理界面

在前面随笔中,我们介绍了对图标的抽取、图标管理界面等内容。我们再来回顾一下Bootstrap的图表类型,Bootstrap图标库里面分为了三类内容:

Font Awesome
:Bootstrap专用图标字体,Font Awesome 中包含的所有图标都是矢量的,也就可以任意缩放,避免了一个图标做多种尺寸的麻烦。CSS对字体可以设置的样式也同样能够运用到这些图标上了。

Simple Icons
:收集众多网站的Logo,并提供高质量、不同尺寸的png格式图片给广大网友,所有Icon版权归其所属公司。

Glyphicons
:包括200个符号字体格式图表集合,由Glyphicons提供,Glyphicons Halflings 一般是收费的,但是经过Bootstrap和Glyphicons作者之间的协商,允许开发人员不需要支付费用即可使用。

我们从样式表中抽取这几类图标信息,放到数据库里面,然后方便界面管理和选择处理。

在前面随笔介绍我的Bootstrap框架的时候,图标管理界面如下所示。

选择图标的时候,提供一个弹出的对话框显示分类不同的图标,让用户选择后返回即可。

虽然有了这些功能界面,能够降低我们寻找图标的过程,但是实际使用的时候,还是有一些不方便,因为寻找一个特定的图标,需要翻了很多页才能寻找到合适的,效率不高,通过了解我们自身的图标名称和它显示的内容还是有很大的关联关系的,因此我们应该提供一个显示名称的搜索,方便查询出来,并可以在查询列表中进行选择,这样就可以大大加快我们寻找Bootstrap图标的速度了。

这个界面比原来改进了很多,我们可以通过名称搜索,并获取数据库里面符合条件的图标进行分页显示,如果选择其中之一,可以把图标和名称显示在上面,这样可以方便我们使用。

查询的操作和其他分页的部分类似,把数据通过AJax方式获取后,在界面上进行分页显示即可。

                            <divclass="form-group">
                                <labelclass="control-label col-md-2">图标显示名称</label>
                                <divclass="input-icon col-md-3">
                                    <divclass="input-group">
                                        <divclass="input-icon ">
                                            <inputid="WHC_DisplayName"name="WHC_DisplayName"type="text"class="form-control"placeholder="显示名称...">
                                        </div>
                                        <spanclass="input-group-btn">
                                            <buttonid="btnSearch"class="btn btn-success"type="button"onclick="SearchDisplayName()">
                                                <iclass="glyphicon glyphicon-list"></i>查询</button>
                                        </span>
                                    </div>
                                </div>
                            </div>
        //根据名称查询图标
        functionSearchDisplayName()
{
var condition = "WHC_DisplayName=" + $("#WHC_DisplayName").val();
SearchCondition(currentPage, condition);
}
//图标查询 functionSearch(page) {var condition = "WHC_SourceType=SimpleLine";//SimpleLine,FontAwesome,Glyphicons SearchCondition(page, condition);
}
functionSearchCondition(page, condition) {var iconUrl = "/BootstrapIcon/FindWithPager?page=" + page + "&rows=" +rows;

$.getJSON(iconUrl
+ "&" + condition, function(data) {
$(
"#grid_body").html("");
$.each(data.rows,
function(i, item) {var tr = "<a href='javascript:;' onclick=\"SetIconClass('" + item.ClassName + "')\" class='icon-btn' title=\"" + item.DisplayName + "\">";
tr
+= " <i class=\"" + item.ClassName + " \" style=\"font-size: 2.2em\"></i>";// tr += "</a>";
$(
"#grid_body").append(tr);
});
var element = $('#grid_paging');if(data.total > 0) {var options ={
bootstrapMajorVersion:
3,
currentPage: page,
numberOfPages: rows,
totalPages: Math.ceil(data.total
/rows),
onPageChanged:
function(event, oldPage, newPage) {
SearchCondition(newPage, condition);
}
}
element.bootstrapPaginator(options);
}
else{
element.html(
"");
}
});
}

另外图标的样式我们也方便一起整合让它显示,如Primary Success Info Warning Danger等经典样式,当然我们也可以设置颜色使图标呈现更多的色彩。

相关的界面代码如下所示。

                                <divclass="input-icon col-md-6">
                                    <buttontype="button"class="btn btn-default"onclick="ChangeIconStyle('default')">Default</button>
                                    <buttontype="button"class="btn btn-primary"onclick="ChangeIconStyle('primary')">Primary</button>
                                    <buttontype="button"class="btn btn-success"onclick="ChangeIconStyle('success')">Success</button>
                                    <buttontype="button"class="btn btn-info"onclick="ChangeIconStyle('info')">Info</button>
                                    <buttontype="button"class="btn btn-warning"onclick="ChangeIconStyle('warning')">Warning</button>
                                    <buttontype="button"class="btn btn-danger"onclick="ChangeIconStyle('danger')">Danger</button>
                                </div>
    <script>
        //通过JS修改界面图标的显示和样式
        functionChangeIconStyle(style) {varicon=$("#WebIcon").val();if(style!= 'default') {
$(
"#i_WebIcon").attr("class", icon+ "icon-state-" +style);
$(
"#lbl_WebIcon").attr("class","label label-" +style);
$(
"#lbl_WebIcon").text(icon+ "icon-state-" +style);
}
else{
$(
"#i_WebIcon").attr("class", icon);
$(
"#lbl_WebIcon").attr("class","");
$(
"#lbl_WebIcon").text(icon);
}
}
</script>

2、图标的应用场景

有了这种方便选择图标的管理界面,可以极大提高我们的效率。我们可以在菜单、按钮等多个地方使用图标,使得界面更加美观。

如在菜单界面中使用如下所示。

或者可以左侧菜单进行使用。

我们还可以把图标用在界面的功能按钮上。

如果感兴趣Bootstrap开发框架系列,可以参考学习下面的文章,感谢您的阅读。

在MVC控制器里面使用dynamic和ExpandoObject,实现数据转义的输出

在我们开发微信页面的时候,需要大量用到了各种呈现的效果,一般可以使用Boostrap的效果来设计不同的页面,不过微信团队也提供很多这方面的资源,包括JSSDK的接口,以及Weui的页面样式和相关功能页面,给我们提供了很大的便利,本文是在自己做的一些H5微信应用页面上做了一些功能总结,希望能够给大家提供一定的帮助。

1、微信网页开发

1) JSSDK

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

目前JSSDK支持的接口分类包括下面几类:基础接口、分享接口、图像接口、音频接口、智能接口、设备信息、地理位置、摇一摇周边、界面操作、微信扫一扫、微信小店、微信卡券、微信支付,随着微信功能的全部整合,估计更多的接口会陆续开放出来。

2)WeUI和Jquery WeUI

WeUI
是一套同微信原生视觉体验一致的基础样式库,由微信官方设计团队为微信内网页开发量身设计,可以令用户的使用感知更加统一。在微信网页开发中使用 WeUI,有如下优势:

  • 同微信客户端一致的视觉效果,令所有微信用户都能更容易地使用你的网站
  • 便捷获取快速使用,降低开发和设计成本
  • 微信设计团队精心打造,清晰明确,简洁大方

该样式库目前包含 button、cell、dialog、progress、toast、article、icon 等各式元素,已经在
GitHub
上开源。访问
http://weui.github.io/weui/
或微信扫码即可预览。

jQuery WeUI
中使用的是官方 WeUI 的 CSS 代码,并提供了 jQuery/Zepto 版本的 API 实现。JQuery WeUI相对于在官方WeUI的基础上做了一些功能扩展,已丰富界面设计和相关功能,因此我们可以考虑直接基于JQuery  WeUI的基础上进行页面开发即可。

在我前面的一些案例中,都利用了We UI样式来进行很多微信H5页面的功能设计,包括微信支付页面、签到页面等等。

如微信支付页面如下所示:



以及签到页面效果如下所示。


当然我们可以根据业务需要,增加很多这样和微信色调样式一致的页面,这个就是利用WeUI样式带来的界面体验一致性的好处。

本篇主要介绍微信H5页面开发的经验总结,上面提到了利用JSSDK和WeUI来对微信应用的H5页面进行开发,因此下面的相关效果也就是利用这些技术进行处理的。

2、判断微信浏览器

有些情况下,我们可能需要用户只能在微信浏览器上打开,不能用其他浏览器去打开连接,还有就是基于一些用户身份信息的获取,也是需要通过微信浏览器才能重定向获取的,否则使用其他浏览器会出错,因此判断是否为微信浏览器有时候也是一个常规的做法。

判断是否为微信浏览器有两种方式可以达到目的,一个是在前端使用JS脚本去处理,一个是采用后台C#代码进行判断处理,两种均可以达到目的。

使用JS代码实现代码和效果如下所示。

    //判断是否在微信中打开
    functionisWeiXin() {var ua =window.navigator.userAgent.toLowerCase();if (ua.match(/MicroMessenger/i) == 'micromessenger') {return true;
}
else{return false;
}
}

在页面输出处理如下所示。

        $(function() {var isWeixin =isWeiXin();if(isWeixin) {
$(
"#weixin").html(window.navigator.userAgent); //请在微信中打开该页面 }var display = "是否在微信浏览器中打开:";
display
+= isWeixin ? "是" : "否";
$(
"#isWeixin").html(display);
});

如果是正常使用微信跳转的页面链接,那么上会提示为:是。

刚才提到了,使用C#后台代码也可以判断是否在浏览器内,一般情况下,我们可以 判断用户的浏览器后做重定向,如果用户确实是微信浏览器的,则继续后面处理,否则重定向到提示页面给用户。

        /// <summary>
        ///检查是否微信中打开,否则重定向/// </summary>
        /// <returns></returns>
        protected stringCheckBrowser()
{
bool passed = false;string userAgent =Request.UserAgent;

passed
= userAgent.ToLower().Contains("micromessenger");if (!passed)
{
var type = "warn";var message = "请在微信中打开该页面";var url = string.Format("{0}/H5/info?type={1}&message={2}", ConfigData.WebsiteDomain, type, message);returnurl;
}
return null;
}

我们就可以在函数开始部分进行判断即可。

                //如果不是微信浏览器,则返回错误页面
                var checkUrl =CheckBrowser();if (!string.IsNullOrEmpty(checkUrl)) return Redirect(checkUrl);

如果非微信浏览器打开页面链接,重定向的页面效果如下所示。

3、字典数据的绑定

和常规网页功能一样,我们在设计微信页面应用的时候,很多数据也是来源字典数据的,而且需要把它们动态绑定在页面上,微信页面的JQuery WeUI提供了一些列表字典数据的展示效果如下所示。

这个常规的数据绑定如下所示,如下JS代码所示。

$("#job").select({
title:
"选择职业",
items: [
"法官", "医生", "猎人", "学生", "记者", "其他"],
onChange:
function(d) {
console.log(
this, d);
},
onClose:
function() {
console.log(
"close");
},
onOpen:
function() {
console.log(
"open");
},
});

也可以使用集合对象进行赋值处理,如下JS代码所示。

    $("#in").select({
title:
"您的爱好",
multi:
true,
min:
2,
max:
3,
items: [
{
title:
"画画",
value:
1,
description:
"额外的数据1"},
{
title:
"打球",
value:
2,
description:
"额外的数据2"}
],
beforeClose:
function(values, titles) {if(values.indexOf("6") !== -1) {
$.toast(
"不能选打球", "cancel");return false;
}
return true;
},
onChange:
function(d) {
console.log(
this, d);
}
});

基于上面的JS脚本,我们选择后者,使用Ajax技术来填充数据,这样可以动态获取后台的字典数据,并进行页面的绑定操作。

为了方便,我们可以设计一个公共函数,用于数据字典的绑定处理,如下所示。

            //绑定字典内容到指定的控件
            functionBindDictItem(ctrlName, dictTypeName, onChange, onClose, onOpen) {var url = '/h5/GetDictJson?dictTypeName=' +encodeURI(dictTypeName);//获取Ajax的内容,并放到items集合
                var control = $('#' +ctrlName);var items =[];
$.ajax({
type:
'GET',
url: url,
//async: false, //同步 dataType: 'json',
success:
function(data) {
control.empty();
//清空下拉框 //把JSON集合加到数组里面 $.each(data, function(i, item) {
items.push({
title: item.Text, value: item.Value
});
});
//设置显示列表 control.select({
title:
"选择" +dictTypeName,
items: items,
onChange: onChange,
onClose: onClose,
onOpen: onOpen
});
},
error:
function(xhr, status, error) {
$.toast(
"操作失败" + xhr.responseText); //xhr.responseText }
});
}

那么我们绑定字典数据,就只需要调用这个函数就可以很简单实现数据字典的绑定操作了。

            $(function() {
BindDictItem(
"Status", "设备状态");
BindDictItem(
"Dept", "科室");
BindDictItem(
"Building", "建筑物");
BindDictItem(
"Floor", "楼层");
BindDictItem(
"Special", "特殊情况");
});

我们看具体在微信中打开对应连接,字典绑定的效果如下所示。

我们则可以在微信后台对数据字典进行维护即可进行实时的数据更新。


4、微信图片的预览功能

在很多页面里面,我们需要展示丰富的图片,我们需要结合微信的图片预览功能接口,我们才能把图片打开后方便进行缩放处理操作,那么该如何利用微信JSSDK的图片预览接口呢?

首先我们需要引入Jquery WeUI的样式类库,以及JSSDK所需的JS文件,如下所示。

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

然后在页面初始化JSSDK的API脚本,如下代码所示。

        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']
});
//所有准备好后 wx.ready(function() {
});

加入我们页面里面包含有两部分的图片,一个是设备铭牌图片,一个是其他附属图片,我们分别展示,如下HTML代码所示。

        <divclass="weui_cells_title"><h3>铭牌图片</h3></div>
        <divclass="weui_cells">
            <divclass="weui_cell">
                <divid="previewImage">@for (var i = 0; i<ViewBag.mainList.Count; i++)
{
<img id
='mainPic_@i'src='@ViewBag.mainList[i]'alt="铭牌图片"style="height:auto;width:100%" />}</div> </div> </div> <divclass="weui_cells_title"><h3>其他图片</h3></div> <divclass="weui_cells"> <divclass="weui_cell"> <divid="previewImage2"> <divclass="weui-row">@for (var i = 0; i<ViewBag.otherList.Count; i++)
{
<div class
="weui-col-50"> <imgid='otherPic_@i'src='@ViewBag.otherList[i]'alt="其他图片"style="height:auto;width:100%" /> </div>}</div> </div> </div> </div>

这些代码构建了很多个图片控件,也就是原始的HTML图片控件,如果仅仅这样做,那么只能利用网页的效果,而无法利用微信浏览器预览图片,可以放大缩小的丰富功能。

为了实现我们说需要的功能,需要进行一定的处理,简单的方法,可以设计一个JS函数,然后通过JS函数来实现微信预览图片功能,代码如下所示。

        functionBindClick(selector) {
document.querySelector(selector).onclick
= function(event) {var imgArray =[];var curImageSrc = $(this).attr('src');var oParent = $(this).parent();if (curImageSrc && !oParent.attr('href')) {
$(selector).each(
function(index, el) {var itemSrc = $(this).attr('src');
imgArray.push(itemSrc);
});

wx.previewImage({
current: curImageSrc,
urls: imgArray
});
}
}
}


BindClick(
'#previewImage img');
BindClick(
'#previewImage2 img');

这个函数的做法,是参考网上一个大牛的做法,不过这样做存在一个问题,图片如果有多张的话,那么需要点击第一张图片才能开始预览,不能点击其他几张开始。

为了改进这个缺点,我们可以可以利用Razor的模板实现我们需要的代码生成,如下所示集合了Razor模板生成JS代码,实现了我们所需要JS代码的生成。

        var urls =[];
@foreach (
var url inViewBag.mainList)
{
<text>urls.push('@url');</text> }
@
for (var i = 0; i < ViewBag.mainList.Count; i++)
{
<text>document.querySelector('#mainPic_@i').onclick = function() {
wx.previewImage({
current: $(
this).attr("src"),//urls[@i], urls: urls
});
};
</text> }var urlsOther =[];
@foreach (
var url inViewBag.otherList)
{
<text>urlsOther.push('@url');</text> }
@
for (var i = 0; i < ViewBag.otherList.Count; i++)
{
<text>document.querySelector('#otherPic_@i').onclick = function() {
wx.previewImage({
current: $(
this).attr("src"),//urls[@i], urls: urlsOther
});
};
</text> }

JS代码的生成后的代码如下所示.

        var urls =[];
urls.push(
'http://www.iqidi.com/UploadFiles/设备铭牌图片/TIzMZl04X1iqkHMP44hXFHjQ-yyvrxS-tgwgzMGfwe9AUMTxKohcVC6w6O.jpg');

document.querySelector(
'#mainPic_0').onclick = function() {
wx.previewImage({
current: $(
this).attr("src"),//urls[0], urls: urls
});
};
var urlsOther =[];
urlsOther.push(
'http://www.iqidi.com/UploadFiles/设备铭牌图片/lJk_LNwxTGfL5SNpmJwWyfyBONa2JRO7uzu3PJV3aGsrilPPHY2r-ymU00.jpg');

document.querySelector(
'#otherPic_0').onclick = function() {
wx.previewImage({
current: $(
this).attr("src"),//urls[0], urls: urlsOther
});
};

这样最终就可以实现我们所需要的效果了,当然多张图片也不会有任何的问题。