wenmo8 发布的文章

微信公众号最新修改了素材的管理模式,提供了两类素材的管理:临时素材和永久素材的管理,原先的素材管理就是临时素材管理,永久素材可以永久保留在微信服务器上,微信素材可以在上传后,进行图片文件或者图文消息的发送,关注的公众号可以在素材有效期内查看相关的资源,对于永久素材,那就不会存在过期的问题,只是纯粹数量上限的限制。本文综合两方面进行介绍素材管理的各种接口和实现。

1、素材类型和功能点

关于素材的官方说明:

临时素材:

公众号经常有需要用到一些临时性的多媒体素材的场景,例如在使用接口特别是发送消息时,对多媒体文件、多媒体消息的获取和调用等操作,是通过media_id来进行的。素材管理接口对所有认证的订阅号和服务号开放。通过本接口,公众号可以新增临时素材(即上传临时多媒体文件)。对于临时素材,每个素材(media_id)会在开发者上传或粉丝发送到微信服务器3天后自动删除。素材的格式大小等要求与公众平台官网一致。具体是,图片大小不超过2M,支持bmp/png/jpeg/jpg/gif格式,语音大小不超过5M,长度不超过60秒,支持mp3/wma/wav/amr格式。

永久素材:

除了3天就会失效的临时素材外,开发者有时需要永久保存一些素材,届时就可以通过本接口新增永久素材。新增的永久素材也可以在公众平台官网素材管理模块中看到。永久素材的数量是有上限的,请谨慎新增。图文消息素材和图片素材的上限为5000,其他类型为1000。

素材管理包含了下面截图的相关功能:

2、临时素材的管理接口定义和实现

我们定义一个IMediaApi接口,用来定义相关的接口处理。

1)上传临时文件

对于上传临时文件,官方的接口定义如下所示。

接口调用请求说明

http请求方式: POST/FORM,需使用https
https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE
调用示例(使用curl命令,用FORM表单方式上传一个多媒体文件):
curl -F media=@test.jpg "https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE"

对于上传临时文件的处理,我们可以定义它的接口如下所示。

        /// <summary>
        ///上传的临时多媒体文件。格式和大小限制,如下:///图片(image): 1M,支持JPG格式///语音(voice):2M,播放长度不超过60s,支持AMR\MP3格式///视频(video):10MB,支持MP4格式///缩略图(thumb):64KB,支持JPG格式。///媒体文件在后台保存时间为3天,即3天后media_id失效。/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="type">媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb)</param>
        /// <param name="file">form-data中媒体文件标识,有filename、filelength、content-type等信息</param>
        /// <returns></returns>
        UploadJsonResult UploadTempMedia(string accessToken, UploadMediaFileType type, string file);

根据官方接口的说明,我们需要上传一个文件,并指定它的类型TYPE就可以了。

具体代码如下所示。

        public UploadJsonResult UploadTempMedia(string accessToken, UploadMediaFileType type, stringfile)
{
string url = string.Format("http://file.api.weixin.qq.com/cgi-bin/media/upload?access_token={0}&type={1}", accessToken, type.ToString());

UploadJsonResult result
= JsonHelper<UploadJsonResult>.PostFile(url, file);returnresult;
}

其中JsonHelper类的PostFile就是发送一个文件流,我们进一步可以看它的实现思路如下所示。

        /// <summary>
        ///提交文件并解析返回的结果/// </summary>
        /// <param name="url">提交文件数据的链接地址</param>
        /// <param name="file">文件地址</param>
        /// <returns></returns>
        public static T PostFile(string url, string file, NameValueCollection nvc = null)
{
HttpHelper helper
= newHttpHelper();string content = helper.PostStream(url, new string[] { file }, nvc);
VerifyErrorCode(content);

T result
= JsonConvert.DeserializeObject<T>(content);returnresult;
}

上面代码主要就是通过POST一个文件流,并获得响应的结果字符串内容,然后我们分析其中是否有错误代码,如果没有,我们把字符串结果解析为对应的实体对象就可以了。

其中返回结果的实体类信息UploadJsonResult的类定义如下所示。

    /// <summary>
    ///上传多媒体文件的返回结果/// </summary>
    public classUploadJsonResult : BaseJsonResult
{
/// <summary> ///媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb,主要用于视频与音乐格式的缩略图)/// </summary> public UploadMediaFileType type { get; set; }/// <summary> ///媒体文件上传后,获取时的唯一标识/// </summary> public string media_id { get; set; }/// <summary> ///媒体文件上传时间戳/// </summary> public long created_at { get; set; }
}

这个接口的调用实例代码如下所示。

        private void btnUpload_Click(objectsender, EventArgs e)
{
string file = FileDialogHelper.OpenImage(false);if (!string.IsNullOrEmpty(file))
{
IMediaApi mediaBLL
= newMediaApi();
UploadJsonResult result
=mediaBLL.UploadTempMedia(token, UploadMediaFileType.image, file);if (result != null)
{
this.image_mediaId =result.media_id;
Console.WriteLine(
"{0} {1}", result.media_id, result.created_at);
}
else{
Console.WriteLine(
"上传文件失败");
}
}
}

2)获取临时素材文件

上传文件是上传一个文件流,并获得对应的返回结果,主要就是一个media_Id的内容;而获取素材文件则是一个逆过程,通过一个media_id的参数获取一个文件流保存到本地的过程。

获取临时文件接口的官方定义如下所示。

接口调用请求说明

http请求方式: GET,https调用
https://api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID
请求示例(示例为通过curl命令获取多媒体文件)
curl -I -G "https://api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID"

对于获取临时文件,我们定义的接口如下所示。

        /// <summary>
        ///获取临时素材/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="mediaId">媒体文件ID</param>
        /// <param name="stream"></param>
        Stream GetTempMedia(string accessToken, string mediaId, ref string fileName);

我们获得文件流的同时,也返回一个文件名参数(不过一般情况下,我们获取不到文件名)。

它的实现代码如下所示,主要逻辑就是解析返回结果,获取返回的文件流。

        /// <summary>
        ///获取临时素材/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="mediaId">媒体文件ID</param>
        /// <param name="stream"></param>
        public Stream GetTempMedia(string accessToken, string mediaId, ref stringfileName)
{
string url = string.Format("http://file.api.weixin.qq.com/cgi-bin/media/get?access_token={0}&media_id={1}", accessToken, mediaId);

HttpHelper helper
= newHttpHelper();
Stream stream
= helper.GetStream(url, ref fileName, null);returnstream;
}

获取素材文件的实例代码如下所示。

        private void btnDownload_Click(objectsender, EventArgs e)
{
if (!string.IsNullOrEmpty(image_mediaId))
{
IMediaApi mediaBLL
= newMediaApi();string fileName = "";
Stream stream
= mediaBLL.GetTempMedia(token, image_mediaId, reffileName);if (stream != null)
{
string filePath =Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, fileName);using (var fileStream =File.Create(filePath))
{
byte[] buffer = new byte[1024];int bytesRead = 0;while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) != 0)
{
fileStream.Write(buffer,
0, bytesRead);
}
fileStream.Flush();
}
stream.Close();
}
Console.WriteLine(
"下载文件:" + (File.Exists(fileName) ? "成功" : "失败"));
}
}

