wenmo8 发布的文章

在一般的Socket应用里面,很多时候数据的发送和接收是分开处理的,也就是我们发送一个消息,不知道这个请求消息什么时候得到应答消息,而且收到对应的应答消息的时候,如果操作界面的内容,也是需要特别处理的,因为它们和界面线程是不在一起的。如果我们在发送消息的时候,能够给一段回调的代码给收到应答消息的时候处理,那么就会方便很多。本文主要介绍如何在Socket应用里面,通过回调函数的处理,实现收到应答消息的时候能够调用对应的函数。

1、回调函数的设计

在上一篇的随笔里面,介绍了基于Json的Socket消息的实体类设计,其中包括了消息回调ID和是否在调用后移除回调两个属性,这个是用来为回调处理服务的,如下所示。

也就是在通用消息对象BaseMessage类里面添加下面两个属性。

但我们需要发送消息的时候,我们把回调的ID添加到本地集合里面,得到应答的时候,在从集合里面提出来,执行就可以了。

        /// <summary>
        ///发送通用格式的数据对象/// </summary>
        /// <param name="data">通用的消息对象</param>
        /// <returns></returns>
        public bool SendData(BaseMessage data, Delegate callBack = null)
{
AddCallback(callBack, data);
//添加回调集合 var toSendData =PackMessage(data);var result =SendData(toSendData);returnresult;
}
        /// <summary>
        ///记录回调的函数信息/// </summary>
        /// <param name="callBack"></param>
        /// <param name="msg"></param>
        private voidAddCallback(Delegate callBack, BaseMessage msg)
{
if (callBack != null)
{
Guid callbackID
=Guid.NewGuid();
ResponseCallbackObject responseCallback
= newResponseCallbackObject()
{
ID
=callbackID,
CallBack
=callBack
};

msg.CallbackID
=callbackID;
callBackList.Add(responseCallback);
}
}

在服务端,需要根据请求的消息构建应答内容,因此我们在应答请求的时候,需要把请求的回调ID给复制到应答的消息体里面,如下所示。

        /// <summary>
        ///封装数据进行发送(复制请求部分数据)/// </summary>
        /// <returns></returns>
        publicBaseMessage PackData(BaseMessage request)
{
BaseMessage info
= newBaseMessage()
{
MsgType
= this.MsgType,
Content
= this.SerializeObject(),
CallbackID
=request.CallbackID
};
if(!string.IsNullOrEmpty(request.ToUserId))
{
info.ToUserId
=request.FromUserId;
info.FromUserId
=request.ToUserId;
}
returninfo;
}

2、本地回调函数的处理及界面处理

