wenmo8 发布的文章

摇一摇周边红包接口是为线下商户提供的发红包功能。用户可以在商家门店等线下场所通过摇一摇周边领取商家发放的红包。我曾经在《
C#开发微信门户及应用(28)--微信“摇一摇·周边”功能的使用和接口的实现
》介绍过微信摇一摇的相关管理,包括页面、设备之间的关系,以及使用等方面内容。本篇继续介绍摇一摇设备的另外一项功能,摇一摇红包功能,介绍如何利用微信摇摇周边的后台配置好页面及地址,然后通过微信JSSDK的方式,摇一摇获取红包的整个流程功能。

1、微信摇一摇红包功能介绍

功能说明

摇一摇周边红包接口是为线下商户提供的发红包功能。用户可以在商家门店等线下场所通过摇一摇周边领取商家发放的红包,在线上转发分享无效。

开发者可通过接口开发摇一摇红包功能,特点包括:

  1. 可选择使用模板加载页或自定义Html5页面调起微信原生红包页面(详见创建红包活动中use_template字段,1为使用模板,2为使用自定义Html5页面)
  2. 原生红包页面拆红包,无需通过公众号消息下发
  3. 提供关注公众号能力,用户可自行选择是否关注(裂变红包分享时无效)
  4. 完成页面可配置跳转链接,可跳转商户的其他自定义Html5页面
  5. 同一个用户在单个红包活动中只能领取1次红包

用户侧交互流程

常规的摇一摇红包的流程如下所示,这里没有使用用户自定义的模板,也就是使用系统内置的(努力加载中。。。)的页面,红包需要自己拆开。

红包组件接口调用流程

  1. 申请红包接口权限:登录摇一摇周边商户后台
    https://zb.weixin.qq.com
    ,进入开发者支持,申请开通摇一摇红包组件接口;
  2. 红包预下单:调用微信支付的api进行红包预下单,告知需要发放的红包金额,人数,生成红包ticket;
  3. 创建活动并录入红包信息:调用摇周边平台的api录入创建红包活动并录入信息,传入预下单时生成的红包ticket;
  4. 调用jsapi抽红包:在摇出的页面中通过调用jsapi抽红包,抽中红包的用户可以拆红包;
  5. 调用以上接口时,红包提供商户和红包发放商户公众号要求一致。

摇一摇红包的处理过程可以先的流程说明,申请权限后,需要在摇一摇后台配置相关的红包处理页面,然后通过红包接口处理提交红包数据,最后通过摇一摇的设备摇出界面,使用JSAPI实现抽取红包的操作,具体过程如下所示。

其中红包接口处理,是调用一系列的红包接口实现的,包括红包预下单、创建红包活动、录入红包信息等操作,如下所示。

摇一摇红包功能开通在后台获取接口即可,如下所示。

假设已经有相关的摇一摇设备(如果没有或者需要加入新的摇一摇设备,请参考上篇随笔《
C#开发微信门户及应用(28)--微信“摇一摇·周边”功能的使用和接口的实现
》的相关介绍,有了设备后在页面管理中添加一个页面,用来处理摇一摇的信息的。

页面的信息,就是添加自定义链接界面,如下所示。

然后配置好URL地址接口,URL地址还需要考虑如何获取用户的openid,因为摇一摇红包接口必须要获得当前用户的openid信息,我们可以通过重定向的方式,使用code获取对应的openid信息,具体后面详细介绍。

2、红包接口处理

前面介绍了,红包接口处理,是调用一系列的红包接口实现的,包括红包预下单、创建红包活动、录入红包信息等操作,如下所示。

那么我们就需要分别对这些操作进行封装,并提交相关的数据了。

根据前面随笔《
C#开发微信门户及应用(33)--微信现金红包的封装及使用
》里面的规则,我们把摇一摇的相关接口也放在这个ILotteryApi接口和LotteryApi实现类里面,如下所示。

下面的摇一摇红包接口全部是基于上面的接口和实现类进行完善处理的。

1)红包预下单

接口说明

设置单个红包的金额,类型等,生成红包信息。预下单完成后,需要在72小时内调用jsapi完成抽红包的操作。(红包过期失效后,资金会退回到商户财付通帐号。)

接口调用说明

服务器端调用
http请求方式: POST
https:
//api.mch.weixin.qq.com/mmpaymkttransfers/hbpreorder POST数据格式:XML
需要商户证书

请求示例

<xml>     
<sign><![CDATA[E1EE61A91C8E90F299DE6AE075D60A2D]]></sign>     
<mch_billno><![CDATA[0010010404201466666670000046545]]></mch_billno>     
<mch_id><![CDATA[10000097]]></mch_id>     
<wxappid><![CDATA[wxcbda96de0b165486]]></wxappid>     
<send_name><![CDATA[send_name]]></send_name>     
<hb_type><![CDATA[NORMAL]]></hb_type>     
<auth_mchid><![CDATA[10000098]]></auth_mchid>     
<auth_appid><![CDATA[wx7777777]]></auth_appid>     
<total_amount><![CDATA[200]]></total_amount>     
<amt_type><![CDATA[ALL_RAND]]></amt_type>     
<total_num><![CDATA[3]]></total_num>     
<wishing><![CDATA[恭喜发财]]></wishing>     
<act_name><![CDATA[新年红包]]></act_name>     
<remark><![CDATA[新年红包]]></remark>     
<risk_cntl><![CDATA[NORMAL]]></risk_cntl>     
<nonce_str><![CDATA[50780e0cca98c8c8e814883e5caa672e]]></nonce_str>
</xml>

返回数据说明

返回格式为xml

成功示例

<xml>     
<return_code><![CDATA[SUCCESS]]></return_code>     
<return_msg><![CDATA[发放成功.]]></return_msg> 
<result_code><![CDATA[SUCCESS]]></result_code> 
<err_code><![CDATA[0]]></err_code>     
<err_code_des><![CDATA[发放成功.]]></err_code_des>     
<mch_billno><![CDATA[0010010404201466666670000046545]]></mch_billno> 
<mch_id>10010404</mch_id>     
<wxappid><![CDATA[wx6fa7e3bab7e15415]]></wxappid>     
<sp_ticket><![CDATA[0cca98c8c8e814883]]></sp_ticket> 
<total_amount>3</total_amount>     
<detail_id><![CDATA[001001040420146666667000004888]]></detail_id> 
<send_time><![CDATA[20150101080000]]></send_time> 
</xml>  

失败示例

<xml>     
<return_code><![CDATA[FAIL]]></return_code>     
<return_msg><![CDATA[系统繁忙,请稍后再试.]]></return_msg>     
<result_code><![CDATA[FAIL]]></result_code>     
<err_code><![CDATA[268458547]]></err_code>     
<err_code_des><![CDATA[系统繁忙,请稍后再试.]]></err_code_des>     
<mch_billno><![CDATA[0010010404201466666670000046542]]></mch_billno>             
<mch_id>10010404</mch_id>     
<wxappid><![CDATA[wx6fa7e3bab7e15415]]></wxappid>      
<total_amount>3</total_amount> 
</xml>

根据请求参数的说明,以及返回的结果,我们可以分别定义它们的传入参数和传出参数,具体的红包预下单的接口定义如下

        /// <summary>
        ///红包预下单接口。需要商户证书///设置单个红包的金额,类型等,生成红包信息。预下单完成后,需要在72小时内调用jsapi完成抽红包的操作。(红包过期失效后,资金会退回到商户财付通帐号。)/// </summary>
        /// <param name="data">传入参数数据</param>
        /// <returns></returns>
        LotteryPreOrderResult LotteryPreOrder(LotteryPreOrderData info);

