diff --git a/aspnet-core/LINGYUN.MicroService.Common.sln b/aspnet-core/LINGYUN.MicroService.Common.sln index 6ea7b8d2d..0a312b43c 100644 --- a/aspnet-core/LINGYUN.MicroService.Common.sln +++ b/aspnet-core/LINGYUN.MicroService.Common.sln @@ -240,15 +240,25 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.DistributedLock EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pushplus", "pushplus", "{0F5A2591-CE08-4184-A5F3-89F6FB3B2B10}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.PushPlus", "modules\pushplus\LINGYUN.Abp.PushPlus\LINGYUN.Abp.PushPlus.csproj", "{5515C7CA-B512-4E36-A202-49A0158A0E74}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.PushPlus", "modules\pushplus\LINGYUN.Abp.PushPlus\LINGYUN.Abp.PushPlus.csproj", "{5515C7CA-B512-4E36-A202-49A0158A0E74}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.Notifications.PushPlus", "modules\pushplus\LINGYUN.Abp.Notifications.PushPlus\LINGYUN.Abp.Notifications.PushPlus.csproj", "{EBA67EAD-4958-46E3-9E0C-8186394D083F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Notifications.PushPlus", "modules\pushplus\LINGYUN.Abp.Notifications.PushPlus\LINGYUN.Abp.Notifications.PushPlus.csproj", "{EBA67EAD-4958-46E3-9E0C-8186394D083F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Notifications.Emailing", "modules\common\LINGYUN.Abp.Notifications.Emailing\LINGYUN.Abp.Notifications.Emailing.csproj", "{25891EE2-3166-420F-8408-E458030C4643}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Notifications.Common", "modules\common\LINGYUN.Abp.Notifications.Common\LINGYUN.Abp.Notifications.Common.csproj", "{F051C960-AA61-4283-A088-611C0B96C953}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.PushPlus.Tests", "tests\LINGYUN.Abp.PushPlus.Tests\LINGYUN.Abp.PushPlus.Tests.csproj", "{1435711B-D796-42AB-B567-0BB23F02EE08}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.PushPlus.Tests", "tests\LINGYUN.Abp.PushPlus.Tests\LINGYUN.Abp.PushPlus.Tests.csproj", "{1435711B-D796-42AB-B567-0BB23F02EE08}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "wx-pusher", "wx-pusher", "{7862CE70-76EF-4228-A703-C2E2A9704D14}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.WxPusher", "modules\wx-pusher\LINGYUN.Abp.WxPusher\LINGYUN.Abp.WxPusher.csproj", "{1A072FF5-1A7E-4F78-B145-1AB873AEB8FF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.WxPusher.Tests", "tests\LINGYUN.Abp.WxPusher.Tests\LINGYUN.Abp.WxPusher.Tests.csproj", "{88412E3D-21C8-4FF1-8EB3-84CB74094336}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.Notifications.WxPusher", "modules\wx-pusher\LINGYUN.Abp.Notifications.WxPusher\LINGYUN.Abp.Notifications.WxPusher.csproj", "{F65A8835-C50F-43B0-B54C-196A92E9539F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.Identity.WxPusher", "modules\wx-pusher\LINGYUN.Abp.Identity.WxPusher\LINGYUN.Abp.Identity.WxPusher.csproj", "{30FA01ED-921A-4E7D-9E83-6719538FB866}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -640,6 +650,22 @@ Global {1435711B-D796-42AB-B567-0BB23F02EE08}.Debug|Any CPU.Build.0 = Debug|Any CPU {1435711B-D796-42AB-B567-0BB23F02EE08}.Release|Any CPU.ActiveCfg = Release|Any CPU {1435711B-D796-42AB-B567-0BB23F02EE08}.Release|Any CPU.Build.0 = Release|Any CPU + {1A072FF5-1A7E-4F78-B145-1AB873AEB8FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A072FF5-1A7E-4F78-B145-1AB873AEB8FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A072FF5-1A7E-4F78-B145-1AB873AEB8FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A072FF5-1A7E-4F78-B145-1AB873AEB8FF}.Release|Any CPU.Build.0 = Release|Any CPU + {88412E3D-21C8-4FF1-8EB3-84CB74094336}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88412E3D-21C8-4FF1-8EB3-84CB74094336}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88412E3D-21C8-4FF1-8EB3-84CB74094336}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88412E3D-21C8-4FF1-8EB3-84CB74094336}.Release|Any CPU.Build.0 = Release|Any CPU + {F65A8835-C50F-43B0-B54C-196A92E9539F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F65A8835-C50F-43B0-B54C-196A92E9539F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F65A8835-C50F-43B0-B54C-196A92E9539F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F65A8835-C50F-43B0-B54C-196A92E9539F}.Release|Any CPU.Build.0 = Release|Any CPU + {30FA01ED-921A-4E7D-9E83-6719538FB866}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30FA01ED-921A-4E7D-9E83-6719538FB866}.Debug|Any CPU.Build.0 = Debug|Any CPU + {30FA01ED-921A-4E7D-9E83-6719538FB866}.Release|Any CPU.ActiveCfg = Release|Any CPU + {30FA01ED-921A-4E7D-9E83-6719538FB866}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -764,6 +790,11 @@ Global {25891EE2-3166-420F-8408-E458030C4643} = {B91F26C5-B148-4094-B5F1-71E5F945DBED} {F051C960-AA61-4283-A088-611C0B96C953} = {B91F26C5-B148-4094-B5F1-71E5F945DBED} {1435711B-D796-42AB-B567-0BB23F02EE08} = {B86C21A4-73B7-471E-B73A-B4B905EC9435} + {7862CE70-76EF-4228-A703-C2E2A9704D14} = {02EA4E78-5891-43BC-944F-3E52FEE032E4} + {1A072FF5-1A7E-4F78-B145-1AB873AEB8FF} = {7862CE70-76EF-4228-A703-C2E2A9704D14} + {88412E3D-21C8-4FF1-8EB3-84CB74094336} = {B86C21A4-73B7-471E-B73A-B4B905EC9435} + {F65A8835-C50F-43B0-B54C-196A92E9539F} = {7862CE70-76EF-4228-A703-C2E2A9704D14} + {30FA01ED-921A-4E7D-9E83-6719538FB866} = {7862CE70-76EF-4228-A703-C2E2A9704D14} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {06C707C6-02C0-411A-AD3B-2D0E13787CB8} diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN.Abp.Notifications.PushPlus.csproj b/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN.Abp.Notifications.PushPlus.csproj index 043ae1b8c..0441362cb 100644 --- a/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN.Abp.Notifications.PushPlus.csproj +++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN.Abp.Notifications.PushPlus.csproj @@ -8,10 +8,6 @@ - - - - diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/AbpPushPlusModule.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/AbpPushPlusModule.cs index e04247263..8de9e44f1 100644 --- a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/AbpPushPlusModule.cs +++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/AbpPushPlusModule.cs @@ -1,5 +1,6 @@ using LINGYUN.Abp.Features.LimitValidation; using LINGYUN.Abp.PushPlus.Channel.Webhook; +using LINGYUN.Abp.PushPlus.Localization; using LINGYUN.Abp.PushPlus.Message; using LINGYUN.Abp.PushPlus.Setting; using LINGYUN.Abp.PushPlus.Token; @@ -9,8 +10,10 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Caching; using Volo.Abp.Json; using Volo.Abp.Json.SystemTextJson; +using Volo.Abp.Localization; using Volo.Abp.Modularity; using Volo.Abp.Settings; +using Volo.Abp.VirtualFileSystem; namespace LINGYUN.Abp.PushPlus; @@ -49,5 +52,17 @@ public class AbpPushPlusModule : AbpModule options.UnsupportedTypes.TryAdd>(); options.UnsupportedTypes.TryAdd>(); }); + + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + Configure(options => + { + options.Resources + .Add() + .AddVirtualJson("/LINGYUN/Abp/PushPlus/Localization/Resources"); + }); } } diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Identity.WxPusher/LINGYUN.Abp.Identity.WxPusher.csproj b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Identity.WxPusher/LINGYUN.Abp.Identity.WxPusher.csproj new file mode 100644 index 000000000..6b874b5ad --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Identity.WxPusher/LINGYUN.Abp.Identity.WxPusher.csproj @@ -0,0 +1,20 @@ + + + + + + + netstandard2.1 + + + + + + + + + + + + + diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Identity.WxPusher/LINGYUN/Abp/Identity/WxPusher/AbpIdentityWxPusherModule.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Identity.WxPusher/LINGYUN/Abp/Identity/WxPusher/AbpIdentityWxPusherModule.cs new file mode 100644 index 000000000..d15685aef --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Identity.WxPusher/LINGYUN/Abp/Identity/WxPusher/AbpIdentityWxPusherModule.cs @@ -0,0 +1,12 @@ +using LINGYUN.Abp.WxPusher; +using Volo.Abp.Identity; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.Identity.WxPusher; + +[DependsOn( + typeof(AbpWxPusherModule), + typeof(AbpIdentityDomainModule))] +public class AbpIdentityWxPusherModule : AbpModule +{ +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Identity.WxPusher/LINGYUN/Abp/Identity/WxPusher/User/IdentityWxPusherUserStore.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Identity.WxPusher/LINGYUN/Abp/Identity/WxPusher/User/IdentityWxPusherUserStore.cs new file mode 100644 index 000000000..c55d763e9 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Identity.WxPusher/LINGYUN/Abp/Identity/WxPusher/User/IdentityWxPusherUserStore.cs @@ -0,0 +1,46 @@ +using LINGYUN.Abp.WxPusher.Security.Claims; +using LINGYUN.Abp.WxPusher.User; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Identity; + +namespace LINGYUN.Abp.Identity.WxPusher.User; + +[Dependency(ServiceLifetime.Transient, ReplaceServices = true)] +[ExposeServices(typeof(IWxPusherUserStore))] +public class IdentityWxPusherUserStore : IWxPusherUserStore +{ + protected IdentityUserManager UserManager { get; } + + public IdentityWxPusherUserStore(IdentityUserManager userManager) + { + UserManager = userManager; + } + + public async virtual Task> GetSubscribeTopicsAsync(IEnumerable userIds, CancellationToken cancellationToken = default) + { + var topics = new List(); + + foreach (var userId in userIds) + { + var user = await UserManager.FindByIdAsync(userId.ToString()); + + var userUidClaim = user?.Claims + .Where(c => c.ClaimType.Equals(AbpWxPusherClaimTypes.Uid)) + .FirstOrDefault(); + + if (userUidClaim != null && + int.TryParse(userUidClaim.ClaimValue, out var topic)) + { + topics.Add(topic); + } + } + + return topics.Distinct().ToList(); + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Identity.WxPusher/README.md b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Identity.WxPusher/README.md new file mode 100644 index 000000000..ead577339 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Identity.WxPusher/README.md @@ -0,0 +1,13 @@ +# LINGYUN.Abp.Identity.WxPusher + +IWxPusherUserStore 接口的Identity模块实现, 通过用户Claims来获取关注的topic列表 + +## 模块引用 + +```csharp +[DependsOn(typeof(AbpIdentityWxPusherModule))] +public class YouProjectModule : AbpModule +{ + // other +} +``` diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN.Abp.Notifications.WxPusher.csproj b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN.Abp.Notifications.WxPusher.csproj new file mode 100644 index 000000000..9b7dceb90 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN.Abp.Notifications.WxPusher.csproj @@ -0,0 +1,16 @@ + + + + + + + netstandard2.0 + + + + + + + + + diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/NotificationDefinitionExtensions.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/NotificationDefinitionExtensions.cs new file mode 100644 index 000000000..55b89346d --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/NotificationDefinitionExtensions.cs @@ -0,0 +1,74 @@ +using LINGYUN.Abp.WxPusher.Messages; +using System.Collections.Generic; + +namespace LINGYUN.Abp.Notifications; + +public static class NotificationDefinitionExtensions +{ + private const string Prefix = "wx-pusher:"; + private const string ContentTypeKey = Prefix + "contentType"; + private const string TopicKey = Prefix + "contentType"; + /// + /// 设定消息内容类型 + /// + /// + /// + /// + public static NotificationDefinition WithContentType( + this NotificationDefinition notification, + MessageContentType contentType) + { + return notification.WithProperty(ContentTypeKey, contentType); + } + /// + /// 获取消息发送通道 + /// + /// + /// + /// + public static MessageContentType GetContentTypeOrDefault( + this NotificationDefinition notification, + MessageContentType defaultContentType = MessageContentType.Text) + { + if (notification.Properties.TryGetValue(ContentTypeKey, out var defineContentType) == true && + defineContentType is MessageContentType contentType) + { + return contentType; + } + + return defaultContentType; + } + /// + /// 消息主题(Topic) + /// see: https://wxpusher.dingliqc.com/docs/#/?id=%e5%90%8d%e8%af%8d%e8%a7%a3%e9%87%8a + /// + /// 群组编码 + /// + /// + /// + /// + public static NotificationDefinition WithTopics( + this NotificationDefinition notification, + List topics) + { + return notification.WithProperty(TopicKey, topics); + } + /// + /// 获取消息群发群组编码 + /// + /// + /// + /// 通知定义的群组编码列表 + /// + public static List GetTopics( + this NotificationDefinition notification) + { + if (notification.Properties.TryGetValue(TopicKey, out var topicsDefine) == true && + topicsDefine is List topics) + { + return topics; + } + + return new List(); + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/WxPusher/AbpNotificationsWxPusherModule.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/WxPusher/AbpNotificationsWxPusherModule.cs new file mode 100644 index 000000000..794e15038 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/WxPusher/AbpNotificationsWxPusherModule.cs @@ -0,0 +1,18 @@ +using LINGYUN.Abp.WxPusher; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.Notifications.WxPusher; + +[DependsOn( + typeof(AbpNotificationModule), + typeof(AbpWxPusherModule))] +public class AbpNotificationsPushPlusModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.PublishProviders.Add(); + }); + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/WxPusher/WxPusherNotificationPublishProvider.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/WxPusher/WxPusherNotificationPublishProvider.cs new file mode 100644 index 000000000..a9bf27a0a --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/LINGYUN/Abp/Notifications/WxPusher/WxPusherNotificationPublishProvider.cs @@ -0,0 +1,100 @@ +using LINGYUN.Abp.RealTime.Localization; +using LINGYUN.Abp.WxPusher.Messages; +using LINGYUN.Abp.WxPusher.User; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Localization; + +namespace LINGYUN.Abp.Notifications.WxPusher; + +public class WxPusherNotificationPublishProvider : NotificationPublishProvider +{ + public const string ProviderName = "WxPusher"; + + public override string Name => ProviderName; + + protected IWxPusherUserStore WxPusherUserStore { get; } + + protected IWxPusherMessageSender WxPusherMessageSender { get; } + + protected IStringLocalizerFactory LocalizerFactory { get; } + + protected AbpLocalizationOptions LocalizationOptions { get; } + + protected INotificationDefinitionManager NotificationDefinitionManager { get; } + + public WxPusherNotificationPublishProvider( + IWxPusherUserStore wxPusherUserStore, + IWxPusherMessageSender wxPusherMessageSender, + IStringLocalizerFactory localizerFactory, + IOptions localizationOptions, + INotificationDefinitionManager notificationDefinitionManager) + { + WxPusherUserStore = wxPusherUserStore; + WxPusherMessageSender = wxPusherMessageSender; + LocalizerFactory = localizerFactory; + LocalizationOptions = localizationOptions.Value; + NotificationDefinitionManager = notificationDefinitionManager; + } + + protected async override Task PublishAsync( + NotificationInfo notification, + IEnumerable identifiers, + CancellationToken cancellationToken = default) + { + var topics = await WxPusherUserStore + .GetSubscribeTopicsAsync( + identifiers.Select(x => x.UserId), + cancellationToken); + + var notificationDefine = NotificationDefinitionManager.GetOrNull(notification.Name); + var topicDefine = notificationDefine?.GetTopics(); + if (topicDefine.Any()) + { + topics = topicDefine; + } + var contentType = notificationDefine?.GetContentTypeOrDefault(MessageContentType.Text) + ?? MessageContentType.Text; + + if (!notification.Data.NeedLocalizer()) + { + var title = notification.Data.TryGetData("title").ToString(); + var message = notification.Data.TryGetData("message").ToString(); + + await WxPusherMessageSender.SendAsync( + content: message, + summary: title, + contentType: contentType, + topicIds: topics, + cancellationToken: cancellationToken); + } + else + { + var titleInfo = notification.Data.TryGetData("title").As(); + var titleResource = GetResource(titleInfo.ResourceName); + var title = LocalizerFactory.Create(titleResource.ResourceType)[titleInfo.Name, titleInfo.Values].Value; + + var messageInfo = notification.Data.TryGetData("message").As(); + var messageResource = GetResource(messageInfo.ResourceName); + var message = LocalizerFactory.Create(messageResource.ResourceType)[messageInfo.Name, messageInfo.Values].Value; + + await WxPusherMessageSender.SendAsync( + content: message, + summary: title, + contentType: contentType, + topicIds: topics, + cancellationToken: cancellationToken); + } + } + + private LocalizationResource GetResource(string resourceName) + { + return LocalizationOptions.Resources.Values + .First(x => x.ResourceName.Equals(resourceName)); + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/README.md b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/README.md new file mode 100644 index 000000000..b479488dd --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.Notifications.WxPusher/README.md @@ -0,0 +1,15 @@ +# LINGYUN.Abp.Notifications.WxPusher + +通知模块的WxPusher实现 + +使应用可通过WxPusher发布实时通知 + +## 模块引用 + +```csharp +[DependsOn(typeof(AbpNotificationsWxPusherModule))] +public class YouProjectModule : AbpModule +{ + // other +} +``` diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/FodyWeavers.xml b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/FodyWeavers.xsd b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/FodyWeavers.xsd new file mode 100644 index 000000000..11da52550 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN.Abp.WxPusher.csproj b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN.Abp.WxPusher.csproj new file mode 100644 index 000000000..cf5c943d2 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN.Abp.WxPusher.csproj @@ -0,0 +1,31 @@ + + + + + + + netstandard2.0 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/AbpWxPusherModule.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/AbpWxPusherModule.cs new file mode 100644 index 000000000..01ab71b81 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/AbpWxPusherModule.cs @@ -0,0 +1,53 @@ +using LINGYUN.Abp.Features.LimitValidation; +using LINGYUN.Abp.WxPusher.Localization; +using LINGYUN.Abp.WxPusher.Messages; +using LINGYUN.Abp.WxPusher.QrCode; +using LINGYUN.Abp.WxPusher.User; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System.Collections.Generic; +using Volo.Abp.Caching; +using Volo.Abp.Json; +using Volo.Abp.Json.SystemTextJson; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.Settings; +using Volo.Abp.VirtualFileSystem; + +namespace LINGYUN.Abp.WxPusher; + +[DependsOn( + typeof(AbpJsonModule), + typeof(AbpSettingsModule), + typeof(AbpCachingModule), + typeof(AbpFeaturesLimitValidationModule))] +public class AbpWxPusherModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddWxPusherClient(); + context.Services.TryAddSingleton(NullWxPusherUserStore.Instance); + + Configure(options => + { + options.UnsupportedTypes.TryAdd>(); + options.UnsupportedTypes.TryAdd>(); + options.UnsupportedTypes.TryAdd>(); + options.UnsupportedTypes.TryAdd>(); + options.UnsupportedTypes.TryAdd>>(); + options.UnsupportedTypes.TryAdd>>(); + }); + + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + Configure(options => + { + options.Resources + .Add() + .AddVirtualJson("/LINGYUN/Abp/WxPusher/Localization/Resources"); + }); + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Features/WxPusherFeatureDefinitionProvider.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Features/WxPusherFeatureDefinitionProvider.cs new file mode 100644 index 000000000..27b13ce08 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Features/WxPusherFeatureDefinitionProvider.cs @@ -0,0 +1,51 @@ +using LINGYUN.Abp.WxPusher.Localization; +using Volo.Abp.Features; +using Volo.Abp.Localization; +using Volo.Abp.Validation.StringValues; + +namespace LINGYUN.Abp.WxPusher.Features; + +public class WxPusherFeatureDefinitionProvider : FeatureDefinitionProvider +{ + public override void Define(IFeatureDefinitionContext context) + { + var group = context.AddGroup( + name: WxPusherFeatureNames.GroupName, + displayName: L("Features:WxPusher")); + group.AddFeature( + name: WxPusherFeatureNames.Enable, + defaultValue: "true", + displayName: L("Features:WxPusherEnable"), + description: L("Features:WxPusherEnableDesc"), + valueType: new ToggleStringValueType(new BooleanValueValidator())); + + var message = group.AddFeature( + name: WxPusherFeatureNames.Message.GroupName, + displayName: L("Features:Message"), + description: L("Features:Message")); + + message.CreateChild( + name: WxPusherFeatureNames.Message.Enable, + defaultValue: "true", + displayName: L("Features:MessageEnable"), + description: L("Features:MessageEnableDesc"), + valueType: new ToggleStringValueType(new BooleanValueValidator())); + message.CreateChild( + name: WxPusherFeatureNames.Message.SendLimit, + defaultValue: "500", + displayName: L("Features:Message.SendLimit"), + description: L("Features:Message.SendLimitDesc"), + valueType: new FreeTextStringValueType(new NumericValueValidator(1, 500))); + message.CreateChild( + name: WxPusherFeatureNames.Message.SendLimitInterval, + defaultValue: "1", + displayName: L("Features:Message.SendLimitInterval"), + description: L("Features:Message.SendLimitIntervalDesc"), + valueType: new FreeTextStringValueType(new NumericValueValidator(1, 1))); + } + + private static LocalizableString L(string name) + { + return LocalizableString.Create(name); + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Features/WxPusherFeatureNames.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Features/WxPusherFeatureNames.cs new file mode 100644 index 000000000..1e95f9611 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Features/WxPusherFeatureNames.cs @@ -0,0 +1,28 @@ +namespace LINGYUN.Abp.WxPusher.Features; + +public static class WxPusherFeatureNames +{ + public const string GroupName = "WxPusher"; + + /// + /// 启用WxPusher + /// + public const string Enable = GroupName + ".Enable"; + + public static class Message + { + public const string GroupName = WxPusherFeatureNames.GroupName + ".Message"; + /// + /// 启用消息推送 + /// + public const string Enable = GroupName + ".Enable"; + /// + /// 发送次数上限 + /// + public const string SendLimit = GroupName + ".SendLimit"; + /// + /// 发送次数上限时长 + /// + public const string SendLimitInterval = GroupName + ".SendLimitInterval"; + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Localization/Resources/en.json b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Localization/Resources/en.json new file mode 100644 index 000000000..cdea368c4 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Localization/Resources/en.json @@ -0,0 +1,17 @@ +{ + "culture": "en", + "texts": { + "Settings:Security.AppToken": "App Token", + "Settings:Security.AppTokenDesc": "If you have APP_TOKEN, you can send messages to users of the corresponding application. Keep it confidential.", + "Features:WxPusher": "WxPusher Wechat push service", + "Features:WxPusherEnable": "Enable WxPusher", + "Features:WxPusherEnableDesc": "Enable to enable the application to have WxPusher capabilities.", + "Features:Message": "WxPusher Wechat message push", + "Features:MessageEnable": "Enable WxPusher Wechat Message Push", + "Features:MessageEnableDesc": "Enable so that apps will have the ability to be pushed to wechat via WxPusher.", + "Features:Message.SendLimit": "Amount of wechat message push", + "Features:Message.SendLimitDesc": "Set to limit the amount of wechat message push.", + "Features:Message.SendLimitInterval": "Wechat message limit interval", + "Features:Message.SendLimitIntervalDesc": "Set the wechat message limit period (time scale: days). A single wechat user (UID) can receive a maximum of 500 messages per day. Please arrange the sending frequency reasonably." + } +} \ No newline at end of file diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Localization/Resources/zh-Hans.json b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Localization/Resources/zh-Hans.json new file mode 100644 index 000000000..f4b5a1bae --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Localization/Resources/zh-Hans.json @@ -0,0 +1,17 @@ +{ + "culture": "zh-Hans", + "texts": { + "Settings:Security.AppToken": "应用的身份标志", + "Settings:Security.AppTokenDesc": "拥有APP_TOKEN,就可以给对应的应用的用户发送消息, 请严格保密.", + "Features:WxPusher": "WxPusher微信推送服务", + "Features:WxPusherEnable": "启用WxPusher", + "Features:WxPusherEnableDesc": "启用以使应用拥有WxPusher的能力.", + "Features:Message": "WxPusher微信消息推送", + "Features:MessageEnable": "启用WxPusher微信消息推送", + "Features:MessageEnableDesc": "启用以使应用将拥有通过WxPusher推送到微信的能力.", + "Features:Message.SendLimit": "微信消息推送量", + "Features:Message.SendLimitDesc": "设置以限制微信消息推送量.", + "Features:Message.SendLimitInterval": "微信消息限制周期", + "Features:Message.SendLimitIntervalDesc": "设置微信消息限制周期(时间刻度: 天).单个微信用户(uid),每天最多接收500条消息,请合理安排发送频率." + } +} \ No newline at end of file diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Localization/WxPusherResource.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Localization/WxPusherResource.cs new file mode 100644 index 000000000..1c61d84f4 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Localization/WxPusherResource.cs @@ -0,0 +1,8 @@ +using Volo.Abp.Localization; + +namespace LINGYUN.Abp.WxPusher.Localization; + +[LocalizationResourceName("WxPusher")] +public class WxPusherResource +{ +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/IWxPusherMessageProvider.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/IWxPusherMessageProvider.cs new file mode 100644 index 000000000..5dd966869 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/IWxPusherMessageProvider.cs @@ -0,0 +1,11 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.WxPusher.Messages; + +public interface IWxPusherMessageProvider +{ + Task> QueryMessageAsync( + int messageId, + CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/IWxPusherMessageSender.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/IWxPusherMessageSender.cs new file mode 100644 index 000000000..a69b01fda --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/IWxPusherMessageSender.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.WxPusher.Messages; + +public interface IWxPusherMessageSender +{ + Task> SendAsync( + string content, + string summary = "", + MessageContentType contentType = MessageContentType.Text, + List topicIds = null, + List uids = null, + string url = "", + CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/MessageContentType.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/MessageContentType.cs new file mode 100644 index 000000000..83de546c2 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/MessageContentType.cs @@ -0,0 +1,16 @@ +namespace LINGYUN.Abp.WxPusher.Messages; +public enum MessageContentType +{ + /// + /// 文字 + /// + Text = 1, + /// + /// html(只发送body标签内部的数据即可,不包括body标签 + /// + Html = 2, + /// + /// markdown + /// + Markdown = 3 +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/MessageHttpClientExtensions.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/MessageHttpClientExtensions.cs new file mode 100644 index 000000000..96646465b --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/MessageHttpClientExtensions.cs @@ -0,0 +1,41 @@ +using Newtonsoft.Json; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.WxPusher.Messages; + +internal static class MessageHttpClientExtensions +{ + public async static Task SendMessageAsync( + this HttpClient httpClient, + SendMessage sendMessage, + CancellationToken cancellationToken = default) + { + var requestMessage = new HttpRequestMessage( + HttpMethod.Post, + "/api/send/message"); + + var requestBody = JsonConvert.SerializeObject(sendMessage); + requestMessage.Content = new StringContent(requestBody, Encoding.UTF8, "application/json"); + + var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken); + + return await httpResponse.Content.ReadAsStringAsync(); + } + + public async static Task QueryMessageAsync( + this HttpClient httpClient, + int messageId, + CancellationToken cancellationToken = default) + { + var requestMessage = new HttpRequestMessage( + HttpMethod.Get, + $"/api/send/query/{messageId}"); + + var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken); + + return await httpResponse.Content.ReadAsStringAsync(); + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/SendMessage.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/SendMessage.cs new file mode 100644 index 000000000..04565984e --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/SendMessage.cs @@ -0,0 +1,52 @@ +using JetBrains.Annotations; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using Volo.Abp; + +namespace LINGYUN.Abp.WxPusher.Messages; + +[Serializable] +public class SendMessage +{ + [JsonProperty("appToken")] + public string AppToken { get; } + + [JsonProperty("content")] + public string Content { get; } + + [JsonProperty("summary")] + public string Summary { get; set; } + + [JsonProperty("contentType")] + public MessageContentType ContentType { get; } + + [JsonProperty("topicIds")] + public List TopicIds { get; } + + [JsonProperty("uids")] + public List Uids { get; } + + [JsonProperty("url")] + public string Url { get; } + public SendMessage( + [NotNull] string appToken, + [NotNull] string content, + string summary = "", + MessageContentType contentType = MessageContentType.Text, + string url = "") + { + Check.NotNullOrWhiteSpace(appToken, nameof(appToken)); + Check.NotNullOrWhiteSpace(content, nameof(content)); + Check.Length(summary, nameof(summary), 100); + + AppToken = appToken; + Content = content; + Summary = summary; + ContentType = contentType; + Url = url; + + TopicIds = new List(); + Uids = new List(); + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/SendMessageResult.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/SendMessageResult.cs new file mode 100644 index 000000000..9b22ab30c --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/SendMessageResult.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; +using System; + +namespace LINGYUN.Abp.WxPusher.Messages; + +[Serializable] +public class SendMessageResult +{ + /// + /// 状态码 + /// + [JsonProperty("code")] + public int Code { get; set; } + /// + /// 消息标识 + /// + [JsonProperty("messageId")] + public long MessageId { get; set; } + /// + /// 状态 + /// + [JsonProperty("status")] + public string Status { get; set; } + /// + /// 用户标识 + /// + [JsonProperty("uid")] + public string Uid { get; set; } + /// + /// 群组标识 + /// + [JsonProperty("topicId")] + public string TopicId { get; set; } + /// + /// 是否调用成功 + /// + public bool IsSuccessed => Code == 1000; +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/WxPusherMessageProvider.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/WxPusherMessageProvider.cs new file mode 100644 index 000000000..4a9a48b05 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/WxPusherMessageProvider.cs @@ -0,0 +1,24 @@ +using LINGYUN.Abp.WxPusher.Features; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Features; + +namespace LINGYUN.Abp.WxPusher.Messages; + +[RequiresFeature(WxPusherFeatureNames.Enable)] +public class WxPusherMessageProvider : WxPusherRequestProvider, IWxPusherMessageProvider +{ + public async virtual Task> QueryMessageAsync( + int messageId, + CancellationToken cancellationToken = default) + { + var client = HttpClientFactory.GetPushPlusClient(); + + var content = await client.QueryMessageAsync( + messageId, + cancellationToken); + + return JsonSerializer.Deserialize>(content); + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/WxPusherMessageSender.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/WxPusherMessageSender.cs new file mode 100644 index 000000000..1ed0290d5 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Messages/WxPusherMessageSender.cs @@ -0,0 +1,62 @@ +using LINGYUN.Abp.Features.LimitValidation; +using LINGYUN.Abp.WxPusher.Features; +using LINGYUN.Abp.WxPusher.Token; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Features; + +namespace LINGYUN.Abp.WxPusher.Messages; + +[RequiresFeature(WxPusherFeatureNames.Enable)] +public class WxPusherMessageSender : WxPusherRequestProvider, IWxPusherMessageSender +{ + protected IWxPusherTokenProvider WxPusherTokenProvider { get; } + + public WxPusherMessageSender(IWxPusherTokenProvider wxPusherTokenProvider) + { + WxPusherTokenProvider = wxPusherTokenProvider; + } + + [RequiresFeature(WxPusherFeatureNames.Message.Enable)] + [RequiresLimitFeature( + WxPusherFeatureNames.Message.SendLimit, + WxPusherFeatureNames.Message.SendLimitInterval, + LimitPolicy.Days)] + public async virtual Task> SendAsync( + string content, + string summary = "", + MessageContentType contentType = MessageContentType.Text, + List topicIds = null, + List uids = null, + string url = "", + CancellationToken cancellationToken = default) + { + var token = await WxPusherTokenProvider.GetTokenAsync(cancellationToken); + var client = HttpClientFactory.GetPushPlusClient(); + var sendMessage = new SendMessage( + token, + content, + summary, + contentType, + url); + if (topicIds != null) + { + sendMessage.TopicIds.AddIfNotContains(topicIds); + } + if (uids != null) + { + sendMessage.Uids.AddIfNotContains(uids); + } + + var resultContent = await client.SendMessageAsync( + sendMessage, + cancellationToken); + + var response = JsonSerializer + .Deserialize>>(resultContent); + + return response.GetData(); + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/QrCode/CreateQrcodeRequest.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/QrCode/CreateQrcodeRequest.cs new file mode 100644 index 000000000..aa5d11e82 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/QrCode/CreateQrcodeRequest.cs @@ -0,0 +1,39 @@ +using JetBrains.Annotations; +using Newtonsoft.Json; +using System; +using Volo.Abp; + +namespace LINGYUN.Abp.WxPusher.QrCode; + +[Serializable] +public class CreateQrcodeRequest +{ + /// + /// 应用的标志 + /// + [JsonProperty("appToken")] + public string AppToken { get; } + /// + /// 二维码携带的参数,最长64位 + /// + [JsonProperty("extra")] + public string Extra { get; } + /// + /// 二维码有效时间,s为单位,最大30天。 + /// + [JsonProperty("validTime")] + public int ValidTime { get; } + + public CreateQrcodeRequest( + [NotNull] string appToken, + [NotNull] string extra, + int validTime = 1800) + { + Check.NotNullOrWhiteSpace(appToken, nameof(appToken)); + Check.NotNullOrWhiteSpace(extra, nameof(extra), 64); + + AppToken = appToken; + Extra = extra; + ValidTime = validTime; + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/QrCode/CreateQrcodeResult.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/QrCode/CreateQrcodeResult.cs new file mode 100644 index 000000000..5f332575e --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/QrCode/CreateQrcodeResult.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; +using System; + +namespace LINGYUN.Abp.WxPusher.QrCode; + +[Serializable] +public class CreateQrcodeResult +{ + [JsonProperty("expires")] + public long Expires { get; set; } + + [JsonProperty("code")] + public string Code { get; set; } + + [JsonProperty("shortUrl")] + public string ShortUrl { get; set; } + + [JsonProperty("url")] + public string Url { get; set; } + + [JsonProperty("extra")] + public string Extra { get; set; } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/QrCode/GetScanQrCodeResult.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/QrCode/GetScanQrCodeResult.cs new file mode 100644 index 000000000..f607f1a10 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/QrCode/GetScanQrCodeResult.cs @@ -0,0 +1,35 @@ +using Newtonsoft.Json; +using System; + +namespace LINGYUN.Abp.WxPusher.QrCode; + +[Serializable] +public class GetScanQrCodeResult +{ + [JsonProperty("appId")] + public int AppId { get; set; } + + [JsonProperty("appKey")] + public string AppKey { get; set; } + + [JsonProperty("appName")] + public string AppName { get; set; } + + [JsonProperty("extra")] + public string Extra { get; set; } + + [JsonProperty("source")] + public string Source { get; set; } + + [JsonProperty("time")] + public long Time { get; set; } + + [JsonProperty("uid")] + public string Uid { get; set; } + + [JsonProperty("userHeadImg")] + public string UserHeadImg { get; set; } + + [JsonProperty("userName")] + public string UserName { get; set; } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/QrCode/IWxPusherQrCodeProvider.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/QrCode/IWxPusherQrCodeProvider.cs new file mode 100644 index 000000000..fdf8bf1e7 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/QrCode/IWxPusherQrCodeProvider.cs @@ -0,0 +1,17 @@ +using JetBrains.Annotations; +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.WxPusher.QrCode; + +public interface IWxPusherQrCodeProvider +{ + Task CreateQrcodeAsync( + [NotNull] string extra, + int validTime = 1800, + CancellationToken cancellationToken = default); + + Task GetScanQrCodeAsync( + [NotNull] string code, + CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/QrCode/QrCodeHttpClientExtensions.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/QrCode/QrCodeHttpClientExtensions.cs new file mode 100644 index 000000000..d8615279b --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/QrCode/QrCodeHttpClientExtensions.cs @@ -0,0 +1,41 @@ +using Newtonsoft.Json; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.WxPusher.QrCode; + +internal static class QrCodeHttpClientExtensions +{ + public async static Task CreateQrcodeAsync( + this HttpClient httpClient, + CreateQrcodeRequest qrcodeRequest, + CancellationToken cancellationToken = default) + { + var requestMessage = new HttpRequestMessage( + HttpMethod.Post, + "/api/fun/create/qrcode"); + + var requestBody = JsonConvert.SerializeObject(qrcodeRequest); + requestMessage.Content = new StringContent(requestBody, Encoding.UTF8, "application/json"); + + var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken); + + return await httpResponse.Content.ReadAsStringAsync(); + } + + public async static Task GetScanQrCodeUidAsync( + this HttpClient httpClient, + string code, + CancellationToken cancellationToken = default) + { + var requestMessage = new HttpRequestMessage( + HttpMethod.Get, + $"/api/fun/scan-qrcode-uid?code={code}"); + + var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken); + + return await httpResponse.Content.ReadAsStringAsync(); + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/QrCode/WxPusherQrCodeProvider.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/QrCode/WxPusherQrCodeProvider.cs new file mode 100644 index 000000000..17faee998 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/QrCode/WxPusherQrCodeProvider.cs @@ -0,0 +1,56 @@ +using JetBrains.Annotations; +using LINGYUN.Abp.WxPusher.Features; +using LINGYUN.Abp.WxPusher.Token; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Features; + +namespace LINGYUN.Abp.WxPusher.QrCode; + +[RequiresFeature(WxPusherFeatureNames.Enable)] +public class WxPusherQrCodeProvider : WxPusherRequestProvider, IWxPusherQrCodeProvider +{ + protected IWxPusherTokenProvider WxPusherTokenProvider { get; } + + public WxPusherQrCodeProvider(IWxPusherTokenProvider wxPusherTokenProvider) + { + WxPusherTokenProvider = wxPusherTokenProvider; + } + + public async virtual Task CreateQrcodeAsync( + [NotNull] string extra, + int validTime = 1800, + CancellationToken cancellationToken = default) + { + var token = await WxPusherTokenProvider.GetTokenAsync(cancellationToken); + var client = HttpClientFactory.GetPushPlusClient(); + var request = new CreateQrcodeRequest(token, extra, validTime); + + var content = await client.CreateQrcodeAsync( + request, + cancellationToken); + + var response = JsonSerializer.Deserialize>(content); + + return response.GetData(); + } + + public async virtual Task GetScanQrCodeAsync( + [NotNull] string code, + CancellationToken cancellationToken = default) + { + Check.NotNullOrWhiteSpace(code, nameof(code)); + + var client = HttpClientFactory.GetPushPlusClient(); + + var content = await client.GetScanQrCodeUidAsync( + code, + cancellationToken); + + var response = JsonSerializer.Deserialize>(content); + + return response.GetData(); + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Security/Claims/AbpWxPusherClaimTypes.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Security/Claims/AbpWxPusherClaimTypes.cs new file mode 100644 index 000000000..73e178a38 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Security/Claims/AbpWxPusherClaimTypes.cs @@ -0,0 +1,9 @@ +namespace LINGYUN.Abp.WxPusher.Security.Claims; + +public static class AbpWxPusherClaimTypes +{ + /// + /// 用户的唯一标识 + /// + public static string Uid { get; set; } = "wx-pusher-uid"; +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Settings/WxPusherSettingDefinitionProvider.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Settings/WxPusherSettingDefinitionProvider.cs new file mode 100644 index 000000000..8cf2b2db3 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Settings/WxPusherSettingDefinitionProvider.cs @@ -0,0 +1,30 @@ +using LINGYUN.Abp.WxPusher.Localization; +using Volo.Abp.Localization; +using Volo.Abp.Settings; + +namespace LINGYUN.Abp.WxPusher.Settings; + +public class WxPusherSettingDefinitionProvider : SettingDefinitionProvider +{ + public override void Define(ISettingDefinitionContext context) + { + context.Add(new[] + { + new SettingDefinition( + name: WxPusherSettingNames.Security.AppToken, + displayName: L("Settings:Security.AppToken"), + description: L("Settings:Security.AppTokenDesc"), + isEncrypted: true) + .WithProviders( + DefaultValueSettingValueProvider.ProviderName, + ConfigurationSettingValueProvider.ProviderName, + GlobalSettingValueProvider.ProviderName, + TenantSettingValueProvider.ProviderName), + }); + } + + public static ILocalizableString L(string name) + { + return LocalizableString.Create(name); + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Settings/WxPusherSettingNames.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Settings/WxPusherSettingNames.cs new file mode 100644 index 000000000..0abc91f67 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Settings/WxPusherSettingNames.cs @@ -0,0 +1,13 @@ +namespace LINGYUN.Abp.WxPusher.Settings; + +public static class WxPusherSettingNames +{ + public const string Prefix = "WxPusher"; + + public static class Security + { + public const string Prefix = WxPusherSettingNames.Prefix + ".Security"; + + public const string AppToken = Prefix + ".AppToken"; + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Token/IWxPusherTokenProvider.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Token/IWxPusherTokenProvider.cs new file mode 100644 index 000000000..5cbf24a92 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Token/IWxPusherTokenProvider.cs @@ -0,0 +1,9 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.WxPusher.Token; + +public interface IWxPusherTokenProvider +{ + Task GetTokenAsync(CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Token/WxPusherTokenProvider.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Token/WxPusherTokenProvider.cs new file mode 100644 index 000000000..c2821e9c3 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/Token/WxPusherTokenProvider.cs @@ -0,0 +1,23 @@ +using LINGYUN.Abp.WxPusher.Settings; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Settings; + +namespace LINGYUN.Abp.WxPusher.Token; + +public class WxPusherTokenProvider : IWxPusherTokenProvider, ITransientDependency +{ + protected ISettingProvider SettingProvider { get; } + + public WxPusherTokenProvider(ISettingProvider settingProvider) + { + SettingProvider = settingProvider; + } + + public async virtual Task GetTokenAsync(CancellationToken cancellationToken = default) + { + return await SettingProvider.GetOrNullAsync( + WxPusherSettingNames.Security.AppToken); + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/FlowType.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/FlowType.cs new file mode 100644 index 000000000..2077f6ead --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/FlowType.cs @@ -0,0 +1,12 @@ +namespace LINGYUN.Abp.WxPusher.User; +public enum FlowType +{ + /// + /// 关注应用 + /// + App = 0, + /// + /// 关注topic + /// + Topic = 1, +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/IWxPusherUserProvider.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/IWxPusherUserProvider.cs new file mode 100644 index 000000000..ecf975f9b --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/IWxPusherUserProvider.cs @@ -0,0 +1,56 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.WxPusher.User; +/// +/// 用户接口 +/// +public interface IWxPusherUserProvider +{ + /// + /// 查询App的关注用户V2 + /// + /// + /// 获取到所有关注应用/主题的微信用户用户信息。需要注意,一个微信用户,如果同时关注应用,主题,甚至关注多个主题,会返回多条记录。 + /// + /// 请求数据的页码 + /// 分页大小,不能超过100 + /// 用户的uid,可选,如果不传就是查询所有用户,传uid就是查某个用户的信息。 + /// 查询拉黑用户,可选,不传查询所有用户,true查询拉黑用户,false查询没有拉黑的用户 + /// 关注的类型,可选,不传查询所有用户,0是应用,1是主题 + /// + /// + Task> GetUserListAsync( + int page = 1, + int pageSize = 10, + string uid = null, + bool? isBlock = null, + FlowType? type = null, + CancellationToken cancellationToken = default); + /// + /// 删除用户 + /// + /// + /// 你可以删除用户对应用、主题的关注,删除以后,用户可以重新关注,如想让用户再次关注,可以调用拉黑接口,对用户拉黑。 + /// + /// 用户id,通过用户查询接口可以获取 + /// + /// + Task DeleteUserAsync( + int id, + CancellationToken cancellationToken = default); + /// + /// 拉黑用户 + /// + /// + /// 拉黑以后不能再发送消息,用户也不能再次关注,除非你取消对他的拉黑。调用拉黑接口,不用再调用删除接口。 + /// + /// 用户id,通过用户查询接口可以获取 + /// 是否拉黑,true表示拉黑,false表示取消拉黑 + /// + /// + Task RejectUserAsync( + int id, + bool reject, + CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/IWxPusherUserStore.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/IWxPusherUserStore.cs new file mode 100644 index 000000000..340828fc5 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/IWxPusherUserStore.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.WxPusher.User; + +public interface IWxPusherUserStore +{ + /// + /// 获取用户订阅的topic列表 + /// + /// 用户标识列表 + /// + /// + Task> GetSubscribeTopicsAsync( + IEnumerable userIds, + CancellationToken cancellationToken = default); +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/NullWxPusherUserStore.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/NullWxPusherUserStore.cs new file mode 100644 index 000000000..80ec02cfe --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/NullWxPusherUserStore.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.WxPusher.User; + +public sealed class NullWxPusherUserStore : IWxPusherUserStore +{ + public readonly static IWxPusherUserStore Instance = new NullWxPusherUserStore(); + + private NullWxPusherUserStore() + { + } + + public Task> GetSubscribeTopicsAsync( + IEnumerable userIds, + CancellationToken cancellationToken = default) + { + return Task.FromResult(new List()); + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/UserHttpClientExtensions.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/UserHttpClientExtensions.cs new file mode 100644 index 000000000..8c307704f --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/UserHttpClientExtensions.cs @@ -0,0 +1,68 @@ +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.WxPusher.User; + +internal static class UserHttpClientExtensions +{ + public async static Task GetUserListAsync( + this HttpClient httpClient, + string appToken, + int page = 1, + int pageSize = 10, + string uid = null, + bool? isBlock = null, + FlowType? type = null, + CancellationToken cancellationToken = default) + { + var requestUrl = "/api/fun/wxuser/v2?appToken=$appToken&page=$page&pageSize=$pageSize&uid=$uid&isBlock=$isBlock&type=$type"; + + requestUrl = requestUrl + .Replace("$appToken", appToken) + .Replace("$page", page.ToString()) + .Replace("$pageSize", pageSize.ToString()) + .Replace("$uid", uid ?? "") + .Replace("$isBlock", isBlock?.ToString() ?? "") + .Replace("$type", type.HasValue ? ((int)type).ToString() : ""); + + var requestMessage = new HttpRequestMessage( + HttpMethod.Get, + requestUrl); + + var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken); + + return await httpResponse.Content.ReadAsStringAsync(); + } + + public async static Task DeleteUserAsync( + this HttpClient httpClient, + string appToken, + int id, + CancellationToken cancellationToken = default) + { + var requestMessage = new HttpRequestMessage( + HttpMethod.Delete, + $"/api/fun/remove?appToken={appToken}&id={id}"); + + var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken); + + return await httpResponse.Content.ReadAsStringAsync(); + } + + public async static Task RejectUserAsync( + this HttpClient httpClient, + string appToken, + int id, + bool reject, + CancellationToken cancellationToken = default) + { + var requestMessage = new HttpRequestMessage( + HttpMethod.Put, + $"/api/fun/reject?appToken={appToken}&id={id}&reject={reject}"); + + var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken); + + return await httpResponse.Content.ReadAsStringAsync(); + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/UserProfile.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/UserProfile.cs new file mode 100644 index 000000000..17f647483 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/UserProfile.cs @@ -0,0 +1,51 @@ +using Newtonsoft.Json; +using System; + +namespace LINGYUN.Abp.WxPusher.User; + +[Serializable] +public class UserProfile +{ + /// + /// 用户uid + /// + [JsonProperty("uid")] + public string Uid { get; set; } + /// + /// 新用户微信不再返回 ,强制返回空 + /// + [JsonProperty("headImg")] + public string HeadImg { get; set; } + /// + /// 创建时间 + /// + [JsonProperty("createTime")] + public long CreateTime { get; set; } + /// + /// 新用户微信不再返回 ,强制返回空 + /// + [JsonProperty("nickName")] + public string NickName { get; set; } + /// + /// 是否拉黑 + /// + [JsonProperty("reject")] + public bool Reject { get; set; } + /// + /// 如果调用删除或者拉黑接口,需要这个id + /// + [JsonProperty("id")] + public int Id { get; set; } + /// + /// 关注类型, + /// 0:关注应用, + /// 1:关注topic + /// + [JsonProperty("type")] + public FlowType Type { get; set; } + /// + /// 关注的应用或者主题名字 + /// + [JsonProperty("target")] + public string Target { get; set; } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/WxPusherUserProvider.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/WxPusherUserProvider.cs new file mode 100644 index 000000000..15f5d0616 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/User/WxPusherUserProvider.cs @@ -0,0 +1,87 @@ +using LINGYUN.Abp.WxPusher.Features; +using LINGYUN.Abp.WxPusher.Token; +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.Features; + +namespace LINGYUN.Abp.WxPusher.User; + +[RequiresFeature(WxPusherFeatureNames.Enable)] +public class WxPusherUserProvider : WxPusherRequestProvider, IWxPusherUserProvider +{ + protected IWxPusherTokenProvider WxPusherTokenProvider { get; } + + public WxPusherUserProvider(IWxPusherTokenProvider wxPusherTokenProvider) + { + WxPusherTokenProvider = wxPusherTokenProvider; + } + + public async virtual Task DeleteUserAsync( + int id, + CancellationToken cancellationToken = default) + { + var token = await WxPusherTokenProvider.GetTokenAsync(cancellationToken); + var client = HttpClientFactory.GetPushPlusClient(); + + var content = await client.DeleteUserAsync( + token, + id, + cancellationToken); + + var response = JsonSerializer.Deserialize>(content); + + return response.Success; + } + + public async virtual Task> GetUserListAsync( + int page = 1, + int pageSize = 10, + string uid = null, + bool? isBlock = null, + FlowType? type = null, + CancellationToken cancellationToken = default) + { + if (pageSize > 100) + { + throw new ArgumentException("pageSize must be equal to or lower than 100!", nameof(pageSize)); + } + + var token = await WxPusherTokenProvider.GetTokenAsync(cancellationToken); + var client = HttpClientFactory.GetPushPlusClient(); + + var content = await client.GetUserListAsync( + token, + page, + pageSize, + uid, + isBlock, + type, + cancellationToken); + + var response = JsonSerializer + .Deserialize>>(content); + + return response.GetData(); + } + + public async virtual Task RejectUserAsync( + int id, + bool reject, + CancellationToken cancellationToken = default) + { + var token = await WxPusherTokenProvider.GetTokenAsync(cancellationToken); + var client = HttpClientFactory.GetPushPlusClient(); + + var content = await client.RejectUserAsync( + token, + id, + reject, + cancellationToken); + + var response = JsonSerializer.Deserialize>(content); + + return response.Success; + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/WxPusherPagedResult.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/WxPusherPagedResult.cs new file mode 100644 index 000000000..b6a31f345 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/WxPusherPagedResult.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; + +namespace LINGYUN.Abp.WxPusher; + +[Serializable] +public class WxPusherPagedResult +{ + /// + /// 总数 + /// + [JsonProperty("total")] + public int Total { get; set; } + /// + /// 当前页码 + /// + [JsonProperty("page")] + public int Page { get; set; } + /// + /// 页码大小 + /// + [JsonProperty("pageSize")] + public int PageSize { get; set; } + /// + /// 记录列表 + /// + [JsonProperty("records")] + public List Records { get; set; } = new List(); +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/WxPusherRemoteCallException.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/WxPusherRemoteCallException.cs new file mode 100644 index 000000000..6f1bb700f --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/WxPusherRemoteCallException.cs @@ -0,0 +1,15 @@ +using Volo.Abp; +using Volo.Abp.ExceptionHandling; + +namespace LINGYUN.Abp.WxPusher; + +public class WxPusherRemoteCallException : AbpException, IHasErrorCode +{ + public string Code { get; } + + public WxPusherRemoteCallException(string code, string message) + : base($"The WxPusher api returns an error: {code} - {message}") + { + Code = code; + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/WxPusherRequestProvider.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/WxPusherRequestProvider.cs new file mode 100644 index 000000000..ddc99774f --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/WxPusherRequestProvider.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using System.Net.Http; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Json; + +namespace LINGYUN.Abp.WxPusher; + +public abstract class WxPusherRequestProvider : ITransientDependency +{ + public IAbpLazyServiceProvider LazyServiceProvider { get; set; } + + protected ILoggerFactory LoggerFactory => LazyServiceProvider.LazyGetRequiredService(); + protected ILogger Logger => LazyServiceProvider.LazyGetService(provider => LoggerFactory?.CreateLogger(GetType().FullName) ?? NullLogger.Instance); + protected IJsonSerializer JsonSerializer => LazyServiceProvider.LazyGetRequiredService(); + protected IHttpClientFactory HttpClientFactory => LazyServiceProvider.LazyGetRequiredService(); +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/WxPusherResult.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/WxPusherResult.cs new file mode 100644 index 000000000..e3f72752c --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/LINGYUN/Abp/WxPusher/WxPusherResult.cs @@ -0,0 +1,61 @@ +using Newtonsoft.Json; +using System; + +namespace LINGYUN.Abp.WxPusher; + +[Serializable] +public class WxPusherResult +{ + /// + /// 状态码 + /// + [JsonProperty("code")] + public int Code { get; set; } + /// + /// 错误消息 + /// + [JsonProperty("msg")] + public string Message { get; set; } + /// + /// 返回数据 + /// + [JsonProperty("data")] + public T Data { get; set; } + /// + /// 是否调用成功 + /// + [JsonProperty("success")] + public bool Success { get; set; } + + public WxPusherResult() + { + } + + public WxPusherResult(int code, string message) + { + Code = code; + Message = message; + } + + public WxPusherResult(int code, string message, T data) + { + Code = code; + Message = message; + Data = data; + } + + public T GetData() + { + ThrowOfFailed(); + + return Data; + } + + public void ThrowOfFailed() + { + if (!Success) + { + throw new WxPusherRemoteCallException(Code.ToString(), Message); + } + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/Microsoft/Extensions/DependencyInjection/IServiceCollectionExtensions.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/Microsoft/Extensions/DependencyInjection/IServiceCollectionExtensions.cs new file mode 100644 index 000000000..d0f09df8c --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/Microsoft/Extensions/DependencyInjection/IServiceCollectionExtensions.cs @@ -0,0 +1,19 @@ +using System; + +namespace Microsoft.Extensions.DependencyInjection; + +internal static class IServiceCollectionExtensions +{ + public static IServiceCollection AddWxPusherClient( + this IServiceCollection services) + { + services.AddHttpClient( + "_Abp_WxPusher_Client", + (httpClient) => + { + httpClient.BaseAddress = new Uri("https://wxpusher.zjiecode.com"); + }); + + return services; + } +} diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/README.md b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/README.md new file mode 100644 index 000000000..d1ef7c447 --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/README.md @@ -0,0 +1,35 @@ +# LINGYUN.Abp.WxPusher + +集成WxPusher + +实现WxPusher相关Api文档,拥有WxPusher开放能力 + +详情见WxPusher文档: https://wxpusher.dingliqc.com/docs/#/ + +## 模块引用 + +```csharp +[DependsOn(typeof(AbpWxPusherModule))] +public class YouProjectModule : AbpModule +{ + // other +} +``` + +## 用户订阅 + +实现 [IWxPusherUserStore](./LINGYUN/Abp/WxPusher/User/IWxPusherUserStore) 接口获取用户订阅列表 + +## Features + +* WxPusher WxPusher特性分组 +* WxPusher.Enable 全局启用WxPusher +* WxPusher.Message.Enable 全局启用WxPusher消息通道 +* WxPusher.Message WxPusher消息推送 +* WxPusher.Message.Enable 启用WxPusher消息推送 +* WxPusher.Message.SendLimit WxPusher消息推送限制次数 +* WxPusher.Message.SendLimitInterval WxPusher消息推送限制周期(天) + +## Settings + +* WxPusher.Security.AppToken 应用的身份标志,拥有APP_TOKEN,就可以给对应的应用的用户发送消息, 请严格保密. diff --git a/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/System/Net/Http/IHttpClientFactoryExtensions.cs b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/System/Net/Http/IHttpClientFactoryExtensions.cs new file mode 100644 index 000000000..1134d85cb --- /dev/null +++ b/aspnet-core/modules/wx-pusher/LINGYUN.Abp.WxPusher/System/Net/Http/IHttpClientFactoryExtensions.cs @@ -0,0 +1,10 @@ +namespace System.Net.Http; + +internal static class IHttpClientFactoryExtensions +{ + public static HttpClient GetPushPlusClient( + this IHttpClientFactory httpClientFactory) + { + return httpClientFactory.CreateClient("_Abp_WxPusher_Client"); + } +} diff --git a/aspnet-core/tests/LINGYUN.Abp.WxPusher.Tests/LINGYUN.Abp.WxPusher.Tests.csproj b/aspnet-core/tests/LINGYUN.Abp.WxPusher.Tests/LINGYUN.Abp.WxPusher.Tests.csproj new file mode 100644 index 000000000..8af003eb7 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.WxPusher.Tests/LINGYUN.Abp.WxPusher.Tests.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + + false + + + + + + + + + + + + diff --git a/aspnet-core/tests/LINGYUN.Abp.WxPusher.Tests/LINGYUN/Abp/WxPusher/AbpWxPusherTestBase.cs b/aspnet-core/tests/LINGYUN.Abp.WxPusher.Tests/LINGYUN/Abp/WxPusher/AbpWxPusherTestBase.cs new file mode 100644 index 000000000..481499740 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.WxPusher.Tests/LINGYUN/Abp/WxPusher/AbpWxPusherTestBase.cs @@ -0,0 +1,7 @@ +using LINGYUN.Abp.Tests; + +namespace LINGYUN.Abp.WxPusher; + +public class AbpWxPusherTestBase : AbpTestsBase +{ +} diff --git a/aspnet-core/tests/LINGYUN.Abp.WxPusher.Tests/LINGYUN/Abp/WxPusher/AbpWxPusherTestModule.cs b/aspnet-core/tests/LINGYUN.Abp.WxPusher.Tests/LINGYUN/Abp/WxPusher/AbpWxPusherTestModule.cs new file mode 100644 index 000000000..c27d43a09 --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.WxPusher.Tests/LINGYUN/Abp/WxPusher/AbpWxPusherTestModule.cs @@ -0,0 +1,24 @@ +using LINGYUN.Abp.Tests; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.WxPusher; + +[DependsOn( + typeof(AbpWxPusherModule), + typeof(AbpTestsBaseModule))] +public class AbpWxPusherTestModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + var configurationOptions = new AbpConfigurationBuilderOptions + { + BasePath = @"D:\Projects\Development\Abp\WxPusher", + EnvironmentName = "Test" + }; + var configuration = ConfigurationHelper.BuildConfiguration(configurationOptions); + + context.Services.ReplaceConfiguration(configuration); + } +} diff --git a/aspnet-core/tests/LINGYUN.Abp.WxPusher.Tests/LINGYUN/Abp/WxPusher/Messages/WxPusherMessageSenderTests.cs b/aspnet-core/tests/LINGYUN.Abp.WxPusher.Tests/LINGYUN/Abp/WxPusher/Messages/WxPusherMessageSenderTests.cs new file mode 100644 index 000000000..06972d26b --- /dev/null +++ b/aspnet-core/tests/LINGYUN.Abp.WxPusher.Tests/LINGYUN/Abp/WxPusher/Messages/WxPusherMessageSenderTests.cs @@ -0,0 +1,60 @@ +using Shouldly; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace LINGYUN.Abp.WxPusher.Messages; + +public class WxPusherMessageSenderTests : AbpWxPusherTestBase +{ + protected IWxPusherMessageSender WxPusherMessageSender { get; } + public WxPusherMessageSenderTests() + { + WxPusherMessageSender = GetRequiredService(); + } + + [Theory] + [InlineData("Content from the Xunit unit test. \r\n Click the link at the top to redirect baidu site.")] + public async virtual Task Send_Text_Test(string content) + { + var result = await WxPusherMessageSender + .SendAsync( + content, + contentType: MessageContentType.Text, + topicIds: new List { 7182 }, + url: "https://www.baidu.com/"); + + result.ShouldNotBeNull(); + result.Count.ShouldBeGreaterThanOrEqualTo(1); + } + + [Theory] + [InlineData("Content from the Xunit unit test.
Click to redirect baidu site.")] + public async virtual Task Send_Html_Test(string content) + { + var result = await WxPusherMessageSender + .SendAsync( + content, + contentType: MessageContentType.Html, + topicIds: new List { 7182 }, + url: "https://www.baidu.com/"); + + result.ShouldNotBeNull(); + result.Count.ShouldBeGreaterThanOrEqualTo(1); + } + + [Theory] + [InlineData("**Content from the Xunit unit test.**
Click to redirect baidu site.")] + public async virtual Task Send_Markdown_Test(string content) + { + var result = await WxPusherMessageSender + .SendAsync( + content, + contentType: MessageContentType.Markdown, + topicIds: new List { 7182 }, + url: "https://www.baidu.com/"); + + result.ShouldNotBeNull(); + result.Count.ShouldBeGreaterThanOrEqualTo(1); + } +}