3、永久素材的管理接口定义和实现

根据官方接口的描述,我们可以把新增永久素材接口定义为三种:新增图文素材、
其他类型永久素材和视频素材三种接口。

1)新增永久图文素材

接口调用请求说明

http请求方式: POST
https://api.weixin.qq.com/cgi-bin/material/add_news?access_token=ACCESS_TOKEN

调用示例

{
  "articles": [{
       "title": TITLE,
       "thumb_media_id": THUMB_MEDIA_ID,
       "author": AUTHOR,
       "digest": DIGEST,
       "show_cover_pic": SHOW_COVER_PIC(0 / 1),
       "content": CONTENT,
       "content_source_url": CONTENT_SOURCE_URL
    },
    //若新增的是多图文素材,则此处应还有几段articles结构
 ]
}

2)新增其他类型永久素材

接口调用请求说明

通过POST表单来调用接口,表单id为media,包含需要上传的素材内容,有filename、filelength、content-type等信息。请注意:图片素材将进入公众平台官网素材管理模块中的默认分组。

http请求方式: POST
http://api.weixin.qq.com/cgi-bin/material/add_material?access_token=ACCESS_TOKEN
调用示例(使用curl命令,用FORM表单方式新增一个其他类型的永久素材):
curl -F media=@test.jpg "http://file.api.weixin.qq.com/cgi-bin/material/add_material?access_token=ACCESS_TOKEN"

3)新增永久视频素材

在上传视频素材时需要POST另一个表单,id为description,包含素材的描述信息,内容格式为JSON,格式如下:

{
  "title":VIDEO_TITLE,
  "introduction":INTRODUCTION
}

新增永久视频素材的调用示例:

curl "http://file.api.weixin.qq.com/cgi-bin/material/add_material?access_token=ACCESS_TOKEN" -F media=@media.file -F  description='{"title":VIDEO_TITLE, "introduction":INTRODUCTION}'

根据上面的说明,我们定义新增永久图文素材的接口代码如下所示。

        /// <summary>
        ///新增永久图文素材/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="newsList">图文消息组</param>
        /// <returns></returns>
        MaterialResult UploadMaterialNews(string accessToken, List<NewsUploadJson> newsList);

定义新增其他永久素材接口如下:

        /// <summary>
        ///新增其他类型永久素材(图片(image)、语音(voice)和缩略图(thumb))/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="type">媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb)</param>
        /// <param name="file">form-data中媒体文件标识,有filename、filelength、content-type等信息</param>
        /// <returns></returns>
        MaterialResult UploadMaterialMedia(string accessToken, UploadMediaFileType type, string file);

定义新增视频永久素材接口如下所示:

        /// <summary>
        ///在上传视频素材时需要POST另一个表单,id为description,包含素材的描述信息,内容格式为JSON./// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="file">form-data中媒体文件标识,有filename、filelength、content-type等信息</param>
        /// <param name="title">视频标题</param>
        /// <param name="introduction">视频描述</param>
        /// <returns></returns>
        MaterialResult UploadMaterialVideo(string accessToken, string file, string title, string introduction);

这几个接口都没有太多难度,不过在微信接口讨论组里面,很多人对于上传永久素材的操作总是不成功,觉得可能是微信API本身的问题,其实不然,这个接口我还是测试通过了,并且在服务器上看到对应的素材信息,具体我们来看看上传其他类型素材的接口实现代码。

        /// <summary>
        ///新增其他类型永久素材(图片(image)、语音(voice)和缩略图(thumb))/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="type">媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb)</param>
        /// <param name="file">form-data中媒体文件标识,有filename、filelength、content-type等信息</param>
        /// <returns></returns>
        public MaterialResult UploadMaterialMedia(string accessToken, UploadMediaFileType type, stringfile)
{
string url = string.Format("http://api.weixin.qq.com/cgi-bin/material/add_material?access_token={0}&type={1}", accessToken, type.ToString());

MaterialResult result
= JsonHelper<MaterialResult>.PostFile(url, file);returnresult;
}

注意这个URL是http而不是https,有点特殊。

另外,我们在使用POST文件流的时候,HttpWebRequest对象的内容一定要设置好,主要是需要和微信定义的media这个保持一直才可以。如下是HttpHelper 辅助类里面的PostStream的部分代码,供参考。

永久素材上传后的结果可以在微信公众号后台进行查看到,具体界面如下所示。

对于永久素材的接口,我们还可以根据微信API的要求,完善永久素材的更新、删除、获取素材,以及获取素材总数、获取图文素材列表等功能,由于大多数操作类似,不需要一一列出,希望再次抛砖引玉,使得大家能够更好了解、利用好微信公众号的素材管理接口,从而实现我们更加丰富的数据管理。

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

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)--开始使用微信接口

通过模板消息接口,公众号能向关注其账号的用户发送预设模板的消息。模板消息仅用于公众号向用户发送重要的服务通知,只能用于符合其要求的服务场景中,如信用卡刷卡通知,商品购买成功通知等。不支持广告等营销类消息以及其它所有可能对用户造成骚扰的消息。本文主要介绍基于C#开发实现公众号模板消息的管理功能。

“模板消息功能的推出,将极大地增强服务号的服务通知能力”,在一些一直期待微信模板消息功能开放的公众号运营者看来,微信一对一沟通的社交属性,让信息推送的触达率更加精准,这也让企业在成本、服务效率、性能上有了不少优势,不仅丰富了企业的服务形式,增强用户的互动和粘性,还能为用户带来更多元、丰富、及时的服务体验。

1、模板的行业分类管理及说明

模版信息依行业进行划分,并根据使用场景不同设计了不同的模版,如软件行业下就有报名成功通知、看房提醒、订单提醒、会员充值、会员消费通知等各种场景下可能使用到的模版。

如果我们公众号需要使用模板,那么我们需要从模板库里面添加所需的模板(目前数量上限为15个)。模板添加到我的模板后,每个模板就生成了一个随机值,也就是【模板ID】,我们发送信息,就是依照这个模板ID进行发送的。

每个模板里面有详细的参数说明,以及示例效果。

微信团队相关负责人表示:模板消息的开放主要是为了帮助公众号完成闭环服务,现有的公众号,主动发消息能力有限(每月可群发四条消息),这让许多企业无法向用户推送服务结果等消息的主动通知。模板消息开放后,企业可以借助微信平台,运用模板消息,在外部服务和内部管理过程中,让信息的触达更为迅捷,为用户提供更加周到的服务。

