220 changed files with 3930 additions and 471 deletions
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,21 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<None Remove="LINGYUN\Abp\WeChat\Common\Localization\Resources\*.json" /> |
|||
<EmbeddedResource Include="LINGYUN\Abp\WeChat\Common\Localization\Resources\*.json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.EventBus" Version="$(VoloAbpPackageVersion)" /> |
|||
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonPackageVersion)" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,33 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Localization; |
|||
using Volo.Abp.EventBus; |
|||
using Volo.Abp.Localization; |
|||
using Volo.Abp.Localization.ExceptionHandling; |
|||
using Volo.Abp.Modularity; |
|||
using Volo.Abp.VirtualFileSystem; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpEventBusModule))] |
|||
public class AbpWeChatCommonModule : AbpModule |
|||
{ |
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpVirtualFileSystemOptions>(options => |
|||
{ |
|||
options.FileSets.AddEmbedded<AbpWeChatCommonModule>(); |
|||
}); |
|||
|
|||
Configure<AbpLocalizationOptions>(options => |
|||
{ |
|||
options.Resources |
|||
.Add<WeChatCommonResource>("zh-Hans") |
|||
.AddVirtualJson("/LINGYUN/Abp/WeChat/Common/Localization/Resources"); |
|||
}); |
|||
|
|||
Configure<AbpExceptionLocalizationOptions>(options => |
|||
{ |
|||
options.MapCodeNamespace(WeChatCommonErrorCodes.Namespace, typeof(WeChatCommonResource)); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
using LINGYUN.Abp.WeChat.Common; |
|||
using System; |
|||
using System.Runtime.Serialization; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Crypto; |
|||
public class AbpWeChatCryptoException : AbpWeChatException |
|||
{ |
|||
public AbpWeChatCryptoException() |
|||
{ |
|||
} |
|||
|
|||
public AbpWeChatCryptoException( |
|||
SerializationInfo serializationInfo, |
|||
StreamingContext context) : base(serializationInfo, context) |
|||
{ |
|||
} |
|||
|
|||
public AbpWeChatCryptoException( |
|||
string appId, |
|||
string message = null, |
|||
string details = null, |
|||
Exception innerException = null) |
|||
: this(appId, "WeChat:100400", message, details, innerException) |
|||
{ |
|||
} |
|||
|
|||
public AbpWeChatCryptoException( |
|||
string appId, |
|||
string code = null, |
|||
string message = null, |
|||
string details = null, |
|||
Exception innerException = null) |
|||
: base(code, message, details, innerException) |
|||
{ |
|||
WithData("AppId", appId); |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Crypto.Models; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Crypto |
|||
{ |
|||
public interface IWeChatCryptoService |
|||
{ |
|||
/// <summary>
|
|||
/// 校验
|
|||
/// </summary>
|
|||
/// <param name="data"></param>
|
|||
/// <param name="sReplyEchoStr"></param>
|
|||
/// <returns></returns>
|
|||
string Validation(WeChatCryptoEchoData data); |
|||
/// <summary>
|
|||
/// 解密
|
|||
/// </summary>
|
|||
/// <param name="data"></param>
|
|||
/// <param name="sMsg"></param>
|
|||
/// <returns></returns>
|
|||
string Decrypt(WeChatCryptoDecryptData data); |
|||
/// <summary>
|
|||
/// 加密
|
|||
/// </summary>
|
|||
/// <param name="data"></param>
|
|||
/// <param name="sEncryptMsg"></param>
|
|||
/// <returns></returns>
|
|||
string Encrypt(WeChatCryptoEncryptData data); |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
namespace LINGYUN.Abp.WeChat.Common.Crypto.Models; |
|||
public class WeChatCryptoDecryptData |
|||
{ |
|||
public string MsgSignature { get; } |
|||
public string ReceiveId { get; } |
|||
public string Token { get; } |
|||
public string EncodingAESKey { get; } |
|||
public string TimeStamp { get; } |
|||
public string Nonce { get; } |
|||
public string PostData { get; } |
|||
public WeChatCryptoDecryptData( |
|||
string postData, |
|||
string receiveId, |
|||
string token, |
|||
string encodingAESKey, |
|||
string msgSignature, |
|||
string timeStamp, |
|||
string nonce) |
|||
{ |
|||
PostData = postData; |
|||
ReceiveId = receiveId; |
|||
Token = token; |
|||
EncodingAESKey = encodingAESKey; |
|||
MsgSignature = msgSignature; |
|||
TimeStamp = timeStamp; |
|||
Nonce = nonce; |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
namespace LINGYUN.Abp.WeChat.Common.Crypto.Models; |
|||
public class WeChatCryptoEncryptData |
|||
{ |
|||
public string Data { get; } |
|||
public string ReceiveId { get; } |
|||
public string Token { get; } |
|||
public string EncodingAESKey { get; } |
|||
public WeChatCryptoEncryptData( |
|||
string data, |
|||
string receiveId, |
|||
string token, |
|||
string encodingAESKey) |
|||
{ |
|||
Data = data; |
|||
ReceiveId = receiveId; |
|||
Token = token; |
|||
EncodingAESKey = encodingAESKey; |
|||
} |
|||
} |
|||
@ -0,0 +1,94 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Crypto.Models; |
|||
using LINGYUN.Abp.WeChat.Common.Utils; |
|||
using System; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Crypto |
|||
{ |
|||
public class WeChatCryptoService : IWeChatCryptoService, ITransientDependency |
|||
{ |
|||
public string Decrypt(WeChatCryptoDecryptData data) |
|||
{ |
|||
var crypto = new WXBizMsgCrypt( |
|||
data.Token, |
|||
data.EncodingAESKey, |
|||
data.ReceiveId); |
|||
|
|||
var retMsg = ""; |
|||
var ret = crypto.DecryptMsg( |
|||
data.MsgSignature, |
|||
data.TimeStamp, |
|||
data.Nonce, |
|||
data.PostData, |
|||
ref retMsg); |
|||
|
|||
if (ret != 0) |
|||
{ |
|||
throw new AbpWeChatCryptoException(data.ReceiveId, code: $"WeChat:{ret}"); |
|||
} |
|||
|
|||
return retMsg; |
|||
} |
|||
|
|||
public string Encrypt(WeChatCryptoEncryptData data) |
|||
{ |
|||
var crypto = new WXBizMsgCrypt( |
|||
data.Token, |
|||
data.EncodingAESKey, |
|||
data.ReceiveId); |
|||
|
|||
var sinature = ""; |
|||
var timestamp = DateTimeOffset.Now.ToLocalTime().ToUnixTimeSeconds().ToString(); |
|||
var nonce = DateTimeOffset.Now.Ticks.ToString("x"); |
|||
var sinatureRet = WXBizMsgCrypt.GenarateSinature( |
|||
data.Token, |
|||
timestamp, |
|||
nonce, |
|||
data.Data, |
|||
ref sinature); |
|||
if (sinatureRet != 0) |
|||
{ |
|||
throw new AbpWeChatCryptoException(data.ReceiveId, code: $"WeChat:{sinatureRet}"); |
|||
} |
|||
|
|||
var retMsg = ""; |
|||
|
|||
var ret = crypto.EncryptMsg( |
|||
sinature, |
|||
timestamp, |
|||
nonce, |
|||
ref retMsg); |
|||
|
|||
if (ret != 0) |
|||
{ |
|||
throw new AbpWeChatCryptoException(data.ReceiveId, code: $"WeChat:{ret}"); |
|||
} |
|||
|
|||
return retMsg; |
|||
} |
|||
|
|||
public string Validation(WeChatCryptoEchoData data) |
|||
{ |
|||
var crypto = new WXBizMsgCrypt( |
|||
data.Token, |
|||
data.EncodingAESKey, |
|||
data.ReceiveId); |
|||
|
|||
var retMsg = ""; |
|||
|
|||
var ret = crypto.VerifyURL( |
|||
data.MsgSignature, |
|||
data.TimeStamp, |
|||
data.Nonce, |
|||
data.EchoStr, |
|||
ref retMsg); |
|||
|
|||
if (ret != 0) |
|||
{ |
|||
throw new AbpWeChatCryptoException(data.ReceiveId, code: $"WeChat:{ret}"); |
|||
} |
|||
|
|||
return retMsg; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
{ |
|||
"culture": "en", |
|||
"texts": { |
|||
"WeChat:-40001": "签名验证错误", |
|||
"WeChat:-40002": "xml/json解析失败", |
|||
"WeChat:-40003": "sha加密生成签名失败", |
|||
"WeChat:-40004": "AESKey 非法", |
|||
"WeChat:-40005": "AppId 校验错误", |
|||
"WeChat:-40006": "AES 加密失败", |
|||
"WeChat:-40007": "AES 解密失败", |
|||
"WeChat:-40008": "解密后得到的buffer非法", |
|||
"WeChat:-40009": "base64加密失败", |
|||
"WeChat:-40010": "base64解密失败", |
|||
"WeChat:-40011": "生成xml/json失败" |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
{ |
|||
"culture": "zh-Hans", |
|||
"texts": { |
|||
"WeChat:-40001": "签名验证错误", |
|||
"WeChat:-40002": "xml/json解析失败", |
|||
"WeChat:-40003": "sha加密生成签名失败", |
|||
"WeChat:-40004": "AESKey 非法", |
|||
"WeChat:-40005": "AppId 校验错误", |
|||
"WeChat:-40006": "AES 加密失败", |
|||
"WeChat:-40007": "AES 解密失败", |
|||
"WeChat:-40008": "解密后得到的buffer非法", |
|||
"WeChat:-40009": "base64加密失败", |
|||
"WeChat:-40010": "base64解密失败", |
|||
"WeChat:-40011": "生成xml/json失败" |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Localization; |
|||
|
|||
[LocalizationResourceName("WeChatCommon")] |
|||
public class WeChatCommonResource |
|||
{ |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Messages; |
|||
public class AbpWeChatMessageResolveOptions |
|||
{ |
|||
public List<IMessageResolveContributor> MessageResolvers { get; } |
|||
|
|||
public AbpWeChatMessageResolveOptions() |
|||
{ |
|||
MessageResolvers = new List<IMessageResolveContributor>(); |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Messages.Handlers; |
|||
public class AbpWeChatMessageHandleOptions |
|||
{ |
|||
internal IDictionary<Type, IList<Type>> EventHandlers { get; } |
|||
internal IDictionary<Type, IList<Type>> MessageHandlers { get; } |
|||
|
|||
public AbpWeChatMessageHandleOptions() |
|||
{ |
|||
EventHandlers = new Dictionary<Type, IList<Type>>(); |
|||
MessageHandlers = new Dictionary<Type, IList<Type>>(); |
|||
} |
|||
|
|||
public void MapEvent<TEvent, TEventHandler>() |
|||
where TEvent : WeChatEventMessage |
|||
where TEventHandler : IEventHandleContributor<TEvent> |
|||
{ |
|||
var eventType = typeof(TEvent); |
|||
if (!EventHandlers.ContainsKey(eventType)) |
|||
{ |
|||
EventHandlers.Add(eventType, new List<Type>()); |
|||
} |
|||
EventHandlers[eventType].AddIfNotContains(typeof(TEventHandler)); |
|||
} |
|||
|
|||
public void MapMessage<TMessage, TMessageHandler>() |
|||
where TMessage : WeChatGeneralMessage |
|||
where TMessageHandler : IMessageHandleContributor<TMessage> |
|||
{ |
|||
var eventType = typeof(TMessage); |
|||
if (!MessageHandlers.ContainsKey(eventType)) |
|||
{ |
|||
MessageHandlers.Add(eventType, new List<Type>()); |
|||
} |
|||
MessageHandlers[eventType].AddIfNotContains(typeof(TMessageHandler)); |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Messages.Handlers; |
|||
public interface IEventHandleContributor<TMessage> where TMessage : WeChatEventMessage |
|||
{ |
|||
Task HandleAsync(MessageHandleContext<TMessage> context); |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Messages.Handlers; |
|||
public interface IMessageHandleContributor<TMessage> where TMessage : WeChatGeneralMessage |
|||
{ |
|||
Task HandleAsync(MessageHandleContext<TMessage> context); |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Messages.Handlers; |
|||
public interface IMessageHandler |
|||
{ |
|||
Task HandleEventAsync<TMessage>(TMessage data) where TMessage : WeChatEventMessage; |
|||
|
|||
Task HandleMessageAsync<TMessage>(TMessage data) where TMessage : WeChatGeneralMessage; |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using System; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Messages.Handlers; |
|||
public class MessageHandleContext<TMessage> where TMessage : WeChatMessage |
|||
{ |
|||
public TMessage Message { get; } |
|||
public IServiceProvider ServiceProvider { get; } |
|||
public MessageHandleContext(TMessage message, IServiceProvider serviceProvider) |
|||
{ |
|||
Message = message; |
|||
ServiceProvider = serviceProvider; |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Messages.Handlers; |
|||
public class MessageHandler : IMessageHandler, ITransientDependency |
|||
{ |
|||
private readonly IServiceScopeFactory _serviceScopeFactory; |
|||
private readonly AbpWeChatMessageHandleOptions _handleOptions; |
|||
|
|||
public MessageHandler( |
|||
IServiceScopeFactory serviceScopeFactory, |
|||
IOptions<AbpWeChatMessageHandleOptions> handleOptions) |
|||
{ |
|||
_serviceScopeFactory = serviceScopeFactory; |
|||
_handleOptions = handleOptions.Value; |
|||
} |
|||
|
|||
public async virtual Task HandleEventAsync<TMessage>(TMessage data) where TMessage : WeChatEventMessage |
|||
{ |
|||
if (_handleOptions.EventHandlers.TryGetValue(data.GetType(), out var handleTypes)) |
|||
{ |
|||
using var scope = _serviceScopeFactory.CreateScope(); |
|||
foreach (var handleType in handleTypes) |
|||
{ |
|||
var handlerService = ActivatorUtilities.CreateInstance(scope.ServiceProvider, handleType); |
|||
if (handlerService is IEventHandleContributor<TMessage> handler) |
|||
{ |
|||
var context = new MessageHandleContext<TMessage>(data, scope.ServiceProvider); |
|||
await handler.HandleAsync(context); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public async virtual Task HandleMessageAsync<TMessage>(TMessage data) where TMessage : WeChatGeneralMessage |
|||
{ |
|||
if (_handleOptions.MessageHandlers.TryGetValue(data.GetType(), out var handleTypes)) |
|||
{ |
|||
using var scope = _serviceScopeFactory.CreateScope(); |
|||
foreach (var handleType in handleTypes) |
|||
{ |
|||
var handlerService = ActivatorUtilities.CreateInstance(scope.ServiceProvider, handleType); |
|||
if (handlerService is IMessageHandleContributor<TMessage> handler) |
|||
{ |
|||
var context = new MessageHandleContext<TMessage>(data, scope.ServiceProvider); |
|||
await handler.HandleAsync(context); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using System.Xml.Linq; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Messages; |
|||
public interface IMessageResolveContext : IServiceProviderAccessor |
|||
{ |
|||
string Origin { get; } |
|||
XDocument MessageData { get; } |
|||
bool Handled { get; set; } |
|||
WeChatMessage Message { get; set; } |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
using System; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Messages; |
|||
public static class IMessageResolveContextExtensions |
|||
{ |
|||
public static bool HasMessageKey(this IMessageResolveContext context, string key) |
|||
{ |
|||
return context.MessageData.Root.Element(key) != null; |
|||
} |
|||
|
|||
public static string GetMessageData(this IMessageResolveContext context, string key) |
|||
{ |
|||
return context.MessageData.Root.Element(key)?.Value; |
|||
} |
|||
|
|||
public static T GetWeChatMessage<T>(this IMessageResolveContext context) where T : WeChatMessage |
|||
{ |
|||
return context.Origin.DeserializeWeChatMessage<T>(); |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Messages; |
|||
public interface IMessageResolveContributor |
|||
{ |
|||
string Name { get; } |
|||
|
|||
Task ResolveAsync(IMessageResolveContext context); |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Messages; |
|||
public interface IMessageResolver |
|||
{ |
|||
Task<MessageResolveResult> ResolveMessageAsync(MessageResolveData messageData); |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
using System; |
|||
using System.Xml.Linq; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Messages; |
|||
public class MessageResolveContext : IMessageResolveContext |
|||
{ |
|||
public IServiceProvider ServiceProvider { get; } |
|||
public string Origin { get; } |
|||
public XDocument MessageData { get; } |
|||
public bool Handled { get; set; } |
|||
public WeChatMessage Message { get; set; } |
|||
|
|||
public bool HasResolvedMessage() |
|||
{ |
|||
return Handled || Message != null; |
|||
} |
|||
|
|||
public MessageResolveContext( |
|||
string origin, |
|||
XDocument messageData, |
|||
IServiceProvider serviceProvider) |
|||
{ |
|||
Origin = origin; |
|||
MessageData = messageData; |
|||
ServiceProvider = serviceProvider; |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Messages; |
|||
public abstract class MessageResolveContributorBase : IMessageResolveContributor |
|||
{ |
|||
public abstract string Name { get; } |
|||
|
|||
public abstract Task ResolveAsync(IMessageResolveContext context); |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
namespace LINGYUN.Abp.WeChat.Common.Messages; |
|||
public class MessageResolveData |
|||
{ |
|||
public string AppId { get; set; } |
|||
|
|||
public string Token { get; set; } |
|||
|
|||
public string EncodingAESKey { get; set; } |
|||
|
|||
public string Signature { get; set; } |
|||
|
|||
public int TimeStamp { get; set; } |
|||
|
|||
public string Nonce { get; set; } |
|||
|
|||
public string Data { get; set; } |
|||
public MessageResolveData( |
|||
string appId, |
|||
string token, |
|||
string encodingAESKey, |
|||
string signature, |
|||
int timeStamp, |
|||
string nonce, |
|||
string data) |
|||
{ |
|||
AppId = appId; |
|||
Token = token; |
|||
EncodingAESKey = encodingAESKey; |
|||
Signature = signature; |
|||
TimeStamp = timeStamp; |
|||
Nonce = nonce; |
|||
Data = data; |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Messages; |
|||
public class MessageResolveResult |
|||
{ |
|||
public string Input { get; internal set; } |
|||
|
|||
public WeChatMessage Message { get; set; } |
|||
|
|||
public List<string> AppliedResolvers { get; } |
|||
|
|||
public MessageResolveResult(string input) |
|||
{ |
|||
Input = input; |
|||
AppliedResolvers = new List<string>(); |
|||
} |
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Crypto; |
|||
using LINGYUN.Abp.WeChat.Common.Crypto.Models; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using System.Xml.Linq; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Messages; |
|||
public class MessageResolver : IMessageResolver, ITransientDependency |
|||
{ |
|||
private readonly IWeChatCryptoService _cryptoService; |
|||
private readonly IServiceProvider _serviceProvider; |
|||
private readonly AbpWeChatMessageResolveOptions _options; |
|||
|
|||
public MessageResolver( |
|||
IOptions<AbpWeChatMessageResolveOptions> options, |
|||
IWeChatCryptoService cryptoService, |
|||
IServiceProvider serviceProvider) |
|||
{ |
|||
_serviceProvider = serviceProvider; |
|||
_cryptoService = cryptoService; |
|||
_options = options.Value; |
|||
} |
|||
/// <summary>
|
|||
/// 解析微信服务器推送消息/事件
|
|||
/// </summary>
|
|||
/// <param name="messageData"></param>
|
|||
/// <returns></returns>
|
|||
public async virtual Task<MessageResolveResult> ResolveMessageAsync(MessageResolveData messageData) |
|||
{ |
|||
var result = new MessageResolveResult(messageData.Data); |
|||
using (var serviceScope = _serviceProvider.CreateScope()) |
|||
{ |
|||
/* 明文数据格式 |
|||
* <xml> |
|||
* <ToUserName><![CDATA[toUser]]></ToUserName> |
|||
<FromUserName><![CDATA[fromUserName]]></FromUserName> |
|||
<CreateTime>1699433172</CreateTime> |
|||
<MsgType><![CDATA[event]]></MsgType> |
|||
<Event><![CDATA[event]]></Event> |
|||
<EventKey><![CDATA[eventKey]]></EventKey> |
|||
<Ticket><![CDATA[gQH97zwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAyTVoyOTBoLVJka0YxazFiYnhCMXcAAgTFSktlAwQ8AAAA]]></Ticket> |
|||
</xml> |
|||
*/ |
|||
var xmlDocument = XDocument.Parse(messageData.Data); |
|||
var encryptData = xmlDocument.Root.Element("Encrypt")?.Value; |
|||
if (!encryptData.IsNullOrWhiteSpace()) |
|||
{ |
|||
/* 加密数据格式 |
|||
* <xml> |
|||
* <ToUserName><![CDATA[toUser]]></ToUserName> |
|||
<Encrypt><![CDATA[msg_encrypt]]></Encrypt> |
|||
</xml> |
|||
*/ |
|||
var cryptoDecryptData = new WeChatCryptoDecryptData( |
|||
encryptData, |
|||
messageData.AppId, |
|||
messageData.Token, |
|||
messageData.EncodingAESKey, |
|||
messageData.Signature, |
|||
messageData.TimeStamp.ToString(), |
|||
messageData.Nonce); |
|||
// 经过解密函数得到如上真实数据
|
|||
var decryptMessage = _cryptoService.Decrypt(cryptoDecryptData); |
|||
xmlDocument = XDocument.Parse(decryptMessage); |
|||
result.Input = decryptMessage; |
|||
} |
|||
|
|||
var context = new MessageResolveContext(result.Input, xmlDocument, serviceScope.ServiceProvider); |
|||
|
|||
foreach (var messageResolver in _options.MessageResolvers) |
|||
{ |
|||
await messageResolver.ResolveAsync(context); |
|||
|
|||
result.AppliedResolvers.Add(messageResolver.Name); |
|||
|
|||
if (context.HasResolvedMessage()) |
|||
{ |
|||
result.Message = context.Message; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
return result; |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System.Xml.Serialization; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Messages; |
|||
/// <summary>
|
|||
/// 微信事件消息
|
|||
/// </summary>
|
|||
public abstract class WeChatEventMessage : WeChatMessage |
|||
{ |
|||
/// <summary>
|
|||
/// 事件类型
|
|||
/// </summary>
|
|||
[XmlElement("Event")] |
|||
public string Event { get; set; } |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System.Xml.Serialization; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Messages; |
|||
/// <summary>
|
|||
/// 微信普通消息
|
|||
/// </summary>
|
|||
public abstract class WeChatGeneralMessage : WeChatMessage |
|||
{ |
|||
/// <summary>
|
|||
/// 消息id,64位整型
|
|||
/// </summary>
|
|||
[XmlElement("MsgId")] |
|||
public long MsgId { get; set; } |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
using System; |
|||
using System.Xml.Serialization; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Messages; |
|||
/// <summary>
|
|||
/// 微信消息
|
|||
/// </summary>
|
|||
[Serializable] |
|||
[XmlRoot("xml")] |
|||
public abstract class WeChatMessage |
|||
{ |
|||
/// <summary>
|
|||
/// 开发者微信号
|
|||
/// </summary>
|
|||
[XmlElement("ToUserName")] |
|||
public string ToUserName { get; set; } |
|||
/// <summary>
|
|||
/// 发送方账号(一个OpenID)
|
|||
/// </summary>
|
|||
[XmlElement("FromUserName")] |
|||
public string FromUserName { get; set; } |
|||
/// <summary>
|
|||
/// 消息创建时间 (整型)
|
|||
/// </summary>
|
|||
[XmlElement("CreateTime")] |
|||
public int CreateTime { get; set; } |
|||
/// <summary>
|
|||
/// 消息类型,event
|
|||
/// </summary>
|
|||
[XmlElement("MsgType")] |
|||
public string MsgType { get; set; } |
|||
|
|||
public abstract WeChatMessageEto ToEto(); |
|||
|
|||
public virtual string SerializeToJson() |
|||
{ |
|||
return WeChatObjectSerializeExtensions.SerializeToJson(this); |
|||
} |
|||
|
|||
public virtual string SerializeToXml() |
|||
{ |
|||
return this.SerializeWeChatMessage(); |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
using Volo.Abp.Domain.Entities.Events.Distributed; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Common.Messages; |
|||
|
|||
public abstract class WeChatMessageEto : EtoBase |
|||
{ |
|||
} |
|||
@ -1,4 +1,4 @@ |
|||
namespace LINGYUN.Abp.WeChat.Security.Claims |
|||
namespace LINGYUN.Abp.WeChat.Common.Security.Claims |
|||
{ |
|||
/// <summary>
|
|||
/// 微信认证身份类型,可以像 <see cref="Volo.Abp.Security.Claims.AbpClaimTypes"/> 自行配置
|
|||
@ -0,0 +1,5 @@ |
|||
namespace LINGYUN.Abp.WeChat.Common; |
|||
public static class WeChatCommonErrorCodes |
|||
{ |
|||
public const string Namespace = "WeChatCommon"; |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
# LINGYUN.Abp.WeChat.Common |
|||
|
|||
## 模块说明 |
|||
|
|||
由于微信体系众多产品部分功能有共同点, 抽象一个通用模块, 实现一些通用的接口. |
|||
|
|||
### 基础模块 |
|||
|
|||
### 高阶模块 |
|||
|
|||
### 权限定义 |
|||
|
|||
### 功能定义 |
|||
|
|||
### 配置定义 |
|||
|
|||
### 如何使用 |
|||
|
|||
|
|||
```csharp |
|||
|
|||
[DependsOn( |
|||
typeof(AbpWeChatCommonModule))] |
|||
public class YouProjectModule : AbpModule |
|||
{ |
|||
} |
|||
|
|||
``` |
|||
|
|||
### 更新日志 |
|||
@ -0,0 +1,10 @@ |
|||
using Newtonsoft.Json; |
|||
|
|||
namespace System; |
|||
internal static class WeChatObjectSerializeExtensions |
|||
{ |
|||
public static string SerializeToJson(this object @object) |
|||
{ |
|||
return JsonConvert.SerializeObject(@object); |
|||
} |
|||
} |
|||
@ -0,0 +1,66 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using System.Collections; |
|||
using System.IO; |
|||
using System.Text; |
|||
using System.Text.RegularExpressions; |
|||
using System.Xml; |
|||
using System.Xml.Serialization; |
|||
|
|||
namespace System; |
|||
public static class WeChatXmlDataSerializeExtensions |
|||
{ |
|||
private readonly static Hashtable _xmlSerializers = new(); |
|||
private readonly static XmlRootAttribute _xmlRoot = new("xml"); |
|||
|
|||
private static XmlSerializer GetTypedSerializer(Type type) |
|||
{ |
|||
if (type == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(type)); |
|||
} |
|||
|
|||
var skey = type.AssemblyQualifiedName ?? type.GetHashCode().ToString(); |
|||
if (_xmlSerializers[skey] is not XmlSerializer xmlSerializer) |
|||
{ |
|||
xmlSerializer = new XmlSerializer(type, _xmlRoot); |
|||
_xmlSerializers[skey] = xmlSerializer; |
|||
} |
|||
|
|||
return xmlSerializer; |
|||
} |
|||
|
|||
public static T DeserializeWeChatMessage<T>(this string xml, XmlDeserializationEvents? events = null) where T : WeChatMessage |
|||
{ |
|||
var objectType = typeof(T); |
|||
using var stringReader = new StringReader(xml); |
|||
using var xmlReader = XmlReader.Create(stringReader); |
|||
var serializer = GetTypedSerializer(objectType); |
|||
var usingEvents = events ?? new XmlDeserializationEvents(); |
|||
return (T)serializer.Deserialize(xmlReader, usingEvents); |
|||
} |
|||
|
|||
public static string SerializeWeChatMessage(this WeChatMessage message) |
|||
{ |
|||
var objectType = message.GetType(); |
|||
var settings = new XmlWriterSettings |
|||
{ |
|||
Encoding = Encoding.UTF8, |
|||
Indent = false, |
|||
OmitXmlDeclaration = true, |
|||
WriteEndDocumentOnClose = false, |
|||
NamespaceHandling = NamespaceHandling.OmitDuplicates |
|||
}; |
|||
using var stream = new MemoryStream(); |
|||
using var writer = XmlWriter.Create(stream, settings); |
|||
var serializer = GetTypedSerializer(objectType); |
|||
var ns = new XmlSerializerNamespaces(); |
|||
ns.Add(string.Empty, string.Empty); |
|||
serializer.Serialize(writer, message, ns); |
|||
writer.Flush(); |
|||
var xml = Encoding.UTF8.GetString(stream.ToArray()); |
|||
xml = Regex.Replace(xml, "\\s*<\\w+ ([a-zA-Z0-9]+):nil=\"true\"[^>]*/>", string.Empty, RegexOptions.IgnoreCase); |
|||
xml = Regex.Replace(xml, "<\\?xml[^>]*\\?>", string.Empty, RegexOptions.IgnoreCase); |
|||
|
|||
return xml; |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,15 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Ddd.Application.Contracts" Version="$(VoloAbpPackageVersion)" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,10 @@ |
|||
using Volo.Abp.Application; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpDddApplicationContractsModule))] |
|||
public class AbpWeChatOfficialApplicationContractsModule : AbpModule |
|||
{ |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
namespace LINGYUN.Abp.WeChat.Official; |
|||
|
|||
public class AbpWeChatOfficialRemoteServiceConsts |
|||
{ |
|||
public const string RemoteServiceName = "AbpWeChatOfficial"; |
|||
|
|||
public const string ModuleName = "wechat-official"; |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
namespace LINGYUN.Abp.WeChat.Official.Account; |
|||
public class ParametricQrCodeGenerateInput |
|||
{ |
|||
public int SceneEnum { get; set; } |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.Content; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Account; |
|||
public interface IParametricQrCodeAppService : IApplicationService |
|||
{ |
|||
Task<IRemoteStreamContent> GenerateAsync(ParametricQrCodeGenerateInput input); |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using LINGYUN.Abp.WeChat.Official.Models; |
|||
using System; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Message; |
|||
|
|||
[Serializable] |
|||
public class MessageHandleInput : WeChatMessage |
|||
{ |
|||
public string Data { get; set; } |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using LINGYUN.Abp.WeChat.Official.Models; |
|||
using System.Text.Json.Serialization; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Message; |
|||
public class MessageValidationInput : WeChatMessage |
|||
{ |
|||
/// <summary>
|
|||
/// 加密的字符串。需要解密得到消息内容明文,解密后有random、msg_len、msg、receiveid四个字段,其中msg即为消息内容明文
|
|||
/// </summary>
|
|||
[JsonPropertyName("echostr")] |
|||
public string EchoStr { get; set; } |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Message; |
|||
/// <summary>
|
|||
/// 微信消息接口
|
|||
/// </summary>
|
|||
public interface IWeChatMessageAppService : IApplicationService |
|||
{ |
|||
/// <summary>
|
|||
/// 校验微信消息
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 参考文档:<see cref="https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html"/>
|
|||
/// </remarks>
|
|||
/// <param name="input"></param>
|
|||
/// <returns></returns>
|
|||
Task<string> Handle(MessageValidationInput input); |
|||
/// <summary>
|
|||
/// 处理微信消息
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 参考文档:<see cref="https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Access_Overview.html"/>
|
|||
/// </remarks>
|
|||
/// <param name="input"></param>
|
|||
/// <returns></returns>
|
|||
Task<string> Handle(MessageHandleInput input); |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
using System.Text.Json.Serialization; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Models; |
|||
public class WeChatMessage |
|||
{ |
|||
/// <summary>
|
|||
/// 微信加密签名,
|
|||
/// signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 签名计算方法参考: https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Before_Develop/Message_encryption_and_decryption.html
|
|||
/// </remarks>
|
|||
[JsonPropertyName("signature")] |
|||
public string Signature { get; set; } |
|||
/// <summary>
|
|||
/// 时间戳。与nonce结合使用,用于防止请求重放攻击。
|
|||
/// </summary>
|
|||
[JsonPropertyName("timestamp")] |
|||
public int TimeStamp { get; set; } |
|||
/// <summary>
|
|||
/// 随机数。与timestamp结合使用,用于防止请求重放攻击。
|
|||
/// </summary>
|
|||
[JsonPropertyName("nonce")] |
|||
public string Nonce { get; set; } |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,20 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Ddd.Application" Version="$(VoloAbpPackageVersion)" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\LINGYUN.Abp.WeChat.Official\LINGYUN.Abp.WeChat.Official.csproj" /> |
|||
<ProjectReference Include="..\LINGYUN.Abp.WeChat.Official.Application.Contracts\LINGYUN.Abp.WeChat.Official.Application.Contracts.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,13 @@ |
|||
using Volo.Abp.Application; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpWeChatOfficialApplicationContractsModule), |
|||
typeof(AbpWeChatOfficialModule), |
|||
typeof(AbpDddApplicationModule))] |
|||
public class AbpWeChatOfficialApplicationModule : AbpModule |
|||
{ |
|||
|
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
using LINGYUN.Abp.WeChat.Official.Account.Models; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.Content; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Account; |
|||
public class ParametricQrCodeAppService : ApplicationService, IParametricQrCodeAppService |
|||
{ |
|||
private readonly IParametricQrCodeGenerator _qrCodeGenerator; |
|||
|
|||
public ParametricQrCodeAppService(IParametricQrCodeGenerator qrCodeGenerator) |
|||
{ |
|||
_qrCodeGenerator = qrCodeGenerator; |
|||
} |
|||
|
|||
public async virtual Task<IRemoteStreamContent> GenerateAsync(ParametricQrCodeGenerateInput input) |
|||
{ |
|||
var createTicketModel = CreateTicketModel.EnumScene(input.SceneEnum); |
|||
var ticketModel = await _qrCodeGenerator.CreateTicketAsync(createTicketModel); |
|||
var stream = await _qrCodeGenerator.ShowQrCodeAsync(ticketModel.Ticket); |
|||
|
|||
return new RemoteStreamContent(stream); |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Crypto; |
|||
using LINGYUN.Abp.WeChat.Common.Crypto.Models; |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using Microsoft.Extensions.Logging; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp; |
|||
using Volo.Abp.Application.Services; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Message; |
|||
public class WeChatMessageAppService : ApplicationService, IWeChatMessageAppService |
|||
{ |
|||
private readonly IWeChatCryptoService _cryptoService; |
|||
private readonly AbpWeChatOfficialOptionsFactory _optionsFactory; |
|||
private readonly IDistributedEventBus _distributedEventBus; |
|||
private readonly IMessageResolver _messageResolver; |
|||
public WeChatMessageAppService( |
|||
IMessageResolver messageResolver, |
|||
IWeChatCryptoService cryptoService, |
|||
IDistributedEventBus distributedEventBus, |
|||
AbpWeChatOfficialOptionsFactory optionsFactory) |
|||
{ |
|||
_cryptoService = cryptoService; |
|||
_optionsFactory = optionsFactory; |
|||
_messageResolver = messageResolver; |
|||
_distributedEventBus = distributedEventBus; |
|||
} |
|||
|
|||
public async virtual Task<string> Handle(MessageValidationInput input) |
|||
{ |
|||
var options = await _optionsFactory.CreateAsync(); |
|||
|
|||
Check.NotNull(options, nameof(options)); |
|||
|
|||
// 沙盒测试时,无需验证消息
|
|||
if (options.IsSandBox) |
|||
{ |
|||
return input.EchoStr; |
|||
} |
|||
|
|||
var echoData = new WeChatCryptoEchoData( |
|||
input.EchoStr, |
|||
options.AppId, |
|||
options.Token, |
|||
options.EncodingAESKey, |
|||
input.Signature, |
|||
input.TimeStamp.ToString(), |
|||
input.Nonce); |
|||
|
|||
var echoStr = _cryptoService.Validation(echoData); |
|||
|
|||
return echoStr; |
|||
} |
|||
|
|||
public async virtual Task<string> Handle(MessageHandleInput input) |
|||
{ |
|||
var options = await _optionsFactory.CreateAsync(); |
|||
|
|||
Check.NotNull(options, nameof(options)); |
|||
|
|||
var messageData = new MessageResolveData( |
|||
options.AppId, |
|||
options.Token, |
|||
options.EncodingAESKey, |
|||
input.Signature, |
|||
input.TimeStamp, |
|||
input.Nonce, |
|||
input.Data); |
|||
|
|||
var result = await _messageResolver.ResolveMessageAsync(messageData); |
|||
if (result.Message == null) |
|||
{ |
|||
Logger.LogWarning(input.Data); |
|||
Logger.LogWarning("解析消息失败, 无法处理微信消息."); |
|||
} |
|||
else |
|||
{ |
|||
Logger.LogInformation(result.Message.SerializeToXml()); |
|||
var eto = result.Message.ToEto(); |
|||
await _distributedEventBus.PublishAsync(eto.GetType(), eto); |
|||
} |
|||
// https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Passive_user_reply_message.html
|
|||
return "success"; |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,19 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net7.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.AspNetCore.Mvc" Version="$(VoloAbpPackageVersion)" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\LINGYUN.Abp.WeChat.Official.Application.Contracts\LINGYUN.Abp.WeChat.Official.Application.Contracts.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,19 @@ |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.Modularity; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official; |
|||
|
|||
[DependsOn( |
|||
typeof(AbpWeChatOfficialApplicationContractsModule), |
|||
typeof(AbpAspNetCoreMvcModule))] |
|||
public class AbpWeChatOfficialHttpApiModule : AbpModule |
|||
{ |
|||
public override void PreConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
PreConfigure<IMvcBuilder>(mvcBuilder => |
|||
{ |
|||
mvcBuilder.AddApplicationPartIfNotExists(typeof(AbpWeChatOfficialHttpApiModule).Assembly); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
using Volo.Abp.Content; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Account; |
|||
|
|||
[Controller] |
|||
[RemoteService(Name = AbpWeChatOfficialRemoteServiceConsts.RemoteServiceName)] |
|||
[Area(AbpWeChatOfficialRemoteServiceConsts.ModuleName)] |
|||
[Route("api/wechat/official/account/parametric-qrcode")] |
|||
public class ParametricQrCodeController : AbpControllerBase, IParametricQrCodeAppService |
|||
{ |
|||
private readonly IParametricQrCodeAppService _service; |
|||
|
|||
public ParametricQrCodeController(IParametricQrCodeAppService service) |
|||
{ |
|||
_service = service; |
|||
} |
|||
|
|||
[HttpPost] |
|||
[Route("generate")] |
|||
public virtual Task<IRemoteStreamContent> GenerateAsync(ParametricQrCodeGenerateInput input) |
|||
{ |
|||
return _service.GenerateAsync(input); |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using System.IO; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp; |
|||
using Volo.Abp.AspNetCore.Mvc; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Message; |
|||
|
|||
[Controller] |
|||
[RemoteService(Name = AbpWeChatOfficialRemoteServiceConsts.RemoteServiceName)] |
|||
[Area(AbpWeChatOfficialRemoteServiceConsts.ModuleName)] |
|||
[Route("api/wechat/official/messages")] |
|||
public class WeChatMessageController : AbpControllerBase, IWeChatMessageAppService |
|||
{ |
|||
private readonly IWeChatMessageAppService _service; |
|||
|
|||
public WeChatMessageController(IWeChatMessageAppService service) |
|||
{ |
|||
_service = service; |
|||
} |
|||
|
|||
[HttpGet] |
|||
public virtual Task<string> Handle([FromQuery] MessageValidationInput input) |
|||
{ |
|||
return _service.Handle(input); |
|||
} |
|||
|
|||
[HttpPost] |
|||
public async virtual Task<string> Handle([FromQuery] MessageHandleInput input) |
|||
{ |
|||
using var reader = new StreamReader(Request.Body, Encoding.UTF8); |
|||
var content = await reader.ReadToEndAsync(); |
|||
|
|||
input.Data = content; |
|||
|
|||
return await _service.Handle(input); |
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
namespace LINGYUN.Abp.WeChat.Official.Account.Enums; |
|||
public enum SceneEnum |
|||
{ |
|||
Login = 0, |
|||
Binding = 1, |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
using LINGYUN.Abp.WeChat.Official.Account.Models; |
|||
using System.IO; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Account; |
|||
/// <summary>
|
|||
/// 生成带参数的二维码接口
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 详情见: https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html
|
|||
/// </remarks>
|
|||
public interface IParametricQrCodeGenerator |
|||
{ |
|||
/// <summary>
|
|||
/// 创建二维码ticket
|
|||
/// </summary>
|
|||
/// <param name="model"></param>
|
|||
/// <param name="cancellationToken"></param>
|
|||
/// <returns></returns>
|
|||
Task<TicketModel> CreateTicketAsync(CreateTicketModel model, CancellationToken cancellationToken = default); |
|||
/// <summary>
|
|||
/// 通过ticket换取二维码
|
|||
/// </summary>
|
|||
/// <param name="ticket"></param>
|
|||
/// <param name="cancellationToken"></param>
|
|||
/// <returns></returns>
|
|||
Task<Stream> ShowQrCodeAsync(string ticket, CancellationToken cancellationToken = default); |
|||
} |
|||
@ -0,0 +1,97 @@ |
|||
using Newtonsoft.Json; |
|||
using System.Text.Json.Serialization; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Account.Models; |
|||
public class CreateTicketModel : WeChatRequest |
|||
{ |
|||
/// <summary>
|
|||
/// 该二维码有效时间,以秒为单位。 最大不超过2592000(即30天),此字段如果不填,则默认有效期为60秒。
|
|||
/// </summary>
|
|||
[JsonProperty("expire_seconds")] |
|||
[JsonPropertyName("expire_seconds")] |
|||
public int? ExpireSeconds { get; set; } |
|||
/// <summary>
|
|||
/// 二维码类型
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// <list type="table">
|
|||
/// <item>QR_SCENE为临时的整型参数值</item>
|
|||
/// <item>QR_STR_SCENE为临时的字符串参数值</item>
|
|||
/// <item>QR_LIMIT_SCENE为永久的整型参数值</item>
|
|||
/// <item>QR_LIMIT_STR_SCENE为永久的字符串参数值</item>
|
|||
/// </list>
|
|||
/// </remarks>
|
|||
[JsonProperty("action_name")] |
|||
[JsonPropertyName("action_name")] |
|||
public string ActionName { get; private set; } |
|||
/// <summary>
|
|||
/// 二维码详细信息
|
|||
/// </summary>
|
|||
[JsonProperty("action_info")] |
|||
[JsonPropertyName("action_info")] |
|||
public Scene SceneInfo { get; private set; } |
|||
private CreateTicketModel() |
|||
{ |
|||
|
|||
} |
|||
/// <summary>
|
|||
/// 通过场景值名称创建临时二维码ticket
|
|||
/// </summary>
|
|||
/// <param name="sceneStr">场景值名称</param>
|
|||
/// <param name="expireSeconds">二维码有效时间</param>
|
|||
/// <returns></returns>
|
|||
public static CreateTicketModel StringScene( |
|||
string sceneStr, |
|||
int expireSeconds = 60) |
|||
{ |
|||
return new CreateTicketModel |
|||
{ |
|||
ExpireSeconds = expireSeconds, |
|||
ActionName = "QR_STR_SCENE", |
|||
SceneInfo = new StringScene(sceneStr), |
|||
}; |
|||
} |
|||
/// <summary>
|
|||
/// 通过场景值名称创建永久二维码ticket
|
|||
/// </summary>
|
|||
/// <param name="sceneStr">场景值名称</param>
|
|||
/// <returns></returns>
|
|||
public static CreateTicketModel LimitStringScene(string sceneStr) |
|||
{ |
|||
return new CreateTicketModel |
|||
{ |
|||
ActionName = "QR_LIMIT_STR_SCENE", |
|||
SceneInfo = new StringScene(sceneStr), |
|||
}; |
|||
} |
|||
/// <summary>
|
|||
/// 通过场景值标识创建二维码ticket
|
|||
/// </summary>
|
|||
/// <param name="sceneId">场景值标识</param>
|
|||
/// <param name="expireSeconds">二维码有效时间</param>
|
|||
/// <returns></returns>
|
|||
public static CreateTicketModel EnumScene( |
|||
int sceneId, |
|||
int expireSeconds = 60) |
|||
{ |
|||
return new CreateTicketModel |
|||
{ |
|||
ExpireSeconds = expireSeconds, |
|||
ActionName = "QR_SCENE", |
|||
SceneInfo = new EnumScene(sceneId), |
|||
}; |
|||
} |
|||
/// <summary>
|
|||
/// 通过场景值标识创建永久二维码ticket
|
|||
/// </summary>
|
|||
/// <param name="sceneId">场景值标识</param>
|
|||
/// <returns></returns>
|
|||
public static CreateTicketModel LimitEnumScene(int sceneId) |
|||
{ |
|||
return new CreateTicketModel |
|||
{ |
|||
ActionName = "QR_LIMIT_SCENE", |
|||
SceneInfo = new EnumScene(sceneId), |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
using Newtonsoft.Json; |
|||
using System.Text.Json.Serialization; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Account.Models; |
|||
public class EnumScene : Scene |
|||
{ |
|||
/// <summary>
|
|||
/// 场景值ID,临时二维码时为32位非0整型,永久二维码时最大值为100000(目前参数只支持1--100000)
|
|||
/// </summary>
|
|||
[JsonProperty("scene_id")] |
|||
[JsonPropertyName("scene_id")] |
|||
public int SceneId { get; } |
|||
public EnumScene(int sceneId) |
|||
{ |
|||
SceneId = sceneId; |
|||
} |
|||
|
|||
public override string GetKey() |
|||
{ |
|||
return SceneId.ToString(); |
|||
} |
|||
} |
|||
@ -0,0 +1,5 @@ |
|||
namespace LINGYUN.Abp.WeChat.Official.Account.Models; |
|||
public abstract class Scene |
|||
{ |
|||
public abstract string GetKey(); |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
using Newtonsoft.Json; |
|||
using System.Text.Json.Serialization; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Account.Models; |
|||
public class StringScene : Scene |
|||
{ |
|||
/// <summary>
|
|||
/// 场景值ID(字符串形式的ID),字符串类型,长度限制为1到64
|
|||
/// </summary>
|
|||
[JsonProperty("scene_str")] |
|||
[JsonPropertyName("scene_str")] |
|||
public string SceneStr { get; } |
|||
public StringScene(string sceneStr) |
|||
{ |
|||
SceneStr = sceneStr; |
|||
} |
|||
|
|||
public override string GetKey() |
|||
{ |
|||
return SceneStr; |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
using Newtonsoft.Json; |
|||
using System.Text.Json.Serialization; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Account.Models; |
|||
public class TicketModel |
|||
{ |
|||
/// <summary>
|
|||
/// 获取的二维码ticket,凭借此ticket可以在有效时间内换取二维码
|
|||
/// </summary>
|
|||
[JsonProperty("ticket")] |
|||
[JsonPropertyName("ticket")] |
|||
public string Ticket { get; set; } |
|||
/// <summary>
|
|||
/// 该二维码有效时间,以秒为单位。 最大不超过2592000(即30天)。
|
|||
/// </summary>
|
|||
[JsonProperty("expire_seconds")] |
|||
[JsonPropertyName("expire_seconds")] |
|||
public int ExpireSeconds { get; set; } |
|||
/// <summary>
|
|||
/// 二维码图片解析后的地址,开发者可根据该地址自行生成需要的二维码图片
|
|||
/// </summary>
|
|||
[JsonProperty("url")] |
|||
[JsonPropertyName("url")] |
|||
public string Url { get; set; } |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
namespace LINGYUN.Abp.WeChat.Official.Account.Models; |
|||
public class TicketModelCacheItem |
|||
{ |
|||
public string Ticket { get; set; } |
|||
|
|||
public int ExpireSeconds { get; set; } |
|||
|
|||
public string Url { get; set; } |
|||
|
|||
public TicketModelCacheItem() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public TicketModelCacheItem(string ticket, int expireSeconds, string url) |
|||
{ |
|||
Ticket = ticket; |
|||
ExpireSeconds = expireSeconds; |
|||
Url = url; |
|||
} |
|||
|
|||
public static string CalculateCacheKey(string action, string scene) |
|||
{ |
|||
return "a:" + action + ";s:" + scene; |
|||
} |
|||
} |
|||
@ -0,0 +1,87 @@ |
|||
using LINGYUN.Abp.WeChat.Official.Account.Models; |
|||
using LINGYUN.Abp.WeChat.Token; |
|||
using Microsoft.Extensions.Caching.Distributed; |
|||
using Newtonsoft.Json; |
|||
using System; |
|||
using System.IO; |
|||
using System.Net.Http; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Caching; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Account; |
|||
public class ParametricQrCodeGenerator : IParametricQrCodeGenerator, ITransientDependency |
|||
{ |
|||
protected IHttpClientFactory HttpClientFactory { get; } |
|||
protected AbpWeChatOfficialOptionsFactory OfficialOptionsFactory { get; } |
|||
protected IWeChatTokenProvider WeChatTokenProvider { get; } |
|||
protected IDistributedCache<TicketModelCacheItem> TicketModelCache { get; } |
|||
public ParametricQrCodeGenerator( |
|||
IHttpClientFactory httpClientFactory, |
|||
IWeChatTokenProvider weChatTokenProvider, |
|||
AbpWeChatOfficialOptionsFactory officialOptionsFactory, |
|||
IDistributedCache<TicketModelCacheItem> ticketModelCache) |
|||
{ |
|||
TicketModelCache = ticketModelCache; |
|||
HttpClientFactory = httpClientFactory; |
|||
WeChatTokenProvider = weChatTokenProvider; |
|||
OfficialOptionsFactory = officialOptionsFactory; |
|||
} |
|||
|
|||
public async virtual Task<TicketModel> CreateTicketAsync(CreateTicketModel model, CancellationToken cancellationToken = default) |
|||
{ |
|||
var cacheItem = await GetOrCreateTicketModelCacheItem(model, cancellationToken); |
|||
|
|||
return new TicketModel |
|||
{ |
|||
ExpireSeconds = cacheItem.ExpireSeconds, |
|||
Ticket = cacheItem.Ticket, |
|||
Url = cacheItem.Url, |
|||
}; |
|||
} |
|||
|
|||
public async virtual Task<Stream> ShowQrCodeAsync(string ticket, CancellationToken cancellationToken = default) |
|||
{ |
|||
var client = HttpClientFactory.CreateClient(AbpWeChatOfficialConsts.HttpClient); |
|||
var response = await client.GetAsync($"/cgi-bin/showqrcode?ticket={ticket}", cancellationToken); |
|||
response.ThrowNotSuccessStatusCode(); |
|||
|
|||
return await response.Content.ReadAsStreamAsync(); |
|||
} |
|||
|
|||
protected async virtual Task<TicketModelCacheItem> GetOrCreateTicketModelCacheItem(CreateTicketModel model, CancellationToken cancellationToken = default) |
|||
{ |
|||
var cacheKey = TicketModelCacheItem.CalculateCacheKey(model.ActionName, model.SceneInfo.GetKey()); |
|||
var cacheItem = await TicketModelCache.GetAsync(cacheKey, token: cancellationToken); |
|||
if (cacheItem != null) |
|||
{ |
|||
return cacheItem; |
|||
} |
|||
|
|||
var options = await OfficialOptionsFactory.CreateAsync(); |
|||
|
|||
var token = await WeChatTokenProvider.GetTokenAsync(options.AppId, options.AppSecret, cancellationToken); |
|||
|
|||
var client = HttpClientFactory.CreateClient(AbpWeChatGlobalConsts.HttpClient); |
|||
var response = await client.PostAsync( |
|||
$"/cgi-bin/qrcode/create?access_token={token.AccessToken}", |
|||
new StringContent(model.SerializeToJson())); |
|||
response.ThrowNotSuccessStatusCode(); |
|||
|
|||
var responseContent = await response.Content.ReadAsStringAsync(); |
|||
var ticketModel = JsonConvert.DeserializeObject<TicketModel>(responseContent); |
|||
|
|||
cacheItem = new TicketModelCacheItem(ticketModel.Ticket, ticketModel.ExpireSeconds, ticketModel.Url); |
|||
|
|||
var cacheOptions = new DistributedCacheEntryOptions |
|||
{ |
|||
// 设置绝对过期时间为Token有效期剩余的二分钟
|
|||
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(ticketModel.ExpireSeconds) |
|||
}; |
|||
|
|||
await TicketModelCache.SetAsync(cacheKey, cacheItem, cacheOptions, token: cancellationToken); |
|||
|
|||
return cacheItem; |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Messages; |
|||
public class AbpWeChatOfficialMessageResolveOptions |
|||
{ |
|||
public IDictionary<string, Func<IMessageResolveContext, WeChatEventMessage>> EventMaps { get; } |
|||
public IDictionary<string, Func<IMessageResolveContext, WeChatGeneralMessage>> MessageMaps { get; } |
|||
public AbpWeChatOfficialMessageResolveOptions() |
|||
{ |
|||
EventMaps = new Dictionary<string, Func<IMessageResolveContext, WeChatEventMessage>>(); |
|||
MessageMaps = new Dictionary<string, Func<IMessageResolveContext, WeChatGeneralMessage>>(); |
|||
} |
|||
|
|||
public void MapEvent(string eventName, Func<IMessageResolveContext, WeChatEventMessage> mapFunc) |
|||
{ |
|||
EventMaps[eventName] = mapFunc; |
|||
} |
|||
|
|||
public void MapMessage(string messageType, Func<IMessageResolveContext, WeChatGeneralMessage> mapFunc) |
|||
{ |
|||
MessageMaps[messageType] = mapFunc; |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages.Handlers; |
|||
using LINGYUN.Abp.WeChat.Official.Messages.Models; |
|||
using LINGYUN.Abp.WeChat.Official.Services; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Messages.Handlers; |
|||
/// <summary>
|
|||
/// 文本消息客服回复
|
|||
/// </summary>
|
|||
public class TextMessageReplyContributor : IMessageHandleContributor<TextMessage> |
|||
{ |
|||
public async virtual Task HandleAsync(MessageHandleContext<TextMessage> context) |
|||
{ |
|||
var messageSender = context.ServiceProvider.GetRequiredService<IServiceCenterMessageSender>(); |
|||
|
|||
await messageSender.SendAsync( |
|||
new Services.Models.TextMessageModel( |
|||
context.Message.FromUserName, |
|||
new Services.Models.TextMessage( |
|||
context.Message.Content))); |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages.Handlers; |
|||
using LINGYUN.Abp.WeChat.Official.Messages.Models; |
|||
using LINGYUN.Abp.WeChat.Official.Services; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Messages.Handlers; |
|||
/// <summary>
|
|||
/// 用户关注回复消息
|
|||
/// </summary>
|
|||
public class UserSubscribeEventContributor : IEventHandleContributor<UserSubscribeEvent> |
|||
{ |
|||
public async virtual Task HandleAsync(MessageHandleContext<UserSubscribeEvent> context) |
|||
{ |
|||
var messageSender = context.ServiceProvider.GetRequiredService<IServiceCenterMessageSender>(); |
|||
|
|||
await messageSender.SendAsync( |
|||
new Services.Models.TextMessageModel( |
|||
context.Message.FromUserName, |
|||
new Services.Models.TextMessage( |
|||
"感谢您的关注, 点击菜单了解更多."))); |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using LINGYUN.Abp.WeChat.Common.Messages.Handlers; |
|||
using LINGYUN.Abp.WeChat.Official.Messages.Models; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Messages.Handlers; |
|||
public class WeChatOfficialEventEventHandler : |
|||
IDistributedEventHandler<WeChatOfficialEventMessageEto<CustomMenuEvent>>, |
|||
IDistributedEventHandler<WeChatOfficialEventMessageEto<UserSubscribeEvent>>, |
|||
IDistributedEventHandler<WeChatOfficialEventMessageEto<UserUnSubscribeEvent>>, |
|||
IDistributedEventHandler<WeChatOfficialEventMessageEto<ParametricQrCodeEvent>>, |
|||
IDistributedEventHandler<WeChatOfficialEventMessageEto<MenuClickJumpLinkEvent>>, |
|||
IDistributedEventHandler<WeChatOfficialEventMessageEto<ReportingGeoLocationEvent>>, |
|||
ITransientDependency |
|||
{ |
|||
private readonly IMessageHandler _messageHandler; |
|||
|
|||
public WeChatOfficialEventEventHandler(IMessageHandler messageHandler) |
|||
{ |
|||
_messageHandler = messageHandler; |
|||
} |
|||
|
|||
public async virtual Task HandleEventAsync(WeChatOfficialEventMessageEto<CustomMenuEvent> eventData) |
|||
{ |
|||
await _messageHandler.HandleEventAsync(eventData.Event); |
|||
} |
|||
|
|||
public async virtual Task HandleEventAsync(WeChatOfficialEventMessageEto<UserSubscribeEvent> eventData) |
|||
{ |
|||
await _messageHandler.HandleEventAsync(eventData.Event); |
|||
} |
|||
|
|||
public async virtual Task HandleEventAsync(WeChatOfficialEventMessageEto<UserUnSubscribeEvent> eventData) |
|||
{ |
|||
await _messageHandler.HandleEventAsync(eventData.Event); |
|||
} |
|||
|
|||
public async virtual Task HandleEventAsync(WeChatOfficialEventMessageEto<ParametricQrCodeEvent> eventData) |
|||
{ |
|||
await _messageHandler.HandleEventAsync(eventData.Event); |
|||
} |
|||
|
|||
public async virtual Task HandleEventAsync(WeChatOfficialEventMessageEto<MenuClickJumpLinkEvent> eventData) |
|||
{ |
|||
await _messageHandler.HandleEventAsync(eventData.Event); |
|||
} |
|||
|
|||
public async virtual Task HandleEventAsync(WeChatOfficialEventMessageEto<ReportingGeoLocationEvent> eventData) |
|||
{ |
|||
await _messageHandler.HandleEventAsync(eventData.Event); |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using LINGYUN.Abp.WeChat.Common.Messages.Handlers; |
|||
using LINGYUN.Abp.WeChat.Official.Messages.Models; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.EventBus.Distributed; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Messages.Handlers; |
|||
public class WeChatOfficialMessageEventHandler : |
|||
IDistributedEventHandler<WeChatOfficialGeneralMessageEto<TextMessage>>, |
|||
IDistributedEventHandler<WeChatOfficialGeneralMessageEto<LinkMessage>>, |
|||
IDistributedEventHandler<WeChatOfficialGeneralMessageEto<VoiceMessage>>, |
|||
IDistributedEventHandler<WeChatOfficialGeneralMessageEto<VideoMessage>>, |
|||
IDistributedEventHandler<WeChatOfficialGeneralMessageEto<PictureMessage>>, |
|||
IDistributedEventHandler<WeChatOfficialGeneralMessageEto<GeoLocationMessage>>, |
|||
ITransientDependency |
|||
{ |
|||
private readonly IMessageHandler _messageHandler; |
|||
|
|||
public WeChatOfficialMessageEventHandler(IMessageHandler messageHandler) |
|||
{ |
|||
_messageHandler = messageHandler; |
|||
} |
|||
|
|||
public async virtual Task HandleEventAsync(WeChatOfficialGeneralMessageEto<TextMessage> eventData) |
|||
{ |
|||
await _messageHandler.HandleMessageAsync(eventData.Message); |
|||
} |
|||
|
|||
public async virtual Task HandleEventAsync(WeChatOfficialGeneralMessageEto<LinkMessage> eventData) |
|||
{ |
|||
await _messageHandler.HandleMessageAsync(eventData.Message); |
|||
} |
|||
|
|||
public async virtual Task HandleEventAsync(WeChatOfficialGeneralMessageEto<VoiceMessage> eventData) |
|||
{ |
|||
await _messageHandler.HandleMessageAsync(eventData.Message); |
|||
} |
|||
|
|||
public async virtual Task HandleEventAsync(WeChatOfficialGeneralMessageEto<VideoMessage> eventData) |
|||
{ |
|||
await _messageHandler.HandleMessageAsync(eventData.Message); |
|||
} |
|||
|
|||
public async virtual Task HandleEventAsync(WeChatOfficialGeneralMessageEto<PictureMessage> eventData) |
|||
{ |
|||
await _messageHandler.HandleMessageAsync(eventData.Message); |
|||
} |
|||
|
|||
public async virtual Task HandleEventAsync(WeChatOfficialGeneralMessageEto<GeoLocationMessage> eventData) |
|||
{ |
|||
await _messageHandler.HandleMessageAsync(eventData.Message); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using System.Xml.Serialization; |
|||
using Volo.Abp.EventBus; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Messages.Models; |
|||
/// <summary>
|
|||
/// 自定义菜单事件
|
|||
/// </summary>
|
|||
[EventName("custom_menu")] |
|||
public class CustomMenuEvent : WeChatEventMessage |
|||
{ |
|||
/// <summary>
|
|||
/// 事件KEY值
|
|||
/// </summary>
|
|||
[XmlElement("EventKey")] |
|||
public string EventKey { get; set; } |
|||
|
|||
public override WeChatMessageEto ToEto() |
|||
{ |
|||
return new WeChatOfficialEventMessageEto<CustomMenuEvent>(this); |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using System.Xml.Serialization; |
|||
using Volo.Abp.EventBus; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Messages.Models; |
|||
/// <summary>
|
|||
/// 地理位置消息
|
|||
/// </summary>
|
|||
[EventName("geo_location")] |
|||
public class GeoLocationMessage : WeChatOfficialGeneralMessage |
|||
{ |
|||
/// <summary>
|
|||
/// 地理位置纬度
|
|||
/// </summary>
|
|||
[XmlElement("Location_X")] |
|||
public double Latitude { get; set; } |
|||
/// <summary>
|
|||
/// 地理位置经度
|
|||
/// </summary>
|
|||
[XmlElement("Location_Y")] |
|||
public double Longitude { get; set; } |
|||
/// <summary>
|
|||
/// 地图缩放大小
|
|||
/// </summary>
|
|||
[XmlElement("Scale")] |
|||
public double Scale { get; set; } |
|||
/// <summary>
|
|||
/// 地理位置信息
|
|||
/// </summary>
|
|||
[XmlElement("Label")] |
|||
public string Label { get; set; } |
|||
public override WeChatMessageEto ToEto() |
|||
{ |
|||
return new WeChatOfficialGeneralMessageEto<GeoLocationMessage>(this); |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using System.Xml.Serialization; |
|||
using Volo.Abp.EventBus; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Messages.Models; |
|||
/// <summary>
|
|||
/// 链接消息
|
|||
/// </summary>
|
|||
[EventName("link")] |
|||
public class LinkMessage : WeChatOfficialGeneralMessage |
|||
{ |
|||
/// <summary>
|
|||
/// 消息标题
|
|||
/// </summary>
|
|||
[XmlElement("Title")] |
|||
public string Title { get; set; } |
|||
/// <summary>
|
|||
/// 消息描述
|
|||
/// </summary>
|
|||
[XmlElement("Description")] |
|||
public string Description { get; set; } |
|||
/// <summary>
|
|||
/// 消息链接
|
|||
/// </summary>
|
|||
[XmlElement("Url")] |
|||
public string Url { get; set; } |
|||
public override WeChatMessageEto ToEto() |
|||
{ |
|||
return new WeChatOfficialGeneralMessageEto<LinkMessage>(this); |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using System.Xml.Serialization; |
|||
using Volo.Abp.EventBus; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Messages.Models; |
|||
/// <summary>
|
|||
/// 点击菜单跳转链接时的事件推送
|
|||
/// </summary>
|
|||
[EventName("menu_click_jump_link")] |
|||
public class MenuClickJumpLinkEvent : WeChatEventMessage |
|||
{ |
|||
/// <summary>
|
|||
/// 事件KEY值
|
|||
/// </summary>
|
|||
[XmlElement("EventKey")] |
|||
public string EventKey { get; set; } |
|||
public override WeChatMessageEto ToEto() |
|||
{ |
|||
return new WeChatOfficialEventMessageEto<MenuClickJumpLinkEvent>(this); |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using System.Xml.Serialization; |
|||
using Volo.Abp.EventBus; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Messages.Models; |
|||
/// <summary>
|
|||
/// 扫描带参数二维码事件
|
|||
/// </summary>
|
|||
[EventName("parametric_qr_code")] |
|||
public class ParametricQrCodeEvent : WeChatEventMessage |
|||
{ |
|||
/// <summary>
|
|||
/// 事件KEY值
|
|||
/// </summary>
|
|||
[XmlElement("EventKey")] |
|||
public string EventKey { get; set; } |
|||
/// <summary>
|
|||
/// 二维码的ticket,可用来换取二维码图片
|
|||
/// </summary>
|
|||
[XmlElement("Ticket")] |
|||
public string Ticket { get; set; } |
|||
public override WeChatMessageEto ToEto() |
|||
{ |
|||
return new WeChatOfficialEventMessageEto<ParametricQrCodeEvent>(this); |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using System.Xml.Serialization; |
|||
using Volo.Abp.EventBus; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Messages.Models; |
|||
/// <summary>
|
|||
/// 图片消息
|
|||
/// </summary>
|
|||
[EventName("picture")] |
|||
public class PictureMessage : WeChatOfficialGeneralMessage |
|||
{ |
|||
/// <summary>
|
|||
/// 图片链接(由系统生成)
|
|||
/// </summary>
|
|||
[XmlElement("PicUrl")] |
|||
public string PicUrl { get; set; } |
|||
/// <summary>
|
|||
/// 图片消息媒体id,可以调用获取临时素材接口拉取数据。
|
|||
/// </summary>
|
|||
[XmlElement("MediaId")] |
|||
public string MediaId { get; set; } |
|||
public override WeChatMessageEto ToEto() |
|||
{ |
|||
return new WeChatOfficialGeneralMessageEto<PictureMessage>(this); |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using System.Xml.Serialization; |
|||
using Volo.Abp.EventBus; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Messages.Models; |
|||
/// <summary>
|
|||
/// 上报地理位置事件
|
|||
/// </summary>
|
|||
[EventName("reporting_geo_location")] |
|||
public class ReportingGeoLocationEvent : WeChatEventMessage |
|||
{ |
|||
/// <summary>
|
|||
/// 地理位置纬度
|
|||
/// </summary>
|
|||
[XmlElement("Latitude")] |
|||
public double Latitude { get; set; } |
|||
/// <summary>
|
|||
/// 地理位置经度
|
|||
/// </summary>
|
|||
[XmlElement("Longitude")] |
|||
public double Longitude { get; set; } |
|||
/// <summary>
|
|||
/// 地理位置精度
|
|||
/// </summary>
|
|||
[XmlElement("Precision")] |
|||
public double Precision { get; set; } |
|||
public override WeChatMessageEto ToEto() |
|||
{ |
|||
return new WeChatOfficialEventMessageEto<ReportingGeoLocationEvent>(this); |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using System.Xml.Serialization; |
|||
using Volo.Abp.EventBus; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Messages.Models; |
|||
/// <summary>
|
|||
/// 文本消息
|
|||
/// </summary>
|
|||
[EventName("text")] |
|||
public class TextMessage : WeChatOfficialGeneralMessage |
|||
{ |
|||
/// <summary>
|
|||
/// 文本消息内容
|
|||
/// </summary>
|
|||
[XmlElement("Content")] |
|||
public string Content { get; set; } |
|||
public override WeChatMessageEto ToEto() |
|||
{ |
|||
return new WeChatOfficialGeneralMessageEto<TextMessage>(this); |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using Volo.Abp.EventBus; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Messages.Models; |
|||
/// <summary>
|
|||
/// 用户关注事件
|
|||
/// </summary>
|
|||
[EventName("user_subscribe")] |
|||
public class UserSubscribeEvent : WeChatEventMessage |
|||
{ |
|||
public override WeChatMessageEto ToEto() |
|||
{ |
|||
return new WeChatOfficialEventMessageEto<UserSubscribeEvent>(this); |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using Volo.Abp.EventBus; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Messages.Models; |
|||
/// <summary>
|
|||
/// 用户取消关注事件
|
|||
/// </summary>
|
|||
[EventName("user_un_subscribe")] |
|||
public class UserUnSubscribeEvent : WeChatEventMessage |
|||
{ |
|||
public override WeChatMessageEto ToEto() |
|||
{ |
|||
return new WeChatOfficialEventMessageEto<UserUnSubscribeEvent>(this); |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using System.Xml.Serialization; |
|||
using Volo.Abp.EventBus; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Messages.Models; |
|||
/// <summary>
|
|||
/// 视频消息
|
|||
/// </summary>
|
|||
[EventName("video")] |
|||
public class VideoMessage : WeChatOfficialGeneralMessage |
|||
{ |
|||
/// <summary>
|
|||
/// 视频消息缩略图的媒体id,可以调用多媒体文件下载接口拉取数据。
|
|||
/// </summary>
|
|||
[XmlElement("ThumbMediaId")] |
|||
public string ThumbMediaId { get; set; } |
|||
/// <summary>
|
|||
/// 视频消息媒体id,可以调用获取临时素材接口拉取数据。
|
|||
/// </summary>
|
|||
[XmlElement("MediaId")] |
|||
public string MediaId { get; set; } |
|||
public override WeChatMessageEto ToEto() |
|||
{ |
|||
return new WeChatOfficialGeneralMessageEto<VideoMessage>(this); |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using System.Xml.Serialization; |
|||
using Volo.Abp.EventBus; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Messages.Models; |
|||
/// <summary>
|
|||
/// 语音消息
|
|||
/// </summary>
|
|||
[EventName("voice")] |
|||
public class VoiceMessage : WeChatOfficialGeneralMessage |
|||
{ |
|||
/// <summary>
|
|||
/// 语音格式,如amr,speex等
|
|||
/// </summary>
|
|||
[XmlElement("Format")] |
|||
public string Format { get; set; } |
|||
/// <summary>
|
|||
/// 语音识别结果,UTF8编码
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// 开通语音识别后,用户每次发送语音给公众号时,微信会在推送的语音消息XML数据包中,增加一个Recognition字段(
|
|||
/// 注:由于客户端缓存,开发者开启或者关闭语音识别功能,对新关注者立刻生效,对已关注用户需要24小时生效。开发者可以重新关注此账号进行测试)。
|
|||
/// </remarks>
|
|||
[XmlElement("Recognition")] |
|||
public string Recognition { get; set; } |
|||
/// <summary>
|
|||
/// 语音消息媒体id,可以调用获取临时素材接口拉取该媒体
|
|||
/// </summary>
|
|||
[XmlElement("MediaId")] |
|||
public string MediaId { get; set; } |
|||
public override WeChatMessageEto ToEto() |
|||
{ |
|||
return new WeChatOfficialGeneralMessageEto<VoiceMessage>(this); |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using Volo.Abp.EventBus; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Messages; |
|||
|
|||
[GenericEventName(Prefix = "wechat.official.events")] |
|||
public class WeChatOfficialEventMessageEto<TEvent> : WeChatMessageEto |
|||
where TEvent : WeChatEventMessage |
|||
{ |
|||
public TEvent Event { get; set; } |
|||
public WeChatOfficialEventMessageEto() |
|||
{ |
|||
|
|||
} |
|||
public WeChatOfficialEventMessageEto(TEvent @event) |
|||
{ |
|||
Event = @event; |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
using LINGYUN.Abp.WeChat.Common.Messages; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Microsoft.Extensions.Options; |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.WeChat.Official.Messages; |
|||
/// <summary>
|
|||
/// 微信公众号事件处理器
|
|||
/// </summary>
|
|||
public class WeChatOfficialEventResolveContributor : MessageResolveContributorBase |
|||
{ |
|||
public override string Name => "WeChat.Official.Event"; |
|||
|
|||
public override Task ResolveAsync(IMessageResolveContext context) |
|||
{ |
|||
var options = context.ServiceProvider.GetRequiredService<IOptions<AbpWeChatOfficialMessageResolveOptions>>().Value; |
|||
var messageType = context.GetMessageData("MsgType"); |
|||
var eventName = context.GetMessageData("Event"); |
|||
if ("event".Equals(messageType, StringComparison.InvariantCultureIgnoreCase) && |
|||
!eventName.IsNullOrWhiteSpace() && |
|||
options.EventMaps.TryGetValue(eventName, out var eventFactory)) |
|||
{ |
|||
context.Message = eventFactory(context); |
|||
context.Handled = true; |
|||
} |
|||
return Task.CompletedTask; |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue