This commit is contained in:
commit
170f05af0c
|
@ -0,0 +1,4 @@
|
||||||
|
/.deprecated
|
||||||
|
/.idea
|
||||||
|
/bin
|
||||||
|
/obj
|
|
@ -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; }
|
||||||
|
}
|
|
@ -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<WeChatAccessTokenService>();
|
||||||
|
self.AddSingleton<WeChatUserInfoService>();
|
||||||
|
self.AddSingleton<IWeChatSettings,WeChatSettingsService>();
|
||||||
|
self.AddSingleton<WeChatHttpClient>();
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace WeChatSharp.Interfaces;
|
||||||
|
/// <summary>
|
||||||
|
/// 微信模板消息接口定义
|
||||||
|
/// </summary>
|
||||||
|
public interface IWeChatTemplateMessage
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 接收者(用户)的 openid
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("touser")]
|
||||||
|
string ToUser { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// 模板Id
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("template_id")]
|
||||||
|
string TemplateId { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// 消息的Url
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("url")]
|
||||||
|
string Url { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// 消息的id,通常为Guid.New
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("client_msg_id")]
|
||||||
|
string ClientMsgId { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// 模板消息的数据
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("data")]
|
||||||
|
IDictionary<string, WeChatTemplateMessageData> Data { get; }
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace WeChatSharp.Interfaces.Topic;
|
||||||
|
|
||||||
|
public interface ITopModel
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
Guid id { get; }
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace WeChatSharp.Interfaces.Topic;
|
||||||
|
|
||||||
|
public interface ITopicService
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 值
|
||||||
|
/// </summary>
|
||||||
|
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<string, WeChatTemplateMessageData> 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":"手动提交"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
|
@ -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": ""
|
||||||
|
}
|
||||||
|
*/
|
|
@ -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<WeChatAccessTokenService> _logger;
|
||||||
|
private string AccessToken { get; set; }=string.Empty;
|
||||||
|
|
||||||
|
public WeChatAccessTokenService(HttpClient httpClient, IWeChatSettings weChatSettings, ILogger<WeChatAccessTokenService> 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<string> 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<JObject>(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<int>())
|
||||||
|
{
|
||||||
|
case 40164:
|
||||||
|
_logger.LogWarning("调用接口的IP地址不在白名单中,请在接口IP白名单中进行设置。");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
throw new Exception(errmsg.ToObject<string>());
|
||||||
|
}
|
||||||
|
if (jObject.TryGetValue("access_token", out var token))
|
||||||
|
{
|
||||||
|
return newAccessToken = token.ToObject<string>() ?? string.Empty;
|
||||||
|
}
|
||||||
|
_logger.LogWarning(json);
|
||||||
|
throw new InvalidOperationException("获取AccessToken失败")
|
||||||
|
{
|
||||||
|
Source = json,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<string> PostJsonAsync<T>(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<string> PostAsync(string url,HttpContent content)
|
||||||
|
{
|
||||||
|
url += $"?{WeChatAccessTokenService._AccessToken}={await _accessTokenService.GetAccessTokenAsync()}";
|
||||||
|
var responseMessage = await _httpClient.PostAsync(url, content);
|
||||||
|
return await responseMessage.Content.ReadAsStringAsync();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<WeChatUserInfoService> _logger;
|
||||||
|
private readonly ConcurrentDictionary<string,WeChatUserInfo> weChatUserInfos = new();
|
||||||
|
private readonly ConcurrentDictionary<string,WeChatUserInfo> codeBasedWeChatUserInfos = new();
|
||||||
|
public WeChatUserInfoService(HttpClient httpClient, WeChatAccessTokenService accessTokenService, IWeChatSettings weChatSettings, ILogger<WeChatUserInfoService> logger)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
this.accessTokenService = accessTokenService;
|
||||||
|
_weChatSettings = weChatSettings;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
public List<WeChatUserInfo> GetCachedWeChatUserInfos()
|
||||||
|
{
|
||||||
|
return weChatUserInfos.Values.ToList();
|
||||||
|
}
|
||||||
|
public async Task<WeChatUserInfo> 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<JObject>(json)!;
|
||||||
|
if (jObject.TryGetValue("errcode", out var value))
|
||||||
|
{
|
||||||
|
var ErrorMessage = jObject["errmsg"]!.ToObject<string>();
|
||||||
|
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<string>()!;
|
||||||
|
userInfo = await GetUserInfoAsync(openId,accessToken.ToObject<string>()!);
|
||||||
|
|
||||||
|
_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<WeChatUserInfo> 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<WeChatUserInfo> 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<JObject>(json)!;
|
||||||
|
|
||||||
|
if (jObject.ContainsKey("errcode"))
|
||||||
|
{
|
||||||
|
Console.WriteLine(jObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
var weChatUserInfo = JsonConvert.DeserializeObject<WeChatUserInfo>(json);
|
||||||
|
return weChatUserInfo;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0-preview.4.23259.5" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
|
<PackageReference Include="UniTask" Version="2.3.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
Loading…
Reference in New Issue