微信一直在不断优化用户体验,模板消息的开放,为企业提供了更多的基础能力,比如更丰富的双向互动,更精准的信息提醒等,这些都提升了企业精品化、个性化服务的深度和广度,这也是为什么金融、民航、政务等多领域的机构、企业都期待微信开放模板消息功能的原因。未来,随着模板消息功能的进一步完善,或许企业员工工资明细、住户每月用电量、电费等用电详单,甚至是驾驶证到期需更换等,都能通过企业、部门机构微信公众帐号的模板消息即时传递给相应用户。

2、使用模板消息进行开发

前面介绍了模板的相关信息以及单个模板的介绍,我们如果需要在后台程序中集成模板消息发送的话,那么我们需要了解模板消息的API有那些?如何利用模板消息的API进行消息发送?

我们先来看看模板消息使用的说明:

1、所有服务号都可以在功能->添加功能插件处看到申请模板消息功能的入口,但只有认证后的服务号才可以申请模板消息的使用权限并获得该权限;
2、需要选择公众账号服务所处的2个行业,每月可更改1次所选行业;
3、在所选择行业的模板库中选用已有的模板进行调用;
4、每个账号可以同时使用15个模板。
5、当前每个模板的日调用上限为10万次【2014年11月18日将接口调用频率从默认的日1万次提升为日10万次,可在MP登录后的开发者中心查看】。

模板消息的管理功能有:

1 设置所属行业
2 获得模板ID
3 发送模板消息
4 事件推送

2.1设置所属行业

设置行业可在MP中完成,每月可修改行业1次,账号仅可使用所属行业中相关的模板,为方便第三方开发者,提供通过接口调用的方式来修改账号所属行业,具体如下:

接口调用请求说明

http请求方式: POST
https://api.weixin.qq.com/cgi-bin/template/api_set_industry?access_token=ACCESS_TOKEN

POST数据说明

POST数据示例如下:

      {
          "industry_id1":"1",
          "industry_id2":"4"
       }

根据说明,我们可以定义一个接口类ITemplateMessageApi,然后定义设置所属行业的接口函数如下所示:

        /// <summary>
        ///设置所属行业/// </summary>
        /// <param name="accessToken"></param>
        /// <param name="industry_id1">公众号模板消息所属行业编号(主营行业)</param>
        /// <param name="industry_id2">公众号模板消息所属行业编号(副营行业)</param>
        /// <returns></returns>
        CommonResult SetIndustry(string accessToken, IndustryCode industry_id1, IndustryCode industry_id2);

而为了方便,我们定义IndustryCode为一个枚举对象,里面列出了系统支持的所有行业代码,如下所示。

而实现代码和之前的函数处理类似,都是POST数据到一个连接即可,并解析返回的结果就可以了,具体实现代码如下所示。

        /// <summary>
        ///设置所属行业/// </summary>
        /// <param name="accessToken">访问凭证</param>
        /// <param name="industry_id1">公众号模板消息所属行业编号(主营行业)</param>
        /// <param name="industry_id2">公众号模板消息所属行业编号(副营行业)</param>
        /// <returns></returns>
        public CommonResult SetIndustry(stringaccessToken, IndustryCode industry_id1, IndustryCode industry_id2)
{
var url = string.Format("https://api.weixin.qq.com/cgi-bin/template/api_set_industry?access_token={0}", accessToken);var data = new{
industry_id1
= (int)industry_id1,
industry_id2
= (int)industry_id2
};
string postData =data.ToJson();returnHelper.GetExecuteResult(url, postData);
}

2.2 获得模板ID

获得模板ID,也就是从模板库里面添加对应的模板消息到我的模板里面。

从行业模板库选择模板到账号后台,获得模板ID的过程可在MP中完成。为方便第三方开发者,提供通过接口调用的方式来修改账号所属行业,具体如下:

接口调用请求说明

http请求方式: POST
https://api.weixin.qq.com/cgi-bin/template/api_add_template?access_token=ACCESS_TOKEN

POST数据说明

POST数据示例如下:

      {
           "template_id_short":"TM00015"
       }

C#函数实现代码如下所示:

        /// <summary>
        ///获得模板ID.///从行业模板库选择模板到账号后台,获得模板ID的过程可在MP中完成。/// </summary>
        /// <param name="accessToken">访问凭证</param>
        /// <param name="template_id_short">模板库中模板的编号,有“TM**”和“OPENTMTM**”等形式</param>
        /// <returns></returns>
        public AddTemplateResult AddTemplate(string accessToken, stringtemplate_id_short)
{
var url = string.Format("https://api.weixin.qq.com/cgi-bin/template/api_add_template?access_token={0}", accessToken);var data = new{
template_id_short
=template_id_short
};
string postData =data.ToJson();return JsonHelper<AddTemplateResult>.ConvertJson(url, postData);
}

2.3 发送模板消息

根据上面小节处理,添加到我的模板里面的操作得到的模板ID,我们就可以调用发送模板消息的API进行模板消息发送了。

接口调用请求说明

http请求方式: POST
https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN

POST数据说明

POST数据示例如下:

      {
           "touser":"OPENID",
           "template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY",
           "url":"http://weixin.qq.com/download",
           "topcolor":"#FF0000",
           "data":{
                   "first": {
                       "value":"恭喜你购买成功!",
                       "color":"#173177"
                   },
                   "keynote1":{
                       "value":"巧克力",
                       "color":"#173177"
                   },
                   "keynote2": {
                       "value":"39.8元",
                       "color":"#173177"
                   },
                   "keynote3": {
                       "value":"2014年9月16日",
                       "color":"#173177"
                   },
                   "remark":{
                       "value":"欢迎再次购买!",
                       "color":"#173177"
                   }
           }
       }

根据上面的JSON参数,我们可以看到,有部分是模板消息公共的部分,有部分则是模板消息的具体参数,这些参数需要根据不同的模板进行不同的赋值。

如这部分是共同的:

   touser":"OPENID",
   "template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY",
   "url":"http://weixin.qq.com/download",
   "topcolor":"#FF0000",

根据这个特点,我们定义发送模板消息的接口如下所示:

        /// <summary>
        ///模板消息仅用于公众号向用户发送重要的服务通知,只能用于符合其要求的服务场景中,如信用卡刷卡通知,商品购买成功通知等。///不支持广告等营销类消息以及其它所有可能对用户造成骚扰的消息。/// </summary>
        /// <param name="accessToken">访问凭证</param>
        /// <param name="openId">账号的openID</param>
        /// <param name="templateId">在公众平台线上模板库中选用模板获得ID</param>
        /// <param name="data">模板的变化参数数据</param>
        /// <param name="url">,URL置空,则在发送后,点击模板消息会进入一个空白页面(ios),或无法点击(android)。</param>
        /// <param name="topcolor">顶部颜色,默认为#173177</param>
        /// <returns></returns>
        SendMassMessageResult SendTemplateMessage(string accessToken, string openId, string templateId, object data, string url, string topcolor = "#173177");

我们用
object
data
来定义模板的变化参数数据。