调用方在收到服务器的应答消息的时候,会根据回调的ID ,从本地集合里面调出来并执行处理,实现了我们回调的操作。

                var md5 =MD5Util.GetMD5_32(message.Content);if (md5 ==message.MD5)
{
InvokeMessageCallback(message, message.DeleteCallbackAfterInvoke);
//触发回调 OnMessageReceived(message);//给子类重载 }
        /// <summary>
        ///执行回调函数/// </summary>
        /// <param name="msg">消息基础对象</param>
        /// <param name="deleteCallback">是否移除回调</param>
        private void InvokeMessageCallback(BaseMessage msg, booldeleteCallback)
{
var callBackObject = callBackList.SingleOrDefault(x => x.ID ==msg.CallbackID);if (callBackObject != null)
{
if(deleteCallback)
{
callBackList.Remove(callBackObject);
}
callBackObject.CallBack.DynamicInvoke(msg);
}
}

这样,我们在调用的时候,传入一个回调的Action,让收到消息后进行动态执行就可以了。例如在登陆的时候,我们如果需要在登陆成功后显示主窗体,那么可以执行下面的处理代码。

            var request = newAuthRequest(userNo, password);var message =request.PackData();
Singleton
<CommonManager>.Instance.Send(message, (msg) =>{var instance = Singleton<CommonManager>.Instance;try{var response = JsonTools.DeserializeObject<AuthResponse>(msg.Content);if (response.ValidateResult == 0)
{
instance.ShowLoadFormText(
"登录成功,加载基础数据。。。。");//放置初始化代码 Portal.gc.User = newUser(userNo);
instance.SetClientId(userNo);

instance.ShowMainForm();
instance.CloseLoadForm();
}
else{
instance.CloseLoadForm();
instance.ShowMessage(
"登录失败:" +response.Message);
instance.ShowLogin();
}
}
catch(Exception ex)
{
instance.ShowMessage(
"初始化异常:" +ex.Message);
instance.Exit();
}
});

或者我们来看看另外一个例子,这个例子是在用户登陆的时候,请求一次在线用户列表,如果用户在线,那么在界面上展示列表,具体操作代码如下所示,也是利用了回调函数的处理方式。

        /// <summary>
        ///发送获取在线用户列表的请求,并在收到应答数据后进行本地界面更新/// </summary>
        private voidRefreshUser()
{
CommonRequest request
= newCommonRequest(DataTypeKey.UserListRequest);var data =request.PackData();

Singleton
<CommonManager>.Instance.Send(data, (msg) =>{
UserListResponse response
= JsonTools.DeserializeObject<UserListResponse>(msg.Content);if (response != null)
{
this.InvokeUI(() =>{this.listView1.Items.Clear();foreach (CListItem item inresponse.UserList)
{
if (item.Value !=Portal.gc.User.UserNo)
{
this.listView1.Items.Add(item.Text, 0);
}
}
});
}
});
}

例如,客户端登陆几个用户后,用户可以获得在线用户列表,界面展示如下所示。

以上就是我们在Socket应用里面处理回调函数的实现过程,这样处理可以很好利用回调代码来封装处理的细节,对于理解相关的应答操作也是很直观的。

有一段时间没有接着微信的主题继续介绍里面的功能模块了,这段时间来,微信也做了不少的变化改动,针对这些特性我全面核对了一下相关的微信公众号和企业号的接口,对原有的微信API和系统管理做了全面的更新,本随笔以及后面的随笔就是基于这个工作上的总结,以期把微信涉及的功能模块,都使用C#实现的方式来介绍。本随笔主要介绍微信公众号的个性化菜单的实现,虽然目前微信公众号和企业号已经在功能上接近一致,不过在企业号上还没有个性化菜单的相关接口。

1、个性化菜单介绍

我们先了解一下个性化菜单的介绍,根据官方的资料,如下介绍。

为了帮助公众号实现灵活的业务运营,微信公众平台新增了个性化菜单接口,开发者可以通过该接口,让公众号的不同用户群体看到不一样的自定义菜单。该接口开放给已认证订阅号和已认证服务号。

开发者可以通过以下条件来设置用户看到的菜单:

1、用户分组(开发者的业务需求可以借助用户分组来完成)
2、性别
3、手机操作系统
4、地区(用户在微信客户端设置的地区)
5、语言(用户在微信客户端设置的语言)

个性化菜单接口说明:

1、个性化菜单要求用户的微信客户端版本在iPhone6.2.2,Android 6.2.4以上。
2、菜单的刷新策略是,在用户进入公众号会话页或公众号profile页时,如果发现上一次拉取菜单的请求在5分钟以前,就会拉取一下菜单,如果菜单有更新,就会刷新客户端的菜单。测试时可以尝试取消关注公众账号后再次关注,则可以看到创建后的效果。
3、普通公众号的个性化菜单的新增接口每日限制次数为2000次,删除接口也是2000次,测试个性化菜单匹配结果接口为20000次
4、出于安全考虑,一个公众号的所有个性化菜单,最多只能设置为跳转到3个域名下的链接
5、创建个性化菜单之前必须先创建默认菜单(默认菜单是指使用普通自定义菜单创建接口创建的菜单)。如果删除默认菜单,个性化菜单也会全部删除

个性化菜单匹配规则说明:

当公众号创建多个个性化菜单时,将按照发布顺序,由新到旧逐一匹配,直到用户信息与matchrule相符合。如果全部个性化菜单都没有匹配成功,则返回默认菜单。
例如公众号先后发布了默认菜单,个性化菜单1,个性化菜单2,个性化菜单3。那么当用户进入公众号页面时,将从个性化菜单3开始匹配,如果个性化菜单3匹配成功,则直接返回个性化菜单3,否则继续尝试匹配个性化菜单2,直到成功匹配到一个菜单。
根据上述匹配规则,为了避免菜单生效时间的混淆,决定不予提供个性化菜单编辑API,开发者需要更新菜单时,需将完整配置重新发布一轮。

除正常的默认菜单外,个性化菜单提供了下面几个处理操作。

1 创建个性化菜单

2 删除个性化菜单

3 测试个性化菜单匹配结果

4 查询个性化菜单

5 删除所有菜单

个性化自定义菜单功能,是指商家可以根据粉丝的分组、性别、手机操作系统,甚至地区,来分别展示公众号的菜单。

比如,一家健身会所的公众号,将自定义菜单根据粉丝性别进行了分开设置。男性粉丝进入公众号会展示男生们比较感兴趣菜单内容,跆拳道、搏击等;若是女性粉丝,则会展示瑜伽、舞蹈、防身术等女性粉丝感兴趣的菜单课程等。

2、个性化菜单的C#设计处理

微信的菜单是微信处理中很重要的一环,因为它是微信给用户入口体验重要的一环,也是很直观的界面呈现之一,菜单的展示就是需要我们绞尽脑汁来精简的界面元素了,由于菜单的重要性以及功能的丰富性,我在之前好几篇随笔专门介绍了菜单的各种处理。

在《
C#开发微信门户及应用(6)--微信门户菜单的管理操作
》、《
C#开发微信门户及应用(9)-微信门户菜单管理及提交到微信服务器
》、《
C#开发微信门户及应用(11)--微信菜单的多种表现方式介绍
》、《
C#开发微信门户及应用(20)-微信企业号的菜单管理
》分别对相关的菜单有过介绍,有需要可以参考了解一下。

菜单模块,在微信公众号和企业号,它们都可以共用一个模型,因此我们把这些内容都放在微信共用模块里面,包括它的实体类信息(如菜单模块),虽然有些地方,部分字段内容没有,但是我们可以通过实体类的JSON标识来进行处理,从而实现比较弹性化的实体类信息承载。

如下标识代码所示。

    /// <summary>
    ///菜单基本信息(公众号、企业号公用,其他部分一样)/// </summary>
    public classMenuJson : BaseJsonResult
{
/// <summary> ///按钮描述,既按钮名字,不超过16个字节,子菜单不超过40个字节/// </summary> public string name { get; set; }/// <summary> ///按钮类型(click或view)/// </summary> [JsonProperty(NullValueHandling =NullValueHandling.Ignore)]public string type { get; set; }/// <summary> ///按钮KEY值,用于消息接口(event类型)推送,不超过128字节/// </summary> [JsonProperty(NullValueHandling =NullValueHandling.Ignore)]public string key { get; set; }/// <summary> ///网页链接,用户点击按钮可打开链接,不超过256字节/// </summary> [JsonProperty(NullValueHandling =NullValueHandling.Ignore)]public string url { get; set; }/// <summary> ///media_id类型和view_limited类型必须,调用新增永久素材接口返回的合法media_id/// </summary> [JsonProperty(NullValueHandling =NullValueHandling.Ignore)]public string media_id { get; set; }/// <summary> ///子按钮数组,按钮个数应为2~5个/// </summary> [JsonProperty(NullValueHandling =NullValueHandling.Ignore)]public List<MenuJson> sub_button { get; set; }

上面这个实体类是菜单信息的基础信息,我们需要从这个基础上扩展出来几个实体类,方便在构建,获取菜单信息的时候进行信息转换,如下几个类图的设计所示。

通过这几个类的关系,我们就可以适应所有的默认菜单和个性化菜单,以及企业号的菜单信息转换处理了,通过JSON转换为对应的实体类,实现信息的强类型处理。

其中我们菜单处理的API实现类代码如下所示。

    /// <summary>
    ///菜单的相关操作/// </summary>
    public class MenuApi : IMenuApi

这样我们在IMenuApi的基础上增加几个个性化的接口。

        /// <summary>
        ///创建个性化菜单/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="menuJson">菜单对象</param>
        /// <returns></returns>
        string CreateConditionalMenu(stringaccessToken, MenuListJson menuJson);/// <summary>
        ///根据菜单ID删除个性化菜单/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="menuid">菜单ID</param>
        /// <returns></returns>
        CommonResult DeleteConditionalMenu(string accessToken, stringmenuid);/// <summary>
        ///测试个性化菜单匹配结果/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <returns></returns>
        MenuListJson TryMatchConditionalMenu(string accessToken, string user_id);

然后在MenuApi实现类里面实现相关的逻辑处理即可。

下面是创建个性化菜单的接口说明。

创建个性化菜单

http请求方式:POST(请使用https协议)

https://api.weixin.qq.com/cgi-bin/menu/addconditional?access_token=ACCESS_TOKEN

请求示例

{
 	"button":[
 	{	
    	"type":"click",
    	"name":"今日歌曲",
     	"key":"V1001_TODAY_MUSIC" 
	},
	{ 
		"name":"菜单",
		"sub_button":[
		{	
			"type":"view",
			"name":"搜索",
			"url":"http://www.soso.com/"
		},
		{
			"type":"view",
			"name":"视频",
			"url":"http://v.qq.com/"
		},
		{
			"type":"click",
			"name":"赞一下我们",
			"key":"V1001_GOOD"
		}]
 }],
"matchrule":{
  "group_id":"2",
  "sex":"1",
  "country":"中国",
  "province":"广东",
  "city":"广州",
  "client_platform_type":"2"
  "language":"zh_CN"
  }
}

其中上面的信息,转换为我们的实体类对象信息就是如下所示。

MenuListJsonmenuJson

它API返回结果如下所示。

{
	"menuid":"208379533"
}

为了承载这个信息,我们需要定义一个实体类来承载这个信息。

    /// <summary>
    ///返回MenuId的结果/// </summary>
    public classMenuIdResult
{
/// <summary> ///菜单的ID/// </summary> public string menuid { get; set; }
}

最后我们的API接口只需要获取里面的菜单ID值,字符串类型的就可以了,API具体实现代码如下所示。

        /// <summary>
        ///创建个性化菜单/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="menuJson">菜单对象</param>
        /// <returns></returns>
        public string CreateConditionalMenu(stringaccessToken, MenuListJson menuJson)
{
string result = "";var url = string.Format("https://api.weixin.qq.com/cgi-bin/menu/addconditional?access_token={0}", accessToken);//matchrule不能为空 ArgumentValidation.CheckForNullReference(menuJson.matchrule, "matchrule");string postData =menuJson.ToJson();
MenuIdResult list
= JsonHelper<MenuIdResult>.ConvertJson(url, postData);if (list != null)
{
result
=list.menuid;
}
returnresult;
}

而删除个性化菜单的接口也是类似的处理,如下所示。

        /// <summary>
        ///根据菜单ID删除个性化菜单/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="menuid">菜单ID</param>
        /// <returns></returns>
        public CommonResult DeleteConditionalMenu(string accessToken, stringmenuid)
{
var url = string.Format("https://api.weixin.qq.com/cgi-bin/menu/delconditional?access_token={0}", accessToken);var data = new{
menuid
=menuid
};
string postData =data.ToJson();returnHelper.GetExecuteResult(url, postData);
}

另外,对于个性化菜单来说,查询、删除所有的菜单操作接口和普通默认菜单是一样的,因此可以重用这两个接口,不过由于我们扩展了查询返回的信息类,所以如果是个性化菜单,那么它返回的JSON字符串里面,包含了conditionalmenu的属性,这个是一个集合 ,也就是说个性化菜单是多个规则的个性化菜单的集合。

菜单的JSON数据结构如下所示。

3、个性化菜单封装接口的使用

有了上面接口的封装,我们可以在创建个性化菜单的时候很方便了,构建个性化菜单的时候,我们需要把全部需要出现的菜单都需要放在一个conditionalmenu的单元里面,虽然这样的单元在集合里面出现多个。

如下面是构建一个个性化的菜单项目
,和普通的默认菜单不同,它需要指定一个matchrule的对象
,如下代码所示。

            MenuJson conditional = new MenuJson("相关链接", newMenuJson[] {new MenuJson("个性化菜单", ButtonType.click, "event_company")
});
MatchRule rule
= newMatchRule()
{
sex
= "1"};

MenuListJson menuJson
= newMenuListJson();
menuJson.button.AddRange(
newMenuJson[] { conditional });
menuJson.matchrule
= rule;

通过上面的代码了解,我们如果需要增加多个个性化菜单,那么就是通过这个方法,每次创建一个个性化菜单的方式进行构建,当然如果我们使用查询个性化菜单的结果可以看到,如果创建多个,那么
conditionalmenu集合里面是多个对象的。

调用代码每次添加一个个性化菜单的操作如下所示。

                string menuid =menuBLL.CreateConditionalMenu(token, menuJson);
Console.WriteLine(
"创建菜单:" + (!string.IsNullOrEmpty(menuid) ? "成功:" + menuid : "失败"));

我们创建多个规则后,可以通过TryMatch的方式来检查匹配的结果,也就是对应不同用户(openid)会出现不同的个性化菜单列表了。

                    var myResult =menuBLL.TryMatchConditionalMenu(token, openId);if (myResult != null)
{
Console.WriteLine(myResult.ToJson());
}
var userid_female = "oSiLnt2J4mYkhVG3aLTdMIF1hv-s";//女性的ID myResult =menuBLL.TryMatchConditionalMenu(token, userid_female);if (myResult != null)
{
Console.WriteLine(myResult.ToJson());
}

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

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

在很多场合下,我们可能需要利用微信公众号的优势,定期给指定用户群发送一些推广消息或者新闻内容,以便给关注客户一种经常更新公众号内容的感觉,同时也方便我们经常和用户进行互动。微信公众号的高级群发接口就是为了处理这个场景的,本文介绍在C#代码中如何封装消息的群发和预览等功能。

1、消息群发的功能和限制

对于公众号中的服务号和订阅号,群发的消息有一定的限制,具体规则如下所示。

1、对于认证订阅号,群发接口每天可成功调用1次,此次群发可选择发送给全部用户或某个分组;
2、对于认证服务号虽然开发者使用高级群发接口的每日调用限制为100次,但是用户每月只能接收4条,无论在公众平台网站上,还是使用接口群发,用户每月只能接收4条群发消息,多于4条的群发将对该用户发送失败;
3、具备微信支付权限的公众号,在使用群发接口上传、群发图文消息类型时,可使用<a>标签加入外链;
4、开发者可以使用预览接口校对消息样式和排版,通过预览接口可发送编辑好的消息给指定用户校验效果。

群发图文消息的过程如下:

1、首先,预先将图文消息中需要用到的图片,使用上传图文消息内图片接口,上传成功并获得图片URL
2、上传图文消息素材,需要用到图片时,请使用上一步获取的图片URL
3、使用对用户分组的群发,或对OpenID列表的群发,将图文消息群发出去
4、在上述过程中,如果需要,还可以预览图文消息、查询群发状态,或删除已群发的消息等

群发图片、文本等其他消息类型的过程如下:

1、如果是群发文本消息,则直接根据下面的接口说明进行群发即可
2、如果是群发图片、视频等消息,则需要预先通过素材管理接口准备好mediaID

2、消息的群发处理

虽然群发的消息类型有几种,如包括图文消息、文本消息、图片、视频、语音、卡劵等等,不过消息群发方式分为两类:根据群组发送消息和根据OpenID发送消息两种。

根据微信接口的定义,我们设计了对上面两种不同方式的发送接口,我们把不同类型的消息放到枚举MassMessageType 进行定义。

        /// <summary>
        ///根据分组进行群发消息(图文消息、文本消息、语音消息、视频消息、图片、卡劵等)/// </summary>
        /// <param name="accessToken">访问凭证</param>
        /// <param name="mediaIdOrContent">群发媒体文件时传入mediaId,群发文本消息时传入content,群发卡券时传入cardId</param>
        /// <param name="groupId">群发到的分组的group_id</param>
        /// <param name="isToAll">
        ///使用is_to_all为true且成功群发,会使得此次群发进入历史消息列表。///设置is_to_all为false时是可以多次群发的,但每个用户只会收到最多4条,且这些群发不会进入历史消息列表</param>
        /// <returns></returns>
        MassMessageResult SendByGroup(string accessToken, MassMessageType messageType, string mediaIdOrContent, string groupId, bool isToAll = false);/// <summary>
        ///根据OpenId进行群发消息(视频消息需要单独)/// </summary>
        /// <param name="accessToken">访问凭证</param>
        /// <param name="messageType">消息类型</param>
        /// <param name="mediaIdOrContent">用于群发的消息的media_id</param>
        /// <param name="openIdList">openId字符串数组</param>        
        /// <returns></returns>
        MassMessageResult SendByOpenId(string accessToken, MassMessageType messageType, string mediaIdOrContent, List<string> openIdList);

其中枚举MassMessageType定义代码如下所示。

   /// <summary>
    ///群发消息的类型/// </summary>
    public enumMassMessageType
{
/// <summary> ///图文消息/// </summary> mpnews,/// <summary> ///文本消息/// </summary> text,/// <summary> ///图片/// </summary> image,/// <summary> ///语音/// </summary> voice,/// <summary> ///音乐/// </summary> music,/// <summary> ///视频/// </summary> video,/// <summary> ///卡劵/// </summary> wxcard
}

然后我们根据上面的接口实现相关的处理函数,群发消息的类定义代码如下所示。

    /// <summary>
    ///消息群发.///在公众平台网站上,为订阅号提供了每天1条的群发权限,为服务号提供每月(自然月)4条的群发权限。///而对于某些具备开发能力的公众号运营者,可以通过高级群发接口,实现更灵活的群发能力。/// </summary>
    public class MassSendApi : IMassSendApi

对于图文消息的群发规则,微信接口定义如下。

接口调用请求说明

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

POST数据说明

POST数据示例如下:

图文消息(注意图文消息的media_id需要通过上述方法来得到):

{"filter":{"is_to_all":false,"group_id":2},"mpnews":{"media_id":"123dsdajkasd231jhksad"},"msgtype":"mpnews"}

其他类似文本消息、图片、视频、语音、卡劵等发送方式类似,都是提供一个不同的JSON字符串,然后提交到对应的连接地址就可以了,因此我们可以把它们进行统一的封装处理。

我们可以在一个条件语句里面对内容进行组装,例如对于图文消息的处理代码如下所示。

switch(messageType)
{
case MassMessageType.mpnews://图文消息 postData = new{
filter
= new{
is_to_all
= isToAll, //是否让此次群发进入历史消息列表 group_id = groupId //群发到的分组的group_id },
mpnews
= new{
media_id
= mediaIdOrContent //用于群发的消息的media_id },
msgtype
= "mpnews"}.ToJson();break;

对于文本消息的组装如下所示。

                case MassMessageType.text://文本消息
                    postData = new{
filter
= new{
is_to_all
= isToAll, //是否让此次群发进入历史消息列表 group_id = groupId //群发到的分组的group_id },
text
= new{
content
= mediaIdOrContent //用于群发的消息的内容 },
msgtype
= "text"}.ToJson();break;

最后我们通过代码进行提交JSON数据,并获取返回结果即可,如下代码所示。

            string url = string.Format("https://api.weixin.qq.com/cgi-bin/message/mass/sendall?access_token={0}", accessToken);

MassMessageResult result
= JsonHelper<MassMessageResult>.ConvertJson(url, postData);return result;

这样,整合各个消息类型的处理,我们就可以得到一个完整的消息群发操作了。

群发给openid的操作也是类似上面的处理方式,也是通过一个switch的条件语句,进行不同内容的构建,然后统一发送即可。

请注意:
在返回成功时,意味着群发任务提交成功,并不意味着此时群发已经结束
,所以,仍有可能在后续的发送过程中出现异常情况导致用户未收到消息,如消息有时会进行审核、服务器不稳定等。此外,
群发任务一般需要较长的时间才能全部发送完毕,请耐心等待

由于群发任务提交后,群发任务可能在一定时间后才完成,因此,群发接口调用时,仅会给出群发任务是否提交成功的提示,若群发任务提交成功,则在群发任务结束时,会向开发者在公众平台填写的开发者URL(callback URL)推送事件。

推送的XML结构如下(发送成功时):

<xml>
<ToUserName><![CDATA[gh_3e8adccde292]]></ToUserName>
<FromUserName><![CDATA[oR5Gjjl_eiZoUpGozMo7dbBJ362A]]></FromUserName>
<CreateTime>1394524295</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[MASSSENDJOBFINISH]]></Event>
<MsgID>1988</MsgID>
<Status><![CDATA[sendsuccess]]></Status>
<TotalCount>100</TotalCount>
<FilterCount>80</FilterCount>
<SentCount>75</SentCount>
<ErrorCount>5</ErrorCount>
</xml>

对应的字段说明如下所示。

参数 说明
ToUserName 公众号的微信号
FromUserName 公众号群发助手的微信号,为mphelper
CreateTime 创建时间的时间戳
MsgType 消息类型,此处为event
Event 事件信息,此处为MASSSENDJOBFINISH
MsgID 群发的消息ID
Status 群发的结构,为“send success”或“send fail”或“err(num)”。但send success时,也有可能因用户拒收公众号的消息、系统错误等原因造成少量用户接收失败。err(num)是审核失败的具体原因
TotalCount group_id下粉丝数;或者openid_list中的粉丝数
FilterCount 过滤(过滤是指特定地区、性别的过滤、用户设置拒收的过滤,用户接收已超4条的过滤)后,准备发送的粉丝数,原则上,FilterCount = SentCount + ErrorCount
SentCount 发送成功的粉丝数
ErrorCount 发送失败的粉丝数

因此我们需要通过处理消息群发的发送完成操作,定义一个实体类来承载这个消息。

    public classRequestMassSendJobFinish : BaseEvent
{
publicRequestMassSendJobFinish()
{
this.MsgType =RequestMsgType.Event.ToString().ToLower();this.Event =RequestEvent.MASSSENDJOBFINISH.ToString();
}
/// <summary> ///群发的消息ID/// </summary> public int MsgID { get; set; }/// <summary> ///返回状态。/// </summary> public string Status { get; set; }/// <summary> ///group_id下粉丝数;或者openid_list中的粉丝数/// </summary> public int TotalCount { get; set; }/// <summary> ///过滤(过滤是指,有些用户在微信设置不接收该公众号的消息)后,准备发送的粉丝数,原则上,FilterCount = SentCount + ErrorCount/// </summary> public int FilterCount { get; set; }/// <summary> ///发送成功的粉丝数/// </summary> public int SendCount { get; set; }/// <summary> ///发送失败的粉丝数/// </summary> public int ErrorCount { get; set; }
}

在我们需要记录或者更新处理这种群发消息的状态的时候,我们可以在整个微信的消息链里面对这样的请求事件进行处理,如下代码是处理这种群发消息的通知的。

                            caseRequestEvent.MASSSENDJOBFINISH:
{
//由于群发任务彻底完成需要较长时间,将会在群发任务即将完成的时候,就推送群发结果,此时的推送人数数据将会与实际情形存在一定误差 RequestMassSendJobFinish info = XmlConvertor.XmlToObject(postStr, typeof(RequestMassSendJobFinish)) asRequestMassSendJobFinish;if(info != null)
{
//在此记录群发完成的处理 }
LogTextHelper.Info(eventName
+ ((info == null) ? "info is null": info.ToJson()));
}
break;

3、待群发消息的预览

在很多时候,我们群发消息之前,我们希望通过自己的微信号来看看具体的群发消息效果,如果没有问题我们在统一群发,相当于一个真实的审核过程,这样对于我们发送高质量的消息是一个很好的习惯。

对于普通的消息预览,我们定义的接口如下所示。

        /// <summary>
        ///预览接口【订阅号与服务号认证后均可用】。///开发者可通过该接口发送消息给指定用户,在手机端查看消息的样式和排版。///为了满足第三方平台开发者的需求,在保留对openID预览能力的同时,增加了对指定微信号发送预览的能力,但该能力每日调用次数有限制(100次),请勿滥用。/// </summary>
        /// <param name="accessToken">访问凭证</param>
        /// <param name="messageType">消息类型</param>
        /// <param name="media_id">用于群发的消息的media_id</param>
        /// <param name="touserOpenId">接收消息用户对应该公众号的openid</param>
        /// <param name="towxname">可以针对微信号进行预览(而非openID),towxname和touser同时赋值时,以towxname优先</param>
        /// <returns></returns>
        MassMessageResult PreviewMessage(string accessToken, MassMessageType messageType, string media_id, string touserOpenId, string towxname = null);

具体的实现也就是针对不同的消息类型,构建一个不同的处理机制,把它们差异性的JSON构造出来,然后统一调用就可以了,具体代码如下所示。

        public MassMessageResult PreviewMessage(string accessToken, MassMessageType messageType, string media_id, string touserOpenId, string towxname = null)
{
string postData = "";switch(messageType)
{
case MassMessageType.mpnews://图文消息 postData = new{
touser
=touserOpenId,
towxname
=towxname,
mpnews
= new{
media_id
=media_id
},
msgtype
= "mpnews"}.ToJson();break;case MassMessageType.text://文本消息 postData = new{
touser
=touserOpenId,
towxname
=towxname,
text
= new{
content
=media_id
},
msgtype
= "text"}.ToJson();break;case MassMessageType.voice://语音 postData = new{
touser
=touserOpenId,
towxname
=towxname,
voice
= new{
media_id
=media_id
},
msgtype
= "voice"}.ToJson();break;case MassMessageType.image://图片 postData = new{
touser
=touserOpenId,
towxname
=towxname,
image
= new{
media_id
=media_id
},
msgtype
= "image"}.ToJson();break;case MassMessageType.video://视频 postData = new{
touser
=touserOpenId,
towxname
=towxname,
mpvideo
= new{
media_id
=media_id
},
msgtype
= "mpvideo"}.ToJson();break;case MassMessageType.wxcard: //卡劵 throw new WeixinException("发送卡券息请使用PreviewCardMessage方法。");break;
}
var url = string.Format("https://api.weixin.qq.com/cgi-bin/message/mass/preview?access_token={0}", accessToken);return JsonHelper<MassMessageResult>.ConvertJson(url, postData);
}

消息的预览在我们正式群发消息前的审核是比较有用的,我们可以通过接口进行一个消息的预览,可以在微信公众号上看到的效果与正式群发后的消息是一样的。

例如我们通过下面的代码进行一个简单的预览消息操作。

        /// <summary>
        ///群发消息的预览/// </summary>
        private void btnPreviewMass_Click(objectsender, EventArgs e)
{
//上传图片 btnUpload_Click(null, null);//上传图文消息 btnUploadNews_Click(null, null);//消息群发前的预览操作 List<string> list = new List<string>() { openId };
IMassSendApi api
= newMassSendApi();var mediaId = this.news_mediaId;
MassMessageResult result
=api.PreviewMessage(token, MassMessageType.mpnews, mediaId, openId);if (result != null)
{
Console.WriteLine(result.msg_id);
}
}

最后可以看到例子代码的预览效果如下所示。

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

C#开发微信门户及应用(29)--微信个性化菜单的实现

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

微信语义理解接口提供从用户自然语言输入到结构化解析的技术实现,使用先进的自然语言处理技术给开发者提供一站式的语义解析方案。该平台覆盖多个垂直领域的语义场景,部分领域还可以支持取得最终的展示结果。开发者无需掌握语义理解及相关技术,只需根据自己的产品特点,选择相应的服务即可搭建一套智能语义服务。结合语音识别接口,通过微信语音识别得到用户的语音信息之后,经过语义分析理解,得到用户需求,及时回复用户。本文介绍如何实现对微信语义接口的封装处理,以及一些常用场景的调用。

1)微信语义理解接口

