From 170f05af0c95c692cba065719728edae06572b41 Mon Sep 17 00:00:00 2001 From: CortexCore <2630229280@qq.com> Date: Fri, 12 Jul 2024 19:32:02 +0800 Subject: [PATCH] 1 --- .gitignore | 4 + Commands/OnReceiveWeChatMessage.cs | 22 ++++ Extensions/WeChatExtensions.cs | 17 +++ .../TemplateMessage/IWeChatTemplateMessage.cs | 35 ++++++ Interfaces/Topic/ITopModel.cs | 9 ++ Interfaces/Topic/ITopicService.cs | 6 ++ Models/WeChatTemplateMessage.cs | 52 +++++++++ Models/WeChatUserInfo.cs | 60 +++++++++++ Services/WeChatAccessTokenService.cs | 66 ++++++++++++ Services/WeChatHttpClient.cs | 28 +++++ Services/WeChatSettingsService.cs | 22 ++++ Services/WeChatUserInfoService.cs | 102 ++++++++++++++++++ WeChatSharp.csproj | 17 +++ 13 files changed, 440 insertions(+) create mode 100644 .gitignore create mode 100644 Commands/OnReceiveWeChatMessage.cs create mode 100644 Extensions/WeChatExtensions.cs create mode 100644 Interfaces/TemplateMessage/IWeChatTemplateMessage.cs create mode 100644 Interfaces/Topic/ITopModel.cs create mode 100644 Interfaces/Topic/ITopicService.cs create mode 100644 Models/WeChatTemplateMessage.cs create mode 100644 Models/WeChatUserInfo.cs create mode 100644 Services/WeChatAccessTokenService.cs create mode 100644 Services/WeChatHttpClient.cs create mode 100644 Services/WeChatSettingsService.cs create mode 100644 Services/WeChatUserInfoService.cs create mode 100644 WeChatSharp.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..993f4fb --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.deprecated +/.idea +/bin +/obj diff --git a/Commands/OnReceiveWeChatMessage.cs b/Commands/OnReceiveWeChatMessage.cs new file mode 100644 index 0000000..b4960b7 --- /dev/null +++ b/Commands/OnReceiveWeChatMessage.cs @@ -0,0 +1,22 @@ +namespace WeChatSharp.Commands; + +public interface IReceiveWeChatMessage +{ + string OpenId { get; } + string UserName { get; } + WeChatUserInfo UserInfo { get; } + string Context { get; } +} +public readonly struct OnReceiveWeChatMessage:IReceiveWeChatMessage +{ + public string OpenId => UserInfo.OpenId; + + public string UserName => UserInfo switch + { + var x when string.IsNullOrEmpty(x.NickName) is false=>x.NickName, + var x when string.IsNullOrEmpty(x.Remark) is false=>x.Remark, + _ => OpenId, + }; + public WeChatUserInfo UserInfo { get; init; } + public string Context { get; init; } +} \ No newline at end of file diff --git a/Extensions/WeChatExtensions.cs b/Extensions/WeChatExtensions.cs new file mode 100644 index 0000000..8ab8195 --- /dev/null +++ b/Extensions/WeChatExtensions.cs @@ -0,0 +1,17 @@ +using System.Runtime.CompilerServices; +using Microsoft.Extensions.DependencyInjection; +using WeChatSharp.Services; + +namespace WeChatSharp.Extensions; + +public static class WeChatExtensions +{ + public static IServiceCollection AddWeChatService(this IServiceCollection self) + { + self.AddSingleton(); + self.AddSingleton(); + self.AddSingleton(); + self.AddSingleton(); + return self; + } +} \ No newline at end of file diff --git a/Interfaces/TemplateMessage/IWeChatTemplateMessage.cs b/Interfaces/TemplateMessage/IWeChatTemplateMessage.cs new file mode 100644 index 0000000..797ac34 --- /dev/null +++ b/Interfaces/TemplateMessage/IWeChatTemplateMessage.cs @@ -0,0 +1,35 @@ +using System.Text.Json.Serialization; +using Newtonsoft.Json; + +namespace WeChatSharp.Interfaces; +/// +/// 微信模板消息接口定义 +/// +public interface IWeChatTemplateMessage +{ + /// + /// 接收者(用户)的 openid + /// + [JsonProperty("touser")] + string ToUser { get; } + /// + /// 模板Id + /// + [JsonProperty("template_id")] + string TemplateId { get; } + /// + /// 消息的Url + /// + [JsonProperty("url")] + string Url { get; } + /// + /// 消息的id,通常为Guid.New + /// + [JsonProperty("client_msg_id")] + string ClientMsgId { get; } + /// + /// 模板消息的数据 + /// + [JsonProperty("data")] + IDictionary Data { get; } +} diff --git a/Interfaces/Topic/ITopModel.cs b/Interfaces/Topic/ITopModel.cs new file mode 100644 index 0000000..8205808 --- /dev/null +++ b/Interfaces/Topic/ITopModel.cs @@ -0,0 +1,9 @@ +using System.ComponentModel.DataAnnotations; + +namespace WeChatSharp.Interfaces.Topic; + +public interface ITopModel +{ + [Key] + Guid id { get; } +} \ No newline at end of file diff --git a/Interfaces/Topic/ITopicService.cs b/Interfaces/Topic/ITopicService.cs new file mode 100644 index 0000000..3fb1dbb --- /dev/null +++ b/Interfaces/Topic/ITopicService.cs @@ -0,0 +1,6 @@ +namespace WeChatSharp.Interfaces.Topic; + +public interface ITopicService +{ + +} \ No newline at end of file diff --git a/Models/WeChatTemplateMessage.cs b/Models/WeChatTemplateMessage.cs new file mode 100644 index 0000000..a15debf --- /dev/null +++ b/Models/WeChatTemplateMessage.cs @@ -0,0 +1,52 @@ +using System.Text.Json.Serialization; +using Newtonsoft.Json; +using WeChatSharp.Interfaces; + +namespace WeChatSharp; + +public struct WeChatTemplateMessageData +{ + public WeChatTemplateMessageData(string value) + { + this.value = value; + } + + /// + /// 值 + /// + public string value { get; set; } +} +public struct WeChatTemplateMessage: IWeChatTemplateMessage +{ + [JsonProperty("touser")] + public string ToUser { get; set; } + [JsonProperty("template_id")] + public string TemplateId { get; set; } + [JsonProperty("url")] + public string Url { get; set; } + [JsonProperty("client_msg_id")] + public string ClientMsgId { get; set; } + [JsonProperty("data")] + public IDictionary Data { get; set; } +} +// { +// "touser":"oY0tZ6_aqq_MEWsej9zJEY6OVspI", +// "template_id":"TA6ogf8kMiB31M0oQ8WCxteITUauajrtuGL1LtptNg0", +// "url":"http://weixin.qq.com/download", +// "client_msg_id":"MSG_000002", +// "data":{ +// +// "character_string5":{ +// "value":"202307251515" +// }, +// "thing4": { +// "value":"已创建" +// }, +// "thing9": { +// "value":"CAICT" +// }, +// "phrase13":{ +// "value":"手动提交" +// } +// } +// } \ No newline at end of file diff --git a/Models/WeChatUserInfo.cs b/Models/WeChatUserInfo.cs new file mode 100644 index 0000000..cd2cbbe --- /dev/null +++ b/Models/WeChatUserInfo.cs @@ -0,0 +1,60 @@ +using Newtonsoft.Json; + +namespace WeChatSharp; +[Serializable] +public struct WeChatUserInfo +{ + [JsonProperty(propertyName: "subscribe")] + public int Subscribe { get; internal set; } + [JsonProperty(propertyName: "openid")] + public string OpenId { get; internal set; } + [JsonProperty(propertyName: "nickname")] + public string NickName{ get; internal set; } + + [JsonProperty(propertyName: "sex")] + public int Sex { get; internal set; } + [JsonProperty(propertyName: "language")] + public string Language{ get; internal set; } + [JsonProperty(propertyName: "city")] + public string City{ get; internal set; } + [JsonProperty(propertyName: "province")] + public string Province{ get; internal set; } + [JsonProperty(propertyName: "country")] + public string Country{ get; internal set; } + [JsonProperty(propertyName: "headimgurl")] + public string HeadImgUrl{ get; internal set; } + [JsonProperty(propertyName: "subscribe_time")] + public int SubscribeTime{ get; internal set; } + [JsonProperty(propertyName: "remark")] + public string Remark{ get; internal set; } + [JsonProperty(propertyName: "groupid")] + public int GroupId{ get; internal set; } + [JsonProperty(propertyName: "tagid_list")] + public string[] TagIdList{ get; internal set; } + [JsonProperty(propertyName: "subscribe_scene")] + public string SubscribeScene{ get; internal set; } + [JsonProperty(propertyName: "qr_scene")] + public int QrScene{ get; internal set; } + [JsonProperty(propertyName: "qr_scene_str")] + public string QrSceneStr{ get; internal set; } +} +/* +{ + "subscribe": 1, + "openid": "oY0tZ6_aqq_MEWsej9zJEY6OVspI", + "nickname": "", + "sex": 0, + "language": "zh_CN", + "city": "", + "province": "", + "country": "", + "headimgurl": "", + "subscribe_time": 1687668622, + "remark": "Root", + "groupid": 0, + "tagid_list": [], + "subscribe_scene": "ADD_SCENE_SEARCH", + "qr_scene": 0, + "qr_scene_str": "" +} +*/ \ No newline at end of file diff --git a/Services/WeChatAccessTokenService.cs b/Services/WeChatAccessTokenService.cs new file mode 100644 index 0000000..0974bb5 --- /dev/null +++ b/Services/WeChatAccessTokenService.cs @@ -0,0 +1,66 @@ +using System.Timers; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Timer = System.Timers.Timer; + +namespace WeChatSharp.Services; + +public class WeChatAccessTokenService +{ + public const string _AccessToken = "access_token"; + private readonly HttpClient _httpClient; + private readonly IWeChatSettings _weChatSettings; + private readonly Timer _timer=new(); + private readonly ILogger _logger; + private string AccessToken { get; set; }=string.Empty; + + public WeChatAccessTokenService(HttpClient httpClient, IWeChatSettings weChatSettings, ILogger logger) + { + _httpClient = httpClient; + _weChatSettings = weChatSettings; + _logger = logger; + _timer.AutoReset = true; + _timer.Interval = 3600 * 1000; + _timer.Elapsed+=_GetAccessToken; + } + private async void _GetAccessToken(object? sender, ElapsedEventArgs? e) + { + AccessToken = await GetAccessTokenAsync(); + } + + public async Task GetAccessTokenAsync() + { + var newAccessToken = AccessToken; + if (!string.IsNullOrEmpty(newAccessToken)) return newAccessToken; + var url = + $"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={_weChatSettings.AppId}&secret={_weChatSettings.AppSecret}&token={_weChatSettings.Token}"; + var json = await _httpClient.GetStringAsync(url); + var jObject = JsonConvert.DeserializeObject(json); + if (jObject is null) + { + _logger.LogWarning(json); + throw new NullReferenceException("json为空值"); + } + if (jObject.TryGetValue("errcode", out var errcode) && jObject.TryGetValue("errmsg",out var errmsg)) + { + switch (errcode.ToObject()) + { + case 40164: + _logger.LogWarning("调用接口的IP地址不在白名单中,请在接口IP白名单中进行设置。"); + break; + } + throw new Exception(errmsg.ToObject()); + } + if (jObject.TryGetValue("access_token", out var token)) + { + return newAccessToken = token.ToObject() ?? string.Empty; + } + _logger.LogWarning(json); + throw new InvalidOperationException("获取AccessToken失败") + { + Source = json, + }; + } + +} \ No newline at end of file diff --git a/Services/WeChatHttpClient.cs b/Services/WeChatHttpClient.cs new file mode 100644 index 0000000..77c1b1e --- /dev/null +++ b/Services/WeChatHttpClient.cs @@ -0,0 +1,28 @@ +using System.Text; +using Newtonsoft.Json; + +namespace WeChatSharp.Services; + +public class WeChatHttpClient +{ + private readonly WeChatAccessTokenService _accessTokenService; + private readonly HttpClient _httpClient; + + public WeChatHttpClient(WeChatAccessTokenService accessTokenService, HttpClient httpClient) + { + _accessTokenService = accessTokenService; + _httpClient = httpClient; + } + public async Task PostJsonAsync(string url,T model) + { + var json = JsonConvert.SerializeObject(model); + var jsonContent = new StringContent(json, Encoding.UTF8, "application/json"); + return await PostAsync(url, jsonContent); + } + public async Task PostAsync(string url,HttpContent content) + { + url += $"?{WeChatAccessTokenService._AccessToken}={await _accessTokenService.GetAccessTokenAsync()}"; + var responseMessage = await _httpClient.PostAsync(url, content); + return await responseMessage.Content.ReadAsStringAsync(); + } +} \ No newline at end of file diff --git a/Services/WeChatSettingsService.cs b/Services/WeChatSettingsService.cs new file mode 100644 index 0000000..1f5399f --- /dev/null +++ b/Services/WeChatSettingsService.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Configuration; + +namespace WeChatSharp.Services; + +public interface IWeChatSettings +{ + public string AppId { get; } + public string AppSecret { get; } + public string Token { get; } +} +public class WeChatSettingsService:IWeChatSettings +{ + public string AppId { get;private set; } + public string AppSecret { get;private set; } + public string Token { get; private set; } + public WeChatSettingsService(IConfiguration configuration) + { + AppId = configuration["WeChat:AppId"]; + AppSecret = configuration["WeChat:AppSecret"]; + Token = configuration["WeChat:Token"]; + } +} \ No newline at end of file diff --git a/Services/WeChatUserInfoService.cs b/Services/WeChatUserInfoService.cs new file mode 100644 index 0000000..727eaf8 --- /dev/null +++ b/Services/WeChatUserInfoService.cs @@ -0,0 +1,102 @@ + +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Numerics; +using Cysharp.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.VisualBasic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace WeChatSharp.Services; + +public class WeChatUserInfoService +{ + private readonly HttpClient _httpClient; + private readonly WeChatAccessTokenService accessTokenService; + private readonly IWeChatSettings _weChatSettings; + private readonly ILogger _logger; + private readonly ConcurrentDictionary weChatUserInfos = new(); + private readonly ConcurrentDictionary codeBasedWeChatUserInfos = new(); + public WeChatUserInfoService(HttpClient httpClient, WeChatAccessTokenService accessTokenService, IWeChatSettings weChatSettings, ILogger logger) + { + _httpClient = httpClient; + this.accessTokenService = accessTokenService; + _weChatSettings = weChatSettings; + _logger = logger; + } + public List GetCachedWeChatUserInfos() + { + return weChatUserInfos.Values.ToList(); + } + public async Task GetUserInfoByCode(string code) + { + if (codeBasedWeChatUserInfos.TryGetValue(code, out var userInfo)) + return userInfo; + + var json = await _httpClient.GetStringAsync( + $""" +https://api.weixin.qq.com/sns/oauth2/access_token?appid={_weChatSettings.AppId}&secret={_weChatSettings.AppSecret}&code={code}&grant_type=authorization_code +"""); + var jObject = JsonConvert.DeserializeObject(json)!; + if (jObject.TryGetValue("errcode", out var value)) + { + var ErrorMessage = jObject["errmsg"]!.ToObject(); + if (codeBasedWeChatUserInfos.TryGetValue(code, out userInfo)) + { + return userInfo; + } + + throw new InvalidOperationException(ErrorMessage); + } + + if (jObject.TryGetValue("openid", out var _token) && jObject.TryGetValue("access_token",out var accessToken)) + { + var openId = _token.ToObject()!; + userInfo = await GetUserInfoAsync(openId,accessToken.ToObject()!); + + _logger.LogInformation($"已经获取到了{code}的用户:{userInfo.OpenId}");; + _logger.LogInformation(json); + + codeBasedWeChatUserInfos.TryAdd(code, userInfo); + return userInfo; + } + + _logger.LogWarning("没有找到OpenId,获取的数据为;"); + _logger.LogWarning(json); + throw new InvalidOperationException("无效的Code"); + } + + public async Task GetUserInfoAsync(string openId,string accessToken=default!) + { + if(weChatUserInfos.TryGetValue(openId,out var userInfo)) + return userInfo; + userInfo =await GetWeChatUserInfoFromServer(openId,accessToken); + weChatUserInfos.TryAdd(openId,userInfo); + return userInfo; + } + private async Task GetWeChatUserInfoFromServer(string openId,string _accessToken=default!) + { + var accessToken = _accessToken; + string url; + if (string.IsNullOrEmpty(accessToken)) + { + accessToken = await accessTokenService.GetAccessTokenAsync(); + url = $"https://api.weixin.qq.com/cgi-bin/user/info?access_token={accessToken}&openid={openId}&lang=zh_CN"; + } + else + { + url = $"https://api.weixin.qq.com/sns/userinfo?access_token={accessToken}&openid={openId}&lang=zh_CN"; + } + var json = await _httpClient.GetStringAsync(url); + var jObject = JsonConvert.DeserializeObject(json)!; + + if (jObject.ContainsKey("errcode")) + { + Console.WriteLine(jObject); + } + + var weChatUserInfo = JsonConvert.DeserializeObject(json); + return weChatUserInfo; + } +} \ No newline at end of file diff --git a/WeChatSharp.csproj b/WeChatSharp.csproj new file mode 100644 index 0000000..27549b4 --- /dev/null +++ b/WeChatSharp.csproj @@ -0,0 +1,17 @@ + + + + net7.0 + enable + enable + + + + + + + + + + +