具体的实现还是和前面的方法提交数据处理差不多,代码如下所示。

        /// <summary>
        ///模板消息仅用于公众号向用户发送重要的服务通知,只能用于符合其要求的服务场景中,如信用卡刷卡通知,商品购买成功通知等。///不支持广告等营销类消息以及其它所有可能对用户造成骚扰的消息。/// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="accessToken">访问凭证</param>
        /// <param name="openId"></param>
        /// <param name="templateId">在公众平台线上模板库中选用模板获得ID</param>
        /// <param name="data"></param>
        /// <param name="url">,URL置空,则在发送后,点击模板消息会进入一个空白页面(ios),或无法点击(android)。</param>
        /// <param name="topcolor"></param>
        /// <returns></returns>
        public SendMassMessageResult SendTemplateMessage(string accessToken, string openId, string templateId, object data, string url, string topcolor = "#173177")
{
var postUrl = string.Format("https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={0}", accessToken);var msgData = newTemplateData()
{
touser
=openId,
template_id
=templateId,
topcolor
=topcolor,
url
=url,
data
=data
};
string postData =msgData.ToJson();

SendMassMessageResult result
= JsonHelper<SendMassMessageResult>.ConvertJson(postUrl, postData);returnresult;
}

发送模板的消息相对其他两个接口的使用复杂一些,例如我以一个会员通知的模板消息为例,模板的详细情况如下:

具体的测试代码如下所示。

            #region 发送模板消息

            var data = new{//使用TemplateDataItem简单创建数据。
                first = new TemplateDataItem("您好,您已成为微信【广州爱奇迪】会员。"),
type
= new TemplateDataItem("18620292076"),
address
= new TemplateDataItem("广州市白云区广州大道北"),
VIPName
= new{//使用new 方式,构建数据,包括value, color两个固定属性。 value = "伍华聪",
color
= "#173177"},
VIPPhone
= new TemplateDataItem("18620292076"),
expDate
= new TemplateDataItem("2016年4月18日"),
remark
= new TemplateDataItem("如有疑问,请咨询18620292076。", "#173177"),
};
#endregion string url = "http://www.iqidi.com";string topColor = "#173177";string templateId = "-5LbClAa9KUlEmr5bCSS0rxU_I2iT16iYBDxCVU1iJg";
SendMassMessageResult sendResult
=api.SendTemplateMessage(token, openId, templateId, data, url, topColor);if(sendResult != null)
{
Console.WriteLine(sendResult.msg_id);
}

那么我们得到的提示效果如下所示。

微信模板消息,能够让我们与客户之间沟通不受每月几条数量的限制,同时也能够利用微信模板库丰富的内容,实现强大的应用场景。

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

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)--开始使用微信接口

在使用Entity Framework 实体框架的时候,我们大多数时候操作的都是实体模型Entity,这个和数据库操作上下文结合,可以利用LINQ等各种方便手段,实现起来非常方便,一切看起来很美好。但是如果考虑使用WCF的时候,可能就会碰到很多相关的陷阱或者错误了。因为实体模型Entity的对象可能包括了其他实体的引用,在WCF里面就无法进行序列化,出现错误;而且基于WCF的时候,可能无法有效利用Express表达式,无法直接使用LINQ等问题都一股脑出现了。本文基于上面的种种问题,阐述了我的整个Entity Framework 实体框架的解决思路,并且在其中引入了数据传输模型DTO来解决问题,本文主要介绍数据传输模型DTO和实体模型Entity的分离与联合,从而实现我们通畅、高效的WCF应用框架。

1、实体模型Entity无法在WCF中序列化

例如,我们定义的Entity Framework 实体类里面包含了其他对象的引用,例如有一个Role对象,有和其他表的关联关系的,默认使用传统方式,在实体类里面添加[DataContract]方式。

    /// <summary>
    ///角色/// </summary>
    [DataContract(IsReference = true)]public classRole
{
/// <summary> ///默认构造函数(需要初始化属性的在此处理)/// </summary> publicRole()
{
this.ID=System.Guid.NewGuid().ToString();//Children = new HashSet<Role>();//Users = new HashSet<User>(); }#region Property Members[DataMember]public virtual string ID { get; set; }/// <summary> ///角色名称/// </summary> [DataMember]public virtual string Name { get; set; }/// <summary> ///父ID/// </summary> [DataMember]public virtual string ParentID { get; set; }

[DataMember]
public virtual ICollection<Role> Children { get; set; }

[DataMember]
public virtual Role Parent { get; set; }

[DataMember]
public virtual ICollection<User> Users { get; set; }#endregion}

在WCF服务接口里面使用代码如下所示。

    public classService1 : IService1
{
public List<Role>GetAllRoles()
{
return IFactory.Instance<IRoleBLL>().GetAll().ToList();
}

.........

那么我们在WCF里面使用的时候,会得到下面的提示。

接收对 http://localhost:11229/Service1.svc 的 HTTP 响应时发生错误。这可能是由于服务终结点绑定未使用 HTTP 协议造成的。这还可能是由于服务器中止了 HTTP 请求上下文(可能由于服务关闭)所致。有关详细信息,请参见服务器日志。

默认情况下,Entity Framework为了支持它的一些高级特性(延迟加载等),默认将自动生成代理类是设置为true。如果我们需要禁止自动生成代理类,那么可以在数据库操作上下文DbContext里面进行处理设置。

Configuration.ProxyCreationEnabled = false;

如果设置为false,那么WCF服务可以工作正常,但是实体类对象里面的其他对象集合则为空了,也就是WCF无法返回这些引用的内容。

同时,在Entity Framework框架里面,这种把实体类贯穿各个层里面,也是一种不推荐的做法,由于WCF里面传输的数据都是序列号过的数据,也无法像本地一样利用LINQ来实现数据的处理操作的。

那么我们应该如何构建基于WCF引用的Entity Framework实体框架呢?

2、数据传输对象DTO的引入

前面介绍了直接利用Entity Framework实体类对象的弊端,并且如果是一路到底都使用这个实体类,里面的很多对象引用都是空的,对我们在界面层使用不便,而且也可能引发了很多WCF框架里面的一些相关问题。

我们根据上面的问题,引入了一个DTO(数据传输对象)的东西。

数据传输对象(DTO)是没有行为的POCO对象,它的目的只是为了对领域对象进行数据封装,实现层与层之间的数据传递,界面表现层与应用层之间是通过数据传输对象(DTO)进行交互的。数据传输对象DTO本身并不是业务对象,数据传输对象是根据UI的需求进行设计的。

这个对象和具体数据存储的实体类是独立的,它可以说是实体类的一个映射体,名称可以和实体类不同,属性数量也可以实体类不一致。那么既然在实体对象层外引入了另外一个DTO对象层,那么相互转换肯定是避免不了的了,我们为了避免手工的映射方式,引入了另外一个强大的自动化映射的工具AutoMapper,来帮助我们快速、高效、智能的实现两个层对象的映射处理。

AutoMapper的使用比较简单,一般如果对象属性一直,他们会实现属性自动映射了,如下所示。

Mapper.CreateMap<RoleInfo, Role>();

如果两者的属性名称不一致,那么可以通过ForMember方式指定,类似下面代码所示。

AutoMapper.Mapper.CreateMap<BlogEntry, BlogPostDto>()
.ForMember(dto
=> dto.PostId, opt => opt.MapFrom(entity => entity.ID));

AutoMapper也可以把映射信息写到一个类里面,然后统一进行加载。

Mapper.Initialize(cfg =>{
cfg.AddProfile
<OrganizationProfile>();
});

那么基于上面的图示模式,由于我们采用代码生成工具自动生成的DTO和Entity,他们属性名称是保持一致的,那么我们只需要在应用层对它们两者对象进行相互映射就可以了。

    public class RoleService : BaseLocalService<RoleInfo, Role>, IRoleService
{
private IRoleBLL bll = null;public RoleService() : base(IFactory.Instance<IRoleBLL>())
{
bll
= baseBLL asIRoleBLL;//DTO和Entity模型的相互映射 Mapper.CreateMap<RoleInfo, Role>();
Mapper.CreateMap
<Role, RoleInfo>();
}
}

基于这个内部对接的映射关系,我们就可以在Facade接口层提供统一的DTO对象服务,而业务逻辑层(也就是利用Entity Framework 实体框架的处理成)则依旧使用它的Entity对象来传递。下面我提供几个封装好的基类接口供了解DTO和Entity的相互衔接处理。

1)传入DTO对象,并转换为Entity对象,使用EF对象插入。

        /// <summary>
        ///插入指定对象到数据库中/// </summary>
        /// <param name="dto">指定的对象</param>
        /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns>
        public virtual boolInsert(DTO dto)
{
Entity t
= dto.MapTo<Entity>();returnbaseBLL.Insert(t);
}

2)根据条件从EF框架中获取Entity对象,并转换后返回DTO对象

        /// <summary>
        ///查询数据库,返回指定ID的对象/// </summary>
        /// <param name="id">ID主键的值</param>
        /// <returns>存在则返回指定的对象,否则返回Null</returns>
        public virtual DTO FindByID(objectid)
{
Entity t
=baseBLL.FindByID(id);return t.MapTo<DTO>();
}

3)根据条件从EF框架中获取Entity集合对象,并转换为DTO列表对象

        /// <summary>
        ///返回数据库所有的对象集合/// </summary>
        /// <returns></returns>
        public virtual ICollection<DTO>GetAll()
{
ICollection
<Entity> tList =baseBLL.GetAll();return tList.MapToList<Entity, DTO>();
}