其中LotteryPreOrderResult返回的对象结果如下所示,主要的信息是要记录sp_ticket,目前没有通过API接口获取已经预下单红包的sp_ticket信息,所以一定要先记录好,后面在录入红包的时候,需要使用到这个票据。

    public classLotteryPreOrderResult : PayResult
{
/// <summary> ///商户订单号/// </summary> public string mch_billno { get; set; }/// <summary> ///公众账号appid/// </summary> public string wxappid { get; set; }/// <summary> ///总付款金额,单位分/// </summary> public int total_amount { get; set; }/// <summary> ///一个普通红包对应一个ticket/// </summary> public string sp_ticket { get; set; }/// <summary> ///红包内部订单号/// </summary> public string detail_id { get; set; }/// <summary> ///红包发放时间/// </summary> public string send_time { get; set; }
}

2)创建红包活动

接口说明

创建红包活动,设置红包活动有效期,红包活动开关等基本信息,返回活动id

接口调用说明

服务器端调用
http请求方式: POST
URL: https:
//api.weixin.qq.com/shakearound/lottery/addlotteryinfo?access_token=ACCESSTOKEN&use_template=1&logo_url=”LOGO_URL”

请求参数说明

参数 类型 说明
access_token string accesstoken,以参数的形式拼装在url后
use_template int 是否使用模板,1:使用,2:不使用,以参数的形式拼装在url后。(模版即交互流程图中的红包加载页,使用模板用户不需要点击可自动打开红包;不使用模版需自行开发HTML5页面,并在页面调用红包jsapi)
logo_url string 使用模板页面的logo_url,不使用模板时可不加。展示在摇一摇界面的消息图标。图片尺寸为120x120。

POST BODY:JSON格式的结构体,具体信息不在赘述,
不过值得说明的是其中的Key是需要注意的,这个值一定需要使用一个确定的值,因为需要和后面的抽取红包的签名处理一致,否则会出错无法抽取红包。

参数 类型 说明
title string 抽奖活动名称(选择使用模板时,也作为摇一摇消息主标题),最长6个汉字,12个英文字母。
desc string 抽奖活动描述(选择使用模板时,也作为摇一摇消息副标题),最长7个汉字,14个英文字母。
onoff int 抽奖开关。0关闭,1开启,默认为1
begin_time long 抽奖活动开始时间,unix时间戳,单位秒
expire_time long 抽奖活动结束时间,unix时间戳,单位秒,红包活动有效期最长为91天
sponsor_appid string 红包提供商户公众号的appid,需与预下单中的公众账号appid(wxappid)一致
total long 红包总数,红包总数是录入红包ticket总数的上限,因此红包总数应该大于等于预下单时红包ticket总数。
jump_url string 红包关注界面后可以跳转到第三方自定义的页面
key string 开发者自定义的key,用来生成活动抽奖接口的签名参数,长度32位。使用方式见sign生成规则

请求示例

Content-Type: application/json Post Body:
{
"title": "title","desc": "desc","onoff": 1,"begin_time": 1428854400,"expire_time": 1428940800,"sponsor_appid": "wxxxxxxxxxxxxxx","total": 10,"jump_url": JUMP_URL,"key": "keyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"}

返回数据说明

示例

{"errcode":0,"errmsg":"","lottery_id":"xxxxxxllllll","page_id":1, 
}

根据上面的接口定义和接口参数说明,我们可以定义创建红包活动的接口定义,如下所示

        /// <summary>
        ///创建红包活动,设置红包活动有效期,红包活动开关等基本信息,返回活动id/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="useTempate">是否使用模板</param>
        /// <param name="login_url">使用模板页面的logo_url,不使用模板时可不加。展示在摇一摇界面的消息图标。图片尺寸为120x120。</param>
        /// <returns></returns>
        AddLotterResult AddLotteryInfo(string accessToken, bool useTempate, string login_url, AddLotteryJson json);

其中AddLotteryResult为我们定义的接口处理结果,主要需要记录其中的LotteryID,这个值需要在后面的录入红包接口使用到。

    /// <summary>
    ///添加红包活动的结果/// </summary>
    public classAddLotterResult : ErrorJsonResult
{
/// <summary> ///生成的红包活动id/// </summary> public string lottery_id { get; set; }/// <summary> ///生成的模板页面ID/// </summary> public int page_id { get; set; }
}

3)录入红包信息

在红包预下单,以及创建红包活动后,就需要把前面两者的信息关联起来,这个操作就是录入红包信息,录入红包信息包括提交红包活动的ID,以及红包预下单的sp_ticket的票据信息,这样就可以让用户抽取具体的红包信息了。

接口说明

在调用"创建红包活动"接口之后,调用此接口录入红包信息。注意,此接口每次调用,都会向某个活动新增一批红包信息,如果红包数少于100个,请通过一次调用添加所有红包信息。如果红包数大于100,可以多次调用接口添加。请注意确保多次录入的红包ticket总的数目不大于创建该红包活动时设置的total值。

接口调用说明

服务器端调用
http请求方式: POST
URL:https:
//api.weixin.qq.com/shakearound/lottery/setprizebucket?access_token=ACCESSTOKEN

POST BODY:JSON格式的结构体

参数 类型 说明
lottery_id string 红包抽奖id,来自addlotteryinfo返回的lottery_id
mchid string 红包提供者的商户号,,需与预下单中的商户号mch_id一致
sponsor_appid string 红包提供商户公众号的appid,需与预下单中的公众账号appid(wxappid)一致
prize_info_list json数组 红包ticket列表,如果红包数较多,可以一次传入多个红包,批量调用该接口设置红包信息。每次请求传入的红包个数上限为100
ticket string 预下单时返回的红包ticket,单个活动红包ticket数量上限为100000个,可添加多次。

请求示例

Content-Type: application/json Post Body:
{
"lottery_id": "xxxxxxllllll","mchid": "10000098","sponsor_appid": "wx8888888888888888" "prize_info_list": [
{
"ticket": "v1|ZiPs2l0hpMBp3uwGI1rwp45vOdz/V/zQ/00jP9MeWT+e47/q1FJjwCIP34frSjzOxAEzJ7k2CtAg1pmcShvkChBWqbThxPm6MBuzceoHtj79iHuHaEn0WAO+j4sXnXnbGswFOlDYWg1ngvrRYnCY3g==",
}
]
}

返回数据说明

示例

{"errcode":0,"errmsg":"","repeat_ticket_list":[         
{
"ticket": "v1|ZiPs2l0hpMBp3uwGI1rwp45vOdz/V/zQ/00jP9MeWT+e47/q1FJjwCIP34frSjzOxAEzJ7k2CtAg1pmcShvkChBWqbThxPm6MBuzceoHtj79iHuHaEn0WAO+j4sXnXnbGswFOlDYWg1ngvrRYnCY3g==",
}
]
"success_num":100}

根据这些接口定义和传递参数信息,我们可以定义录入红包的接口。

        /// <summary>
        ///录入红包信息///在调用"创建红包活动"接口之后,调用此接口录入红包信息。///注意,此接口每次调用,都会向某个活动新增一批红包信息,如果红包数少于100个,请通过一次调用添加所有红包信息。///如果红包数大于100,可以多次调用接口添加。请注意确保多次录入的红包ticket总的数目不大于创建该红包活动时设置的total值。/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="json">录入红包信息</param>
        /// <returns></returns>
        SetPrizeBucketResult SetPrizeBucket(string accessToken, SetPrizeBucketJson json);