这个东西也就是把我们日常的话语(称之为自然语言)解析为对应的信息结构体,方便我们提取里面的相关信息进行搜索查询,并精确回应给对应的请求者的一个桥梁,其主要的功能就是解析我们所说的内容。

微信开放平台语义理解接口调用(http请求)简单方便,用户无需掌握语义理解及相关技术,只需根据自己的产品特点,选择相应的服务即可搭建一套智能语义服务。我们来看看微信语义理解接口的定义内容。

http请求方式: POST(请使用https协议)

https://api.weixin.qq.com/semantic/semproxy/search?access_token=YOUR_ACCESS_TOKEN

POST数据格式:JSON,
POST数据例子:

{

"query":"查一下明天从北京到上海的南航机票",

"city":"北京",

"category": "flight,hotel",

"appid":"wxaaaaaaaaaaaaaaaa",

"uid":"123456"

} 

参数说明

参数 是否必须 参数类型 说明
access_token String 根据appid和appsecret获取到的token
query String 输入文本串
category String 需要使用的服务类型,多个用“,”隔开,不能为空
latitude 见接口协议文档 Float 纬度坐标,与经度同时传入;与城市二选一传入
longitude 见接口协议文档 Float 经度坐标,与纬度同时传入;与城市二选一传入
city 见接口协议文档 String 城市名称,与经纬度二选一传入
region 见接口协议文档 String 区域名称,在城市存在的情况下可省;与经纬度二选一传入
appid String 公众号唯一标识,用于区分公众号开发者
uid String 用户唯一id(非开发者id),用户区分公众号下的不同用户(建议填入用户openid),如果为空,则无法使用上下文理解功能。appid和uid同时存在的情况下,才可以使用上下文理解功能。

