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