其中录入红包返回的结果类SetPrizeBucketResult 定义如下所示。

    /// <summary>
    ///录入红包返回的结果/// </summary>
    public classSetPrizeBucketResult : ErrorJsonResult
{
/// <summary> ///重复使用的ticket列表,如为空,将不返回/// </summary> public List<PrizeTicket> repeat_ticket_list { get; set; }/// <summary> ///过期的ticket列表,如为空,将不返回/// </summary> public List<PrizeTicket> expire_ticket_list { get; set; }/// <summary> ///金额不在大于1元,小于1000元的ticket列表,如为空,将不返回/// </summary> public List<PrizeTicket> invalid_amount_ticket_list { get; set; }/// <summary> ///原因:生成红包的时候,授权商户号auth_mchid和auth_appid没有写摇周边的商户号/// </summary> public List<PrizeTicket> wrong_authmchid_ticket_list { get; set; }/// <summary> ///ticket解析失败,可能有错别字符或不完整/// </summary> public List<PrizeTicket> invalid_ticket_list { get; set; }/// <summary> ///成功录入的红包数量/// </summary> public int success_num { get; set; }
}

3、红包接口的使用及JSAPI的摇一摇页面处理

前面说明了相关的接口信息和对应的结果类的定义,我们具体在根据相关的说明,实现接口的处理就可以了,这个小节介绍如何使用这些接口,实现我们对摇一摇红包的整个流程的处理。

如第一步,调用红包预下单处理。

        string sp_ticket = "";/// <summary>
        ///红包预下单,并记录红包的ticket信息/// </summary>
        private void btnHbPreOrder_Click(objectsender, EventArgs e)
{
LotteryPreOrderData info
= newLotteryPreOrderData()
{
total_amount
= 100,
total_num
= 1,
act_name
= "恭喜发财",
remark
= "恭喜发财",
wishing
= "恭喜发财",
};
var result =hbApi.LotteryPreOrder(info);
sp_ticket
= result.sp_ticket;//赋值,方便下面的录入红包操作 Console.WriteLine(XmlConvertor.FormatXml(result.ToXml()));
}

第二部创建红包活动,如下所示。

        string lotteryId = "";/// <summary>
        ///创建红包活动,并记录活动的ID/// </summary>
        private void btnCreateLottery_Click(objectsender, EventArgs e)
{
AddLotteryJson json
= newAddLotteryJson()
{
begin_time
=DateTime.Now.DateTimeToInt(),
expire_time
= DateTime.Now.AddDays(90).DateTimeToInt(),
title
= "测试红包",
desc
= "测试红包",
sponsor_appid
= this.AppId,
onoff
= 1,
total
= 1,
jump_url
= "http://www.iqidi.com",
key
=accountInfo.PayAPIKey
};
var result = hbApi.AddLotteryInfo(token, false, null, json);
lotteryId
= result.lottery_id;//赋值方便查询红包 Console.WriteLine(result.ToJson());
}

第三步,根据红包预下单结果和红包活动创建结果,录入红包信息

        /// <summary>
        ///录入红包信息,供使用摇一摇页面处理/// </summary>
        private void btnSetHB_Click(objectsender, EventArgs e)
{
if (string.IsNullOrEmpty(sp_ticket))
{
MessageUtil.ShowError(
"红包票据ticket为空");return;
}

SetPrizeBucketJson json
= newSetPrizeBucketJson()
{
lottery_id
= lotteryId, //使用前面的红包活动 mchid =accountInfo.MchID,
sponsor_appid
=accountInfo.AppID,
prize_info_list
= new List<PrizeTicket>()
{
new PrizeTicket() { ticket = sp_ticket},//使用前面的红包预下单ticket }
};
var result = hbApi.SetPrizeBucket(this.token, json);
Console.WriteLine(
"录入红包的结果:");
Console.WriteLine(result.ToJson());
}

第四步,配置好重定向的页面,方便获取用户的openid

1)用户同意授权,获取code

这个步骤,我们利用的是“网页授权获取用户基本信息”操作,其中相关的信息说明如下所示。

在确保微信公众账号拥有授权作用域(scope参数)的权限的前提下(服务号获得高级接口后,默认拥有scope参数中的snsapi_base和snsapi_userinfo),引导关注者打开如下页面:

https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
若提示“该链接无法访问”,请检查参数是否填写错误,是否拥有scope参数对应的授权作用域权限。

尤其注意:由于授权操作安全等级较高,所以在发起授权请求时,微信会对授权链接做正则强匹配校验,如果链接的参数顺序不对,授权页面将无法正常访问

参考链接(请在微信客户端中打开此链接体验)
Scope为snsapi_base
https:
//open.weixin.qq.com/connect/oauth2/authorize?appid=wx520c15f417810387&redirect_uri=https%3A%2F%2Fchong.qq.com%2Fphp%2Findex.php%3Fd%3D%26c%3DwxAdapter%26m%3DmobileDeal%26showwxpaytitle%3D1%26vb2ctag%3D4_2030_5_1194_60&response_type=code&scope=snsapi_base&state=123#wechat_redirect Scope为snsapi_userinfo
https:
//open.weixin.qq.com/connect/oauth2/authorize?appid=wxf0e81c3bee622d60&redirect_uri=http%3A%2F%2Fnba.bluewebgame.com%2Foauth_response.php&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect

尤其注意:跳转回调redirect_uri,应当使用https链接来确保授权code的安全性。

这样,如果我们配置的连接为http://www.iqidi.com/JSSDKTest/RedPack?uid=iqidiSoftware ,其中uid为我们对应的账号名称。

那么我们根据上面规则,得到重定向的连接地址就是如下所示。

https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx3d81fc2886d86526&redirect_uri=http%3a%2f%2fwww.iqidi.com%2fJSSDKTest%2fRedPack%3fuid%3diqidiSoftware&response_type=code&scope=snsapi_base&state=state#wechat_redirect

2)通过code换取网页授权access_token

首先请注意,这里通过code换取的是一个特殊的网页授权access_token,与基础支持中的access_token(该access_token用于调用其他接口)不同。公众号可通过下述接口来获取网页授权access_token。如果网页授权的作用域为snsapi_base,则本步骤中获取到网页授权access_token的同时,也获取到了openid,snsapi_base式的网页授权流程即到此为止。

尤其注意:由于公众号的secret和获取到的access_token安全级别都非常高,必须只保存在服务器,不允许传给客户端。后续刷新access_token、通过access_token获取用户信息等步骤,也必须从服务器发起。

请求方法

获取code后,请求以下链接获取access_token: 
https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

参数说明

在具体的页面里面,我们可以获得相关的参数,如uid我们可以把它转换为我们对应的账号信息,如下所示。

        /// <summary>
        ///根据账号名获取对应的对象/// </summary>
        /// <returns></returns>
        privateAccountInfo GetAccount()
{
AccountInfo accountInfo
= null;string accountNo = Request.QueryString["uid"];if (!string.IsNullOrEmpty(accountNo))
{
accountInfo
= BLLFactory<Account>.Instance.FindByAccountNo(accountNo);}returnaccountInfo;
}

而其中的code,我们根据这个信息,也可以获得用户的相关信息,我们需要获得访问用户的openid,如下所示。

                string code = Request.QueryString["code"];