3、Entity Framework 实体框架结构

基于方便管理的目的,每个模块都可以采用一种固定分层的方式来组织模块的业务内容,每个模块都是以麻雀虽小、五脏俱全的方针实施。实例模块的整个业务逻辑层的项目结构如下所示。

如果考虑使用WCF,那么整体的结构和我之前的混合框架差不多,各个模块的职责基本没什么变化,不过由原先在DAL层分开的各个实现层,变化为各个数据库的Mapping层了,而模型增加了DTO,具体项目结构如下所示。

具体的项目说明如下所示:

EFRelationship

系统的业务模块及接口、数据库访问模块及接口、DTO对象、实体类对象、各种数据库映射Mapping类等相关内容。该模块内容紧密结合Database2Sharp强大代码生成工具生成的代码、各层高度抽象继承及使用泛型支持多数据库。

EFRelationship.WCFLibrary

系统的WCF服务的业务逻辑模块,该模块通过引用文件方式,把业务管理逻辑放在一起,方便WCF服务部署及调用。

EFRelationshipService

框架WCF服务模块,包括基础服务模块BaseWcf和业务服务模块,他们为了方便,分开管理发布。

EFRelationship.Caller

定义了具体业务模块实现的Façade应用接口层,并对Winform调用方式和WCF调用方式进行包装的项目。

具体我们以一个会员系统设计为例,它的程序集关系如下所示。

我们来看看整个架构的设计效果如下所示。

其中业务逻辑层模块(以及其它应用层)我们提供了很多基于实体框架的公用类库(WHC.Framework.EF),其中的继承关系我们将它放大,了解其中的继承细节关系,效果如下所示。

上图很好的概述了我的EF实体框架的设计思路,这些层最终还是通过代码生成工具Database2Sharp进行一体化的生成,以提高快速生产的目的,并且统一所有的命名规则。后面有机会再写一篇随笔介绍代码生成的逻辑部分。

上图左边突出的两个工厂类,一个IFactory是基于本地直连方式,也就是直接使用EF框架的对象进行处理;一个CallerFactory是基于Facade层实现的接口,根据配置指向WCF数据服务对象,或者直连对象进行数据的操作处理。

这个系列文章如下所示:

Entity Framework 实体框架的形成之旅--基于泛型的仓储模式的实体框架(1)

Entity Framework 实体框架的形成之旅--利用Unity对象依赖注入优化实体框架(2)

Entity Framework 实体框架的形成之旅--基类接口的统一和异步操作的实现(3)

Entity Framework 实体框架的形成之旅--实体数据模型 (EDM)的处理(4)

Entity Framework 实体框架的形成之旅--Code First的框架设计(5)

Entity Framework 实体框架的形成之旅--Code First模式中使用 Fluent API 配置(6)

Entity Framework 实体框架的形成之旅--数据传输模型DTO和实体模型Entity的分离与联合

在很多应用系统里面,虽然一般采用一种数据库运行,但是由于各种情况的需要,可能业务系统会部署在不同类型的数据库上,如果开发的系统能够很方便支持多种数据库的切换,那可以为我们减少很多烦恼,同时提高系统的适应性和强壮型。还有一种情况,由于业务数据库的不断膨胀或者方便数据库的切割隔离,有时候也会把不同的业务数据库进行分拆,如权限提供数据库,客户关系管理数据库,工作流程数据库,企业营运数据库等等,因此在一个系统里面,同时使用2个或者以上的数据库的情况也是有的。

在我较早期的一篇随笔《
Winform开发框架中实现多种数据库类型切换以及分拆数据库的支持
》里面,介绍了框架如何在一个项目里支持多个数据库的拆分处理。一般情况下,我们都是在一个数据库类型的情况下,分拆多个数据库,但是也有变态的需求例外,如我们可能把一些常规的数据库存储在本地SQLite数据库里面,其他的一些数据放在局域网其他类型数据库(如SQLServer)里面;那么这种同一时刻支持多种数据库类型,并且给用户创建BLL层类的时候,动态指定数据库是否可以实现的呢?

当然可以,我们在上面的基础上稍微调整一下创建方法即可。

1、同时支持多种类型数据库处理并存