注:单类别意图比较明确,识别的覆盖率比较大,所以如果只要使用特定某个类别,建议将category只设置为该类别。

返回说明 正常情况下,微信会返回下述JSON数据包:

{ 

“errcode”:
0,

“query”:”查一下明天从北京到上海的南航机票”,

“type”:”flight”,

“semantic”:{

“details”:{

“start_loc”:{

“type”:”LOC_CITY”,

“city”:”北京市”,

“city_simple”:”北京”,

“loc_ori”:”北京”

},

“end_loc”: {

“type”:”LOC_CITY”,

“city”:”上海市”,

“city_simple”:”上海”,

“loc_ori”:”上海”

},

“start_date”: {

“type”:”DT_ORI”,

“date”:”
2014-03-05”,

“date_ori”:”明天”

},

“airline”:”中国南方航空公司”

},

“intent”:”SEARCH”

}

返回参数说明

参数 是否必须 参数类型 说明
errcode Int 表示请求后的状态
query String 用户的输入字符串
type String 服务的全局类型id,详见协议文档中垂直服务协议定义
semantic Object 语义理解后的结构化标识,各服务不同
result Array 部分类别的结果
answer String 部分类别的结果html5展示,目前不支持
text String 特殊回复说明

上面就是微信官方给出的代码案例,以及一个《
语义理解接口协议文档
》,里面介绍了各个场景的语义结构信息,虽然这个文档好像好久都没怎么更新,不过总体内容还是稳定的,我们可以通过这个文档进行相关的类库设计工作。

2、语义理解接口的C#实现

根据《
语义理解接口协议文档
》文档,我们可以定义各种所需的语义结构类库,这些是我们开展语义接口的基础类。

例如我们定义基础的时间协议类,如下所示。

    /// <summary>
    ///时间相关协议datetime/// </summary>
    public classSemantic_DateTime
{
/// <summary> ///单时间的描述协议类型:“DT_SINGLE”。DT_SINGLE又细分为两个类别:DT_ORI和DT_INFER。DT_ORI是字面时间,比如:“上午九点”;///DT_INFER是推理时间,比如:“提前5分钟”。 时间段的描述协议类型:“DT_INTERVAL”///重复时间的描述协议类型:“DT_REPEAT” DT_ REPEAT又细分为两个类别:DT_RORI和DT_RINFER。DT_RORI是字面时间,比如:“每天上午九点”;DT_RINFER是推理时间,比如:“工作日除外”/// </summary> public string type { get; set; }/// <summary> ///24小时制,格式:HH:MM:SS,默认为00:00:00/// </summary> public string time { get; set; }/// <summary> ///Time的原始字符串/// </summary> public string time_ori { get; set; }
}
/// <summary> ///单时间的描述协议datetime/// </summary> public classSemantic_SingleDateTime : Semantic_DateTime
{
/// <summary> ///格式:YYYY-MM-DD,默认是当天时间/// </summary> public string date { get; set; }/// <summary> ///格式:YYYY-MM-DD 农历/// </summary> public string date_lunar { get; set; }/// <summary> ///date的原始字符串/// </summary> public string date_ori { get; set; }
}

当然时间还有很多类型的定义,都基本上按照文档所列的字段进行处理,上面的代码只是定义了常用的单时间的描述协议类型:“DT_SINGLE”。

除了时间协议,还有数字,地点位置等相关协议,如数字协议如下所示。

    public classSemantic_Number
{
/// <summary> ///大类型:“NUMBER” NUMBER又细分为如下类别:NUM_PRICE、NUM_PADIUS、NUM_DISCOUNT、NUM_SEASON、NUM_EPI、NUM_CHAPTER。/// </summary> public string type { get; set; }/// <summary> ///开始/// </summary> public string begin { get; set; }/// <summary> ///结束/// </summary> public string end { get; set; }
}

地点位置协议如下所示

    /// <summary>
    ///地点相关协议/// </summary>
    public classSemantic_Location
{
/// <summary> ///大类型:“LOC” LOC又细分为如下类别:LOC_COUNTRY、LOC_PROVINCE、LOC_CITY、LOC_TOWN、LOC_POI、NORMAL_POI。/// </summary> public string type { get; set; }/// <summary> ///国家/// </summary> [JsonProperty(NullValueHandling =NullValueHandling.Ignore)]public string country { get; set; }/// <summary> ///省全称,例如:广东省/// </summary> [JsonProperty(NullValueHandling =NullValueHandling.Ignore)]public string province { get; set; }/// <summary> ///省简称,例如:广东|粤/// </summary> [JsonProperty(NullValueHandling =NullValueHandling.Ignore)]public string province_simple { get; set; }/// <summary> ///市全称,例如:北京市/// </summary> [JsonProperty(NullValueHandling =NullValueHandling.Ignore)]public string city { get; set; }/// <summary> ///市简称,例如:北京/// </summary> [JsonProperty(NullValueHandling =NullValueHandling.Ignore)]public string city_simple { get; set; }

..............

前面我们看到了,语音立即的POST数据格式是一个较为固定的格式内容,我们可以把它定义为一个类,方便数据处理。

POST数据格式:JSON,POST数据例子如下所示:

{

"query":"查一下明天从北京到上海的南航机票",

"city":"北京",

"category": "flight,hotel",

"appid":"wxaaaaaaaaaaaaaaaa",

"uid":"123456"

} 

那么我们可以定义它的类库如下所示。

    /// <summary>
    ///语义查询条件/// </summary>
    public classSemanticQueryJson
{
/// <summary> ///输入文本串///必填/// </summary> public string query { get; set; }/// <summary> ///需要使用的服务类别,多个用,隔开,不能为空///必填/// </summary> public string category { get; set; }/// <summary> ///城市名称,与经纬度二选一传入///见说明,选填/// </summary> [JsonProperty(NullValueHandling =NullValueHandling.Ignore)]public string city { get; set; }/// <summary> ///App id,开发者的唯一标识,用于区分开放者,如果为空,则没法使用上下文理解功能。///非必填/// </summary> public string appid { get; set; }/// <summary> ///用户唯一id(并非开发者id),用于区分该开发者下不同用户,如果为空,则没法使用上下文理解功能。appid和uid同时存在的情况下,才可以使用上下文理解功能。///非必填/// </summary> public string uid { get; set; }

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

接着我们分析语义理解的接口返回值,它们基本上都是很有规律的内容,如下所示。

这样我们也就可以定义一个通用的类库对象,用来存储不同的返回内容了,如下代码所示。

    /// <summary>
    ///微信语义结果/// </summary>
    public class SemanticResultJson<T>: ErrorJsonResult
{
/// <summary> ///用户的输入字符串/// </summary> public string query { get; set; }/// <summary> ///服务的全局类别id/// </summary> public string type { get; set; }/// <summary> ///语义理解后的结构化标识,各服务不同/// </summary> public T semantic { get; set; }
}

而其中的T semantic就是另外一个结构体里面的内容,这个结构体总体也是固定的内容,我们继续定义一个如下的类。

    /// <summary>
    ///详细信息里面的对象/// </summary>
    /// <typeparam name="T"></typeparam>
    public class SemanticDetail<T>{/// <summary>
        ///详细信息/// </summary>
        public T details { get; set; }/// <summary>
        ///查询类型/// </summary>
        public string intent { get; set; }
}

有了这些类库的支持,我们可以封装语义理解接口的返回值了,这样它的接口定义和封装处理代码如下所示。

    /// <summary>
    ///语意理解接口///微信开放平台语义理解接口调用(http请求)简单方便,用户无需掌握语义理解及相关技术,只需根据自己的产品特点,选择相应的服务即可搭建一套智能语义服务。/// </summary>
    public classSemanticApi : ISemanticApi
{
/// <summary> ///发送语义理解请求/// </summary> /// <param name="accessToken">调用接口凭证</param> /// <param name="data">查询条件</param> public SemanticResultJson<SemanticDetail<T>> SearchSemantic<T>(stringaccessToken, SemanticQueryJson data)
{
var url = string.Format("https://api.weixin.qq.com/semantic/semproxy/search?access_token={0}", accessToken);string postData =data.ToJson();return JsonHelper<SemanticResultJson<SemanticDetail<T>>>.ConvertJson(url, postData);
}

由于微信语义结果是针对不同的服务协议,我们需要根据这些不同的服务协议,来定义属于这些信息结构,如在文档里,我们可以看到有很多不同类型的服务协议。

根据文档的详细字段说明,我们可以定义不同服务的对应类库。

例如对于旅游服务的语义理解,它们的协议类如下所示。

    /// <summary>
    ///旅游服务(travel)/// </summary>
    public classSemantic_Details_Travel
{
/// <summary> ///旅游目的地/// </summary> public Semantic_Location location { get; set; }/// <summary> ///景点名称/// </summary> public string spot { get; set; }/// <summary> ///旅游日期/// </summary> public Semantic_SingleDateTime datetime { get; set; }/// <summary> ///旅游类型词/// </summary> public string tag { get; set; }/// <summary> ///0默认,1自由行,2跟团游/// </summary> public int category { get; set; }
}

那么调用的旅游语义的案例代码如下所示

            var api = newSemanticApi();var json = newSemanticQueryJson
{
appid
=appId,
uid
=openId,
category
=SemanticCategory.travel.ToString(),
query
= "故宫门票多少钱",
city
= "北京市"};var travel = api.SearchSemantic<Semantic_Details_Travel>(token, json);
Console.WriteLine(travel.ToJson());

如果我们测试,上面的代码跑起来会返回一个旅游的协议对象,包括了相关的数据信息。

{"errcode" : 0,"query" : "故宫门票多少钱","semantic": {"details": {"answer" : "","context_info": {},"hit_str" : "故宫 门票 多少 钱","spot" : "故宫"},"intent" : "PRICE"},"type" : "travel"}

我们再来看一个例子,例如对于航班服务,我们定义它的语义理解协议如下所示。

    /// <summary>
    ///航班服务(flight)/// </summary>
    public classSemantic_Details_Flight
{
/// <summary> ///航班号/// </summary> public string flight_no { get; set; }/// <summary> ///出发地/// </summary> public Semantic_Location start_loc { get; set; }/// <summary> ///目的地/// </summary> public Semantic_Location end_loc { get; set; }/// <summary> ///出发日期/// </summary> public Semantic_SingleDateTime start_date { get; set; }/// <summary> ///返回日期/// </summary> public Semantic_SingleDateTime end_date { get; set; }/// <summary> ///航空公司/// </summary> public string airline { get; set; }/// <summary> ///座位级别(默认无限制):ECONOMY(经济舱)BIZ(商务舱)FIRST(头等舱)/// </summary> public string seat { get; set; }/// <summary> ///排序类型:0排序无要求(默认),1价格升序,2价格降序,3时间升序,4时间降序/// </summary> public int sort { get; set; }
}

那么调用获取语义理解内容的代码如下所示。

            json = newSemanticQueryJson
{
appid
=appId,
uid
=openId,
category
=SemanticCategory.flight.ToString(),
query
= "查一下明天从广州到上海的南航机票",
city
= "广州"};var flight = api.SearchSemantic<Semantic_Details_Flight>(token, json);
Console.WriteLine(flight.ToJson());

我们可以获取到的JSON数据如下所示

{"errcode" : 0,"query" : "查一下明天从广州到上海的南航机票","semantic": {"details": {"airline" : "中国南方航空公司","answer" : "已帮您预定2016-04-13,从广州市出发,前往上海市的航班。","context_info": {"isFinished" : "1","null_times" : "0"},"end_loc": {"city" : "上海市","city_simple" : "上海|沪|申","loc_ori" : "上海","modify_times" : "0","slot_content_type" : "2","type" : "LOC_CITY"},"hit_str" : "查 一下 明天 从 广州 到 上海 南航 机票","sort" : "1","start_date": {"date" : "2016-04-13","date_lunar" : "2016-03-07","date_ori" : "明天","modify_times" : "0","slot_content_type" : "2","type" : "DT_ORI","week" : "3"},"start_loc": {"city" : "广州市","city_simple" : "广州","loc_ori" : "广州","modify_times" : "0","province" : "广东省","province_simple" : "广东|粤","slot_content_type" : "2","type" : "LOC_CITY"}
},
"intent" : "SEARCH"},"type" : "flight"}

这样就是我们把我们常规的语义,分析成了机器可以识别的准确的数据结构了,我们可以根据不同的语义场合对它进行分析,然后给用户进行不同的响应就可以了,结合微信语音识别为文本内容,我们可以把它做得很强大,有的类似机器智能的味道了。

微信语义理解是一个好东西,不过在微信官网上没有看到进一步的案例,如果能够有一些与实际结合的例子,估计更能帮助我们理解和应用了。

在微信的应用上,微信支付是一个比较有用的部分,但也是比较复杂的技术要点,在微商大行其道的年代,自己的商店没有增加微信支付好像也说不过去,微信支付旨在为广大微信用户及商户提供更优质的支付服务,微信的支付和安全系统由腾讯财付通提供支持。本文主要介绍如何在微信公众号上实现微信支付的接入、微信支付API的封装,以及API的调用,实现我们一些常见的业务调用。

1、开通微信支付并配置

微信支付是需要微信公众号的认证基础,也就是只对认证的公众号开放,微信认证需要签署相关的资料,并且进行对账认证,一般会有电话联系确认相关的信息的。

在微信支付API开始使用前,我们一般需要在后台进行一定的配置,如我们需要配置公众号支付的授权目录,测试白名单等信息,以及扫码支持的回调处理地址(这个实现在后面再讲),如下所示。

在使用API之前,我们要知道微信一些关键的操作,如退款、撤销订单等操作是需要证书的,而且常规的支付操作,我们也需要商户号、商户支付秘钥等信息,这些证书和秘钥信息,是我们从微信支付的商户平台上获取的,我们微信支付开通并审核通过后,我们就可以登录商户平台进行相关的操作了。

首先我们需要在开发的电脑上安装证书。

然后需要设置API的秘钥

最后在【API安全】项目上下载证书供我们开发环境使用。

2、微信支付API的介绍

微信支付配置相关的参数,并获得证书、API秘钥、商户号等信息后,我们可以开始了解微信支付的API的具体使用了,我们需要先把API封装为C#的类库进行使用,这样才能在各种应用里面方便调用。

微信支付分为有多种方式,如扫码支付、公众号支付、JSAPI支付、APP支付等方面,不过核心的API都差不多,基本上都覆盖了下面截图的几个API,只是有部分的接口差异。

我们可以从其中扫码支付开始了解,这个是对二维码进行扫码支付的场景,分为了模式一和模式二两种方式。

扫码支付可分为两种模式,商户根据支付场景选择相应模式。

【模式一】:商户后台系统根据微信支付规则链接生成二维码,链接中带固定参数productid(可定义为产品标识或订单号)。用户扫码后,微信支付系统将productid和用户唯一标识(openid)回调商户后台系统(需要设置支付回调URL),商户后台系统根据productid生成支付交易,最后微信支付系统发起用户支付流程。

【模式二】:商户后台系统调用微信支付【统一下单API】生成预付交易,将接口返回的链接生成二维码,用户扫码后输入密码完成支付交易。注意:该模式的预付单有效期为
2
小时

,过期后无法支付。

根据扫码支付的API说明,我们可以分别对这些接口(如统一下单、查询订单、关闭订单、申请退款、查询退款、下载对账单等接口进行逐一封装,以方便我们的开发使用。

模式一和模式二,都需要使用到了统一下单的接口,然后生成相应的二维码给客户扫码支付使用。

那么我们先来看看统一下单的接口说明,以了解它的具体使用。

1)应用场景

除被扫支付场景以外,商户系统先调用该接口在微信支付服务后台生成预支付交易单,返回正确的预支付交易回话标识后再按扫码、JSAPI、APP等不同场景生成交易串调起支付。

2)接口链接

URL地址:https://api.mch.weixin.qq.com/pay/unifiedorder

3)是否需要证书

不需要

4)请求参数

请求参数看似很多,大概分为两部分,一部分是系统必须的固定参数,一部分是业务所需的参数。

系统必须的固定参数如下所示。

一部分是业务参数,业务参数如下所示,主要是记录订单的相关产品ID、说明、费用等等

微信支付接口的调用和公众号其他接口调用不太一样,这里全部是采用XML进行交换的,感觉没有JSON那么方便灵活,如下所示是统一下单的接口提交数据。

然后返回的数据也是XML的,如下面例子代码所示,而且其中的字段内容还不太确定,因此按官网的建议,使用字典集合来存储返回的数据对象。

3、微信支付APIC#封装和调用

根据上面的描述,我们大概了解了微信支付API 的大概说明,根据这些信息,我们可以对它进行C#代码的封装了,对于代码的封装,我们关键点在其中第一个,如果顺利封装好第一个接口,那么后面的根据通用的方式,就很容易继续处理这些接口了。

例如,我们可以定义好微信支付的API接口定义,如下所示。

    /// <summary>
    ///微信支付接口/// </summary>
    public interfaceITenPayApi
{
/// <summary> ///生成扫描支付模式一URL/// </summary> /// <param name="productId">商品ID</param> /// <returns></returns> string GetPrePayUrl(stringproductId);/// <summary> ///生成直接支付url,支付url有效期为2小时,模式二/// </summary> /// <param name="info">商品订单数据</param> /// <returns></returns> stringGetPayUrl(WxPayOrderData info);/// <summary> ///统一下单。(不需要证书,默认不需要)///除被扫支付场景以外,商户系统先调用该接口在微信支付服务后台生成预支付交易单,///返回正确的预支付交易回话标识后再按扫码、JSAPI、APP等不同场景生成交易串调起支付。/// </summary> /// <param name="info">商品订单数据</param> WxPayData UnifiedOrder(WxPayOrderData info);

.............

其中的接口方法的输入参数我们定义一个实体类
WxPayOrderData
来存储一些业务参数,这些参数根据第二点的接口说明进行定义,代码如下所示

    /// <summary>
    ///统一下单的商品订单信息/// </summary>
    public classWxPayOrderData
{
/// <summary> ///商品ID, trade_type=NATIVE,此参数必传/// </summary> public string product_id { get; set; }/// <summary> ///商品或支付单简要描述/// </summary> public string body { get; set; }/// <summary> ///订单总金额,单位为分/// </summary> public int total_fee { get; set; }/// <summary> ///商品标记,代金券或立减优惠功能的参数,说明详见代金券或立减优惠/// </summary> public string goods_tag { get; set; }/// <summary> ///交易类型,默认为:NATIVE。///JSAPI--公众号支付、NATIVE--原生扫码支付、APP--app支付/// </summary> public string trade_type { get; set; }/// <summary> ///商品名称明细列表/// </summary> public string detail { get; set; }/// <summary> ///附加数据///在查询API和支付通知中原样返回,该字段主要用于商户携带订单的自定义数据/// </summary> public string attach { get; set; }/// <summary> ///用户标识///trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识。/// </summary> public string openid { get; set; }publicWxPayOrderData()
{
this.trade_type = "NATIVE";
}
}

然后我们定义一个接口返回的类WxPayData,它用来存储返回的对象信息的,这个类在官网例子里面有说明,其里面内置一个排序过的字典对象进行存储数据,部分代码如下所示,我对它进行了相关的修改,以方便在构造函数里面初始化一些必备的参数(固定参数)。

    public classWxPayData
{
//采用排序的Dictionary的好处是方便对数据包进行签名,不用再签名之前再做一次排序 private SortedDictionary<string, object> m_values = new SortedDictionary<string, object>();/// <summary> ///默认构造函数///如果initDefault为true,则自动填入字段(appid,mch_id,time_stamp,nonce_str,out_trade_no,)/// </summary> public WxPayData(bool initDefault = false)
{
if(initDefault)
{
Init();
}
}
/// <summary> ///对象初始化后,自动填入字段(appid,mch_id,time_stamp,nonce_str,out_trade_no,)/// </summary> public voidInit()
{
//初始化几个参数 this.SetValue("appid", WxPayConfig.APPID);//公众帐号id this.SetValue("mch_id", WxPayConfig.MCHID);//商户号 this.SetValue("nonce_str", GenerateNonceStr());//随机字符串 this.SetValue("out_trade_no", GenerateOutTradeNo(WxPayConfig.MCHID));//随机字符串 }

然后我们根据上面的数据定义,可以实现统一下单的函数内容,主要是把输入参数转换为我们需要的字典参数集合,如下代码所示。

        /// <summary>
        ///统一下单。(不需要证书,默认不需要)///除被扫支付场景以外,商户系统先调用该接口在微信支付服务后台生成预支付交易单,///返回正确的预支付交易回话标识后再按扫码、JSAPI、APP等不同场景生成交易串调起支付。/// </summary>
        /// <param name="info">商品订单数据</param>
        publicWxPayData UnifiedOrder(WxPayOrderData info)
{
WxPayData data
= new WxPayData(true);
data.SetValue(
"product_id", info.product_id);//商品ID data.SetValue("openid", info.openid);//商品ID//其他信息 data.SetValue("body", info.body);//商品描述 data.SetValue("attach", info.attach);//附加数据 data.SetValue("total_fee", info.total_fee);//总金额 data.SetValue("goods_tag", info.goods_tag);//商品标记 data.SetValue("trade_type", info.trade_type);//交易类型//默认构建 data.SetValue("time_start", DateTime.Now.ToString("yyyyMMddHHmmss"));//交易起始时间 data.SetValue("time_expire", DateTime.Now.AddMinutes(10).ToString("yyyyMMddHHmmss"));//交易结束时间 ..............

最后的数据交换逻辑,我们通过对URL进行POST提交XML数据给它获取返回结果就可以了,如下所示。

            string url = "https://api.mch.weixin.qq.com/pay/unifiedorder";return GetPostResult(data, url);

其中上面的函数的代码逻辑如下所示,主要是把返回的结果再还原为XML对象类WxPayData。

        /// <summary>
        ///通用的获取结果函数/// </summary>
        private WxPayData GetPostResult(WxPayData data, stringurl)
{
string xml =data.ToXml();string response = helper.GetHtml(url, xml, true);

WxPayData result
= newWxPayData();
result.FromXml(response);
returnresult;
}

对于扫码操作的模式二,直接生成一种二维码,不需要后台进行回调的,那么它的实现逻辑只需要对上面代码进行封装就可以了,如先构建二维码的函数代码如下所示。

        /// <summary>
        ///生成直接支付url,支付url有效期为2小时,模式二/// </summary>
        /// <param name="info">商品订单数据</param>
        /// <returns></returns>
        public stringGetPayUrl(WxPayOrderData info)
{
WxPayData result
= UnifiedOrder(info);//调用统一下单接口 return result.GetString("code_url");//获得统一下单接口返回的二维码链接 }

如在Winform界面里面,调用生成二维码的代码如下所示,主要逻辑就是构建好二维码,然后显示在界面上。

        private void btnGetPayUrl_Click(objectsender, EventArgs e)
{
//测试扫码模式二的生成二维码方式 WxPayOrderData data = newWxPayOrderData()
{
product_id
= "123456789",
body
= "测试支付-模式二",
attach
= "爱奇迪技术支持",
detail
= "测试扫码支付-模式二",
total_fee
= 1,
goods_tag
= "test1"};var url =api.GetPayUrl(data);var image =api.GenerateQRImage(url);this.imgGetPayUrl.Image =image;this.imgGetPayUrl.SizeMode =PictureBoxSizeMode.StretchImage;
}

另外对于模式一,它在前端传入一个简单的产品ID,生成二维码,当用户扫码的时候,微信后台会调用商户平台(我们服务器)的回调处理方法,这个回调方法会调用统一下单的API进行生成支付交易,过程有点复杂,我们来看看,我们的实现代码如下所示。

        /// <summary>
        ///生成扫描支付模式一URL/// </summary>
        /// <param name="productId">商品ID</param>
        /// <returns></returns>
        public string GetPrePayUrl(stringproductId)
{
WxPayData data
= new WxPayData(true);
data.SetValue(
"product_id", productId);//商品ID data.SetValue("time_stamp", data.GenerateTimeStamp());//随机字符串 data.SetValue("sign", data.MakeSign());//签名 string str = data.ToUrlParams();//转换为URL串 string url = "weixin://wxpay/bizpayurl?" +str;returnurl;
}

它的调用代码生成二维码操作如下所示。

        private void btnGetPrePayUrl_Click(objectsender, EventArgs e)
{
var productId = "12345678";var url =api.GetPrePayUrl(productId);var image =api.GenerateQRImage(url);this.imgGetPrePayUrl.Image =image;this.imgGetPayUrl.SizeMode =PictureBoxSizeMode.StretchImage;
}

我们在第一小节里面介绍了,需要在微信后台配置扫码的回调函数,如下所示。

这样我们还需要添加一个页面aspx、或者一般处理程序ashx的方式来实现扫码的回调过程。具体的逻辑也就是在这个页面里面获取到提交过来的参数,然后调用统一下单处理后,进行数据返回即可,代码逻辑如下所示。

4、在页面上进行扫码处理

前面的例子,我介绍了Winfrom的扫码例子,很多时候,我们的应用可能是基于Web的,那么它的实现是如何的呢,下面我继续介绍一下。

首先我们在自己的业务Web后台系统里面,添加两个页面,主要是用来生成二维码在页面上进行展示的,如下所示。

最终我们在NativePayPage.aspx页面上展示我们的二维码,方便用户进行扫码支付处理,页面的代码很简单,我们只需要在前端页面放置两个图片控件,图片内容通过MakeQRCode.aspx页面进行生成就可以了。

<!DOCTYPE html>
<htmlxmlns="http://www.w3.org/1999/xhtml">
<headrunat="server">
    <metahttp-equiv="content-type"content="text/html;image/gif;charset=utf-8"/>
    <metaname="viewport"content="width=device-width, initial-scale=1" /> 
    <title>微信支付样例-扫码支付</title>
</head>
<body>
    <divstyle="margin-left: 10px;color:#00CD00;font-size:30px;font-weight: bolder;">扫码支付模式一</div><br/>
    <asp:ImageID="Image1"runat="server"style="width:200px;height:200px;"/>
    <br/><br/><br/>
    <divstyle="margin-left: 10px;color:#00CD00;font-size:30px;font-weight: bolder;">扫码支付模式二</div><br/>
    <asp:ImageID="Image2"runat="server"style="width:200px;height:200px;"/>
    
</body>
</html>

页面后台的代码就是绑定二维码的过程,代码如下所示,和Winform的代码类似操作。

        protected void Page_Load(objectsender, EventArgs e)
{
TenPayApi api
= newTenPayApi();var productId = "123456789";//生成扫码支付模式一url string url1 =api.GetPrePayUrl(productId);//生成扫码支付模式二url WxPayOrderData info = newWxPayOrderData()
{
product_id
= "123456789",
body
= "测试支付-模式二",
attach
= "爱奇迪技术支持",
detail
= "测试扫码支付-模式二",
total_fee
= 1,
goods_tag
= "test1"};string url2 =api.GetPayUrl(info);//将url生成二维码图片 Image1.ImageUrl = "MakeQRCode.aspx?data=" +HttpUtility.UrlEncode(url1);
Image2.ImageUrl
= "MakeQRCode.aspx?data=" +HttpUtility.UrlEncode(url2);
}

实现后的页面效果如下所示。

实现并预览效果,确定是我们所需的页面后,我们可以发布在公众号的菜单连接上进行测试使用了。

打开微信公众号-广州爱奇迪,我们可以看到对应的菜单发生改变,并且看到进入微信支付的菜单可以进行支付了。



以上就是微信支付的扫码过程的一个实现,微信支付还包括很多其他API接口,后面有机会可以继续进行介绍。微信支付的接口实现虽然相对其他微信接口比较复杂一些,但是我们一旦完成几个案例,后面的就相对比较容易的了,因为它的调用方式基本上比较一致,很类似。

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

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