IBasicApi baseApi
= newBasicApi();var result =baseApi.GetAuthToken(accountInfo.UniteAppId, accountInfo.UniteAppSecret, code);if (result != null && !string.IsNullOrEmpty(result.openid))
{
var openid = result.openid;

有了这些信息,我们就可以构建我们的红包参数,并封装好签名了。

                    WxPayData data = newWxPayData();
data.SetValue(
"openid", openid);
data.SetValue(
"lottery_id", lottery_id);
data.SetValue(
"noncestr", data.GenerateNonceStr());
data.SetValue(
"sign", data.MakeSign(accountInfo.PayAPIKey));

然后,我们在后台,把这些信息赋值给ViewBag,就可以在页面上顺利使用了。

                    var sign = data.GetString("sign");//获取生成的sign
                    var noncestr = data.GetString("noncestr");//获取生成的sign
ViewBag.sign=sign;
ViewBag.lottery_id
=lottery_id;
ViewBag.openid
=openid;
ViewBag.noncestr2
= noncestr;

在摇一摇
红包的处理视图页面里面,我们先引入对应的js文件,如下所示

    <title>摇一摇红包页面</title>
    <linkrel="stylesheet"href="http://demo.open.weixin.qq.com/jssdk/css/style.css?ts=1420774989">
    <scripttype="text/javascript"src="http://res.wx.qq.com/open/js/jweixin-1.1.0.js"></script>
    <scripttype="text/javascript"src="http://zb.weixin.qq.com/app/shakehb/BeaconShakehbJsBridge.js"></script>

然后在JS里面添加相关的处理函数,并赋值给对应的接口参数。

    <scripttype="text/javascript">BeaconShakehbJsBridge.ready(function() {//跳转到抽红包页面
BeaconShakehbJsBridge.invoke('jumpHongbao', {
lottery_id:
'@ViewBag.lottery_id',
noncestr:
'@ViewBag.noncestr2',
openid:
'@ViewBag.openid',
sign:
'@ViewBag.sign'});
});
</script>

最后,我们测试相关的结果,使用手机摇一摇获得红包的过程界面效果如下所示。



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

C#开发微信门户及应用(38)--微信摇一摇红包功能

C#开发微信门户及应用(37)--微信公众号标签管理功能

C#开发微信门户及应用(36)--微信卡劵管理的封装操作

C#开发微信门户及应用(35)--微信支付之企业付款封装操作

C#开发微信门户及应用(34)--微信裂变红包

C#开发微信门户及应用(33)--微信现金红包的封装及使用

C#开发微信门户及应用(32)--微信支付接入和API封装使用

C#开发微信门户及应用(31)--微信语义理解接口的实现和处理

C#开发微信门户及应用(30)--消息的群发处理和预览功能

C#开发微信门户及应用(28)--微信“摇一摇·周边”功能的使用和接口的实现

C#开发微信门户及应用(27)-公众号模板消息管理

C#开发微信门户及应用(26)-公众号微信素材管理

C#开发微信门户及应用(25)-微信企业号的客户端管理功能

C#开发微信门户及应用(24)-微信小店货架信息管理

C#开发微信门户及应用(23)-微信小店商品管理接口的封装和测试

C#开发微信门户及应用(22)-微信小店的开发和使用

C#开发微信门户及应用(21)-微信企业号的消息和事件的接收处理及解密

C#开发微信门户及应用(20)-微信企业号的菜单管理

C#开发微信门户及应用(19)-微信企业号的消息发送(文本、图片、文件、语音、视频、图文消息等)

C#开发微信门户及应用(18)-微信企业号的通讯录管理开发之成员管理

C#开发微信门户及应用(17)-微信企业号的通讯录管理开发之部门管理

C#开发微信门户及应用(16)-微信企业号的配置和使用

C#开发微信门户及应用(15)-微信菜单增加扫一扫、发图片、发地理位置功能

C#开发微信门户及应用(14)-在微信菜单中采用重定向获取用户数据

C#开发微信门户及应用(13)-使用地理位置扩展相关应用

C#开发微信门户及应用(12)-使用语音处理

C#开发微信门户及应用(11)--微信菜单的多种表现方式介绍

C#开发微信门户及应用(10)--在管理系统中同步微信用户分组信息

C#开发微信门户及应用(9)-微信门户菜单管理及提交到微信服务器

C#开发微信门户及应用(8)-微信门户应用管理系统功能介绍

C#开发微信门户及应用(7)-微信多客服功能及开发集成

C#开发微信门户及应用(6)--微信门户菜单的管理操作

C#开发微信门户及应用(5)--用户分组信息管理

C#开发微信门户及应用(4)--关注用户列表及详细信息管理

C#开发微信门户及应用(3)--文本消息和图文消息的应答


C#开发微信门户及应用(2)--微信消息的处理和应答


C#开发微信门户及应用(1)--开始使用微信接口

随着微信开逐步开放更多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引用。

    <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.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:'',//必填,签名,见附录1jsApiList: []//必填,需要使用的JS接口列表,所有JS接口列表见附录2 });

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

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

    <scriptlanguage="javascript">
    varappid= '@ViewBag.appid';varnoncestr= '@ViewBag.noncestr';varsignature= '@ViewBag.signature';vartimestamp= '@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
=1414587457url=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 stringGetTimeStamp()
{
TimeSpan ts
= DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);returnConvert.ToInt64(ts.TotalSeconds).ToString();
}
/// <summary> ///生成随机串,随机串包含字母或数字/// </summary> /// <returns>随机串</returns> private static stringGetNonceStr()
{
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, stringurl)
{
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
= newHashtable();
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);returnsignPackage;
}

我们注意到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(stringrawstring)
{
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, stringurl)
{
string accessToken =GetAccessToken(appid, appSecret);string jsTicket =GetJSAPI_Ticket(accessToken);returnJSSDKHelper.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接口进行了调用,如下所示。

    <scriptlanguage="javascript">
    varappid= '@ViewBag.appid';varnoncestr= '@ViewBag.noncestr';varsignature= '@ViewBag.signature';vartimestamp= '@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) {varlatitude=res.latitude;//纬度,浮点数,范围为90 ~ -90 varlongitude=res.longitude;//经度,浮点数,范围为180 ~ -180。 varspeed=res.speed;//速度,以米/每秒计 varaccuracy=res.accuracy;//位置精度 $("#lblLoacation").text(latitude+ "," +longitude);//解析坐标地址 varlocation=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) {varnetworkType=res.networkType;//返回网络类型2g,3g,4g,wifi $("#lblNetwork").text(networkType);
}
});

