微信公众号-入坑指南(一)

       微信公众号开发,没接触过的时候觉得挺高大上,自脱离大厂,入职中小公司后,技术几乎全靠自己一个人踩抗,当初入职公司的时候,第一个主任务中有一部分就涉及到了公众号开发,包括订阅,推送,学生扫码支付宿舍水电费,住宿费等等。由于从来没接触过微信开发,所以抱着无比激动的心情啃了一遍又一遍微信接口文档,不明就里。因为完全没有一点概念,而且还有所谓的订阅号,企业号,服务号,公众号。各自还不一样,个人目前只能申请订阅号,微信提供的接口贼少,连个自定义菜单接口都么有,个人推推文章啥的凑合着用,要想尝试更多功能,只能是企业身份去注册公众号,当然有些功能还是需要额外收费的,比如模板推送消息就要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

 

posted @ 2020-08-24 19:31  郎中令  阅读(527)  评论(0编辑  收藏  举报