之前介绍了,需要分拆数据库的话,我们需要在数据访问基类
AbstractBaseDAL
里面,添加一个SetConfigName的方法,用来指定具体的数据库配置项目,如下所示。

    /// <summary>
    ///数据访问层的超级基类,所有数据库的数据访问基类都继承自这个超级基类,包括Oracle、SqlServer、Sqlite、MySql、Access等/// </summary>
    public abstract class AbstractBaseDAL<T> where T : BaseEntity, new()
{
/// <summary> ///设置数据库配置项名称/// </summary> /// <param name="dbConfigName">数据库配置项名称</param> public virtual void SetDbConfigName(stringdbConfigName)
{
this.dbConfigName =dbConfigName;
}

....................
}

那么我们这次只需要在这个基础上调整一下就可以实现同一时刻变换不同数据库支持的了,由于在框架里面,我们一般已经实现了多种数据库访问的逻辑(如下所示),因此切换起来访问肯定也是没问题的(保证有数据库即可)。

方法就是在BLL层里面添加一个公开方法,可以设置配置项和数据库类型的函数,如下所示。

        /// <summary>
        ///根据参数信息,重新初始化数据访问层(例:可以指定不同的数据访问层)/// </summary>
        /// <param name="dbConfigName">数据库配置项名称</param>
        /// <param name="componentDbType">数据库类型,默认从ComponentDbType中读取,如果dbConfigName指定不同类型的数据库连接,需要指定componentDbType。</param>
        public void SetConfigName(string dbConfigName, string componentDbType = null)
{
//componentDbType = null时,从配置项取ComponentDbType的值 string dbType =componentDbType;if (string.IsNullOrEmpty(componentDbType))
{
AppConfig config
= newAppConfig();
dbType
= config.AppConfigGet("ComponentDbType");
}
string DALPrefix =GetDALPrefix(dbType);this.dalName = bllFullName.Replace(bllPrefix, DALPrefix);//替换中级的BLL.为DAL.,就是DAL类的全名 baseDal = Reflect<IBaseDAL<T>>.Create(this.dalName, dalAssemblyName);//构造对应的DAL数据访问层的对象类 if (!string.IsNullOrEmpty(dbConfigName))
{
baseDal.SetDbConfigName(dbConfigName);
//设置数据库配置项名称 }
}

这样我们除了可以设置EnterpriseLibrary的配置项外,还可以指定这个数据库的类型,不需要全部使用统一的ComponentDbType的值。

如下代码处理,我们就可以在访问其他数据库的时候,切换这个BLL层的对象为其他类型的数据库(SQLite),这样不管其他类如何变化,这个Province的数据访问的是SQLite数据库里面的数据。

BLLFactory<Province>.Instance.SetConfig("sqlite", "sqlite");

2、支持多数据库操作的基类CommonDAL,实现不同数据库访问

有时候,我们在一个比较小的应用程序里面,想灵活对数据库表进行一些简单的处理操作,不想使用代码生成工具生成整个架构的代码,那么这个时候,这个CommonDAL就派上用场了,这个可以快速访问数据库的表,它的定义如下所示。

这个类的几个构造函数如下所示,参数分别为表名,主键字段,数据库类型。

        /// <summary>
        ///默认构造函数/// </summary>
        publicCommonDAL()
{
}
/// <summary> ///指定表名以及主键,对基类进构造/// </summary> /// <param name="tableName">表名</param> /// <param name="primaryKey">表主键</param> /// <param name="dbType">数据库类型,如果为空从配置文件里面获取ComponentDbType的键值</param> public CommonDAL(string tableName) : this(tableName, null, null)
{

}
/// <summary> ///指定表名以及主键,对基类进构造/// </summary> /// <param name="tableName">表名</param> /// <param name="primaryKey">表主键</param> /// <param name="dbType">数据库类型,如果为空从配置文件里面获取ComponentDbType的键值</param> public CommonDAL(string tableName, string primaryKey, string dbType = null)
:
this()
{
}

这个就是一个精简版本的AbstractBaseDAL基类,提供了我们能够使用的多数数据库操作方法。

例如我在一个验证视图及其格式的例子程序里面,就使用了这个类来实现快速的数据库操作处理。

    /// <summary>
    ///判断视图名称是否存在的任务/// </summary>
    public classViewNameExistJob : IExecuteJob
{
public boolExecute()
{
List
<string> list =DataHelper.GetViewList();bool allSuccess = true;foreach (string view inlist)
{
CommonDb dal
= new CommonDb(view, "F_Guid");try{
DataTable dt
=dal.GetReaderSchema(view);
}
catch(Exception ex)
{
allSuccess
= false;
LogTextHelper.Error(
string.Format("视图:{0}不存在。{1}", view, ex.Message));
}
}
if(allSuccess)
{
LogTextHelper.Info(
"视图全部存在。");
}
returnallSuccess;
}
}

或者其他数据库访问处理。

                CommonDb dal = new CommonDb(view, "F_Guid");try{int count =dal.GetRecordCount();if (count == 0)
{
LogTextHelper.Info(
string.Format("视图【{0}】数据为空,请检查!", view));
allSuccess
= false;
}
}
catch(Exception ex)
{
allSuccess
= false;
LogTextHelper.Error(
string.Format("视图:【{0}】不存在。{1}", view, ex.Message));
}

这样也可以实现多数据库的随便切换,不过这个是用于简易的数据库访问,对于需要多种业务封装的处理类,我们还是使用常规的框架分层模式来实现数据的处理操作。

”摇一摇周边“是微信提供的一种新的基于位置的连接方式。用户通过“摇一摇”的“周边”页卡,可以与线下商户进行互动,获得商户提供的个性化的服务。微信4月份有一个赠送摇一摇设备的活动,我们有幸获得赠送资格,取得一个摇一摇的设备用来测试这个新增的、很有潜力的功能。”摇一摇周边“是微信基于低功耗蓝牙技术的 O2O 入口级应用,与微信的其他线下连接能力一道,加速促成了微信 O2O 闭环的实现。本文主要介绍一摇设备的配置使用,以及如何在开发层面上,定义及实现微信摇一摇的功能接口。

1、IBeacon基础知识介绍

摇一摇周边是基于IBeacon来实现的。IBeacon是苹果公司开发的一种通过低功耗蓝牙技术进行一个十分精确的微定位技术。IBeacon设备通过蓝牙信号广播设备id, 手机等终端进入IBeacon设备的信号范围,可以收到该设备的id.

实现分如下四个步骤:

第一步. 服务提供者向微信后台申请服务,微信后台生成一个IBeaconId,并将其映射到服务提供者提供的服务,再将IBeaconId告诉服务提供者;

第二步. 服务提供者把第一步拿到的IBeaconId设置到IBeacon设备上,让IBeacon设备广播该IBeaconId;

第三步. 用户在该IBeacon设备的信号范围内打开微信摇一摇周边,微信App拿到该IBeaconId;

第四步. 微信通过第三步拿到的IBeaconId,向微信后台拉取相应的服务,展示在摇出来的结果上。

第五步. 用户点击摇出来结果,在微信内嵌的浏览器上,会带上用户信息跳转到服务提供者在第一步申请服务时填的url,进入应用页面

应用场景:

2、摇一摇设备的使用

摇一摇的beacon设备很小,底座可以用赠送的双面胶粘贴在墙面上,底面还可以贴一个微信的提示标签,挺有意思的效果。

同时可以下载这个beacon设备的App软件RealKit进行设置设备,设备界面管理界面如下所示。

根据官方摇一摇(
https://zb.weixin.qq.com/
)的配置说明,我们拥有设备后,需要配置设备响应的页面,流程如下所示。

由于是微信赠送的设备,默认情况下,微信后台已经给我们添加赠送的摇一摇设备记录了;

如果是自己购买这种摇一摇的设备,那么需要自己手工添加设备记录,并输入相关的参数即可。

添加设备后,我们需要配置摇一摇的页面,页面就是在摇动微信的时候,显示给微信用户的一个界面,页面管理界面如下所示。

最终配置完成后,我们就来试一下这个神秘的设备了,看看效果如何。这个设备的信号穿透力还是很不错,隔了10米,两堵墙摇一摇还是很快出来界面的。

这个摇一摇周边的功能,我在IPhone4S里面始终无法出现周边这个模块,在5、6plus里面倒是很正常的出现并响应处理,而IPAD则是在摇一摇后出现页面的,但是页面无法响应。

3、基于C#的微信摇一摇接口的实现

摇一摇和功能界面相呼应,提供了设备管理、页面管理、素材管理、关联关系绑定、设备及用户信息、数据统计等功能接口,如下所示。

1)设备管理