chooseImage();
});
</script>

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

        //拍照显示
        varlocalIds;functionchooseImage() {
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。

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

        //上传图片
        varserverId;functionupload() {
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#开发微信门户及应用》系列感兴趣,可以关注我的其他文章,系列随笔如下所示:

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

C#开发微信门户及应用(38)--微信摇一摇红包功能

C#开发微信门户及应用(37)--微信公众号标签管理功能

C#开发微信门户及应用(36)--微信卡劵管理的封装操作

C#开发微信门户及应用(35)--微信支付之企业付款封装操作

C#开发微信门户及应用(34)--微信裂变红包

C#开发微信门户及应用(33)--微信现金红包的封装及使用

C#开发微信门户及应用(32)--微信支付接入和API封装使用

C#开发微信门户及应用(31)--微信语义理解接口的实现和处理

C#开发微信门户及应用(30)--消息的群发处理和预览功能

C#开发微信门户及应用(28)--微信“摇一摇·周边”功能的使用和接口的实现

C#开发微信门户及应用(27)-公众号模板消息管理

C#开发微信门户及应用(26)-公众号微信素材管理

C#开发微信门户及应用(25)-微信企业号的客户端管理功能

C#开发微信门户及应用(24)-微信小店货架信息管理

C#开发微信门户及应用(23)-微信小店商品管理接口的封装和测试

C#开发微信门户及应用(22)-微信小店的开发和使用

C#开发微信门户及应用(21)-微信企业号的消息和事件的接收处理及解密

C#开发微信门户及应用(20)-微信企业号的菜单管理

C#开发微信门户及应用(19)-微信企业号的消息发送(文本、图片、文件、语音、视频、图文消息等)

C#开发微信门户及应用(18)-微信企业号的通讯录管理开发之成员管理

C#开发微信门户及应用(17)-微信企业号的通讯录管理开发之部门管理

C#开发微信门户及应用(16)-微信企业号的配置和使用

C#开发微信门户及应用(15)-微信菜单增加扫一扫、发图片、发地理位置功能

C#开发微信门户及应用(14)-在微信菜单中采用重定向获取用户数据

C#开发微信门户及应用(13)-使用地理位置扩展相关应用

C#开发微信门户及应用(12)-使用语音处理

C#开发微信门户及应用(11)--微信菜单的多种表现方式介绍

C#开发微信门户及应用(10)--在管理系统中同步微信用户分组信息

C#开发微信门户及应用(9)-微信门户菜单管理及提交到微信服务器

C#开发微信门户及应用(8)-微信门户应用管理系统功能介绍

C#开发微信门户及应用(7)-微信多客服功能及开发集成

C#开发微信门户及应用(6)--微信门户菜单的管理操作

C#开发微信门户及应用(5)--用户分组信息管理

C#开发微信门户及应用(4)--关注用户列表及详细信息管理

C#开发微信门户及应用(3)--文本消息和图文消息的应答


C#开发微信门户及应用(2)--微信消息的处理和应答


C#开发微信门户及应用(1)--开始使用微信接口

在我前面的几篇博客,有介绍了微信支付、微信红包、企业付款等各种和支付相关的操作,不过上面都是基于微信普通API的封装,本篇随笔继续微信支付这一主题,继续介绍基于微信网页JSAPI的方式发起的微信支付功能实现,微信的JSAPI相对于普通的API操作,调试没有那么方便,而且有时候有些错误需要反复核实。本篇随笔基于实际的微信网页支付案例,对微信JSAPI的支付实现进行介绍。

1、微信JS-SDK的知识

在我前面《
C#开发微信门户及应用(39)--使用微信JSSDK实现签到的功能
》介绍的内容里面,有介绍了很多JS-SDK的基础知识,我们基于网页发起的微信支付,我们也是基于JS-SDK的基础上进行发起的,因此需要了解这些JS-SDK的使用步骤。

一般来说,我们网页JSAPI发起的微信支付,需要使用wx.chooseWXPay的操作方法,而这个方法也是需要在完成wx.config初始化的时候,由界面元素进行触发处理的。

例如我们可以这样实现整个微信支付的处理过程:

1)先使用JS对API进行初始化配置

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

2)使用wx.chooseWXPay发起微信支付

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

备注:prepay_id 通过微信支付统一下单接口拿到,paySign 采用统一的微信支付 Sign 签名生成方法,注意这里 appId 也要参与签名,appId 与 config 中传入的 appId 一致,即最后参与签名的参数有appId, timeStamp, nonceStr, package, signType。

3)获取用户的openid

在一些JSAPI操作里面,有时候需要传入当前用户的openid,由于这个值,一般是不能直接获得的,但可以通过用户授权代码获取,因此我们可以在菜单中配置好重定向的URL,根据URL获取对应的code,然后解析code为对应的openid即可。

通过code换取的是一个特殊的网页授权access_token,与基础支持中的access_token(该access_token用于调用其他接口)不同。公众号可通过下述接口来获取网页授权access_token。如果网页授权的作用域为snsapi_base,则本步骤中获取到网页授权access_token的同时,也获取到了openid,snsapi_base式的网页授权流程即到此为止。

获取code后,请求以下链接获取access_token: 
https:
//api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

2、微信支付JSAPI初始化的参数处理

要获取相关的JS-SDK的相关接口参数,我们需要先生成JSAPI-Ticket凭证,生成这个凭证代码接口实现如下所示。一般来说,这个接口的数据需要缓存起来的,具体可以自己实现处理。

        /// <summary>
        ///获取JSAPI_TICKET接口/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <returns></returns>
        public string GetJSAPI_Ticket(stringaccessToken)
{
var url = string.Format("https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={0}&type=jsapi", accessToken);var result = JsonHelper<GetTicketResult>.ConvertJson(url);return result != null ? result.ticket : null;
}

我们要实现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, stringurl)
{
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
= newHashtable();
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);returnsignPackage;
}

这样把它们放在哈希表里面,方便我们提取出来使用。

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

为了在MVC视图页面里面,设置我们计算出来的值,一般我们需要在后台进行计算好,并把它们放在ViewBag变量中就可以在页面前端使用了,如下所示是MVC视图页面的后台代码。

        /// <summary>
        ///刷新JS-SDK的票据/// </summary>
        protected virtual voidRefreshTicket(AccountInfo accountInfo)
{
Hashtable ht
=baseApi.GetJSAPI_Parameters(accountInfo.AppID, accountInfo.AppSecret, Request.Url.AbsoluteUri);
ViewBag.appid
= ht["appid"].ToString();
ViewBag.nonceStr
= ht["noncestr"].ToString();
ViewBag.timestamp
= ht["timestamp"].ToString();
ViewBag.signature
= ht["signature"].ToString();
}

这样,在MVC的视图页面里面,我们的代码可以这样实现JSAPI变量的初始化。

    <scriptlanguage="javascript">
    varopenid= '@ViewBag.openid';varappid= '@ViewBag.appid';varnoncestr= '@ViewBag.noncestr';varsignature= '@ViewBag.signature';vartimestamp= '@ViewBag.timestamp';

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

3、微信支付JSAPI发起微信支付的参数处理

在第一小节里面,我提到了,初始化JS-API后,还需要使用wx.chooseWXPay发起微信支付,这个接口也有几个相关的参数。

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

其中这里的timestamp和nonceStr的规则和前面初始化操作的参数规则一样,但是注意不能和初始化接口的timestamp和nonceStr保持一样,否则发起支付会出现【
支付验证签名失败
】的错误。

package的变量就是我们调用统一下单接口的获得的预下单id,格式如下所示:

prepay_id=wx2016051517463160322779de0375788970

而为了获得这个预下单的ID,我们先需要根据统一下单接口的需要,构建一个数据对象,如下所示。

                PayOrderData data = newPayOrderData()
{
product_id
=id,
body
= "测试支付" +id,
attach
= "爱奇迪技术支持",
detail
= "测试JSAPI支付" +id,
total_fee
= 1,
goods_tag
= "test" +id,
trade_type
= "JSAPI",
openid
=openid
};

然后调用前面封装过的统一下单接口API获取对应的统一下单ID

TenPayApi api = new TenPayApi(accountInfo); var orderResult =api.UnifiedOrder(data);
LogHelper.Debug(
string.Format("统一下单结果:{0}", (orderResult != null) ? orderResult.ToJson() : "为空值"));if (string.IsNullOrEmpty(orderResult.prepay_id) || string.IsNullOrEmpty(orderResult.appid))
{
throw new WeixinException("统一下单结果返回失败!");
}

signType固定为MD5,

最后剩下paySign这个比较复杂的参数了,这个参数就是需要根据前面这些参数进行签名的值。微信支付的签名还是和普通API的做法(在前面介绍微信支付的时候,有介绍过相关的规则,具体可以看看《
C#开发微信门户及应用(32)--微信支付接入和API封装使用
》),引入实体类
WxPayData
来存储一些业务参数,以及实现参数的签名处理。

