From f9c3345e6525be3fa2a35ef7be6f5c716a201055 Mon Sep 17 00:00:00 2001 From: feijie Date: Fri, 21 Mar 2025 19:12:32 +0800 Subject: [PATCH 01/10] =?UTF-8?q?=E2=9C=A8=20feat(common.props):=20?= =?UTF-8?q?=E5=90=AF=E7=94=A8=20XML=20=E6=96=87=E6=A1=A3=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=B9=B6=E6=B7=BB=E5=8A=A0=20NU1900=20=E8=AD=A6=E5=91=8A?= =?UTF-8?q?=E6=8A=91=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aspnet-core/templates/aio/content/common.props | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/aspnet-core/templates/aio/content/common.props b/aspnet-core/templates/aio/content/common.props index d7d7622cf..877d09535 100644 --- a/aspnet-core/templates/aio/content/common.props +++ b/aspnet-core/templates/aio/content/common.props @@ -3,13 +3,15 @@ latest 8.2.1 colin - $(NoWarn);CS1591;CS0436;CS8618;NU1803 + $(NoWarn);CS1591;CS0436;CS8618;NU1803;NU1900 https://github.com/colinin/abp-next-admin - $(SolutionDir)LocalNuget 8.2.1 MIT git https://github.com/colinin/abp-next-admin + + true + $(NoWarn);1591 true @@ -30,9 +32,4 @@ - - - $(SolutionDir)LocalNuget - - \ No newline at end of file From 60e90a5c3dc841f029728f998c82d8e6c5b2b228 Mon Sep 17 00:00:00 2001 From: feijie Date: Fri, 21 Mar 2025 19:13:44 +0800 Subject: [PATCH 02/10] =?UTF-8?q?=E2=9C=A8=20feat(Directory.Packages.props?= =?UTF-8?q?):=20=E6=9B=B4=E6=96=B0=E4=BE=9D=E8=B5=96=E5=8C=85=E7=89=88?= =?UTF-8?q?=E6=9C=AC,=20=E7=A7=BB=E9=99=A4=E5=A4=9A=E4=BD=99=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=EF=BC=8C=E5=B9=B6=E6=B7=BB=E5=8A=A0=20Hangfire.Memory?= =?UTF-8?q?Storage=20=E4=BE=9D=E8=B5=96.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aio/content/Directory.Packages.props | 403 ++++++++---------- 1 file changed, 181 insertions(+), 222 deletions(-) diff --git a/aspnet-core/templates/aio/content/Directory.Packages.props b/aspnet-core/templates/aio/content/Directory.Packages.props index f464588df..b71e9419d 100644 --- a/aspnet-core/templates/aio/content/Directory.Packages.props +++ b/aspnet-core/templates/aio/content/Directory.Packages.props @@ -7,51 +7,11 @@ 8.0.0 8.0.0 8.0.0 - 3.1.1 - 8.0.0 - 2.6.1 - 1.8.1 - 6.8.0 - 17.8.0 - 1.12.0 - 6.2.0 - 8.2.0 - 3.7.0 - 1.8.6 - 1.0.5 - 1.0.2 - 16.18.9 - 3.0.2 - 6.0.0 - 3.0.0 - 5.1.0 - 4.2.1 - 2.5.3 - 1.5.10 - 2.13.0 - 1.6.9 - 0.34.0 - 13.0.3 - 7.15.1 - 0.9.2 - 20.0.0 - 4.0.0 - 2023.3.0 - 2.0.1 - 2.7.4 - 6.5.0 - 2.0.3 - 1.0.0-beta.11 - 5.0.0 - 5.4.37 - 3.0.712 - 2.1.0 - 5.5.0 true - + @@ -106,8 +66,11 @@ + + + + - @@ -176,6 +139,8 @@ + + @@ -236,10 +201,9 @@ - - + @@ -255,10 +219,9 @@ - - + + - @@ -276,7 +239,6 @@ - @@ -289,70 +251,68 @@ - - - + + - - - + + + - - - - - - - - + + + + + + + + - - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - + + + + + @@ -362,8 +322,8 @@ - - + + @@ -372,14 +332,14 @@ - - - - - - - - + + + + + + + + @@ -403,135 +363,134 @@ - + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 436b81cee64fc2a2319d4f7188bcb84a22607e31 Mon Sep 17 00:00:00 2001 From: feijie Date: Fri, 21 Mar 2025 19:21:55 +0800 Subject: [PATCH 03/10] =?UTF-8?q?=E2=9C=A8=20feat(host):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=20Hangfire=20=E6=94=AF=E6=8C=81=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E5=90=AF=E7=94=A8=E4=BB=AA=E8=A1=A8=E7=9B=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为应用添加后台任务调度框架 Hangfire,并启用其仪表盘,以便监视和管理后台任务。 --- ...rviceApplicationsSingleModule.Configure.cs | 71 +-- .../MicroServiceApplicationsSingleModule.cs | 91 +--- ...me.CompanyName.ProjectName.AIO.Host.csproj | 494 +++++++++--------- .../Program.cs | 1 + 4 files changed, 283 insertions(+), 374 deletions(-) diff --git a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/MicroServiceApplicationsSingleModule.Configure.cs b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/MicroServiceApplicationsSingleModule.Configure.cs index 765a6ceb2..fe7548c03 100644 --- a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/MicroServiceApplicationsSingleModule.Configure.cs +++ b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/MicroServiceApplicationsSingleModule.Configure.cs @@ -1,8 +1,8 @@ using Elsa; using Elsa.Options; +using Hangfire; +using Hangfire.Redis.StackExchange; using LINGYUN.Abp.Aliyun.Localization; -using LINGYUN.Abp.BackgroundTasks; -using LINGYUN.Abp.DataProtectionManagement; using LINGYUN.Abp.ExceptionHandling; using LINGYUN.Abp.ExceptionHandling.Emailing; using LINGYUN.Abp.Exporter.MiniExcel; @@ -29,7 +29,6 @@ using LINGYUN.Abp.WeChat.Work; using LINGYUN.Abp.Wrapper; using LINGYUN.Platform.Localization; using PackageName.CompanyName.ProjectName.AIO.Host.Microsoft.Extensions.DependencyInjection; -using PackageName.CompanyName.ProjectName.EntityFrameworkCore; using Medallion.Threading; using Medallion.Threading.Redis; using Microsoft.AspNetCore.Authentication.Cookies; @@ -41,13 +40,11 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Caching.StackExchangeRedis; using Microsoft.IdentityModel.Logging; using Microsoft.OpenApi.Models; -using MiniExcelLibs.Attributes; using OpenIddict.Server; using OpenIddict.Server.AspNetCore; using PackageName.CompanyName.ProjectName.AIO.Host.Authentication; using PackageName.CompanyName.ProjectName.AIO.Host.IdentityResources; using PackageName.CompanyName.ProjectName.AIO.Host.WeChat.Official.Messages; -using Quartz; using StackExchange.Redis; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; @@ -56,9 +53,11 @@ using System.Text.Unicode; using Volo.Abp; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc.AntiForgery; +using Volo.Abp.AspNetCore.Mvc.Libs; using Volo.Abp.AspNetCore.Mvc.UI.Bundling; using Volo.Abp.Auditing; using Volo.Abp.Authorization.Permissions; +using Volo.Abp.BackgroundWorkers; using Volo.Abp.BlobStoring; using Volo.Abp.BlobStoring.FileSystem; using Volo.Abp.Caching; @@ -77,7 +76,6 @@ using Volo.Abp.MultiTenancy; using Volo.Abp.OpenIddict; using Volo.Abp.OpenIddict.Localization; using Volo.Abp.PermissionManagement; -using Volo.Abp.Quartz; using Volo.Abp.Security.Claims; using Volo.Abp.SettingManagement; using Volo.Abp.SettingManagement.Localization; @@ -232,34 +230,6 @@ public partial class MicroServiceApplicationsSingleModule } } - private void PreConfigureQuartz(IConfiguration configuration) - { - PreConfigure(options => - { - // 如果使用持久化存储, 则配置quartz持久层 - if (configuration.GetSection("Quartz:UsePersistentStore").Get()) - { - var settings = configuration.GetSection("Quartz:Properties").Get>(); - if (settings != null) - { - foreach (var setting in settings) - { - options.Properties[setting.Key] = setting.Value; - } - } - - options.Configurator += (config) => - { - config.UsePersistentStore(store => - { - store.UseProperties = false; - store.UseNewtonsoftJsonSerializer(); - }); - }; - } - }); - } - private void PreConfigureElsa(IServiceCollection services, IConfiguration configuration) { var elsaSection = configuration.GetSection("Elsa"); @@ -406,17 +376,6 @@ public partial class MicroServiceApplicationsSingleModule }); } - private void ConfigureBackgroundTasks() - { - Configure(options => - { - options.NodeName = ApplicationName; - options.JobCleanEnabled = true; - options.JobFetchEnabled = true; - options.JobCheckEnabled = true; - }); - } - private void ConfigureTextTemplating(IConfiguration configuration) { if (configuration.GetValue("TextTemplating:IsDynamicStoreEnabled")) @@ -767,6 +726,28 @@ public partial class MicroServiceApplicationsSingleModule // } //); }); + + Configure(options => + { + options.CheckLibs = false; + }); + } + + private void ConfigureHangfire(IServiceCollection services, IConfiguration configuration) + { + // 配置Hangfire存储和设置 + Configure(options => + { + options.IsEnabled = true; + }); + + var redis = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]); + // 配置Hangfire + services.AddHangfire(config => + { + config.UseRedisStorage(redis); + }); + } private void ConfigureLocalization() diff --git a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/MicroServiceApplicationsSingleModule.cs b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/MicroServiceApplicationsSingleModule.cs index c3b097cba..578b8eb24 100644 --- a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/MicroServiceApplicationsSingleModule.cs +++ b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/MicroServiceApplicationsSingleModule.cs @@ -10,22 +10,11 @@ using LINGYUN.Abp.AuditLogging.EntityFrameworkCore; using LINGYUN.Abp.Authentication.QQ; using LINGYUN.Abp.Authentication.WeChat; using LINGYUN.Abp.Authorization.OrganizationUnits; -using LINGYUN.Abp.BackgroundTasks; -using LINGYUN.Abp.BackgroundTasks.Activities; -using LINGYUN.Abp.BackgroundTasks.DistributedLocking; -using LINGYUN.Abp.BackgroundTasks.EventBus; -using LINGYUN.Abp.BackgroundTasks.ExceptionHandling; -using LINGYUN.Abp.BackgroundTasks.Jobs; -using LINGYUN.Abp.BackgroundTasks.Notifications; -using LINGYUN.Abp.BackgroundTasks.Quartz; using LINGYUN.Abp.CachingManagement; using LINGYUN.Abp.CachingManagement.StackExchangeRedis; -using LINGYUN.Abp.Dapr.Client; using LINGYUN.Abp.Data.DbMigrator; using LINGYUN.Abp.DataProtectionManagement; using LINGYUN.Abp.DataProtectionManagement.EntityFrameworkCore; -// using LINGYUN.Abp.Demo; -// using LINGYUN.Abp.Demo.EntityFrameworkCore; using LINGYUN.Abp.ExceptionHandling; using LINGYUN.Abp.ExceptionHandling.Emailing; using LINGYUN.Abp.Exporter.MiniExcel; @@ -66,7 +55,6 @@ using LINGYUN.Abp.OpenIddict.WeChat; using LINGYUN.Abp.OpenIddict.WeChat.Work; using LINGYUN.Abp.OssManagement; using LINGYUN.Abp.OssManagement.FileSystem; -// using LINGYUN.Abp.OssManagement.Imaging; using LINGYUN.Abp.OssManagement.SettingManagement; using LINGYUN.Abp.PermissionManagement; using LINGYUN.Abp.PermissionManagement.HttpApi; @@ -77,8 +65,6 @@ using LINGYUN.Abp.Serilog.Enrichers.Application; using LINGYUN.Abp.Serilog.Enrichers.UniqueId; using LINGYUN.Abp.SettingManagement; using LINGYUN.Abp.Sms.Aliyun; -using LINGYUN.Abp.TaskManagement; -using LINGYUN.Abp.TaskManagement.EntityFrameworkCore; using LINGYUN.Abp.Tencent.QQ; using LINGYUN.Abp.Tencent.SettingManagement; using LINGYUN.Abp.TextTemplating; @@ -111,8 +97,11 @@ using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.Autofac; +using Volo.Abp.BackgroundJobs.Hangfire; +using Volo.Abp.BackgroundWorkers.Hangfire; using Volo.Abp.Caching.StackExchangeRedis; using Volo.Abp.Data; +using Volo.Abp.EntityFrameworkCore.PostgreSql; using Volo.Abp.EventBus; using Volo.Abp.FeatureManagement.EntityFrameworkCore; using Volo.Abp.Imaging; @@ -124,20 +113,9 @@ using Volo.Abp.PermissionManagement.OpenIddict; using Volo.Abp.SettingManagement; using Volo.Abp.SettingManagement.EntityFrameworkCore; using Volo.Abp.Threading; -#if MySQL -using Volo.Abp.EntityFrameworkCore.MySQL; -#elif SqlServer -using Volo.Abp.EntityFrameworkCore.SqlServer; -using Microsoft.EntityFrameworkCore.Infrastructure; -#elif Sqlite -using Volo.Abp.EntityFrameworkCore.Sqlite; -#elif Oracle -using Volo.Abp.EntityFrameworkCore.Oracle; -#elif OracleDevart -using Volo.Abp.EntityFrameworkCore.Oracle.Devart; -#elif PostgreSql -using Volo.Abp.EntityFrameworkCore.PostgreSql; -#endif +// using LINGYUN.Abp.Demo; +// using LINGYUN.Abp.Demo.EntityFrameworkCore; +// using LINGYUN.Abp.OssManagement.Imaging; namespace PackageName.CompanyName.ProjectName.AIO.Host; @@ -172,12 +150,6 @@ namespace PackageName.CompanyName.ProjectName.AIO.Host; typeof(AbpNotificationsApplicationModule), typeof(AbpNotificationsHttpApiModule), typeof(AbpNotificationsEntityFrameworkCoreModule), - - //typeof(AbpIdentityServerSessionModule), - //typeof(AbpIdentityServerApplicationModule), - //typeof(AbpIdentityServerHttpApiModule), - //typeof(AbpIdentityServerEntityFrameworkCoreModule), - typeof(AbpOpenIddictAspNetCoreModule), typeof(AbpOpenIddictAspNetCoreSessionModule), typeof(AbpOpenIddictApplicationModule), @@ -188,6 +160,9 @@ namespace PackageName.CompanyName.ProjectName.AIO.Host; typeof(AbpOpenIddictWeChatModule), typeof(AbpOpenIddictWeChatWorkModule), + typeof(AbpBackgroundWorkersHangfireModule), + typeof(AbpBackgroundJobsHangfireModule), + //typeof(AbpOssManagementMinioModule), // 取消注释以使用Minio typeof(AbpOssManagementFileSystemModule), // typeof(AbpOssManagementImagingModule), @@ -210,11 +185,6 @@ namespace PackageName.CompanyName.ProjectName.AIO.Host; typeof(AbpSaasHttpApiModule), typeof(AbpSaasEntityFrameworkCoreModule), - typeof(TaskManagementDomainModule), - typeof(TaskManagementApplicationModule), - typeof(TaskManagementHttpApiModule), - typeof(TaskManagementEntityFrameworkCoreModule), - typeof(AbpTextTemplatingDomainModule), typeof(AbpTextTemplatingApplicationModule), typeof(AbpTextTemplatingHttpApiModule), @@ -246,19 +216,7 @@ namespace PackageName.CompanyName.ProjectName.AIO.Host; typeof(AbpPermissionManagementEntityFrameworkCoreModule), typeof(AbpPermissionManagementDomainOrganizationUnitsModule), // 组织机构权限管理 -#if MySQL - typeof(AbpEntityFrameworkCoreMySQLModule), -#elif SqlServer - typeof(AbpEntityFrameworkCoreSqlServerModule), -#elif Sqlite - typeof(AbpEntityFrameworkCoreSqliteModule), -#elif Oracle - typeof(AbpEntityFrameworkCoreOracleModule), -#elif OracleDevart - typeof(AbpEntityFrameworkCoreOracleDevartModule), -#elif PostgreSql typeof(AbpEntityFrameworkCorePostgreSqlModule), -#endif typeof(AbpAliyunSmsModule), typeof(AbpAliyunSettingManagementModule), @@ -268,24 +226,9 @@ namespace PackageName.CompanyName.ProjectName.AIO.Host; typeof(AbpAuthorizationOrganizationUnitsModule), typeof(AbpIdentityOrganizaztionUnitsModule), - typeof(AbpBackgroundTasksModule), - typeof(AbpBackgroundTasksActivitiesModule), - typeof(AbpBackgroundTasksDistributedLockingModule), - typeof(AbpBackgroundTasksEventBusModule), - typeof(AbpBackgroundTasksExceptionHandlingModule), - typeof(AbpBackgroundTasksJobsModule), - typeof(AbpBackgroundTasksNotificationsModule), - typeof(AbpBackgroundTasksQuartzModule), - typeof(AbpDataProtectionManagementApplicationModule), typeof(AbpDataProtectionManagementHttpApiModule), typeof(AbpDataProtectionManagementEntityFrameworkCoreModule), - - // typeof(AbpDemoApplicationModule), - // typeof(AbpDemoHttpApiModule), - // typeof(AbpDemoEntityFrameworkCoreModule), - - typeof(AbpDaprClientModule), typeof(AbpExceptionHandlingModule), typeof(AbpEmailingExceptionHandlingModule), typeof(AbpFeaturesLimitValidationModule), @@ -327,17 +270,6 @@ namespace PackageName.CompanyName.ProjectName.AIO.Host; typeof(AbpAccountTemplatesModule), typeof(AbpAspNetCoreAuthenticationJwtBearerModule), typeof(AbpCachingStackExchangeRedisModule), - // typeof(AbpElsaModule), - // typeof(AbpElsaServerModule), - // typeof(AbpElsaActivitiesModule), - // typeof(AbpElsaEntityFrameworkCoreModule), - // typeof(AbpElsaEntityFrameworkCorePostgreSqlModule), - // typeof(AbpElsaModule), - // typeof(AbpElsaServerModule), - // typeof(AbpElsaActivitiesModule), - // typeof(AbpElsaEntityFrameworkCoreModule), - // typeof(AbpElsaEntityFrameworkCoreMySqlModule), - typeof(AbpExporterMiniExcelModule), typeof(AbpAspNetCoreMvcUiMultiTenancyModule), typeof(AbpAspNetCoreSerilogModule), @@ -348,7 +280,7 @@ namespace PackageName.CompanyName.ProjectName.AIO.Host; typeof(AbpAspNetCoreMvcUiBasicThemeModule), typeof(AbpEventBusModule), typeof(AbpAutofacModule), - + typeof(ProjectNameApplicationModule), typeof(ProjectNameHttpApiModule), typeof(ProjectNameEntityFrameworkCoreModule), @@ -365,7 +297,6 @@ public partial class MicroServiceApplicationsSingleModule : AbpModule PreConfigureFeature(); PreConfigureIdentity(); PreConfigureApp(configuration); - PreConfigureQuartz(configuration); PreConfigureAuthServer(configuration); PreConfigureElsa(context.Services, configuration); PreConfigureCertificate(configuration, hostingEnvironment); @@ -386,7 +317,7 @@ public partial class MicroServiceApplicationsSingleModule : AbpModule ConfigureDataSeeder(); ConfigureLocalization(); ConfigureKestrelServer(); - ConfigureBackgroundTasks(); + ConfigureHangfire(context.Services, configuration); ConfigureExceptionHandling(); ConfigureVirtualFileSystem(); ConfigureEntityDataProtected(); diff --git a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/PackageName.CompanyName.ProjectName.AIO.Host.csproj b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/PackageName.CompanyName.ProjectName.AIO.Host.csproj index 9e7ea06d7..f5c2d27ba 100644 --- a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/PackageName.CompanyName.ProjectName.AIO.Host.csproj +++ b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/PackageName.CompanyName.ProjectName.AIO.Host.csproj @@ -2,264 +2,260 @@ net9.0 enable + $(NoWarn);CS1591;CS0436;CS8618;NU1803;NU1900 - + - - - - - + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/Program.cs b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/Program.cs index 3daf07fe4..cbbf3048a 100644 --- a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/Program.cs +++ b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/Program.cs @@ -76,6 +76,7 @@ app.UseSwaggerUI(options => options.SwaggerEndpoint("/swagger/v1/swagger.json", "Support App API"); }); app.UseAuditing(); +app.UseAbpHangfireDashboard(); app.UseAbpSerilogEnrichers(); app.UseConfiguredEndpoints(); From bc2e5e3b7b47b833191cd4810a9fade8e1b71f18 Mon Sep 17 00:00:00 2001 From: feijie Date: Fri, 21 Mar 2025 19:53:30 +0800 Subject: [PATCH 04/10] =?UTF-8?q?=E2=9C=A8=20feat(user):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E5=8C=85=E6=8B=AC=E5=88=9B=E5=BB=BA=E3=80=81=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E3=80=81=E5=88=A0=E9=99=A4=E3=80=81=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=AD=89=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DataSeeder/IProjectNameDataSeeder.cs | 15 + .../ProjectNameDataSeederDataSeeder.cs | 503 ++++++++++++++++++ ...ProjectNamePermissionDefinitionProvider.cs | 6 + .../Permissions/ProjectNamePermissions.cs | 8 + .../Users/Dtos/CreateUpdateUserDto.cs | 37 ++ .../ProjectName/Users/Dtos/UserDto.cs | 39 ++ .../ProjectName/Users/Dtos/UserItemDto.cs | 39 ++ .../UserPagedAndSortedResultRequestDto.cs | 14 + .../ProjectName/Users/IUserAppService.cs | 55 ++ ...CompanyName.ProjectName.Application.csproj | 1 + .../ProjectNameApplicationMapperProfile.cs | 10 + .../ProjectNameApplicationModule.cs | 2 + .../ProjectName/Users/UserAppService.cs | 198 +++++++ ...Name.CompanyName.ProjectName.Domain.csproj | 4 + .../ProjectName/ProjectNameDomainModule.cs | 4 + .../ProjectName/TreeCodes/IHaveTreeCode.cs | 21 + .../TreeCodes/ITreeCodeGenerator.cs | 33 ++ .../TreeCodes/TreeCodeGenerator.cs | 114 ++++ .../ProjectName/Users/IUserManager.cs | 69 +++ .../ProjectName/Users/IUserRepository.cs | 9 + .../CompanyName/ProjectName/Users/User.cs | 59 ++ .../ProjectName/Users/UserManager.cs | 406 ++++++++++++++ ...ame.ProjectName.EntityFrameworkCore.csproj | 1 + .../IProjectNameDbContext.cs | 5 +- .../ProjectNameDbContext.cs | 9 +- ...ectNameDbContextModelCreatingExtensions.cs | 17 + .../ProjectNameEntityFrameworkCoreModule.cs | 6 +- .../ProjectName/Users/UserRepository.cs | 22 + .../ProjectName/Users/UserController.cs | 134 +++++ 29 files changed, 1836 insertions(+), 4 deletions(-) create mode 100644 aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore/DataSeeder/IProjectNameDataSeeder.cs create mode 100644 aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore/DataSeeder/ProjectNameDataSeederDataSeeder.cs create mode 100644 aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/CreateUpdateUserDto.cs create mode 100644 aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/UserDto.cs create mode 100644 aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/UserItemDto.cs create mode 100644 aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/UserPagedAndSortedResultRequestDto.cs create mode 100644 aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/IUserAppService.cs create mode 100644 aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/Users/UserAppService.cs create mode 100644 aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/TreeCodes/IHaveTreeCode.cs create mode 100644 aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/TreeCodes/ITreeCodeGenerator.cs create mode 100644 aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/TreeCodes/TreeCodeGenerator.cs create mode 100644 aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/IUserManager.cs create mode 100644 aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/IUserRepository.cs create mode 100644 aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/User.cs create mode 100644 aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/UserManager.cs create mode 100644 aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/Users/UserRepository.cs create mode 100644 aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.HttpApi/PackageName/CompanyName/ProjectName/Users/UserController.cs diff --git a/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore/DataSeeder/IProjectNameDataSeeder.cs b/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore/DataSeeder/IProjectNameDataSeeder.cs new file mode 100644 index 000000000..a1e6c8708 --- /dev/null +++ b/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore/DataSeeder/IProjectNameDataSeeder.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using Volo.Abp.Data; + +namespace PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DataSeeder +{ + public interface IProjectNameDataSeeder + { + /// + /// 初始化数据 + /// + /// 数据种子上下文 + /// 任务 + Task SeedAsync(DataSeedContext context); + } +} diff --git a/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore/DataSeeder/ProjectNameDataSeederDataSeeder.cs b/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore/DataSeeder/ProjectNameDataSeederDataSeeder.cs new file mode 100644 index 000000000..57276f6be --- /dev/null +++ b/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore/DataSeeder/ProjectNameDataSeederDataSeeder.cs @@ -0,0 +1,503 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Guids; +using Volo.Abp.Identity; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Uow; +using IdentityRole = Volo.Abp.Identity.IdentityRole; +using IdentityUser = Volo.Abp.Identity.IdentityUser; + +namespace PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DataSeeder +{ + public class ProjectNameDataSeederDataSeeder : IProjectNameDataSeeder, ITransientDependency + { + private readonly ICurrentTenant _currentTenant; + private readonly IGuidGenerator _guidGenerator; + private readonly ILogger _logger; + private readonly IRepository _orgRepository; + private readonly IRepository _workUnitRepository; + private readonly IRepository _userRepository; + private readonly IdentityUserManager _identityUserManager; + private readonly IdentityRoleManager _identityRoleManager; + private readonly IUnitOfWorkManager _unitOfWorkManager; + + /// + /// 构造函数 + /// + public ProjectNameDataSeeder( + ICurrentTenant currentTenant, + IGuidGenerator guidGenerator, + ILogger logger, + IRepository orgRepository, + IRepository workUnitRepository, + IRepository userRepository, + IdentityUserManager identityUserManager, + IdentityRoleManager identityRoleManager, + IUnitOfWorkManager unitOfWorkManager) + { + _currentTenant = currentTenant; + _guidGenerator = guidGenerator; + _logger = logger; + _orgRepository = orgRepository; + _workUnitRepository = workUnitRepository; + _userRepository = userRepository; + _identityUserManager = identityUserManager; + _identityRoleManager = identityRoleManager; + _unitOfWorkManager = unitOfWorkManager; + } + + /// + /// 初始化数据 + /// + /// 数据种子上下文 + /// 任务 + public async Task SeedAsync(DataSeedContext context) + { + using (_currentTenant.Change(context.TenantId)) + { + _logger.LogInformation("开始初始化巡检数据..."); + + // 初始化角色 + using (var uow = _unitOfWorkManager.Begin(requiresNew: true)) + { + await SeedRolesAsync(); + await uow.CompleteAsync(); + } + + // 初始化单位数据 + using (var uow = _unitOfWorkManager.Begin(requiresNew: true)) + { + await SeedWorkUnitsAsync(); + await uow.CompleteAsync(); + } + + // 初始化组织数据 + using (var uow = _unitOfWorkManager.Begin(requiresNew: true)) + { + await SeedOrgsAsync(); + await uow.CompleteAsync(); + } + + // 初始化用户数据 + using (var uow = _unitOfWorkManager.Begin(requiresNew: true)) + { + await SeedUsersAsync(); + await uow.CompleteAsync(); + } + + _logger.LogInformation("巡检数据初始化完成"); + } + } + + /// + /// 初始化角色数据 + /// + private async Task SeedRolesAsync() + { + // 超级管理员 + await CreateRoleIfNotExistsAsync( + "超级管理员", + "系统超级管理员,拥有所有权限"); + + // 区委办公室-督查室管理员 + await CreateRoleIfNotExistsAsync( + "区委办公室-督查室管理员", + "区委办公室督查室管理员"); + + // 责任领导秘书 + await CreateRoleIfNotExistsAsync( + "责任领导秘书", + "负责协助责任领导进行工作"); + + // 责任单位管理员 + await CreateRoleIfNotExistsAsync( + "责任单位管理员", + "负责管理责任单位的信息"); + + // 责任单位落实人员 + await CreateRoleIfNotExistsAsync( + "责任单位落实人员", + "负责执行责任单位的任务"); + + // 责任单位分管领导 + await CreateRoleIfNotExistsAsync( + "责任单位分管领导", + "负责管理责任单位的部分工作"); + + // 责任单位党组书记 + await CreateRoleIfNotExistsAsync( + "责任单位党组书记", + "负责责任单位的党组工作"); + } + + /// + /// 创建角色(如果不存在) + /// + private async Task CreateRoleIfNotExistsAsync(string roleName, string description) + { + if (await _identityRoleManager.FindByNameAsync(roleName) == null) + { + var role = new IdentityRole( + _guidGenerator.Create(), + roleName, + _currentTenant.Id) + { + IsStatic = true, + IsPublic = true + }; + + await _identityRoleManager.CreateAsync(role); + + _logger.LogInformation($"创建角色:{roleName}"); + } + } + + /// + /// 初始化单位数据 + /// + private async Task SeedWorkUnitsAsync() + { + // 创建永川区 + var yongchuanDistrict = await CreateWorkUnitIfNotExistsAsync( + "永川区", + "001", + "001", + "永川区"); + + // 创建区政府 + var districtGovernment = await CreateWorkUnitIfNotExistsAsync( + "区政府", + "002", + "001.002", + "永川区政府", + yongchuanDistrict.Id); + + // 创建区委办公室 + var districtCommitteeOffice = await CreateWorkUnitIfNotExistsAsync( + "区委办公室", + "003", + "001.003", + "永川区委办公室", + yongchuanDistrict.Id); + + // 创建督查室 + await CreateWorkUnitIfNotExistsAsync( + "督查室", + "004", + "001.003.004", + "区委办公室督查室", + districtCommitteeOffice.Id); + + // 创建区委扫黑办 + await CreateWorkUnitIfNotExistsAsync( + "区委扫黑办", + "005", + "001.005", + "永川区委扫黑办", + yongchuanDistrict.Id); + + // 创建区体育局 + await CreateWorkUnitIfNotExistsAsync( + "区体育局", + "006", + "001.006", + "永川区体育局", + yongchuanDistrict.Id); + + // 创建区统计局 + await CreateWorkUnitIfNotExistsAsync( + "区统计局", + "007", + "001.007", + "永川区统计局", + yongchuanDistrict.Id); + + // 创建区信访办 + await CreateWorkUnitIfNotExistsAsync( + "区信访办", + "008", + "001.008", + "永川区信访办", + yongchuanDistrict.Id); + + // 创建区医保局 + await CreateWorkUnitIfNotExistsAsync( + "区医保局", + "009", + "001.009", + "永川区医保局", + yongchuanDistrict.Id); + + // 创建区大数据发展局 + await CreateWorkUnitIfNotExistsAsync( + "区大数据发展局", + "010", + "001.010", + "永川区大数据发展局", + yongchuanDistrict.Id); + + // 创建区机关事务局 + await CreateWorkUnitIfNotExistsAsync( + "区机关事务局", + "011", + "001.011", + "永川区机关事务局", + yongchuanDistrict.Id); + + // 创建区广播电视局 + await CreateWorkUnitIfNotExistsAsync( + "区广播电视局", + "012", + "001.012", + "永川区广播电视局", + yongchuanDistrict.Id); + + // 创建区中新项目管理局 + await CreateWorkUnitIfNotExistsAsync( + "区中新项目管理局", + "013", + "001.013", + "永川区中新项目管理局", + yongchuanDistrict.Id); + + // 创建镇街 + await CreateWorkUnitIfNotExistsAsync( + "镇街", + "014", + "001.014", + "永川区镇街", + yongchuanDistrict.Id); + } + + /// + /// 创建单位(如果不存在) + /// + private async Task CreateWorkUnitIfNotExistsAsync( + string name, + string code, + string treeCode, + string description, + Guid? parentId = null) + { + var workUnit = await _workUnitRepository.FindAsync(w => w.Name == name); + + if (workUnit == null) + { + workUnit = new WorkUnit( + _guidGenerator.Create(), + code, + treeCode, + name, + description) + { + ParentId = parentId + }; + + await _workUnitRepository.InsertAsync(workUnit); + + _logger.LogInformation($"创建单位:{name}"); + } + + return workUnit; + } + + /// + /// 初始化组织数据 + /// + private async Task SeedOrgsAsync() + { + // 创建区委办公室-督查室 + var supervisionOffice = await CreateOrgIfNotExistsAsync( + "区委办公室-督查室", + "001", + "001", + "区委办公室督查室"); + } + + /// + /// 创建组织(如果不存在) + /// + private async Task CreateOrgIfNotExistsAsync( + string name, + string code, + string treeCode, + string description, + Guid? parentId = null) + { + var org = await _orgRepository.FindAsync(o => o.Name == name); + + if (org == null) + { + org = new Org( + _guidGenerator.Create(), + code, + treeCode, + name, + description) + { + ParentId = parentId + }; + + await _orgRepository.InsertAsync(org); + + _logger.LogInformation($"创建组织:{name}"); + } + + return org; + } + + /// + /// 初始化用户数据 + /// + private async Task SeedUsersAsync() + { + // 获取所需单位 + var supervisionOffice = await _workUnitRepository.FindAsync(w => w.Name == "督查室"); + if (supervisionOffice == null) + { + _logger.LogError("未找到督查室单位,无法创建用户"); + return; + } + + // 获取所需组织 + var supervisionOrg = await _orgRepository.FindAsync(o => o.Name == "区委办公室-督查室"); + if (supervisionOrg == null) + { + _logger.LogError("未找到区委办公室-督查室组织,无法创建用户"); + return; + } + + // 查找超级管理员角色 + var superAdminRole = await _identityRoleManager.FindByNameAsync("超级管理员"); + if (superAdminRole == null) + { + _logger.LogError("未找到超级管理员角色,无法为用户分配角色"); + return; + } + + // 创建用户数据(使用固定用户名避免生成问题) + await CreateUserIfNotExistsAsync("李达康", "lidk001", GenderType.Male, supervisionOffice.Id, supervisionOrg.Id, "超级管理员"); + await CreateUserIfNotExistsAsync("高育良", "gyl002", GenderType.Male, supervisionOffice.Id, supervisionOrg.Id, "超级管理员"); + await CreateUserIfNotExistsAsync("祁同伟", "qtw003", GenderType.Male, supervisionOffice.Id, supervisionOrg.Id, "超级管理员"); + await CreateUserIfNotExistsAsync("侯亮平", "hlp004", GenderType.Male, supervisionOffice.Id, supervisionOrg.Id, "超级管理员"); + await CreateUserIfNotExistsAsync("赵瑞龙", "zrl005", GenderType.Female, supervisionOffice.Id, supervisionOrg.Id, "超级管理员"); + await CreateUserIfNotExistsAsync("宋梦平", "smp006", GenderType.Male, supervisionOffice.Id, supervisionOrg.Id, "超级管理员, 部门领导"); + await CreateUserIfNotExistsAsync("李太亮", "ltl007", GenderType.Male, supervisionOffice.Id, supervisionOrg.Id, "超级管理员, 部门领导"); + await CreateUserIfNotExistsAsync("高峰", "gf008", GenderType.Male, supervisionOffice.Id, supervisionOrg.Id, "超级管理员, 部门领导"); + await CreateUserIfNotExistsAsync("朱丽平", "zlp009", GenderType.Female, supervisionOffice.Id, supervisionOrg.Id, "超级管理员, 部门领导"); + await CreateUserIfNotExistsAsync("安欣", "ax010", GenderType.Male, supervisionOffice.Id, supervisionOrg.Id, "超级管理员"); + } + + /// + /// 创建用户(如果不存在) + /// + private async Task CreateUserIfNotExistsAsync( + string name, + string userName, + GenderType gender, + Guid workUnitId, + Guid orgId, + string roles) + { + // 检查用户是否已存在 + var existingUser = await _userRepository.FindAsync(u => u.NickName == name); + if (existingUser != null) + { + _logger.LogInformation($"用户[{name}]已存在,跳过创建"); + return; + } + + var identityUser = await _identityUserManager.FindByNameAsync(userName); + if (identityUser == null) + { + // 创建Identity用户 + identityUser = new IdentityUser( + _guidGenerator.Create(), + userName, + $"{userName}@example.com", + _currentTenant.Id) + { + Name = name, + Surname = "" + }; + + // 设置默认密码 123456 + var identityResult = await _identityUserManager.CreateAsync(identityUser, "123456"); + if (!identityResult.Succeeded) + { + _logger.LogError($"创建Identity用户[{name}]失败: {string.Join(", ", identityResult.Errors.Select(e => e.Description))}"); + return; + } + + // 分配角色 + if (!string.IsNullOrWhiteSpace(roles)) + { + var roleNames = roles.Split(',', StringSplitOptions.RemoveEmptyEntries); + foreach (var roleName in roleNames) + { + var trimmedRoleName = roleName.Trim(); + var role = await _identityRoleManager.FindByNameAsync(trimmedRoleName); + if (role != null) + { + var roleResult = await _identityUserManager.AddToRoleAsync(identityUser, trimmedRoleName); + if (!roleResult.Succeeded) + { + _logger.LogWarning($"为用户[{name}]分配角色[{trimmedRoleName}]失败: {string.Join(", ", roleResult.Errors.Select(e => e.Description))}"); + } + } + else + { + _logger.LogWarning($"角色[{trimmedRoleName}]不存在,无法为用户[{name}]分配"); + } + } + } + + // 创建系统用户 + var user = new User( + _guidGenerator.Create(), + name, + workUnitId, + identityUser.Id, + gender) + { + OrgId = orgId + }; + + // 保存用户 + await _userRepository.InsertAsync(user); + + _logger.LogInformation($"创建用户:{name},用户名:{userName}"); + } + else + { + _logger.LogInformation($"Identity用户[{userName}]已存在,检查是否需要创建业务用户"); + + // 检查是否需要创建业务用户 + var businessUser = await _userRepository.FindAsync(u => u.IdentityUserId == identityUser.Id); + if (businessUser == null) + { + // 创建系统用户 + var user = new User( + _guidGenerator.Create(), + name, + workUnitId, + identityUser.Id, + gender) + { + OrgId = orgId + }; + + // 保存用户 + await _userRepository.InsertAsync(user); + + _logger.LogInformation($"为已存在的Identity用户创建业务用户:{name}"); + } + } + } + } +} diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Permissions/ProjectNamePermissionDefinitionProvider.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Permissions/ProjectNamePermissionDefinitionProvider.cs index 4a170551b..2adf0a13f 100644 --- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Permissions/ProjectNamePermissionDefinitionProvider.cs +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Permissions/ProjectNamePermissionDefinitionProvider.cs @@ -13,6 +13,12 @@ public class ProjectNamePermissionDefinitionProvider : PermissionDefinitionProvi group.AddPermission( ProjectNamePermissions.ManageSettings, L("Permission:ManageSettings")); + + var userPermission = group.AddPermission(ProjectNamePermissions.User.Default, L("Permission:User")); + userPermission.AddChild(ProjectNamePermissions.User.Create, L("Permission:Create")); + userPermission.AddChild(ProjectNamePermissions.User.Update, L("Permission:Update")); + userPermission.AddChild(ProjectNamePermissions.User.Delete, L("Permission:Delete")); + } private static LocalizableString L(string name) diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Permissions/ProjectNamePermissions.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Permissions/ProjectNamePermissions.cs index 98a957ff9..fb03f41bc 100644 --- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Permissions/ProjectNamePermissions.cs +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Permissions/ProjectNamePermissions.cs @@ -5,4 +5,12 @@ public static class ProjectNamePermissions public const string GroupName = "ProjectName"; public const string ManageSettings = GroupName + ".ManageSettings"; + + public class User + { + public const string Default = GroupName + ".User"; + public const string Update = Default + ".Update"; + public const string Create = Default + ".Create"; + public const string Delete = Default + ".Delete"; + } } diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/CreateUpdateUserDto.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/CreateUpdateUserDto.cs new file mode 100644 index 000000000..ae1e5e270 --- /dev/null +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/CreateUpdateUserDto.cs @@ -0,0 +1,37 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace PackageName.CompanyName.ProjectName.Users.Dtos +{ + [Serializable] + public class CreateUpdateUserDto + { + /// + /// 用户名称 + /// + [Required(ErrorMessage = "用户名称不能为空")] + [StringLength(50, ErrorMessage = "用户名称长度不能超过50个字符")] + public string NickName { get; set; } + + /// + /// 密码 + /// + [StringLength(20, MinimumLength = 6, ErrorMessage = "密码长度必须在6-20个字符之间")] + public string Password { get; set; } + + /// + /// 联系方式 + /// + public string ContactInfo { get; set; } + + /// + /// 职位 + /// + public string Position { get; set; } + + /// + /// 是否启用 + /// + public bool IsActive { get; set; } = true; + } +} \ No newline at end of file diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/UserDto.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/UserDto.cs new file mode 100644 index 000000000..2c5dd4f3d --- /dev/null +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/UserDto.cs @@ -0,0 +1,39 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace PackageName.CompanyName.ProjectName.Users.Dtos +{ + [Serializable] + public class UserDto : FullAuditedEntityDto + { + /// + /// 用户名称 + /// + public string NickName { get; set; } + + /// + /// Identity用户Id + /// + public Guid IdentityUserId { get; set; } + + /// + /// 用户状态 + /// + public bool IsActive { get; set; } + + /// + /// 联系方式 + /// + public string ContactInfo { get; set; } + + /// + /// 职位 + /// + public string Position { get; set; } + + /// + /// 角色名称 + /// + public string RoleNames { get; set; } + } +} \ No newline at end of file diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/UserItemDto.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/UserItemDto.cs new file mode 100644 index 000000000..92014ffbe --- /dev/null +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/UserItemDto.cs @@ -0,0 +1,39 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace PackageName.CompanyName.ProjectName.Users.Dtos +{ + [Serializable] + public class UserItemDto : FullAuditedEntityDto + { + /// + /// 用户名称 + /// + public string NickName { get; set; } + + /// + /// Identity用户Id + /// + public Guid IdentityUserId { get; set; } + + /// + /// 用户状态 + /// + public bool IsActive { get; set; } + + /// + /// 联系方式 + /// + public string ContactInfo { get; set; } + + /// + /// 职位 + /// + public string Position { get; set; } + + /// + /// 角色名称 + /// + public string RoleNames { get; set; } + } +} \ No newline at end of file diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/UserPagedAndSortedResultRequestDto.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/UserPagedAndSortedResultRequestDto.cs new file mode 100644 index 000000000..9c7c26ee9 --- /dev/null +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/UserPagedAndSortedResultRequestDto.cs @@ -0,0 +1,14 @@ +using System; +using Volo.Abp.Application.Dtos; + +namespace PackageName.CompanyName.ProjectName.Users.Dtos +{ + [Serializable] + public class UserPagedAndSortedResultRequestDto : PagedAndSortedResultRequestDto + { + /// + /// 用户名称 + /// + public string NickName { get; set; } + } +} \ No newline at end of file diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/IUserAppService.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/IUserAppService.cs new file mode 100644 index 000000000..606d0ddc7 --- /dev/null +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/IUserAppService.cs @@ -0,0 +1,55 @@ +using PackageName.CompanyName.ProjectName.Users.Dtos; +using System; +using System.Threading.Tasks; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; + +namespace PackageName.CompanyName.ProjectName.Users +{ + /// + /// 用户应用服务接口 + /// + public interface IUserAppService : + IApplicationService + { + /// + /// 创建用户 + /// + Task CreateAsync(CreateUpdateUserDto input); + + /// + /// 更新用户 + /// + Task UpdateAsync(Guid id, CreateUpdateUserDto input); + + /// + /// 删除用户 + /// + Task DeleteAsync(Guid id); + + /// + /// 获取用户 + /// + Task GetAsync(Guid id); + + /// + /// 获取用户列表 + /// + Task> GetListAsync(UserPagedAndSortedResultRequestDto input); + + /// + /// 修改用户密码 + /// + Task ChangePasswordAsync(Guid id, string currentPassword, string newPassword); + + /// + /// 重置用户密码(管理员操作) + /// + Task ResetPasswordAsync(Guid id, string newPassword); + + /// + /// 启用或禁用用户 + /// + Task SetUserActiveStatusAsync(Guid id, bool isActive); + } +} \ No newline at end of file diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName.CompanyName.ProjectName.Application.csproj b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName.CompanyName.ProjectName.Application.csproj index b36251c2d..10f4f0ddc 100644 --- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName.CompanyName.ProjectName.Application.csproj +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName.CompanyName.ProjectName.Application.csproj @@ -14,6 +14,7 @@ + diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/ProjectNameApplicationMapperProfile.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/ProjectNameApplicationMapperProfile.cs index 0ace9b456..2dd396079 100644 --- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/ProjectNameApplicationMapperProfile.cs +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/ProjectNameApplicationMapperProfile.cs @@ -1,4 +1,6 @@ using AutoMapper; +using PackageName.CompanyName.ProjectName.Users; +using PackageName.CompanyName.ProjectName.Users.Dtos; namespace PackageName.CompanyName.ProjectName; @@ -6,5 +8,13 @@ public class ProjectNameApplicationMapperProfile : Profile { public ProjectNameApplicationMapperProfile() { + CreateMap() + .ForMember(d => d.IsActive, o => o.Ignore()) + .ForMember(d => d.RoleNames, o => o.Ignore()); + CreateMap() + .ForMember(d => d.IsActive, o => o.Ignore()) + .ForMember(d => d.RoleNames, o => o.Ignore()); + CreateMap(MemberList.None); + } } diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/ProjectNameApplicationModule.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/ProjectNameApplicationModule.cs index 6e1cf009f..d8704f2ac 100644 --- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/ProjectNameApplicationModule.cs +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/ProjectNameApplicationModule.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Application; using Volo.Abp.Authorization; using Volo.Abp.AutoMapper; +using Volo.Abp.BackgroundJobs; using Volo.Abp.Modularity; namespace PackageName.CompanyName.ProjectName; @@ -12,6 +13,7 @@ namespace PackageName.CompanyName.ProjectName; typeof(AbpDddApplicationModule), typeof(ProjectNameDomainModule), typeof(ProjectNameApplicationContractsModule), + typeof(AbpBackgroundJobsModule), typeof(AbpDynamicQueryableApplicationModule))] public class ProjectNameApplicationModule : AbpModule { diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/Users/UserAppService.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/Users/UserAppService.cs new file mode 100644 index 000000000..25eef3a6e --- /dev/null +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/Users/UserAppService.cs @@ -0,0 +1,198 @@ +using Microsoft.AspNetCore.Authorization; +using PackageName.CompanyName.ProjectName.Permissions; +using PackageName.CompanyName.ProjectName.Users.Dtos; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; +using Volo.Abp.Identity; + +namespace PackageName.CompanyName.ProjectName.Users +{ + /// + /// 用户 + /// + public class UserAppService : ApplicationService, IUserAppService + { + private readonly IUserRepository _userRepository; + private readonly IUserManager _userManager; + private readonly IdentityUserManager _identityUserManager; + + public UserAppService( + IUserRepository userRepository, + IUserManager userManager, + IdentityUserManager identityUserManager) + { + _userRepository = userRepository; + _userManager = userManager; + _identityUserManager = identityUserManager; + } + + /// + /// 创建用户 + /// + [Authorize(ProjectNamePermissions.User.Create)] + public async Task CreateAsync(CreateUpdateUserDto input) + { + // 参数检查和验证逻辑可以在这里添加 + if (string.IsNullOrEmpty(input.NickName)) + { + throw new UserFriendlyException("昵称不能为空"); + } + if (string.IsNullOrEmpty(input.Password)) + { + throw new UserFriendlyException("密码不能为空"); + } + + // 使用UserManager创建用户 + var user = await _userManager.CreateAsync( + input.NickName, + input.Password, + input.ContactInfo, + input.Position, + input.IsActive); + + // 返回DTO对象 + return await MapToUserDtoAsync(user); + } + + /// + /// 更新用户 + /// + [Authorize(ProjectNamePermissions.User.Update)] + public async Task UpdateAsync(Guid id, CreateUpdateUserDto input) + { + // 使用UserManager更新用户基本信息 + var user = await _userManager.UpdateAsync( + id, + input.NickName, + input.Password, + input.ContactInfo, + input.Position, + input.IsActive); + + // 返回DTO对象 + return await MapToUserDtoAsync(user); + } + + /// + /// 删除用户 + /// + [Authorize(ProjectNamePermissions.User.Delete)] + public Task DeleteAsync(Guid id) + { + return _userManager.DeleteAsync(id); + } + + /// + /// 获取用户 + /// + [Authorize(ProjectNamePermissions.User.Default)] + public async Task GetAsync(Guid id) + { + var user = await _userManager.GetAsync(id); + return await MapToUserDtoAsync(user); + } + + /// + /// 获取用户列表 + /// + [Authorize(ProjectNamePermissions.User.Default)] + public async Task> GetListAsync(UserPagedAndSortedResultRequestDto input) + { + // 创建查询 + var query = await CreateFilteredQueryAsync(input); + + // 获取总记录数 + var totalCount = await AsyncExecuter.CountAsync(query); + + // 获取已排序和分页的查询结果 + var users = await AsyncExecuter.ToListAsync( + query.OrderBy(input.Sorting ?? nameof(User.NickName)) + .Skip(input.SkipCount) + .Take(input.MaxResultCount)); + + // 转换为DTO并返回 + var userDtos = new List(); + foreach (var user in users) + { + var userDto = ObjectMapper.Map(user); + + // 填充角色信息 + if (user.IdentityUser != null) + { + var roles = await _identityUserManager.GetRolesAsync(user.IdentityUser); + userDto.RoleNames = string.Join("、", roles); + userDto.IsActive = user.IdentityUser.LockoutEnd == null || user.IdentityUser.LockoutEnd < DateTimeOffset.Now; + } + + userDtos.Add(userDto); + } + + return new PagedResultDto(totalCount, userDtos); + } + + /// + /// 修改用户密码 + /// + [Authorize] + public async Task ChangePasswordAsync(Guid id, string currentPassword, string newPassword) + { + await _userManager.ChangePasswordAsync(id, currentPassword, newPassword); + } + + /// + /// 重置用户密码(管理员操作) + /// + [Authorize(ProjectNamePermissions.User.Update)] + public async Task ResetPasswordAsync(Guid id, string newPassword) + { + await _userManager.ResetPasswordAsync(id, newPassword); + } + + /// + /// 启用或禁用用户 + /// + [Authorize(ProjectNamePermissions.User.Update)] + public async Task SetUserActiveStatusAsync(Guid id, bool isActive) + { + await _userManager.SetUserActiveStatusAsync(id, isActive); + } + + /// + /// 创建基础查询,应用过滤条件 + /// + protected async virtual Task> CreateFilteredQueryAsync( + UserPagedAndSortedResultRequestDto input) + { + // 获取基础查询,并加载相关实体 + var query = await _userRepository.WithDetailsAsync( + x => x.IdentityUser); + + // 应用过滤条件 + return query + .WhereIf(!string.IsNullOrWhiteSpace(input.NickName), + x => x.NickName.Contains(input.NickName)); + } + + /// + /// 将User实体映射为UserDto,并填充权限信息 + /// + private Task MapToUserDtoAsync(User user) + { + var userDto = ObjectMapper.Map(user); + + // 设置用户状态 + if (user.IdentityUser != null) + { + userDto.IsActive = user.IdentityUser.LockoutEnd == null || user.IdentityUser.LockoutEnd < DateTimeOffset.Now; + } + + return Task.FromResult(userDto); + } + } +} \ No newline at end of file diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName.CompanyName.ProjectName.Domain.csproj b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName.CompanyName.ProjectName.Domain.csproj index 160b74c5c..4158f0a15 100644 --- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName.CompanyName.ProjectName.Domain.csproj +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName.CompanyName.ProjectName.Domain.csproj @@ -14,10 +14,14 @@ + + + + diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/ProjectNameDomainModule.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/ProjectNameDomainModule.cs index 401aa849e..6ca9ae998 100644 --- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/ProjectNameDomainModule.cs +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/ProjectNameDomainModule.cs @@ -1,7 +1,9 @@ using LINGYUN.Abp.DataProtection; +using LINGYUN.Abp.Identity; using Microsoft.Extensions.DependencyInjection; using PackageName.CompanyName.ProjectName.ObjectExtending; using Volo.Abp.AutoMapper; +using Volo.Abp.BackgroundWorkers.Hangfire; using Volo.Abp.Domain.Entities.Events.Distributed; using Volo.Abp.Modularity; using Volo.Abp.ObjectExtending.Modularity; @@ -12,6 +14,8 @@ namespace PackageName.CompanyName.ProjectName; [DependsOn( typeof(AbpAutoMapperModule), typeof(AbpDataProtectionModule), + typeof(AbpIdentityDomainModule), + typeof(AbpBackgroundWorkersHangfireModule), typeof(ProjectNameDomainSharedModule))] public class ProjectNameDomainModule : AbpModule { diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/TreeCodes/IHaveTreeCode.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/TreeCodes/IHaveTreeCode.cs new file mode 100644 index 000000000..f1641be31 --- /dev/null +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/TreeCodes/IHaveTreeCode.cs @@ -0,0 +1,21 @@ +using System; +using Volo.Abp.Auditing; + +namespace PackageName.CompanyName.ProjectName.TreeCodes +{ + /// + /// 定义具有树形编码的实体接口 + /// + public interface IHaveTreeCode : IHasCreationTime + { + /// + /// 树形编码 + /// + string TreeCode { get; set; } + + /// + /// 父级Id + /// + Guid? ParentId { get; set; } + } +} diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/TreeCodes/ITreeCodeGenerator.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/TreeCodes/ITreeCodeGenerator.cs new file mode 100644 index 000000000..0c3ce0754 --- /dev/null +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/TreeCodes/ITreeCodeGenerator.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Domain.Repositories; + +namespace PackageName.CompanyName.ProjectName.TreeCodes +{ + /// + /// 树形编码生成器接口 + /// + public interface ITreeCodeGenerator + { + /// + /// 生成树形编码 + /// + /// 实体类型 + /// 仓储 + /// 父级Id + /// 生成的树形编码 + Task GenerateAsync( + IRepository repository, + Guid? parentId) + where TEntity : class, IEntity, IHaveTreeCode; + + /// + /// 更新节点及其所有子节点的TreeCode + /// + Task UpdateTreeCodesAsync( + IRepository repository, + Guid entityId) + where TEntity : class, IEntity, IHaveTreeCode; + } +} diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/TreeCodes/TreeCodeGenerator.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/TreeCodes/TreeCodeGenerator.cs new file mode 100644 index 000000000..2b08bc0f5 --- /dev/null +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/TreeCodes/TreeCodeGenerator.cs @@ -0,0 +1,114 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Domain.Repositories; + +namespace PackageName.CompanyName.ProjectName.TreeCodes +{ + /// + /// 树形编码生成器 + /// + public class TreeCodeGenerator : ITreeCodeGenerator, ISingletonDependency + { + /// + /// 生成树形编码 + /// + /// 实体类型 + /// 仓储 + /// 父级Id + /// 生成的树形编码 + public async virtual Task GenerateAsync( + IRepository repository, + Guid? parentId) + where TEntity : class, IEntity, IHaveTreeCode + { + if (!parentId.HasValue) + { + // 生成根节点编码 + return await GenerateRootCodeAsync(repository); + } + else + { + // 生成子节点编码 + return await GenerateChildCodeAsync(repository, parentId.Value); + } + } + + private async Task GenerateRootCodeAsync( + IRepository repository) + where TEntity : class, IEntity, IHaveTreeCode + { + var query = await repository.GetQueryableAsync(); + query = query.Where(e => e.ParentId == null).OrderByDescending(e => e.CreationTime); + var maxCodeEntity = await repository.AsyncExecuter.FirstOrDefaultAsync(query); + + if (maxCodeEntity == null) + { + return "0001"; + } + + int maxCode = int.Parse(maxCodeEntity.TreeCode.Split('.').LastOrDefault("0")); + return (maxCode + 1).ToString("D4"); + } + + private async Task GenerateChildCodeAsync( + IRepository repository, + Guid parentId) + where TEntity : class, IEntity, IHaveTreeCode + { + var parent = await repository.GetAsync(parentId); + if (parent == null) + { + throw new EntityNotFoundException(typeof(TEntity), parentId); + } + + var query = await repository.GetQueryableAsync(); + query = query.Where(e => e.ParentId == parentId).OrderByDescending(e => e.CreationTime); + var maxCodeEntity = await repository.AsyncExecuter.FirstOrDefaultAsync(query); + + string newCode; + if (maxCodeEntity == null) + { + newCode = "0001"; + } + else + { + int maxCode = int.Parse(maxCodeEntity.TreeCode.Split('.').Last()); + newCode = (maxCode + 1).ToString("D4"); + } + + // 构建完整的TreeCode: 父TreeCode.新编码 + return $"{parent.TreeCode}.{newCode}"; + } + + /// + /// 更新节点及其所有子节点的TreeCode + /// + public async virtual Task UpdateTreeCodesAsync( + IRepository repository, + Guid entityId) + where TEntity : class, IEntity, IHaveTreeCode + { + var entity = await repository.GetAsync(entityId); + var query = await repository.GetQueryableAsync(); + var children = await repository.AsyncExecuter.ToListAsync( + query.Where(e => e.ParentId == entityId)); + + foreach (var child in children) + { + // 获取子节点编码(TreeCode最后一部分)或生成新编码 + string childCode = child.TreeCode.Contains('.') + ? child.TreeCode.Split('.').Last() + : child.TreeCode; + + child.TreeCode = $"{entity.TreeCode}.{childCode}"; + await repository.UpdateAsync(child); + + // 递归更新子节点 + await UpdateTreeCodesAsync(repository, child.Id); + } + } + } +} \ No newline at end of file diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/IUserManager.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/IUserManager.cs new file mode 100644 index 000000000..7d15b7792 --- /dev/null +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/IUserManager.cs @@ -0,0 +1,69 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.Domain.Services; + +namespace PackageName.CompanyName.ProjectName.Users +{ + /// + /// 用户管理服务接口 + /// + public interface IUserManager : IDomainService + { + /// + /// 创建新用户 + /// + Task CreateAsync( + string nickName, + string password, + string contactInfo = null, + string position = null, + bool isActive = true + ); + + /// + /// 更新用户信息 + /// + Task UpdateAsync( + Guid id, + string nickName, + string password, + string contactInfo = null, + string position = null, + bool isActive = true); + + /// + /// 删除用户 + /// + Task DeleteAsync(Guid id); + + /// + /// 修改用户密码 + /// + Task ChangePasswordAsync(Guid id, string currentPassword, string newPassword); + + /// + /// 重置用户密码 + /// + Task ResetPasswordAsync(Guid id, string newPassword); + + /// + /// 获取用户信息 + /// + Task GetAsync(Guid id); + + /// + /// 根据Identity用户ID获取用户 + /// + Task FindByIdentityUserIdAsync(Guid identityUserId); + + /// + /// 根据用户昵称查找用户 + /// + Task FindByNickNameAsync(string nickName); + + /// + /// 禁用或启用用户 + /// + Task SetUserActiveStatusAsync(Guid id, bool isActive); + } +} \ No newline at end of file diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/IUserRepository.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/IUserRepository.cs new file mode 100644 index 000000000..75fac813b --- /dev/null +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/IUserRepository.cs @@ -0,0 +1,9 @@ +using System; +using Volo.Abp.Domain.Repositories; + +namespace PackageName.CompanyName.ProjectName.Users +{ + public interface IUserRepository : IRepository + { + } +} \ No newline at end of file diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/User.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/User.cs new file mode 100644 index 000000000..855fb5116 --- /dev/null +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/User.cs @@ -0,0 +1,59 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.Identity; + +namespace PackageName.CompanyName.ProjectName.Users +{ + /// + /// 用户实体 + /// + public class User : FullAuditedEntity + { + /// + /// 用户名称 + /// + [MaxLength(50)] + public string NickName { get; set; } + + /// + /// Identity用户Id + /// + public Guid IdentityUserId { get; set; } + + /// + /// Identity用户 + /// + public virtual IdentityUser IdentityUser { get; set; } + + /// + /// 联系方式 + /// + [MaxLength(50)] + public string ContactInfo { get; set; } + + /// + /// 职位 + /// + [MaxLength(50)] + public string Position { get; set; } + + protected User() + { + } + + public User( + Guid id, + string nickName, + Guid identityUserId, + string contactInfo = null, + string position = null + ) : base(id) + { + NickName = nickName; + IdentityUserId = identityUserId; + ContactInfo = contactInfo; + Position = position; + } + } +} diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/UserManager.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/UserManager.cs new file mode 100644 index 000000000..bd616ba34 --- /dev/null +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/UserManager.cs @@ -0,0 +1,406 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Domain.Services; +using Volo.Abp.Identity; + +namespace PackageName.CompanyName.ProjectName.Users +{ + /// + /// 用户管理服务,用于处理用户的CRUD操作 + /// + public class UserManager : DomainService, IUserManager + { + private readonly IUserRepository _userRepository; + private readonly IdentityUserManager _identityUserManager; + private readonly ILogger _logger; + + public UserManager( + IUserRepository userRepository, + IdentityUserManager identityUserManager, + ILogger logger) + { + _userRepository = userRepository; + _identityUserManager = identityUserManager; + _logger = logger; + } + + /// + /// 创建新用户 + /// + /// 用户昵称 + /// 用户密码 + /// 联系方式 + /// 职位 + /// 是否启用 + /// 创建的用户实体 + public async Task CreateAsync( + string nickName, + string password, + string contactInfo = null, + string position = null, + bool isActive = true) + { + // 参数验证 + if (string.IsNullOrWhiteSpace(nickName)) + { + throw new UserFriendlyException("用户名不能为空"); + } + + // 密码校验 + if (string.IsNullOrWhiteSpace(password) || password.Length < 6) + { + throw new UserFriendlyException("密码不能为空且长度不能少于6位"); + } + + // 检查用户昵称是否已存在 + var existingUser = await _userRepository.FindAsync(u => u.NickName == nickName); + if (existingUser != null) + { + throw new UserFriendlyException($"昵称为 '{nickName}' 的用户已存在"); + } + + // 创建Identity用户 + var identityUser = new IdentityUser(GuidGenerator.Create(), nickName, $"{nickName}@inspection.com"); + var identityResult = await _identityUserManager.CreateAsync(identityUser, password); + if (!identityResult.Succeeded) + { + throw new UserFriendlyException("创建用户失败: " + + string.Join(", ", identityResult.Errors.Select(x => x.Description))); + } + + // 设置用户状态 + if (!isActive) + { + var lockoutResult = await _identityUserManager.SetLockoutEndDateAsync(identityUser, DateTimeOffset.MaxValue); + if (!lockoutResult.Succeeded) + { + throw new UserFriendlyException("设置用户状态失败: " + + string.Join(", ", lockoutResult.Errors.Select(e => e.Description))); + } + } + + // 创建业务用户 + var user = new User( + GuidGenerator.Create(), + nickName, + identityUser.Id, + contactInfo, + position + ); + + // 保存用户 + await _userRepository.InsertAsync(user, true); + + _logger.LogInformation($"创建了新用户:{nickName},ID:{user.Id}"); + + return user; + } + + /// + /// 更新用户信息 + /// + /// 用户ID + /// 用户昵称 + /// 用户密码(可选,如不修改则传入null) + /// 联系方式 + /// 职位 + /// 是否启用 + /// 更新后的用户实体 + public async Task UpdateAsync( + Guid id, + string nickName, + string password, + string contactInfo = null, + string position = null, + bool isActive = true) + { + // 最大重试次数 + const int maxRetries = 3; + int retryCount = 0; + + while (true) + { + try + { + // 每次尝试时重新获取最新的用户数据 + var user = await _userRepository.GetAsync(id, false); + if (user == null) + { + throw new UserFriendlyException("用户不存在"); + } + + // 检查用户昵称是否已被其他用户使用 + var existingUser = await _userRepository.FindAsync(u => u.NickName == nickName && u.Id != id); + if (existingUser != null) + { + throw new UserFriendlyException($"昵称为 '{nickName}' 的用户已存在"); + } + + // 更新Identity用户 + var identityUser = await _identityUserManager.FindByIdAsync(user.IdentityUserId.ToString()); + if (identityUser == null) + { + throw new UserFriendlyException("Identity用户不存在"); + } + + // 更新用户名 + var usernameResult = await _identityUserManager.SetUserNameAsync(identityUser, nickName); + if (!usernameResult.Succeeded) + { + throw new UserFriendlyException("更新用户名失败: " + + string.Join(", ", usernameResult.Errors.Select(e => e.Description))); + } + + // 更新电子邮件 + var emailResult = await _identityUserManager.SetEmailAsync(identityUser, $"{nickName}@inspection.com"); + if (!emailResult.Succeeded) + { + throw new UserFriendlyException("更新电子邮件失败: " + + string.Join(", ", emailResult.Errors.Select(e => e.Description))); + } + + // 如果提供了新密码,则更新密码 + if (!string.IsNullOrEmpty(password)) + { + // 移除当前密码 + await _identityUserManager.RemovePasswordAsync(identityUser); + // 设置新密码 + var passwordResult = await _identityUserManager.AddPasswordAsync(identityUser, password); + if (!passwordResult.Succeeded) + { + throw new UserFriendlyException("更新密码失败: " + + string.Join(", ", passwordResult.Errors.Select(e => e.Description))); + } + } + + // 设置用户状态 + await SetUserActiveStatusAsync(id, isActive); + + // 更新用户信息 + user.NickName = nickName; + user.ContactInfo = contactInfo; + user.Position = position; + + // 保存更新 + await _userRepository.UpdateAsync(user, true); + + _logger.LogInformation($"更新了用户信息:{nickName},ID:{user.Id}"); + + return user; + } + catch (Volo.Abp.Data.AbpDbConcurrencyException ex) + { + // 增加重试计数 + retryCount++; + + // 如果达到最大重试次数,则抛出用户友好的异常 + if (retryCount >= maxRetries) + { + throw new UserFriendlyException( + "更新用户信息失败:数据已被其他用户修改。请刷新页面后重试。", + "409", ex.Message, + ex); + } + + // 短暂延迟后重试 + await Task.Delay(100 * retryCount); // 逐步增加延迟时间 + + // 记录重试信息 + _logger.LogWarning($"检测到用户[{id}]更新时发生并发冲突,正在进行第{retryCount}次重试..."); + } + } + } + + /// + /// 删除用户 + /// + /// 用户ID + /// 操作任务 + public async Task DeleteAsync(Guid id) + { + // 获取用户 + var user = await _userRepository.GetAsync(id); + if (user == null) + { + throw new UserFriendlyException("用户不存在"); + } + + // 删除Identity用户 + var identityUser = await _identityUserManager.FindByIdAsync(user.IdentityUserId.ToString()); + if (identityUser != null) + { + var result = await _identityUserManager.DeleteAsync(identityUser); + if (!result.Succeeded) + { + throw new UserFriendlyException("删除Identity用户失败: " + + string.Join(", ", result.Errors.Select(e => e.Description))); + } + } + + // 删除用户 + await _userRepository.DeleteAsync(user); + + _logger.LogInformation($"删除了用户,ID:{id}"); + } + + /// + /// 修改用户密码 + /// + /// 用户ID + /// 当前密码 + /// 新密码 + /// 操作结果 + public async Task ChangePasswordAsync(Guid id, string currentPassword, string newPassword) + { + if (string.IsNullOrEmpty(newPassword) || newPassword.Length < 6) + { + throw new UserFriendlyException("新密码不能为空且长度不能少于6位"); + } + + // 获取用户 + var user = await _userRepository.GetAsync(id); + if (user == null) + { + throw new UserFriendlyException("用户不存在"); + } + + // 获取Identity用户 + var identityUser = await _identityUserManager.FindByIdAsync(user.IdentityUserId.ToString()); + if (identityUser == null) + { + throw new UserFriendlyException("Identity用户不存在"); + } + + // 修改密码 + var result = await _identityUserManager.ChangePasswordAsync(identityUser, currentPassword, newPassword); + if (!result.Succeeded) + { + throw new UserFriendlyException("修改密码失败: " + + string.Join(", ", result.Errors.Select(e => e.Description))); + } + + _logger.LogInformation($"用户[{user.NickName}]成功修改了密码"); + } + + /// + /// 重置用户密码(管理员操作) + /// + /// 用户ID + /// 新密码 + /// 操作结果 + public async Task ResetPasswordAsync(Guid id, string newPassword) + { + if (string.IsNullOrEmpty(newPassword) || newPassword.Length < 6) + { + throw new UserFriendlyException("新密码不能为空且长度不能少于6位"); + } + + // 获取用户 + var user = await _userRepository.GetAsync(id); + if (user == null) + { + throw new UserFriendlyException("用户不存在"); + } + + // 获取Identity用户 + var identityUser = await _identityUserManager.FindByIdAsync(user.IdentityUserId.ToString()); + if (identityUser == null) + { + throw new UserFriendlyException("Identity用户不存在"); + } + + // 生成重置令牌 + var token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityUser); + + // 重置密码 + var result = await _identityUserManager.ResetPasswordAsync(identityUser, token, newPassword); + if (!result.Succeeded) + { + throw new UserFriendlyException("重置密码失败: " + + string.Join(", ", result.Errors.Select(e => e.Description))); + } + + _logger.LogInformation($"管理员重置了用户[{user.NickName}]的密码"); + } + + /// + /// 获取用户信息 + /// + /// 用户ID + /// 用户实体 + public async Task GetAsync(Guid id) + { + return await _userRepository.GetAsync(id); + } + + /// + /// 根据Identity用户ID获取用户 + /// + /// Identity用户ID + /// 用户实体,如果不存在则返回null + public async Task FindByIdentityUserIdAsync(Guid identityUserId) + { + return await _userRepository.FindAsync(u => u.IdentityUserId == identityUserId); + } + + /// + /// 根据用户昵称查找用户 + /// + /// 用户昵称 + /// 用户实体,如果不存在则返回null + public async Task FindByNickNameAsync(string nickName) + { + return await _userRepository.FindAsync(u => u.NickName == nickName); + } + + /// + /// 禁用或启用用户 + /// + /// 用户ID + /// 是否启用 + /// 操作任务 + public async Task SetUserActiveStatusAsync(Guid id, bool isActive) + { + // 获取用户 + var user = await _userRepository.GetAsync(id); + if (user == null) + { + throw new UserFriendlyException("用户不存在"); + } + + // 获取Identity用户 + var identityUser = await _identityUserManager.FindByIdAsync(user.IdentityUserId.ToString()); + if (identityUser == null) + { + throw new UserFriendlyException("Identity用户不存在"); + } + + // 设置用户状态 + if (isActive) + { + // 启用用户 + var result = await _identityUserManager.SetLockoutEndDateAsync(identityUser, null); + if (!result.Succeeded) + { + throw new UserFriendlyException($"启用用户失败: " + + string.Join(", ", result.Errors.Select(e => e.Description))); + } + } + else + { + // 禁用用户(设置永久锁定) + var result = await _identityUserManager.SetLockoutEndDateAsync(identityUser, DateTimeOffset.MaxValue); + if (!result.Succeeded) + { + throw new UserFriendlyException($"禁用用户失败: " + + string.Join(", ", result.Errors.Select(e => e.Description))); + } + } + + _logger.LogInformation($"已{(isActive ? "启用" : "禁用")}用户[{user.NickName}]"); + } + } +} \ No newline at end of file diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName.CompanyName.ProjectName.EntityFrameworkCore.csproj b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName.CompanyName.ProjectName.EntityFrameworkCore.csproj index a7904db7d..ada1145a0 100644 --- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName.CompanyName.ProjectName.EntityFrameworkCore.csproj +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName.CompanyName.ProjectName.EntityFrameworkCore.csproj @@ -28,6 +28,7 @@ + diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/IProjectNameDbContext.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/IProjectNameDbContext.cs index c39c2923e..f3749036f 100644 --- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/IProjectNameDbContext.cs +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/IProjectNameDbContext.cs @@ -1,4 +1,6 @@ -using Volo.Abp.Data; +using Microsoft.EntityFrameworkCore; +using PackageName.CompanyName.ProjectName.Users; +using Volo.Abp.Data; using Volo.Abp.EntityFrameworkCore; namespace PackageName.CompanyName.ProjectName.EntityFrameworkCore; @@ -6,4 +8,5 @@ namespace PackageName.CompanyName.ProjectName.EntityFrameworkCore; [ConnectionStringName(ProjectNameDbProperties.ConnectionStringName)] public interface IProjectNameDbContext : IEfCoreDbContext { + DbSet Users { get; } } diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameDbContext.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameDbContext.cs index 5cb2c015b..988271a16 100644 --- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameDbContext.cs +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameDbContext.cs @@ -1,12 +1,16 @@ using LINGYUN.Abp.DataProtection.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; +using PackageName.CompanyName.ProjectName.Users; using Volo.Abp.Data; +using Volo.Abp.Identity.EntityFrameworkCore; namespace PackageName.CompanyName.ProjectName.EntityFrameworkCore; [ConnectionStringName(ProjectNameDbProperties.ConnectionStringName)] public class ProjectNameDbContext : AbpDataProtectionDbContext, IProjectNameDbContext { + public virtual DbSet Users { get; set; } + public ProjectNameDbContext( DbContextOptions options) : base(options) { @@ -14,8 +18,9 @@ public class ProjectNameDbContext : AbpDataProtectionDbContext(b => + { + b.ToTable(ProjectNameDbProperties.DbTablePrefix + "Users", ProjectNameDbProperties.DbSchema); + b.ConfigureByConvention(); + b.Property(x => x.NickName).HasComment("用户名称"); + b.Property(x => x.IdentityUserId).HasComment("Identity用户Id"); + + // 用户与IdentityUser的关系(一对一) + b.HasOne(x => x.IdentityUser) + .WithOne() + .HasForeignKey(u => u.IdentityUserId) + .IsRequired() + .OnDelete(DeleteBehavior.Cascade); + }); } } diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameEntityFrameworkCoreModule.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameEntityFrameworkCoreModule.cs index b18f43183..aac635d62 100644 --- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameEntityFrameworkCoreModule.cs +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameEntityFrameworkCoreModule.cs @@ -2,7 +2,9 @@ using LINGYUN.Abp.DataProtection.EntityFrameworkCore; using LINGYUN.Abp.Saas.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using PackageName.CompanyName.ProjectName.Users; using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.Identity.EntityFrameworkCore; using Volo.Abp.Modularity; #if MySQL using Volo.Abp.EntityFrameworkCore.MySQL; @@ -38,6 +40,7 @@ namespace PackageName.CompanyName.ProjectName.EntityFrameworkCore; #elif PostgreSql typeof(AbpEntityFrameworkCorePostgreSqlModule), #endif + typeof(AbpIdentityEntityFrameworkCoreModule), typeof(AbpSaasEntityFrameworkCoreModule))] public class ProjectNameEntityFrameworkCoreModule : AbpModule { @@ -70,7 +73,8 @@ public class ProjectNameEntityFrameworkCoreModule : AbpModule context.Services.AddAbpDbContext(options => { - options.AddDefaultRepositories(); + options.AddDefaultRepositories(true); + options.AddRepository(); }); } } diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/Users/UserRepository.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/Users/UserRepository.cs new file mode 100644 index 000000000..72d1575bf --- /dev/null +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/Users/UserRepository.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore; +using PackageName.CompanyName.ProjectName.EntityFrameworkCore; +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.EntityFrameworkCore; + +namespace PackageName.CompanyName.ProjectName.Users +{ + public class UserRepository : EfCoreRepository, IUserRepository + { + public UserRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider) + { + } + + public override async Task> WithDetailsAsync() + { + return (await GetDbSetAsync()).Include(x => x.IdentityUser); + } + } +} \ No newline at end of file diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.HttpApi/PackageName/CompanyName/ProjectName/Users/UserController.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.HttpApi/PackageName/CompanyName/ProjectName/Users/UserController.cs new file mode 100644 index 000000000..85d3f28f9 --- /dev/null +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.HttpApi/PackageName/CompanyName/ProjectName/Users/UserController.cs @@ -0,0 +1,134 @@ +using Microsoft.AspNetCore.Mvc; +using PackageName.CompanyName.ProjectName.Users.Dtos; +using System; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Application.Dtos; + +namespace PackageName.CompanyName.ProjectName.Users +{ + /// + /// 用户管理控制器 + /// + [RemoteService] + [Route("api/app/user")] + public class UserController : ProjectNameControllerBase + { + private readonly IUserAppService _userAppService; + + public UserController(IUserAppService userAppService) + { + _userAppService = userAppService; + } + + /// + /// 创建用户 + /// + [HttpPost] + public async Task CreateAsync(CreateUpdateUserDto input) + { + return await _userAppService.CreateAsync(input); + } + + /// + /// 更新用户 + /// + [HttpPut("{id}")] + public async Task UpdateAsync(Guid id, CreateUpdateUserDto input) + { + return await _userAppService.UpdateAsync(id, input); + } + + /// + /// 删除用户 + /// + [HttpDelete("{id}")] + public async Task DeleteAsync(Guid id) + { + await _userAppService.DeleteAsync(id); + } + + /// + /// 获取用户 + /// + [HttpGet("{id}")] + public async Task GetAsync(Guid id) + { + return await _userAppService.GetAsync(id); + } + + /// + /// 获取用户列表 + /// + [HttpGet] + public async Task> GetListAsync(UserPagedAndSortedResultRequestDto input) + { + return await _userAppService.GetListAsync(input); + } + + /// + /// 修改用户密码 + /// + [HttpPost("{id}/change-password")] + public async Task ChangePasswordAsync(Guid id, [FromBody] ChangePasswordRequest request) + { + await _userAppService.ChangePasswordAsync(id, request.CurrentPassword, request.NewPassword); + } + + /// + /// 重置用户密码(管理员操作) + /// + [HttpPost("{id}/reset-password")] + public async Task ResetPasswordAsync(Guid id, [FromBody] ResetPasswordRequest request) + { + await _userAppService.ResetPasswordAsync(id, request.NewPassword); + } + + /// + /// 启用或禁用用户 + /// + [HttpPost("{id}/set-active")] + public async Task SetUserActiveStatusAsync(Guid id, [FromBody] SetUserActiveRequest request) + { + await _userAppService.SetUserActiveStatusAsync(id, request.IsActive); + } + } + + /// + /// 修改密码请求 + /// + public class ChangePasswordRequest + { + /// + /// 当前密码 + /// + public string CurrentPassword { get; set; } + + /// + /// 新密码 + /// + public string NewPassword { get; set; } + } + + /// + /// 重置密码请求 + /// + public class ResetPasswordRequest + { + /// + /// 新密码 + /// + public string NewPassword { get; set; } + } + + /// + /// 设置用户状态请求 + /// + public class SetUserActiveRequest + { + /// + /// 是否启用 + /// + public bool IsActive { get; set; } + } +} From b7972bd36377f91863cee9ace10db7656823f089 Mon Sep 17 00:00:00 2001 From: feijie Date: Fri, 21 Mar 2025 20:05:32 +0800 Subject: [PATCH 05/10] =?UTF-8?q?=E2=9C=A8=20refactor(appsettings):=20?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E5=86=97=E4=BD=99=E6=97=A5=E5=BF=97=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=92=8CQuartz=E9=85=8D=E7=BD=AE=EF=BC=8C=E7=AE=80?= =?UTF-8?q?=E5=8C=96=E6=95=B0=E6=8D=AE=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除appsettings.json和appsettings.Development.json中无用的日志文件配置和Quartz配置信息。 重命名数据初始化类,并简化数据初始化逻辑,仅保留用户数据初始化。 --- .../appsettings.Development.json | 16 - .../appsettings.json | 18 - .../DataSeeder/ProjectNameDataSeeder.cs | 226 ++++++++ .../ProjectNameDataSeederDataSeeder.cs | 503 ------------------ 4 files changed, 226 insertions(+), 537 deletions(-) create mode 100644 aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore/DataSeeder/ProjectNameDataSeeder.cs delete mode 100644 aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore/DataSeeder/ProjectNameDataSeederDataSeeder.cs diff --git a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/appsettings.Development.json b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/appsettings.Development.json index 07d801a94..89e46c01d 100644 --- a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/appsettings.Development.json +++ b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/appsettings.Development.json @@ -92,22 +92,6 @@ "BaseUrl": "http://127.0.0.1:30000" } }, - "Quartz": { - "UsePersistentStore": false, - "Properties": { - "quartz.jobStore.dataSource": "tkm", - "quartz.jobStore.type": "Quartz.Impl.AdoJobStore.JobStoreTX,Quartz", - "quartz.dataSource.tkm.connectionStringName": "Default", - "quartz.jobStore.driverDelegateType": "Quartz.Impl.AdoJobStore.MySQLDelegate,Quartz", - "quartz.dataSource.tkm.connectionString": "Default": "Server=127.0.0.1;Database=ProjectName;User Id=root;Password=123456", - "quartz.dataSource.tkm.provider": "DatabaseManagementNameConnector", -// "quartz.jobStore.driverDelegateType": "Quartz.Impl.AdoJobStore.PostgreSQLDelegate,Quartz", -// "quartz.dataSource.tkm.connectionString": "Default": "Server=127.0.0.1;Database=ProjectName;User Id=root;Password=123456", -// "quartz.dataSource.tkm.provider": "Npgsql", - "quartz.jobStore.clustered": "true", - "quartz.serializer.type": "json" - } - }, "Redis": { "IsEnabled": true, "Configuration": "127.0.0.1,defaultDatabase=15", diff --git a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/appsettings.json b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/appsettings.json index ff9beea3e..6ba75de37 100644 --- a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/appsettings.json +++ b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/appsettings.json @@ -39,24 +39,6 @@ "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{ProcessId}] [{ThreadId}] - {Message:lj}{NewLine}{Exception}" } }, - { - "Name": "File", - "Args": { - "path": "Logs/Debug-.log", - "restrictedToMinimumLevel": "Debug", - "rollingInterval": "Day", - "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{ProcessId}] [{ThreadId}] - {Message:lj}{NewLine}{Exception}" - } - }, - { - "Name": "File", - "Args": { - "path": "Logs/Info-.log", - "restrictedToMinimumLevel": "Information", - "rollingInterval": "Day", - "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{ProcessId}] [{ThreadId}] - {Message:lj}{NewLine}{Exception}" - } - }, { "Name": "File", "Args": { diff --git a/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore/DataSeeder/ProjectNameDataSeeder.cs b/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore/DataSeeder/ProjectNameDataSeeder.cs new file mode 100644 index 000000000..d322894fc --- /dev/null +++ b/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore/DataSeeder/ProjectNameDataSeeder.cs @@ -0,0 +1,226 @@ +using Microsoft.Extensions.Logging; +using PackageName.CompanyName.ProjectName.Users; +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Guids; +using Volo.Abp.Identity; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Uow; +using IdentityRole = Volo.Abp.Identity.IdentityRole; +using IdentityUser = Volo.Abp.Identity.IdentityUser; + +namespace PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DataSeeder +{ + public class ProjectNameDataSeeder : IProjectNameDataSeeder, ITransientDependency + { + private readonly ICurrentTenant _currentTenant; + private readonly IGuidGenerator _guidGenerator; + private readonly ILogger _logger; + private readonly IRepository _userRepository; + private readonly IdentityUserManager _identityUserManager; + private readonly IdentityRoleManager _identityRoleManager; + private readonly IUnitOfWorkManager _unitOfWorkManager; + + /// + /// 构造函数 + /// + public ProjectNameDataSeeder( + ICurrentTenant currentTenant, + IGuidGenerator guidGenerator, + ILogger logger, + IRepository userRepository, + IdentityUserManager identityUserManager, + IdentityRoleManager identityRoleManager, + IUnitOfWorkManager unitOfWorkManager) + { + _currentTenant = currentTenant; + _guidGenerator = guidGenerator; + _logger = logger; + _userRepository = userRepository; + _identityUserManager = identityUserManager; + _identityRoleManager = identityRoleManager; + _unitOfWorkManager = unitOfWorkManager; + } + + /// + /// 初始化数据 + /// + /// 数据种子上下文 + /// 任务 + public async Task SeedAsync(DataSeedContext context) + { + using (_currentTenant.Change(context.TenantId)) + { + _logger.LogInformation("开始初始化数据..."); + + // 初始化角色 + using (var uow = _unitOfWorkManager.Begin(requiresNew: true)) + { + await SeedRolesAsync(); + await uow.CompleteAsync(); + } + + // 初始化用户数据 + using (var uow = _unitOfWorkManager.Begin(requiresNew: true)) + { + await SeedUsersAsync(); + await uow.CompleteAsync(); + } + + _logger.LogInformation("数据初始化完成"); + } + } + + /// + /// 初始化角色数据 + /// + private async Task SeedRolesAsync() + { + // 超级管理员 + await CreateRoleIfNotExistsAsync( + "超级管理员", + "系统超级管理员,拥有所有权限"); + } + + /// + /// 创建角色(如果不存在) + /// + private async Task CreateRoleIfNotExistsAsync(string roleName, string description) + { + if (await _identityRoleManager.FindByNameAsync(roleName) == null) + { + var role = new IdentityRole( + _guidGenerator.Create(), + roleName, + _currentTenant.Id) + { + IsStatic = true, + IsPublic = true + }; + + await _identityRoleManager.CreateAsync(role); + + _logger.LogInformation($"创建角色:{roleName}"); + } + } + + /// + /// 初始化用户数据 + /// + private async Task SeedUsersAsync() + { + + // 查找超级管理员角色 + var superAdminRole = await _identityRoleManager.FindByNameAsync("超级管理员"); + if (superAdminRole == null) + { + _logger.LogError("未找到超级管理员角色,无法为用户分配角色"); + return; + } + + // 创建用户数据(使用固定用户名避免生成问题) + await CreateUserIfNotExistsAsync("user1", "user1", "超级管理员"); + await CreateUserIfNotExistsAsync("user2", "user2", "超级管理员"); + await CreateUserIfNotExistsAsync("user3", "user3", "超级管理员"); + await CreateUserIfNotExistsAsync("user4", "user4", "超级管理员"); + } + + /// + /// 创建用户(如果不存在) + /// + private async Task CreateUserIfNotExistsAsync( + string name, + string userName, + string roles) + { + // 检查用户是否已存在 + var existingUser = await _userRepository.FindAsync(u => u.NickName == name); + if (existingUser != null) + { + _logger.LogInformation($"用户[{name}]已存在,跳过创建"); + return; + } + + var identityUser = await _identityUserManager.FindByNameAsync(userName); + if (identityUser == null) + { + // 创建Identity用户 + identityUser = new IdentityUser( + _guidGenerator.Create(), + userName, + $"{userName}@example.com", + _currentTenant.Id) + { + Name = name, + Surname = "" + }; + + // 设置默认密码 123456 + var identityResult = await _identityUserManager.CreateAsync(identityUser, "123456"); + if (!identityResult.Succeeded) + { + _logger.LogError($"创建Identity用户[{name}]失败: {string.Join(", ", identityResult.Errors.Select(e => e.Description))}"); + return; + } + + // 分配角色 + if (!string.IsNullOrWhiteSpace(roles)) + { + var roleNames = roles.Split(',', StringSplitOptions.RemoveEmptyEntries); + foreach (var roleName in roleNames) + { + var trimmedRoleName = roleName.Trim(); + var role = await _identityRoleManager.FindByNameAsync(trimmedRoleName); + if (role != null) + { + var roleResult = await _identityUserManager.AddToRoleAsync(identityUser, trimmedRoleName); + if (!roleResult.Succeeded) + { + _logger.LogWarning($"为用户[{name}]分配角色[{trimmedRoleName}]失败: {string.Join(", ", roleResult.Errors.Select(e => e.Description))}"); + } + } + else + { + _logger.LogWarning($"角色[{trimmedRoleName}]不存在,无法为用户[{name}]分配"); + } + } + } + + // 创建系统用户 + var user = new User( + _guidGenerator.Create(), + name, + identityUser.Id); + + // 保存用户 + await _userRepository.InsertAsync(user); + + _logger.LogInformation($"创建用户:{name},用户名:{userName}"); + } + else + { + _logger.LogInformation($"Identity用户[{userName}]已存在,检查是否需要创建业务用户"); + + // 检查是否需要创建业务用户 + var businessUser = await _userRepository.FindAsync(u => u.IdentityUserId == identityUser.Id); + if (businessUser == null) + { + // 创建系统用户 + var user = new User( + _guidGenerator.Create(), + name, + identityUser.Id); + + // 保存用户 + await _userRepository.InsertAsync(user); + + _logger.LogInformation($"为已存在的Identity用户创建业务用户:{name}"); + } + } + } + } +} diff --git a/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore/DataSeeder/ProjectNameDataSeederDataSeeder.cs b/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore/DataSeeder/ProjectNameDataSeederDataSeeder.cs deleted file mode 100644 index 57276f6be..000000000 --- a/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore/DataSeeder/ProjectNameDataSeederDataSeeder.cs +++ /dev/null @@ -1,503 +0,0 @@ -using Microsoft.Extensions.Logging; -using System; -using System.Linq; -using System.Threading.Tasks; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Guids; -using Volo.Abp.Identity; -using Volo.Abp.MultiTenancy; -using Volo.Abp.Uow; -using IdentityRole = Volo.Abp.Identity.IdentityRole; -using IdentityUser = Volo.Abp.Identity.IdentityUser; - -namespace PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DataSeeder -{ - public class ProjectNameDataSeederDataSeeder : IProjectNameDataSeeder, ITransientDependency - { - private readonly ICurrentTenant _currentTenant; - private readonly IGuidGenerator _guidGenerator; - private readonly ILogger _logger; - private readonly IRepository _orgRepository; - private readonly IRepository _workUnitRepository; - private readonly IRepository _userRepository; - private readonly IdentityUserManager _identityUserManager; - private readonly IdentityRoleManager _identityRoleManager; - private readonly IUnitOfWorkManager _unitOfWorkManager; - - /// - /// 构造函数 - /// - public ProjectNameDataSeeder( - ICurrentTenant currentTenant, - IGuidGenerator guidGenerator, - ILogger logger, - IRepository orgRepository, - IRepository workUnitRepository, - IRepository userRepository, - IdentityUserManager identityUserManager, - IdentityRoleManager identityRoleManager, - IUnitOfWorkManager unitOfWorkManager) - { - _currentTenant = currentTenant; - _guidGenerator = guidGenerator; - _logger = logger; - _orgRepository = orgRepository; - _workUnitRepository = workUnitRepository; - _userRepository = userRepository; - _identityUserManager = identityUserManager; - _identityRoleManager = identityRoleManager; - _unitOfWorkManager = unitOfWorkManager; - } - - /// - /// 初始化数据 - /// - /// 数据种子上下文 - /// 任务 - public async Task SeedAsync(DataSeedContext context) - { - using (_currentTenant.Change(context.TenantId)) - { - _logger.LogInformation("开始初始化巡检数据..."); - - // 初始化角色 - using (var uow = _unitOfWorkManager.Begin(requiresNew: true)) - { - await SeedRolesAsync(); - await uow.CompleteAsync(); - } - - // 初始化单位数据 - using (var uow = _unitOfWorkManager.Begin(requiresNew: true)) - { - await SeedWorkUnitsAsync(); - await uow.CompleteAsync(); - } - - // 初始化组织数据 - using (var uow = _unitOfWorkManager.Begin(requiresNew: true)) - { - await SeedOrgsAsync(); - await uow.CompleteAsync(); - } - - // 初始化用户数据 - using (var uow = _unitOfWorkManager.Begin(requiresNew: true)) - { - await SeedUsersAsync(); - await uow.CompleteAsync(); - } - - _logger.LogInformation("巡检数据初始化完成"); - } - } - - /// - /// 初始化角色数据 - /// - private async Task SeedRolesAsync() - { - // 超级管理员 - await CreateRoleIfNotExistsAsync( - "超级管理员", - "系统超级管理员,拥有所有权限"); - - // 区委办公室-督查室管理员 - await CreateRoleIfNotExistsAsync( - "区委办公室-督查室管理员", - "区委办公室督查室管理员"); - - // 责任领导秘书 - await CreateRoleIfNotExistsAsync( - "责任领导秘书", - "负责协助责任领导进行工作"); - - // 责任单位管理员 - await CreateRoleIfNotExistsAsync( - "责任单位管理员", - "负责管理责任单位的信息"); - - // 责任单位落实人员 - await CreateRoleIfNotExistsAsync( - "责任单位落实人员", - "负责执行责任单位的任务"); - - // 责任单位分管领导 - await CreateRoleIfNotExistsAsync( - "责任单位分管领导", - "负责管理责任单位的部分工作"); - - // 责任单位党组书记 - await CreateRoleIfNotExistsAsync( - "责任单位党组书记", - "负责责任单位的党组工作"); - } - - /// - /// 创建角色(如果不存在) - /// - private async Task CreateRoleIfNotExistsAsync(string roleName, string description) - { - if (await _identityRoleManager.FindByNameAsync(roleName) == null) - { - var role = new IdentityRole( - _guidGenerator.Create(), - roleName, - _currentTenant.Id) - { - IsStatic = true, - IsPublic = true - }; - - await _identityRoleManager.CreateAsync(role); - - _logger.LogInformation($"创建角色:{roleName}"); - } - } - - /// - /// 初始化单位数据 - /// - private async Task SeedWorkUnitsAsync() - { - // 创建永川区 - var yongchuanDistrict = await CreateWorkUnitIfNotExistsAsync( - "永川区", - "001", - "001", - "永川区"); - - // 创建区政府 - var districtGovernment = await CreateWorkUnitIfNotExistsAsync( - "区政府", - "002", - "001.002", - "永川区政府", - yongchuanDistrict.Id); - - // 创建区委办公室 - var districtCommitteeOffice = await CreateWorkUnitIfNotExistsAsync( - "区委办公室", - "003", - "001.003", - "永川区委办公室", - yongchuanDistrict.Id); - - // 创建督查室 - await CreateWorkUnitIfNotExistsAsync( - "督查室", - "004", - "001.003.004", - "区委办公室督查室", - districtCommitteeOffice.Id); - - // 创建区委扫黑办 - await CreateWorkUnitIfNotExistsAsync( - "区委扫黑办", - "005", - "001.005", - "永川区委扫黑办", - yongchuanDistrict.Id); - - // 创建区体育局 - await CreateWorkUnitIfNotExistsAsync( - "区体育局", - "006", - "001.006", - "永川区体育局", - yongchuanDistrict.Id); - - // 创建区统计局 - await CreateWorkUnitIfNotExistsAsync( - "区统计局", - "007", - "001.007", - "永川区统计局", - yongchuanDistrict.Id); - - // 创建区信访办 - await CreateWorkUnitIfNotExistsAsync( - "区信访办", - "008", - "001.008", - "永川区信访办", - yongchuanDistrict.Id); - - // 创建区医保局 - await CreateWorkUnitIfNotExistsAsync( - "区医保局", - "009", - "001.009", - "永川区医保局", - yongchuanDistrict.Id); - - // 创建区大数据发展局 - await CreateWorkUnitIfNotExistsAsync( - "区大数据发展局", - "010", - "001.010", - "永川区大数据发展局", - yongchuanDistrict.Id); - - // 创建区机关事务局 - await CreateWorkUnitIfNotExistsAsync( - "区机关事务局", - "011", - "001.011", - "永川区机关事务局", - yongchuanDistrict.Id); - - // 创建区广播电视局 - await CreateWorkUnitIfNotExistsAsync( - "区广播电视局", - "012", - "001.012", - "永川区广播电视局", - yongchuanDistrict.Id); - - // 创建区中新项目管理局 - await CreateWorkUnitIfNotExistsAsync( - "区中新项目管理局", - "013", - "001.013", - "永川区中新项目管理局", - yongchuanDistrict.Id); - - // 创建镇街 - await CreateWorkUnitIfNotExistsAsync( - "镇街", - "014", - "001.014", - "永川区镇街", - yongchuanDistrict.Id); - } - - /// - /// 创建单位(如果不存在) - /// - private async Task CreateWorkUnitIfNotExistsAsync( - string name, - string code, - string treeCode, - string description, - Guid? parentId = null) - { - var workUnit = await _workUnitRepository.FindAsync(w => w.Name == name); - - if (workUnit == null) - { - workUnit = new WorkUnit( - _guidGenerator.Create(), - code, - treeCode, - name, - description) - { - ParentId = parentId - }; - - await _workUnitRepository.InsertAsync(workUnit); - - _logger.LogInformation($"创建单位:{name}"); - } - - return workUnit; - } - - /// - /// 初始化组织数据 - /// - private async Task SeedOrgsAsync() - { - // 创建区委办公室-督查室 - var supervisionOffice = await CreateOrgIfNotExistsAsync( - "区委办公室-督查室", - "001", - "001", - "区委办公室督查室"); - } - - /// - /// 创建组织(如果不存在) - /// - private async Task CreateOrgIfNotExistsAsync( - string name, - string code, - string treeCode, - string description, - Guid? parentId = null) - { - var org = await _orgRepository.FindAsync(o => o.Name == name); - - if (org == null) - { - org = new Org( - _guidGenerator.Create(), - code, - treeCode, - name, - description) - { - ParentId = parentId - }; - - await _orgRepository.InsertAsync(org); - - _logger.LogInformation($"创建组织:{name}"); - } - - return org; - } - - /// - /// 初始化用户数据 - /// - private async Task SeedUsersAsync() - { - // 获取所需单位 - var supervisionOffice = await _workUnitRepository.FindAsync(w => w.Name == "督查室"); - if (supervisionOffice == null) - { - _logger.LogError("未找到督查室单位,无法创建用户"); - return; - } - - // 获取所需组织 - var supervisionOrg = await _orgRepository.FindAsync(o => o.Name == "区委办公室-督查室"); - if (supervisionOrg == null) - { - _logger.LogError("未找到区委办公室-督查室组织,无法创建用户"); - return; - } - - // 查找超级管理员角色 - var superAdminRole = await _identityRoleManager.FindByNameAsync("超级管理员"); - if (superAdminRole == null) - { - _logger.LogError("未找到超级管理员角色,无法为用户分配角色"); - return; - } - - // 创建用户数据(使用固定用户名避免生成问题) - await CreateUserIfNotExistsAsync("李达康", "lidk001", GenderType.Male, supervisionOffice.Id, supervisionOrg.Id, "超级管理员"); - await CreateUserIfNotExistsAsync("高育良", "gyl002", GenderType.Male, supervisionOffice.Id, supervisionOrg.Id, "超级管理员"); - await CreateUserIfNotExistsAsync("祁同伟", "qtw003", GenderType.Male, supervisionOffice.Id, supervisionOrg.Id, "超级管理员"); - await CreateUserIfNotExistsAsync("侯亮平", "hlp004", GenderType.Male, supervisionOffice.Id, supervisionOrg.Id, "超级管理员"); - await CreateUserIfNotExistsAsync("赵瑞龙", "zrl005", GenderType.Female, supervisionOffice.Id, supervisionOrg.Id, "超级管理员"); - await CreateUserIfNotExistsAsync("宋梦平", "smp006", GenderType.Male, supervisionOffice.Id, supervisionOrg.Id, "超级管理员, 部门领导"); - await CreateUserIfNotExistsAsync("李太亮", "ltl007", GenderType.Male, supervisionOffice.Id, supervisionOrg.Id, "超级管理员, 部门领导"); - await CreateUserIfNotExistsAsync("高峰", "gf008", GenderType.Male, supervisionOffice.Id, supervisionOrg.Id, "超级管理员, 部门领导"); - await CreateUserIfNotExistsAsync("朱丽平", "zlp009", GenderType.Female, supervisionOffice.Id, supervisionOrg.Id, "超级管理员, 部门领导"); - await CreateUserIfNotExistsAsync("安欣", "ax010", GenderType.Male, supervisionOffice.Id, supervisionOrg.Id, "超级管理员"); - } - - /// - /// 创建用户(如果不存在) - /// - private async Task CreateUserIfNotExistsAsync( - string name, - string userName, - GenderType gender, - Guid workUnitId, - Guid orgId, - string roles) - { - // 检查用户是否已存在 - var existingUser = await _userRepository.FindAsync(u => u.NickName == name); - if (existingUser != null) - { - _logger.LogInformation($"用户[{name}]已存在,跳过创建"); - return; - } - - var identityUser = await _identityUserManager.FindByNameAsync(userName); - if (identityUser == null) - { - // 创建Identity用户 - identityUser = new IdentityUser( - _guidGenerator.Create(), - userName, - $"{userName}@example.com", - _currentTenant.Id) - { - Name = name, - Surname = "" - }; - - // 设置默认密码 123456 - var identityResult = await _identityUserManager.CreateAsync(identityUser, "123456"); - if (!identityResult.Succeeded) - { - _logger.LogError($"创建Identity用户[{name}]失败: {string.Join(", ", identityResult.Errors.Select(e => e.Description))}"); - return; - } - - // 分配角色 - if (!string.IsNullOrWhiteSpace(roles)) - { - var roleNames = roles.Split(',', StringSplitOptions.RemoveEmptyEntries); - foreach (var roleName in roleNames) - { - var trimmedRoleName = roleName.Trim(); - var role = await _identityRoleManager.FindByNameAsync(trimmedRoleName); - if (role != null) - { - var roleResult = await _identityUserManager.AddToRoleAsync(identityUser, trimmedRoleName); - if (!roleResult.Succeeded) - { - _logger.LogWarning($"为用户[{name}]分配角色[{trimmedRoleName}]失败: {string.Join(", ", roleResult.Errors.Select(e => e.Description))}"); - } - } - else - { - _logger.LogWarning($"角色[{trimmedRoleName}]不存在,无法为用户[{name}]分配"); - } - } - } - - // 创建系统用户 - var user = new User( - _guidGenerator.Create(), - name, - workUnitId, - identityUser.Id, - gender) - { - OrgId = orgId - }; - - // 保存用户 - await _userRepository.InsertAsync(user); - - _logger.LogInformation($"创建用户:{name},用户名:{userName}"); - } - else - { - _logger.LogInformation($"Identity用户[{userName}]已存在,检查是否需要创建业务用户"); - - // 检查是否需要创建业务用户 - var businessUser = await _userRepository.FindAsync(u => u.IdentityUserId == identityUser.Id); - if (businessUser == null) - { - // 创建系统用户 - var user = new User( - _guidGenerator.Create(), - name, - workUnitId, - identityUser.Id, - gender) - { - OrgId = orgId - }; - - // 保存用户 - await _userRepository.InsertAsync(user); - - _logger.LogInformation($"为已存在的Identity用户创建业务用户:{name}"); - } - } - } - } -} From db61240c234f7d85d4e7944bfe8263da639c1389 Mon Sep 17 00:00:00 2001 From: feijie Date: Fri, 21 Mar 2025 20:18:35 +0800 Subject: [PATCH 06/10] =?UTF-8?q?=E2=9C=A8=20feat(tests):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E7=94=A8=E6=88=B7=E5=BA=94=E7=94=A8=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=92=8C=E6=95=B0=E6=8D=AE=E7=A7=8D=E5=AD=90=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增用户应用服务单元测试,包含创建、查询、更新和删除等功能测试。同时增加数据种子初始化测试,验证角色和用户的创建。更新EF Core测试模块,使用PostgreSQL代替内存数据库,并添加数据种子。 --- ...yName.ProjectName.Application.Tests.csproj | 5 + .../DataSeeder/ProjectNameDataSeederTests.cs | 137 +++++++++ .../ProjectNameApplicationTestModule.cs | 54 +++- .../ProjectName/TestFileProvider.cs | 64 +++++ .../ProjectName/TestHostEnvironment.cs | 21 ++ .../ProjectName/Users/UserAppServiceTests.cs | 265 ++++++++++++++++++ ...ojectName.EntityFrameworkCore.Tests.csproj | 6 +- ...rojectNameEntityFrameworkCoreTestModule.cs | 62 +++- 8 files changed, 599 insertions(+), 15 deletions(-) create mode 100644 aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/DataSeeder/ProjectNameDataSeederTests.cs create mode 100644 aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/TestFileProvider.cs create mode 100644 aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/TestHostEnvironment.cs create mode 100644 aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/Users/UserAppServiceTests.cs diff --git a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName.CompanyName.ProjectName.Application.Tests.csproj b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName.CompanyName.ProjectName.Application.Tests.csproj index a661f6026..938edc57b 100644 --- a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName.CompanyName.ProjectName.Application.Tests.csproj +++ b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName.CompanyName.ProjectName.Application.Tests.csproj @@ -7,12 +7,17 @@ + + + + + diff --git a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/DataSeeder/ProjectNameDataSeederTests.cs b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/DataSeeder/ProjectNameDataSeederTests.cs new file mode 100644 index 000000000..dd8ff6727 --- /dev/null +++ b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/DataSeeder/ProjectNameDataSeederTests.cs @@ -0,0 +1,137 @@ +using Microsoft.Extensions.DependencyInjection; +using PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DataSeeder; +using PackageName.CompanyName.ProjectName.Users; +using Shouldly; +using System; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Identity; +using Volo.Abp.Uow; +using Xunit; + +namespace PackageName.CompanyName.ProjectName.DataSeeder +{ + /// + /// 数据种子初始化测试 + /// + [Collection("Database")] + public class ProjectNameDataSeederTests : ProjectNameApplicationTestBase + { + private readonly IProjectNameDataSeeder _inspectionDataSeeder; + private readonly IRepository _userRepository; + private readonly IIdentityRoleRepository _identityRoleRepository; + private readonly IIdentityUserRepository _identityUserRepository; + + public ProjectNameDataSeederTests() + { + _inspectionDataSeeder = GetRequiredService(); + _userRepository = GetRequiredService>(); + _identityRoleRepository = GetRequiredService(); + _identityUserRepository = GetRequiredService(); + } + + [Fact] + public async Task Should_Seed_Data_Successfully() + { + // Arrange + var context = new DataSeedContext(); + + // Act + await _inspectionDataSeeder.SeedAsync(context); + + // Assert - 使用单元工作方法包装所有数据库操作 + await WithUnitOfWorkAsync(async () => + { + // 测试角色 + var roles = await _identityRoleRepository.GetListAsync(); + roles.Count.ShouldBeGreaterThanOrEqualTo(7); // 至少应该有 7 个角色 + + var superAdminRole = await _identityRoleRepository.FindByNormalizedNameAsync("超级管理员".ToUpperInvariant()); + superAdminRole.ShouldNotBeNull(); + // 测试用户 + var users = await _userRepository.GetListAsync(); + users.Count.ShouldBeGreaterThanOrEqualTo(10); // 至少应该有 10 个用户 + + foreach (var user in users) + { + user.IdentityUserId.ShouldNotBe(Guid.Empty); + + var identityUser = await _identityUserRepository.GetAsync(user.IdentityUserId); + identityUser.ShouldNotBeNull(); + } + + return true; + }); + } + + [Theory] + [InlineData("超级管理员")] + [InlineData("普通用户")] + public async Task Should_Create_Roles(string roleName) + { + // Arrange + var context = new DataSeedContext(); + await _inspectionDataSeeder.SeedAsync(context); + + // Act & Assert - 使用单元工作方法包装 + await WithUnitOfWorkAsync(async () => + { + var role = await _identityRoleRepository.FindByNormalizedNameAsync(roleName.ToUpperInvariant()); + role.ShouldNotBeNull(); + role.Name.ShouldBe(roleName); + + return true; + }); + } + + [Theory] + [InlineData("testuser1")] + [InlineData("testuser2")] + public async Task Should_Create_Users(string nickName) + { + // Arrange + var context = new DataSeedContext(); + await _inspectionDataSeeder.SeedAsync(context); + + // Act & Assert - 使用单元工作方法包装 + await WithUnitOfWorkAsync(async () => + { + var users = await _userRepository.GetListAsync(); + var user = users.FirstOrDefault(u => u.NickName == nickName); + user.ShouldNotBeNull(); + user.NickName.ShouldBe(nickName); + user.IdentityUserId.ShouldNotBe(Guid.Empty); + + var identityUser = await _identityUserRepository.GetAsync(user.IdentityUserId); + identityUser.ShouldNotBeNull(); + identityUser.Name.ShouldBe(nickName); + + return true; + }); + } + + // 添加单元工作方法 + protected override Task WithUnitOfWorkAsync(Func> func) + { + return WithUnitOfWorkAsync(new AbpUnitOfWorkOptions(), func); + } + + // 可选:添加重载方法以支持更多场景 + protected async override Task WithUnitOfWorkAsync(AbpUnitOfWorkOptions options, Func> func) + { + using (var scope = ServiceProvider.CreateScope()) + { + var uowManager = scope.ServiceProvider.GetRequiredService(); + + using (var uow = uowManager.Begin(options)) + { + var result = await func(); + await uow.CompleteAsync(); + return result; + } + } + } + } +} diff --git a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/ProjectNameApplicationTestModule.cs b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/ProjectNameApplicationTestModule.cs index 7e9fa658a..e07d40f8e 100644 --- a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/ProjectNameApplicationTestModule.cs +++ b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/ProjectNameApplicationTestModule.cs @@ -1,11 +1,63 @@ +using Hangfire; +using Hangfire.MemoryStorage; +using LINGYUN.Abp.Identity; +using LINGYUN.Abp.Identity.Session; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DataSeeder; +using PackageName.CompanyName.ProjectName.EntityFrameworkCore; using Volo.Abp.Modularity; +using Volo.Abp.PermissionManagement.Identity; +using Volo.Abp.Security.Claims; namespace PackageName.CompanyName.ProjectName; [DependsOn( typeof(ProjectNameDomainTestModule), - typeof(ProjectNameApplicationModule) + typeof(ProjectNameApplicationModule), + typeof(AbpIdentityApplicationModule), + typeof(AbpPermissionManagementDomainIdentityModule), + typeof(ProjectNameEntityFrameworkCoreTestModule) )] public class ProjectNameApplicationTestModule : AbpModule { + public override void ConfigureServices(ServiceConfigurationContext context) + { + // //设置ILogger为NullLogger + context.Services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); + context.Services.AddTransient(); + context.Services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); + context.Services.AddTransient(); + context.Services.AddTransient(); + + // 增加配置文件定义,在新建租户时需要 + Configure(options => + { + // 允许中文用户名 + options.User.AllowedUserNameCharacters = null; + // 支持弱密码 + options.Password.RequireDigit = false; + options.Password.RequiredLength = 1; + options.Password.RequireLowercase = false; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = false; + }); + Configure(options => + { + options.IsDynamicClaimsEnabled = true; + }); + Configure(options => + { + options.IsCleanupEnabled = true; + }); + // 配置Hangfire + context.Services.AddHangfire(config => + { + config.UseMemoryStorage(); + }); + } } diff --git a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/TestFileProvider.cs b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/TestFileProvider.cs new file mode 100644 index 000000000..c7a075cb2 --- /dev/null +++ b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/TestFileProvider.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Primitives; +using System; +using System.Collections.Generic; +using System.IO; + +namespace PackageName.CompanyName.ProjectName; + +public class TestFileProvider : IFileProvider +{ + private readonly Dictionary _files; + + public TestFileProvider() + { + _files = new Dictionary(); + } + + public IDirectoryContents GetDirectoryContents(string subpath) + { + return new NotFoundDirectoryContents(); + } + + public IFileInfo GetFileInfo(string subpath) + { + if (_files.TryGetValue(subpath, out var fileInfo)) + { + return fileInfo; + } + return new NotFoundFileInfo(subpath); + } + + public IChangeToken Watch(string filter) + { + return NullChangeToken.Singleton; + } + + public void AddFile(string path, string contents) + { + _files[path] = new TestFileInfo(path, contents); + } +} + +public class TestFileInfo : IFileInfo +{ + private readonly string _contents; + + public TestFileInfo(string name, string contents) + { + Name = name; + _contents = contents; + } + + public bool Exists => true; + public long Length => _contents.Length; + public string PhysicalPath => null; + public string Name { get; } + public DateTimeOffset LastModified => DateTimeOffset.UtcNow; + public bool IsDirectory => false; + + public Stream CreateReadStream() + { + return new MemoryStream(System.Text.Encoding.UTF8.GetBytes(_contents)); + } +} \ No newline at end of file diff --git a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/TestHostEnvironment.cs b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/TestHostEnvironment.cs new file mode 100644 index 000000000..ae140f85d --- /dev/null +++ b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/TestHostEnvironment.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; +using System; + +namespace PackageName.CompanyName.ProjectName; + +public class TestHostEnvironment : IHostEnvironment +{ + public TestHostEnvironment() + { + EnvironmentName = "Test"; + ApplicationName = "TestApplication"; + ContentRootPath = AppDomain.CurrentDomain.BaseDirectory; + ContentRootFileProvider = new PhysicalFileProvider(ContentRootPath); + } + + public string EnvironmentName { get; set; } + public string ApplicationName { get; set; } + public string ContentRootPath { get; set; } + public IFileProvider ContentRootFileProvider { get; set; } +} \ No newline at end of file diff --git a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/Users/UserAppServiceTests.cs b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/Users/UserAppServiceTests.cs new file mode 100644 index 000000000..212d79fe2 --- /dev/null +++ b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/Users/UserAppServiceTests.cs @@ -0,0 +1,265 @@ +using PackageName.CompanyName.ProjectName.Users.Dtos; +using Shouldly; +using System; +using System.Threading.Tasks; +using Volo.Abp.Domain.Entities; +using Volo.Abp.Validation; +using Xunit; + +namespace PackageName.CompanyName.ProjectName.Users +{ + /// + /// UserAppService 的单元测试 + /// + [Collection("Database")] + public class UserAppServiceTests : ProjectNameApplicationTestBase + { + private readonly IUserAppService _userAppService; + private readonly IUserManager _userManager; + + public UserAppServiceTests() + { + _userAppService = GetRequiredService(); + _userManager = GetRequiredService(); + } + + [Theory] + [InlineData("testuser1", "Test123456!", true)] + [InlineData("testuser2", "Test123456!", false)] + public async Task Should_Create_User( + string nickName, + string password, + bool isActive) + { + // Arrange + var input = new CreateUpdateUserDto + { + NickName = nickName, + Password = password, + IsActive = isActive + }; + + // Act + var result = await _userAppService.CreateAsync(input); + + // Assert + result.ShouldNotBeNull(); + result.NickName.ShouldBe(nickName); + result.IsActive.ShouldBe(isActive); + } + + [Theory] + [InlineData("", "Test123456!", "用户名称不能为空")] + [InlineData("test", "123", "密码长度必须在6-20个字符之间")] + public async Task Should_Not_Create_User_With_Invalid_Input(string nickName, string password, + string expectedErrorMessage) + { + // Arrange + var input = new CreateUpdateUserDto + { + NickName = nickName, + Password = password + }; + + // Act & Assert + var exception = await Assert.ThrowsAsync(async () => + { + await _userAppService.CreateAsync(input); + }); + + exception.ValidationErrors.ShouldContain(x => x.ErrorMessage.Contains(expectedErrorMessage)); + } + + [Fact] + public async Task Should_Get_User_List() + { + // Arrange + await CreateTestUserAsync("testuser1", "Test123456!"); + await CreateTestUserAsync("testuser2", "Test123456!"); + + // Act + var result = await _userAppService.GetListAsync( + new UserPagedAndSortedResultRequestDto + { + MaxResultCount = 10, + SkipCount = 0, + Sorting = "NickName" + }); + + // Assert + result.ShouldNotBeNull(); + result.TotalCount.ShouldBeGreaterThanOrEqualTo(2); + result.Items.ShouldContain(x => x.NickName == "testuser1"); + result.Items.ShouldContain(x => x.NickName == "testuser2"); + } + + [Fact] + public async Task Should_Filter_Users_By_NickName() + { + // Arrange + await CreateTestUserAsync("testuser1", "Test123456!"); + await CreateTestUserAsync("testuser2", "Test123456!"); + await CreateTestUserAsync("otheruser", "Test123456!"); + + // Act + var result = await _userAppService.GetListAsync( + new UserPagedAndSortedResultRequestDto + { + MaxResultCount = 10, + SkipCount = 0, + Sorting = "NickName", + NickName = "testuser" + }); + + // Assert + result.ShouldNotBeNull(); + result.TotalCount.ShouldBe(2); + result.Items.ShouldContain(x => x.NickName == "testuser1"); + result.Items.ShouldContain(x => x.NickName == "testuser2"); + result.Items.ShouldNotContain(x => x.NickName == "otheruser"); + } + + [Fact] + public async Task Should_Update_User() + { + // Arrange + var user = await CreateTestUserAsync("updatetest", "Test123456!"); + var updateInput = new CreateUpdateUserDto + { + NickName = "updateduser", + Password = "NewPassword123!", + ContactInfo = "13800138000", + Position = "开发工程师", + IsActive = true + }; + + // Act + var result = await _userAppService.UpdateAsync(user.Id, updateInput); + + // Assert + result.ShouldNotBeNull(); + result.NickName.ShouldBe("updateduser"); + result.ContactInfo.ShouldBe("13800138000"); + result.Position.ShouldBe("开发工程师"); + + // 验证更新后的用户信息 + var updatedUser = await _userAppService.GetAsync(user.Id); + updatedUser.NickName.ShouldBe("updateduser"); + } + + [Fact] + public async Task Should_Not_Update_Non_Existing_User() + { + // Arrange + var input = new CreateUpdateUserDto + { + NickName = "testuser", + Password = "Test123456!" + }; + + // Act & Assert + await Assert.ThrowsAsync(async () => + { + await _userAppService.UpdateAsync(Guid.NewGuid(), input); + }); + } + + [Fact] + public async Task Should_Delete_User() + { + // Arrange + var user = await CreateTestUserAsync("deletetest", "Test123456!"); + + // Act + await _userAppService.DeleteAsync(user.Id); + + // Assert - 尝试获取已删除的用户应该抛出异常 + await Assert.ThrowsAsync(async () => + { + await _userAppService.GetAsync(user.Id); + }); + } + + [Fact] + public async Task Should_Change_User_Password() + { + // Arrange + var user = await CreateTestUserAsync("passwordtest", "OldPassword123!"); + + // Act & Assert + await _userAppService.ChangePasswordAsync(user.Id, "OldPassword123!", "NewPassword123!"); + + // 尝试用新密码登录(这个需要集成测试才能完整测试) + // 这里我们只是验证方法执行不会抛出异常 + } + + [Fact] + public async Task Should_Reset_User_Password() + { + // Arrange + var user = await CreateTestUserAsync("resetpasswordtest", "OldPassword123!"); + + // Act & Assert + await _userAppService.ResetPasswordAsync(user.Id, "NewPassword123!"); + + // 同样,完整测试需要验证用户能用新密码登录,这需要集成测试 + } + + [Fact] + public async Task Should_Set_User_Active_Status() + { + // Arrange + var user = await CreateTestUserAsync("activestatustest", "Password123!"); + + // Act + await _userAppService.SetUserActiveStatusAsync(user.Id, false); + var disabledUser = await _userAppService.GetAsync(user.Id); + + await _userAppService.SetUserActiveStatusAsync(user.Id, true); + var enabledUser = await _userAppService.GetAsync(user.Id); + + // Assert + disabledUser.IsActive.ShouldBeFalse(); + enabledUser.IsActive.ShouldBeTrue(); + } + + [Theory] + [InlineData("13900000000", "工程师")] + [InlineData("13800000000", "设计师")] + [InlineData(null, null)] + public async Task Should_Create_User_With_Optional_Fields(string contactInfo, string position) + { + // Arrange + var input = new CreateUpdateUserDto + { + NickName = $"user_{Guid.NewGuid():N}", + Password = "Test123456!", + ContactInfo = contactInfo, + Position = position + }; + + // Act + var result = await _userAppService.CreateAsync(input); + + // Assert + result.ShouldNotBeNull(); + result.ContactInfo.ShouldBe(contactInfo); + result.Position.ShouldBe(position); + } + + private async Task CreateTestUserAsync(string nickName, string password) + { + return await WithUnitOfWorkAsync(async () => + { + var input = new CreateUpdateUserDto + { + NickName = nickName, + Password = password, + IsActive = true + }; + + return await _userAppService.CreateAsync(input); + }); + } + } +} \ No newline at end of file diff --git a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests.csproj b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests.csproj index 0a3e9f02c..944bbe75a 100644 --- a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests.csproj +++ b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests.csproj @@ -8,10 +8,14 @@ - + + + + + diff --git a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameEntityFrameworkCoreTestModule.cs b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameEntityFrameworkCoreTestModule.cs index da8ce7c6c..b5fff765e 100644 --- a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameEntityFrameworkCoreTestModule.cs +++ b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameEntityFrameworkCoreTestModule.cs @@ -1,38 +1,74 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore; using System; +using Volo.Abp; +using Volo.Abp.Data; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.Modularity; +using Volo.Abp.Threading; +using Volo.Abp.Timing; using Volo.Abp.Uow; namespace PackageName.CompanyName.ProjectName.EntityFrameworkCore; [DependsOn( typeof(ProjectNameTestBaseModule), - typeof(ProjectNameEntityFrameworkCoreModule) - )] + typeof(ProjectNameEntityFrameworkCoreModule), + typeof(SingleMigrationsEntityFrameworkCoreModule) +)] public class ProjectNameEntityFrameworkCoreTestModule : AbpModule { + // 数据库配置 + private const string DefaultPostgresConnectionString = + "Host=127.0.0.1;Port=5432;Database=test_db;User Id=postgres;Password=postgres;"; + public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddEntityFrameworkInMemoryDatabase(); + var connectionString = Environment.GetEnvironmentVariable("TEST_CONNECTION_STRING") ?? + DefaultPostgresConnectionString; - var databaseName = Guid.NewGuid().ToString(); + // 配置数据库连接字符串 + Configure(options => + { + options.ConnectionStrings.Default = connectionString; + }); - Configure(options => + Configure(options => { options.Kind = DateTimeKind.Utc; }); + context.Services.AddAbpDbContext(options => { - options.Configure(abpDbContextConfigurationContext => - { - abpDbContextConfigurationContext.DbContextOptions.EnableDetailedErrors(); - abpDbContextConfigurationContext.DbContextOptions.EnableSensitiveDataLogging(); + options.AddDefaultRepositories(true); + }); - abpDbContextConfigurationContext.DbContextOptions.UseInMemoryDatabase(databaseName); - }); + // 配置所有DbContext + Configure(options => + { + AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); + options.UseNpgsql(); }); Configure(options => { - options.TransactionBehavior = UnitOfWorkTransactionBehavior.Disabled; //EF in-memory database does not support transactions + options.TransactionBehavior = UnitOfWorkTransactionBehavior.Disabled; }); } -} + + public override void OnPreApplicationInitialization(ApplicationInitializationContext context) + { + var dbContext = context.ServiceProvider.GetRequiredService(); + // 重置数据库 + dbContext.Database.EnsureDeleted(); + // // 创建数据库 + dbContext.Database.EnsureCreated(); + dbContext.Database.GenerateCreateScript(); + // dbContext.Database.Migrate(); + + // 初始化种子数据 + var dataSeeder = context.ServiceProvider.GetRequiredService(); + AsyncHelper.RunSync(() => dataSeeder.SeedAsync()); + } + + public override void OnApplicationShutdown(ApplicationShutdownContext context) + { + } +} \ No newline at end of file From 4dea327cb75b40c43c94b671f18b4938c2625765 Mon Sep 17 00:00:00 2001 From: feijie Date: Fri, 21 Mar 2025 21:04:38 +0800 Subject: [PATCH 07/10] =?UTF-8?q?=E2=9C=A8=20feat(UserAppService):=20?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E6=9B=B4=E6=96=B0=E7=94=A8=E6=88=B7=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E4=B8=8EDTO=E4=B8=80=E8=87=B4,=20=E5=A1=AB=E5=85=85?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🐛 fix(UserManager): 修复删除用户时的并发冲突问题, 增加重试机制 ✨ feat(ProjectNameApplicationTestModule): 添加默认的双因素令牌提供者配置 ✨ feat(Migrate.ps1): 动态查找所有可用的DbContext, 提升灵活性 --- .../aio/content/migrations/Migrate.ps1 | 73 ++++++++++++++++-- .../ProjectName/Users/UserAppService.cs | 23 ++++-- .../ProjectName/Users/UserManager.cs | 77 +++++++++++++++---- .../ProjectNameApplicationTestModule.cs | 2 + 4 files changed, 147 insertions(+), 28 deletions(-) diff --git a/aspnet-core/templates/aio/content/migrations/Migrate.ps1 b/aspnet-core/templates/aio/content/migrations/Migrate.ps1 index b983af60a..dd0543c2c 100755 --- a/aspnet-core/templates/aio/content/migrations/Migrate.ps1 +++ b/aspnet-core/templates/aio/content/migrations/Migrate.ps1 @@ -9,15 +9,76 @@ $env:FROM_MIGRATION = "true" # 定义项目路径 $projectPath = Resolve-Path (Join-Path $PSScriptRoot "..") -# 定义可用的DbContext -$dbContexts = @{ - "1" = @{ - Name = "PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DatabaseManagementName" - Context = "SingleMigrationsDbContext" - Factory = "SingleMigrationsDbContextFactory" +# 定义函数来动态查找所有可用的DbContext +function Get-AvailableDbContexts { + $migrationsPath = Join-Path $projectPath "migrations" + $dbContexts = @{} + $counter = 1 + + # 查找所有包含EntityFrameworkCore的目录 + $efCoreDirectories = Get-ChildItem -Path $migrationsPath -Directory | Where-Object { $_.Name -like "*EntityFrameworkCore*" } + + foreach ($dir in $efCoreDirectories) { + # 优先查找 DbContextFactory 文件 + $factoryFiles = Get-ChildItem -Path $dir.FullName -Filter "*DbContextFactory.cs" -Recurse -File + + # 如果找到了 Factory 文件 + $foundFactory = $false + foreach ($factoryFile in $factoryFiles) { + $factoryContent = Get-Content $factoryFile.FullName -Raw + + # 查找 Factory 类名和对应的 DbContext 类 + if ($factoryContent -match 'class\s+(\w+Factory)\s*:\s*IDesignTimeDbContextFactory<(\w+)>') { + $factoryName = $matches[1] + $contextName = $matches[2] + + # 如果找到了上下文和工厂,添加到列表中 + $dbContexts["$counter"] = @{ + Name = $dir.Name + Context = $contextName + Factory = $factoryName + } + $counter++ + $foundFactory = $true + } + } + + # 只有当没有找到 Factory 时,才查找 DbContext 文件作为备选 + if (-not $foundFactory) { + $dbContextFiles = Get-ChildItem -Path $dir.FullName -Filter "*DbContext.cs" -Recurse -File + + foreach ($contextFile in $dbContextFiles) { + $contextContent = Get-Content $contextFile.FullName -Raw + if ($contextContent -match 'class\s+(\w+DbContext)') { + $contextName = $matches[1] + + # 添加到列表中,但没有对应的 Factory + $dbContexts["$counter"] = @{ + Name = $dir.Name + Context = $contextName + Factory = $null + } + $counter++ + } + } + } + } + + # 如果没有找到任何上下文,使用默认的 + if ($dbContexts.Count -eq 0) { + $dbContexts["1"] = @{ + Name = "PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DatabaseManagementName" + Context = "SingleMigrationsDbContext" + Factory = "SingleMigrationsDbContextFactory" + } } + + return $dbContexts } +# 获取可用的DbContext +$dbContexts = Get-AvailableDbContexts + # 显示DbContext选择菜单 function Show-DbContextMenu { $host.UI.RawUI.BackgroundColor = "Black" diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/Users/UserAppService.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/Users/UserAppService.cs index 25eef3a6e..ff2b0c031 100644 --- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/Users/UserAppService.cs +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/Users/UserAppService.cs @@ -75,6 +75,17 @@ namespace PackageName.CompanyName.ProjectName.Users input.Position, input.IsActive); + // 确保更新后的用户状态与DTO中的一致 + if (user.IdentityUser != null) + { + bool currentIsActive = user.IdentityUser.LockoutEnd == null || user.IdentityUser.LockoutEnd < DateTimeOffset.Now; + if (currentIsActive != input.IsActive) + { + await _userManager.SetUserActiveStatusAsync(id, input.IsActive); + user = await _userManager.GetAsync(id); + } + } + // 返回DTO对象 return await MapToUserDtoAsync(user); } @@ -121,7 +132,7 @@ namespace PackageName.CompanyName.ProjectName.Users foreach (var user in users) { var userDto = ObjectMapper.Map(user); - + // 填充角色信息 if (user.IdentityUser != null) { @@ -129,7 +140,7 @@ namespace PackageName.CompanyName.ProjectName.Users userDto.RoleNames = string.Join("、", roles); userDto.IsActive = user.IdentityUser.LockoutEnd == null || user.IdentityUser.LockoutEnd < DateTimeOffset.Now; } - + userDtos.Add(userDto); } @@ -182,17 +193,19 @@ namespace PackageName.CompanyName.ProjectName.Users /// /// 将User实体映射为UserDto,并填充权限信息 /// - private Task MapToUserDtoAsync(User user) + private async Task MapToUserDtoAsync(User user) { var userDto = ObjectMapper.Map(user); - // 设置用户状态 + // 设置用户状态和角色信息 if (user.IdentityUser != null) { + var roles = await _identityUserManager.GetRolesAsync(user.IdentityUser); + userDto.RoleNames = string.Join("、", roles); userDto.IsActive = user.IdentityUser.LockoutEnd == null || user.IdentityUser.LockoutEnd < DateTimeOffset.Now; } - return Task.FromResult(userDto); + return userDto; } } } \ No newline at end of file diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/UserManager.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/UserManager.cs index bd616ba34..08ccdba57 100644 --- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/UserManager.cs +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/UserManager.cs @@ -81,6 +81,17 @@ namespace PackageName.CompanyName.ProjectName.Users string.Join(", ", lockoutResult.Errors.Select(e => e.Description))); } } + else + { + // 确保用户处于活动状态 + await _identityUserManager.SetLockoutEnabledAsync(identityUser, false); + var lockoutResult = await _identityUserManager.SetLockoutEndDateAsync(identityUser, null); + if (!lockoutResult.Succeeded) + { + throw new UserFriendlyException("设置用户状态失败: " + + string.Join(", ", lockoutResult.Errors.Select(e => e.Description))); + } + } // 创建业务用户 var user = new User( @@ -221,29 +232,61 @@ namespace PackageName.CompanyName.ProjectName.Users /// 操作任务 public async Task DeleteAsync(Guid id) { - // 获取用户 - var user = await _userRepository.GetAsync(id); - if (user == null) - { - throw new UserFriendlyException("用户不存在"); - } + // 最大重试次数 + const int maxRetries = 3; + int retryCount = 0; - // 删除Identity用户 - var identityUser = await _identityUserManager.FindByIdAsync(user.IdentityUserId.ToString()); - if (identityUser != null) + while (true) { - var result = await _identityUserManager.DeleteAsync(identityUser); - if (!result.Succeeded) + try { - throw new UserFriendlyException("删除Identity用户失败: " + - string.Join(", ", result.Errors.Select(e => e.Description))); + // 每次尝试时重新获取最新的用户数据 + var user = await _userRepository.GetAsync(id); + if (user == null) + { + _logger.LogWarning($"尝试删除不存在的用户,ID:{id}"); + return; // 如果用户不存在,就不再继续处理 + } + + // 删除Identity用户 + var identityUser = await _identityUserManager.FindByIdAsync(user.IdentityUserId.ToString()); + if (identityUser != null) + { + var result = await _identityUserManager.DeleteAsync(identityUser); + if (!result.Succeeded) + { + throw new UserFriendlyException("删除Identity用户失败: " + + string.Join(", ", result.Errors.Select(e => e.Description))); + } + } + + // 删除用户 + await _userRepository.DeleteAsync(user); + + _logger.LogInformation($"删除了用户,ID:{id}"); + return; } - } + catch (Volo.Abp.Data.AbpDbConcurrencyException ex) + { + // 增加重试计数 + retryCount++; - // 删除用户 - await _userRepository.DeleteAsync(user); + // 如果达到最大重试次数,则抛出用户友好的异常 + if (retryCount >= maxRetries) + { + throw new UserFriendlyException( + "删除用户失败:数据已被其他用户修改。请刷新页面后重试。", + "409", ex.Message, + ex); + } - _logger.LogInformation($"删除了用户,ID:{id}"); + // 短暂延迟后重试 + await Task.Delay(100 * retryCount); // 逐步增加延迟时间 + + // 记录重试信息 + _logger.LogWarning($"检测到用户[{id}]删除时发生并发冲突,正在进行第{retryCount}次重试..."); + } + } } /// diff --git a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/ProjectNameApplicationTestModule.cs b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/ProjectNameApplicationTestModule.cs index e07d40f8e..e9ac52aca 100644 --- a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/ProjectNameApplicationTestModule.cs +++ b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/ProjectNameApplicationTestModule.cs @@ -45,6 +45,8 @@ public class ProjectNameApplicationTestModule : AbpModule options.Password.RequireLowercase = false; options.Password.RequireNonAlphanumeric = false; options.Password.RequireUppercase = false; + // 添加默认的双因素令牌提供者配置 + options.Tokens.ProviderMap.Add("Default", new TokenProviderDescriptor(typeof(EmailTokenProvider))); }); Configure(options => { From 72b7d192c9c3ac3f6f583ea692278a7631f5d04b Mon Sep 17 00:00:00 2001 From: feijie Date: Fri, 21 Mar 2025 21:24:08 +0800 Subject: [PATCH 08/10] =?UTF-8?q?=20docs(README.md):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=20README=EF=BC=8C=E6=8F=90=E4=BE=9B=E6=9B=B4=E6=B8=85=E6=99=B0?= =?UTF-8?q?=E7=9A=84=E5=BF=AB=E9=80=9F=E5=90=AF=E5=8A=A8=E6=8C=87=E5=8D=97?= =?UTF-8?q?=E5=92=8C=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aspnet-core/templates/aio/content/README.md | 170 +++++++++--------- .../templates/aio/content/README.zh-CN.md | 170 +++++++++--------- 2 files changed, 168 insertions(+), 172 deletions(-) diff --git a/aspnet-core/templates/aio/content/README.md b/aspnet-core/templates/aio/content/README.md index 7d7b52ed3..65291e29c 100644 --- a/aspnet-core/templates/aio/content/README.md +++ b/aspnet-core/templates/aio/content/README.md @@ -1,135 +1,133 @@ -# LINGYUN.Abp.Templates +# PackageName.CompanyName.ProjectName [English](README.md) | [中文](README.zh-CN.md) -## Introduction +## Quick Start Guide -LINGYUN.Abp.Templates provides two types of project templates based on ABP Framework: +This guide will help you quickly set up and run the project. Follow these steps to get started. -1. **Microservice Template**: A complete microservice architecture template with distributed services. -2. **All-in-One Template**: A single-application template that combines all services into one project. +### Prerequisites -## Features +- .NET SDK 9.0 or higher +- A supported database (SQL Server, MySQL, PostgreSQL, Oracle, or SQLite) +- PowerShell 7.0+ (recommended for running migration scripts) -### Common Features +### Step 1: Restore and Build the Project -- Integrated authentication (IdentityServer4/OpenIddict) -- Database integration (multiple databases supported) -- Unified configuration management -- Distributed event bus support -- Background job processing - -### Microservice Template Features +```bash +# Navigate to the project root directory +cd /path/to/project -- Complete microservice project structure -- Service discovery and registration -- Distributed deployment support +# Restore dependencies +dotnet restore -### All-in-One Template Features +# Build the solution +dotnet build +``` -- Simplified deployment -- Easier maintenance -- Lower resource requirements +### Step 2: Create Database Schema -## How to Use +Use the Migrate.ps1 script to create the database tables structure: -### Install labp CLI Tool +```powershell +# Navigate to the migrations directory +cd migrations -```bash -dotnet tool install --global LINGYUN.Abp.Cli +# Run the migration script +./Migrate.ps1 ``` -### Install Templates +The script will: + +1. Detect available DbContext classes in the project +2. Ask you to select which DbContext to use for migration +3. Prompt for a migration name +4. Create the migration +5. Optionally generate SQL scripts for the migration + +### Step 3: Initialize Seed Data + +Run the DbMigrator project to initialize seed data: ```bash -# Install Microservice Template -dotnet new install LINGYUN.Abp.MicroService.Templates +# Navigate to the DbMigrator project directory +cd migrations/PackageName.CompanyName.ProjectName.AIO.DbMigrator -# Install All-in-One Template -dotnet new install LINGYUN.Abp.AllInOne.Templates +# Run the DbMigrator project +dotnet run ``` -### Create New Project +The DbMigrator will: -#### For Microservice Project +1. Apply all database migrations +2. Seed initial data (users, roles, etc.) +3. Set up tenant configurations if applicable -```bash -# Short name: lam (LINGYUN Abp Microservice) -labp create YourCompanyName.YourProjectName -pk YourPackageName -t lam -o /path/to/output --dbms MySql --cs "Server=127.0.0.1;Database=Platform-V70;User Id=root;Password=123456;SslMode=None" --no-random-port -``` +### Step 4: Launch the Application -#### For All-in-One Project +After successfully setting up the database, you can run the host project: ```bash -# Short name: laa (LINGYUN Abp AllInOne) -labp create YourCompanyName.YourProjectName -pk YourPackageName -t laa -o /path/to/output --dbms MySql --cs "Server=127.0.0.1;Database=Platform-V70;User Id=root;Password=123456;SslMode=None" --no-random-port +# Navigate to the host project directory +cd host/PackageName.CompanyName.ProjectName.AIO.Host + +# Run the host project +dotnet run --launch-profile "PackageName.CompanyName.ProjectName.Development" ``` -## How to Run +The application will start and be accessible at the configured URL (typically [https://localhost:44300](https://localhost:44300)). -After creating your project, you can run it using the following command: +## Database-based Unit Testing -### For Microservice Project +To run database-based unit tests, follow these steps: -```bash -cd /path/to/output/host/YourPackageName.YourCompanyName.YourProjectName.HttpApi.Host -dotnet run --launch-profile "YourPackageName.YourCompanyName.YourProjectName.Development" -``` +### Step 1: Prepare Test Database -### For All-in-One Project +Before running tests, make sure the test database exists. The test database connection string is defined in the `ProjectNameEntityFrameworkCoreTestModule.cs` file. -```bash -cd /path/to/output/host/YourPackageName.YourCompanyName.YourProjectName.AIO.Host -dotnet run --launch-profile "YourPackageName.YourCompanyName.YourProjectName.Development" -``` +The default connection string is: -## How to Package and Publish +```csharp +private const string DefaultPostgresConnectionString = + "Host=127.0.0.1;Port=5432;Database=test_db;User Id=postgres;Password=postgres;"; +``` -1. Clone the Project +You can either create this database manually or modify the connection string to use an existing database. -```bash -git clone -cd /aspnet-core/templates/content -``` +### Step 2: Configure Test Environment -2. Modify Version - Edit the project files to update versions: - - For Microservice: `../PackageName.CompanyName.ProjectName.csproj` - - For All-in-One: `../PackageName.CompanyName.ProjectName.AIO.csproj` +Modify the connection string in `ProjectNameEntityFrameworkCoreTestModule.cs` if needed: -```xml -8.3.0 +```csharp +// You can also set an environment variable to override the default connection string +var connectionString = Environment.GetEnvironmentVariable("TEST_CONNECTION_STRING") ?? + DefaultPostgresConnectionString; ``` -3. Execute Packaging Script +### Step 3: Run Tests -```powershell -# Windows PowerShell -.\pack.ps1 +Run the Application.Tests project: + +```bash +# Navigate to the test project directory +cd tests/PackageName.CompanyName.ProjectName.Application.Tests -# PowerShell Core (Windows/Linux/macOS) -pwsh pack.ps1 +# Run the tests +dotnet test ``` -The script will prompt you to choose which template to package: +The test framework will: -1. Microservice Template -2. All-in-One Template -3. Both Templates +1. Create a clean test database environment +2. Run all unit tests +3. Report test results -## Supported Databases +## Note About Naming -- SqlServer -- MySQL -- PostgreSQL -- Oracle -- SQLite +This is a template project, so all project names contain placeholders that will be replaced when the template is used to create a new project: -## Notes +- `PackageName` will be replaced with your package name +- `CompanyName` will be replaced with your company name +- `ProjectName` will be replaced with your project name -- Ensure .NET SDK 8.0 or higher is installed -- Choose the appropriate template based on your needs: - - Microservice Template: For large-scale distributed applications - - All-in-One Template: For smaller applications or simpler deployment requirements -- Pay attention to NuGet publish address and key when packaging -- Complete testing is recommended before publishing +When creating a new project from this template, you'll specify these values and they'll be substituted throughout the entire solution. diff --git a/aspnet-core/templates/aio/content/README.zh-CN.md b/aspnet-core/templates/aio/content/README.zh-CN.md index ae492e5b8..fff301c57 100644 --- a/aspnet-core/templates/aio/content/README.zh-CN.md +++ b/aspnet-core/templates/aio/content/README.zh-CN.md @@ -1,135 +1,133 @@ -# LINGYUN.Abp.Templates +# PackageName.CompanyName.ProjectName [English](README.md) | [中文](README.zh-CN.md) -## 简介 +## 快速启动指南 -LINGYUN.Abp.Templates 基于 ABP Framework 提供两种项目模板: +本指南将帮助您快速设置和运行项目。请按照以下步骤开始。 -1. **微服务模板**:完整的分布式微服务架构模板 -2. **单体应用模板**:将所有服务集成到一个项目中的单体应用模板 +### 前提条件 -## 特性 +- .NET SDK 9.0 或更高版本 +- 支持的数据库(SQL Server、MySQL、PostgreSQL、Oracle 或 SQLite) +- PowerShell 7.0+(推荐用于运行迁移脚本) -### 共同特性 +### 第一步:还原和构建项目 -- 集成身份认证(支持 IdentityServer4/OpenIddict) -- 数据库集成(支持多种数据库) -- 统一配置管理 -- 分布式事件总线支持 -- 后台作业处理 +```bash +# 导航到项目根目录 +cd /path/to/project -### 微服务模板特性 +# 还原依赖项 +dotnet restore -- 完整的微服务项目结构 -- 服务发现与注册 -- 支持分布式部署 +# 构建解决方案 +dotnet build +``` -### 单体应用模板特性 +### 第二步:创建数据库结构 -- 简化的部署流程 -- 更容易的维护 -- 更低的资源需求 +使用 Migrate.ps1 脚本创建数据库表结构: -## 使用方法 +```powershell +# 导航到 migrations 目录 +cd migrations -### 安装模板 +# 运行迁移脚本 +./Migrate.ps1 +``` -```bash -# 安装微服务模板:lam -dotnet new install LINGYUN.Abp.MicroService.Templates +该脚本将: -# 安装单体应用模板:laa -dotnet new install LINGYUN.Abp.AllInOne.Templates -``` +1. 检测项目中可用的 DbContext 类 +2. 要求您选择用于迁移的 DbContext +3. 提示输入迁移名称 +4. 创建迁移 +5. 可选地为迁移生成 SQL 脚本 -### 安装 labp 命令行工具 +### 第三步:初始化种子数据 + +运行 DbMigrator 项目来初始化种子数据: ```bash - dotnet tool install --global LINGYUN.Abp.Cli +# 导航到 DbMigrator 项目目录 +cd migrations/PackageName.CompanyName.ProjectName.AIO.DbMigrator + +# 运行 DbMigrator 项目 +dotnet run ``` -### 创建新项目 +DbMigrator 将: -#### 创建微服务项目 +1. 应用所有数据库迁移 +2. 初始化种子数据(用户、角色等) +3. 如适用,设置租户配置 -```bash -# 简写名称:lam (LINGYUN Abp Microservice) -labp create YourCompanyName.YourProjectName -pk YourPackageName -t lam -o /path/to/output --dbms MySql --cs "Server=127.0.0.1;Database=Platform-V70;User Id=root;Password=123456;SslMode=None" --no-random-port -``` +### 第四步:启动应用程序 -#### 创建单体应用项目 +成功设置数据库后,您可以运行 host 项目: ```bash -# 简写名称:laa (LINGYUN Abp AllInOne) -labp create YourCompanyName.YourProjectName -pk YourPackageName -t laa -o /path/to/output --dbms MySql --cs "Server=127.0.0.1;Database=Platform-V70;User Id=root;Password=123456;SslMode=None" --no-random-port +# 导航到 host 项目目录 +cd host/PackageName.CompanyName.ProjectName.AIO.Host + +# 运行 host 项目 +dotnet run --launch-profile "PackageName.CompanyName.ProjectName.Development" ``` -## 运行项目 +应用程序将启动并可通过配置的 URL 访问(通常是 [https://localhost:44300](https://localhost:44300))。 -创建项目后,可以使用以下命令运行: +## 基于数据库的单元测试 -### 运行微服务项目 +要运行基于数据库的单元测试,请按照以下步骤操作: -```bash -cd /path/to/output/host/YourPackageName.YourCompanyName.YourProjectName.HttpApi.Host -dotnet run --launch-profile "YourPackageName.YourCompanyName.YourProjectName.Development" -``` +### 第一步:准备测试数据库 -### 运行单体应用项目 +在运行测试之前,确保测试数据库存在。测试数据库连接字符串在 `ProjectNameEntityFrameworkCoreTestModule.cs` 文件中定义。 -```bash -cd /path/to/output/host/YourPackageName.YourCompanyName.YourProjectName.AIO.Host -dotnet run --launch-profile "YourPackageName.YourCompanyName.YourProjectName.Development" -``` +默认连接字符串是: -## 打包与发布 +```csharp +private const string DefaultPostgresConnectionString = + "Host=127.0.0.1;Port=5432;Database=test_db;User Id=postgres;Password=postgres;"; +``` -1. 克隆项目 +您可以手动创建此数据库或修改连接字符串以使用现有数据库。 -```bash -git clone -cd /aspnet-core/templates/content -``` +### 第二步:配置测试环境 -2. 修改版本号 - 编辑项目文件更新版本号: - - 微服务模板:`../PackageName.CompanyName.ProjectName.csproj` - - 单体应用模板:`../PackageName.CompanyName.ProjectName.AIO.csproj` +如需修改 `ProjectNameEntityFrameworkCoreTestModule.cs` 中的连接字符串: -```xml -8.3.0 +```csharp +// 您也可以设置环境变量来覆盖默认连接字符串 +var connectionString = Environment.GetEnvironmentVariable("TEST_CONNECTION_STRING") ?? + DefaultPostgresConnectionString; ``` -3. 执行打包脚本 +### 第三步:运行测试 -```powershell -# Windows PowerShell -.\pack.ps1 +运行 Application.Tests 项目: + +```bash +# 导航到测试项目目录 +cd tests/PackageName.CompanyName.ProjectName.Application.Tests -# PowerShell Core (Windows/Linux/macOS) -pwsh pack.ps1 +# 运行测试 +dotnet test ``` -脚本会提示您选择要打包的模板: +测试框架将: -1. 微服务模板 -2. 单体应用模板 -3. 两种模板都打包 +1. 创建清洁的测试数据库环境 +2. 运行所有单元测试 +3. 报告测试结果 -## 支持的数据库 +## 关于命名的说明 -- SqlServer -- MySQL -- PostgreSQL -- Oracle -- SQLite +这是一个模板项目,所以所有项目名称包含的占位符在使用模板创建新项目时将被替换: -## 注意事项 +- `PackageName` 将被替换为您的包名 +- `CompanyName` 将被替换为您的公司名 +- `ProjectName` 将被替换为您的项目名 -- 确保已安装 .NET SDK 8.0 或更高版本 -- 根据需求选择合适的模板: - - 微服务模板:适用于大规模分布式应用 - - 单体应用模板:适用于小型应用或简单部署需求 -- 打包时注意 NuGet 发布地址和密钥 -- 发布前建议进行完整测试 +当从此模板创建新项目时,您将指定这些值,它们将在整个解决方案中进行替换。 From 413f18eae64e28573aadd1d32fcf05913f35cb6b Mon Sep 17 00:00:00 2001 From: feijie Date: Fri, 21 Mar 2025 21:33:02 +0800 Subject: [PATCH 09/10] =?UTF-8?q?=EF=B8=8F=20chore(EntityFrameworkCore):?= =?UTF-8?q?=20=E4=BF=AE=E5=A4=8D=20PostgreSQL=20=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E6=88=B3=E5=85=BC=E5=AE=B9=E6=80=A7=E9=97=AE=E9=A2=98=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E6=B7=BB=E5=8A=A0=E7=BC=BA=E5=A4=B1=E5=BC=95=E7=94=A8?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MicroServiceApplicationsSingleModule.Configure.cs | 1 + ...MigrationsEntityFrameworkCoreDatabaseManagementNameModule.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/MicroServiceApplicationsSingleModule.Configure.cs b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/MicroServiceApplicationsSingleModule.Configure.cs index fe7548c03..7a729b009 100644 --- a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/MicroServiceApplicationsSingleModule.Configure.cs +++ b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/MicroServiceApplicationsSingleModule.Configure.cs @@ -82,6 +82,7 @@ using Volo.Abp.SettingManagement.Localization; using Volo.Abp.Threading; using Volo.Abp.UI.Navigation.Urls; using Volo.Abp.VirtualFileSystem; +using PackageName.CompanyName.ProjectName.EntityFrameworkCore; using VoloAbpExceptionHandlingOptions = Volo.Abp.AspNetCore.ExceptionHandling.AbpExceptionHandlingOptions; namespace PackageName.CompanyName.ProjectName.AIO.Host; diff --git a/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DatabaseManagementName/SingleMigrationsEntityFrameworkCoreDatabaseManagementNameModule.cs b/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DatabaseManagementName/SingleMigrationsEntityFrameworkCoreDatabaseManagementNameModule.cs index 6ef5f8205..40f03bfdd 100644 --- a/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DatabaseManagementName/SingleMigrationsEntityFrameworkCoreDatabaseManagementNameModule.cs +++ b/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DatabaseManagementName/SingleMigrationsEntityFrameworkCoreDatabaseManagementNameModule.cs @@ -60,7 +60,7 @@ public class SingleMigrationsEntityFrameworkCoreDatabaseManagementNameModule : A options.UseOracle(); options.UseOracle(); #elif PostgreSql - AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);//解决PostgreSql设置为utc时间后无法写入local时区的问题 + System.AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);//解决PostgreSql设置为utc时间后无法写入local时区的问题 options.UseNpgsql(); options.UseNpgsql(); #endif From b557781ddff0ce79f366a565de5d7e377d147259 Mon Sep 17 00:00:00 2001 From: feijie Date: Sun, 23 Mar 2025 22:25:35 +0800 Subject: [PATCH 10/10] =?UTF-8?q?=F0=9F=90=9B=20fix(DbContext):=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8DDbContext=E5=9F=BA=E7=B1=BB=EF=BC=8C=E4=BD=BF?= =?UTF-8?q?=E7=94=A8AbpDbContext=E6=9B=BF=E4=BB=A3AbpDataProtectionDbConte?= =?UTF-8?q?xt=E3=80=82=E8=A7=A3=E5=86=B3=E6=9B=B4=E6=96=B0=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E6=97=A0=E6=95=88=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=8C?= =?UTF-8?q?=E5=A6=82=E9=9C=80=E8=A6=81=E5=90=AF=E7=94=A8=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E4=BF=9D=E6=8A=A4=E5=8F=AF=E4=BB=A5=E4=BD=BF=E7=94=A8AbpDataPr?= =?UTF-8?q?otectionDbContext?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ProjectName/EntityFrameworkCore/ProjectNameDbContext.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameDbContext.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameDbContext.cs index 988271a16..065418dc6 100644 --- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameDbContext.cs +++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameDbContext.cs @@ -2,12 +2,14 @@ using Microsoft.EntityFrameworkCore; using PackageName.CompanyName.ProjectName.Users; using Volo.Abp.Data; +using Volo.Abp.EntityFrameworkCore; using Volo.Abp.Identity.EntityFrameworkCore; namespace PackageName.CompanyName.ProjectName.EntityFrameworkCore; [ConnectionStringName(ProjectNameDbProperties.ConnectionStringName)] -public class ProjectNameDbContext : AbpDataProtectionDbContext, IProjectNameDbContext +// public class ProjectNameDbContext : AbpDataProtectionDbContext, IProjectNameDbContext +public class ProjectNameDbContext : AbpDbContext, IProjectNameDbContext { public virtual DbSet Users { get; set; }