而其中设备管理部分,又分为了好几个API的处理。

1 申请设备ID
2 编辑设备信息
3 配置设备与门店的关联关系
4 查询设备列表

申请设备ID

接口说明
申请配置设备所需的UUID、Major、Minor。申请成功后返回批次ID,可用返回的批次ID用“查询设备列表”接口拉取本次申请的设备ID。单次新增设备超过500个,需走人工审核流程,大概需要三个工作日;单次新增设备不超过500个的,当日可返回申请的设备ID。一个公众账号最多可申请99999个设备ID,如需申请的设备ID数超过最大限额,请邮件至zhoubian@tencent.com,邮件格式如下: 标题:申请提升设备ID额度 内容:1、公众账号名称及appid(wx开头的字符串,在mp平台可查看) 2、用途 3、预估需要多少设备ID。

接口调用说明

http请求方式: POST(请使用https协议)
https://api.weixin.qq.com/shakearound/device/applyid?access_token=ACCESS_TOKEN
POST数据格式:json
POST数据例子:
{
   "quantity":3,	
   "apply_reason":"测试",	
   "comment":"测试专用",
   "poi_id":1234	
}

参数说明

参数 是否必须 说明
access_token 调用接口凭证
quantity 申请的设备ID的数量,单次新增设备超过500个,需走人工审核流程
apply_reason 申请理由,不超过100个字
comment 备注,不超过15个汉字或30个英文字母
poi_id 设备关联的门店ID,关联门店后,在门店1KM的范围内有优先摇出信息的机会。门店相关信息具体可
查看门店相关的接口文档

返回说明
正常时的返回JSON数据包示例:

当申请个数小于等于500时,
{
"data": {
       "apply_id": 123,
       "device_identifiers":[
			{
				"device_id":10100,	
				"uuid":"FDA50693-A4E2-4FB1-AFCF-C6EB07647825",		
				"major":10001,
				"minor":10002
			}
		]
    },
    "errcode": 0,
    "errmsg": "success."
}
当申请个数大于500时,
{
"data": {
               "apply_id": 123,
		"audit_status": 0,	
		"audit_comment": "审核未通过"	
   },
   "errcode": 0,
   "errmsg": "success."
}


参数说明

参数 说明
device_identifiers 指定的设备ID列表
device_id 设备编号
UUID、major、minor UUID、major、minor
audit_status 审核状态。0:审核未通过、1:审核中、2:审核已通过;审核会在三个工作日内完成
audit_comment 审核备注,包括审核不通过的原因
apply_id 申请的批次ID,可用在“查询设备列表”接口按批次查询本次申请成功的设备ID。

根据这些接口定义,我们可以创建一个摇一摇专用的接口类IShakeAround。

    /// <summary>
    ///摇一摇周边的接口定义/// </summary>
    public interfaceIShakeAround
{
#region 设备管理 /// <summary> ///申请设备ID。///接口说明 申请配置设备所需的UUID、Major、Minor。///若激活率小于50%,不能新增设备。单次新增设备超过500个,需走人工审核流程。审核通过后,可用返回的批次ID用“查询设备列表”接口拉取本次申请的设备ID。/// </summary> /// <param name="accessToken">调用接口凭证</param> /// <param name="applyInfo">设备申请信息</param> ShakeDeviceApplyJson ApplyDevice(stringaccessToken, ShakeDeviceAddJson applyInfo);/// <summary> ///编辑设备信息。///接口说明 编辑设备的备注信息。可用设备ID或完整的UUID、Major、Minor指定设备,二者选其一。/// </summary> /// <param name="accessToken">调用接口凭证</param> /// <param name="info">设备信息</param> /// <param name="comment">设备的备注信息,不超过15个汉字或30个英文字母。</param> /// <returns></returns> CommonResult UpdateDevice(string accessToken, ShakeDeviceIdentifier info, stringcomment);/// <summary> ///配置设备与门店的关联关系。接口说明 修改设备关联的门店ID、设备的备注信息。可用设备ID或完整的UUID、Major、Minor指定设备,二者选其一。/// </summary> /// <param name="accessToken">调用接口凭证</param> /// <param name="info">设备信息</param> /// <param name="poi_id">待关联的门店ID</param> /// <returns></returns> CommonResult BindDevice(string accessToken, ShakeDeviceIdentifier info, intpoi_id);/// <summary> ///查询设备列表.///接口说明 查询已有的设备ID、UUID、Major、Minor、激活状态、备注信息、关联门店、关联页面等信息。可指定设备ID或完整的UUID、Major、Minor查询,也可批量拉取设备信息列表。/// </summary> /// <param name="accessToken">调用接口凭证</param> /// <param name="device_identifiers">设备列表信息</param> /// <returns></returns> ShakeDeviceSearchList SearchDevice(string accessToken, List<ShakeDeviceIdentifier>device_identifiers);/// <summary> ///查询设备列表.///接口说明 查询已有的设备ID、UUID、Major、Minor、激活状态、备注信息、关联门店、关联页面等信息。可指定设备ID或完整的UUID、Major、Minor查询,也可批量拉取设备信息列表。/// </summary> /// <param name="accessToken">调用接口凭证</param> /// <param name="data">分页查询条件。apply_id为批次ID,如果指定则以批次进行查询,否则以指定范围查询。</param> /// <returns></returns> ShakeDeviceSearchList SearchDevice(stringaccessToken, ShakeDeviceSearchPaging data);#endregion

接口定义好,我们增加对应的类实现即可,如下是申请设备的接口具体实现函数,其他遵循
同样的规则
就不再赘述。

        /// <summary>
        ///申请设备ID。///接口说明 申请配置设备所需的UUID、Major、Minor。///若激活率小于50%,不能新增设备。单次新增设备超过500个,需走人工审核流程。审核通过后,可用返回的批次ID用“查询设备列表”接口拉取本次申请的设备ID。/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="applyInfo">设备申请信息</param>
        public ShakeDeviceApplyJson ApplyDevice(stringaccessToken, ShakeDeviceAddJson applyInfo)
{
var url = string.Format("https://api.weixin.qq.com/shakearound/device/applyid?access_token={0}", accessToken);string postData =applyInfo.ToJson();

ShakeDeviceApplyJson data
= null;
ShakeDeviceApplyResult result
= JsonHelper<ShakeDeviceApplyResult>.ConvertJson(url, postData);if (result != null)
{
data
=result.data;
}
returndata;
}

2)页面管理

同样页面管理也包含了几个不同的方法,用来创建、编辑、删除页面等处理操作

1 新增页面
2 编辑页面信息
3 查询页面列表
4 删除页面

新增页面

新增摇一摇出来的页面信息,包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。其中,图片必须为用素材管理接口上传至微信侧服务器后返回的链接。

接口调用说明

http请求方式: POST(请使用https协议)
https://api.weixin.qq.com/shakearound/page/add?access_token=ACCESS_TOKEN
POST数据格式:json
POST数据例子:
{
   "title":"主标题",   
   "description":"副标题",	
   "page_url":" https://zb.weixin.qq.com ",	
   "comment":"数据示例",
   "icon_url":"http://3gimg.qq.com/shake_nearby/dy/icon "
}

根据这些接口定义,同样我们可以为IShakeAround接口类增加对应的接口定义了。

        #region 页面管理

        /// <summary>
        ///新增页面。///新增摇一摇出来的页面信息,包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。///其中,图片必须为用素材管理接口上传至微信侧服务器后返回的链接。/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="info">新增页面POST数据对象</param>
        /// <returns></returns>
        ShakePageResult AddPage(stringaccessToken, ShakePageJson info);/// <summary>
        ///编辑页面信息。///编辑摇一摇出来的页面信息,包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="info">编辑页面POST数据对象</param>
        /// <returns></returns>
        ShakePageResult UpdatePage(stringaccessToken, ShakePageJson info);/// <summary>
        ///查询页面列表。///查询已有的页面,包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。///提供询方式:指定页面ID查询。/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="begin">页面列表的起始索引值</param>
        /// <param name="count">待查询的页面个数</param>
        /// <returns></returns>
        ShakePageSearchJson SearchPage(string accessToken, int begin, intcount);/// <summary>
        ///查询页面列表。///查询已有的页面,包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。///提供询方式:批量拉取页面列表/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="page_ids">指定页面的id列表</param>
        /// <returns></returns>
        ShakePageSearchJson SearchPage(string accessToken, List<int>page_ids);/// <summary>
        ///查询页面列表。///查询已有的页面,包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。///提供两种查询方式,可指定页面ID查询,也可批量拉取页面列表。/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="info"></param>
        /// <returns></returns>
        ShakePageSearchJson SearchPage(stringaccessToken, ShakePageSearchPaging info);/// <summary>
        ///删除页面。///删除已有的页面,包括在摇一摇页面出现的主标题、副标题、图片和点击进去的超链接。///只有页面与设备没有关联关系时,才可被删除。/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="page_ids">指定页面的id列表</param>
        /// <returns></returns>
        CommonResult DeletePage(string accessToken, List<int>page_ids);#endregion 页面管理

3)其他管理

当然除了上面设备、页面的管理,还有一些如

上传图片素材、配置设备与页面的关联关系、以设备为维度的数据统计接口、以页面为维度的数据统计接口等方法接口用来实现,根据上面的处理方式定义即可。

4)测试代码

增加相关的接口定义,以及完成对应的接口实现,我们就需要编写一些测试类来对我们的接口进行测试的,这些对设备、页面的测试代码如下所示。

       /// <summary>
        ///摇一摇设备的申请、修改、绑定处理操作测试/// </summary>
        private void btnDevice_Click(objectsender, EventArgs e)
{
try{int poi_id = 275961135;
ShakeDeviceApplyJson result
= api.ApplyDevice(this.token, new ShakeDeviceAddJson() { quantity = 1, apply_reason = "测试", comment = "测试备注", poi_id =poi_id });if (result != null)
{
Console.WriteLine(result.ToJson());
if (result.device_identifiers != null)
{
ShakeDeviceIdentifier device
= result.device_identifiers[0];if (device != null)
{
int device_id = device.device_id; //465123; Console.WriteLine(device_id);string comment = "修改的备注";
ShakeDeviceIdentifier info
= newShakeDeviceIdentifier(device.device_id, device.uuid, device.major, device.minor);
CommonResult comResult
= api.UpdateDevice(this.token, info, comment);
MessageUtil.ShowTips(comResult.Success
? "操作成功" : "修改设备失败:" +comResult.ErrorMessage);

comResult
= api.BindDevice(this.token, info, poi_id);
MessageUtil.ShowTips(comResult.Success
? "操作成功" : "修改设备失败:" +comResult.ErrorMessage);
}
}
}
}
catch(Exception ex)
{
Console.WriteLine(ex);
}

}

        /// <summary>
        ///增加摇一摇素材、增加页面、修改页面、删除页面的操作示例代码/// </summary>
        private void btnPage_Click(objectsender, EventArgs e)
{
string file = FileDialogHelper.OpenImage(false);if(string.IsNullOrEmpty(file))
{
return;
}

ShakeMaterialJson mediaJson
= api.AddMaterail(this.token, file);if (mediaJson != null)
{
ShakePageJson json
= newShakePageJson()
{
title
= "主标题",
description
= "副标题",
comment
= "备注说明",
page_url
= "https://www.iqidi.com",
icon_url
=mediaJson.pic_url
};
ShakePageResult result
= api.AddPage(this.token, json);if (result != null)
{
Console.WriteLine(result.ToJson());
if (result.data != null)
{
json.page_id
=result.data.page_id;
json.comment
= "修改备注信息";

result
= api.UpdatePage(this.token, json);if (result != null)
{
Console.WriteLine(
"修改页面");
Console.WriteLine(result.ToJson());
}

CommonResult comResult
= api.DeletePage(this.token, new List<int>() { json.page_id.Value });
MessageUtil.ShowTips(comResult.Success
? "删除页面操作成功" : "删除页面失败:" +comResult.ErrorMessage);
}
}
}
}

个人认为,摇一摇设备,开启了一个更广阔的应用空间,随着越来越多设备商的支持,微信接口的完善和增加,可以迸发出更多有意思、实用的应用场景。