值得注意的是,使用普通API的签名为Sign,而使用JSAPI的签名变量名称为paySign,两者处理逻辑一样,只是名称不同。

这样我们在后台处理相关的变量的代码如下所示。

        /// <summary>
        ///获取JSAPI方式的微信字符串参数对象/// </summary>
        /// <param name="accountInfo">当前账号</param>
        /// <param name="prepay_id">统一下单ID</param>
        /// <returns></returns>
        private WxPayData GetJSPayParam(AccountInfo accountInfo, stringprepay_id)
{
WxPayData data
= newWxPayData();
data.SetValue(
"appId", ViewBag.appId);
data.SetValue(
"timeStamp", data.GenerateTimeStamp());
data.SetValue(
"nonceStr", data.GenerateNonceStr());
data.SetValue(
"signType", "MD5");
data.SetValue(
"package", string.Format("prepay_id={0}", prepay_id));

data.SetValue(
"paySign", data.MakeSign(accountInfo.PayAPIKey));//签名 returndata;
}

然后,再定义一个控制器接口,返回相关的参数数据,部分逻辑代码如下所示。这样方便前端通过JSON的格式获取对应的变量值。

                //支付需要的参数
                WxPayData param =GetJSPayParam(accountInfo, orderResult.prepay_id);
LogHelper.Debug(
"GetJSPayParam:" +param.ToJson());var obj = new{
timeStamp
= param.GetString("timeStamp"),
nonceStr
= param.GetString("nonceStr"),
signType
= param.GetString("signType"),
package
= param.GetString("package"),
paySign
= param.GetString("paySign")
};
return Content(obj.ToJson());

