diff --git a/aspnet-core/LINGYUN.MicroService.SingleProject.sln b/aspnet-core/LINGYUN.MicroService.SingleProject.sln index ecbc892c7..0ce842067 100644 --- a/aspnet-core/LINGYUN.MicroService.SingleProject.sln +++ b/aspnet-core/LINGYUN.MicroService.SingleProject.sln @@ -537,6 +537,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.WeChat.Official EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.WeChat.Work.Handlers", "framework\wechat\LINGYUN.Abp.WeChat.Work.Handlers\LINGYUN.Abp.WeChat.Work.Handlers.csproj", "{DB80C55F-8B70-4840-942A-ED021ED88BD6}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.MultiTenancy.Saas", "modules\saas\LINGYUN.Abp.MultiTenancy.Saas\LINGYUN.Abp.MultiTenancy.Saas.csproj", "{E3C07A77-EAF9-4A3F-8814-7D2F116C8E26}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1415,6 +1417,10 @@ Global {DB80C55F-8B70-4840-942A-ED021ED88BD6}.Debug|Any CPU.Build.0 = Debug|Any CPU {DB80C55F-8B70-4840-942A-ED021ED88BD6}.Release|Any CPU.ActiveCfg = Release|Any CPU {DB80C55F-8B70-4840-942A-ED021ED88BD6}.Release|Any CPU.Build.0 = Release|Any CPU + {E3C07A77-EAF9-4A3F-8814-7D2F116C8E26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E3C07A77-EAF9-4A3F-8814-7D2F116C8E26}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E3C07A77-EAF9-4A3F-8814-7D2F116C8E26}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E3C07A77-EAF9-4A3F-8814-7D2F116C8E26}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1677,6 +1683,7 @@ Global {4634B421-36E6-4169-AA1A-11050902495F} = {D94D6AFE-20BD-4F21-8708-03F5E34F49FC} {BB2DF96A-6ED8-4F47-948C-230EA2065C4C} = {91867618-0D86-4410-91C6-B1166A9ACDF9} {DB80C55F-8B70-4840-942A-ED021ED88BD6} = {91867618-0D86-4410-91C6-B1166A9ACDF9} + {E3C07A77-EAF9-4A3F-8814-7D2F116C8E26} = {ECE6E6D7-A4F6-4F50-BC21-AE2EB14A3129} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {711A43C0-A2F8-4E5C-9B9F-F2551E4B3FF1} diff --git a/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore/SingleDbMigrationEventHandler.cs b/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore/SingleDbMigrationEventHandler.cs index cccd2122b..7fa971af3 100644 --- a/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore/SingleDbMigrationEventHandler.cs +++ b/aspnet-core/migrations/LY.MicroService.Applications.Single.EntityFrameworkCore/SingleDbMigrationEventHandler.cs @@ -3,6 +3,7 @@ using LINGYUN.Abp.Saas.Tenants; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -43,13 +44,19 @@ public SingleDbMigrationEventHandler( IGuidGenerator guidGenerator, IdentityUserManager identityUserManager, IdentityRoleManager identityRoleManager, - IPermissionDataSeeder permissionDataSeeder) + IPermissionDataSeeder permissionDataSeeder, + IJobStore jobStore, + IJobScheduler jobScheduler, + IOptions options) : base("SingleDbMigrator", currentTenant, unitOfWorkManager, tenantStore, abpDistributedLock, distributedEventBus, loggerFactory) { GuidGenerator = guidGenerator; IdentityUserManager = identityUserManager; IdentityRoleManager = identityRoleManager; PermissionDataSeeder = permissionDataSeeder; + JobStore = jobStore; + JobScheduler = jobScheduler; + Options = options.Value; } public async virtual Task HandleEventAsync(EntityDeletedEto eventData) { diff --git a/aspnet-core/modules/realtime-notifications/LINGYUN.Abp.Notifications.Application/LINGYUN/Abp/Notifications/AbpNotificationsApplicationAutoMapperProfile.cs b/aspnet-core/modules/realtime-notifications/LINGYUN.Abp.Notifications.Application/LINGYUN/Abp/Notifications/AbpNotificationsApplicationAutoMapperProfile.cs index 479671877..4c41a76c0 100644 --- a/aspnet-core/modules/realtime-notifications/LINGYUN.Abp.Notifications.Application/LINGYUN/Abp/Notifications/AbpNotificationsApplicationAutoMapperProfile.cs +++ b/aspnet-core/modules/realtime-notifications/LINGYUN.Abp.Notifications.Application/LINGYUN/Abp/Notifications/AbpNotificationsApplicationAutoMapperProfile.cs @@ -8,7 +8,7 @@ public class AbpNotificationsApplicationAutoMapperProfile : Profile public AbpNotificationsApplicationAutoMapperProfile() { CreateMap() - .ForMember(dto => dto.Id, map => map.MapFrom(src => src.Id.ToString())) + .ForMember(dto => dto.Id, map => map.MapFrom(src => src.NotificationId.ToString())) .ForMember(dto => dto.Lifetime, map => map.Ignore()) .ForMember(dto => dto.Data, map => map.MapFrom((src, nfi) => { diff --git a/aspnet-core/modules/realtime-notifications/LINGYUN.Abp.Notifications.Domain/LINGYUN/Abp/Notifications/AbpNotificationsDomainAutoMapperProfile.cs b/aspnet-core/modules/realtime-notifications/LINGYUN.Abp.Notifications.Domain/LINGYUN/Abp/Notifications/AbpNotificationsDomainAutoMapperProfile.cs index fedff66b7..21a144526 100644 --- a/aspnet-core/modules/realtime-notifications/LINGYUN.Abp.Notifications.Domain/LINGYUN/Abp/Notifications/AbpNotificationsDomainAutoMapperProfile.cs +++ b/aspnet-core/modules/realtime-notifications/LINGYUN.Abp.Notifications.Domain/LINGYUN/Abp/Notifications/AbpNotificationsDomainAutoMapperProfile.cs @@ -28,7 +28,7 @@ public AbpNotificationsDomainAutoMapperProfile() })); CreateMap() - .ForMember(dto => dto.Id, map => map.MapFrom(src => src.Id.ToString())) + .ForMember(dto => dto.Id, map => map.MapFrom(src => src.NotificationId.ToString())) .ForMember(dto => dto.Name, map => map.MapFrom(src => src.Name)) .ForMember(dto => dto.Lifetime, map => map.Ignore()) .ForMember(dto => dto.Type, map => map.MapFrom(src => src.Type)) diff --git a/aspnet-core/modules/realtime-notifications/LINGYUN.Abp.Notifications.Domain/LINGYUN/Abp/Notifications/UserNotificationInfo.cs b/aspnet-core/modules/realtime-notifications/LINGYUN.Abp.Notifications.Domain/LINGYUN/Abp/Notifications/UserNotificationInfo.cs index 98e042315..b62312895 100644 --- a/aspnet-core/modules/realtime-notifications/LINGYUN.Abp.Notifications.Domain/LINGYUN/Abp/Notifications/UserNotificationInfo.cs +++ b/aspnet-core/modules/realtime-notifications/LINGYUN.Abp.Notifications.Domain/LINGYUN/Abp/Notifications/UserNotificationInfo.cs @@ -8,6 +8,7 @@ public class UserNotificationInfo public Guid? TenantId { get; set; } public string Name { get; set; } public long Id { get; set; } + public long NotificationId { get; set; } public ExtraPropertyDictionary ExtraProperties { get; set; } public string NotificationTypeName { get; set; } public DateTime CreationTime { get; set; } diff --git a/aspnet-core/modules/realtime-notifications/LINGYUN.Abp.Notifications.EntityFrameworkCore/LINGYUN/Abp/Notifications/EntityFrameworkCore/EfCoreUserNotificationRepository.cs b/aspnet-core/modules/realtime-notifications/LINGYUN.Abp.Notifications.EntityFrameworkCore/LINGYUN/Abp/Notifications/EntityFrameworkCore/EfCoreUserNotificationRepository.cs index 8e9c8a85b..345053ead 100644 --- a/aspnet-core/modules/realtime-notifications/LINGYUN.Abp.Notifications.EntityFrameworkCore/LINGYUN/Abp/Notifications/EntityFrameworkCore/EfCoreUserNotificationRepository.cs +++ b/aspnet-core/modules/realtime-notifications/LINGYUN.Abp.Notifications.EntityFrameworkCore/LINGYUN/Abp/Notifications/EntityFrameworkCore/EfCoreUserNotificationRepository.cs @@ -47,7 +47,8 @@ on un.NotificationId equals n.NotificationId where n.NotificationId.Equals(notificationId) select new UserNotificationInfo { - Id = n.NotificationId, + Id = un.Id, + NotificationId = n.NotificationId, TenantId = n.TenantId, Name = n.NotificationName, ExtraProperties = n.ExtraProperties, @@ -89,7 +90,7 @@ join n in dbContext.Set() on un.NotificationId equals n.NotificationId select new UserNotificationInfo { - Id = n.NotificationId, + NotificationId = n.NotificationId, TenantId = n.TenantId, Name = n.NotificationName, ExtraProperties = n.ExtraProperties, @@ -121,7 +122,7 @@ on un.NotificationId equals n.NotificationId where un.UserId == userId select new UserNotificationInfo { - Id = n.NotificationId, + NotificationId = n.NotificationId, TenantId = n.TenantId, Name = n.NotificationName, ExtraProperties = n.ExtraProperties, @@ -170,7 +171,7 @@ on un.NotificationId equals n.NotificationId where un.UserId == userId select new UserNotificationInfo { - Id = n.NotificationId, + NotificationId = n.NotificationId, TenantId = n.TenantId, Name = n.NotificationName, ExtraProperties = n.ExtraProperties, diff --git a/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Application/LINGYUN/Abp/Saas/Tenants/TenantAppService.cs b/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Application/LINGYUN/Abp/Saas/Tenants/TenantAppService.cs index ffbbec1e3..680622be9 100644 --- a/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Application/LINGYUN/Abp/Saas/Tenants/TenantAppService.cs +++ b/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Application/LINGYUN/Abp/Saas/Tenants/TenantAppService.cs @@ -1,5 +1,6 @@ using LINGYUN.Abp.Saas.Features; using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; @@ -8,7 +9,6 @@ using Volo.Abp.Application.Dtos; using Volo.Abp.Data; using Volo.Abp.EventBus.Distributed; -using Volo.Abp.Features; using Volo.Abp.MultiTenancy; using Volo.Abp.ObjectExtending; @@ -20,15 +20,18 @@ public class TenantAppService : AbpSaasAppServiceBase, ITenantAppService protected IDistributedEventBus EventBus { get; } protected ITenantRepository TenantRepository { get; } protected ITenantManager TenantManager { get; } + protected IConnectionStringChecker ConnectionStringChecker { get; } public TenantAppService( ITenantRepository tenantRepository, ITenantManager tenantManager, - IDistributedEventBus eventBus) + IDistributedEventBus eventBus, + IConnectionStringChecker connectionStringChecker) { EventBus = eventBus; TenantRepository = tenantRepository; TenantManager = tenantManager; + ConnectionStringChecker = connectionStringChecker; } public async virtual Task GetAsync(Guid id) @@ -80,6 +83,7 @@ public async virtual Task CreateAsync(TenantCreateDto input) if (!input.UseSharedDatabase && !input.DefaultConnectionString.IsNullOrWhiteSpace()) { + await CheckConnectionString(input.DefaultConnectionString); tenant.SetDefaultConnectionString(input.DefaultConnectionString); } @@ -87,6 +91,7 @@ public async virtual Task CreateAsync(TenantCreateDto input) { foreach (var connectionString in input.ConnectionStrings) { + await CheckConnectionString(connectionString.Value, connectionString.Key); tenant.SetConnectionString(connectionString.Key, connectionString.Value); } } @@ -119,6 +124,7 @@ public async virtual Task CreateAsync(TenantCreateDto input) await EventBus.PublishAsync(eto); }); + await CurrentUnitOfWork.SaveChangesAsync(); return ObjectMapper.Map(tenant); @@ -157,12 +163,17 @@ public async virtual Task DeleteAsync(Guid id) } // 租户删除时查询会失效, 在删除前确认 - var strategy = await FeatureChecker.GetAsync(SaasFeatureNames.Tenant.RecycleStrategy, RecycleStrategy.Recycle); + var recycleStrategy = RecycleStrategy.Recycle; + var strategySet = await FeatureChecker.GetOrNullAsync(SaasFeatureNames.Tenant.RecycleStrategy); + if (!strategySet.IsNullOrWhiteSpace() && Enum.TryParse(strategySet, out var strategy)) + { + recycleStrategy = strategy; + } var eto = new TenantDeletedEto { Id = tenant.Id, Name = tenant.Name, - Strategy = strategy, + Strategy = recycleStrategy, EntityVersion = tenant.EntityVersion, DefaultConnectionString = tenant.FindDefaultConnectionString(), }; @@ -259,4 +270,34 @@ public async virtual Task DeleteConnectionStringAsync(Guid id, string name) await CurrentUnitOfWork.SaveChangesAsync(); } + + protected async virtual Task CheckConnectionString(string connectionString, string name = null) + { + try + { + var checkResult = await ConnectionStringChecker.CheckAsync(connectionString); + // 检查连接是否可用 + if (!checkResult.Connected) + { + throw name.IsNullOrWhiteSpace() + ? new BusinessException(AbpSaasErrorCodes.InvalidDefaultConnectionString) + : new BusinessException(AbpSaasErrorCodes.InvalidConnectionString) + .WithData("Name", name); + } + // 默认连接字符串改变不能影响到现有数据库 + if (checkResult.DatabaseExists && name.IsNullOrWhiteSpace()) + { + throw new BusinessException(AbpSaasErrorCodes.DefaultConnectionStringDatabaseExists); + } + } + catch (Exception e) + { + Logger.LogWarning("An error occurred while checking the validity of the connection string"); + Logger.LogWarning(e.Message); + throw name.IsNullOrWhiteSpace() + ? new BusinessException(AbpSaasErrorCodes.InvalidDefaultConnectionString) + : new BusinessException(AbpSaasErrorCodes.InvalidConnectionString) + .WithData("Name", name); + } + } } diff --git a/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/AbpSaasErrorCodes.cs b/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/AbpSaasErrorCodes.cs index ec33cb29b..851e4642d 100644 --- a/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/AbpSaasErrorCodes.cs +++ b/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/AbpSaasErrorCodes.cs @@ -7,4 +7,16 @@ public static class AbpSaasErrorCodes public const string DuplicateEditionDisplayName = Namespace + ":010001"; public const string DeleteUsedEdition = Namespace + ":010002"; public const string DuplicateTenantName = Namespace + ":020001"; + /// + /// 无效的默认连接字符串 + /// + public const string InvalidDefaultConnectionString = Namespace + ":020101"; + /// + /// 默认连接字符串指向的数据库已经存在 + /// + public const string DefaultConnectionStringDatabaseExists = Namespace + ":020102"; + /// + /// {Name} 的连接字符串无效 + /// + public const string InvalidConnectionString = Namespace + ":020103"; } diff --git a/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/Localization/Resources/en.json b/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/Localization/Resources/en.json index 4054a2cb5..1f6f1d75c 100644 --- a/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/Localization/Resources/en.json +++ b/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/Localization/Resources/en.json @@ -4,7 +4,9 @@ "Saas:010001": "Unable to create duplicate editions {DisplayName}!", "Saas:010002": "Tried to delete the edition in use: {DisplayName}!", "Saas:020001": "Unable to create duplicate tenants {Name}!", - "Saas:020002": "The database string that cannot be connected!", + "Saas:020101": "The default connection string cannot open a connection to the database!", + "Saas:020102": "The database pointed to by the default connection string already exists!", + "Saas:020103": "Unable to open the database connection pointed to by {Name}!", "Volo.AbpIo.MultiTenancy:010001": "The tenant is unavailable or restricted!", "Volo.AbpIo.MultiTenancy:010002": "Tenant unavailable!", "Menu:Saas": "Saas", diff --git a/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/Localization/Resources/zh-Hans.json b/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/Localization/Resources/zh-Hans.json index cce383bb1..4f974c1ef 100644 --- a/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/Localization/Resources/zh-Hans.json +++ b/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain.Shared/LINGYUN/Abp/Saas/Localization/Resources/zh-Hans.json @@ -4,7 +4,9 @@ "Saas:010001": "已经存在名为 {DisplayName} 的版本!", "Saas:010002": "试图删除正在使用的版本: {DisplayName}!", "Saas:020001": "已经存在名为 {Name} 的租户!", - "Saas:020002": "无法连接的数据库字符串!", + "Saas:020101": "无法打开默认连接字符串指向的数据库连接!", + "Saas:020102": "默认连接字符串指向的数据库已经存在!", + "Saas:020103": "无法打开 {Name} 指向的数据库连接!", "Volo.AbpIo.MultiTenancy:010001": "租户不可用或受限制!", "Volo.AbpIo.MultiTenancy:010002": "租户不可用!", "Menu:Saas": "Saas", diff --git a/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain/LINGYUN/Abp/Saas/Features/SaasFeatureDefinitionProvider.cs b/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain/LINGYUN/Abp/Saas/Features/SaasFeatureDefinitionProvider.cs index 62f036f69..4e3a508d4 100644 --- a/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain/LINGYUN/Abp/Saas/Features/SaasFeatureDefinitionProvider.cs +++ b/aspnet-core/modules/saas/LINGYUN.Abp.Saas.Domain/LINGYUN/Abp/Saas/Features/SaasFeatureDefinitionProvider.cs @@ -18,22 +18,22 @@ public override void Define(IFeatureDefinitionContext context) ItemSource = new StaticSelectionStringValueItemSource( new LocalizableSelectionStringValueItem { - Value = RecycleStrategy.Reserve.ToString(), + Value = "0", DisplayText = new LocalizableStringInfo( - LocalizationResourceNameAttribute.GetName(typeof(AbpSaasResource)), - "RecycleStrategy:Reserve") + LocalizationResourceNameAttribute.GetName(typeof(AbpSaasResource)), + "RecycleStrategy:Reserve") }, new LocalizableSelectionStringValueItem { - Value = RecycleStrategy.Recycle.ToString(), + Value = "1", DisplayText = new LocalizableStringInfo( - LocalizationResourceNameAttribute.GetName(typeof(AbpSaasResource)), - "RecycleStrategy:Recycle") + LocalizationResourceNameAttribute.GetName(typeof(AbpSaasResource)), + "RecycleStrategy:Recycle") }) }; saas.AddFeature( name: SaasFeatureNames.Tenant.RecycleStrategy, - defaultValue: RecycleStrategy.Recycle.ToString(), + defaultValue: "1", displayName: L("Features:RecycleStrategy"), description: L("Features:RecycleStrategyDesc"), valueType: selectionValueType,