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>
|
/// <summary>
|
||||
/// 微信认证身份类型,可以像 <see cref="Volo.Abp.Security.Claims.AbpClaimTypes"/> 自行配置
|
/// 微信认证身份类型,可以像 <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