在前面页面,通过ajax方式获得发起微信支付的相关参数,代码如下所示。

    //发起一个微信支付
    functionchooseWXPay(id) {//alert(window.location.href);
        var uid = getUrlVars()["uid"];
$.ajax({
type:
'POST',
url:
'/JSSDKTest/GetWXPayData',//async: false, //同步 dataType: 'json',
data : {
id: id,
uid: uid,
openid : openid
},
success:
function(json) {
wx.chooseWXPay({
appId: appid,
timestamp: json.timeStamp,
//支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符 nonceStr: json.nonceStr, //支付签名随机串,不长于 32 位 package: json.package, //统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=***) signType: json.signType, //签名方式,默认为'SHA1',使用新版支付需传入'MD5' paySign: json.paySign, //支付签名 success: function (res) { //支付成功后的回调函数 if (res.errMsg == 'chooseWXPay:ok') {
$.toast(
'支付成功');//setTimeout(function () { //window.location.href = "/";//这里默认跳转到主页 //}, 2000); //window.location.href = "/Pay/order_details?orderId=" + $("#orderId").val(); } else if (res.errMsg == 'chooseWXPay:cancel' || res.errMsg == 'chooseWXPay:fail') {
$.toast(
"支付失败");//window.location.href = "/Pay/order_details?orderId=" + $("#orderId").val(); }
},
cancel:
function() {
$.toast(
"用户取消了支付");//window.location.href = "/Pay/order_details?orderId=" + $("#orderId").val(); }
});
wx.error(
function(res) {
$.toast(
"调用支付出现异常");//window.location.href = "/Pay/order_details?orderId=" + $("#orderId").val(); })
},
error:
function(xhr, status, error) {
$.toast(
"操作失败" + xhr.responseText); //xhr.responseText }
});
};

4、微信支付JSAPI发起微信支付的界面效果

通过上面的代码,我们可以顺利发起微信支付,并可以看到具体的测试结果了,读者可以关注我们的公众号【广州爱奇迪】对其中微信支付进行测试了解。





微信支付成功后,我们同样可以在微信支付的对话里面看到响应的结果了。

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

C#开发微信门户及应用(40)--使用微信JSAPI实现微信支付功能

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

C#开发微信门户及应用(38)--微信摇一摇红包功能

C#开发微信门户及应用(37)--微信公众号标签管理功能

C#开发微信门户及应用(36)--微信卡劵管理的封装操作

C#开发微信门户及应用(35)--微信支付之企业付款封装操作

C#开发微信门户及应用(34)--微信裂变红包

C#开发微信门户及应用(33)--微信现金红包的封装及使用

C#开发微信门户及应用(32)--微信支付接入和API封装使用

C#开发微信门户及应用(31)--微信语义理解接口的实现和处理

C#开发微信门户及应用(30)--消息的群发处理和预览功能

C#开发微信门户及应用(28)--微信“摇一摇·周边”功能的使用和接口的实现

C#开发微信门户及应用(27)-公众号模板消息管理

C#开发微信门户及应用(26)-公众号微信素材管理

C#开发微信门户及应用(25)-微信企业号的客户端管理功能

C#开发微信门户及应用(24)-微信小店货架信息管理

C#开发微信门户及应用(23)-微信小店商品管理接口的封装和测试

C#开发微信门户及应用(22)-微信小店的开发和使用

C#开发微信门户及应用(21)-微信企业号的消息和事件的接收处理及解密

C#开发微信门户及应用(20)-微信企业号的菜单管理

C#开发微信门户及应用(19)-微信企业号的消息发送(文本、图片、文件、语音、视频、图文消息等)

C#开发微信门户及应用(18)-微信企业号的通讯录管理开发之成员管理

C#开发微信门户及应用(17)-微信企业号的通讯录管理开发之部门管理

C#开发微信门户及应用(16)-微信企业号的配置和使用

C#开发微信门户及应用(15)-微信菜单增加扫一扫、发图片、发地理位置功能

C#开发微信门户及应用(14)-在微信菜单中采用重定向获取用户数据

C#开发微信门户及应用(13)-使用地理位置扩展相关应用

C#开发微信门户及应用(12)-使用语音处理

C#开发微信门户及应用(11)--微信菜单的多种表现方式介绍

C#开发微信门户及应用(10)--在管理系统中同步微信用户分组信息

C#开发微信门户及应用(9)-微信门户菜单管理及提交到微信服务器

C#开发微信门户及应用(8)-微信门户应用管理系统功能介绍

C#开发微信门户及应用(7)-微信多客服功能及开发集成

C#开发微信门户及应用(6)--微信门户菜单的管理操作

C#开发微信门户及应用(5)--用户分组信息管理

C#开发微信门户及应用(4)--关注用户列表及详细信息管理

C#开发微信门户及应用(3)--文本消息和图文消息的应答


C#开发微信门户及应用(2)--微信消息的处理和应答


C#开发微信门户及应用(1)--开始使用微信接口

在我的混合式开发框架里面,集成了WebAPI的访问,这种访问方式不仅可以实现简便的数据交换,而且可以在多种平台上进行接入,如Winform程序、Web网站、移动端APP等多种接入方式,Web API的处理方式和微信提供的接口处理规则类似,也是通过向服务器获得访问令牌(AccessToken),然后传递给每个Web API接口,实现数据的交换处理。本篇随笔主要介绍混合框架中Winform对Web API访问的处理。

1、Web API接入方式介绍

《混合式开发框架》混合了Web API接口访问、WCF接口访问,以及直接访问数据库三种方式的接入,以适应多种场景的应用,是基于门面层的一种接口实现处理和封装。是一种弹性化非常好的框架应用,既可用于单机版软件或者基于局域网内的应用软件,也可以用于分布式技术的互联网环境应用,是一种成熟稳定、安全高效的技术框架。

关于这个框架的详细介绍,可以查看我的随笔《
Winform混合式开发框架的特点总结
》进行详细了解。

这里主要关注Web API的接入方式,我们知道,如果是一般的接口,如果公布在互联网上面,就会有很多接入的风险,因此需要对接口的调用进行检查校验,确保访问令牌有效,而且对数据发生修改的,还需要对数据的加密签名进行检查,才能保证我们的接口运行在较为安全的环境中。

混合框架调用Web API接口的详细过程,可以通过《
Web API应用架构在Winform混合框架中的应用(3)--Winfrom界面调用WebAPI的过程分解
》、《
Web API应用架构在Winform混合框架中的应用(1)
》、《
Web API接口设计经验总结
》进行了解。

2、Web API的接口访问令牌的处理

由于我们需要对接口访问的身份进行核实,因此一般要求我们的接口都带有一个token参数,用来对用户身份进行识别,如下所示是Web API层的MVC控制器的接口定义。

[HttpGet]public UserInfo GetUserByName(string userName, stringtoken)
{
//令牌检查,不通过则抛出异常 CheckResult checkResult =CheckToken(token);return BLLFactory<User>.Instance.GetUserByName(userName);
}

如果
我们在客户端需要调用这个接口,那么就需要传入这个token参数,也就是说这个token令牌需要在调用任何接口前获得,这样才能为我们后面的接口调用做好准备。

而这个token的产生是非常重要的,需要严格颁发,因此需要对获取这个token的方法的参数进行签名校验,如下面代码是WebAPI接口对产生token的处理。

        /// <summary>
        ///注册用户获取访问令牌接口/// </summary>
        /// <param name="username">用户登录名称</param>
        /// <param name="password">用户密码</param>
        /// <param name="signature">加密签名字符串</param>
        /// <param name="timestamp">时间戳</param>
        /// <param name="nonce">随机数</param>
        /// <param name="appid">应用接入ID</param>
[HttpGet]public TokenResult GetAccessToken(string username, string password, string signature, string timestamp, string nonce, string appid)

也就是需要传入用户名、密码、加密签名、时间戳、随机数、应用接入ID等信息,从而构建出来一个访问令牌,通过用户名、密码、加密签名校验等方式,可以实现对访问令牌(token)的严格颁发处理。

在客户端调用所有Web API接口前,我们需要先通过上面的Web API接口,获取到该用户的访问令牌,为了方便,我们可以在客户端封装一个函数,通过这个函数获取到对应的访问令牌,然后把它存储在缓存里面,方便各个模块的接口访问处理。

    /// <summary>
    ///用户获取令牌的辅助类/// </summary>
    public classAccessTokenHelper
{
private const string APPID = "APPID";//应用ID,由系统管理员分配 private const string APPSECRET = "APPSECRET";//应用秘钥,,由系统管理员分配 private const string DEFAULT_API_URL = "http://localhost:9001/api/Auth/GetAccessToken";//默认调试的Web API获取授权地址 /// <summary> ///设置签名参数。///由于Web API大多数的接口,都需要验证用户身份的访问令牌(accesstoken),因此用户在登陆的时候,需要使用这个步骤去获取令牌信息,然后在继续后续的接口操作。///该接口用到的应用ID、应用秘钥等参数,由系统管理员统一分配。/// </summary> public static bool GetAccessToken(string username, stringpassword)
{
bool result = false;//配置使用Web API模式,需要构建登陆token才能访问 AppConfig config = newAppConfig();string callerType = config.AppConfigGet("CallerType");string apiUrl = config.AppConfigGet("AuthApiUrl");
apiUrl
= string.IsNullOrEmpty(apiUrl) ?DEFAULT_API_URL : apiUrl;if (callerType.Equals("api", StringComparison.OrdinalIgnoreCase))
{
//使用API方式,需要在缓存里面设置特殊的信息 var url = apiUrl +SignatureHelper.GetSignatureUrl(APPID, APPSECRET);
url
+= string.Format("&username={0}&password={1}", username, password);

TokenResult tokenResult
= JsonHelper<TokenResult>.ConvertJson(url);
result
= !string.IsNullOrEmpty(tokenResult.access_token);if (tokenResult == null)
{
var message = "获取授权信息出错,请检查地址是否正确!";
MessageDxUtil.ShowError(message);
}
var SignatureInfo = newSignatureInfo()
{
appid
=APPID,
appsecret
=APPSECRET,
token
= (tokenResult != null) ? tokenResult.access_token : null};
Cache.Instance.Add(
"SignatureInfo", SignatureInfo);
}
returnresult;
}

有了这个辅助方法,我们可以在程序启动后,用户进行身份登录的时候,先调用这个方法来获取令牌。

                string ip =NetworkUtil.GetLocalIP();string macAddr =HardwareInfoHelper.GetMacAddress();string loginName = this.txtLoginName.Text.Trim();string password = this.txtPassword.Text;//如果无法获取访问令牌,则返回
                bool hasGotToken =AccessTokenHelper.GetAccessToken(loginName, password);if (!hasGotToken)
{
return;
}

刚才我提到了Web API层的MVC控制器的接口定义,默认后面一般都有一个token参数,如下代码所示

[HttpGet]public UserInfo GetUserByName(string userName, stringtoken)
{
//令牌检查,不通过则抛出异常 CheckResult checkResult =CheckToken(token);return BLLFactory<User>.Instance.GetUserByName(userName);
}

而我们为了方便客户端调用,一般在客户端调用Web API的时候进行简化了一下,把token参数拿掉,它的值从缓存里面提取。如客户端调用的封装代码如下所示。

        public UserInfo GetUserByName(stringuserName)
{
var action = "GetUserByName";string url = GetTokenUrl(action) + string.Format("&userName={0}", userName);

UserInfo result
= JsonHelper<UserInfo>.ConvertJson(url);returnresult;
}

其中GetTokenUrl就是我们根据token和方法名称,构建一个连接字符串,函数实现如下所示。

        /// <summary>
        ///获取单纯包含token参数的连接/// </summary>
        /// <param name="action">控制器方法名称</param>
        /// <returns></returns>
        protected string GetTokenUrl(stringaction)
{
string url = "";if (this.SignatureInfo != null)
{
var append = string.Format("?token={0}", SignatureInfo.token);string baseUrl =GetBaseUrl();
url
= CombindUrl(baseUrl, action + append);//组合为完整的访问地址 }else{throw new ArgumentNullException("没有在缓存里面设置SignatureInfo签名信息");
}
returnurl;
}

这样最终我们可以获得类似下面的连接地址:

http://localhost:27206/api/Account/GetAccountTypeList?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiIxIiwiaWF0IjoxNDYzNTU3OTAzLCJqdGkiOiI3OGMyOGRhNC01ZjRjLTQxYzItOThkNC1lYmFkZTM3YjA4NjUiLCJuYW1lIjoiYWRtaW4iLCJjaGFubmVsIjoiMCIsInNoYXJlZGtleSI6IjEyMzRhYmNkIn0.DysdbGx70xuIxXBz3G3x3MkGh9ZxL2zF9Fzu8FGVS0w

有了这个令牌组装好的URL,我们可以对访问结果的JSON字符串进行解析,把它解析为对应的数据就可以了。

当然,在实际的Web API接口开发过程中,我们还可以使用Web API工具进行接口调试,如下所示。

下面的1-5的标识就是获取token所需要的签名数据,当然连接还带有几个账号认证所需要的信息了,如账号密码、所在公司等信息。

当然我们也可以使用浏览器进行测试获取Token的信息,只是没有那么方便而已。

在很多时候,我们在数据库里面定义表字段和实际在页面中展示的内容,往往是不太匹配的,页面数据可能是多个表数据的综合体,因此除了我们在表设计的时候考虑周到外,还需要考虑数据展现的处理。如果是常规的处理,那么需要对部分外键字段进行特别的转义处理,如果需要增加多一些字段,那么这种处理可能就相对比较麻烦一些。本文介绍如何在MVC控制器里面使用dynamic和ExpandoObject,实现数据转义后一体化的输出,包括增加任意多的字段信息。

1、数据信息的展示

一般情况下,我们在界面里面展示的信息是相对比较丰富的,尽管我们设计数据表的时候,考虑的是如何精简且避免重复,但是在界面上展示的信息,往往是考虑如何让用户更加方便,因此可能尽可能的展示相关信息。

如对于这样的场景,设备信息作为主要的基础信息,其相关的业务包括设备检查、设备维护、设备报修等信息,如下所示。

基于上面的数据设计,我们如果在展示设备检查、设备维护、设备报修等信息的时候,那么我们一般还需要展示部分的设备基础信息,这样我们更容易了解整个记录数据,但是我们在数据设计的时候,是把它们分开的,因此需要在输出到界面的时候,把它们综合起来。

我以前在《
基于MVC4+EasyUI的Web开发框架经验总结(9)--在Datagrid里面实现外键字段的转义操作
》介绍过一些数据转义的处理,不过那种方式并不是比较理想的方式。本篇介绍的使用dynamic和ExpandoObject才是我理想的处理模式。

我们来看看我最终通过这种方式实现的界面效果,之后我们再来一步步介绍如何实现这个操作过程的。

2、数据转义的实现

在上面的界面效果里面,我们是基于MVC实现后台的处理,在界面上利用Bootstrap进行展示的(利用EaysUI组件也是类似的处理)。我们分为两部分进行介绍实现的,一部分是采用MVC的输出数据,一部分是界面的展示。

1)MVC的控制器数据处理

在MVC里面,我们一般通过基类的FindWithPager进行数据的分页处理,基于如何在MVC控制器里面实现数据的分页处理,大家感兴趣可以参考《
基于Metronic的Bootstrap开发框架经验总结(2)--列表分页处理和插件JSTree的使用
》随笔进行了解。

常规的做法,如果是主表信息,我们可以把它们简单的输出,如下所示。

        public overrideActionResult FindWithPager()
{
//检查用户是否有权限,否则抛出MyDenyAccessException异常 base.CheckAuthorized(AuthorizeKey.ListKey);string where =GetPagerCondition();
PagerInfo pagerInfo
=GetPagerInfo();
List
<DeviceInfo> list = baseBLL.FindWithPager(where, pagerInfo);//Json格式的要求{total:22,rows:{}}//构造成Json的格式传递 var result = new { total = pagerInfo.RecordCount, rows =list };returnToJsonContentDate(result);
}

也就是不需要经过任何转义就直接把查询到的数据列表输出给调用者,由界面进行数据的筛选处理。

如果对于上面提到的设备检查、设备维修等和设备信息相关的,我们就需要利用dynamic和ExpandoObject,把设备信息整合一起提供给界面了,具体代码如下所示。

我们首先对查询的记录进行遍历,把每条记录进行转换,如下所示。

            List<ExpandoObject> objList = new List<ExpandoObject>();foreach (DeviceCheckInfo info inlist)
{
dynamic obj = new ExpandoObject();

注意上面我们定义了List<ExpandoObject>的列表和dynamic obj的对象,这样我们通过动态定义的对象,把我们需要的字段属性加到动态对象里面,然后放到集合里面即可。

完整的分页控制器代码如下所示。

        public overrideActionResult FindWithPager()
{
//检查用户是否有权限,否则抛出MyDenyAccessException异常 base.CheckAuthorized(AuthorizeKey.ListKey);string where =GetPagerCondition();
PagerInfo pagerInfo
=GetPagerInfo();
List
<DeviceCheckInfo> list = baseBLL.FindWithPager(where, pagerInfo);//设备编码 所属科室 品牌 品类 型号 设备序列号 检查时间 处理人 List<ExpandoObject> objList = new List<ExpandoObject>();foreach (DeviceCheckInfo info inlist)
{
dynamic obj = newExpandoObject();

DeviceInfo deviceInfo
= BLLFactory<Device>.Instance.FindByCode(info.DeviceCode);if (deviceInfo != null)
{
obj.Dept
=deviceInfo.Dept;
obj.Brand
=deviceInfo.Brand;
obj.Name
=deviceInfo.Name;
obj.Model
=deviceInfo.Model;
obj.SerialNo
=deviceInfo.SerialNo;
}
obj.ID
=info.ID;
obj.DeviceCode
=info.DeviceCode;
obj.OperateTime
=info.OperateTime;
obj.Operator
=info.Operator;

objList.Add(obj);
}
//Json格式的要求{total:22,rows:{}}//构造成Json的格式传递 var result = new { total = pagerInfo.RecordCount, rows =objList };returnToJsonContentDate(result);
}

2)界面的数据展示

上面定义了数据的获取方式,也就是我们需要任何数据都可以在MVC控制器里面,通过动态属性的方式添加到集合对象里面,从而简化了我们界面的处理,我们只需要把获得的信息展示在界面上即可,非常简便了。

界面视图的HTML代码如下所示

            <tableid="grid"class="table table-striped table-bordered table-hover"cellpadding="0"cellspacing="0"border="0"class="display"width="100%">
                <theadid="grid_head">
                    <tr>
                        <!--设备编码    所属科室    品牌    品类    型号    设备序列号    检查时间    处理人-->
                        <thclass="table-checkbox"style="width:40px"><inputclass="group-checkable"type="checkbox"onclick="selectAll(this)"></th>
                         <th>设备编码</th>
                         <th>所属科室</th>
                         <th>品牌</th>
                         <th>品类</th>
                         <th>型号</th>  
                        <th>设备序列号</th>
                        <th>检查时间</th>
                        <th>处理人</th>
                        <thstyle="width:90px">操作</th>
                    </tr>
                </thead>
                <tbodyid="grid_body"></tbody>
            </table>

我们绑定到界面上,是通过Ajax的方式获取数据,然后绑定显示的,JS代码如下所示。

        functionSearchCondition(page, condition) {//获取Json对象集合,并生成数据显示内容
            url = "/DeviceCheck/FindWithPager?page=" + page + "&rows=" +rows;
$.getJSON(url
+ "&" + condition, function(data) {
$(
"#totalCount").text(data.total);
$(
"#totalPageCount").text(Math.ceil(data.total /rows));

$(
"#grid_body").html("");//<!--设备编码 所属科室 品牌 品类 型号 设备序列号 检查时间 处理人 --> $.each(data.rows, function(i, item) {var tr = "<tr>";
tr
+= "<td><input class='checkboxes' type=\"checkbox\" name=\"checkbox\" value=" + item.ID + "></td>";
tr
+= "<td>" + item.DeviceCode + "</td>";
tr
+= "<td>" + item.Dept + "</td>";
tr
+= "<td>" + item.Brand + "</td>";
tr
+= "<td>" + item.Name + "</td>";
tr
+= "<td>" + item.Model + "</td>";
tr
+= "<td>" + item.SerialNo + "</td>";
tr
+= "<td>" + item.OperateTime + "</td>";
tr
+= "<td>" + item.Operator + "</td>";

tr
+= getActionHtml(item.ID); //获取查看、编辑、删除操作代码 tr+= "</tr>";
$(
"#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(
"");
}
});
}

这样就最终优雅的实现了我们前面介绍的界面效果了。