From c2f5da5fbd9cc7dfbf7997eb3a3fa26e2ffe0989 Mon Sep 17 00:00:00 2001
From: cKey <35512826+colinin@users.noreply.github.com>
Date: Thu, 25 Aug 2022 20:03:53 +0800
Subject: [PATCH] feature: Integrated PushPlus.
---
aspnet-core/LINGYUN.MicroService.Common.sln | 38 ++++
.../FodyWeavers.xml | 3 +
.../LINGYUN.Abp.Notifications.PushPlus.csproj | 20 ++
.../NotificationDefinitionExtensions.cs | 36 +++
.../AbpNotificationsPushPlusModule.cs | 18 ++
.../PushPlusNotificationPublishProvider.cs | 88 +++++++
.../README.md | 16 ++
.../LINGYUN.Abp.PushPlus/FodyWeavers.xml | 3 +
.../LINGYUN.Abp.PushPlus/FodyWeavers.xsd | 30 +++
.../LINGYUN.Abp.PushPlus.csproj | 29 +++
.../LINGYUN/Abp/PushPlus/AbpPushPlusModule.cs | 53 +++++
.../PushPlus/Channel/PushPlusChannelType.cs | 27 +++
.../Channel/PushPlusChannelTypeExtensions.cs | 16 ++
.../Webhook/IPushPlusWebhookProvider.cs | 62 +++++
.../Channel/Webhook/PushPlusWebhook.cs | 44 ++++
.../Webhook/PushPlusWebhookProvider.cs | 125 ++++++++++
.../Channel/Webhook/PushPlusWebhookType.cs | 20 ++
.../Webhook/WebhookHttpClientExtensions.cs | 108 +++++++++
.../PushPlusFeatureDefinitionProvider.cs | 168 ++++++++++++++
.../PushPlus/Features/PushPlusFeatureNames.cs | 103 +++++++++
.../PushPlus/Localization/PushPlusResource.cs | 8 +
.../PushPlus/Localization/Resources/en.json | 47 ++++
.../Localization/Resources/zh-Hans.json | 47 ++++
.../Message/IPushPlusMessageProvider.cs | 28 +++
.../Message/IPushPlusMessageSender.cs | 52 +++++
.../Message/MessageHttpClientExtensions.cs | 88 +++++++
.../Abp/PushPlus/Message/PushPlusMessage.cs | 44 ++++
.../Message/PushPlusMessageProvider.cs | 67 ++++++
.../PushPlus/Message/PushPlusMessageSender.cs | 214 ++++++++++++++++++
.../PushPlus/Message/PushPlusMessageStatus.cs | 21 ++
.../Message/PushPlusMessageTemplate.cs | 35 +++
.../PushPlus/Message/PushPlusMessageType.cs | 13 ++
.../Message/SendPushPlusMessageResult.cs | 27 +++
.../Abp/PushPlus/PushPlusPagedResponse.cs | 38 ++++
.../Abp/PushPlus/PushPlusRequestException.cs | 14 ++
.../LINGYUN/Abp/PushPlus/PushPlusResponse.cs | 58 +++++
.../Setting/IPushPlusChannelProvider.cs | 38 ++++
.../Abp/PushPlus/Setting/PushPlusChannel.cs | 35 +++
.../Setting/PushPlusChannelProvider.cs | 83 +++++++
.../Setting/PushPlusChannelRecevieLimit.cs | 15 ++
.../Setting/PushPlusChannelSendLimit.cs | 27 +++
.../Setting/SettingHttpClientExtensions.cs | 65 ++++++
.../PushPlusSettingDefinitionProvider.cs | 42 ++++
.../PushPlus/Settings/PushPlusSettingNames.cs | 15 ++
.../PushPlus/Token/IPushPlusTokenProvider.cs | 16 ++
.../Abp/PushPlus/Token/PushPlusToken.cs | 28 +++
.../PushPlus/Token/PushPlusTokenCacheItem.cs | 26 +++
.../PushPlus/Token/PushPlusTokenProvider.cs | 104 +++++++++
.../Token/TokenHttpClientExtensions.cs | 29 +++
.../PushPlus/Topic/IPushPlusTopicProvider.cs | 100 ++++++++
.../Abp/PushPlus/Topic/PushPlusTopic.cs | 30 +++
.../Abp/PushPlus/Topic/PushPlusTopicForMe.cs | 19 ++
.../PushPlus/Topic/PushPlusTopicProfile.cs | 34 +++
.../PushPlus/Topic/PushPlusTopicProvider.cs | 196 ++++++++++++++++
.../Abp/PushPlus/Topic/PushPlusTopicQrCode.cs | 19 ++
.../PushPlus/Topic/PushPlusTopicQrCodeType.cs | 12 +
.../Abp/PushPlus/Topic/PushPlusTopicType.cs | 12 +
.../Abp/PushPlus/Topic/PushPlusTopicUser.cs | 48 ++++
.../Topic/TopicHttpClientExtensions.cs | 177 +++++++++++++++
.../PushPlus/User/IPushPlusUserProvider.cs | 28 +++
.../PushPlus/User/PushPlusUserEmailStatus.cs | 17 ++
.../PushPlus/User/PushPlusUserFollowStatus.cs | 12 +
.../PushPlus/User/PushPlusUserLimitTime.cs | 20 ++
.../User/PushPlusUserPhoneBindStatus.cs | 12 +
.../Abp/PushPlus/User/PushPlusUserProfile.cs | 69 ++++++
.../Abp/PushPlus/User/PushPlusUserProvider.cs | 74 ++++++
.../PushPlus/User/PushPlusUserSendLimit.cs | 17 ++
.../Abp/PushPlus/User/PushPlusUserSex.cs | 16 ++
.../PushPlus/User/UserHttpClientExtensions.cs | 56 +++++
.../IServiceConnectionExtensions.cs | 19 ++
.../pushplus/LINGYUN.Abp.PushPlus/README.md | 42 ++++
.../Net/Http/IHttpClientFactoryExtensions.cs | 10 +
.../LINGYUN.Abp.PushPlus.Tests.csproj | 18 ++
.../Abp/PushPlus/AbpPushPlusTestBase.cs | 7 +
.../Abp/PushPlus/AbpPushPlusTestModule.cs | 24 ++
.../Message/PushPlusMessageSenderTests.cs | 40 ++++
.../Abp/Tests/Features/FakeFeatureStore.cs | 7 +-
77 files changed, 3464 insertions(+), 2 deletions(-)
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/FodyWeavers.xml
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN.Abp.Notifications.PushPlus.csproj
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/NotificationDefinitionExtensions.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/PushPlus/AbpNotificationsPushPlusModule.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/PushPlus/PushPlusNotificationPublishProvider.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/README.md
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/FodyWeavers.xml
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/FodyWeavers.xsd
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN.Abp.PushPlus.csproj
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/AbpPushPlusModule.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/PushPlusChannelType.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/PushPlusChannelTypeExtensions.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/Webhook/IPushPlusWebhookProvider.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/Webhook/PushPlusWebhook.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/Webhook/PushPlusWebhookProvider.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/Webhook/PushPlusWebhookType.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/Webhook/WebhookHttpClientExtensions.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Features/PushPlusFeatureDefinitionProvider.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Features/PushPlusFeatureNames.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Localization/PushPlusResource.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Localization/Resources/en.json
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Localization/Resources/zh-Hans.json
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/IPushPlusMessageProvider.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/IPushPlusMessageSender.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/MessageHttpClientExtensions.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessage.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessageProvider.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessageSender.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessageStatus.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessageTemplate.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessageType.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/SendPushPlusMessageResult.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/PushPlusPagedResponse.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/PushPlusRequestException.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/PushPlusResponse.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/IPushPlusChannelProvider.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/PushPlusChannel.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/PushPlusChannelProvider.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/PushPlusChannelRecevieLimit.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/PushPlusChannelSendLimit.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/SettingHttpClientExtensions.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Settings/PushPlusSettingDefinitionProvider.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Settings/PushPlusSettingNames.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Token/IPushPlusTokenProvider.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Token/PushPlusToken.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Token/PushPlusTokenCacheItem.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Token/PushPlusTokenProvider.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Token/TokenHttpClientExtensions.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/IPushPlusTopicProvider.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopic.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicForMe.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicProfile.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicProvider.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicQrCode.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicQrCodeType.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicType.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicUser.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/TopicHttpClientExtensions.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/IPushPlusUserProvider.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserEmailStatus.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserFollowStatus.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserLimitTime.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserPhoneBindStatus.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserProfile.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserProvider.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserSendLimit.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserSex.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/UserHttpClientExtensions.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/Microsoft/Extensions/DependencyInjection/IServiceConnectionExtensions.cs
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/README.md
create mode 100644 aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/System/Net/Http/IHttpClientFactoryExtensions.cs
create mode 100644 aspnet-core/tests/LINGYUN.Abp.PushPlus.Tests/LINGYUN.Abp.PushPlus.Tests.csproj
create mode 100644 aspnet-core/tests/LINGYUN.Abp.PushPlus.Tests/LINGYUN/Abp/PushPlus/AbpPushPlusTestBase.cs
create mode 100644 aspnet-core/tests/LINGYUN.Abp.PushPlus.Tests/LINGYUN/Abp/PushPlus/AbpPushPlusTestModule.cs
create mode 100644 aspnet-core/tests/LINGYUN.Abp.PushPlus.Tests/LINGYUN/Abp/PushPlus/Message/PushPlusMessageSenderTests.cs
diff --git a/aspnet-core/LINGYUN.MicroService.Common.sln b/aspnet-core/LINGYUN.MicroService.Common.sln
index 8fc9805b8..6ea7b8d2d 100644
--- a/aspnet-core/LINGYUN.MicroService.Common.sln
+++ b/aspnet-core/LINGYUN.MicroService.Common.sln
@@ -238,6 +238,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Identity.Organi
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.DistributedLocking.Dapr", "modules\dapr\LINGYUN.Abp.DistributedLocking.Dapr\LINGYUN.Abp.DistributedLocking.Dapr.csproj", "{C71F6273-BCDE-4A63-A0CF-EAFD1D924DA0}"
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}"
+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}"
+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}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -608,6 +620,26 @@ Global
{C71F6273-BCDE-4A63-A0CF-EAFD1D924DA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C71F6273-BCDE-4A63-A0CF-EAFD1D924DA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C71F6273-BCDE-4A63-A0CF-EAFD1D924DA0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5515C7CA-B512-4E36-A202-49A0158A0E74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5515C7CA-B512-4E36-A202-49A0158A0E74}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5515C7CA-B512-4E36-A202-49A0158A0E74}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5515C7CA-B512-4E36-A202-49A0158A0E74}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EBA67EAD-4958-46E3-9E0C-8186394D083F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EBA67EAD-4958-46E3-9E0C-8186394D083F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EBA67EAD-4958-46E3-9E0C-8186394D083F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EBA67EAD-4958-46E3-9E0C-8186394D083F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {25891EE2-3166-420F-8408-E458030C4643}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {25891EE2-3166-420F-8408-E458030C4643}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {25891EE2-3166-420F-8408-E458030C4643}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {25891EE2-3166-420F-8408-E458030C4643}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F051C960-AA61-4283-A088-611C0B96C953}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F051C960-AA61-4283-A088-611C0B96C953}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F051C960-AA61-4283-A088-611C0B96C953}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F051C960-AA61-4283-A088-611C0B96C953}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1435711B-D796-42AB-B567-0BB23F02EE08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -726,6 +758,12 @@ Global
{D9339CBB-45B9-4701-B2AC-2A75FF20D77B} = {3971AD93-BF97-4E05-972D-CB5EB9F6CB88}
{474AA48F-65F9-436B-A0B1-1E95BD16CA8D} = {3971AD93-BF97-4E05-972D-CB5EB9F6CB88}
{C71F6273-BCDE-4A63-A0CF-EAFD1D924DA0} = {7FDFB22F-1BFF-4E05-9427-78B7A8461D50}
+ {0F5A2591-CE08-4184-A5F3-89F6FB3B2B10} = {02EA4E78-5891-43BC-944F-3E52FEE032E4}
+ {5515C7CA-B512-4E36-A202-49A0158A0E74} = {0F5A2591-CE08-4184-A5F3-89F6FB3B2B10}
+ {EBA67EAD-4958-46E3-9E0C-8186394D083F} = {0F5A2591-CE08-4184-A5F3-89F6FB3B2B10}
+ {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}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {06C707C6-02C0-411A-AD3B-2D0E13787CB8}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/FodyWeavers.xml b/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/FodyWeavers.xml
new file mode 100644
index 000000000..1715698cc
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
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
new file mode 100644
index 000000000..043ae1b8c
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN.Abp.Notifications.PushPlus.csproj
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/NotificationDefinitionExtensions.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/NotificationDefinitionExtensions.cs
new file mode 100644
index 000000000..8d93ece6c
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/NotificationDefinitionExtensions.cs
@@ -0,0 +1,36 @@
+namespace LINGYUN.Abp.Notifications;
+public static class NotificationDefinitionExtensions
+{
+ ///
+ /// 消息群发群组编码
+ /// see: https://www.pushplus.plus/doc/guide/api.html#%E4%B8%80%E3%80%81%E5%8F%91%E9%80%81%E6%B6%88%E6%81%AF%E6%8E%A5%E5%8F%A3
+ ///
+ /// 群组编码
+ ///
+ ///
+ ///
+ ///
+ public static NotificationDefinition WithTopic(
+ this NotificationDefinition notification,
+ string topic)
+ {
+ return notification.WithProperty("topic", topic);
+ }
+ ///
+ /// 获取消息群发群组编码
+ ///
+ ///
+ ///
+ /// 通知定义的群组编码,未定义返回null
+ ///
+ public static string GetTopicOrNull(
+ this NotificationDefinition notification)
+ {
+ if (notification.Properties.TryGetValue("topic", out var topicDefine) == true)
+ {
+ return topicDefine.ToString();
+ }
+
+ return null;
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/PushPlus/AbpNotificationsPushPlusModule.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/PushPlus/AbpNotificationsPushPlusModule.cs
new file mode 100644
index 000000000..58eb09ec4
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/PushPlus/AbpNotificationsPushPlusModule.cs
@@ -0,0 +1,18 @@
+using LINGYUN.Abp.PushPlus;
+using Volo.Abp.Modularity;
+
+namespace LINGYUN.Abp.Notifications.PushPlus;
+
+[DependsOn(
+ typeof(AbpNotificationModule),
+ typeof(AbpPushPlusModule))]
+public class AbpNotificationsPushPlusModule : AbpModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ Configure(options =>
+ {
+ options.PublishProviders.Add();
+ });
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/PushPlus/PushPlusNotificationPublishProvider.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/PushPlus/PushPlusNotificationPublishProvider.cs
new file mode 100644
index 000000000..ec69e5363
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/LINGYUN/Abp/Notifications/PushPlus/PushPlusNotificationPublishProvider.cs
@@ -0,0 +1,88 @@
+using LINGYUN.Abp.PushPlus.Message;
+using LINGYUN.Abp.RealTime.Localization;
+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.PushPlus;
+
+public class PushPlusNotificationPublishProvider : NotificationPublishProvider
+{
+ public const string ProviderName = "PushPlus";
+
+ public override string Name => ProviderName;
+
+ protected IPushPlusMessageSender PushPlusMessageSender { get; }
+
+ protected IStringLocalizerFactory LocalizerFactory { get; }
+
+ protected AbpLocalizationOptions LocalizationOptions { get; }
+
+ protected INotificationDefinitionManager NotificationDefinitionManager { get; }
+
+ public PushPlusNotificationPublishProvider(
+ IPushPlusMessageSender pushPlusMessageSender,
+ IStringLocalizerFactory localizerFactory,
+ IOptions localizationOptions,
+ INotificationDefinitionManager notificationDefinitionManager)
+ {
+ PushPlusMessageSender = pushPlusMessageSender;
+ LocalizerFactory = localizerFactory;
+ LocalizationOptions = localizationOptions.Value;
+ NotificationDefinitionManager = notificationDefinitionManager;
+ }
+
+ protected async override Task PublishAsync(
+ NotificationInfo notification,
+ IEnumerable identifiers,
+ CancellationToken cancellationToken = default)
+ {
+ var topic = "";
+
+ var notificationDefine = NotificationDefinitionManager.GetOrNull(notification.Name);
+ var topicDefine = notificationDefine?.GetTopicOrNull();
+ if (!topicDefine.IsNullOrWhiteSpace())
+ {
+ topic = topicDefine;
+ }
+
+ if (!notification.Data.NeedLocalizer())
+ {
+ var title = notification.Data.TryGetData("title").ToString();
+ var message = notification.Data.TryGetData("message").ToString();
+
+ await PushPlusMessageSender.SendAsync(
+ title,
+ message,
+ topic,
+ 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 PushPlusMessageSender.SendAsync(
+ title,
+ message,
+ topic,
+ cancellationToken: cancellationToken);
+ }
+ }
+
+ private LocalizationResource GetResource(string resourceName)
+ {
+ return LocalizationOptions.Resources.Values
+ .First(x => x.ResourceName.Equals(resourceName));
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/README.md b/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/README.md
new file mode 100644
index 000000000..a1b59cf25
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.Notifications.PushPlus/README.md
@@ -0,0 +1,16 @@
+# LINGYUN.Abp.Notifications.PushPlus
+
+通知模块的PushPlus实现
+
+使应用可通过PushPlus发布实时通知
+
+## 模块引用
+
+```csharp
+[DependsOn(typeof(AbpNotificationsPushPlusModule))]
+public class YouProjectModule : AbpModule
+{
+ // other
+}
+```
+
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/FodyWeavers.xml b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/FodyWeavers.xml
new file mode 100644
index 000000000..1715698cc
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/FodyWeavers.xsd b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/FodyWeavers.xsd
new file mode 100644
index 000000000..11da52550
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/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/pushplus/LINGYUN.Abp.PushPlus/LINGYUN.Abp.PushPlus.csproj b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN.Abp.PushPlus.csproj
new file mode 100644
index 000000000..93331c526
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN.Abp.PushPlus.csproj
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
new file mode 100644
index 000000000..e04247263
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/AbpPushPlusModule.cs
@@ -0,0 +1,53 @@
+using LINGYUN.Abp.Features.LimitValidation;
+using LINGYUN.Abp.PushPlus.Channel.Webhook;
+using LINGYUN.Abp.PushPlus.Message;
+using LINGYUN.Abp.PushPlus.Setting;
+using LINGYUN.Abp.PushPlus.Token;
+using LINGYUN.Abp.PushPlus.Topic;
+using LINGYUN.Abp.PushPlus.User;
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.Caching;
+using Volo.Abp.Json;
+using Volo.Abp.Json.SystemTextJson;
+using Volo.Abp.Modularity;
+using Volo.Abp.Settings;
+
+namespace LINGYUN.Abp.PushPlus;
+
+[DependsOn(
+ typeof(AbpJsonModule),
+ typeof(AbpSettingsModule),
+ typeof(AbpCachingModule),
+ typeof(AbpFeaturesLimitValidationModule))]
+public class AbpPushPlusModule : AbpModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.AddPushPlusClient();
+
+ Configure(options =>
+ {
+ options.UnsupportedTypes.TryAdd>();
+ options.UnsupportedTypes.TryAdd>();
+ options.UnsupportedTypes.TryAdd>();
+ options.UnsupportedTypes.TryAdd>();
+
+ options.UnsupportedTypes.TryAdd>();
+ options.UnsupportedTypes.TryAdd>();
+ options.UnsupportedTypes.TryAdd>>();
+
+ options.UnsupportedTypes.TryAdd>();
+
+ options.UnsupportedTypes.TryAdd>();
+
+ options.UnsupportedTypes.TryAdd>();
+ options.UnsupportedTypes.TryAdd>();
+ options.UnsupportedTypes.TryAdd>();
+ options.UnsupportedTypes.TryAdd>>();
+ options.UnsupportedTypes.TryAdd>>();
+
+ options.UnsupportedTypes.TryAdd>();
+ options.UnsupportedTypes.TryAdd>();
+ });
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/PushPlusChannelType.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/PushPlusChannelType.cs
new file mode 100644
index 000000000..a5886b2a9
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/PushPlusChannelType.cs
@@ -0,0 +1,27 @@
+namespace LINGYUN.Abp.PushPlus.Channel;
+///
+/// 发送渠道(channel)枚举
+///
+public enum PushPlusChannelType
+{
+ ///
+ /// 微信公众号
+ ///
+ WeChat = 0,
+ ///
+ /// 第三方webhook
+ ///
+ Webhook = 1,
+ ///
+ /// 企业微信应用
+ ///
+ WeWork = 2,
+ ///
+ /// 邮箱
+ ///
+ Email = 3,
+ ///
+ /// 短信
+ ///
+ Sms = 4
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/PushPlusChannelTypeExtensions.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/PushPlusChannelTypeExtensions.cs
new file mode 100644
index 000000000..437afb756
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/PushPlusChannelTypeExtensions.cs
@@ -0,0 +1,16 @@
+namespace LINGYUN.Abp.PushPlus.Channel;
+public static class PushPlusChannelTypeExtensions
+{
+ public static string GetChannelName(this PushPlusChannelType channelType)
+ {
+ return channelType switch
+ {
+ PushPlusChannelType.WeChat => "wechat",
+ PushPlusChannelType.WeWork => "cp",
+ PushPlusChannelType.Webhook => "webhook",
+ PushPlusChannelType.Email => "mail",
+ PushPlusChannelType.Sms => "sms",
+ _ => "wechat",
+ };
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/Webhook/IPushPlusWebhookProvider.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/Webhook/IPushPlusWebhookProvider.cs
new file mode 100644
index 000000000..04cb4f6b6
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/Webhook/IPushPlusWebhookProvider.cs
@@ -0,0 +1,62 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.PushPlus.Channel.Webhook;
+///
+/// Webhook配置接口
+///
+public interface IPushPlusWebhookProvider
+{
+ ///
+ /// 获取webhook列表
+ ///
+ /// 当前所在分页数
+ /// 每页大小,最大值为50
+ ///
+ ///
+ Task> GetWebhookListAsync(
+ int current = 1,
+ int pageSize = 20,
+ CancellationToken cancellationToken = default);
+ ///
+ /// webhook详情
+ ///
+ /// webhook编号
+ ///
+ ///
+ Task GetWebhookAsync(
+ int webhookId,
+ CancellationToken cancellationToken = default);
+ ///
+ /// 新增webhook
+ ///
+ /// webhook编码
+ /// webhook名称
+ /// webhook类型;1-企业微信,2-钉钉,3-飞书,4-server酱
+ /// 调用的url地址
+ ///
+ ///
+ Task CreateWebhookAsync(
+ string webhookCode,
+ string webhookName,
+ PushPlusWebhookType webhookType,
+ string webhookUrl,
+ CancellationToken cancellationToken = default);
+ ///
+ /// 修改webhook配置
+ ///
+ /// webhook编号
+ /// webhook编码
+ /// webhook名称
+ /// webhook类型;1-企业微信,2-钉钉,3-飞书,4-server酱
+ /// 调用的url地址
+ ///
+ ///
+ Task UpdateWebhookAsync(
+ int id,
+ string webhookCode,
+ string webhookName,
+ PushPlusWebhookType webhookType,
+ string webhookUrl,
+ CancellationToken cancellationToken = default);
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/Webhook/PushPlusWebhook.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/Webhook/PushPlusWebhook.cs
new file mode 100644
index 000000000..8af28f890
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/Webhook/PushPlusWebhook.cs
@@ -0,0 +1,44 @@
+using Newtonsoft.Json;
+using System;
+
+namespace LINGYUN.Abp.PushPlus.Channel.Webhook;
+///
+/// webhook
+///
+public class PushPlusWebhook
+{
+ ///
+ /// webhook编号
+ ///
+ [JsonProperty("id")]
+ public int Id { get; set; }
+ ///
+ /// webhook编码
+ ///
+ [JsonProperty("webhookCode")]
+ public string WebhookCode { get; set; }
+ ///
+ /// webhook名称
+ ///
+ [JsonProperty("webhookName")]
+ public string WebhookName { get; set; }
+ ///
+ /// webhook类型;
+ /// 1-企业微信,
+ /// 2-钉钉,
+ /// 3-飞书,
+ /// 4-server酱
+ ///
+ [JsonProperty("webhookType")]
+ public PushPlusWebhookType WebhookType { get; set; }
+ ///
+ /// 调用的url地址
+ ///
+ [JsonProperty("webhookUrl")]
+ public string WebhookUrl { get; set; }
+ ///
+ /// 创建日期
+ ///
+ [JsonProperty("createTime")]
+ public DateTime CreateTime { get; set; }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/Webhook/PushPlusWebhookProvider.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/Webhook/PushPlusWebhookProvider.cs
new file mode 100644
index 000000000..8bccafb56
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/Webhook/PushPlusWebhookProvider.cs
@@ -0,0 +1,125 @@
+using LINGYUN.Abp.PushPlus.Token;
+using Microsoft.Extensions.Logging;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Json;
+
+namespace LINGYUN.Abp.PushPlus.Channel.Webhook;
+
+public class PushPlusWebhookProvider : IPushPlusWebhookProvider, ITransientDependency
+{
+ protected ILogger Logger { get; }
+ protected IJsonSerializer JsonSerializer { get; }
+ protected IHttpClientFactory HttpClientFactory { get; }
+ protected IPushPlusTokenProvider PushPlusTokenProvider { get; }
+
+ public PushPlusWebhookProvider(
+ ILogger logger,
+ IJsonSerializer jsonSerializer,
+ IHttpClientFactory httpClientFactory,
+ IPushPlusTokenProvider pushPlusTokenProvider)
+ {
+ Logger = logger;
+ JsonSerializer = jsonSerializer;
+ HttpClientFactory = httpClientFactory;
+ PushPlusTokenProvider = pushPlusTokenProvider;
+ }
+
+ public async virtual Task CreateWebhookAsync(
+ string webhookCode,
+ string webhookName,
+ PushPlusWebhookType webhookType,
+ string webhookUrl,
+ CancellationToken cancellationToken = default)
+ {
+ Check.NotNullOrWhiteSpace(webhookCode, nameof(webhookCode));
+ Check.NotNullOrWhiteSpace(webhookName, nameof(webhookName));
+ Check.NotNullOrWhiteSpace(webhookUrl, nameof(webhookUrl));
+ Check.NotNull(webhookType, nameof(webhookType));
+
+ var token = await PushPlusTokenProvider.GetTokenAsync();
+ var client = HttpClientFactory.GetPushPlusClient();
+
+ var content = await client.GetCreateWebhookContentAsync(
+ token.AccessKey,
+ webhookCode,
+ webhookName,
+ webhookType,
+ webhookUrl,
+ cancellationToken);
+
+ var pushPlusResponse = JsonSerializer.Deserialize>(content);
+
+ return pushPlusResponse.GetData();
+ }
+
+ public async virtual Task GetWebhookAsync(
+ int webhookId,
+ CancellationToken cancellationToken = default)
+ {
+ var token = await PushPlusTokenProvider.GetTokenAsync();
+ var client = HttpClientFactory.GetPushPlusClient();
+
+ var content = await client.GetWebhookContentAsync(
+ token.AccessKey,
+ webhookId,
+ cancellationToken);
+
+ var pushPlusResponse = JsonSerializer.Deserialize>(content);
+
+ return pushPlusResponse.GetData();
+ }
+
+ public async virtual Task> GetWebhookListAsync(
+ int current = 1,
+ int pageSize = 20,
+ CancellationToken cancellationToken = default)
+ {
+ var token = await PushPlusTokenProvider.GetTokenAsync();
+ var client = HttpClientFactory.GetPushPlusClient();
+
+ var content = await client.GetWebhookListContentAsync(
+ token.AccessKey,
+ current,
+ pageSize,
+ cancellationToken);
+
+ var pushPlusResponse = JsonSerializer
+ .Deserialize>>(content);
+
+ return pushPlusResponse.GetData();
+ }
+
+ public async virtual Task UpdateWebhookAsync(
+ int id,
+ string webhookCode,
+ string webhookName,
+ PushPlusWebhookType webhookType,
+ string webhookUrl,
+ CancellationToken cancellationToken = default)
+ {
+ Check.NotNullOrWhiteSpace(webhookCode, nameof(webhookCode));
+ Check.NotNullOrWhiteSpace(webhookName, nameof(webhookName));
+ Check.NotNullOrWhiteSpace(webhookUrl, nameof(webhookUrl));
+ Check.NotNull(webhookType, nameof(webhookType));
+
+ var token = await PushPlusTokenProvider.GetTokenAsync();
+ var client = HttpClientFactory.GetPushPlusClient();
+
+ var content = await client.GetUpdateWebhookContentAsync(
+ token.AccessKey,
+ id,
+ webhookCode,
+ webhookName,
+ webhookType,
+ webhookUrl,
+ cancellationToken);
+
+ var pushPlusResponse = JsonSerializer.Deserialize>(content);
+
+ return pushPlusResponse.GetData();
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/Webhook/PushPlusWebhookType.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/Webhook/PushPlusWebhookType.cs
new file mode 100644
index 000000000..5619622bc
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/Webhook/PushPlusWebhookType.cs
@@ -0,0 +1,20 @@
+namespace LINGYUN.Abp.PushPlus.Channel.Webhook;
+public enum PushPlusWebhookType
+{
+ ///
+ /// 企业微信
+ ///
+ WeWork = 1,
+ ///
+ /// 钉钉
+ ///
+ DingTalk = 2,
+ ///
+ /// 飞书
+ ///
+ FeiShu = 3,
+ ///
+ /// Server酱
+ ///
+ SctFtqq = 4,
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/Webhook/WebhookHttpClientExtensions.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/Webhook/WebhookHttpClientExtensions.cs
new file mode 100644
index 000000000..35e6cdab5
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Channel/Webhook/WebhookHttpClientExtensions.cs
@@ -0,0 +1,108 @@
+using System.Net.Http;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.PushPlus.Channel.Webhook;
+
+internal static class WebhookHttpClientExtensions
+{
+ private const string _getWebhookListTemplate = "{\"current\":$current,\"pageSize\":$pageSize}";
+ public async static Task GetWebhookListContentAsync(
+ this HttpClient httpClient,
+ string accessKey,
+ int current,
+ int pageSize,
+ CancellationToken cancellationToken = default)
+ {
+ var requestMessage = new HttpRequestMessage(
+ HttpMethod.Post,
+ "/api/open/webhook/list");
+
+ var requestBody = _getWebhookListTemplate
+ .Replace("$current", current.ToString())
+ .Replace("$pageSize", pageSize.ToString());
+
+ requestMessage.Content = new StringContent(requestBody, Encoding.UTF8, "application/json");
+ requestMessage.Headers.TryAddWithoutValidation("access-key", accessKey);
+
+ var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken);
+
+ return await httpResponse.Content.ReadAsStringAsync();
+ }
+
+ public async static Task GetWebhookContentAsync(
+ this HttpClient httpClient,
+ string accessKey,
+ int webhookId,
+ CancellationToken cancellationToken = default)
+ {
+ var requestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ $"/api/open/webhook/detail?webhookId={webhookId}");
+
+ requestMessage.Headers.TryAddWithoutValidation("access-key", accessKey);
+
+ var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken);
+
+ return await httpResponse.Content.ReadAsStringAsync();
+ }
+
+ private const string _createWebhookTemplate = "{\"webhookCode\":\"$webhookCode\",\"webhookName\":\"$webhookName\",\"webhookType\":$webhookType,\"webhookUrl\":\"$webhookUrl\"}";
+ public async static Task GetCreateWebhookContentAsync(
+ this HttpClient httpClient,
+ string accessKey,
+ string webhookCode,
+ string webhookName,
+ PushPlusWebhookType webhookType,
+ string webhookUrl,
+ CancellationToken cancellationToken = default)
+ {
+ var requestMessage = new HttpRequestMessage(
+ HttpMethod.Post,
+ "/api/open/webhook/add");
+
+ var requestBody = _createWebhookTemplate
+ .Replace("$webhookCode", webhookCode)
+ .Replace("$webhookName", webhookName)
+ .Replace("$webhookType", ((int)webhookType).ToString())
+ .Replace("$webhookUrl", webhookUrl);
+
+ requestMessage.Content = new StringContent(requestBody, Encoding.UTF8, "application/json");
+ requestMessage.Headers.TryAddWithoutValidation("access-key", accessKey);
+
+ var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken);
+
+ return await httpResponse.Content.ReadAsStringAsync();
+ }
+
+ private const string _updateWebhookTemplate = "{\"id\":$id,\"webhookCode\":\"$webhookCode\",\"webhookName\":\"$webhookName\",\"webhookType\":$webhookType,\"webhookUrl\":\"$webhookUrl\"}";
+ public async static Task GetUpdateWebhookContentAsync(
+ this HttpClient httpClient,
+ string accessKey,
+ int id,
+ string webhookCode,
+ string webhookName,
+ PushPlusWebhookType webhookType,
+ string webhookUrl,
+ CancellationToken cancellationToken = default)
+ {
+ var requestMessage = new HttpRequestMessage(
+ HttpMethod.Post,
+ "/api/open/topic/edit");
+
+ var requestBody = _updateWebhookTemplate
+ .Replace("$id", id.ToString())
+ .Replace("$webhookCode", webhookCode)
+ .Replace("$webhookName", webhookName)
+ .Replace("$webhookType", ((int)webhookType).ToString())
+ .Replace("$webhookUrl", webhookUrl);
+
+ requestMessage.Content = new StringContent(requestBody, Encoding.UTF8, "application/json");
+ requestMessage.Headers.TryAddWithoutValidation("access-key", accessKey);
+
+ var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken);
+
+ return await httpResponse.Content.ReadAsStringAsync();
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Features/PushPlusFeatureDefinitionProvider.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Features/PushPlusFeatureDefinitionProvider.cs
new file mode 100644
index 000000000..57797b2eb
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Features/PushPlusFeatureDefinitionProvider.cs
@@ -0,0 +1,168 @@
+using LINGYUN.Abp.PushPlus.Localization;
+using Volo.Abp.Features;
+using Volo.Abp.Localization;
+using Volo.Abp.Validation.StringValues;
+
+namespace LINGYUN.Abp.PushPlus.Features;
+public class PushPlusFeatureDefinitionProvider : FeatureDefinitionProvider
+{
+ public override void Define(IFeatureDefinitionContext context)
+ {
+ var group = context.AddGroup(
+ name: PushPlusFeatureNames.GroupName,
+ displayName: L("Features:PushPlus"));
+
+ group.AddFeature(
+ name: PushPlusFeatureNames.Message.Enable,
+ defaultValue: "true",
+ displayName: L("Features:MessageEnable"),
+ description: L("Features:MessageEnableDesc"),
+ valueType: new ToggleStringValueType(new BooleanValueValidator()));
+
+ CreateWeChatChannelFeature(group);
+ CreateWeWorkChannelFeature(group);
+ CreateWebhookChannelFeature(group);
+ CreateEmailChannelFeature(group);
+ CreateSmsChannelFeature(group);
+ }
+
+ private static void CreateWeChatChannelFeature(
+ FeatureGroupDefinition group)
+ {
+ var weChatChannel = group.AddFeature(
+ name: PushPlusFeatureNames.Channel.WeChat.GroupName,
+ displayName: L("Features:Channel.WeChat"),
+ description: L("Features:Channel.WeChat"));
+ weChatChannel.CreateChild(
+ name: PushPlusFeatureNames.Channel.WeChat.Enable,
+ defaultValue: "true",
+ displayName: L("Features:Channel.WeChat.Enable"),
+ description: L("Features:Channel.WeChat.EnableDesc"),
+ valueType: new ToggleStringValueType(new BooleanValueValidator()));
+ weChatChannel.CreateChild(
+ name: PushPlusFeatureNames.Channel.WeChat.SendLimit,
+ defaultValue: "200",
+ displayName: L("Features:Channel.WeChat.SendLimit"),
+ description: L("Features:Channel.WeChat.SendLimitDesc"),
+ valueType: new FreeTextStringValueType(new NumericValueValidator(1, 1000)));
+ weChatChannel.CreateChild(
+ name: PushPlusFeatureNames.Channel.WeChat.SendLimitInterval,
+ defaultValue: "1",
+ displayName: L("Features:Channel.WeChat.SendLimitInterval"),
+ description: L("Features:Channel.WeChat.SendLimitIntervalDesc"),
+ valueType: new FreeTextStringValueType(new NumericValueValidator(1, 1)));
+ }
+
+ private static void CreateWeWorkChannelFeature(
+ FeatureGroupDefinition group)
+ {
+ var weWorkChannel = group.AddFeature(
+ name: PushPlusFeatureNames.Channel.WeWork.GroupName,
+ displayName: L("Features:Channel.WeWork"),
+ description: L("Features:Channel.WeWork"));
+ weWorkChannel.CreateChild(
+ name: PushPlusFeatureNames.Channel.WeWork.Enable,
+ defaultValue: "true",
+ displayName: L("Features:Channel.WeWork.Enable"),
+ description: L("Features:Channel.WeWork.EnableDesc"),
+ valueType: new ToggleStringValueType(new BooleanValueValidator()));
+ weWorkChannel.CreateChild(
+ name: PushPlusFeatureNames.Channel.WeWork.SendLimit,
+ defaultValue: "200",
+ displayName: L("Features:Channel.WeWork.SendLimit"),
+ description: L("Features:Channel.WeWork.SendLimitDesc"),
+ valueType: new FreeTextStringValueType(new NumericValueValidator(1, 10000)));
+ weWorkChannel.CreateChild(
+ name: PushPlusFeatureNames.Channel.WeWork.SendLimitInterval,
+ defaultValue: "1",
+ displayName: L("Features:Channel.WeWork.SendLimitInterval"),
+ description: L("Features:Channel.WeWork.SendLimitIntervalDesc"),
+ valueType: new FreeTextStringValueType(new NumericValueValidator(1, 1)));
+ }
+
+ private static void CreateWebhookChannelFeature(
+ FeatureGroupDefinition group)
+ {
+ var webhookChannel = group.AddFeature(
+ name: PushPlusFeatureNames.Channel.Webhook.GroupName,
+ displayName: L("Features:Channel.Webhook"),
+ description: L("Features:Channel.Webhook"));
+ webhookChannel.CreateChild(
+ name: PushPlusFeatureNames.Channel.Webhook.Enable,
+ defaultValue: "true",
+ displayName: L("Features:Channel.Webhook.Enable"),
+ description: L("Features:Channel.Webhook.EnableDesc"),
+ valueType: new ToggleStringValueType(new BooleanValueValidator()));
+ webhookChannel.CreateChild(
+ name: PushPlusFeatureNames.Channel.Webhook.SendLimit,
+ defaultValue: "200",
+ displayName: L("Features:Channel.Webhook.SendLimit"),
+ description: L("Features:Channel.Webhook.SendLimitDesc"),
+ valueType: new FreeTextStringValueType(new NumericValueValidator(1, 10000)));
+ webhookChannel.CreateChild(
+ name: PushPlusFeatureNames.Channel.Webhook.SendLimitInterval,
+ defaultValue: "1",
+ displayName: L("Features:Channel.Webhook.SendLimitInterval"),
+ description: L("Features:Channel.Webhook.SendLimitIntervalDesc"),
+ valueType: new FreeTextStringValueType(new NumericValueValidator(1, 1)));
+ }
+
+ private static void CreateEmailChannelFeature(
+ FeatureGroupDefinition group)
+ {
+ var emailChannel = group.AddFeature(
+ name: PushPlusFeatureNames.Channel.Email.GroupName,
+ displayName: L("Features:Channel.Email"),
+ description: L("Features:Channel.Email"));
+ emailChannel.CreateChild(
+ name: PushPlusFeatureNames.Channel.Email.Enable,
+ defaultValue: "true",
+ displayName: L("Features:Channel.Email.Enable"),
+ description: L("Features:Channel.Email.EnableDesc"),
+ valueType: new ToggleStringValueType(new BooleanValueValidator()));
+ emailChannel.CreateChild(
+ name: PushPlusFeatureNames.Channel.Email.SendLimit,
+ defaultValue: "200",
+ displayName: L("Features:Channel.Email.SendLimit"),
+ description: L("Features:Channel.Email.SendLimitDesc"),
+ valueType: new FreeTextStringValueType(new NumericValueValidator(1, 1000)));
+ emailChannel.CreateChild(
+ name: PushPlusFeatureNames.Channel.Email.SendLimitInterval,
+ defaultValue: "1",
+ displayName: L("Features:Channel.Email.SendLimitInterval"),
+ description: L("Features:Channel.Email.SendLimitIntervalDesc"),
+ valueType: new FreeTextStringValueType(new NumericValueValidator(1, 1)));
+ }
+
+ private static void CreateSmsChannelFeature(
+ FeatureGroupDefinition group)
+ {
+ var smsChannel = group.AddFeature(
+ name: PushPlusFeatureNames.Channel.Sms.GroupName,
+ displayName: L("Features:Channel.Sms"),
+ description: L("Features:Channel.Sms"));
+ smsChannel.CreateChild(
+ name: PushPlusFeatureNames.Channel.Sms.Enable,
+ defaultValue: "false",
+ displayName: L("Features:Channel.Sms.Enable"),
+ description: L("Features:Channel.Sms.EnableDesc"),
+ valueType: new ToggleStringValueType(new BooleanValueValidator()));
+ smsChannel.CreateChild(
+ name: PushPlusFeatureNames.Channel.Sms.SendLimit,
+ defaultValue: "200",
+ displayName: L("Features:Channel.Sms.SendLimit"),
+ description: L("Features:Channel.Sms.SendLimitDesc"),
+ valueType: new FreeTextStringValueType(new NumericValueValidator(1, 1000)));
+ smsChannel.CreateChild(
+ name: PushPlusFeatureNames.Channel.Sms.SendLimitInterval,
+ defaultValue: "1",
+ displayName: L("Features:Channel.Sms.SendLimitInterval"),
+ description: L("Features:Channel.Sms.SendLimitIntervalDesc"),
+ valueType: new FreeTextStringValueType(new NumericValueValidator(1, 1)));
+ }
+
+ private static LocalizableString L(string name)
+ {
+ return LocalizableString.Create(name);
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Features/PushPlusFeatureNames.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Features/PushPlusFeatureNames.cs
new file mode 100644
index 000000000..73036c45d
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Features/PushPlusFeatureNames.cs
@@ -0,0 +1,103 @@
+namespace LINGYUN.Abp.PushPlus.Features;
+
+public static class PushPlusFeatureNames
+{
+ public const string GroupName = "PushPlus";
+
+ public static class Message
+ {
+ public const string GroupName = PushPlusFeatureNames.GroupName + ".Message";
+
+ public const string Enable = GroupName + ".Enable";
+ }
+
+ public static class Channel
+ {
+ public const string GroupName = PushPlusFeatureNames.GroupName + ".Channel";
+
+ public static class WeChat
+ {
+ public const string GroupName = Channel.GroupName + ".WeChat";
+ ///
+ /// 启用微信通道
+ ///
+ public const string Enable = GroupName + ".Enable";
+ ///
+ /// 发送次数上限
+ ///
+ public const string SendLimit = GroupName + ".SendLimit";
+ ///
+ /// 发送次数上限时长
+ ///
+ public const string SendLimitInterval = GroupName + ".SendLimitInterval";
+ }
+
+ public static class WeWork
+ {
+ public const string GroupName = Channel.GroupName + ".WeWork";
+ ///
+ /// 启用企业微信通道
+ ///
+ public const string Enable = GroupName + ".Enable";
+ ///
+ /// 发送次数上限
+ ///
+ public const string SendLimit = GroupName + ".SendLimit";
+ ///
+ /// 发送次数上限时长
+ ///
+ public const string SendLimitInterval = GroupName + ".SendLimitInterval";
+ }
+
+ public static class Webhook
+ {
+ public const string GroupName = Channel.GroupName + ".Webhook";
+ ///
+ /// 启用Webhook通道
+ ///
+ public const string Enable = GroupName + ".Enable";
+ ///
+ /// 发送次数上限
+ ///
+ public const string SendLimit = GroupName + ".SendLimit";
+ ///
+ /// 发送次数上限时长
+ ///
+ public const string SendLimitInterval = GroupName + ".SendLimitInterval";
+ }
+
+ public static class Email
+ {
+ public const string GroupName = Channel.GroupName + ".Email";
+ ///
+ /// 启用Email通道
+ ///
+ public const string Enable = GroupName + ".Enable";
+ ///
+ /// 发送次数上限
+ ///
+ public const string SendLimit = GroupName + ".SendLimit";
+ ///
+ /// 发送次数上限时长
+ ///
+ public const string SendLimitInterval = GroupName + ".SendLimitInterval";
+ }
+
+ public static class Sms
+ {
+ public const string GroupName = Channel.GroupName + ".Sms";
+ ///
+ /// 启用Sms通道
+ ///
+ public const string Enable = GroupName + ".Enable";
+ ///
+ /// 发送次数上限
+ ///
+ public const string SendLimit = GroupName + ".SendLimit";
+ ///
+ /// 发送次数上限时长
+ ///
+ public const string SendLimitInterval = GroupName + ".SendLimitInterval";
+ }
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Localization/PushPlusResource.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Localization/PushPlusResource.cs
new file mode 100644
index 000000000..2a3b4f21e
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Localization/PushPlusResource.cs
@@ -0,0 +1,8 @@
+using Volo.Abp.Localization;
+
+namespace LINGYUN.Abp.PushPlus.Localization;
+
+[LocalizationResourceName("PushPlus")]
+public class PushPlusResource
+{
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Localization/Resources/en.json b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Localization/Resources/en.json
new file mode 100644
index 000000000..c89f605bf
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Localization/Resources/en.json
@@ -0,0 +1,47 @@
+{
+ "culture": "en",
+ "texts": {
+ "Settings:Security.Token": "User Token",
+ "Settings:Security.TokenDesc": "It can be obtained after logging in to the public account of PushPlus.",
+ "Settings:Security.SecretKey": "Secret Key",
+ "Settings:Security.SecretKeyDesc": "It can be obtained after logging in to the public account of PushPlus.",
+ "Features:PushPlus": "PushPlus",
+ "Features:MessageEnable": "Enable PushPlus",
+ "Features:MessageEnableDesc": "Enable so that the application will have the ability to push messages via PushPlus.",
+ "Features:Channel.WeChat": "Wechat Channel",
+ "Features:Channel.WeChat.Enable": "Enable Wechat Channel",
+ "Features:Channel.WeChat.EnableDesc": "Enable to allow PushPlus to push messages via the wechat official account.",
+ "Features:Channel.WeChat.SendLimit": "Wechat Channel Limit",
+ "Features:Channel.WeChat.SendLimitDesc": "Set to limit the amount of wechat channel push.",
+ "Features:Channel.WeChat.SendLimitInterval": "Wechat Channel Limit Interval",
+ "Features:Channel.WeChat.SendLimitIntervalDesc": "Set the limit period of wechat channel (time scale: days).",
+ "Features:Channel.WeWork": "WeWork Channel",
+ "Features:Channel.WeWork.Enable": "Enable WeWork Channel",
+ "Features:Channel.WeWork.EnableDesc": "Enable to allow PushPlus to push messages via the enterprise wechat.",
+ "Features:Channel.WeWork.SendLimit": "WeWork Channel Limit",
+ "Features:Channel.WeWork.SendLimitDesc": "Set to limit the amount of enterprise wechat channel push.",
+ "Features:Channel.WeWork.SendLimitInterval": "WeWork Channel Limit Interval",
+ "Features:Channel.WeWork.SendLimitIntervalDesc": "Set the limit period of enterprise wechat channel (time scale: days).",
+ "Features:Channel.Webhook": "Webhook Channel",
+ "Features:Channel.Webhook.Enable": "Enable Webhook Channel",
+ "Features:Channel.Webhook.EnableDesc": "Enable to allow PushPlus to push messages through third-party webhooks.",
+ "Features:Channel.Webhook.SendLimit": "Webhook Channel Limit",
+ "Features:Channel.Webhook.SendLimitDesc": "Set to limit the amount of Webhook channel push.",
+ "Features:Channel.Webhook.SendLimitInterval": "Webhook Channel Limit Interval",
+ "Features:Channel.Webhook.SendLimitIntervalDesc": "Set the Webhook channel limit period (time scale: days).",
+ "Features:Channel.Email": "Email Channel",
+ "Features:Channel.Email.Enable": "Enable Email Channel",
+ "Features:Channel.Email.EnableDesc": "Enable to allow PushPlus to push messages via Email.",
+ "Features:Channel.Email.SendLimit": "Email Channel Limit",
+ "Features:Channel.Email.SendLimitDesc": "Set to limit the amount of Email channel push.",
+ "Features:Channel.Email.SendLimitInterval": "Email Channel Limit Interval",
+ "Features:Channel.Email.SendLimitIntervalDesc": "Set the Email channel limit period (time scale: days).",
+ "Features:Channel.Sms": "Sms Channel",
+ "Features:Channel.Sms.Enable": "Enable Sms Channel",
+ "Features:Channel.Sms.EnableDesc": "Enable to allow PushPlus to push messages via sms.",
+ "Features:Channel.Sms.SendLimit": "Sms Channel Limit",
+ "Features:Channel.Sms.SendLimitDesc": "Set to limit the amount of sms channel push.",
+ "Features:Channel.Sms.SendLimitInterval": "Sms Channel Limit Interval",
+ "Features:Channel.Sms.SendLimitIntervalDesc": "Set the sms channel limit period (time scale: days)."
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Localization/Resources/zh-Hans.json b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Localization/Resources/zh-Hans.json
new file mode 100644
index 000000000..90782486e
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Localization/Resources/zh-Hans.json
@@ -0,0 +1,47 @@
+{
+ "culture": "zh-Hans",
+ "texts": {
+ "Settings:Security.Token": "用户令牌",
+ "Settings:Security.TokenDesc": "在PushPlus公众号登录后获取.",
+ "Settings:Security.SecretKey": "用户密钥",
+ "Settings:Security.SecretKeyDesc": "在PushPlus公众号登录后获取.",
+ "Features:PushPlus": "PushPlus(推送加)",
+ "Features:MessageEnable": "启用PushPlus消息推送",
+ "Features:MessageEnableDesc": "启用以使应用将拥有通过PushPlus推送消息的能力.",
+ "Features:Channel.WeChat": "微信消息通道",
+ "Features:Channel.WeChat.Enable": "启用微信消息通道",
+ "Features:Channel.WeChat.EnableDesc": "启用以允许PushPlus通过微信公众号推送消息.",
+ "Features:Channel.WeChat.SendLimit": "微信渠道推送量",
+ "Features:Channel.WeChat.SendLimitDesc": "设置以限制微信渠道推送量.",
+ "Features:Channel.WeChat.SendLimitInterval": "微信渠道限制周期",
+ "Features:Channel.WeChat.SendLimitIntervalDesc": "设置微信渠道限制周期(时间刻度: 天).",
+ "Features:Channel.WeWork": "企业微信消息通道",
+ "Features:Channel.WeWork.Enable": "启用企业微信消息通道",
+ "Features:Channel.WeWork.EnableDesc": "启用以允许PushPlus通过企业微信应用推送消息.",
+ "Features:Channel.WeWork.SendLimit": "企业微信渠道推送量",
+ "Features:Channel.WeWork.SendLimitDesc": "设置以限制企业微信渠道推送量.",
+ "Features:Channel.WeWork.SendLimitInterval": "企业微信渠道限制周期",
+ "Features:Channel.WeWork.SendLimitIntervalDesc": "设置企业微信渠道限制周期(时间刻度: 天).",
+ "Features:Channel.Webhook": "Webhook消息通道",
+ "Features:Channel.Webhook.Enable": "启用Webhook消息通道",
+ "Features:Channel.Webhook.EnableDesc": "启用以允许PushPlus通过第三方Webhook推送消息.",
+ "Features:Channel.Webhook.SendLimit": "Webhook渠道推送量",
+ "Features:Channel.Webhook.SendLimitDesc": "设置以限制Webhook渠道推送量.",
+ "Features:Channel.Webhook.SendLimitInterval": "Webhook渠道限制周期",
+ "Features:Channel.Webhook.SendLimitIntervalDesc": "设置Webhook渠道限制周期(时间刻度: 天).",
+ "Features:Channel.Email": "Email消息通道",
+ "Features:Channel.Email.Enable": "启用Email消息通道",
+ "Features:Channel.Email.EnableDesc": "启用以允许PushPlus通过Email推送消息.",
+ "Features:Channel.Email.SendLimit": "Email渠道推送量",
+ "Features:Channel.Email.SendLimitDesc": "设置以限制Email渠道推送量.",
+ "Features:Channel.Email.SendLimitInterval": "Email渠道限制周期",
+ "Features:Channel.Email.SendLimitIntervalDesc": "设置Email渠道限制周期(时间刻度: 天).",
+ "Features:Channel.Sms": "短信消息通道",
+ "Features:Channel.Sms.Enable": "启用短信消息通道",
+ "Features:Channel.Sms.EnableDesc": "启用以允许PushPlus通过短信推送消息.",
+ "Features:Channel.Sms.SendLimit": "短信渠道推送量",
+ "Features:Channel.Sms.SendLimitDesc": "设置以限制短信渠道推送量.",
+ "Features:Channel.Sms.SendLimitInterval": "短信渠道限制周期",
+ "Features:Channel.Sms.SendLimitIntervalDesc": "设置短信渠道限制周期(时间刻度: 天)."
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/IPushPlusMessageProvider.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/IPushPlusMessageProvider.cs
new file mode 100644
index 000000000..8705d0f6d
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/IPushPlusMessageProvider.cs
@@ -0,0 +1,28 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.PushPlus.Message;
+
+public interface IPushPlusMessageProvider
+{
+ ///
+ /// 查询消息发送结果
+ ///
+ /// 消息短链码
+ ///
+ ///
+ Task GetSendResultAsync(
+ string shortCode,
+ CancellationToken cancellationToken = default);
+ ///
+ /// 消息列表
+ ///
+ /// 当前所在分页数
+ /// 每页大小,最大值为50
+ ///
+ ///
+ Task> GetMessageListAsync(
+ int current,
+ int pageSize,
+ CancellationToken cancellationToken = default);
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/IPushPlusMessageSender.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/IPushPlusMessageSender.cs
new file mode 100644
index 000000000..0d7d11844
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/IPushPlusMessageSender.cs
@@ -0,0 +1,52 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.PushPlus.Message;
+
+public interface IPushPlusMessageSender
+{
+ Task SendWeChatAsync(
+ string title,
+ string content,
+ string topic = "",
+ PushPlusMessageTemplate template = PushPlusMessageTemplate.Html,
+ string webhook = "",
+ string callbackUrl = "",
+ CancellationToken cancellationToken = default);
+
+ Task SendEmailAsync(
+ string title,
+ string content,
+ string topic = "",
+ PushPlusMessageTemplate template = PushPlusMessageTemplate.Html,
+ string webhook = "",
+ string callbackUrl = "",
+ CancellationToken cancellationToken = default);
+
+ Task SendWeWorkAsync(
+ string title,
+ string content,
+ string topic = "",
+ PushPlusMessageTemplate template = PushPlusMessageTemplate.Html,
+ string webhook = "",
+ string callbackUrl = "",
+ CancellationToken cancellationToken = default);
+
+ Task SendWebhookAsync(
+ string title,
+ string content,
+ string topic = "",
+ PushPlusMessageTemplate template = PushPlusMessageTemplate.Html,
+ string webhook = "",
+ string callbackUrl = "",
+ CancellationToken cancellationToken = default);
+
+ Task SendSmsAsync(
+ string title,
+ string content,
+ string topic = "",
+ PushPlusMessageTemplate template = PushPlusMessageTemplate.Html,
+ string webhook = "",
+ string callbackUrl = "",
+ CancellationToken cancellationToken = default);
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/MessageHttpClientExtensions.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/MessageHttpClientExtensions.cs
new file mode 100644
index 000000000..0b4837165
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/MessageHttpClientExtensions.cs
@@ -0,0 +1,88 @@
+using System.Net.Http;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.PushPlus.Message;
+
+internal static class MessageHttpClientExtensions
+{
+ private const string _sendMessagTemplate = "{\"token\":\"$token\",\"title\":\"$title\",\"content\":\"$content\",\"topic\":\"$topic\",\"template\":\"$template\",\"channel\":\"$channel\",\"webhook\":\"$webhook\",\"callbackUrl\":\"$callbackUrl\",\"timestamp\":\"$timestamp\"}";
+
+ public async static Task GetSendMessageContentAsync(
+ this HttpClient httpClient,
+ string token,
+ string title,
+ string content,
+ string topic = "",
+ string template = "html",
+ string channel = "wechat",
+ string webhook = "",
+ string callbackUrl = "",
+ string timestamp = "",
+ CancellationToken cancellationToken = default)
+ {
+ var requestMessage = new HttpRequestMessage(
+ HttpMethod.Post,
+ "/send");
+
+ var requestBody = _sendMessagTemplate
+ .Replace("$token", token)
+ .Replace("$title", title)
+ .Replace("$content", content)
+ .Replace("$topic", topic)
+ .Replace("$template", template)
+ .Replace("$channel", channel)
+ .Replace("$webhook", webhook)
+ .Replace("$callbackUrl", callbackUrl)
+ .Replace("$timestamp", timestamp);
+
+ 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 GetSendResultContentAsync(
+ this HttpClient httpClient,
+ string accessKey,
+ string shortCode,
+ CancellationToken cancellationToken = default)
+ {
+ var requestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ $"/api/open/message/sendMessageResult?shortCode={shortCode}");
+
+ requestMessage.Headers.TryAddWithoutValidation("access-key", accessKey);
+
+ var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken);
+
+ return await httpResponse.Content.ReadAsStringAsync();
+ }
+
+ public async static Task GetMessageListContentAsync(
+ this HttpClient httpClient,
+ string accessKey,
+ int current,
+ int pageSize,
+ CancellationToken cancellationToken = default)
+ {
+ var requestMessage = new HttpRequestMessage(
+ HttpMethod.Post,
+ "/api/open/message/list");
+
+ var requestBodyTemplate = "{\"current\":\"$current\",\"pageSize\":\"$pageSize\"}";
+ var requestBody = requestBodyTemplate
+ .Replace("$current", current.ToString())
+ .Replace("$pageSize", pageSize.ToString());
+
+ requestMessage.Content = new StringContent(requestBody);
+ requestMessage.Headers.TryAddWithoutValidation("access-key", accessKey);
+
+ var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken);
+
+ return await httpResponse.Content.ReadAsStringAsync();
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessage.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessage.cs
new file mode 100644
index 000000000..d0ca1c805
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessage.cs
@@ -0,0 +1,44 @@
+using Newtonsoft.Json;
+using System;
+
+namespace LINGYUN.Abp.PushPlus.Message;
+
+public class PushPlusMessage
+{
+ ///
+ /// 消息发送渠道;
+ /// wechat-微信公众号,
+ /// mail-邮件,
+ /// cp-企业微信应用,
+ /// webhook-第三方webhook
+ ///
+ [JsonProperty("channel")]
+ public string Channel { get; set; }
+ ///
+ /// 消息类型;
+ /// 1-一对一消息,
+ /// 2-一对多消息
+ ///
+ [JsonProperty("messageType")]
+ public PushPlusMessageType MessageType { get; set; }
+ ///
+ /// 消息短链码;可用于查询消息发送结果
+ ///
+ [JsonProperty("shortCode")]
+ public string ShortCode { get; set; }
+ ///
+ /// 消息标题
+ ///
+ [JsonProperty("title")]
+ public string Title { get; set; }
+ ///
+ /// 群组名称,一对多消息才有值
+ ///
+ [JsonProperty("topicName")]
+ public string TopicName { get; set; }
+ ///
+ /// 更新日期
+ ///
+ [JsonProperty("updateTime")]
+ public DateTime UpdateTime { get; set; }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessageProvider.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessageProvider.cs
new file mode 100644
index 000000000..c5ae6735e
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessageProvider.cs
@@ -0,0 +1,67 @@
+using LINGYUN.Abp.PushPlus.Token;
+using Microsoft.Extensions.Logging;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Json;
+
+namespace LINGYUN.Abp.PushPlus.Message;
+
+public class PushPlusMessageProvider : IPushPlusMessageProvider, ITransientDependency
+{
+ protected ILogger Logger { get; }
+ protected IJsonSerializer JsonSerializer { get; }
+ protected IHttpClientFactory HttpClientFactory { get; }
+ protected IPushPlusTokenProvider PushPlusTokenProvider { get; }
+
+ public PushPlusMessageProvider(
+ ILogger logger,
+ IJsonSerializer jsonSerializer,
+ IHttpClientFactory httpClientFactory,
+ IPushPlusTokenProvider pushPlusTokenProvider)
+ {
+ Logger = logger;
+ JsonSerializer = jsonSerializer;
+ HttpClientFactory = httpClientFactory;
+ PushPlusTokenProvider = pushPlusTokenProvider;
+ }
+
+ public async virtual Task> GetMessageListAsync(
+ int current,
+ int pageSize,
+ CancellationToken cancellationToken = default)
+ {
+ var token = await PushPlusTokenProvider.GetTokenAsync();
+ var client = HttpClientFactory.GetPushPlusClient();
+
+ var content = await client.GetMessageListContentAsync(
+ token.AccessKey,
+ current,
+ pageSize,
+ cancellationToken);
+
+ var pushPlusResponse = JsonSerializer
+ .Deserialize>>(content);
+
+ return pushPlusResponse.GetData();
+ }
+
+ public async virtual Task GetSendResultAsync(
+ string shortCode,
+ CancellationToken cancellationToken = default)
+ {
+ var token = await PushPlusTokenProvider.GetTokenAsync();
+ var client = HttpClientFactory.GetPushPlusClient();
+
+ var content = await client.GetSendResultContentAsync(
+ token.AccessKey,
+ shortCode,
+ cancellationToken);
+
+ var pushPlusResponse = JsonSerializer
+ .Deserialize>(content);
+
+ return pushPlusResponse.GetData();
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessageSender.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessageSender.cs
new file mode 100644
index 000000000..81dadca10
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessageSender.cs
@@ -0,0 +1,214 @@
+using LINGYUN.Abp.Features.LimitValidation;
+using LINGYUN.Abp.PushPlus.Channel;
+using LINGYUN.Abp.PushPlus.Features;
+using LINGYUN.Abp.PushPlus.Settings;
+using Microsoft.Extensions.Logging;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Features;
+using Volo.Abp.Json;
+using Volo.Abp.Settings;
+using Volo.Abp.Timing;
+
+namespace LINGYUN.Abp.PushPlus.Message;
+
+[RequiresFeature(PushPlusFeatureNames.Message.Enable)]
+public class PushPlusMessageSender : IPushPlusMessageSender, ITransientDependency
+{
+ protected ILogger Logger { get; }
+ protected IClock Clock { get; }
+ protected IJsonSerializer JsonSerializer { get; }
+ protected ISettingProvider SettingProvider { get; }
+ protected IHttpClientFactory HttpClientFactory { get; }
+
+ public PushPlusMessageSender(
+ ILogger logger,
+ IClock clock,
+ IJsonSerializer jsonSerializer,
+ ISettingProvider settingProvider,
+ IHttpClientFactory httpClientFactory)
+ {
+ Logger = logger;
+ Clock = clock;
+ JsonSerializer = jsonSerializer;
+ SettingProvider = settingProvider;
+ HttpClientFactory = httpClientFactory;
+ }
+
+ [RequiresFeature(PushPlusFeatureNames.Channel.WeChat.Enable)]
+ [RequiresLimitFeature(
+ PushPlusFeatureNames.Channel.WeChat.SendLimit,
+ PushPlusFeatureNames.Channel.WeChat.SendLimitInterval,
+ LimitPolicy.Days)]
+ public async virtual Task SendWeChatAsync(
+ string title,
+ string content,
+ string topic = "",
+ PushPlusMessageTemplate template = PushPlusMessageTemplate.Html,
+ string webhook = "",
+ string callbackUrl = "",
+ CancellationToken cancellationToken = default)
+ {
+ return await SendAsync(
+ title,
+ content,
+ topic,
+ template,
+ PushPlusChannelType.WeChat,
+ webhook,
+ callbackUrl,
+ cancellationToken);
+ }
+
+ [RequiresFeature(PushPlusFeatureNames.Channel.WeWork.Enable)]
+ [RequiresLimitFeature(
+ PushPlusFeatureNames.Channel.WeWork.SendLimit,
+ PushPlusFeatureNames.Channel.WeWork.SendLimitInterval,
+ LimitPolicy.Days)]
+ public async virtual Task SendWeWorkAsync(
+ string title,
+ string content,
+ string topic = "",
+ PushPlusMessageTemplate template = PushPlusMessageTemplate.Html,
+ string webhook = "",
+ string callbackUrl = "",
+ CancellationToken cancellationToken = default)
+ {
+ return await SendAsync(
+ title,
+ content,
+ topic,
+ template,
+ PushPlusChannelType.WeWork,
+ webhook,
+ callbackUrl,
+ cancellationToken);
+ }
+
+ [RequiresFeature(PushPlusFeatureNames.Channel.Email.Enable)]
+ [RequiresLimitFeature(
+ PushPlusFeatureNames.Channel.Email.SendLimit,
+ PushPlusFeatureNames.Channel.Email.SendLimitInterval,
+ LimitPolicy.Days)]
+ public async virtual Task SendEmailAsync(
+ string title,
+ string content,
+ string topic = "",
+ PushPlusMessageTemplate template = PushPlusMessageTemplate.Html,
+ string webhook = "",
+ string callbackUrl = "",
+ CancellationToken cancellationToken = default)
+ {
+ return await SendAsync(
+ title,
+ content,
+ topic,
+ template,
+ PushPlusChannelType.Email,
+ webhook,
+ callbackUrl,
+ cancellationToken);
+ }
+
+ [RequiresFeature(PushPlusFeatureNames.Channel.Webhook.Enable)]
+ [RequiresLimitFeature(
+ PushPlusFeatureNames.Channel.Webhook.SendLimit,
+ PushPlusFeatureNames.Channel.Webhook.SendLimitInterval,
+ LimitPolicy.Days)]
+ public async virtual Task SendWebhookAsync(
+ string title,
+ string content,
+ string topic = "",
+ PushPlusMessageTemplate template = PushPlusMessageTemplate.Html,
+ string webhook = "",
+ string callbackUrl = "",
+ CancellationToken cancellationToken = default)
+ {
+ return await SendAsync(
+ title,
+ content,
+ topic,
+ template,
+ PushPlusChannelType.Webhook,
+ webhook,
+ callbackUrl,
+ cancellationToken);
+ }
+
+ [RequiresFeature(PushPlusFeatureNames.Channel.Sms.Enable)]
+ [RequiresLimitFeature(
+ PushPlusFeatureNames.Channel.Sms.SendLimit,
+ PushPlusFeatureNames.Channel.Sms.SendLimitInterval,
+ LimitPolicy.Days)]
+ public async virtual Task SendSmsAsync(
+ string title,
+ string content,
+ string topic = "",
+ PushPlusMessageTemplate template = PushPlusMessageTemplate.Html,
+ string webhook = "",
+ string callbackUrl = "",
+ CancellationToken cancellationToken = default)
+ {
+ return await SendAsync(
+ title,
+ content,
+ topic,
+ template,
+ PushPlusChannelType.Sms,
+ webhook,
+ callbackUrl,
+ cancellationToken);
+ }
+
+ protected async virtual Task SendAsync(
+ string title,
+ string content,
+ string topic = "",
+ PushPlusMessageTemplate template = PushPlusMessageTemplate.Html,
+ PushPlusChannelType channel = PushPlusChannelType.WeChat,
+ string webhook = "",
+ string callbackUrl = "",
+ CancellationToken cancellationToken = default)
+ {
+ Check.NotNullOrEmpty(title, nameof(title));
+ Check.NotNullOrEmpty(content, nameof(content));
+
+ var token = await SettingProvider.GetOrNullAsync(PushPlusSettingNames.Security.Token);
+ Check.NotNullOrEmpty(token, PushPlusSettingNames.Security.Token);
+
+ var client = HttpClientFactory.GetPushPlusClient();
+
+ var sendContent = await client.GetSendMessageContentAsync(
+ token,
+ title,
+ content,
+ topic,
+ GetTemplate(template),
+ channel.GetChannelName(),
+ webhook,
+ callbackUrl,
+ cancellationToken: cancellationToken);
+
+ var pushPlusResponse = JsonSerializer.Deserialize>(sendContent);
+
+ return pushPlusResponse.GetData();
+ }
+
+ private static string GetTemplate(PushPlusMessageTemplate template)
+ {
+ return template switch
+ {
+ PushPlusMessageTemplate.Html => "html",
+ PushPlusMessageTemplate.Text => "txt",
+ PushPlusMessageTemplate.Json => "json",
+ PushPlusMessageTemplate.Markdown => "markdown",
+ PushPlusMessageTemplate.CloudMonitor => "cloudMonitor",
+ PushPlusMessageTemplate.Jenkins => "jenkins",
+ PushPlusMessageTemplate.Route => "route",
+ _ => "html",
+ };
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessageStatus.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessageStatus.cs
new file mode 100644
index 000000000..953fb9077
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessageStatus.cs
@@ -0,0 +1,21 @@
+namespace LINGYUN.Abp.PushPlus.Message;
+
+public enum PushPlusMessageStatus
+{
+ ///
+ /// 未发送
+ ///
+ NotSend = 0,
+ ///
+ /// 发送中
+ ///
+ Sending = 1,
+ ///
+ /// 已发送
+ ///
+ Successed = 2,
+ ///
+ /// 发送失败
+ ///
+ Failed = 3,
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessageTemplate.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessageTemplate.cs
new file mode 100644
index 000000000..d9936694a
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessageTemplate.cs
@@ -0,0 +1,35 @@
+namespace LINGYUN.Abp.PushPlus.Message;
+///
+/// 模板(template)枚举。默认使用html模板
+///
+public enum PushPlusMessageTemplate
+{
+ ///
+ /// 默认模板,支持html文本
+ ///
+ Html = 0,
+ ///
+ /// 纯文本展示,不转义html
+ ///
+ Text = 1,
+ ///
+ /// 内容基于json格式展示
+ ///
+ Json = 2,
+ ///
+ /// 内容基于markdown格式展示
+ ///
+ Markdown = 3,
+ ///
+ /// 阿里云监控报警定制模板
+ ///
+ CloudMonitor = 4,
+ ///
+ /// jenkins插件定制模板
+ ///
+ Jenkins = 5,
+ ///
+ /// 路由器插件定制模板
+ ///
+ Route = 6
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessageType.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessageType.cs
new file mode 100644
index 000000000..fd4143c4b
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/PushPlusMessageType.cs
@@ -0,0 +1,13 @@
+namespace LINGYUN.Abp.PushPlus.Message;
+
+public enum PushPlusMessageType
+{
+ ///
+ /// 一对一消息
+ ///
+ Once,
+ ///
+ /// 一对多消息
+ ///
+ Multiple
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/SendPushPlusMessageResult.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/SendPushPlusMessageResult.cs
new file mode 100644
index 000000000..702f9af07
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Message/SendPushPlusMessageResult.cs
@@ -0,0 +1,27 @@
+using Newtonsoft.Json;
+using System;
+
+namespace LINGYUN.Abp.PushPlus.Message;
+
+public class SendPushPlusMessageResult
+{
+ ///
+ /// 消息投递状态
+ /// 0-未投递,
+ /// 1-发送中,
+ /// 2-已发送,
+ /// 3-发送失败
+ ///
+ [JsonProperty("status")]
+ public PushPlusMessageStatus Status { get; set; }
+ ///
+ /// 发送失败原因
+ ///
+ [JsonProperty("errorMessage")]
+ public string ErrorMessage { get; set; }
+ ///
+ /// 更新时间
+ ///
+ [JsonProperty("updateTime")]
+ public DateTime UpdateTime { get; set; }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/PushPlusPagedResponse.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/PushPlusPagedResponse.cs
new file mode 100644
index 000000000..23828db56
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/PushPlusPagedResponse.cs
@@ -0,0 +1,38 @@
+using Newtonsoft.Json;
+using System.Collections.Generic;
+
+namespace LINGYUN.Abp.PushPlus;
+
+public class PushPlusPagedResponse
+{
+ ///
+ /// 当前页码
+ ///
+ [JsonProperty("pageNum")]
+ public int PageNum { get; set; }
+ ///
+ /// 分页大小
+ ///
+ [JsonProperty("pageSize")]
+ public int PageSize { get; set; }
+ ///
+ /// 总行数
+ ///
+ [JsonProperty("total")]
+ public int Total { get; set; }
+ ///
+ /// 总页数
+ ///
+ [JsonProperty("pages")]
+ public int Pages { get; set; }
+ ///
+ /// 消息列表
+ ///
+ [JsonProperty("list")]
+ public List Items { get; set; }
+
+ public PushPlusPagedResponse()
+ {
+ Items = new List();
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/PushPlusRequestException.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/PushPlusRequestException.cs
new file mode 100644
index 000000000..2696758c9
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/PushPlusRequestException.cs
@@ -0,0 +1,14 @@
+using Volo.Abp;
+using Volo.Abp.ExceptionHandling;
+
+namespace LINGYUN.Abp.PushPlus;
+public class PushPlusRequestException : AbpException, IHasErrorCode
+{
+ public string Code { get; }
+
+ public PushPlusRequestException(string code, string message)
+ : base($"The PushPlush API returns an error: {code} - {message}")
+ {
+ Code = code;
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/PushPlusResponse.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/PushPlusResponse.cs
new file mode 100644
index 000000000..c1057e46e
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/PushPlusResponse.cs
@@ -0,0 +1,58 @@
+using Newtonsoft.Json;
+
+namespace LINGYUN.Abp.PushPlus;
+
+public class PushPlusResponse
+{
+ ///
+ /// 状态码
+ ///
+ [JsonProperty("code")]
+ public string Code { get; set; }
+ ///
+ /// 错误消息
+ ///
+ [JsonProperty("msg")]
+ public string Message { get; set; }
+ ///
+ /// 返回数据
+ ///
+ [JsonProperty("data")]
+ public T Data { get; set; }
+ ///
+ /// 是否调用成功
+ ///
+ public bool IsSuccessed => string.Equals("200", Code);
+
+ public PushPlusResponse()
+ {
+ }
+
+ public PushPlusResponse(string code, string message)
+ {
+ Code = code;
+ Message = message;
+ }
+
+ public PushPlusResponse(string code, string message, T data)
+ {
+ Code = code;
+ Message = message;
+ Data = data;
+ }
+
+ public T GetData()
+ {
+ ThrowOfFailed();
+
+ return Data;
+ }
+
+ public void ThrowOfFailed()
+ {
+ if (!IsSuccessed)
+ {
+ throw new PushPlusRequestException(Code, Message);
+ }
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/IPushPlusChannelProvider.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/IPushPlusChannelProvider.cs
new file mode 100644
index 000000000..c2404cc9d
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/IPushPlusChannelProvider.cs
@@ -0,0 +1,38 @@
+using LINGYUN.Abp.PushPlus.Channel;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.PushPlus.Setting;
+///
+/// 发送渠道接口
+///
+public interface IPushPlusChannelProvider
+{
+ ///
+ /// 获取默认发送渠道
+ ///
+ ///
+ ///
+ Task GetDefaultChannelAsync(
+ CancellationToken cancellationToken = default);
+ ///
+ /// 修改默认发送渠道
+ ///
+ /// 默认渠道;wechat-微信公众号,mail-邮件,cp-企业微信应用,webhook-第三方webhook
+ /// 渠道参数;webhook和cp渠道需要填写具体的webhook编号或自定义编码
+ ///
+ ///
+ Task UpdateDefaultChannelAsync(
+ PushPlusChannelType defaultChannel,
+ string defaultWebhook = "",
+ CancellationToken cancellationToken = default);
+ ///
+ /// 修改接收消息限制
+ ///
+ /// 接收消息限制;0-接收全部,1-不接收消息
+ ///
+ ///
+ Task UpdateRecevieLimitAsync(
+ PushPlusChannelRecevieLimit recevieLimit,
+ CancellationToken cancellationToken = default);
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/PushPlusChannel.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/PushPlusChannel.cs
new file mode 100644
index 000000000..c3e9b64c2
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/PushPlusChannel.cs
@@ -0,0 +1,35 @@
+using Newtonsoft.Json;
+
+namespace LINGYUN.Abp.PushPlus.Setting;
+///
+/// 发送渠道
+///
+public class PushPlusChannel
+{
+ ///
+ /// 默认渠道编码
+ ///
+ [JsonProperty("defaultChannel")]
+ public string DefaultChannel { get; set; }
+ ///
+ /// 默认渠道名称
+ ///
+ [JsonProperty("defaultChannelTxt")]
+ public string DefaultChannelName { get; set; }
+ ///
+ /// 渠道参数
+ ///
+ [JsonProperty("defaultWebhook")]
+ public string DefaultWebhook { get; set; }
+ ///
+ /// 发送限制;0-无限制,1-禁止所有渠道发送,2-限制微信渠道,3-限制邮件渠道
+ ///
+ [JsonProperty("sendLimit")]
+ public PushPlusChannelRecevieLimit SendLimit { get; set; }
+ ///
+ /// 接收限制;0-接收全部,1-不接收消息
+ ///
+ [JsonProperty("recevieLimit")]
+ public PushPlusChannelRecevieLimit RecevieLimit { get; set; }
+
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/PushPlusChannelProvider.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/PushPlusChannelProvider.cs
new file mode 100644
index 000000000..0f92d9d5d
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/PushPlusChannelProvider.cs
@@ -0,0 +1,83 @@
+using LINGYUN.Abp.PushPlus.Channel;
+using LINGYUN.Abp.PushPlus.Token;
+using Microsoft.Extensions.Logging;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Json;
+
+namespace LINGYUN.Abp.PushPlus.Setting;
+
+public class PushPlusChannelProvider : IPushPlusChannelProvider, ITransientDependency
+{
+ protected ILogger Logger { get; }
+ protected IJsonSerializer JsonSerializer { get; }
+ protected IHttpClientFactory HttpClientFactory { get; }
+ protected IPushPlusTokenProvider PushPlusTokenProvider { get; }
+
+ public PushPlusChannelProvider(
+ ILogger logger,
+ IJsonSerializer jsonSerializer,
+ IHttpClientFactory httpClientFactory,
+ IPushPlusTokenProvider pushPlusTokenProvider)
+ {
+ Logger = logger;
+ JsonSerializer = jsonSerializer;
+ HttpClientFactory = httpClientFactory;
+ PushPlusTokenProvider = pushPlusTokenProvider;
+ }
+
+ public async virtual Task GetDefaultChannelAsync(CancellationToken cancellationToken = default)
+ {
+ var token = await PushPlusTokenProvider.GetTokenAsync();
+ var client = HttpClientFactory.GetPushPlusClient();
+
+ var content = await client.GetDefaultChannelContentAsync(
+ token.AccessKey,
+ cancellationToken);
+
+ var pushPlusResponse = JsonSerializer
+ .Deserialize>(content);
+
+ return pushPlusResponse.GetData();
+ }
+
+ public async virtual Task UpdateDefaultChannelAsync(
+ PushPlusChannelType defaultChannel,
+ string defaultWebhook = "",
+ CancellationToken cancellationToken = default)
+ {
+ var token = await PushPlusTokenProvider.GetTokenAsync();
+ var client = HttpClientFactory.GetPushPlusClient();
+
+ var content = await client.GetUpdateDefaultChannelContentAsync(
+ token.AccessKey,
+ defaultChannel.GetChannelName(),
+ defaultWebhook,
+ cancellationToken);
+
+ var pushPlusResponse = JsonSerializer
+ .Deserialize>(content);
+
+ pushPlusResponse.ThrowOfFailed();
+ }
+
+ public async virtual Task UpdateRecevieLimitAsync(
+ PushPlusChannelRecevieLimit recevieLimit,
+ CancellationToken cancellationToken = default)
+ {
+ var token = await PushPlusTokenProvider.GetTokenAsync();
+ var client = HttpClientFactory.GetPushPlusClient();
+
+ var content = await client.GetUpdateRecevieLimitContentAsync(
+ token.AccessKey,
+ recevieLimit,
+ cancellationToken);
+
+ var pushPlusResponse = JsonSerializer
+ .Deserialize>(content);
+
+ pushPlusResponse.ThrowOfFailed();
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/PushPlusChannelRecevieLimit.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/PushPlusChannelRecevieLimit.cs
new file mode 100644
index 000000000..4c888e401
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/PushPlusChannelRecevieLimit.cs
@@ -0,0 +1,15 @@
+namespace LINGYUN.Abp.PushPlus.Setting;
+///
+/// 接收限制;0-接收全部,1-不接收消息
+///
+public enum PushPlusChannelRecevieLimit
+{
+ ///
+ /// 接收全部
+ ///
+ RecevieAll = 0,
+ ///
+ /// 不接收消息
+ ///
+ DontRecevied = 1
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/PushPlusChannelSendLimit.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/PushPlusChannelSendLimit.cs
new file mode 100644
index 000000000..4e50c1073
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/PushPlusChannelSendLimit.cs
@@ -0,0 +1,27 @@
+namespace LINGYUN.Abp.PushPlus.Setting;
+///
+/// 发送限制;
+/// 0-无限制,
+/// 1-禁止所有渠道发送,
+/// 2-限制微信渠道,
+/// 3-限制邮件渠道
+///
+public enum PushPlusChannelSendLimit
+{
+ ///
+ /// 无限制
+ ///
+ None = 0,
+ ///
+ /// 禁止所有渠道发送
+ ///
+ ForbidAllChannel = 1,
+ ///
+ /// 限制微信渠道
+ ///
+ ForbidWeChatChannel = 2,
+ ///
+ /// 限制邮件渠道
+ ///
+ ForbidEmailChannel = 3,
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/SettingHttpClientExtensions.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/SettingHttpClientExtensions.cs
new file mode 100644
index 000000000..b6d218ccb
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Setting/SettingHttpClientExtensions.cs
@@ -0,0 +1,65 @@
+using System.Net.Http;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.PushPlus.Setting;
+
+internal static class SettingHttpClientExtensions
+{
+ public async static Task GetDefaultChannelContentAsync(
+ this HttpClient httpClient,
+ string accessKey,
+ CancellationToken cancellationToken = default)
+ {
+ var requestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ "/api/open/setting/getUserSettings");
+ requestMessage.Headers.TryAddWithoutValidation("access-key", accessKey);
+
+ var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken);
+
+ return await httpResponse.Content.ReadAsStringAsync();
+ }
+
+ private const string _updateDefaultChannelTemplate = "{\"defaultChannel\":\"$defaultChannel\",\"defaultWebhook\":\"$defaultWebhook\"}";
+ public async static Task GetUpdateDefaultChannelContentAsync(
+ this HttpClient httpClient,
+ string accessKey,
+ string defaultChannel,
+ string defaultWebhook = "",
+ CancellationToken cancellationToken = default)
+ {
+ var requestMessage = new HttpRequestMessage(
+ HttpMethod.Post,
+ "/api/open/setting/changeDefaultChannel");
+
+ var requestBody = _updateDefaultChannelTemplate
+ .Replace("$defaultChannel", defaultChannel)
+ .Replace("$defaultWebhook", defaultWebhook);
+ requestMessage.Content = new StringContent(requestBody, Encoding.UTF8, "application/json");
+
+ requestMessage.Headers.TryAddWithoutValidation("access-key", accessKey);
+
+ var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken);
+
+ return await httpResponse.Content.ReadAsStringAsync();
+ }
+
+ public async static Task GetUpdateRecevieLimitContentAsync(
+ this HttpClient httpClient,
+ string accessKey,
+ PushPlusChannelRecevieLimit recevieLimit,
+ CancellationToken cancellationToken = default)
+ {
+ var requestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ $"/api/open/setting/changeRecevieLimit?recevieLimit=${(int)recevieLimit}");
+
+ requestMessage.Headers.TryAddWithoutValidation("access-key", accessKey);
+
+ var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken);
+
+ return await httpResponse.Content.ReadAsStringAsync();
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Settings/PushPlusSettingDefinitionProvider.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Settings/PushPlusSettingDefinitionProvider.cs
new file mode 100644
index 000000000..c1fc6dc58
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Settings/PushPlusSettingDefinitionProvider.cs
@@ -0,0 +1,42 @@
+using LINGYUN.Abp.PushPlus.Localization;
+using Volo.Abp.Localization;
+using Volo.Abp.Settings;
+
+namespace LINGYUN.Abp.PushPlus.Settings;
+
+public class PushPlusSettingDefinitionProvider : SettingDefinitionProvider
+{
+ public override void Define(ISettingDefinitionContext context)
+ {
+ context.Add(new[]
+ {
+ new SettingDefinition(
+ name: PushPlusSettingNames.Security.Token,
+ defaultValue: "qGE8NZ8rrQYj207kSv9vb5XzG1a+iK6z8yGPICjx3cY5p8bGcmMav5t0DRLjCprA",
+ displayName: L("Settings:Security.Token"),
+ description: L("Settings:Security.TokenDesc"),
+ isEncrypted: true)
+ .WithProviders(
+ DefaultValueSettingValueProvider.ProviderName,
+ ConfigurationSettingValueProvider.ProviderName,
+ GlobalSettingValueProvider.ProviderName,
+ TenantSettingValueProvider.ProviderName),
+ new SettingDefinition(
+ name: PushPlusSettingNames.Security.SecretKey,
+ defaultValue: "HXGIfCpkUNonrOB8znJzNcDoKvZBKpNZ0tN38tktgrg=",
+ displayName: L("Settings:Security.SecretKey"),
+ description: L("Settings:Security.SecretKeyDesc"),
+ 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/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Settings/PushPlusSettingNames.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Settings/PushPlusSettingNames.cs
new file mode 100644
index 000000000..99ea91ea7
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Settings/PushPlusSettingNames.cs
@@ -0,0 +1,15 @@
+namespace LINGYUN.Abp.PushPlus.Settings;
+
+public static class PushPlusSettingNames
+{
+ public const string Prefix = "PushPlus";
+
+ public static class Security
+ {
+ public const string Prefix = PushPlusSettingNames.Prefix + ".Security";
+
+ public const string Token = Prefix + ".Token";
+
+ public const string SecretKey = Prefix + ".SecretKey";
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Token/IPushPlusTokenProvider.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Token/IPushPlusTokenProvider.cs
new file mode 100644
index 000000000..8976e4bff
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Token/IPushPlusTokenProvider.cs
@@ -0,0 +1,16 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.PushPlus.Token;
+///
+/// 获取AccessKey
+///
+public interface IPushPlusTokenProvider
+{
+ ///
+ /// 获取全局可用Token
+ ///
+ ///
+ ///
+ Task GetTokenAsync(CancellationToken cancellationToken = default);
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Token/PushPlusToken.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Token/PushPlusToken.cs
new file mode 100644
index 000000000..5a27a05bb
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Token/PushPlusToken.cs
@@ -0,0 +1,28 @@
+using Newtonsoft.Json;
+
+namespace LINGYUN.Abp.PushPlus.Token;
+
+public class PushPlusToken
+{
+ ///
+ /// 访问令牌,后续请求需加到header中
+ ///
+ [JsonProperty("accessKey")]
+ public string AccessKey { get; set; }
+ ///
+ /// 过期时间,过期后需要重新获取
+ ///
+ [JsonProperty("expiresIn")]
+ public int ExpiresIn { get; set; }
+
+ public PushPlusToken()
+ {
+
+ }
+
+ public PushPlusToken(string accessKey, int expiresIn)
+ {
+ AccessKey = accessKey;
+ ExpiresIn = expiresIn;
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Token/PushPlusTokenCacheItem.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Token/PushPlusTokenCacheItem.cs
new file mode 100644
index 000000000..5f6a460f1
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Token/PushPlusTokenCacheItem.cs
@@ -0,0 +1,26 @@
+namespace LINGYUN.Abp.PushPlus.Token;
+
+public class PushPlusTokenCacheItem
+{
+ public const string KeyFormat = "t:{0};s:{1}";
+
+ public string AccessKey { get; set; }
+
+ public int ExpiresIn { get; set; }
+
+ public PushPlusTokenCacheItem()
+ {
+
+ }
+
+ public PushPlusTokenCacheItem(string accessKey, int expiresIn)
+ {
+ AccessKey = accessKey;
+ ExpiresIn = expiresIn;
+ }
+
+ public static string CalculateCacheKey(string token, string secretKey)
+ {
+ return string.Format(KeyFormat, token, secretKey);
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Token/PushPlusTokenProvider.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Token/PushPlusTokenProvider.cs
new file mode 100644
index 000000000..3a5937e08
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Token/PushPlusTokenProvider.cs
@@ -0,0 +1,104 @@
+using LINGYUN.Abp.PushPlus.Settings;
+using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using System;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp;
+using Volo.Abp.Caching;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Json;
+using Volo.Abp.Settings;
+
+namespace LINGYUN.Abp.PushPlus.Token;
+
+public class PushPlusTokenProvider : IPushPlusTokenProvider, ITransientDependency
+{
+ public ILogger Logger { protected get; set; }
+
+ protected IDistributedCache Cache { get; }
+ protected IJsonSerializer JsonSerializer { get; }
+ protected ISettingProvider SettingProvider { get; }
+ protected IHttpClientFactory HttpClientFactory { get; }
+
+ public PushPlusTokenProvider(
+ IDistributedCache cache,
+ IJsonSerializer jsonSerializer,
+ ISettingProvider settingProvider,
+ IHttpClientFactory httpClientFactory)
+ {
+ Cache = cache;
+ JsonSerializer = jsonSerializer;
+ SettingProvider = settingProvider;
+ HttpClientFactory = httpClientFactory;
+
+ Logger = NullLogger.Instance;
+ }
+
+ public async virtual Task GetTokenAsync(CancellationToken cancellationToken = default)
+ {
+ var token = await SettingProvider.GetOrNullAsync(PushPlusSettingNames.Security.Token);
+ var secretKey = await SettingProvider.GetOrNullAsync(PushPlusSettingNames.Security.SecretKey);
+
+ Check.NotNullOrEmpty(token, PushPlusSettingNames.Security.Token);
+ Check.NotNullOrEmpty(secretKey, PushPlusSettingNames.Security.SecretKey);
+
+ return await GetTokenAsync(token, secretKey, cancellationToken);
+ }
+
+ public async virtual Task GetTokenAsync(
+ string token,
+ string secretKey,
+ CancellationToken cancellationToken = default)
+ {
+ var cacheItem = await GetCacheItemAsync(token, secretKey, cancellationToken);
+
+ return new PushPlusToken(cacheItem.AccessKey, cacheItem.ExpiresIn);
+ }
+
+ protected async virtual Task GetCacheItemAsync(
+ string token,
+ string secretKey,
+ CancellationToken cancellationToken = default)
+ {
+ var cacheKey = PushPlusTokenCacheItem.CalculateCacheKey(token, secretKey);
+
+ Logger.LogDebug($"PushPlusTokenProvider.GetCacheItemAsync: {cacheKey}");
+
+ var cacheItem = await Cache.GetAsync(cacheKey, token: cancellationToken);
+
+ if (cacheItem != null)
+ {
+ Logger.LogDebug($"Found in the cache: {cacheKey}");
+ return cacheItem;
+ }
+
+ Logger.LogDebug($"Not found in the cache, getting from the httpClient: {cacheKey}");
+
+ var client = HttpClientFactory.GetPushPlusClient();
+
+ var content = await client.GetTokenContentAsync(token, secretKey, cancellationToken);
+
+ var pushPlusResponse = JsonSerializer.Deserialize>(content);
+ var pushPlusToken = pushPlusResponse.GetData();
+
+ cacheItem = new PushPlusTokenCacheItem(
+ pushPlusToken.AccessKey,
+ pushPlusToken.ExpiresIn);
+
+ await Cache.SetAsync(
+ cacheKey,
+ cacheItem,
+ new DistributedCacheEntryOptions
+ {
+ AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(pushPlusToken.ExpiresIn - 120)
+ },
+ token: cancellationToken);
+
+ Logger.LogDebug($"Finished setting the cache item: {cacheKey}");
+
+ return cacheItem;
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Token/TokenHttpClientExtensions.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Token/TokenHttpClientExtensions.cs
new file mode 100644
index 000000000..46a4bb0fc
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Token/TokenHttpClientExtensions.cs
@@ -0,0 +1,29 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.PushPlus.Token;
+
+internal static class TokenHttpClientExtensions
+{
+ public async static Task GetTokenContentAsync(
+ this HttpClient httpClient,
+ string token,
+ string secretKey,
+ CancellationToken cancellationToken = default)
+ {
+ var requestMessage = new HttpRequestMessage(
+ HttpMethod.Post,
+ "/api/common/openApi/getAccessKey");
+ var requestBodyTemplate = "{\"token\":\"$token\",\"secretKey\":\"$secretKey\"}";
+ var requestBody = requestBodyTemplate
+ .Replace("$token", token)
+ .Replace("$secretKey", secretKey);
+
+ requestMessage.Content = new StringContent(requestBody);
+
+ var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken);
+
+ return await httpResponse.Content.ReadAsStringAsync();
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/IPushPlusTopicProvider.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/IPushPlusTopicProvider.cs
new file mode 100644
index 000000000..5597125ee
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/IPushPlusTopicProvider.cs
@@ -0,0 +1,100 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.PushPlus.Topic;
+///
+/// 群组接口
+///
+public interface IPushPlusTopicProvider
+{
+ ///
+ /// 获取群组列表
+ ///
+ /// 当前所在分页数
+ /// 每页大小,最大值为50
+ /// 群组类型;0-我创建的,1-我加入的
+ ///
+ ///
+ Task> GetTopicListAsync(
+ int current,
+ int pageSize,
+ PushPlusTopicType topicType = PushPlusTopicType.Create,
+ CancellationToken cancellationToken = default);
+ ///
+ /// 获取我创建的群组详情
+ ///
+ /// 群组编号
+ ///
+ ///
+ Task GetTopicProfileAsync(
+ int topicId,
+ CancellationToken cancellationToken = default);
+ ///
+ /// 获取我加入的群详情
+ ///
+ /// 群组编号
+ ///
+ ///
+ Task GetTopicForMeProfileAsync(
+ int topicId,
+ CancellationToken cancellationToken = default);
+ ///
+ /// 新增群组
+ ///
+ /// 群组编码
+ /// 群组名称
+ /// 联系方式
+ /// 群组简介
+ /// 加入后回复内容
+ ///
+ ///
+ Task CreateTopicAsync(
+ string topicCode,
+ string topicName,
+ string contact,
+ string introduction,
+ string receiptMessage = "",
+ CancellationToken cancellationToken = default);
+ ///
+ /// 获取群组二维码
+ ///
+ /// 群组编号
+ /// 二维码类型;0-临时二维码,1-永久二维码
+ ///
+ ///
+ Task GetTopicQrCodeAsync(
+ int topicId,
+ PushPlusTopicQrCodeType forever = PushPlusTopicQrCodeType.Temporary,
+ CancellationToken cancellationToken = default);
+ ///
+ /// 退出群组
+ ///
+ /// 群组编号
+ ///
+ ///
+ Task QuitTopicAsync(
+ int topicId,
+ CancellationToken cancellationToken = default);
+ ///
+ /// 获取群组内用户
+ ///
+ /// 当前所在分页数
+ /// 每页大小,最大值为50
+ /// 群组编号
+ ///
+ ///
+ Task> GetSubscriberListAsync(
+ int current,
+ int pageSize,
+ int topicId,
+ CancellationToken cancellationToken = default);
+ ///
+ /// 删除群组内用户
+ ///
+ /// 用户编号
+ ///
+ ///
+ Task UnSubscriberAsync(
+ int topicRelationId,
+ CancellationToken cancellationToken = default);
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopic.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopic.cs
new file mode 100644
index 000000000..8cdbd2787
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopic.cs
@@ -0,0 +1,30 @@
+using Newtonsoft.Json;
+using System;
+
+namespace LINGYUN.Abp.PushPlus.Topic;
+///
+/// 群组
+///
+public class PushPlusTopic
+{
+ ///
+ /// 群组编号
+ ///
+ [JsonProperty("topicId")]
+ public int TopicId { get; set; }
+ ///
+ /// 群组编码
+ ///
+ [JsonProperty("topicCode")]
+ public string TopicCode { get; set; }
+ ///
+ /// 群组名称
+ ///
+ [JsonProperty("topicName")]
+ public string TopicName { get; set; }
+ ///
+ /// 创建时间
+ ///
+ [JsonProperty("createTime")]
+ public DateTime CreateTime { get; set; }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicForMe.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicForMe.cs
new file mode 100644
index 000000000..f7dc882c2
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicForMe.cs
@@ -0,0 +1,19 @@
+using Newtonsoft.Json;
+
+namespace LINGYUN.Abp.PushPlus.Topic;
+///
+/// 我加入的群详情
+///
+public class PushPlusTopicForMe : PushPlusTopic
+{
+ ///
+ /// 联系方式
+ ///
+ [JsonProperty("contact")]
+ public string Contact { get; set; }
+ ///
+ /// 群组简介
+ ///
+ [JsonProperty("introduction")]
+ public string Introduction { get; set; }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicProfile.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicProfile.cs
new file mode 100644
index 000000000..a9f04697c
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicProfile.cs
@@ -0,0 +1,34 @@
+using Newtonsoft.Json;
+
+namespace LINGYUN.Abp.PushPlus.Topic;
+///
+/// 群组详情
+///
+public class PushPlusTopicProfile : PushPlusTopic
+{
+ ///
+ /// 永久二维码图片地址
+ ///
+ [JsonProperty("qrCodeImgUrl")]
+ public string QrCodeImgUrl { get; set; }
+ ///
+ /// 联系方式
+ ///
+ [JsonProperty("contact")]
+ public string Contact { get; set; }
+ ///
+ /// 群组简介
+ ///
+ [JsonProperty("introduction")]
+ public string Introduction { get; set; }
+ ///
+ /// 加入后回复内容
+ ///
+ [JsonProperty("receiptMessage")]
+ public string ReceiptMessage { get; set; }
+ ///
+ /// 群组订阅人总数
+ ///
+ [JsonProperty("topicUserCount")]
+ public string TopicUserCount { get; set; }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicProvider.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicProvider.cs
new file mode 100644
index 000000000..693338854
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicProvider.cs
@@ -0,0 +1,196 @@
+using LINGYUN.Abp.PushPlus.Token;
+using Microsoft.Extensions.Logging;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Json;
+
+namespace LINGYUN.Abp.PushPlus.Topic;
+
+public class PushPlusTopicProvider : IPushPlusTopicProvider, ITransientDependency
+{
+ protected ILogger Logger { get; }
+ protected IJsonSerializer JsonSerializer { get; }
+ protected IHttpClientFactory HttpClientFactory { get; }
+ protected IPushPlusTokenProvider PushPlusTokenProvider { get; }
+
+ public PushPlusTopicProvider(
+ ILogger logger,
+ IJsonSerializer jsonSerializer,
+ IHttpClientFactory httpClientFactory,
+ IPushPlusTokenProvider pushPlusTokenProvider)
+ {
+ Logger = logger;
+ JsonSerializer = jsonSerializer;
+ HttpClientFactory = httpClientFactory;
+ PushPlusTokenProvider = pushPlusTokenProvider;
+ }
+
+ public async virtual Task CreateTopicAsync(
+ string topicCode,
+ string topicName,
+ string contact,
+ string introduction,
+ string receiptMessage = "",
+ CancellationToken cancellationToken = default)
+ {
+ Check.NotNullOrWhiteSpace(topicCode, nameof(topicCode));
+ Check.NotNullOrWhiteSpace(topicName, nameof(topicName));
+ Check.NotNullOrWhiteSpace(contact, nameof(contact));
+ Check.NotNullOrWhiteSpace(introduction, nameof(introduction));
+
+ var token = await PushPlusTokenProvider.GetTokenAsync();
+ var client = HttpClientFactory.GetPushPlusClient();
+
+ var content = await client.GetCreateTopicContentAsync(
+ token.AccessKey,
+ topicCode,
+ topicName,
+ contact,
+ introduction,
+ receiptMessage,
+ cancellationToken);
+
+ var pushPlusResponse = JsonSerializer.Deserialize>(content);
+
+ return pushPlusResponse.GetData();
+ }
+
+ public async virtual Task GetTopicForMeProfileAsync(
+ int topicId,
+ CancellationToken cancellationToken = default)
+ {
+ var token = await PushPlusTokenProvider.GetTokenAsync();
+ var client = HttpClientFactory.GetPushPlusClient();
+
+ var content = await client.GetTopicForMeProfileContentAsync(
+ token.AccessKey,
+ topicId,
+ cancellationToken);
+
+ var pushPlusResponse = JsonSerializer
+ .Deserialize>(content);
+
+ return pushPlusResponse.GetData();
+ }
+
+ public async virtual Task> GetTopicListAsync(
+ int current,
+ int pageSize,
+ PushPlusTopicType topicType = PushPlusTopicType.Create,
+ CancellationToken cancellationToken = default)
+ {
+ var token = await PushPlusTokenProvider.GetTokenAsync();
+ var client = HttpClientFactory.GetPushPlusClient();
+
+ var content = await client.GetTopicListContentAsync(
+ token.AccessKey,
+ current,
+ pageSize,
+ topicType,
+ cancellationToken);
+
+ var pushPlusResponse = JsonSerializer
+ .Deserialize>>(content);
+
+ return pushPlusResponse.GetData();
+ }
+
+ public async virtual Task GetTopicProfileAsync(
+ int topicId,
+ CancellationToken cancellationToken = default)
+ {
+ var token = await PushPlusTokenProvider.GetTokenAsync();
+ var client = HttpClientFactory.GetPushPlusClient();
+
+ var content = await client.GetTopicProfileContentAsync(
+ token.AccessKey,
+ topicId,
+ cancellationToken);
+
+ var pushPlusResponse = JsonSerializer
+ .Deserialize>(content);
+
+ return pushPlusResponse.GetData();
+ }
+
+ public async virtual Task GetTopicQrCodeAsync(
+ int topicId,
+ PushPlusTopicQrCodeType forever = PushPlusTopicQrCodeType.Temporary,
+ CancellationToken cancellationToken = default)
+ {
+ var token = await PushPlusTokenProvider.GetTokenAsync();
+ var client = HttpClientFactory.GetPushPlusClient();
+
+ var content = await client.GetTopicQrCodeContentAsync(
+ token.AccessKey,
+ topicId,
+ forever,
+ cancellationToken);
+
+ var pushPlusResponse = JsonSerializer
+ .Deserialize>(content);
+
+ return pushPlusResponse.GetData();
+ }
+
+ public async virtual Task QuitTopicAsync(
+ int topicId,
+ CancellationToken cancellationToken = default)
+ {
+ var token = await PushPlusTokenProvider.GetTokenAsync();
+ var client = HttpClientFactory.GetPushPlusClient();
+
+ var content = await client.GetQuitTopicContentAsync(
+ token.AccessKey,
+ topicId,
+ cancellationToken);
+
+ var pushPlusResponse = JsonSerializer
+ .Deserialize>(content);
+
+ return pushPlusResponse.GetData();
+ }
+
+ public async virtual Task> GetSubscriberListAsync(
+ int current,
+ int pageSize,
+ int topicId,
+ CancellationToken cancellationToken = default)
+ {
+ var token = await PushPlusTokenProvider.GetTokenAsync();
+ var client = HttpClientFactory.GetPushPlusClient();
+
+ var content = await client.GetSubscriberListContentAsync(
+ token.AccessKey,
+ current,
+ pageSize,
+ topicId,
+ cancellationToken);
+
+ var pushPlusResponse = JsonSerializer
+ .Deserialize>>(content);
+
+ return pushPlusResponse.GetData();
+ }
+
+ public async virtual Task UnSubscriberAsync(
+ int topicRelationId,
+ CancellationToken cancellationToken = default)
+ {
+ var token = await PushPlusTokenProvider.GetTokenAsync();
+ var client = HttpClientFactory.GetPushPlusClient();
+
+ var content = await client.GetUnSubscriberContentAsync(
+ token.AccessKey,
+ topicRelationId,
+ cancellationToken);
+
+ var pushPlusResponse = JsonSerializer
+ .Deserialize>(content);
+
+ return pushPlusResponse.GetData();
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicQrCode.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicQrCode.cs
new file mode 100644
index 000000000..17cbe6cb4
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicQrCode.cs
@@ -0,0 +1,19 @@
+using Newtonsoft.Json;
+
+namespace LINGYUN.Abp.PushPlus.Topic;
+
+public class PushPlusTopicQrCode
+{
+ ///
+ /// 群组二维码图片路径
+ ///
+ [JsonProperty("qrCodeImgUrl")]
+ public string QrCodeImgUrl { get; set; }
+ ///
+ /// 二维码类型;
+ /// 0-临时二维码,
+ /// 1-永久二维码
+ ///
+ [JsonProperty("forever")]
+ public PushPlusTopicQrCodeType Forever { get; set; }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicQrCodeType.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicQrCodeType.cs
new file mode 100644
index 000000000..87db1f58f
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicQrCodeType.cs
@@ -0,0 +1,12 @@
+namespace LINGYUN.Abp.PushPlus.Topic;
+public enum PushPlusTopicQrCodeType
+{
+ ///
+ /// 临时
+ ///
+ Temporary = 0,
+ ///
+ /// 永久
+ ///
+ Permanent = 1,
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicType.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicType.cs
new file mode 100644
index 000000000..72101d5a4
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicType.cs
@@ -0,0 +1,12 @@
+namespace LINGYUN.Abp.PushPlus.Topic;
+public enum PushPlusTopicType
+{
+ ///
+ /// 创建的
+ ///
+ Create = 0,
+ ///
+ /// 加入的
+ ///
+ Join = 1
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicUser.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicUser.cs
new file mode 100644
index 000000000..3eec790b8
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/PushPlusTopicUser.cs
@@ -0,0 +1,48 @@
+using LINGYUN.Abp.PushPlus.User;
+using Newtonsoft.Json;
+
+namespace LINGYUN.Abp.PushPlus.Topic;
+
+public class PushPlusTopicUser
+{
+ ///
+ /// 用户编号;可用于删除用户
+ ///
+ [JsonProperty("id")]
+ public int Id { get; set; }
+ ///
+ /// 昵称
+ ///
+ [JsonProperty("nickName")]
+ public string NickName { get; set; }
+ ///
+ /// 用户微信openId
+ ///
+ [JsonProperty("openId")]
+ public string OpenId { get; set; }
+ ///
+ /// 头像url地址
+ ///
+ [JsonProperty("headImgUrl")]
+ public string HeadImgUrl { get; set; }
+ ///
+ /// 头像url地址
+ ///
+ [JsonProperty("userSex")]
+ public PushPlusUserSex? UserSex { get; set; }
+ ///
+ /// 是否绑定手机;0-未绑定,1-已绑定
+ ///
+ [JsonProperty("havePhone")]
+ public PushPlusUserPhoneBindStatus? HavePhone { get; set; }
+ ///
+ /// 是否关注微信公众号;0-未关注,1-已关注
+ ///
+ [JsonProperty("isFollow")]
+ public PushPlusUserFollowStatus? IsFollow { get; set; }
+ ///
+ /// 邮箱验证状态;0-未验证,1-待验证,2-已验证
+ ///
+ [JsonProperty("emailStatus")]
+ public PushPlusUserEmailStatus? EmailStatus { get; set; }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/TopicHttpClientExtensions.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/TopicHttpClientExtensions.cs
new file mode 100644
index 000000000..23265456a
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/Topic/TopicHttpClientExtensions.cs
@@ -0,0 +1,177 @@
+using System.Net.Http;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.PushPlus.Topic;
+
+internal static class TopicHttpClientExtensions
+{
+ private const string _getTopicListTemplate = "{\"current\":$current,\"pageSize\":$pageSize,\"params\":{\"topicType\":$topicType}}";
+ public async static Task GetTopicListContentAsync(
+ this HttpClient httpClient,
+ string accessKey,
+ int current,
+ int pageSize,
+ PushPlusTopicType topicType = PushPlusTopicType.Create,
+ CancellationToken cancellationToken = default)
+ {
+ var requestMessage = new HttpRequestMessage(
+ HttpMethod.Post,
+ "/api/open/topic/list");
+
+ var requestBody = _getTopicListTemplate
+ .Replace("$current", current.ToString())
+ .Replace("$pageSize", pageSize.ToString())
+ .Replace("$topicType", ((int)topicType).ToString());
+
+ requestMessage.Content = new StringContent(requestBody, Encoding.UTF8, "application/json");
+ requestMessage.Headers.TryAddWithoutValidation("access-key", accessKey);
+
+ var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken);
+
+ return await httpResponse.Content.ReadAsStringAsync();
+ }
+
+ public async static Task GetTopicProfileContentAsync(
+ this HttpClient httpClient,
+ string accessKey,
+ int topicId,
+ CancellationToken cancellationToken = default)
+ {
+ var requestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ $"/api/open/topic/detail?topicId={topicId}");
+
+ requestMessage.Headers.TryAddWithoutValidation("access-key", accessKey);
+
+ var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken);
+
+ return await httpResponse.Content.ReadAsStringAsync();
+ }
+
+ public async static Task GetTopicForMeProfileContentAsync(
+ this HttpClient httpClient,
+ string accessKey,
+ int topicId,
+ CancellationToken cancellationToken = default)
+ {
+ var requestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ $"/api/open/topic/joinTopicDetail?topicId={topicId}");
+
+ requestMessage.Headers.TryAddWithoutValidation("access-key", accessKey);
+
+ var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken);
+
+ return await httpResponse.Content.ReadAsStringAsync();
+ }
+
+ private const string _createTopicTemplate = "{\"topicCode\":\"$topicCode\",\"topicName\":\"$topicName\",\"contact\":\"$contact\",\"introduction\":\"$introduction\",\"receiptMessage\":\"$receiptMessage\"}";
+ public async static Task GetCreateTopicContentAsync(
+ this HttpClient httpClient,
+ string accessKey,
+ string topicCode,
+ string topicName,
+ string contact,
+ string introduction,
+ string receiptMessage = "",
+ CancellationToken cancellationToken = default)
+ {
+ var requestMessage = new HttpRequestMessage(
+ HttpMethod.Post,
+ "/api/open/topic/add");
+
+ var requestBody = _createTopicTemplate
+ .Replace("$topicCode", topicCode)
+ .Replace("$topicName", topicName)
+ .Replace("$contact", contact)
+ .Replace("$introduction", introduction)
+ .Replace("$receiptMessage", receiptMessage);
+
+ requestMessage.Content = new StringContent(requestBody, Encoding.UTF8, "application/json");
+ requestMessage.Headers.TryAddWithoutValidation("access-key", accessKey);
+
+ var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken);
+
+ return await httpResponse.Content.ReadAsStringAsync();
+ }
+
+ public async static Task GetTopicQrCodeContentAsync(
+ this HttpClient httpClient,
+ string accessKey,
+ int topicId,
+ PushPlusTopicQrCodeType forever = PushPlusTopicQrCodeType.Temporary,
+ CancellationToken cancellationToken = default)
+ {
+ var requestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ $"/api/open/topic/qrCode?topicId={topicId}&forever={(int)forever}");
+
+ requestMessage.Headers.TryAddWithoutValidation("access-key", accessKey);
+
+ var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken);
+
+ return await httpResponse.Content.ReadAsStringAsync();
+ }
+
+ public async static Task GetQuitTopicContentAsync(
+ this HttpClient httpClient,
+ string accessKey,
+ int topicId,
+ CancellationToken cancellationToken = default)
+ {
+ var requestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ $"/api/open/topic/exitTopic?topicId={topicId}");
+
+ requestMessage.Headers.TryAddWithoutValidation("access-key", accessKey);
+
+ var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken);
+
+ return await httpResponse.Content.ReadAsStringAsync();
+ }
+
+ private const string _getSubscriberListTemplate = "{\"current\":$current,\"pageSize\":$pageSize,\"params\":{\"topicId\":$topicId}}";
+ public async static Task GetSubscriberListContentAsync(
+ this HttpClient httpClient,
+ string accessKey,
+ int current,
+ int pageSize,
+ int topicId,
+ CancellationToken cancellationToken = default)
+ {
+ var requestMessage = new HttpRequestMessage(
+ HttpMethod.Post,
+ "/api/open/topicUser/subscriberList");
+
+ var requestBody = _getSubscriberListTemplate
+ .Replace("$current", current.ToString())
+ .Replace("$pageSize", pageSize.ToString())
+ .Replace("$topicId", topicId.ToString());
+
+ requestMessage.Content = new StringContent(requestBody, Encoding.UTF8, "application/json");
+ requestMessage.Headers.TryAddWithoutValidation("access-key", accessKey);
+
+ var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken);
+
+ return await httpResponse.Content.ReadAsStringAsync();
+ }
+
+ public async static Task GetUnSubscriberContentAsync(
+ this HttpClient httpClient,
+ string accessKey,
+ int topicRelationId,
+ CancellationToken cancellationToken = default)
+ {
+ var requestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ $"/api/open/topicUser/deleteTopicUser?topicRelationId={topicRelationId}");
+
+ requestMessage.Headers.TryAddWithoutValidation("access-key", accessKey);
+
+ var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken);
+
+ return await httpResponse.Content.ReadAsStringAsync();
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/IPushPlusUserProvider.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/IPushPlusUserProvider.cs
new file mode 100644
index 000000000..71c816dd8
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/IPushPlusUserProvider.cs
@@ -0,0 +1,28 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.PushPlus.User;
+///
+/// 用户接口
+///
+public interface IPushPlusUserProvider
+{
+ ///
+ /// 获取token
+ ///
+ ///
+ ///
+ Task GetMyTokenAsync(CancellationToken cancellationToken = default);
+ ///
+ /// 个人资料详情
+ ///
+ ///
+ ///
+ Task GetMyProfileAsync(CancellationToken cancellationToken = default);
+ ///
+ /// 获取解封剩余时间
+ ///
+ ///
+ ///
+ Task GetMyLimitTimeAsync(CancellationToken cancellationToken = default);
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserEmailStatus.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserEmailStatus.cs
new file mode 100644
index 000000000..9d09031fa
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserEmailStatus.cs
@@ -0,0 +1,17 @@
+namespace LINGYUN.Abp.PushPlus.User;
+
+public enum PushPlusUserEmailStatus
+{
+ ///
+ /// 未验证
+ ///
+ None = 0,
+ ///
+ /// 待验证
+ ///
+ UnConfirmed = 1,
+ ///
+ /// 已验证
+ ///
+ Confirmed = 2,
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserFollowStatus.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserFollowStatus.cs
new file mode 100644
index 000000000..d754675cd
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserFollowStatus.cs
@@ -0,0 +1,12 @@
+namespace LINGYUN.Abp.PushPlus.User;
+public enum PushPlusUserFollowStatus
+{
+ ///
+ /// 已关注
+ ///
+ Follow = 0,
+ ///
+ /// 未关注
+ ///
+ NoFollow = 1,
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserLimitTime.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserLimitTime.cs
new file mode 100644
index 000000000..8b10699e4
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserLimitTime.cs
@@ -0,0 +1,20 @@
+using Newtonsoft.Json;
+
+namespace LINGYUN.Abp.PushPlus.User;
+
+public class PushPlusUserLimitTime
+{
+ ///
+ /// 发送限制状态;
+ /// 1-无限制,
+ /// 2-短期限制,
+ /// 3-永久限制
+ ///
+ [JsonProperty("sendLimit")]
+ public PushPlusUserSendLimit SendLimit { get; set; }
+ ///
+ /// 解封时间
+ ///
+ [JsonProperty("userLimitTime")]
+ public string UserLimitTime { get; set; }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserPhoneBindStatus.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserPhoneBindStatus.cs
new file mode 100644
index 000000000..6ca31b95b
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserPhoneBindStatus.cs
@@ -0,0 +1,12 @@
+namespace LINGYUN.Abp.PushPlus.User;
+public enum PushPlusUserPhoneBindStatus
+{
+ ///
+ /// 绑定
+ ///
+ Bind = 0,
+ ///
+ /// 未绑定
+ ///
+ NoBind = 1,
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserProfile.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserProfile.cs
new file mode 100644
index 000000000..88a4d7140
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserProfile.cs
@@ -0,0 +1,69 @@
+using Newtonsoft.Json;
+using System;
+
+namespace LINGYUN.Abp.PushPlus.User;
+
+public class PushPlusUserProfile
+{
+ ///
+ /// 用户微信的openId
+ ///
+ [JsonProperty("openId")]
+ public string OpenId { get; set; }
+ ///
+ /// 用户微信的unionId
+ ///
+ [JsonProperty("unionId")]
+ public string UnionId { get; set; }
+ ///
+ /// 昵称
+ ///
+ [JsonProperty("nickName")]
+ public string NickName { get; set; }
+ ///
+ /// 头像
+ ///
+ [JsonProperty("headImgUrl")]
+ public string HeadImgUrl { get; set; }
+ ///
+ /// 性别;
+ /// 0-未设置,
+ /// 1-男,
+ /// 2-女
+ ///
+ [JsonProperty("userSex")]
+ public PushPlusUserSex? Sex { get; set; }
+ ///
+ /// 用户令牌
+ ///
+ [JsonProperty("token")]
+ public string Token { get; set; }
+ ///
+ /// 手机号
+ ///
+ [JsonProperty("phoneNumber")]
+ public string PhoneNumber { get; set; }
+ ///
+ /// 邮箱
+ ///
+ [JsonProperty("email")]
+ public string Email { get; set; }
+ ///
+ /// 邮箱验证状态;
+ /// 0-未验证,
+ /// 1-待验证,
+ /// 2-已验证
+ ///
+ [JsonProperty("emailStatus")]
+ public PushPlusUserEmailStatus? EmailStatus { get; set; }
+ ///
+ /// 生日
+ ///
+ [JsonProperty("birthday")]
+ public DateTime? Birthday { get; set; }
+ ///
+ /// 用户积分
+ ///
+ [JsonProperty("points")]
+ public int? Points { get; set; }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserProvider.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserProvider.cs
new file mode 100644
index 000000000..cb089a032
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserProvider.cs
@@ -0,0 +1,74 @@
+using LINGYUN.Abp.PushPlus.Token;
+using Microsoft.Extensions.Logging;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using Volo.Abp.Json;
+using Volo.Abp.DependencyInjection;
+
+namespace LINGYUN.Abp.PushPlus.User;
+
+public class PushPlusUserProvider : IPushPlusUserProvider, ITransientDependency
+{
+ protected ILogger Logger { get; }
+ protected IJsonSerializer JsonSerializer { get; }
+ protected IHttpClientFactory HttpClientFactory { get; }
+ protected IPushPlusTokenProvider PushPlusTokenProvider { get; }
+
+ public PushPlusUserProvider(
+ ILogger logger,
+ IJsonSerializer jsonSerializer,
+ IHttpClientFactory httpClientFactory,
+ IPushPlusTokenProvider pushPlusTokenProvider)
+ {
+ Logger = logger;
+ JsonSerializer = jsonSerializer;
+ HttpClientFactory = httpClientFactory;
+ PushPlusTokenProvider = pushPlusTokenProvider;
+ }
+
+ public async virtual Task GetMyLimitTimeAsync(CancellationToken cancellationToken = default)
+ {
+ var token = await PushPlusTokenProvider.GetTokenAsync();
+ var client = HttpClientFactory.GetPushPlusClient();
+
+ var content = await client.GetLimitTimeContentAsync(
+ token.AccessKey,
+ cancellationToken);
+
+ var pushPlusResponse = JsonSerializer
+ .Deserialize>(content);
+
+ return pushPlusResponse.GetData();
+ }
+
+ public async virtual Task GetMyProfileAsync(CancellationToken cancellationToken = default)
+ {
+ var token = await PushPlusTokenProvider.GetTokenAsync();
+ var client = HttpClientFactory.GetPushPlusClient();
+
+ var content = await client.GetProfileContentAsync(
+ token.AccessKey,
+ cancellationToken);
+
+ var pushPlusResponse = JsonSerializer
+ .Deserialize>(content);
+
+ return pushPlusResponse.GetData();
+ }
+
+ public async virtual Task GetMyTokenAsync(CancellationToken cancellationToken = default)
+ {
+ var token = await PushPlusTokenProvider.GetTokenAsync();
+ var client = HttpClientFactory.GetPushPlusClient();
+
+ var content = await client.GetTokenContentAsync(
+ token.AccessKey,
+ cancellationToken);
+
+ var pushPlusResponse = JsonSerializer
+ .Deserialize>(content);
+
+ return pushPlusResponse.GetData();
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserSendLimit.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserSendLimit.cs
new file mode 100644
index 000000000..58eb635f8
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserSendLimit.cs
@@ -0,0 +1,17 @@
+namespace LINGYUN.Abp.PushPlus.User;
+
+public enum PushPlusUserSendLimit
+{
+ ///
+ /// 未限制
+ ///
+ None = 1,
+ ///
+ /// 短期限制
+ ///
+ Short = 2,
+ ///
+ /// 永久限制
+ ///
+ Permanent = 3,
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserSex.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserSex.cs
new file mode 100644
index 000000000..e7aafec21
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/PushPlusUserSex.cs
@@ -0,0 +1,16 @@
+namespace LINGYUN.Abp.PushPlus.User;
+public enum PushPlusUserSex
+{
+ ///
+ /// 未设置
+ ///
+ NoSet = 0,
+ ///
+ /// 男
+ ///
+ Man =1,
+ ///
+ /// 女
+ ///
+ Woman = 2,
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/UserHttpClientExtensions.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/UserHttpClientExtensions.cs
new file mode 100644
index 000000000..ab52adf4f
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/LINGYUN/Abp/PushPlus/User/UserHttpClientExtensions.cs
@@ -0,0 +1,56 @@
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LINGYUN.Abp.PushPlus.User;
+
+internal static class UserHttpClientExtensions
+{
+ public async static Task GetTokenContentAsync(
+ this HttpClient httpClient,
+ string accessKey,
+ CancellationToken cancellationToken = default)
+ {
+ var requestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ "/api/open/user/token");
+
+ requestMessage.Headers.TryAddWithoutValidation("access-key", accessKey);
+
+ var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken);
+
+ return await httpResponse.Content.ReadAsStringAsync();
+ }
+
+ public async static Task GetProfileContentAsync(
+ this HttpClient httpClient,
+ string accessKey,
+ CancellationToken cancellationToken = default)
+ {
+ var requestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ "api/open/user/myInfo");
+
+ requestMessage.Headers.TryAddWithoutValidation("access-key", accessKey);
+
+ var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken);
+
+ return await httpResponse.Content.ReadAsStringAsync();
+ }
+
+ public async static Task GetLimitTimeContentAsync(
+ this HttpClient httpClient,
+ string accessKey,
+ CancellationToken cancellationToken = default)
+ {
+ var requestMessage = new HttpRequestMessage(
+ HttpMethod.Get,
+ "api/open/user/userLimitTime");
+
+ requestMessage.Headers.TryAddWithoutValidation("access-key", accessKey);
+
+ var httpResponse = await httpClient.SendAsync(requestMessage, cancellationToken);
+
+ return await httpResponse.Content.ReadAsStringAsync();
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/Microsoft/Extensions/DependencyInjection/IServiceConnectionExtensions.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/Microsoft/Extensions/DependencyInjection/IServiceConnectionExtensions.cs
new file mode 100644
index 000000000..5a3f2d96c
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/Microsoft/Extensions/DependencyInjection/IServiceConnectionExtensions.cs
@@ -0,0 +1,19 @@
+using System;
+
+namespace Microsoft.Extensions.DependencyInjection;
+
+internal static class IServiceConnectionExtensions
+{
+ public static IServiceCollection AddPushPlusClient(
+ this IServiceCollection services)
+ {
+ services.AddHttpClient(
+ "_Abp_PushPlus_Client",
+ (httpClient) =>
+ {
+ httpClient.BaseAddress = new Uri("https://www.pushplus.plus");
+ });
+
+ return services;
+ }
+}
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/README.md b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/README.md
new file mode 100644
index 000000000..d77748b3c
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/README.md
@@ -0,0 +1,42 @@
+# LINGYUN.Abp.PushPlus
+
+集成PushPlus
+
+实现PushPlus相关Api文档,拥有PushPlus开放能力
+
+详情见PushPlus文档: https://www.pushplus.plus/doc/guide/openApi.html#%E6%96%87%E6%A1%A3%E8%AF%B4%E6%98%8E
+
+## 模块引用
+
+```csharp
+[DependsOn(typeof(AbpPushPlusModule))]
+public class YouProjectModule : AbpModule
+{
+ // other
+}
+```
+
+## Features
+
+* PushPlus PushPlus特性分组
+* PushPlus.Message.Enable 全局启用PushPlus消息通道
+* PushPlus.Channel.WeChat 微信公众号消息通道
+* PushPlus.Channel.WeChat.Enable 启用微信公众号消息通道
+* PushPlus.Channel.WeChat.SendLimit 微信公众号消息通道限制次数
+* PushPlus.Channel.WeChat.SendLimitInterval 微信公众号消息通道限制周期(天)
+* PushPlus.Channel.WeWork 企业微信消息通道
+* PushPlus.Channel.WeWork.Enable 启用企业微信消息通道
+* PushPlus.Channel.WeWork.SendLimit 企业微信消息通道限制次数
+* PushPlus.Channel.WeWork.SendLimitInterval 企业微信消息通道限制周期(天)
+* PushPlus.Channel.Webhook Webhook消息通道
+* PushPlus.Channel.Webhook.Enable 启用Webhook消息通道
+* PushPlus.Channel.Webhook.SendLimit Webhook消息通道限制次数
+* PushPlus.Channel.Webhook.SendLimitInterval Webhook消息通道限制周期(天)
+* PushPlus.Channel.Email Email消息通道
+* PushPlus.Channel.Email.Enable 启用Email消息通道
+* PushPlus.Channel.Email.SendLimit Email消息通道限制次数
+* PushPlus.Channel.Email.SendLimitInterval Email消息通道限制周期(天)
+* PushPlus.Channel.Sms 短信消息通道
+* PushPlus.Channel.Sms.Enable 启用短信消息通道
+* PushPlus.Channel.Sms.SendLimit 短信消息通道限制次数
+* PushPlus.Channel.Sms.SendLimitInterval 短信消息通道限制周期(天)
diff --git a/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/System/Net/Http/IHttpClientFactoryExtensions.cs b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/System/Net/Http/IHttpClientFactoryExtensions.cs
new file mode 100644
index 000000000..7be814ecc
--- /dev/null
+++ b/aspnet-core/modules/pushplus/LINGYUN.Abp.PushPlus/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_PushPlus_Client");
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.PushPlus.Tests/LINGYUN.Abp.PushPlus.Tests.csproj b/aspnet-core/tests/LINGYUN.Abp.PushPlus.Tests/LINGYUN.Abp.PushPlus.Tests.csproj
new file mode 100644
index 000000000..3d29463d8
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.PushPlus.Tests/LINGYUN.Abp.PushPlus.Tests.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net6.0
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/tests/LINGYUN.Abp.PushPlus.Tests/LINGYUN/Abp/PushPlus/AbpPushPlusTestBase.cs b/aspnet-core/tests/LINGYUN.Abp.PushPlus.Tests/LINGYUN/Abp/PushPlus/AbpPushPlusTestBase.cs
new file mode 100644
index 000000000..2ad8fdae9
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.PushPlus.Tests/LINGYUN/Abp/PushPlus/AbpPushPlusTestBase.cs
@@ -0,0 +1,7 @@
+using LINGYUN.Abp.Tests;
+
+namespace LINGYUN.Abp.PushPlus;
+
+public class AbpPushPlusTestBase : AbpTestsBase
+{
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.PushPlus.Tests/LINGYUN/Abp/PushPlus/AbpPushPlusTestModule.cs b/aspnet-core/tests/LINGYUN.Abp.PushPlus.Tests/LINGYUN/Abp/PushPlus/AbpPushPlusTestModule.cs
new file mode 100644
index 000000000..17c8c3300
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.PushPlus.Tests/LINGYUN/Abp/PushPlus/AbpPushPlusTestModule.cs
@@ -0,0 +1,24 @@
+using LINGYUN.Abp.Tests;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.Modularity;
+
+namespace LINGYUN.Abp.PushPlus;
+
+[DependsOn(
+ typeof(AbpPushPlusModule),
+ typeof(AbpTestsBaseModule))]
+public class AbpPushPlusTestModule : AbpModule
+{
+ public override void PreConfigureServices(ServiceConfigurationContext context)
+ {
+ var configurationOptions = new AbpConfigurationBuilderOptions
+ {
+ BasePath = @"D:\Projects\Development\Abp\PushPlus",
+ EnvironmentName = "Test"
+ };
+ var configuration = ConfigurationHelper.BuildConfiguration(configurationOptions);
+
+ context.Services.ReplaceConfiguration(configuration);
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.PushPlus.Tests/LINGYUN/Abp/PushPlus/Message/PushPlusMessageSenderTests.cs b/aspnet-core/tests/LINGYUN.Abp.PushPlus.Tests/LINGYUN/Abp/PushPlus/Message/PushPlusMessageSenderTests.cs
new file mode 100644
index 000000000..58b3a511d
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.PushPlus.Tests/LINGYUN/Abp/PushPlus/Message/PushPlusMessageSenderTests.cs
@@ -0,0 +1,40 @@
+using System.Threading.Tasks;
+using Xunit;
+using Shouldly;
+
+namespace LINGYUN.Abp.PushPlus.Message;
+
+///
+/// TODO: 接入webhook测试
+/// TODO: 接入企业微信测试
+///
+public class PushPlusMessageSenderTests : AbpPushPlusTestBase
+{
+ protected IPushPlusMessageSender PushPlusMessageSender { get; }
+ public PushPlusMessageSenderTests()
+ {
+ PushPlusMessageSender = GetRequiredService();
+ }
+
+ [Theory]
+ [InlineData("Title from the Xunit unit test", "Content from the Xunit unit test.")]
+ public async virtual Task Send_To_We_Chat_Test(
+ string title,
+ string content)
+ {
+ var result = await PushPlusMessageSender.SendWeChatAsync(title, content);
+
+ result.ShouldNotBeNullOrWhiteSpace();
+ }
+
+ [Theory]
+ [InlineData("Title from the Xunit unit test", "Content from the Xunit unit test.")]
+ public async virtual Task Send_To_Email_Test(
+ string title,
+ string content)
+ {
+ var result = await PushPlusMessageSender.SendEmailAsync(title, content);
+
+ result.ShouldNotBeNullOrWhiteSpace();
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/Features/FakeFeatureStore.cs b/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/Features/FakeFeatureStore.cs
index e84462a03..3470b0a1a 100644
--- a/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/Features/FakeFeatureStore.cs
+++ b/aspnet-core/tests/LINGYUN.Abp.TestBase/LINGYUN/Abp/Tests/Features/FakeFeatureStore.cs
@@ -21,9 +21,12 @@ namespace LINGYUN.Abp.Tests.Features
{
var feature = FeatureDefinitionManager.Get(name);
- var featureFunc = FakeFeatureOptions.FeatureMaps[name];
+ if (FakeFeatureOptions.FeatureMaps.TryGetValue(name, out var featureFunc))
+ {
+ return Task.FromResult(featureFunc(feature));
+ }
- return Task.FromResult(featureFunc(feature));
+ return Task.FromResult(feature.DefaultValue);
}
}
}