【引言】
利用asp.net搭建微信公众平台的案例并不多,微信官方给的案例是用PHP的,网上能找到的代码很多也是存在着这样那样的问题或者缺少部分方法,无法使用,下面是我依照官方文档写的基于.net 搭建微信公众平台源代码。由于经验不足,内可能存在不严谨之处,欢迎交流。
【分析】
实现的功能较为简单,主要分为验证与消息接收回复两部分,首先是验证:
这已经是验证好后的截图了,需要输入的是URL和你自己设定的Token码,URL为你上传服务器的地址例如:http://XXXXX.com/weixin/weixin.aspx,这里着重要强调的一个问题是加不加WWW的差异,无论加不加WWW,在验证时都是没有影响的,但在接收消息时,加和不加的差异是我最初无法接收到用户消息的直接原因,原因可能和POST带XML的请求机制有关。落实到具体项目中加还是不加,和你配置的DNS等有关,要是不想深入研究,当接收不到用户消息,而公众平台的调试工具里又是正常的,那你可以交替试试。Token码可以随机指定,但必须和代码中的Token指定相同,如果系统安全性要求比较高,建议增加token的复杂程度。
在下面代码第一行中指定token,必须与上面一直
1 const string Token = "nidaye1234";//你的token 2 #region 以下代码只用于第一次验证 验证完后请注释 3 protected void Page_Load(object sender, EventArgs e) 4 { 5 string postStr = ""; 6 if (Request.HttpMethod.ToLower() == "post") 7 { 8 System.IO.Stream s = System.Web.HttpContext.Current.Request.InputStream; 9 byte[] b = new byte[s.Length]; 10 s.Read(b, 0, (int)s.Length); 11 postStr = System.Text.Encoding.UTF8.GetString(b); 12 if (!string.IsNullOrEmpty(postStr)) 13 {16 Response.End(); 17 } 18 //WriteLog("postStr:" + postStr); 19 } 20 else 21 { 22 Valid(); 23 } 24 } 25 #endregion
上面的代码只是pageLoad,里面还要调用到的验证方法这里先不写,在最后会给出全部的源代码。
验证成功后可以调用api实现消息收发,下面是微信官方给的文档
接收消息
发送消息
以及关注事件
我的做法是首先创建一个接收消息实体以及实体填充方法,如下,其中根据MsgType的不同,选择填充合适的Content或者EventName。当然我这里没有使用微信为开发者提供的高级功能(例如语音定位之类的),如有用到可以增加这个类的属性,并对应修改填充器即可。
1 private class ExmlMsg 2 { 3 /// <summary> 4 /// 本公众账号 5 /// </summary> 6 public string ToUserName { get; set; } 7 /// <summary> 8 /// 用户账号 9 /// </summary> 10 public string FromUserName { get; set; } 11 /// <summary> 12 /// 发送时间戳 13 /// </summary> 14 public string CreateTime { get; set; } 15 /// <summary> 16 /// 发送的文本内容 17 /// </summary> 18 public string Content { get; set; } 19 /// <summary> 20 /// 消息的类型 21 /// </summary> 22 public string MsgType { get; set; } 23 /// <summary> 24 /// 事件名称 25 /// </summary> 26 public string EventName { get; set; } 27 28 } 29 30 private ExmlMsg GetExmlMsg(XmlElement root) 31 { 32 ExmlMsg xmlMsg = new ExmlMsg() { 33 FromUserName = root.SelectSingleNode("FromUserName").InnerText, 34 ToUserName = root.SelectSingleNode("ToUserName").InnerText, 35 CreateTime = root.SelectSingleNode("CreateTime").InnerText, 36 MsgType = root.SelectSingleNode("MsgType").InnerText, 37 }; 38 if (xmlMsg.MsgType.Trim().ToLower() == "text") 39 { 40 xmlMsg.Content = root.SelectSingleNode("Content").InnerText; 41 } 42 else if (xmlMsg.MsgType.Trim().ToLower() == "event") 43 { 44 xmlMsg.EventName = root.SelectSingleNode("Event").InnerText; 45 } 46 return xmlMsg; 47 }
普通使用时(非验证时)的pageload,这个方法主要通过调用PostInput()(详间最后的源代码)还获取post过来的数据,并将它们传入消息适配器中。
1 /// <summary> 2 /// 以下是正常使用时的pageload 请在验证时将其注释 并保证在正常使用时可用 3 /// </summary> 4 /// <param name="sender"></param> 5 /// <param name="e"></param> 6 protected void Page_Load(object sender, EventArgs e) 7 { 8 9 if (Request.HttpMethod == "POST") 10 { 11 string weixin = ""; 12 weixin = PostInput();//获取xml数据 13 if (!string.IsNullOrEmpty(weixin)) 14 { 15 ResponseMsg(weixin);////调用消息适配器 16 } 17 } 18 }
以下消息适配器,通过MsgType来区分消息的类型,并调用对应的方法,这里偷了一个懒,就是用户首次关注时推送消息的方法没有抽象出去,因为我暂时也没有别的enven可调用,如果结构复杂时,可以自行抽象。如果需要改变欢迎词的内容改变msg的值即可。日后扩展新功能时,可以根据case的的MsgType新写对应的方法,我这里主要用到的是textCase(),需要传入用户发送过来的消息实体,因为你的业务逻辑中可能需要用到发件者的各种信息。
1 private void ResponseMsg(string weixin)// 服务器响应微信请求 2 { 3 XmlDocument doc = new XmlDocument(); 4 doc.LoadXml(weixin);//读取xml字符串 5 XmlElement root = doc.DocumentElement; 6 ExmlMsg xmlMsg = GetExmlMsg(root); 7 //XmlNode MsgType = root.SelectSingleNode("MsgType"); 8 //string messageType = MsgType.InnerText; 9 string messageType = xmlMsg.MsgType;//获取收到的消息类型。文本(text),图片(image),语音等。 10 11 12 try 13 { 14 15 switch (messageType) 16 { 17 //当消息为文本时 18 case "text": 19 textCase(xmlMsg); 20 break; 21 case "event": 22 if (!string.IsNullOrEmpty(xmlMsg.EventName) && xmlMsg.EventName.Trim() == "subscribe") 23 { 24 //刚关注时的时间,用于欢迎词 25 int nowtime = ConvertDateTimeInt(DateTime.Now); 26 string msg = "你要关注我,我有什么办法。随便发点什么试试吧~~~"; 27 string resxml = "<xml><ToUserName><![CDATA[" + xmlMsg.FromUserName + "]]></ToUserName><FromUserName><![CDATA[" + xmlMsg.ToUserName + "]]></FromUserName><CreateTime>" + nowtime + "</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[" + msg + "]]></Content><FuncFlag>0</FuncFlag></xml>"; 28 Response.Write(resxml); 29 } 30 break; 31 case "image": 32 break; 33 case "voice": 34 break; 35 case "vedio": 36 break; 37 case "location": 38 break; 39 case "link": 40 break; 41 default: 42 break; 43 } 44 Response.End(); 45 } 46 catch (Exception) 47 { 48 49 } 50 }
获取文本回复信息方法,主要目的是按照官方文档的要求,拼接出所要返回给微信服务器的xml格式。它的msg内容来自于getText方法,同样需要传入用户消息实体,我下面给出了我用来测试的方法的内容。
1 private void textCase(ExmlMsg xmlMsg) 2 { 3 int nowtime = ConvertDateTimeInt(DateTime.Now); 4 string msg = ""; 5 msg = getText(xmlMsg); 6 string resxml = "<xml><ToUserName><![CDATA[" + xmlMsg.FromUserName + "]]></ToUserName><FromUserName><![CDATA[" + xmlMsg.ToUserName + "]]></FromUserName><CreateTime>" + nowtime + "</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[" + msg + "]]></Content><FuncFlag>0</FuncFlag></xml>"; 7 Response.Write(resxml); 8 9 }
1 private string getText(ExmlMsg xmlMsg) 2 { 3 string con = xmlMsg.Content.Trim(); 4 5 System.Text.StringBuilder retsb = new StringBuilder(200); 6 retsb.Append("这是测试返回"); 7 retsb.Append("接收到的消息:"+ xmlMsg.Content); 8 retsb.Append("用户的OPEANID:"+ xmlMsg.FromUserName); 9 10 return retsb.ToString(); 11 }
如上代码,我得到的测试结果
好了,可以收工了,这里还会用到的是时间转换的方法,因为文档规定的时间戳为int类型。于是网上找了转换方法。详见下面的完整代码。
【完整代码】
太长了,我折起来了,需要打开,我开始用的是MVC,但后来想想没必要,就用了普通的aspx,其实一般处理程序(ashx)也行,而且性能会更好点。我可以保证以下代码完整,不会缺方法,被坑怕了,呵呵。
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Data;
using System.IO;
using System.Net;
using System.Text;
using System.Xml;
using System.Web.Security;
using System.Text.RegularExpressions;
namespace WhoLove.weixin
{
public partial class weixin : System.Web.UI.Page
{
const string Token = "nidaye1234";//你的token
#region 以下代码只用于第一次验证 验证完后请注释
//protected void Page_Load(object sender, EventArgs e)
//{
// string postStr = "";
// if (Request.HttpMethod.ToLower() == "post")
// {
// System.IO.Stream s = System.Web.HttpContext.Current.Request.InputStream;
// byte[] b = new byte[s.Length];
// s.Read(b, 0, (int)s.Length);
// postStr = System.Text.Encoding.UTF8.GetString(b);
// if (!string.IsNullOrEmpty(postStr))
// {
// Response.End();
// }
// //WriteLog("postStr:" + postStr);
// }
// else
// {
// Valid();
// }
//}
#endregion
#region 以下是正常使用时的pageload 请在验证时将其注释 并保证在正常使用时可用
/// <summary>
/// 以下是正常使用时的pageload 请在验证时将其注释 并保证在正常使用时可用
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected void Page_Load(object sender, EventArgs e)
{
if (Request.HttpMethod == "POST")
{
string weixin = "";
weixin = PostInput();//获取xml数据
if (!string.IsNullOrEmpty(weixin))
{
ResponseMsg(weixin);//调用消息适配器
}
}
}
#endregion
#region 获取post请求数据
/// <summary>
/// 获取post请求数据
/// </summary>
/// <returns></returns>
private string PostInput()
{
Stream s = System.Web.HttpContext.Current.Request.InputStream;
byte[] b = new byte[s.Length];
s.Read(b, 0, (int)s.Length);
return Encoding.UTF8.GetString(b);
}
#endregion
#region 消息类型适配器
private void ResponseMsg(string weixin)// 服务器响应微信请求
{
XmlDocument doc = new XmlDocument();
doc.LoadXml(weixin);//读取xml字符串
XmlElement root = doc.DocumentElement;
ExmlMsg xmlMsg = GetExmlMsg(root);
//XmlNode MsgType = root.SelectSingleNode("MsgType");
//string messageType = MsgType.InnerText;
string messageType = xmlMsg.MsgType;//获取收到的消息类型。文本(text),图片(image),语音等。
try
{
switch (messageType)
{
//当消息为文本时
case "text":
textCase(xmlMsg);
break;
case "event":
if (!string.IsNullOrEmpty(xmlMsg.EventName) && xmlMsg.EventName.Trim() == "subscribe")
{
//刚关注时的时间,用于欢迎词
int nowtime = ConvertDateTimeInt(DateTime.Now);
string msg = "你要关注我,我有什么办法。随便发点什么试试吧~~~";
string resxml = "<xml><ToUserName><![CDATA[" + xmlMsg.FromUserName + "]]></ToUserName><FromUserName><![CDATA[" + xmlMsg.ToUserName + "]]></FromUserName><CreateTime>" + nowtime + "</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[" + msg + "]]></Content><FuncFlag>0</FuncFlag></xml>";
Response.Write(resxml);
}
break;
case "image":
break;
case "voice":
break;
case "vedio":
break;
case "location":
break;
case "link":
break;
default:
break;
}
Response.End();
}
catch (Exception)
{
}
}
#endregion
private string getText(ExmlMsg xmlMsg)
{
string con = xmlMsg.Content.Trim();
System.Text.StringBuilder retsb = new StringBuilder(200);
retsb.Append("这里放你的业务逻辑");
retsb.Append("接收到的消息:"+ xmlMsg.Content);
retsb.Append("用户的OPEANID:"+ xmlMsg.FromUserName);
return retsb.ToString();
}
#region 操作文本消息 + void textCase(XmlElement root)
private void textCase(ExmlMsg xmlMsg)
{
int nowtime = ConvertDateTimeInt(DateTime.Now);
string msg = "";
msg = getText(xmlMsg);
string resxml = "<xml><ToUserName><![CDATA[" + xmlMsg.FromUserName + "]]></ToUserName><FromUserName><![CDATA[" + xmlMsg.ToUserName + "]]></FromUserName><CreateTime>" + nowtime + "</CreateTime><MsgType><![CDATA[text]]></MsgType><Content><![CDATA[" + msg + "]]></Content><FuncFlag>0</FuncFlag></xml>";
Response.Write(resxml);
}
#endregion
#region 将datetime.now 转换为 int类型的秒
/// <summary>
/// datetime转换为unixtime
/// </summary>
/// <param name="time"></param>
/// <returns></returns>
private int ConvertDateTimeInt(System.DateTime time)
{
System.DateTime startTime = TimeZone.CurrentTimeZone.ToLocalTime(new System.DateTime(1970, 1, 1));
return (int)(time - startTime).TotalSeconds;
}
private int converDateTimeInt(System.DateTime time)
{
System.DateTime startTime = TimeZone.CurrentTimeZone.ToLocalTime(new System.DateTime(1970, 1, 1));
return (int)(time - startTime).TotalSeconds;
}
/// <summary>
/// unix时间转换为datetime
/// </summary>
/// <param name="timeStamp"></param>
/// <returns></returns>
private DateTime UnixTimeToTime(string timeStamp)
{
DateTime dtStart = TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1));
long lTime = long.Parse(timeStamp + "0000000");
TimeSpan toNow = new TimeSpan(lTime);
return dtStart.Add(toNow);
}
#endregion
#region 验证微信签名 保持默认即可
/// <summary>
/// 验证微信签名
/// </summary>
/// * 将token、timestamp、nonce三个参数进行字典序排序
/// * 将三个参数字符串拼接成一个字符串进行sha1加密
/// * 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信。
/// <returns></returns>
private bool CheckSignature()
{
string signature = Request.QueryString["signature"].ToString();
string timestamp = Request.QueryString["timestamp"].ToString();
string nonce = Request.QueryString["nonce"].ToString();
string[] ArrTmp = { Token, timestamp, nonce };
Array.Sort(ArrTmp); //字典排序
string tmpStr = string.Join("", ArrTmp);
tmpStr = FormsAuthentication.HashPasswordForStoringInConfigFile(tmpStr, "SHA1");
tmpStr = tmpStr.ToLower();
if (tmpStr == signature)
{
return true;
}
else
{
return false;
}
}
private void Valid()
{
string echoStr = Request.QueryString["echoStr"].ToString();
if (CheckSignature())
{
if (!string.IsNullOrEmpty(echoStr))
{
Response.Write(echoStr);
Response.End();
}
}
}
#endregion
#region 写日志(用于跟踪) + WriteLog(string strMemo, string path = "*****")
/// <summary>
/// 写日志(用于跟踪)
/// 如果log的路径修改,更改path的默认值
/// </summary>
private void WriteLog(string strMemo, string path = "wx.txt")
{
string filename = Server.MapPath(path);
StreamWriter sr = null;
try
{
if (!File.Exists(filename))
{
sr = File.CreateText(filename);
}
else
{
sr = File.AppendText(filename);
}
sr.WriteLine(strMemo);
}
catch
{
}
finally
{
if (sr != null)
sr.Close();
}
}
//#endregion
#endregion
#region 接收的消息实体类 以及 填充方法
private class ExmlMsg
{
/// <summary>
/// 本公众账号
/// </summary>
public string ToUserName { get; set; }
/// <summary>
/// 用户账号
/// </summary>
public string FromUserName { get; set; }
/// <summary>
/// 发送时间戳
/// </summary>
public string CreateTime { get; set; }
/// <summary>
/// 发送的文本内容
/// </summary>
public string Content { get; set; }
/// <summary>
/// 消息的类型
/// </summary>
public string MsgType { get; set; }
/// <summary>
/// 事件名称
/// </summary>
public string EventName { get; set; }
}
private ExmlMsg GetExmlMsg(XmlElement root)
{
ExmlMsg xmlMsg = new ExmlMsg() {
FromUserName = root.SelectSingleNode("FromUserName").InnerText,
ToUserName = root.SelectSingleNode("ToUserName").InnerText,
CreateTime = root.SelectSingleNode("CreateTime").InnerText,
MsgType = root.SelectSingleNode("MsgType").InnerText,
};
if (xmlMsg.MsgType.Trim().ToLower() == "text")
{
xmlMsg.Content = root.SelectSingleNode("Content").InnerText;
}
else if (xmlMsg.MsgType.Trim().ToLower() == "event")
{
xmlMsg.EventName = root.SelectSingleNode("Event").InnerText;
}
return xmlMsg;
}
#endregion
}
}
【最后说的】
如果以上程序存在错误,欢迎指出,还是学生,水平有限,联系方式sina微博@导弹林瀚,谢谢。
也懒得放github上了,如果要扩展或者二次开发,请看我上面的分析部分,该说的我都说了。