微信公众号-入坑指南(一)
微信公众号开发,没接触过的时候觉得挺高大上,自脱离大厂,入职中小公司后,技术几乎全靠自己一个人踩抗,当初入职公司的时候,第一个主任务中有一部分就涉及到了公众号开发,包括订阅,推送,学生扫码支付宿舍水电费,住宿费等等。由于从来没接触过微信开发,所以抱着无比激动的心情啃了一遍又一遍微信接口文档,不明就里。因为完全没有一点概念,而且还有所谓的订阅号,企业号,服务号,公众号。各自还不一样,个人目前只能申请订阅号,微信提供的接口贼少,连个自定义菜单接口都么有,个人推推文章啥的凑合着用,要想尝试更多功能,只能是企业身份去注册公众号,当然有些功能还是需要额外收费的,比如模板推送消息就要300元/年。其实网上很多教程代码给的都有坑,Copy下来很多都是自定义的类,又不提供全,很不利于新手的学习,新手最想的是什么,先来一个简单的,可以跑起来的程序,然后由浅及深的自我去认知,找个某系列教程, copy下来后 ,报红,一看是自定义类的锅,源码中又没有,真是坑,换一个教程,copy下来又是如此,反反复复几次,心态很容易就蹦,如果有接触过微信开发,或者能静下心来啃文档,这些都还好,当初我呢,时间很急,又从来没有接触过,身边更没有同事去请教,真是越慌越糟糕,最后一点一点的啃出来。只要程序能跑起来,心里大致有谱了,这个时间在去接触接口文档,看需求,加功能就得心应手了,总所周一,万事开头难,古人诚不欺我也!
第一 ,在 微信公众平台官 网注册后(一键注册这里略过), 若是个人订阅号,必须要有一个外网(企业号跳过此步骤)将网站映射出去,这里是在花生壳上注册一个域名,要保证域名解析正常,不正常的及时到官网找原因,或者提交工单,让后台人员处理,毕竟免费的壳域名不稳定,收费的顶级域名果然稍微好点(现在域名需要实名认证),在域名解析正常之后,开始设置内网映射。注意这里内网主机的端口号设置为80,没办法微信规定只对接80(http) 或者443(https),之前可以转接端口号后来被封了只能老老实实用80。如下图所示:
第二,外网映射的问题搞定后,意味已经拥有了自己的外网域名网站,可着手开发属于自己的网站,如自动回复消息。这里新建一 MVC空项目,在引用中下载微信negut 包,主要时盛派的 Senparc.Weixin.MP,Senparc.Weixin.MP.MVC 两个包,然后新建一控制器WeChat,方法如下,这是方法是用来验证订阅号中相关参数配置是否正确,需要登录微信公众平台在基本参数里面配置时 ,这里填写 url 的一定要为外网,并且指向地址一定要是这里新建的控制器 Wechat 保持一致
// GET: WeChat [HttpGet] [ActionName("Index")] public Task<ActionResult> Get(string signature, string timestamp, string nonce, string echostr) { return Task.Factory.StartNew(() => {
//申请订阅号token var token = ConfigHelper.ExitCache("Token"); if (CheckSignature.Check(signature, timestamp, nonce, token)) { //获取Token var acestoken = TokenHelper.IsExistAccess_Token(); return echostr; //返回随机字符串则表示验证通过 } else { return "failed:" + signature + "," + CheckSignature.GetSignature(timestamp, nonce, token) + "。" + "如果你在浏览器中看到这句话,说明此地址可以被作为微信公众账号后台的Url,请注意保持Token一致。"; } }).ContinueWith<ActionResult>(task => Content(task.Result)); }
填写一致后,微信公众平台里保存配置时,才会跳转到上面那个方法,检查好token,appId 等参数是否配对,有时候由于网络原因需要多点击几次,才会跳转到上面的方法中,调用微信接口时,需要提供一个token,而这个token 请求的次数对于订阅号而言是有限的,也就2000次,请求完了就没有了,所以一般这里,获取token的时候,我们判断一下token是否过期(2小时有效期),若未过期就不去请求最新的 token,避免浪费资源,这里处理方式为在配置文件中添加两个字段,一个保存 token,一个记录当前到期时间。完整代码如下:
public class TokenHelper { /// <summary> /// 根据当前日期 判断Access_Token 是否超期 如果超期返回新的Access_Token 否则返回之前的Access_Token /// </summary> /// <param name="datetime"></param> /// <returns></returns> public static string IsExistAccess_Token() { string token = string.Empty; DateTime youXRQ; // 读取XML文件中的数据,并显示出来 ,注意文件路径 string filepath = ConfigHelper.ExitCache("CurrentTokenPath"); XElement xml = XElement.Load(filepath); token = xml.Descendants("Access_Token").FirstOrDefault().Value.ToString(); youXRQ = Convert.ToDateTime(xml.Descendants("Access_YouXRQ").FirstOrDefault().Value.ToString()); //判断当前 token 是否过期 if (DateTime.Now > youXRQ) { DateTime _youxrq = DateTime.Now; Access_token mode = GetAccess_token(); xml.Descendants("Access_Token").FirstOrDefault().Value = mode.access_token; _youxrq = _youxrq.AddSeconds(int.Parse(mode.expires_in)); xml.Descendants("Access_YouXRQ").FirstOrDefault().Value= _youxrq.ToString(); xml.Save(filepath); token = mode.access_token; } return token; } /// <summary> /// 获取Access_token /// </summary> /// <returns></returns> private static Access_token GetAccess_token() { var appid = ConfigHelper.ExitCache("AppId"); var secret = ConfigHelper.ExitCache("AppSecret"); string strUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appid + "&secret=" + secret; Access_token mode = new Access_token(); HttpWebRequest req = (HttpWebRequest)WebRequest.Create(strUrl); //用GET形式请求指定的地址 req.Method = "GET"; using (WebResponse wr = req.GetResponse()) { StreamReader reader = new StreamReader(wr.GetResponseStream(), Encoding.UTF8); string content = reader.ReadToEnd(); reader.Close(); reader.Dispose(); //在这里对Access_token 赋值 Access_token token = new Access_token(); token = JsonHelper.ParseFromJson<Access_token>(content); mode.access_token = token.access_token; mode.expires_in = token.expires_in; } return mode; } }
public class Access_token { /// <summary> /// 获取到的凭证 /// </summary> public string access_token { get; set; } /// <summary> /// 凭证有效时间,单位:秒 /// </summary> public string expires_in { get; set; } }
其中类 Access_token 包含两个类型为字符串的成员字段 access_token(获取到的凭证),expires_in(凭证有效时间,秒),到这里算是将 微信公众号 和 所开发的程序关联起来。切记:微信公众平台中的白名单中需要添加开发电脑的内网ip,否则获取不到 token,会提示当前往网络不在白名单内。
第三. 到了现在,准备工作才算告一段落,可以进行功能上的开发——入门级的被动回复消息。根据 微信平台开发文档 可知,微信公众平台会以post 请求方式 将用户发送的消息(文本,图片,语音等)请求至配置时设置的URL 中,当Post方法接受到消息后,将其进行解析即可
[HttpPost] [ActionName("Index")] public void Post(Senparc.Weixin.MP.Entities.Request.PostModel postModel) { //校验签名 var token = CacheHelper.GetCache("Token").ToString(); if (!CheckSignature.Check(postModel.Signature, postModel.Timestamp, postModel.Nonce, token)) { System.Web.HttpContext.Current.Response.Write("参数验证失败"); return; } string postString = string.Empty; using (Stream stream = System.Web.HttpContext.Current.Request.InputStream) { byte[] byts = new byte[stream.Length]; stream.Read(byts, 0, (int)stream.Length); postString = Encoding.UTF8.GetString(byts); //处理 接收到数据 string responseContent = DealMessage(postString); System.Web.HttpContext.Current.Response.Write(responseContent); } }
参照微信公众平台开发文档,可知,接收的消息为xml 格式,具体参数如下
将接受到的消息转换为xml 格式的字符串后,使用Linq to xml 对数据进行解析,这里将content 类型改为你想回复的文本消息,即可当作回复
/// <summary> /// 统一全局返回消息处理方法 /// </summary> /// <param name="postStr"></param> /// <returns></returns> public string DealMessage(string postStr) { var responseContent = string.Empty; XElement xml = XElement.Parse(postStr); //获取消息类型 string msgType = xml.Descendants("MsgType").FirstOrDefault().Value.ToString(); //开发者微信号 string ToUserName = xml.Descendants("ToUserName").FirstOrDefault().Value.ToString(); //发送方帐号(一个OpenID) string FromUserName = xml.Descendants("FromUserName").FirstOrDefault().Value.ToString(); //消息内容 string Content = xml.Descendants("MediaId").FirstOrDefault().Value.ToString(); if (msgType != null) { switch (msgType) { case "image": responseContent = ResMessgeHelper.ReceivedText(FromUserName,ToUserName,Content); break; case "text": responseContent = ResMessgeHelper.ReceivedImg(FromUserName, ToUserName, Content); break; case "voice": responseContent = ResMessgeHelper.ReceivedVoice(FromUserName, ToUserName, Content); break; default: break; } } return responseContent; }
这里重新构造回复消息内容,和官方保持一致,需要注意的是,![CDATA["....."]] 这是一个固定写法,不可省略,以及针对不同类型的消息,
<MsgType><![CDATA[text]]></MsgType> 这里的值是不同的,text,image,voice 否则文不对题,无法正确接收消息
public class ResMessgeHelper { /// <summary> /// 文本消息 /// </summary> /// <param name="FromUserName"></param> /// <param name="ToUserName"></param> /// <param name="Content"></param> /// <returns></returns> public static string ReceivedText(string FromUserName, string ToUserName, string Content) { string textpl = string.Empty; Content = "您发送的消息为:" + Content + "\n" + "您的openId:" + FromUserName; textpl = "<xml>" + "<ToUserName><![CDATA[" + FromUserName + "]]></ToUserName>" + "<FromUserName><![CDATA[" + ToUserName + "]]></FromUserName>" + "<CreateTime>" + DateTime.Now + "</CreateTime>" + "<MsgType><![CDATA[text]]></MsgType>" + "<Content><![CDATA[" + Content + "]]></Content>" + "</xml>"; return textpl; } /// <summary> /// 图片消息 /// </summary> /// <param name="FromUserName"></param> /// <param name="ToUserName"></param> /// <param name="Content"></param> /// <returns></returns> public static string ReceivedImg(string FromUserName, string ToUserName, string content) { string textpl = string.Empty; textpl = "<xml>" + "<ToUserName><![CDATA[" + FromUserName + "]]></ToUserName>" + "<FromUserName><![CDATA[" + ToUserName + "]]></FromUserName>" + "<CreateTime>" + DateTime.Now + "</CreateTime>" + "<MsgType><![CDATA[image]]></MsgType>" + "<Image>"+ "<MediaId><![CDATA[" + content + "]]></MediaId>" + "</Image>" + "</xml>"; return textpl; } /// <summary> /// 语音消息 /// </summary> /// <param name="FromUserName"></param> /// <param name="ToUserName"></param> /// <param name="Content"></param> /// <returns></returns> public static string ReceivedVoice(string FromUserName, string ToUserName, string content) { string textpl = string.Empty; textpl = "<xml>" + "<ToUserName><![CDATA[" + FromUserName + "]]></ToUserName>" + "<FromUserName><![CDATA[" + ToUserName + "]]></FromUserName>" + "<CreateTime>" + DateTime.Now + "</CreateTime>" + "<MsgType><![CDATA[voice]]></MsgType>" + "<Voice>" + "<MediaId><![CDATA[" + content + "]]></MediaId>" + "</Voice>" + "</xml>"; return textpl; } }
写到这里,最基础的入门内容算是了解完了,后续会将设置菜单,模板推送消息,扫码支付,跳转支付等内容补全,目前这只是一个小小开始,当心里有点概念,大致知道是怎么回事的时候,再逐渐去深入的了解其他一些功能,就会简单很多,很多时候,我们缺少的不是那种很深的文章,而是如何开头。(注:文中的代码可直接复制即可运行)
github 源码 https://github.com/Sientuo/TestPlay
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构