文档

入门指引

🐜我只是代码的搬运工~

效果

Asp .NetCore 微信订阅号自动回复之文本篇-LMLPHP

环境

  • .Net Core 6.0
  • 订阅号【个人】
  • 域名【备案】⚠ 不备案可能访问不到
  • 服务器【备案】⚠ 不备案可能访问不到
    • Docker
    • Nginx
    • Frp

工具

  • Visual Studio 2022

流程

服务器配置

Docker 安装 https://www.cnblogs.com/linyisonger/p/13964825.html

docker-compose 安装 https://www.cnblogs.com/linyisonger/p/13964931.html

Nginx 安装 https://blog.csdn.net/linyisonger/article/details/123569798

Frp 配置 https://blog.csdn.net/linyisonger/article/details/123567529

Nginx 配置转发到Frp域名上去,实现IP转发域名反向代理。

server {
    listen 80;
    ...
    location /api/ {
        proxy_pass http://a.frp.1998.ink;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection keep-alive;
        # proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
 	...
}
项目搭建
新建项目

我这里是这样新建的,当然也可以使用Visual Studio中的可视化新建。

dotnet new webapi --name AspNetCoreWeChatOAMessage
添加依赖

编辑.csproj文件。

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
  </ItemGroup>

</Project>
封装公众号类

新建OfficialAccount.cs文件,用于封装公众号所用的类型和方法。

🤦‍目前很多还没去实现,主要实现了文本回复…

using Newtonsoft.Json;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Serialization;

namespace AspNetCoreWeChatOAMessage
{
    /// <summary>
    /// 公众号
    /// </summary>
    public class OfficialAccount
    {
        /// <summary>
        /// 公众号 appId
        /// </summary>
        public string appid { get; set; }
        /// <summary>
        /// 公众号 appSecret
        /// </summary>
        public string secret { get; set; }
        /// <summary>
        /// 必须为英文或数字,长度为3-32字符。
        /// 什么是Token?https://mp.weixin.qq.com/wiki
        /// </summary>
        public string token { get; set; }
        /// <summary>
        /// 消息加密密钥由43位字符组成,可随机修改,字符范围为A-Z,a-z,0-9。
        /// 什么是EncodingAESKey?https://mp.weixin.qq.com/wiki
        /// </summary>
        public string encodingAESKey { get; set; }


        public OfficialAccount(string appid, string secret, string token, string encodingAESKey)
        {
            this.appid = appid;
            this.secret = secret;
            this.token = token;
            this.encodingAESKey = encodingAESKey;
        }

        /// <summary>
        /// 检查签名
        /// </summary>
        /// <param name="timestamp">时间戳</param>
        /// <param name="nonce"></param>
        /// <param name="signature">签名</param>
        /// <returns></returns>
        public bool CheckSignature(string timestamp, string nonce, string signature)
        {
            var list = new List<string>() { token, timestamp, nonce };
            list.Sort();
            return SHA1Encryption(string.Join("", list)) == signature.ToUpper();
        }
        /// <summary>
        /// access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。
        /// </summary>
        /// <param name="grant_type">填写 client_credential</param>
        /// <returns></returns>
        public async Task<GetAccessTokenResult?> GetAccessToken(string grant_type = "client_credential")
        {
            var result = await Get($"https://api.weixin.qq.com/cgi-bin/token?grant_type={grant_type}&appid={appid}&secret={secret}");
            return JsonConvert.DeserializeObject<GetAccessTokenResult>(result);
        }


        public class GetAccessTokenResult
        {
            /// <summary>
            /// 获取到的凭证
            /// </summary>
            public string? access_token { get; set; }
            /// <summary>
            /// 凭证有效时间,单位:秒。目前是7200秒之内的值。
            /// </summary> 
            public long expires_in { get; set; }
            /// <summary>
            /// 错误码
            /// </summary>
            public long errcode { get; set; }
            /// <summary>
            /// 错误信息
            /// </summary>
            public string? errmsg { get; set; }
        }
        /// <summary>
        /// 接收普通消息
        /// </summary>
        public class ReceivingStandardMessages
        {
            /// <summary>
            /// 消息基础类
            /// </summary>
            public class Message
            {
                /// <summary>
                /// 开发者微信号
                /// </summary> 
                public string? ToUserName { get; set; }
                /// <summary>
                /// 发送方帐号(一个OpenID)
                /// </summary>  
                public string? FromUserName { get; set; }
                /// <summary>
                /// 消息创建时间
                /// </summary> 
                public long CreateTime { get; set; }
                /// <summary>
                /// 消息类型,文本为text
                /// </summary> 
                public string? MsgType { get; set; }
                /// <summary>
                /// 消息id
                /// </summary> 
                public long MsgId { get; set; }
                /// <summary>
                /// 消息的数据ID(消息如果来自文章时才有)
                /// </summary> 
                public long MsgDataId { get; set; }
                /// <summary>
                /// 多图文时第几篇文章,从1开始(消息如果来自文章时才有)
                /// </summary> 
                public long Idx { get; set; }
            }
            public class TextMessage : Message
            {
                /// <summary>
                /// 文本消息内容
                /// </summary>
                public string? Content { get; set; }
            }
            public class ImageMessage : Message
            {
                /// <summary>
                ///  PicUrl 图片链接(由系统生成)
                /// </summary>
                public string? PicUrl { get; set; }
                /// <summary>
                /// 图片消息媒体id,可以调用获取临时素材接口拉取数据。
                /// </summary>
                public string? MediaId { get; set; }
            }
            public class VoiceMessage : Message
            {
                /// <summary>
                /// 语音消息媒体id,可以调用获取临时素材接口拉取数据。
                /// </summary>
                public string? MediaId { get; set; }
                /// <summary>
                /// 语音格式,如amr,speex等
                /// </summary>
                public string? Format { get; set; }
                /// <summary>
                /// 语音识别结果,UTF8编码
                /// 请注意,开通语音识别后,用户每次发送语音给公众号时,微信会在推送的语音消息 XML 数据包中,增加一个 Recognition 字段(注:由于客户端缓存,开发者开启或者关闭语音识别功能,对新关注者立刻生效,对已关注用户需要24小时生效。开发者可以重新关注此帐号进行测试)。
                /// </summary>
                public string? Recognition { get; set; }



            }
            public class VideoMessage : Message
            {

                /// <summary>
                /// 视频消息媒体id,可以调用获取临时素材接口拉取数据。
                /// </summary>
                public string? MediaId { get; set; }
                /// <summary>
                /// 视频消息缩略图的媒体id,可以调用多媒体文件下载接口拉取数据。
                /// </summary>
                public string? ThumbMediaId { get; set; }
            }
            public class ShortvideoMessage : Message
            {
                /// <summary>
                /// 视频消息媒体id,可以调用获取临时素材接口拉取数据。
                /// </summary>
                public string? MediaId { get; set; }
                /// <summary>
                /// 视频消息缩略图的媒体id,可以调用获取临时素材接口拉取数据。
                /// </summary>
                public string? ThumbMediaId { get; set; }
            }
            public class LocationMessage : Message
            {
                /// <summary>
                /// 地理位置纬度
                /// </summary>
                public double Location_X { get; set; }
                /// <summary>
                /// 地理位置经度
                /// </summary>
                public double Location_Y { get; set; }
                /// <summary>
                /// 地图缩放大小
                /// </summary>
                public double Scale { get; set; }
                /// <summary>
                /// 地理位置信息
                /// </summary>
                public string? Label { get; set; }
            }
            public class LinkMessage : Message
            {
                /// <summary>
                /// 消息标题
                /// </summary>
                public string? Title { get; set; }
                /// <summary>
                /// 消息描述
                /// </summary>
                public string? Description { get; set; }
                /// <summary>
                /// 消息链接
                /// </summary>
                public string? Url { get; set; }
            }

            public static Message? Parse(string text)
            {
                var res = XmlDeSerialize<Message>(text);
                return res?.MsgType switch
                {
                    "text" => XmlDeSerialize<TextMessage>(text),
                    "image" => XmlDeSerialize<ImageMessage>(text),
                    "voice" => XmlDeSerialize<VoiceMessage>(text),
                    "video" => XmlDeSerialize<VideoMessage>(text),
                    "shortvideo" => XmlDeSerialize<ShortvideoMessage>(text),
                    "location" => XmlDeSerialize<LocationMessage>(text),
                    "link" => XmlDeSerialize<LinkMessage>(text),
                    _ => res,
                };
            }
        }
        /// <summary>
        /// 被动回复用户消息
        /// </summary>
        public class PassiveUserReplyMessage
        {
            public class Media
            {
                /// <summary>
                /// 通过素材管理中的接口上传多媒体文件,得到的id
                /// </summary>
                public string? MediaId { get; set; }
            }
            public class Image : Media { }
            public class Voice : Media { }
            public class Video : Media
            {
                /// <summary>
                /// 视频消息的标题
                /// </summary>
                public string? Title { get; set; }
                /// <summary>
                /// 视频消息的描述
                /// </summary>
                public string? Description { get; set; }

            }
            public class Music
            {
                /// <summary>
                /// 音乐标题
                /// </summary>
                public string? Title { get; set; }
                /// <summary>
                /// 音乐描述
                /// </summary>
                public string? Description { get; set; }
                /// <summary>
                /// 音乐链接
                /// </summary>
                public string? MusicUrl { get; set; }
                /// <summary>
                /// 高质量音乐链接,WIFI环境优先使用该链接播放音乐
                /// </summary>
                public string? HQMusicUrl { get; set; }
                /// <summary>
                /// 缩略图的媒体id,通过素材管理中的接口上传多媒体文件,得到的id
                /// </summary>
                public string? ThumbMediaId { get; set; }
            }
            public class Article
            {
                /// <summary>
                /// 图文消息标题
                /// </summary>
                public string? Title { get; set; }
                /// <summary>
                /// 图文消息描述
                /// </summary>
                public string? Description { get; set; }
                /// <summary>
                /// 图片链接,支持JPG、PNG格式,较好的效果为大图360*200,小图200*200
                /// </summary>
                public string? PicUrl { get; set; }
                /// <summary>
                /// 点击图文消息跳转链接
                /// </summary>
                public string? Url { get; set; }
            }
            public class Message
            {
                /// <summary>
                /// 开发者微信号
                /// </summary> 
                public string? ToUserName { get; set; }
                /// <summary>
                /// 发送方帐号(一个OpenID)
                /// </summary>  
                public string? FromUserName { get; set; }
                /// <summary>
                /// 消息创建时间
                /// </summary> 
                public long CreateTime { get; set; }
                /// <summary>
                /// 消息类型, 
                /// </summary> 
                public string? MsgType { get; set; }
            }
            public class TextMessage : Message
            {
                /// <summary>
                /// 回复的消息内容 (换行:在 content 中能够换行,微信客户端就支持换行显示)
                /// </summary>
                public string? Content { get; set; }
            }
            public class ImageMessage : Message
            {
                public Image? Image { get; set; }
            }
            public class VoiceMessage : Message
            {
                public Voice? Voice { get; set; }
            }
            public class VideoMessage : Message
            {
                public Video? Video { get; set; }
            }
            public class MusicMessage : Message
            {
                public Music? Music { get; set; }
            }
            public class ArticleMessage : Message
            {

                /// <summary>
                /// 图文消息个数;当用户发送文本、图片、语音、视频、图文、地理位置这六种消息时,开发者只能回复1条图文消息;其余场景最多可回复8条图文消息
                /// </summary>
                public long ArticleCount { get; set; }
                /// <summary>
                /// 图文消息信息,注意,如果图文数超过限制,则将只发限制内的条数
                /// </summary>
                public Article[]? Articles { get; set; }
            }

            public static string Parse(Message message)
            {
                return $"<xml>${Format(message)}</xml>";
            }

            private static string Format(object obj, string text = "")
            {
                Type type = obj.GetType();
                PropertyInfo[] properties = type.GetProperties();
                foreach (PropertyInfo property in properties)
                {
                    Type propertyType = property.PropertyType;
                    Console.WriteLine(propertyType);
                    if (typeof(string).Equals(property.PropertyType))
                        text += $"<{property.Name}><![CDATA[{ property.GetValue(obj)}]]></{property.Name}>";
                    else
                        text += $"<{property.Name}>{ property.GetValue(obj)}</{property.Name}>";
                }
                return text;
            }
        }


        /// <summary>
        /// 内部使用的通用方法
        /// </summary>
        /// <param name="url"></param>
        /// <returns></returns>
        static async Task<string> Get(string url)
        {
            try
            {
                var httpClient = new HttpClient();
                HttpResponseMessage response = await httpClient.GetAsync(url);
                return response.IsSuccessStatusCode ? await response.Content.ReadAsStringAsync() : ""; ;
            }
            catch (Exception ex)
            {
                throw new Exception("Get 请求出错:" + ex.Message);
            }
        }

        /// <summary>  
        /// SHA1 加密,返回大写字符串  
        /// </summary>  
        /// <param name="content">需要加密字符串</param>  
        /// <param name="encode">指定加密编码</param>  
        /// <returns>返回40位大写字符串</returns>  
        static string SHA1Encryption(string content, Encoding? encode = null)
        {
            try
            {
                if (encode == null) encode = Encoding.UTF8;
                SHA1 sha1 = SHA1.Create();
                byte[] bytes_in = encode.GetBytes(content);
                byte[] bytes_out = sha1.ComputeHash(bytes_in);
                sha1.Dispose();
                string result = BitConverter.ToString(bytes_out);
                result = result.Replace("-", "");
                return result;
            }
            catch (Exception ex)
            {
                throw new Exception("SHA1Encryption加密出错:" + ex.Message);
            }
        }



        /// <summary>  
        /// 反序列化xml字符为对象,默认为Utf-8编码  
        /// </summary>  
        /// <typeparam name="T"></typeparam>  
        /// <param name="text"></param>  
        /// <returns></returns>  
        static T? XmlDeSerialize<T>(string text) where T : new() => XmlDeSerialize<T>(text, Encoding.UTF8);


        /// <summary>  
        /// 反序列化xml字符为对象  
        /// </summary>  
        /// <typeparam name="T"></typeparam>  
        /// <param name="text"></param>  
        /// <param name="encoding"></param>  
        /// <returns></returns>  
        static T? XmlDeSerialize<T>(string text, Encoding encoding) where T : new()
        {
            var xml = new XmlSerializer(typeof(T), new XmlRootAttribute("xml"));
            using var ms = new MemoryStream(encoding.GetBytes(text));
            using var sr = new StreamReader(ms, encoding);
            return (T?)xml.Deserialize(sr);
        }
    }
}
增加单例注入

编辑Program.cs文件,增加一个单例注入,便于各个接口的使用。

🐖 某些信息文本回复可能还用不到,但是为了以后的拓展,这里也在构造中添加了。

using AspNetCoreWeChatOAMessage;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddSingleton<OfficialAccount>(new OfficialAccount(appid,secret,token,encodingAESKey));


var app = builder.Build();
app.UseAuthorization();
app.MapControllers();
app.Run();
新建接口

新建Controllers/HandleController.cs文件,创建api/OfficialAccount接口。

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using System.Security.Cryptography;
using System.Text;

// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860

namespace AspNetCoreWeChatOAMessage.Controllers
{
    [Route("api/")]
    [ApiController]
    public class HandleController : ControllerBase
    {
        private readonly OfficialAccount oa;
        public HandleController(OfficialAccount oa)
        {
            this.oa = oa;
        }
        [HttpGet("OfficialAccount")]
        public string GetOfficialAccount()
        {
            Request.Query.TryGetValue("signature", out StringValues signature);
            Request.Query.TryGetValue("timestamp", out StringValues timestamp);
            Request.Query.TryGetValue("nonce", out StringValues nonce);
            Request.Query.TryGetValue("echostr", out StringValues echostr);
            if (oa.CheckSignature(timestamp, nonce, signature)) return echostr;
            return "hello, this is handle view";
        }
        [HttpPost("OfficialAccount")]
        public string PostOfficialAccount()
        {
            StreamReader stream = new StreamReader(Request.Body);
            string messageStr = stream.ReadToEndAsync().GetAwaiter().GetResult();
            var message = OfficialAccount.ReceivingStandardMessages.Parse(messageStr);
            if (message is OfficialAccount.ReceivingStandardMessages.TextMessage)
            {
                var textMessage = (OfficialAccount.ReceivingStandardMessages.TextMessage)message;
                Console.WriteLine(textMessage.Content);
                var content = textMessage.Content;
                var replyMessage = new OfficialAccount.PassiveUserReplyMessage.TextMessage
                {
                    Content = "你好,很高兴认识你~",
                    CreateTime = DateTimeOffset.Now.ToUnixTimeMilliseconds(),
                    ToUserName = message.FromUserName,
                    FromUserName = message.ToUserName,
                    MsgType = message.MsgType,
                };
                if (content != null)
                {
                    if (content.ToUpper().StartsWith("BMI"))
                    {
                        var bmiArr = content.Split(" ");
                        if (bmiArr.Length == 3 && double.TryParse(bmiArr[1], out double height) && double.TryParse(bmiArr[2], out double weight))
                        {
                            var bmi = (weight / ((height / 100) * (height / 100)));
                            var result = $"您的身体质量指数(BMI)为{bmi:#.0},";
                            if (bmi < 18.5) result += "可有点偏瘦啦,多吃点啦🧋~";
                            else if (bmi < 23.9) result += "在正常范围内,请继续保持🎉 ~";
                            else if (bmi < 26.9) result += "有点偏胖喽,可以去运动运动啦 🏃‍ ~";
                            else if (bmi < 29.9) result += "处于肥胖阶段,要去运动一下啦 🏋 ~";
                            else result += "很危险,去医院看看吧 ⛑ !";
                            replyMessage.Content = result;
                        }
                    }
                }
                return OfficialAccount.PassiveUserReplyMessage.Parse(replyMessage);
            }
            return "hello, this is handle view";
        }

    }
}

调试程序

运行程序,启动frpc对应本地服务端口以及服务器转发域名前缀。

微信公众平台配置

进入微信公众平台 https://mp.weixin.qq.com

服务器配置

Asp .NetCore 微信订阅号自动回复之文本篇-LMLPHP

配置你的服务器内容

Asp .NetCore 微信订阅号自动回复之文本篇-LMLPHP

保存成功的话,代表以上配置正确。

然后就可以进行属于你自己的公众号自动回复的开发啦。

部署

⚠部署成功,需要修改下服务器的nginx转发配置。

06-29 08:17