diff --git a/Sanhe.Abp.Framework.sln b/Sanhe.Abp.Framework.sln index df0fb1f..9bc6d96 100644 --- a/Sanhe.Abp.Framework.sln +++ b/Sanhe.Abp.Framework.sln @@ -67,6 +67,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sanhe.Abp.RealTime", "modul EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sanhe.Abp.IdGenerator", "modules\common\Sanhe.Abp.IdGenerator\Sanhe.Abp.IdGenerator.csproj", "{86CDAB3C-D7EC-4F4B-BCB9-2069DAE1D0FC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sanhe.Abp.Notifications", "modules\common\Sanhe.Abp.Notifications\Sanhe.Abp.Notifications.csproj", "{8F9EA0EF-6C02-4017-AEB4-A0B4CB4D5983}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -157,6 +159,10 @@ Global {86CDAB3C-D7EC-4F4B-BCB9-2069DAE1D0FC}.Debug|Any CPU.Build.0 = Debug|Any CPU {86CDAB3C-D7EC-4F4B-BCB9-2069DAE1D0FC}.Release|Any CPU.ActiveCfg = Release|Any CPU {86CDAB3C-D7EC-4F4B-BCB9-2069DAE1D0FC}.Release|Any CPU.Build.0 = Release|Any CPU + {8F9EA0EF-6C02-4017-AEB4-A0B4CB4D5983}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8F9EA0EF-6C02-4017-AEB4-A0B4CB4D5983}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8F9EA0EF-6C02-4017-AEB4-A0B4CB4D5983}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8F9EA0EF-6C02-4017-AEB4-A0B4CB4D5983}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -188,6 +194,7 @@ Global {EE08FB0C-EDC1-4DEF-B0C8-699F4E7D08AF} = {2A768109-31B7-4C52-928C-3023DAB9F254} {C99CF772-210B-4C21-84FE-089B7D038A28} = {2A768109-31B7-4C52-928C-3023DAB9F254} {86CDAB3C-D7EC-4F4B-BCB9-2069DAE1D0FC} = {2A768109-31B7-4C52-928C-3023DAB9F254} + {8F9EA0EF-6C02-4017-AEB4-A0B4CB4D5983} = {2A768109-31B7-4C52-928C-3023DAB9F254} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AB69BFDE-9DDB-4D16-8CB8-72472C0319CD} diff --git a/modules/common/Sanhe.Abp.IdGenerator/Sanhe.Abp.IdGenerator.csproj b/modules/common/Sanhe.Abp.IdGenerator/Sanhe.Abp.IdGenerator.csproj index 78ba939..a179063 100644 --- a/modules/common/Sanhe.Abp.IdGenerator/Sanhe.Abp.IdGenerator.csproj +++ b/modules/common/Sanhe.Abp.IdGenerator/Sanhe.Abp.IdGenerator.csproj @@ -1,9 +1,10 @@ - + netstandard2.0 + diff --git a/modules/common/Sanhe.Abp.IdGenerator/Sanhe/Abp/IdGenerator/AbpIdGeneratorModule.cs b/modules/common/Sanhe.Abp.IdGenerator/Sanhe/Abp/IdGenerator/AbpIdGeneratorModule.cs index 904e52d..dfad795 100644 --- a/modules/common/Sanhe.Abp.IdGenerator/Sanhe/Abp/IdGenerator/AbpIdGeneratorModule.cs +++ b/modules/common/Sanhe.Abp.IdGenerator/Sanhe/Abp/IdGenerator/AbpIdGeneratorModule.cs @@ -1,9 +1,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Sanhe.Abp.IdGenerator.Sanhe.Abp.IdGenerator.Snowflake; +using Sanhe.Abp.IdGenerator.Snowflake; using Volo.Abp.Modularity; -namespace Sanhe.Abp.IdGenerator.Sanhe.Abp.IdGenerator; +namespace Sanhe.Abp.IdGenerator; public class AbpIdGeneratorModule: AbpModule { diff --git a/modules/common/Sanhe.Abp.IdGenerator/Sanhe/Abp/IdGenerator/IDistributedIdGenerator.cs b/modules/common/Sanhe.Abp.IdGenerator/Sanhe/Abp/IdGenerator/IDistributedIdGenerator.cs index 15e87c0..4bcfae0 100644 --- a/modules/common/Sanhe.Abp.IdGenerator/Sanhe/Abp/IdGenerator/IDistributedIdGenerator.cs +++ b/modules/common/Sanhe.Abp.IdGenerator/Sanhe/Abp/IdGenerator/IDistributedIdGenerator.cs @@ -1,4 +1,4 @@ -namespace Sanhe.Abp.IdGenerator.Sanhe.Abp.IdGenerator; +namespace Sanhe.Abp.IdGenerator; public interface IDistributedIdGenerator { diff --git a/modules/common/Sanhe.Abp.IdGenerator/Sanhe/Abp/IdGenerator/Snowflake/SnowflakeIdGenerator.cs b/modules/common/Sanhe.Abp.IdGenerator/Sanhe/Abp/IdGenerator/Snowflake/SnowflakeIdGenerator.cs index c69591c..1d562a3 100644 --- a/modules/common/Sanhe.Abp.IdGenerator/Sanhe/Abp/IdGenerator/Snowflake/SnowflakeIdGenerator.cs +++ b/modules/common/Sanhe.Abp.IdGenerator/Sanhe/Abp/IdGenerator/Snowflake/SnowflakeIdGenerator.cs @@ -1,7 +1,7 @@ using System; using Volo.Abp; -namespace Sanhe.Abp.IdGenerator.Sanhe.Abp.IdGenerator.Snowflake; +namespace Sanhe.Abp.IdGenerator.Snowflake; // reference: https://github.com/dotnetcore/CAP // reference: https://blog.csdn.net/lq18050010830/article/details/89845790 diff --git a/modules/common/Sanhe.Abp.IdGenerator/Sanhe/Abp/IdGenerator/Snowflake/SnowflakeIdOptions.cs b/modules/common/Sanhe.Abp.IdGenerator/Sanhe/Abp/IdGenerator/Snowflake/SnowflakeIdOptions.cs index a6aa7e9..3eae852 100644 --- a/modules/common/Sanhe.Abp.IdGenerator/Sanhe/Abp/IdGenerator/Snowflake/SnowflakeIdOptions.cs +++ b/modules/common/Sanhe.Abp.IdGenerator/Sanhe/Abp/IdGenerator/Snowflake/SnowflakeIdOptions.cs @@ -1,4 +1,4 @@ -namespace Sanhe.Abp.IdGenerator.Sanhe.Abp.IdGenerator.Snowflake; +namespace Sanhe.Abp.IdGenerator.Snowflake; public class SnowflakeIdOptions { diff --git a/modules/common/Sanhe.Abp.Notifications/FodyWeavers.xml b/modules/common/Sanhe.Abp.Notifications/FodyWeavers.xml new file mode 100644 index 0000000..1715698 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe.Abp.Notifications.csproj b/modules/common/Sanhe.Abp.Notifications/Sanhe.Abp.Notifications.csproj new file mode 100644 index 0000000..6750c0f --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe.Abp.Notifications.csproj @@ -0,0 +1,23 @@ + + + + + + + netstandard2.0 + + + + + + + + + + + + + + + + diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/AbpNotificationsModule.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/AbpNotificationsModule.cs new file mode 100644 index 0000000..acc278f --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/AbpNotificationsModule.cs @@ -0,0 +1,46 @@ +using Microsoft.Extensions.DependencyInjection; +using Sanhe.Abp.IdGenerator; +using Sanhe.Abp.RealTime; +using System; +using System.Collections.Generic; +using Volo.Abp.BackgroundJobs; +using Volo.Abp.BackgroundWorkers; +using Volo.Abp.Json; +using Volo.Abp.Modularity; + +namespace Sanhe.Abp.Notifications; + +// TODO: 需要重命名 AbpNotificationsModule +[DependsOn( + typeof(AbpBackgroundWorkersModule), + typeof(AbpBackgroundJobsAbstractionsModule), + typeof(AbpIdGeneratorModule), + typeof(AbpJsonModule), + typeof(AbpRealTimeModule))] +public class AbpNotificationsModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + AutoAddDefinitionProviders(context.Services); + } + + private void AutoAddDefinitionProviders(IServiceCollection services) + { + var definitionProviders = new List(); + + services.OnRegistred(context => + { + if (typeof(INotificationDefinitionProvider).IsAssignableFrom(context.ImplementationType)) + { + definitionProviders.Add(context.ImplementationType); + } + }); + + var preActions = services.GetPreConfigureActions(); + Configure(options => + { + preActions.Configure(options); + options.DefinitionProviders.AddIfNotContains(definitionProviders); + }); + } +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/AbpNotificationsOptions.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/AbpNotificationsOptions.cs new file mode 100644 index 0000000..0a6d06c --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/AbpNotificationsOptions.cs @@ -0,0 +1,29 @@ +using Volo.Abp.Collections; + +namespace Sanhe.Abp.Notifications; + +/// +/// 通知选项 +/// +public class AbpNotificationsOptions +{ + /// + /// 自定义通知集合 + /// + public ITypeList DefinitionProviders { get; } + /// + /// 发布者集合 + /// + public ITypeList PublishProviders { get; } + /// + /// 可以自定义某个通知的格式 + /// + public NotificationDataMappingDictionary NotificationDataMappings { get; } + + public AbpNotificationsOptions() + { + PublishProviders = new TypeList(); + DefinitionProviders = new TypeList(); + NotificationDataMappings = new NotificationDataMappingDictionary(); + } +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationDefinitionContext.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationDefinitionContext.cs new file mode 100644 index 0000000..460ffc2 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationDefinitionContext.cs @@ -0,0 +1,33 @@ +using JetBrains.Annotations; +using Volo.Abp.Localization; + +namespace Sanhe.Abp.Notifications; + +/// +/// 通知定义上下文 +/// +public interface INotificationDefinitionContext +{ + /// + /// 添加通知组定义 + /// + /// + /// + /// + /// + NotificationGroupDefinition AddGroup( + [NotNull] string name, + ILocalizableString displayName = null, + bool allowSubscriptionToClients = true); + /// + /// 获取通知组定义 + /// + /// + /// + NotificationGroupDefinition GetGroupOrNull(string name); + /// + /// 移除通知组 + /// + /// + void RemoveGroup(string name); +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationDefinitionManager.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationDefinitionManager.cs new file mode 100644 index 0000000..45c6abd --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationDefinitionManager.cs @@ -0,0 +1,34 @@ +using JetBrains.Annotations; +using System.Collections.Generic; + +namespace Sanhe.Abp.Notifications; + +/// +/// 通知定义管理者接口 +/// +public interface INotificationDefinitionManager +{ + /// + /// 获取通知定义 + /// + /// + /// + [NotNull] + NotificationDefinition Get([NotNull] string name); + /// + /// 获取所有通知定义 + /// + /// + IReadOnlyList GetAll(); + /// + /// 获取通知定义,如果为空返回Null + /// + /// + /// + NotificationDefinition GetOrNull(string name); + /// + /// 获取通知定义分组列表 + /// + /// + IReadOnlyList GetGroups(); +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationDefinitionProvider.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationDefinitionProvider.cs new file mode 100644 index 0000000..5c4f885 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationDefinitionProvider.cs @@ -0,0 +1,14 @@ +namespace Sanhe.Abp.Notifications +{ + /// + /// 通知定义提供者接口 + /// + public interface INotificationDefinitionProvider + { + /// + /// 定义 + /// + /// 通知定义上下文 + void Define(INotificationDefinitionContext context); + } +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationPublishProvider.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationPublishProvider.cs new file mode 100644 index 0000000..4751512 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationPublishProvider.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Sanhe.Abp.Notifications; + +/// +/// 通知发布提供者接口 +/// +public interface INotificationPublishProvider +{ + /// + /// 名称 + /// + string Name { get; } + /// + /// 发布通知 + /// + /// 通知信息 + /// 接收用户列表 + /// + Task PublishAsync(NotificationInfo notification, IEnumerable identifiers); +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationPublishProviderManager.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationPublishProviderManager.cs new file mode 100644 index 0000000..d7974b1 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationPublishProviderManager.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace Sanhe.Abp.Notifications +{ + /// + /// 通知发布提供者管理 + /// + public interface INotificationPublishProviderManager + { + /// + /// 提供者列表 + /// + List Providers { get; } + } +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationSender.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationSender.cs new file mode 100644 index 0000000..19833d3 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationSender.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Sanhe.Abp.Notifications; + +/// +/// 发送通知接口 +/// +public interface INotificationSender +{ + /// + /// 发送通知 + /// + /// 名称 + /// 数据 + /// 用户,为空标识发给所有订阅用户 + /// 租户 + /// 严重级别 + /// 通知标识 + Task SendNofiterAsync( + string name, + NotificationData data, + UserIdentifier user = null, + Guid? tenantId = null, + NotificationSeverity severity = NotificationSeverity.Info); + + /// + /// 发送通知 + /// + /// 名称 + /// 数据 + /// 用户列表,为空标识发给所有订阅用户 + /// 租户 + /// 严重级别 + /// 通知标识 + Task SendNofitersAsync( + string name, + NotificationData data, + IEnumerable users = null, + Guid? tenantId = null, + NotificationSeverity severity = NotificationSeverity.Info); +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationStore.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationStore.cs new file mode 100644 index 0000000..6889650 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationStore.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Sanhe.Abp.Notifications; + +/// +/// 通知相关储存处 +/// +public interface INotificationStore +{ + Task InsertUserSubscriptionAsync( + Guid? tenantId, + UserIdentifier identifier, + string notificationName, + CancellationToken cancellationToken = default); + + Task InsertUserSubscriptionAsync( + Guid? tenantId, + IEnumerable identifiers, + string notificationName, + CancellationToken cancellationToken = default); + + Task DeleteUserSubscriptionAsync( + Guid? tenantId, + Guid userId, + string notificationName, + CancellationToken cancellationToken = default); + + Task DeleteAllUserSubscriptionAsync( + Guid? tenantId, + string notificationName, + CancellationToken cancellationToken = default); + + Task DeleteUserSubscriptionAsync( + Guid? tenantId, + IEnumerable identifiers, + string notificationName, + CancellationToken cancellationToken = default); + + Task> GetUserSubscriptionsAsync( + Guid? tenantId, + string notificationName, + IEnumerable identifiers = null, + CancellationToken cancellationToken = default); + + Task> GetUserSubscriptionsAsync( + Guid? tenantId, + Guid userId, + CancellationToken cancellationToken = default); + + Task> GetUserSubscriptionsAsync( + Guid? tenantId, + string userName, + CancellationToken cancellationToken = default); + + Task IsSubscribedAsync( + Guid? tenantId, + Guid userId, + string notificationName, + CancellationToken cancellationToken = default); + + Task InsertNotificationAsync( + NotificationInfo notification, + CancellationToken cancellationToken = default); + + Task DeleteNotificationAsync( + NotificationInfo notification, + CancellationToken cancellationToken = default); + + Task DeleteNotificationAsync( + int batchCount, + CancellationToken cancellationToken = default); + + Task InsertUserNotificationAsync( + NotificationInfo notification, + Guid userId, + CancellationToken cancellationToken = default); + + Task InsertUserNotificationsAsync( + NotificationInfo notification, + IEnumerable userIds, + CancellationToken cancellationToken = default); + + Task DeleteUserNotificationAsync( + Guid? tenantId, + Guid userId, + long notificationId, + CancellationToken cancellationToken = default); + + Task GetNotificationOrNullAsync( + Guid? tenantId, + long notificationId, + CancellationToken cancellationToken = default); + + Task> GetUserNotificationsAsync( + Guid? tenantId, + Guid userId, + NotificationReadState? readState = null, + int maxResultCount = 10, + CancellationToken cancellationToken = default); + + Task GetUserNotificationsCountAsync( + Guid? tenantId, + Guid userId, + string filter = "", + NotificationReadState? readState = null, + CancellationToken cancellationToken = default); + + Task> GetUserNotificationsAsync( + Guid? tenantId, + Guid userId, + string filter = "", + string sorting = nameof(NotificationInfo.CreationTime), + NotificationReadState? readState = null, + int skipCount = 1, + int maxResultCount = 10, + CancellationToken cancellationToken = default); + + Task ChangeUserNotificationReadStateAsync( + Guid? tenantId, + Guid userId, + long notificationId, + NotificationReadState readState, + CancellationToken cancellationToken = default); +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationSubscriptionManager.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationSubscriptionManager.cs new file mode 100644 index 0000000..f68b9ee --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/INotificationSubscriptionManager.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Sanhe.Abp.Notifications; + +/// +/// 通知订阅管理器 +/// +public interface INotificationSubscriptionManager +{ + /// + /// 是否已订阅 + /// + /// 租户 + /// 用户标识 + /// 通知名称 + /// + Task IsSubscribedAsync( + Guid? tenantId, + Guid userId, + string notificationName, + CancellationToken cancellationToken = default); + + /// + /// 订阅通知 + /// + /// 租户 + /// 用户标识 + /// 通知名称 + /// + Task SubscribeAsync( + Guid? tenantId, + UserIdentifier identifier, + string notificationName, + CancellationToken cancellationToken = default); + + /// + /// 订阅通知 + /// + /// 租户 + /// 用户标识列表 + /// 通知名称 + /// + Task SubscribeAsync( + Guid? tenantId, + IEnumerable identifiers, + string notificationName, + CancellationToken cancellationToken = default); + + /// + /// 取消所有用户订阅 + /// + /// 租户 + /// 通知名称 + /// + Task UnsubscribeAllAsync( + Guid? tenantId, + string notificationName, + CancellationToken cancellationToken = default); + + /// + /// 取消订阅 + /// + /// 租户 + /// 用户标识 + /// 通知名称 + /// + Task UnsubscribeAsync( + Guid? tenantId, + UserIdentifier identifier, + string notificationName, + CancellationToken cancellationToken = default); + + /// + /// 取消订阅 + /// + /// 租户 + /// 用户标识列表 + /// 通知名称 + /// + Task UnsubscribeAsync( + Guid? tenantId, + IEnumerable identifiers, + string notificationName, + CancellationToken cancellationToken = default); + + /// + /// 获取通知被订阅用户列表 + /// + /// 租户 + /// 通知名称 + /// 需要检查的用户列表 + /// + Task> GetUsersSubscriptionsAsync( + Guid? tenantId, + string notificationName, + IEnumerable identifiers = null, + CancellationToken cancellationToken = default); + + /// + /// 获取用户订阅列表 + /// + /// 租户 + /// 用户标识 + /// + Task> GetUserSubscriptionsAsync( + Guid? tenantId, + Guid userId, + CancellationToken cancellationToken = default); + + /// + /// 获取用户订阅列表 + /// + /// 租户 + /// 用户名 + /// + Task> GetUserSubscriptionsAsync( + Guid? tenantId, + string userName, + CancellationToken cancellationToken = default); +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/Internal/NotificationSender.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/Internal/NotificationSender.cs new file mode 100644 index 0000000..08fb86f --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/Internal/NotificationSender.cs @@ -0,0 +1,115 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Sanhe.Abp.IdGenerator; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.EventBus.Distributed; +using Volo.Abp.Uow; + +namespace Sanhe.Abp.Notifications.Internal +{ + /// + /// 默认实现通过分布式事件发送通知 + /// 可替换实现来发送实时通知 + /// + public class NotificationSender : INotificationSender, ITransientDependency + { + /// + /// Reference to . + /// + public ILogger Logger { get; set; } + /// + /// Reference to . + /// + public IDistributedEventBus DistributedEventBus { get; } + /// + /// Reference to . + /// + protected IDistributedIdGenerator DistributedIdGenerator { get; } + /// + /// Reference to . + /// + protected IUnitOfWorkManager UnitOfWorkManager { get; } + /// + /// 通知选项 + /// + protected AbpNotificationsOptions Options { get; } + + public NotificationSender( + IDistributedEventBus distributedEventBus, + IDistributedIdGenerator distributedIdGenerator, + IUnitOfWorkManager unitOfWorkManager, + IOptions optionsAccessor) + { + Options = optionsAccessor.Value; + DistributedEventBus = distributedEventBus; + DistributedIdGenerator = distributedIdGenerator; + UnitOfWorkManager = unitOfWorkManager; + Logger = NullLogger.Instance; + } + + public async Task SendNofiterAsync( + string name, + NotificationData data, + UserIdentifier user = null, + Guid? tenantId = null, + NotificationSeverity severity = NotificationSeverity.Info) + { + if (user == null) + { + return await PublishNofiterAsync(name, data, null, tenantId, severity); + + } + else + { + return await PublishNofiterAsync(name, data, new List { user }, tenantId, severity); + } + } + + public async Task SendNofitersAsync( + string name, + NotificationData data, + IEnumerable users = null, + Guid? tenantId = null, + NotificationSeverity severity = NotificationSeverity.Info) + { + return await PublishNofiterAsync(name, data, users, tenantId, severity); + } + + protected async Task PublishNofiterAsync( + string name, + NotificationData data, + IEnumerable users = null, + Guid? tenantId = null, + NotificationSeverity severity = NotificationSeverity.Info) + { + var eto = new NotificationEto(data) + { + Id = DistributedIdGenerator.Create(), + TenantId = tenantId, + Users = users?.ToList(), + Name = name, + CreationTime = DateTime.Now, + Severity = severity + }; + + if (UnitOfWorkManager.Current != null) + { + UnitOfWorkManager.Current.OnCompleted(async () => + { + await DistributedEventBus.PublishAsync(eto); + }); + } + else + { + await DistributedEventBus.PublishAsync(eto); + } + + return eto.Id.ToString(); + } + } +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/Internal/NotificationSubscriptionManager.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/Internal/NotificationSubscriptionManager.cs new file mode 100644 index 0000000..ba8bdf8 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/Internal/NotificationSubscriptionManager.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Sanhe.Abp.Notifications.Internal +{ + internal class NotificationSubscriptionManager : INotificationSubscriptionManager, ITransientDependency + { + private readonly INotificationStore _store; + + public NotificationSubscriptionManager(INotificationStore store) + { + _store = store; + } + + public async virtual Task> GetUsersSubscriptionsAsync( + Guid? tenantId, + string notificationName, + IEnumerable identifiers = null, + CancellationToken cancellationToken = default) + { + return await _store.GetUserSubscriptionsAsync(tenantId, notificationName, identifiers, cancellationToken); + } + + public async virtual Task> GetUserSubscriptionsAsync( + Guid? tenantId, + Guid userId, + CancellationToken cancellationToken = default) + { + return await _store.GetUserSubscriptionsAsync(tenantId, userId, cancellationToken); + } + + public async virtual Task> GetUserSubscriptionsAsync( + Guid? tenantId, + string userName, + CancellationToken cancellationToken = default) + { + return await _store.GetUserSubscriptionsAsync(tenantId, userName, cancellationToken); + } + + public async virtual Task IsSubscribedAsync( + Guid? tenantId, + Guid userId, + string notificationName, + CancellationToken cancellationToken = default) + { + return await _store.IsSubscribedAsync(tenantId, userId, notificationName, cancellationToken); + } + + public async virtual Task SubscribeAsync( + Guid? tenantId, + UserIdentifier identifier, + string notificationName, + CancellationToken cancellationToken = default) + { + if (await IsSubscribedAsync(tenantId, identifier.UserId, notificationName, cancellationToken)) + { + return; + } + await _store.InsertUserSubscriptionAsync(tenantId, identifier, notificationName, cancellationToken); + } + + public async virtual Task SubscribeAsync( + Guid? tenantId, + IEnumerable identifiers, + string notificationName, + CancellationToken cancellationToken = default) + { + foreach (var identifier in identifiers) + { + await SubscribeAsync(tenantId, identifier, notificationName, cancellationToken); + } + } + + public async virtual Task UnsubscribeAsync( + Guid? tenantId, + UserIdentifier identifier, + string notificationName, + CancellationToken cancellationToken = default) + { + await _store.DeleteUserSubscriptionAsync(tenantId, identifier.UserId, notificationName, cancellationToken); + } + + public async virtual Task UnsubscribeAllAsync( + Guid? tenantId, + string notificationName, + CancellationToken cancellationToken = default) + { + await _store.DeleteAllUserSubscriptionAsync(tenantId, notificationName, cancellationToken); + } + + public async virtual Task UnsubscribeAsync( + Guid? tenantId, + IEnumerable identifiers, + string notificationName, + CancellationToken cancellationToken = default) + { + await _store.DeleteUserSubscriptionAsync(tenantId, identifiers, notificationName, cancellationToken); + } + } +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationData.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationData.cs new file mode 100644 index 0000000..35a8599 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationData.cs @@ -0,0 +1,177 @@ +using Sanhe.Abp.RealTime.Localization; +using System; +using Volo.Abp.Data; +using Volo.Abp.EventBus; + +namespace Sanhe.Abp.Notifications; + +/// +/// 通知数据 +/// +/// +/// 把通知的标题和内容设计为 让客户端自行本地化 +/// +[Serializable] +[EventName("notifications")] +public class NotificationData : IHasExtraProperties +{ + /// + /// 用来标识是否需要本地化的信息 + /// + public const string LocalizerKey = "L"; + /// + /// 类型完全名称 + /// + public virtual string Type => GetType().FullName; + /// + /// 获取或设置扩展属性 + /// + /// + /// + public object this[string key] { + get { + return this.GetProperty(key); + } + set { + this.SetProperty(key, value); + } + } + /// + /// 扩展属性 + /// + public ExtraPropertyDictionary ExtraProperties { get; set; } + + public NotificationData() + { + ExtraProperties = new ExtraPropertyDictionary(); + this.SetDefaultsForExtraProperties(); + + TrySetData(LocalizerKey, false); + } + + /// + /// 写入本地化的消息数据 + /// + /// + /// + /// + /// + /// + /// + public NotificationData WriteLocalizedData( + LocalizableStringInfo title, + LocalizableStringInfo message, + DateTime createTime, + string formUser, + LocalizableStringInfo description = null) + { + TrySetData("title", title); + TrySetData("message", message); + TrySetData("formUser", formUser); + TrySetData("createTime", createTime); + TrySetData(LocalizerKey, true); + + if (description != null) + { + TrySetData("description", description); + } + + return this; + } + + /// + /// 写入标准数据 + /// + /// 标题 + /// 内容 + /// 创建时间 + /// 来源用户 + /// 附加说明 + /// + public NotificationData WriteStandardData(string title, string message, DateTime createTime, string formUser, string description = "") + { + TrySetData("title", title); + TrySetData("message", message); + TrySetData("description", description); + TrySetData("formUser", formUser); + TrySetData("createTime", createTime); + TrySetData(LocalizerKey, false); + return this; + } + + /// + /// 写入标准数据 + /// + /// 数据前缀 + /// 标识 + /// 数据内容 + /// + public NotificationData WriteStandardData(string prefix, string key, object value) + { + TrySetData(string.Concat(prefix, key), value); + TrySetData(LocalizerKey, false); + return this; + } + + /// + /// 转换为标准数据 + /// + /// 原始数据 + /// + public static NotificationData ToStandardData(NotificationData sourceData) + { + var data = new NotificationData(); + data.TrySetData("title", sourceData.TryGetData("title")); + data.TrySetData("message", sourceData.TryGetData("message")); + data.TrySetData("description", sourceData.TryGetData("description")); + data.TrySetData("formUser", sourceData.TryGetData("formUser")); + data.TrySetData("createTime", sourceData.TryGetData("createTime")); + data.TrySetData(LocalizerKey, sourceData.TryGetData(LocalizerKey)); + return data; + } + + /// + /// 转换为标准数据 + /// + /// 数据前缀 + /// 原始数据 + /// + public static NotificationData ToStandardData(string prefix, NotificationData sourceData) + { + var data = ToStandardData(sourceData); + + foreach (var property in sourceData.ExtraProperties) + { + if (property.Key.StartsWith(prefix)) + { + var key = property.Key.Replace(prefix, ""); + data.TrySetData(key, property.Value); + } + } + return data; + } + + public object TryGetData(string key) + { + return this.GetProperty(key); + } + + public void TrySetData(string key, object value) + { + this.SetProperty(key, value); + } + + /// + /// 需要本地化 + /// + /// + public bool NeedLocalizer() + { + var localizer = TryGetData(LocalizerKey); + if (localizer != null && localizer is bool needLocalizer) + { + return needLocalizer; + } + return false; + } +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDataConverter.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDataConverter.cs new file mode 100644 index 0000000..30b20e0 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDataConverter.cs @@ -0,0 +1,38 @@ +using Sanhe.Abp.RealTime.Localization; +using Newtonsoft.Json; + +namespace Sanhe.Abp.Notifications; + +public class NotificationDataConverter +{ + public static NotificationData Convert(NotificationData notificationData) + { + if (notificationData != null) + { + if (notificationData.NeedLocalizer()) + { + // 潜在的空对象引用修复 + if (notificationData.ExtraProperties.TryGetValue("title", out var title) && title != null) + { + var titleObj = JsonConvert.DeserializeObject(title.ToString()); + notificationData.TrySetData("title", titleObj); + } + if (notificationData.ExtraProperties.TryGetValue("message", out var message) && message != null) + { + var messageObj = JsonConvert.DeserializeObject(message.ToString()); + notificationData.TrySetData("message", messageObj); + } + + if (notificationData.ExtraProperties.TryGetValue("description", out var description) && description != null) + { + notificationData.TrySetData("description", JsonConvert.DeserializeObject(description.ToString())); + } + } + } + else + { + notificationData = new NotificationData(); + } + return notificationData; + } +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDataMappingDictionary.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDataMappingDictionary.cs new file mode 100644 index 0000000..62c9ddc --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDataMappingDictionary.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Sanhe.Abp.Notifications +{ + public class NotificationDataMappingDictionary : Dictionary> + { + public static string DefaultKey { get; set; } = "Default"; + + /// + /// 处理某个通知的数据 + /// 特定于一个提供程序 + /// + /// + /// + /// + public void Mapping(string provider, string name, Func func) + { + if (!ContainsKey(provider)) + { + this[provider] = new List(); + } + + var mapItem = this[provider].FirstOrDefault(item => item.Name.Equals(name)); + + if (mapItem == null) + { + this[provider].Add(new NotificationDataMappingDictionaryItem(name, func)); + } + else + { + mapItem.Replace(func); + } + } + + /// + /// 处理所有通知的数据 + /// 特定于一个提供程序 + /// + /// + /// + public void MappingDefault(string provider, Func func) + { + Mapping(provider, DefaultKey, func); + } + + /// + /// 获取需要处理数据的方法 + /// + /// + /// + /// + public NotificationDataMappingDictionaryItem GetMapItemOrDefault(string provider, string name) + { + if (ContainsKey(provider)) + { + return this[provider].GetOrNullDefault(name); + } + return null; + } + } +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDataMappingDictionaryItem.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDataMappingDictionaryItem.cs new file mode 100644 index 0000000..d3aae42 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDataMappingDictionaryItem.cs @@ -0,0 +1,30 @@ +using System; + +namespace Sanhe.Abp.Notifications; + +public class NotificationDataMappingDictionaryItem +{ + /// + /// 通知名称 + /// + public string Name { get; } + /// + /// 转换方法 + /// + public Func MappingFunc { get; private set; } + + public NotificationDataMappingDictionaryItem(string name, Func func) + { + Name = name; + MappingFunc = func; + } + + /// + /// 替换 + /// + /// + public void Replace(Func func) + { + MappingFunc = func; + } +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDataMappingDictionaryItemExtensions.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDataMappingDictionaryItemExtensions.cs new file mode 100644 index 0000000..91b4f08 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDataMappingDictionaryItemExtensions.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Sanhe.Abp.Notifications +{ + public static class NotificationDataMappingDictionaryItemExtensions + { + public static NotificationDataMappingDictionaryItem GetOrNullDefault( + this IEnumerable items, + string name) + { + var item = items.FirstOrDefault(i => i.Name.Equals(name)); + if (item == null) + { + return items.FirstOrDefault(i => i.Name.Equals(NotificationDataMappingDictionary.DefaultKey)); + } + return item; + } + } +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDefinition.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDefinition.cs new file mode 100644 index 0000000..3b6a4fa --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDefinition.cs @@ -0,0 +1,94 @@ +using JetBrains.Annotations; +using System; +using System.Collections.Generic; +using Volo.Abp; +using Volo.Abp.Localization; + +namespace Sanhe.Abp.Notifications; + +/// +/// 通知定义 +/// +public class NotificationDefinition +{ + /// + /// 通知名称 + /// + [NotNull] + public string Name { get; set; } + /// + /// 通知显示名称 + /// + [NotNull] + public ILocalizableString DisplayName { + get => _displayName; + set => _displayName = Check.NotNull(value, nameof(value)); + } + private ILocalizableString _displayName; + /// + /// 通知说明 + /// + [CanBeNull] + public ILocalizableString Description { get; set; } + /// + /// 允许客户端显示订阅 + /// + public bool AllowSubscriptionToClients { get; set; } + /// + /// 存活类型 + /// + public NotificationLifetime NotificationLifetime { get; set; } + /// + /// 通知类型 + /// + public NotificationType NotificationType { get; set; } + /// + /// 通知提供者 + /// + public List Providers { get; } + /// + /// 额外属性 + /// + [NotNull] + public Dictionary Properties { get; } + + public NotificationDefinition( + string name, + ILocalizableString displayName = null, + ILocalizableString description = null, + NotificationType notificationType = NotificationType.Application, + NotificationLifetime lifetime = NotificationLifetime.Persistent, + bool allowSubscriptionToClients = false) + { + Name = name; + DisplayName = displayName ?? new FixedLocalizableString(name); + Description = description; + NotificationLifetime = lifetime; + NotificationType = notificationType; + AllowSubscriptionToClients = allowSubscriptionToClients; + + Providers = new List(); + Properties = new Dictionary(); + } + + public virtual NotificationDefinition WithProviders(params string[] providers) + { + if (!providers.IsNullOrEmpty()) + { + Providers.AddRange(providers); + } + + return this; + } + + public virtual NotificationDefinition WithProperty(string key, object value) + { + Properties[key] = value; + return this; + } + + public override string ToString() + { + return $"[{nameof(NotificationDefinition)} {Name}]"; + } +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDefinitionContext.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDefinitionContext.cs new file mode 100644 index 0000000..d3b4c60 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDefinitionContext.cs @@ -0,0 +1,55 @@ +using JetBrains.Annotations; +using System.Collections.Generic; +using Volo.Abp; +using Volo.Abp.Localization; + +namespace Sanhe.Abp.Notifications; + +public class NotificationDefinitionContext : INotificationDefinitionContext +{ + internal Dictionary Groups { get; } + + public NotificationDefinitionContext() + { + Groups = new Dictionary(); + } + + public NotificationGroupDefinition AddGroup( + [NotNull] string name, + ILocalizableString displayName = null, + bool allowSubscriptionToClients = true) + { + Check.NotNull(name, nameof(name)); + + if (Groups.ContainsKey(name)) + { + throw new AbpException($"There is already an existing notification group with name: {name}"); + } + + return Groups[name] = new NotificationGroupDefinition(name, displayName, allowSubscriptionToClients); + } + + public NotificationGroupDefinition GetGroupOrNull(string name) + { + Check.NotNull(name, nameof(name)); + + if (!Groups.ContainsKey(name)) + { + return null; + } + + return Groups[name]; + } + + public void RemoveGroup(string name) + { + Check.NotNull(name, nameof(name)); + + if (!Groups.ContainsKey(name)) + { + throw new AbpException($"Undefined notification group: '{name}'."); + } + + Groups.Remove(name); + } +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDefinitionManager.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDefinitionManager.cs new file mode 100644 index 0000000..0f4f757 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDefinitionManager.cs @@ -0,0 +1,113 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Volo.Abp; +using Volo.Abp.DependencyInjection; + +namespace Sanhe.Abp.Notifications; + +/// +/// 通知定义管理者接口 +/// +public class NotificationDefinitionManager : INotificationDefinitionManager, ISingletonDependency +{ + protected AbpNotificationsOptions Options { get; } + + protected IDictionary NotificationGroupDefinitions => _lazyNotificationGroupDefinitions.Value; + private readonly Lazy> _lazyNotificationGroupDefinitions; + + protected IDictionary NotificationDefinitions => _lazyNotificationDefinitions.Value; + private readonly Lazy> _lazyNotificationDefinitions; + + private readonly IServiceScopeFactory _serviceScopeFactory; + + public NotificationDefinitionManager( + IOptions optionsAccessor, + IServiceScopeFactory serviceScopeFactory) + { + _serviceScopeFactory = serviceScopeFactory; + Options = optionsAccessor.Value; + + _lazyNotificationDefinitions = new Lazy>( + CreateNotificationDefinitions, + isThreadSafe: true + ); + + _lazyNotificationGroupDefinitions = new Lazy>( + CreateNotificationGroupDefinitions, + isThreadSafe: true + ); + } + + public virtual NotificationDefinition Get(string name) + { + Check.NotNull(name, nameof(name)); + + var feature = GetOrNull(name); + + if (feature == null) + { + throw new AbpException("Undefined notification: " + name); + } + + return feature; + } + + public virtual IReadOnlyList GetAll() + { + return NotificationDefinitions.Values.ToImmutableList(); + } + + public virtual NotificationDefinition GetOrNull(string name) + { + return NotificationDefinitions.GetOrDefault(name); + } + + public IReadOnlyList GetGroups() + { + return NotificationGroupDefinitions.Values.ToImmutableList(); + } + + protected virtual Dictionary CreateNotificationDefinitions() + { + var notifications = new Dictionary(); + + foreach (var groupDefinition in NotificationGroupDefinitions.Values) + { + foreach (var notification in groupDefinition.Notifications) + { + if (notifications.ContainsKey(notification.Name)) + { + throw new AbpException("Duplicate notification name: " + notification.Name); + } + + notifications[notification.Name] = notification; + } + } + + return notifications; + } + + protected virtual Dictionary CreateNotificationGroupDefinitions() + { + var context = new NotificationDefinitionContext(); + + using (var scope = _serviceScopeFactory.CreateScope()) + { + var providers = Options + .DefinitionProviders + .Select(p => scope.ServiceProvider.GetRequiredService(p) as INotificationDefinitionProvider) + .ToList(); + + foreach (var provider in providers) + { + provider.Define(context); + } + } + + return context.Groups; + } +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDefinitionProvider.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDefinitionProvider.cs new file mode 100644 index 0000000..a332060 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationDefinitionProvider.cs @@ -0,0 +1,15 @@ +using Volo.Abp.DependencyInjection; + +namespace Sanhe.Abp.Notifications; + +/// +/// 通知定义提供者抽象类 +/// +public abstract class NotificationDefinitionProvider : INotificationDefinitionProvider, ITransientDependency +{ + /// + /// 定义 + /// + /// + public abstract void Define(INotificationDefinitionContext context); +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationEto.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationEto.cs new file mode 100644 index 0000000..49a5da5 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationEto.cs @@ -0,0 +1,55 @@ +using Sanhe.Abp.RealTime; +using System; +using System.Collections.Generic; +using Volo.Abp.EventBus; +using Volo.Abp.MultiTenancy; + +namespace Sanhe.Abp.Notifications; + +/// +/// 通知事件传输对象 +/// +/// +[Serializable] +[GenericEventName(Prefix = "abp.realtime.")] +public class NotificationEto : RealTimeEto, IMultiTenant +{ + /// + /// 通知标识 + /// 自动计算 + /// + public long Id { get; set; } + /// + /// 租户 + /// + public Guid? TenantId { get; set; } + /// + /// 通知名称 + /// + public string Name { get; set; } + /// + /// 创建时间 + /// + public DateTime CreationTime { get; set; } + /// + /// 紧急级别 + /// + public NotificationSeverity Severity { get; set; } + /// + /// 指定的接收用户信息集合 + /// + /// + /// 注:
+ /// 如果指定了用户列表,应该在事件订阅程序中通过此集合过滤订阅用户
+ /// 如果未指定用户列表,应该在事件订阅程序中过滤所有订阅此通知的用户 + ///
+ public List Users { get; set; } = new List(); + + public NotificationEto() : base() + { + } + + public NotificationEto(T data) : base(data) + { + } +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationGroupDefinition.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationGroupDefinition.cs new file mode 100644 index 0000000..3ed5d88 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationGroupDefinition.cs @@ -0,0 +1,84 @@ +using JetBrains.Annotations; +using System.Collections.Generic; +using System.Collections.Immutable; +using Volo.Abp; +using Volo.Abp.Localization; + +namespace Sanhe.Abp.Notifications; + +/// +/// 通知组定义 +/// +public class NotificationGroupDefinition +{ + /// + /// 通知组名称 + /// + [NotNull] + public string Name { get; set; } + /// + /// 通知组显示名称 + /// + [NotNull] + public ILocalizableString DisplayName { + get => _displayName; + set => _displayName = Check.NotNull(value, nameof(value)); + } + private ILocalizableString _displayName; + /// + /// 通知组说明 + /// + [CanBeNull] + public ILocalizableString Description { get; set; } + /// + /// 允许客户端显示订阅 + /// + public bool AllowSubscriptionToClients { get; set; } + + public IReadOnlyList Notifications => _notifications.ToImmutableList(); + private readonly List _notifications; + + protected internal NotificationGroupDefinition( + string name, + ILocalizableString displayName = null, + bool allowSubscriptionToClients = false) + { + Name = name; + DisplayName = displayName ?? new FixedLocalizableString(Name); + AllowSubscriptionToClients = allowSubscriptionToClients; + + _notifications = new List(); + } + + /// + /// 添加通知 + /// + /// 通知组名称 + /// 通知组显示名称 + /// 通知组说明 + /// 通知类型 + /// 通知存活时间 + /// 允许客户端显示订阅 + /// + public virtual NotificationDefinition AddNotification( + string name, + ILocalizableString displayName = null, + ILocalizableString description = null, + NotificationType notificationType = NotificationType.Application, + NotificationLifetime lifetime = NotificationLifetime.Persistent, + bool allowSubscriptionToClients = false) + { + var notification = new NotificationDefinition( + name, + displayName, + description, + notificationType, + lifetime, + allowSubscriptionToClients + ); + + _notifications.Add(notification); + + return notification; + } +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationInfo.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationInfo.cs new file mode 100644 index 0000000..3ddf315 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationInfo.cs @@ -0,0 +1,73 @@ +using System; + +namespace Sanhe.Abp.Notifications; + +/// +/// 通知信息 +/// +public class NotificationInfo +{ + /// + /// 租户Id + /// + public Guid? TenantId { get; set; } + /// + /// 名称 + /// + public string Name { get; set; } + /// + /// Id + /// + public string Id { get; set; } + /// + /// 通知数据 + /// + public NotificationData Data { get; set; } + /// + /// 创建时间 + /// + public DateTime CreationTime { get; set; } + /// + /// 通知存活时间 + /// + public NotificationLifetime Lifetime { get; set; } + /// + /// 通知类型 + /// + public NotificationType Type { get; set; } + /// + /// 通知严重级别 + /// + public NotificationSeverity Severity { get; set; } + + public NotificationInfo() + { + Data = new NotificationData(); + Lifetime = NotificationLifetime.Persistent; + Type = NotificationType.Application; + Severity = NotificationSeverity.Info; + + CreationTime = DateTime.Now; + } + + /// + /// 设置Id + /// + /// + public void SetId(long id) + { + if (Id.IsNullOrWhiteSpace()) + { + Id = id.ToString(); + } + } + + /// + /// 获取Id + /// + /// + public long GetId() + { + return long.Parse(Id); + } +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationLifetime.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationLifetime.cs new file mode 100644 index 0000000..d61ec80 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationLifetime.cs @@ -0,0 +1,17 @@ +namespace Sanhe.Abp.Notifications; + +/// +/// 通知存活时间 +/// 发送之后取消用户订阅,类似于微信小程序 +/// +public enum NotificationLifetime +{ + /// + /// 持久化 + /// + Persistent = 0, + /// + /// 一次性 + /// + OnlyOne = 1 +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationProviderNames.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationProviderNames.cs new file mode 100644 index 0000000..09d0f73 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationProviderNames.cs @@ -0,0 +1,24 @@ +namespace Sanhe.Abp.Notifications; + +/// +/// 内置通知提供者 +/// +public static class NotificationProviderNames +{ + /// + /// SignalR 实时通知 + /// + public const string SignalR = "SignalR"; + /// + /// 短信通知 + /// + public const string Sms = "Sms"; + /// + /// 邮件通知 + /// + public const string Emailing = "Emailing"; + /// + /// 微信小程序模板通知 + /// + public const string WechatMiniProgram = "WeChat.MiniProgram"; +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationPublishProvider.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationPublishProvider.cs new file mode 100644 index 0000000..5465dfb --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationPublishProvider.cs @@ -0,0 +1,68 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Threading; + +namespace Sanhe.Abp.Notifications; + +/// +/// 通知发布提供者抽象类 +/// +public abstract class NotificationPublishProvider : INotificationPublishProvider, ITransientDependency +{ + public abstract string Name { get; } + + protected IServiceProvider ServiceProvider { get; } + + protected readonly object ServiceProviderLock = new(); + + public ILoggerFactory LoggerFactory => LazyGetRequiredService(ref _loggerFactory); + private ILoggerFactory _loggerFactory; + + protected ILogger Logger => LazyLogger.Value; + private Lazy LazyLogger => new(() => LoggerFactory?.CreateLogger(GetType().FullName) ?? NullLogger.Instance, true); + + + protected TService LazyGetRequiredService(ref TService reference) + { + if (reference == null) + { + lock (ServiceProviderLock) + { + if (reference == null) + { + reference = ServiceProvider.GetRequiredService(); + } + } + } + + return reference; + } + + public ICancellationTokenProvider CancellationTokenProvider { get; set; } + + protected NotificationPublishProvider(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + CancellationTokenProvider = NullCancellationTokenProvider.Instance; + } + + public async Task PublishAsync(NotificationInfo notification, IEnumerable identifiers) + { + await PublishAsync(notification, identifiers, CancellationTokenProvider.Token); + } + + /// + /// 重写实现通知发布 + /// + /// + /// + /// + /// + protected abstract Task PublishAsync(NotificationInfo notification, IEnumerable identifiers, CancellationToken cancellationToken = default); +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationPublishProviderManager.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationPublishProviderManager.cs new file mode 100644 index 0000000..8ef2266 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationPublishProviderManager.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using Volo.Abp.DependencyInjection; + +namespace Sanhe.Abp.Notifications; + +public class NotificationPublishProviderManager : INotificationPublishProviderManager, ISingletonDependency +{ + public List Providers => _lazyProviders.Value; + + protected AbpNotificationsOptions Options { get; } + + private readonly Lazy> _lazyProviders; + + public NotificationPublishProviderManager( + IServiceProvider serviceProvider, + IOptions optionsAccessor) + { + Options = optionsAccessor.Value; + + _lazyProviders = new Lazy>( + () => Options + .PublishProviders + .Select(type => serviceProvider.GetRequiredService(type) as INotificationPublishProvider) + .ToList(), + true + ); + } +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationReadState.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationReadState.cs new file mode 100644 index 0000000..ab0528a --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationReadState.cs @@ -0,0 +1,16 @@ +namespace Sanhe.Abp.Notifications; + +/// +/// 读取状态 +/// +public enum NotificationReadState +{ + /// + /// 已读 + /// + Read = 0, + /// + /// 未读 + /// + UnRead = 1 +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationSeverity.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationSeverity.cs new file mode 100644 index 0000000..2ded260 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationSeverity.cs @@ -0,0 +1,28 @@ +namespace Sanhe.Abp.Notifications; + +/// +/// 严重级别 +/// +public enum NotificationSeverity : sbyte +{ + /// + /// 成功 + /// + Success = 0, + /// + /// 信息 + /// + Info = 10, + /// + /// 警告 + /// + Warn = 20, + /// + /// 错误 + /// + Error = 30, + /// + /// 致命错误 + /// + Fatal = 40 +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationSubscriptionInfo.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationSubscriptionInfo.cs new file mode 100644 index 0000000..774adf1 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationSubscriptionInfo.cs @@ -0,0 +1,26 @@ +using System; + +namespace Sanhe.Abp.Notifications; + +/// +/// 通知订阅信息 +/// +public class NotificationSubscriptionInfo +{ + /// + /// 租户Id + /// + public Guid? TenantId { get; set; } + /// + /// 用户Id + /// + public Guid UserId { get; set; } + /// + /// 用户名 + /// + public string UserName { get; set; } + /// + /// 通知名 + /// + public string NotificationName { get; set; } +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationType.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationType.cs new file mode 100644 index 0000000..63c3de2 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NotificationType.cs @@ -0,0 +1,20 @@ +namespace Sanhe.Abp.Notifications; + +/// +/// 通知类型 +/// +public enum NotificationType +{ + /// + /// 应用(对应租户) + /// + Application = 0, + /// + /// 系统(对应宿主) + /// + System = 10, + /// + /// 用户(对应用户) + /// + User = 20 +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NullNotificationStore.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NullNotificationStore.cs new file mode 100644 index 0000000..bfa8ef5 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/NullNotificationStore.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +namespace Sanhe.Abp.Notifications; + +/// +/// 空实现通知相关储存处 +/// +[Dependency(TryRegister = true)] +public class NullNotificationStore : INotificationStore, ISingletonDependency +{ + public Task ChangeUserNotificationReadStateAsync( + Guid? tenantId, + Guid userId, + long notificationId, + NotificationReadState readState, + CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public Task DeleteAllUserSubscriptionAsync( + Guid? tenantId, + string notificationName, + CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public Task DeleteNotificationAsync( + NotificationInfo notification, + CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public Task DeleteNotificationAsync( + int batchCount, + CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public Task DeleteUserNotificationAsync( + Guid? tenantId, + Guid userId, + long notificationId, + CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public Task DeleteUserSubscriptionAsync( + Guid? tenantId, + Guid userId, + string notificationName, + CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public Task DeleteUserSubscriptionAsync( + Guid? tenantId, + IEnumerable identifiers, + string notificationName, + CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public Task GetNotificationOrNullAsync( + Guid? tenantId, + long notificationId, + CancellationToken cancellationToken = default) + { + return Task.FromResult(new NotificationInfo()); + } + + public Task> GetUserSubscriptionsAsync( + Guid? tenantId, + string notificationName, + IEnumerable identifiers, + CancellationToken cancellationToken = default) + { + return Task.FromResult(new List()); + } + + public Task> GetUserNotificationsAsync( + Guid? tenantId, + Guid userId, + NotificationReadState? readState = null, + int maxResultCount = 10, + CancellationToken cancellationToken = default) + { + return Task.FromResult(new List()); + } + + public Task GetUserNotificationsCountAsync( + Guid? tenantId, + Guid userId, + string filter = "", + NotificationReadState? readState = null, + CancellationToken cancellationToken = default) + { + return Task.FromResult(0); + } + + public Task> GetUserNotificationsAsync( + Guid? tenantId, + Guid userId, + string filter = "", + string sorting = nameof(NotificationInfo.CreationTime), + NotificationReadState? readState = null, + int skipCount = 1, + int maxResultCount = 10, + CancellationToken cancellationToken = default) + { + return Task.FromResult(new List()); + } + + public Task> GetUserSubscriptionsAsync( + Guid? tenantId, + Guid userId, + CancellationToken cancellationToken = default) + { + return Task.FromResult(new List()); + } + + public Task> GetUserSubscriptionsAsync( + Guid? tenantId, + string userName, + CancellationToken cancellationToken = default) + { + return Task.FromResult(new List()); + } + + public Task InsertNotificationAsync( + NotificationInfo notification, + CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public Task InsertUserNotificationAsync( + NotificationInfo notification, + Guid userId, + CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public Task InsertUserNotificationsAsync( + NotificationInfo notification, + IEnumerable userIds, + CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public Task InsertUserSubscriptionAsync( + Guid? tenantId, + UserIdentifier identifier, + string notificationName, + CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public Task InsertUserSubscriptionAsync( + Guid? tenantId, + IEnumerable identifiers, + string notificationName, + CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public Task IsSubscribedAsync( + Guid? tenantId, + Guid userId, + string notificationName, + CancellationToken cancellationToken = default) + { + return Task.FromResult(false); + } +} diff --git a/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/UserIdentifier.cs b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/UserIdentifier.cs new file mode 100644 index 0000000..7feefa1 --- /dev/null +++ b/modules/common/Sanhe.Abp.Notifications/Sanhe/Abp/Notifications/UserIdentifier.cs @@ -0,0 +1,24 @@ +using System; + +namespace Sanhe.Abp.Notifications; + +/// +/// 用户信息 +/// +public class UserIdentifier +{ + /// + /// 用户标识 + /// + public Guid UserId { get; set; } + /// + /// 用户名 + /// + public string UserName { get; set; } + + public UserIdentifier(Guid userId, string userName) + { + UserId = userId; + UserName = userName; + } +} diff --git a/modules/common/Sanhe.Abp.RealTime/Sanhe.Abp.RealTime.csproj b/modules/common/Sanhe.Abp.RealTime/Sanhe.Abp.RealTime.csproj index b24d631..7b9bc56 100644 --- a/modules/common/Sanhe.Abp.RealTime/Sanhe.Abp.RealTime.csproj +++ b/modules/common/Sanhe.Abp.RealTime/Sanhe.Abp.RealTime.csproj @@ -1,10 +1,11 @@ - + netstandard2.0 + diff --git a/modules/common/Sanhe.Abp.RealTime/Sanhe/Abp/RealTime/AbpRealTimeModule.cs b/modules/common/Sanhe.Abp.RealTime/Sanhe/Abp/RealTime/AbpRealTimeModule.cs index c6ee508..6da8486 100644 --- a/modules/common/Sanhe.Abp.RealTime/Sanhe/Abp/RealTime/AbpRealTimeModule.cs +++ b/modules/common/Sanhe.Abp.RealTime/Sanhe/Abp/RealTime/AbpRealTimeModule.cs @@ -1,7 +1,7 @@ using Volo.Abp.EventBus.Abstractions; using Volo.Abp.Modularity; -namespace Sanhe.Abp.RealTime.Sanhe.Abp.RealTime; +namespace Sanhe.Abp.RealTime; [DependsOn(typeof(AbpEventBusAbstractionsModule))] public class AbpRealTimeModule : AbpModule diff --git a/modules/common/Sanhe.Abp.RealTime/Sanhe/Abp/RealTime/RealTimeEto.cs b/modules/common/Sanhe.Abp.RealTime/Sanhe/Abp/RealTime/RealTimeEto.cs index 0608388..64ead41 100644 --- a/modules/common/Sanhe.Abp.RealTime/Sanhe/Abp/RealTime/RealTimeEto.cs +++ b/modules/common/Sanhe.Abp.RealTime/Sanhe/Abp/RealTime/RealTimeEto.cs @@ -2,7 +2,7 @@ using Volo.Abp.Domain.Entities.Events.Distributed; using Volo.Abp.EventBus; -namespace Sanhe.Abp.RealTime.Sanhe.Abp.RealTime; +namespace Sanhe.Abp.RealTime; [Serializable] [GenericEventName(Prefix = "abp.realtime.")]