From caa9d309406be5962209ef9091a332309aaede77 Mon Sep 17 00:00:00 2001
From: NoahLan <6995syu@163.com>
Date: Fri, 24 May 2024 14:42:27 +0800
Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E5=A4=9A?=
=?UTF-8?q?=E7=A7=9F=E6=88=B7=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../DbConnOptions.cs | 54 ---
...bute.cs => DefaultTenantTableAttribute.cs} | 10 +-
.../SqlSugarDbContext.cs | 29 +-
.../TenantService.cs | 17 +-
...NPinMultiTenantConnectionStringResolver.cs | 312 +++++++++---------
.../NPinTenantConfigurationProvider.cs | 73 ++++
.../SqlSugarTenantStore.cs | 232 ++++++-------
.../TenantAggregateRoot.cs | 2 +-
.../TenantManagementExtensions.cs | 27 +-
.../Caches/UserInfoCacheItem.cs | 34 ++
.../Dtos/UserDto.cs | 9 +-
.../Etos/UserCreatedEventArgs.cs | 21 +-
.../Authorization/RefreshTokenMiddleware.cs | 96 +++---
.../Managers/AccountManager.cs | 183 +++++++++-
.../Managers/UserManager.cs | 274 +++++++++++++++
.../Repositories/IUserRepository.cs | 29 +-
.../Repositories/UserRepository.cs | 38 +++
17 files changed, 996 insertions(+), 444 deletions(-)
rename framework/NPin.Framework.SqlSugarCore.Abstractions/{MasterTenantAttribute.cs => DefaultTenantTableAttribute.cs} (62%)
create mode 100644 module/tenant-management/NPin.Framework.TenantManagement.Domain/NPinTenantConfigurationProvider.cs
create mode 100644 module/upms/NPin.Framework.Upms.Domain.Shared/Caches/UserInfoCacheItem.cs
create mode 100644 module/upms/NPin.Framework.Upms.Domain/Managers/UserManager.cs
create mode 100644 module/upms/NPin.Framework.Upms.SqlSugarCore/Repositories/UserRepository.cs
diff --git a/framework/NPin.Framework.SqlSugarCore.Abstractions/DbConnOptions.cs b/framework/NPin.Framework.SqlSugarCore.Abstractions/DbConnOptions.cs
index 330f426..3c380a0 100644
--- a/framework/NPin.Framework.SqlSugarCore.Abstractions/DbConnOptions.cs
+++ b/framework/NPin.Framework.SqlSugarCore.Abstractions/DbConnOptions.cs
@@ -48,58 +48,4 @@ public class DbConnOptions
/// 开启Saas多租户
///
public bool EnabledSaasMultiTenancy { get; set; } = false;
-
-
- ///
- /// 默认租户库连接,如果不填,那就是默认库的地址
- ///
- public string? MasterSaasMultiTenancyUrl { get; set; }
-
-
- ///
- /// Saas租户连接
- ///
- public List? SaasMultiTenancy { get; set; }
-
- public static string MasterTenantName = "Master";
- public static string DefaultTenantName = "Default";
-
- public SaasMultiTenancyOptions GetDefaultSaasMultiTenancy()
- {
- return new SaasMultiTenancyOptions { Name = DefaultTenantName, Url = Url };
- }
-
- public SaasMultiTenancyOptions? GetMasterSaasMultiTenancy()
- {
- if (EnabledSaasMultiTenancy == false)
- {
- return null;
- }
-
- if (string.IsNullOrEmpty(MasterSaasMultiTenancyUrl))
- {
- return new SaasMultiTenancyOptions { Name = MasterTenantName, Url = Url };
- }
- else
- {
- return new SaasMultiTenancyOptions()
- {
- Name = MasterTenantName,
- Url = MasterSaasMultiTenancyUrl
- };
- }
- }
-}
-
-public class SaasMultiTenancyOptions
-{
- ///
- /// 租户名称标识
- ///
- public string Name { get; set; }
-
- ///
- /// 连接Url
- ///
- public string Url { get; set; }
}
\ No newline at end of file
diff --git a/framework/NPin.Framework.SqlSugarCore.Abstractions/MasterTenantAttribute.cs b/framework/NPin.Framework.SqlSugarCore.Abstractions/DefaultTenantTableAttribute.cs
similarity index 62%
rename from framework/NPin.Framework.SqlSugarCore.Abstractions/MasterTenantAttribute.cs
rename to framework/NPin.Framework.SqlSugarCore.Abstractions/DefaultTenantTableAttribute.cs
index c61ad57..bdb4a6d 100644
--- a/framework/NPin.Framework.SqlSugarCore.Abstractions/MasterTenantAttribute.cs
+++ b/framework/NPin.Framework.SqlSugarCore.Abstractions/DefaultTenantTableAttribute.cs
@@ -1,6 +1,6 @@
-namespace NPin.Framework.SqlSugarCore.Abstractions;
-
-[AttributeUsage(AttributeTargets.Class)]
-public class MasterTenantAttribute : Attribute
-{
+namespace NPin.Framework.SqlSugarCore.Abstractions;
+
+[AttributeUsage(AttributeTargets.Class)]
+public class DefaultTenantTableAttribute : Attribute
+{
}
\ No newline at end of file
diff --git a/framework/NPin.Framework.SqlSugarCore/SqlSugarDbContext.cs b/framework/NPin.Framework.SqlSugarCore/SqlSugarDbContext.cs
index df79538..eb788c8 100644
--- a/framework/NPin.Framework.SqlSugarCore/SqlSugarDbContext.cs
+++ b/framework/NPin.Framework.SqlSugarCore/SqlSugarDbContext.cs
@@ -22,6 +22,10 @@ public class SqlSugarDbContext : ISqlSugarDbContext
public ISqlSugarClient SqlSugarClient { get; private set; }
public DbConnOptions Options => LazyServiceProvider.LazyGetRequiredService>().Value;
+
+ private AbpDbConnectionOptions _connectionOptions =>
+ LazyServiceProvider.LazyGetRequiredService>().Value;
+
public ICurrentUser CurrentUser => LazyServiceProvider.GetRequiredService();
public ICurrentTenant CurrentTenant => LazyServiceProvider.LazyGetRequiredService();
public IDataFilter DataFilter => LazyServiceProvider.LazyGetRequiredService();
@@ -66,22 +70,23 @@ public class SqlSugarDbContext : ISqlSugarDbContext
///
protected virtual string GetCurrentConnectionString()
{
+ var defaultUrl = Options.Url ??
+ _connectionOptions.GetConnectionStringOrNull(ConnectionStrings.DefaultConnectionStringName);
+ // 如果未开启多租户,返回db url 或者 默认连接字符串
+ if (!Options.EnabledSaasMultiTenancy)
+ {
+ return defaultUrl;
+ }
+
+ // 开启多租户
var connectionStringResolver = LazyServiceProvider.LazyGetRequiredService();
var connectionString = connectionStringResolver.ResolveAsync().Result;
- //没有检测到使用多租户功能,默认使用默认库即可
+ // 没有检测到使用多租户功能,默认使用默认库即可
if (string.IsNullOrWhiteSpace(connectionString))
{
Volo.Abp.Check.NotNull(Options.Url, "租户默认库Default未找到");
- connectionString = Options.Url;
- }
-
- //如果当前租户是主库,单独使用主要库
- if (CurrentTenant.Name == DbConnOptions.MasterTenantName)
- {
- var conStrOrNull = Options.GetMasterSaasMultiTenancy();
- Volo.Abp.Check.NotNull(conStrOrNull, "租户主库Master未找到");
- connectionString = conStrOrNull.Url;
+ connectionString = defaultUrl;
}
return connectionString!;
@@ -254,6 +259,10 @@ public class SqlSugarDbContext : ISqlSugarDbContext
column.IsNullable = true;
}
+ if (property.Name == "ConcurrencyStamp")
+ {
+ column.IsIgnore = true;
+ }
if (property.PropertyType == typeof(ExtraPropertyDictionary))
{
column.IsIgnore = true;
diff --git a/module/tenant-management/NPin.Framework.TenantManagement.Application/TenantService.cs b/module/tenant-management/NPin.Framework.TenantManagement.Application/TenantService.cs
index 946b3a4..e1ac697 100644
--- a/module/tenant-management/NPin.Framework.TenantManagement.Application/TenantService.cs
+++ b/module/tenant-management/NPin.Framework.TenantManagement.Application/TenantService.cs
@@ -7,11 +7,10 @@ using NPin.Framework.TenantManagement.Application.Contracts;
using NPin.Framework.TenantManagement.Application.Contracts.Dtos;
using NPin.Framework.TenantManagement.Domain;
using SqlSugar;
-using Volo.Abp;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Data;
using Volo.Abp.Domain.Repositories;
-using Volo.Abp.Modularity;
+using Volo.Abp.Uow;
namespace NPin.Framework.TenantManagement.Application;
@@ -114,6 +113,7 @@ public class TenantService : NPinCrudAppService();
- var db = await _repository.GetDbContextAsync();
- // 尝试创建数据库
- db.DbMaintenance.CreateDatabase();
+ // 没有数据库,不能创建工作单元,先创建库再关闭
+ ISqlSugarClient db = null;
+ using (var uow = UnitOfWorkManager.Begin(requiresNew: true, isTransactional: false))
+ {
+ db = await _repository.GetDbContextAsync();
+ // 尝试创建数据库
+ db.DbMaintenance.CreateDatabase();
+ await uow.CompleteAsync();
+ }
List types = new List();
foreach (var module in moduleContainer.Modules)
@@ -141,6 +147,7 @@ public class TenantService : NPinCrudAppService x.GetCustomAttribute() == null)
.Where(x => x.GetCustomAttribute() != null)
+ .Where(x => x.GetCustomAttribute() is null)
.Where(x => x.GetCustomAttribute() is null));
}
diff --git a/module/tenant-management/NPin.Framework.TenantManagement.Domain/NPinMultiTenantConnectionStringResolver.cs b/module/tenant-management/NPin.Framework.TenantManagement.Domain/NPinMultiTenantConnectionStringResolver.cs
index 165ddba..a18db7c 100644
--- a/module/tenant-management/NPin.Framework.TenantManagement.Domain/NPinMultiTenantConnectionStringResolver.cs
+++ b/module/tenant-management/NPin.Framework.TenantManagement.Domain/NPinMultiTenantConnectionStringResolver.cs
@@ -1,152 +1,162 @@
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Options;
-using Volo.Abp;
-using Volo.Abp.Data;
-using Volo.Abp.DependencyInjection;
-using Volo.Abp.MultiTenancy;
-
-namespace NPin.Framework.TenantManagement.Domain;
-
-///
-/// 数据库连接字符串解析器,替代原有
-///
-[Dependency(ReplaceServices = true)]
-public class NPinMultiTenantConnectionStringResolver : DefaultConnectionStringResolver
-{
- private readonly ICurrentTenant _currentTenant;
- private readonly IServiceProvider _serviceProvider;
-
- public NPinMultiTenantConnectionStringResolver(IOptionsMonitor options,
- ICurrentTenant currentTenant, IServiceProvider serviceProvider) : base(options)
- {
- _currentTenant = currentTenant;
- _serviceProvider = serviceProvider;
- }
-
- public override async Task ResolveAsync(string? connectionStringName = null)
- {
- if (_currentTenant.Id == null)
- {
- // 无租户信息,使用原有解析器
- return await base.ResolveAsync(connectionStringName);
- }
-
- // 通过Store寻找租户缓存
- var tenant = await FindTenantConfigurationAsync(_currentTenant.Id.Value);
- if (tenant == null || tenant.ConnectionStrings.IsNullOrEmpty())
- {
- // 找不到租户缓存,使用原有解析器
- return await base.ResolveAsync(connectionStringName);
- }
-
- var tenantDefaultConnectionString = tenant.ConnectionStrings?.Default;
-
- var connString = tenant.ConnectionStrings?.FirstOrDefault().Value;
- if (!connString.IsNullOrWhiteSpace())
- {
- return connString;
- }
-
- // 库隔离
- var database = Options.Databases.GetMappedDatabaseOrNull(connectionStringName);
- if (database is { IsUsedByTenants: true })
- {
- connString = tenant.ConnectionStrings?.GetOrDefault(database.DatabaseName);
- if (!connString.IsNullOrWhiteSpace())
- {
- return connString;
- }
- }
-
- if (!tenantDefaultConnectionString.IsNullOrWhiteSpace())
- {
- return tenantDefaultConnectionString;
- }
-
- return await base.ResolveAsync(connectionStringName);
- }
-
- protected virtual async Task FindTenantConfigurationAsync(Guid tenantId)
- {
- using var serviceScope = _serviceProvider.CreateScope();
-
- var tenantStore = serviceScope
- .ServiceProvider
- .GetRequiredService();
-
- return await tenantStore.FindAsync(tenantId);
- }
-
- [Obsolete("Use ResolveAsync Instead.")]
- public override string Resolve(string? connectionStringName = null)
- {
- if (_currentTenant.Id == null)
- {
- // 无租户信息,使用原有解析器
- return base.Resolve(connectionStringName);
- }
-
- // 通过Store寻找租户缓存
- var tenant = FindTenantConfiguration(_currentTenant.Id.Value);
- if (tenant == null || tenant.ConnectionStrings.IsNullOrEmpty())
- {
- // 找不到租户缓存,使用原有解析器
- return base.Resolve(connectionStringName);
- }
-
- var tenantDefaultConnectionString = tenant.ConnectionStrings?.Default;
-
- // Requesting default connection string...
- if (connectionStringName == null ||
- connectionStringName == ConnectionStrings.DefaultConnectionStringName)
- {
- //Return tenant's default or global default
- return !tenantDefaultConnectionString.IsNullOrWhiteSpace()
- ? tenantDefaultConnectionString!
- : Options.ConnectionStrings.Default!;
- }
-
- // Requesting specific connection string...
- var connString = tenant.ConnectionStrings?.GetOrDefault(connectionStringName);
- if (!connString.IsNullOrWhiteSpace())
- {
- // Found for the tenant
- return connString!;
- }
-
- // Fallback to tenant's default connection string if available
- if (!tenantDefaultConnectionString.IsNullOrWhiteSpace())
- {
- return tenantDefaultConnectionString!;
- }
-
- // Try to find the specific connection string for given name
- var connStringInOptions = Options.ConnectionStrings.GetOrDefault(connectionStringName);
- if (!connStringInOptions.IsNullOrWhiteSpace())
- {
- return connStringInOptions!;
- }
-
- // Fallback to the global default connection string
- var defaultConnectionString = Options.ConnectionStrings.Default;
- if (!defaultConnectionString.IsNullOrWhiteSpace())
- {
- return defaultConnectionString!;
- }
-
- throw new AbpException("No connection string defined!");
- }
-
-
- [Obsolete("Use FindTenantConfigurationAsync Instead.")]
- protected virtual TenantConfiguration? FindTenantConfiguration(Guid tenantId)
- {
- using var serviceScope = _serviceProvider.CreateScope();
-
- var tenantStore = serviceScope
- .ServiceProvider
- .GetRequiredService();
-
- return tenantStore.Find(tenantId);
- }
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Volo.Abp.Data;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.MultiTenancy;
+
+namespace NPin.Framework.TenantManagement.Domain;
+
+///
+/// 数据库连接字符串解析器,替代原有
+///
+[Dependency(ReplaceServices = true)]
+public class NPinMultiTenantConnectionStringResolver : DefaultConnectionStringResolver
+{
+ private readonly ICurrentTenant _currentTenant;
+ private readonly IServiceProvider _serviceProvider;
+
+ public NPinMultiTenantConnectionStringResolver(IOptionsMonitor options,
+ ICurrentTenant currentTenant, IServiceProvider serviceProvider) : base(options)
+ {
+ _currentTenant = currentTenant;
+ _serviceProvider = serviceProvider;
+ }
+
+ public override async Task ResolveAsync(string? connectionStringName = null)
+ {
+ if (_currentTenant.Id == null)
+ {
+ // 无租户信息,使用原有解析器
+ return await base.ResolveAsync(connectionStringName);
+ }
+
+ // 通过Store寻找租户缓存
+ var tenant = await FindTenantConfigurationAsync(_currentTenant.Id.Value);
+ if (tenant == null || tenant.ConnectionStrings.IsNullOrEmpty())
+ {
+ // 找不到租户缓存,使用原有解析器
+ return await base.ResolveAsync(connectionStringName);
+ }
+
+ var tenantDefaultConnectionString = tenant.ConnectionStrings?.Default;
+ // Requesting default connection string...
+ if (connectionStringName == null ||
+ connectionStringName == ConnectionStrings.DefaultConnectionStringName)
+ {
+ // Return tenant's default or global default
+ return !tenantDefaultConnectionString.IsNullOrWhiteSpace()
+ ? tenantDefaultConnectionString!
+ : Options.ConnectionStrings.Default!;
+ }
+
+ // Requesting specific connection string...
+ var connString = tenant.ConnectionStrings?.FirstOrDefault().Value;
+ if (!connString.IsNullOrWhiteSpace())
+ {
+ // Found for the tenant
+ return connString!;
+ }
+
+ // 库隔离
+ var database = Options.Databases.GetMappedDatabaseOrNull(connectionStringName);
+ if (database is { IsUsedByTenants: true })
+ {
+ connString = tenant.ConnectionStrings?.GetOrDefault(database.DatabaseName);
+ if (!connString.IsNullOrWhiteSpace())
+ {
+ return connString;
+ }
+ }
+
+ if (!tenantDefaultConnectionString.IsNullOrWhiteSpace())
+ {
+ return tenantDefaultConnectionString;
+ }
+
+ return await base.ResolveAsync(connectionStringName);
+ }
+
+ protected virtual async Task FindTenantConfigurationAsync(Guid tenantId)
+ {
+ using var serviceScope = _serviceProvider.CreateScope();
+
+ var tenantStore = serviceScope
+ .ServiceProvider
+ .GetRequiredService();
+
+ return await tenantStore.FindAsync(tenantId);
+ }
+
+ [Obsolete("Use ResolveAsync Instead.")]
+ public override string Resolve(string? connectionStringName = null)
+ {
+ if (_currentTenant.Id == null)
+ {
+ // 无租户信息,使用原有解析器
+ return base.Resolve(connectionStringName);
+ }
+
+ // 通过Store寻找租户缓存
+ var tenant = FindTenantConfiguration(_currentTenant.Id.Value);
+ if (tenant == null || tenant.ConnectionStrings.IsNullOrEmpty())
+ {
+ // 找不到租户缓存,使用原有解析器
+ return base.Resolve(connectionStringName);
+ }
+
+ var tenantDefaultConnectionString = tenant.ConnectionStrings?.Default;
+
+ // Requesting default connection string...
+ if (connectionStringName == null ||
+ connectionStringName == ConnectionStrings.DefaultConnectionStringName)
+ {
+ //Return tenant's default or global default
+ return !tenantDefaultConnectionString.IsNullOrWhiteSpace()
+ ? tenantDefaultConnectionString!
+ : Options.ConnectionStrings.Default!;
+ }
+
+ // Requesting specific connection string...
+ var connString = tenant.ConnectionStrings?.GetOrDefault(connectionStringName);
+ if (!connString.IsNullOrWhiteSpace())
+ {
+ // Found for the tenant
+ return connString!;
+ }
+
+ // Fallback to tenant's default connection string if available
+ if (!tenantDefaultConnectionString.IsNullOrWhiteSpace())
+ {
+ return tenantDefaultConnectionString!;
+ }
+
+ // Try to find the specific connection string for given name
+ var connStringInOptions = Options.ConnectionStrings.GetOrDefault(connectionStringName);
+ if (!connStringInOptions.IsNullOrWhiteSpace())
+ {
+ return connStringInOptions!;
+ }
+
+ // Fallback to the global default connection string
+ var defaultConnectionString = Options.ConnectionStrings.Default;
+ if (!defaultConnectionString.IsNullOrWhiteSpace())
+ {
+ return defaultConnectionString!;
+ }
+
+ throw new AbpException("No connection string defined!");
+ }
+
+
+ [Obsolete("Use FindTenantConfigurationAsync Instead.")]
+ protected virtual TenantConfiguration? FindTenantConfiguration(Guid tenantId)
+ {
+ using var serviceScope = _serviceProvider.CreateScope();
+
+ var tenantStore = serviceScope
+ .ServiceProvider
+ .GetRequiredService();
+
+ return tenantStore.Find(tenantId);
+ }
}
\ No newline at end of file
diff --git a/module/tenant-management/NPin.Framework.TenantManagement.Domain/NPinTenantConfigurationProvider.cs b/module/tenant-management/NPin.Framework.TenantManagement.Domain/NPinTenantConfigurationProvider.cs
new file mode 100644
index 0000000..c28fe63
--- /dev/null
+++ b/module/tenant-management/NPin.Framework.TenantManagement.Domain/NPinTenantConfigurationProvider.cs
@@ -0,0 +1,73 @@
+using Microsoft.Extensions.Localization;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.MultiTenancy;
+using Volo.Abp.MultiTenancy.Localization;
+
+namespace NPin.Framework.TenantManagement.Domain;
+
+[Dependency(ReplaceServices = true)]
+public class NPinTenantConfigurationProvider : ITenantConfigurationProvider, ITransientDependency
+{
+ protected virtual ITenantResolver TenantResolver { get; }
+ protected virtual ITenantStore TenantStore { get; }
+ protected virtual ITenantResolveResultAccessor TenantResolveResultAccessor { get; }
+ protected virtual IStringLocalizer StringLocalizer { get; }
+
+ public NPinTenantConfigurationProvider(
+ ITenantResolver tenantResolver,
+ ITenantStore tenantStore,
+ ITenantResolveResultAccessor tenantResolveResultAccessor,
+ IStringLocalizer stringLocalizer)
+ {
+ TenantResolver = tenantResolver;
+ TenantStore = tenantStore;
+ TenantResolveResultAccessor = tenantResolveResultAccessor;
+ StringLocalizer = stringLocalizer;
+ }
+
+ public async Task GetAsync(bool saveResolveResult = false)
+ {
+ var resolveResult = await TenantResolver.ResolveTenantIdOrNameAsync();
+ if (saveResolveResult)
+ {
+ TenantResolveResultAccessor.Result = resolveResult;
+ }
+
+ TenantConfiguration? tenant = null;
+ if (resolveResult.TenantIdOrName != null)
+ {
+ tenant = await FindTenantAsync(resolveResult.TenantIdOrName);
+ if (tenant == null)
+ {
+ throw new BusinessException(
+ code: "Volo.AbpIo.MultiTenancy:010001",
+ message: StringLocalizer["TenantNotFoundMessage"],
+ details: StringLocalizer["TenantNotFoundDetails", resolveResult.TenantIdOrName]
+ );
+ }
+
+ if (!tenant.IsActive)
+ {
+ throw new BusinessException(
+ code: "Volo.AbpIo.MultiTenancy:010002",
+ message: StringLocalizer["TenantNotActiveMessage"],
+ details: StringLocalizer["TenantNotActiveDetails", resolveResult.TenantIdOrName]
+ );
+ }
+ }
+
+ return tenant;
+ }
+
+ protected virtual async Task FindTenantAsync(string tenantIdOrName)
+ {
+ if (Guid.TryParse(tenantIdOrName, out var parsedTenantId))
+ {
+ return await TenantStore.FindAsync(parsedTenantId);
+ }
+ else
+ {
+ return await TenantStore.FindAsync(tenantIdOrName);
+ }
+ }
+}
\ No newline at end of file
diff --git a/module/tenant-management/NPin.Framework.TenantManagement.Domain/SqlSugarTenantStore.cs b/module/tenant-management/NPin.Framework.TenantManagement.Domain/SqlSugarTenantStore.cs
index df00a03..3c1321b 100644
--- a/module/tenant-management/NPin.Framework.TenantManagement.Domain/SqlSugarTenantStore.cs
+++ b/module/tenant-management/NPin.Framework.TenantManagement.Domain/SqlSugarTenantStore.cs
@@ -1,124 +1,110 @@
-using JetBrains.Annotations;
-using NPin.Framework.SqlSugarCore.Abstractions;
-using Volo.Abp;
-using Volo.Abp.Caching;
-using Volo.Abp.Data;
-using Volo.Abp.MultiTenancy;
-
-namespace NPin.Framework.TenantManagement.Domain;
-
-///
-/// 实现TenantStore
-///
-public class SqlSugarTenantStore: ITenantStore
-{
- private ISqlSugarTenantRepository TenantRepository { get; }
- protected ICurrentTenant CurrentTenant { get; }
- protected IDistributedCache Cache { get; }
-
- public SqlSugarTenantStore(ISqlSugarTenantRepository tenantRepository, ICurrentTenant currentTenant, IDistributedCache cache)
- {
- TenantRepository = tenantRepository;
- CurrentTenant = currentTenant;
- Cache = cache;
- }
-
-
- public Task FindAsync(string name)
- {
- throw new NotImplementedException();
- }
-
- public Task FindAsync(Guid id)
- {
- throw new NotImplementedException();
- }
-
- protected virtual async Task GetCacheItemAsync(Guid? id, string name)
- {
- var cacheKey = CalculateCacheKey(id, name);
- var cacheItem = await Cache.GetAsync(cacheKey, considerUow: true);
- if (cacheItem != null)
- {
- return cacheItem;
- }
-
- if (id.HasValue)
- {
- using (CurrentTenant.Change(null))
- {
- var tenant = await TenantRepository.FindAsync(id.Value);
- return await SetCacheAsync(cacheKey, tenant);
- }
- }
-
- if (!name.IsNullOrWhiteSpace())
- {
- using (CurrentTenant.Change(null))
- {
- var tenant = await TenantRepository.FindByNameAsync(name);
- return await SetCacheAsync(cacheKey, tenant);
- }
- }
-
- throw new AbpException("Id和Name不能都为空");
- }
-
- protected virtual async Task SetCacheAsync(string cacheKey, TenantAggregateRoot? tenant)
- {
- var tenantConfiguration = tenant != null ? MapToConfiguration(tenant) : null;
- var cacheItem = new TenantCacheItem(tenantConfiguration);
- await Cache.SetAsync(cacheKey, cacheItem, considerUow: true);
- return cacheItem;
- }
-
- private TenantConfiguration MapToConfiguration(TenantAggregateRoot tenantAggregateRoot)
- {
- var tenantConfiguration = new TenantConfiguration
- {
- Id = tenantAggregateRoot.Id,
- Name = tenantAggregateRoot.Name,
- ConnectionStrings = MapToString(tenantAggregateRoot.TenantConnectionString),
- IsActive = true
- };
- return tenantConfiguration;
- }
-
- private ConnectionStrings? MapToString(string tenantConnectionString)
- {
-
- //tenantConnectionString = tenantConnectionString.TrimEnd(';');
- //var strSpiteds = tenantConnectionString.Split(";");
- //if (strSpiteds.Count() == 0)
- //{
- // return null;
-
- //}
-
- var connectionStrings = new ConnectionStrings();
- //foreach (string strSpited in strSpiteds)
- //{
- // var key = strSpited.Split('=')[0];
- // var value = strSpited.Split('=')[1];
- // connectionStrings[key] = value;
- //}
- connectionStrings["test"] = tenantConnectionString;
-
- return connectionStrings;
- }
-
- protected virtual string CalculateCacheKey(Guid? id, string name)
- {
- return TenantCacheItem.CalculateCacheKey(id, name);
- }
-
- public TenantConfiguration? Find(string name)
- {
- throw new NotImplementedException("请使用异步方法");
- }
-
- public TenantConfiguration? Find(Guid id)
- {
- throw new NotImplementedException("请使用异步方法");
- }
+using JetBrains.Annotations;
+using NPin.Framework.SqlSugarCore.Abstractions;
+using Volo.Abp;
+using Volo.Abp.Caching;
+using Volo.Abp.Data;
+using Volo.Abp.MultiTenancy;
+
+namespace NPin.Framework.TenantManagement.Domain;
+
+///
+/// 实现TenantStore
+///
+public class SqlSugarTenantStore : ITenantStore
+{
+ private ISqlSugarTenantRepository TenantRepository { get; }
+ protected ICurrentTenant CurrentTenant { get; }
+ protected IDistributedCache Cache { get; }
+
+ public SqlSugarTenantStore(ISqlSugarTenantRepository tenantRepository, ICurrentTenant currentTenant,
+ IDistributedCache cache)
+ {
+ TenantRepository = tenantRepository;
+ CurrentTenant = currentTenant;
+ Cache = cache;
+ }
+
+ public TenantConfiguration? Find(string name)
+ {
+ throw new NotImplementedException("请使用异步方法");
+ }
+
+ public TenantConfiguration? Find(Guid id)
+ {
+ throw new NotImplementedException("请使用异步方法");
+ }
+
+ public async Task FindAsync(string name)
+ {
+ return (await GetCacheItemAsync(null, name)).Value;
+ }
+
+ public async Task FindAsync(Guid id)
+ {
+ return (await GetCacheItemAsync(id, null)).Value;
+ }
+
+ protected virtual async Task GetCacheItemAsync(Guid? id, string? name)
+ {
+ var cacheKey = CalculateCacheKey(id, name);
+ var cacheItem = await Cache.GetAsync(cacheKey, considerUow: true);
+ if (cacheItem != null)
+ {
+ return cacheItem;
+ }
+
+ if (id.HasValue)
+ {
+ using (CurrentTenant.Change(null))
+ {
+ var tenant = await TenantRepository.FindAsync(id.Value);
+ return await SetCacheAsync(cacheKey, tenant);
+ }
+ }
+
+ if (!name.IsNullOrWhiteSpace())
+ {
+ using (CurrentTenant.Change(null))
+ {
+ var tenant = await TenantRepository.FindByNameAsync(name);
+ return await SetCacheAsync(cacheKey, tenant);
+ }
+ }
+
+ throw new AbpException("Id和Name不能都为空");
+ }
+
+ protected virtual async Task SetCacheAsync(string cacheKey, TenantAggregateRoot? tenant)
+ {
+ var tenantConfiguration = tenant != null ? MapToConfiguration(tenant) : null;
+ var cacheItem = new TenantCacheItem(tenantConfiguration);
+ await Cache.SetAsync(cacheKey, cacheItem, considerUow: true);
+ return cacheItem;
+ }
+
+ private TenantConfiguration MapToConfiguration(TenantAggregateRoot tenantAggregateRoot)
+ {
+ var tenantConfiguration = new TenantConfiguration
+ {
+ Id = tenantAggregateRoot.Id,
+ Name = tenantAggregateRoot.Name,
+ ConnectionStrings = MapToString(tenantAggregateRoot.TenantConnectionString),
+ IsActive = true
+ };
+ return tenantConfiguration;
+ }
+
+ private ConnectionStrings MapToString(string tenantConnectionString)
+ {
+ var connectionStrings = new ConnectionStrings
+ {
+ [ConnectionStrings.DefaultConnectionStringName] = tenantConnectionString
+ };
+ return connectionStrings;
+ }
+
+ protected virtual string CalculateCacheKey(Guid? id, string name)
+ {
+ return TenantCacheItem.CalculateCacheKey(id, name);
+ }
}
\ No newline at end of file
diff --git a/module/tenant-management/NPin.Framework.TenantManagement.Domain/TenantAggregateRoot.cs b/module/tenant-management/NPin.Framework.TenantManagement.Domain/TenantAggregateRoot.cs
index 9a92cef..bf7b99a 100644
--- a/module/tenant-management/NPin.Framework.TenantManagement.Domain/TenantAggregateRoot.cs
+++ b/module/tenant-management/NPin.Framework.TenantManagement.Domain/TenantAggregateRoot.cs
@@ -10,7 +10,7 @@ using Check = Volo.Abp.Check;
namespace NPin.Framework.TenantManagement.Domain;
[SugarTable("SysTenant", "租户表")]
-[MasterTenant]
+[DefaultTenantTable]
public class TenantAggregateRoot : FullAuditedAggregateRoot, IHasEntityVersion
{
[SugarColumn(IsPrimaryKey = true)] public override Guid Id { get; protected set; }
diff --git a/module/tenant-management/NPin.Framework.TenantManagement.Domain/TenantManagementExtensions.cs b/module/tenant-management/NPin.Framework.TenantManagement.Domain/TenantManagementExtensions.cs
index 19e3fed..5b403c2 100644
--- a/module/tenant-management/NPin.Framework.TenantManagement.Domain/TenantManagementExtensions.cs
+++ b/module/tenant-management/NPin.Framework.TenantManagement.Domain/TenantManagementExtensions.cs
@@ -1,17 +1,12 @@
-using NPin.Framework.SqlSugarCore.Abstractions;
-using Volo.Abp.MultiTenancy;
-
-namespace NPin.Framework.TenantManagement.Domain;
-
-public static class TenantManagementExtensions
-{
- public static IDisposable ChangeMaster(this ICurrentTenant currentTenant)
- {
- return currentTenant.Change(null, DbConnOptions.MasterTenantName);
- }
-
- public static IDisposable ChangeDefault(this ICurrentTenant currentTenant)
- {
- return currentTenant.Change(null, DbConnOptions.DefaultTenantName);
- }
+using Volo.Abp.Data;
+using Volo.Abp.MultiTenancy;
+
+namespace NPin.Framework.TenantManagement.Domain;
+
+public static class TenantManagementExtensions
+{
+ public static IDisposable ChangeDefault(this ICurrentTenant currentTenant)
+ {
+ return currentTenant.Change(null, ConnectionStrings.DefaultConnectionStringName);
+ }
}
\ No newline at end of file
diff --git a/module/upms/NPin.Framework.Upms.Domain.Shared/Caches/UserInfoCacheItem.cs b/module/upms/NPin.Framework.Upms.Domain.Shared/Caches/UserInfoCacheItem.cs
new file mode 100644
index 0000000..245d89c
--- /dev/null
+++ b/module/upms/NPin.Framework.Upms.Domain.Shared/Caches/UserInfoCacheItem.cs
@@ -0,0 +1,34 @@
+using NPin.Framework.Upms.Domain.Shared.Dtos;
+
+namespace NPin.Framework.Upms.Domain.Shared.Caches;
+
+public class UserInfoCacheItem
+{
+ ///
+ /// 用户完整信息
+ ///
+ public UserFullDto Info { get; set; }
+
+ public UserInfoCacheItem(UserFullDto info)
+ {
+ Info = info;
+ }
+}
+
+public class UserInfoCacheKey
+{
+ ///
+ /// 用户ID
+ ///
+ public Guid UserId { get; set; }
+
+ public UserInfoCacheKey(Guid userId)
+ {
+ UserId = userId;
+ }
+
+ public override string ToString()
+ {
+ return $"User:{UserId}";
+ }
+}
\ No newline at end of file
diff --git a/module/upms/NPin.Framework.Upms.Domain.Shared/Dtos/UserDto.cs b/module/upms/NPin.Framework.Upms.Domain.Shared/Dtos/UserDto.cs
index bdcd095..6591c42 100644
--- a/module/upms/NPin.Framework.Upms.Domain.Shared/Dtos/UserDto.cs
+++ b/module/upms/NPin.Framework.Upms.Domain.Shared/Dtos/UserDto.cs
@@ -11,12 +11,13 @@ public class UserFullDto
public UserDto User { get; set; } = new();
// Relations
- public HashSet Roles { get; set; } = [];
- public HashSet Posts { get; set; } = [];
- public HashSet Organizations { get; set; } = [];
+ public List Roles { get; set; } = [];
+ public List Posts { get; set; } = [];
+ public List Organizations { get; set; } = [];
- public HashSet PostCodes { get; set; } = [];
public HashSet RoleCodes { get; set; } = [];
+ public HashSet PostCodes { get; set; } = [];
+ public HashSet OrganizationCodes { get; set; } = [];
public HashSet PermissionCodes { get; set; } = [];
}
diff --git a/module/upms/NPin.Framework.Upms.Domain.Shared/Etos/UserCreatedEventArgs.cs b/module/upms/NPin.Framework.Upms.Domain.Shared/Etos/UserCreatedEventArgs.cs
index 9a0b383..e069401 100644
--- a/module/upms/NPin.Framework.Upms.Domain.Shared/Etos/UserCreatedEventArgs.cs
+++ b/module/upms/NPin.Framework.Upms.Domain.Shared/Etos/UserCreatedEventArgs.cs
@@ -1,9 +1,14 @@
-namespace NPin.Framework.Upms.Domain.Shared.Etos;
-
-///
-/// 用户创建事件参数
-///
-public class UserCreatedEventArgs
-{
- public Guid UserId { get; set; }
+namespace NPin.Framework.Upms.Domain.Shared.Etos;
+
+///
+/// 用户创建事件参数
+///
+public class UserCreatedEventArgs
+{
+ public Guid UserId { get; set; }
+
+ public UserCreatedEventArgs(Guid userId)
+ {
+ UserId = userId;
+ }
}
\ No newline at end of file
diff --git a/module/upms/NPin.Framework.Upms.Domain/Authorization/RefreshTokenMiddleware.cs b/module/upms/NPin.Framework.Upms.Domain/Authorization/RefreshTokenMiddleware.cs
index 38ec12a..0d93ced 100644
--- a/module/upms/NPin.Framework.Upms.Domain/Authorization/RefreshTokenMiddleware.cs
+++ b/module/upms/NPin.Framework.Upms.Domain/Authorization/RefreshTokenMiddleware.cs
@@ -1,49 +1,49 @@
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Http;
-using NPin.Framework.Upms.Domain.Shared.Consts;
-using Volo.Abp.DependencyInjection;
-using Volo.Abp.Security.Claims;
-
-namespace NPin.Framework.Upms.Domain.Authorization;
-
-///
-/// RefreshToken 处理中间件
-///
-public class RefreshTokenMiddleware : IMiddleware, ITransientDependency
-{
- public async Task InvokeAsync(HttpContext context, RequestDelegate next)
- {
- var refreshToken = context.Request.Headers["refresh_token"].ToString();
- if (!refreshToken.IsNullOrEmpty())
- {
- var authResult = await context.AuthenticateAsync(TokenTypeConst.Refresh);
- // Token刷新成功
- if (authResult.Succeeded)
- {
- var userId = Guid.Parse(authResult.Principal.FindFirst(AbpClaimTypes.UserId).Value);
- // TODO
- // var accessToken =
- // var refreshToken =
- context.Response.Headers["access_token"] = "";
- context.Response.Headers["refresh_token"] = "";
-
- // 请求头替换
- context.Request.Headers["Authorization"] = $"Bearer {""}";
- }
- }
- await next(context);
- }
-}
-
-///
-/// 扩展
-///
-public static class RefreshTokenExtensions
-{
- public static IApplicationBuilder UseRefreshToken(this IApplicationBuilder builder)
- {
- builder.UseMiddleware();
- return builder;
- }
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using NPin.Framework.Upms.Domain.Shared.Consts;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Security.Claims;
+
+namespace NPin.Framework.Upms.Domain.Authorization;
+
+///
+/// RefreshToken 处理中间件
+///
+public class RefreshTokenMiddleware : IMiddleware, ITransientDependency
+{
+ public async Task InvokeAsync(HttpContext context, RequestDelegate next)
+ {
+ var refreshToken = context.Request.Headers["refresh_token"].ToString();
+ if (!refreshToken.IsNullOrEmpty())
+ {
+ var authResult = await context.AuthenticateAsync(TokenTypeConst.Refresh);
+ // Token刷新成功
+ if (authResult.Succeeded)
+ {
+ var userId = Guid.Parse(authResult.Principal.FindFirst(AbpClaimTypes.UserId).Value);
+ // TODO
+ var accessToken = "";
+ // var refreshToken =
+ context.Response.Headers["access_token"] = "";
+ context.Response.Headers["refresh_token"] = "";
+
+ // 请求头替换
+ context.Request.Headers["Authorization"] = $"Bearer {accessToken}";
+ }
+ }
+ await next(context);
+ }
+}
+
+///
+/// 扩展
+///
+public static class RefreshTokenExtensions
+{
+ public static IApplicationBuilder UseRefreshToken(this IApplicationBuilder builder)
+ {
+ builder.UseMiddleware();
+ return builder;
+ }
}
\ No newline at end of file
diff --git a/module/upms/NPin.Framework.Upms.Domain/Managers/AccountManager.cs b/module/upms/NPin.Framework.Upms.Domain/Managers/AccountManager.cs
index 16f4c04..febe58e 100644
--- a/module/upms/NPin.Framework.Upms.Domain/Managers/AccountManager.cs
+++ b/module/upms/NPin.Framework.Upms.Domain/Managers/AccountManager.cs
@@ -1,12 +1,173 @@
-using Volo.Abp.Domain.Services;
-
-namespace NPin.Framework.Upms.Domain.Managers;
-
-public interface IAccountManager
-{
-}
-
-public class AccountManager : DomainService, IAccountManager
-{
- // private readonly
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using System.Text;
+using Mapster;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
+using NPin.Framework.Upms.Domain.Entities;
+using NPin.Framework.Upms.Domain.Repositories;
+using NPin.Framework.Upms.Domain.Shared.Consts;
+using NPin.Framework.Upms.Domain.Shared.Dtos;
+using NPin.Framework.Upms.Domain.Shared.Etos;
+using NPin.Framework.Upms.Domain.Shared.Options;
+using Volo.Abp.Domain.Services;
+using Volo.Abp.EventBus.Local;
+using Volo.Abp.Security.Claims;
+
+namespace NPin.Framework.Upms.Domain.Managers;
+
+public interface IAccountManager
+{
+ ///
+ /// 创建 RefreshToken
+ ///
+ ///
+ ///
+ string CreateRefreshToken(Guid userId);
+
+ ///
+ /// 获取 AccessToken
+ ///
+ ///
+ ///
+ Task CreateAccessTokenAsync(Guid userId);
+
+ ///
+ /// 重置密码
+ ///
+ ///
+ ///
+ ///
+ Task RestPasswordAsync(Guid userId, string password);
+
+ ///
+ /// 修改密码
+ ///
+ ///
+ ///
+ ///
+ ///
+ Task UpdatePasswordAsync(Guid userId, string newPassword, string oldPassword);
+}
+
+public class AccountManager : DomainService, IAccountManager
+{
+ private readonly IUserRepository _repository;
+ private readonly ILocalEventBus _localEventBus;
+ private readonly JwtOptions _jwtOptions;
+ private readonly RefreshJwtOptions _refreshJwtOptions;
+ private readonly UserManager _userManager;
+ private IHttpContextAccessor _httpContextAccessor;
+
+ public AccountManager(
+ IUserRepository repository,
+ ILocalEventBus localEventBus,
+ IOptions jwtOptions,
+ IOptions refreshJwtOptions,
+ UserManager userManager,
+ IHttpContextAccessor httpContextAccessor)
+ {
+ _repository = repository;
+ _localEventBus = localEventBus;
+ _jwtOptions = jwtOptions.Value;
+ _refreshJwtOptions = refreshJwtOptions.Value;
+ _userManager = userManager;
+ _httpContextAccessor = httpContextAccessor;
+ }
+
+ ///
+ /// 根据用户Id获取AccessToken
+ ///
+ ///
+ ///
+ public async Task CreateAccessTokenAsync(Guid userId)
+ {
+ var userInfo = await _userManager.GetInfoByCacheAsync(userId);
+ if (!userInfo.User.IsEnabled)
+ {
+ throw new UserFriendlyException("该用户已被禁用,请联系管理员进行恢复");
+ }
+
+ // http请求
+ if (_httpContextAccessor.HttpContext is not null)
+ {
+ // TODO eto 与 entity 保证
+ var loginLogEntity = LoginLogEntity.GetInfoByHttpContext(_httpContextAccessor.HttpContext);
+ var loginEto = loginLogEntity.Adapt();
+ loginEto.Username = userInfo.User.Username;
+ loginEto.UserId = userInfo.User.Id;
+ // 异步
+ _ = _localEventBus.PublishAsync(loginEto);
+ }
+
+ return CreateAccessToken(UserInfoToClaims(userInfo));
+ }
+
+ private string CreateAccessToken(List> kvs)
+ {
+ var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.SecurityKey));
+ // RSA2
+ var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
+ var claims = kvs.Select(x => new Claim(x.Key, x.Value)).ToList();
+ var token = new JwtSecurityToken(
+ issuer: _jwtOptions.Issuer,
+ audience: _jwtOptions.Audience,
+ claims: claims,
+ expires: DateTime.Now.AddMinutes(_jwtOptions.ExpiresMinuteTime),
+ notBefore: DateTime.Now,
+ signingCredentials: credentials);
+ return new JwtSecurityTokenHandler().WriteToken(token);
+ }
+
+ public string CreateRefreshToken(Guid userId)
+ {
+ throw new NotImplementedException();
+ }
+
+ private List> UserInfoToClaims(UserFullDto dto)
+ {
+ var claims = new List>();
+
+ AddToClaim(claims, AbpClaimTypes.UserId, dto.User.Id.ToString());
+ AddToClaim(claims, AbpClaimTypes.UserName, dto.User.Username);
+
+ // TODO 各种 Claim
+
+ // 超级管理员
+ if (UserConst.Admin.Equals(dto.User.Username))
+ {
+ AddToClaim(claims, TokenTypeConst.Permission, UserConst.AdminPermissionCode);
+ AddToClaim(claims, TokenTypeConst.Roles, UserConst.AdminRoleCode);
+ }
+ else
+ {
+ }
+
+ return claims;
+ }
+
+ private void AddToClaim(List> claims, string key, string value)
+ {
+ claims.Add(new KeyValuePair(key, value));
+ }
+
+ ///
+ /// 重置密码(仅执行最终重置,无前序判断)
+ ///
+ ///
+ ///
+ ///
+ public async Task RestPasswordAsync(Guid userId, string password)
+ {
+ var user = await _repository.GetByIdAsync(userId);
+ user.EncryptPassword.Password = password;
+ user.BuildPassword();
+ return await _repository.UpdateAsync(user);
+ }
+
+ public Task UpdatePasswordAsync(Guid userId, string newPassword, string oldPassword)
+ {
+ throw new NotImplementedException();
+ }
}
\ No newline at end of file
diff --git a/module/upms/NPin.Framework.Upms.Domain/Managers/UserManager.cs b/module/upms/NPin.Framework.Upms.Domain/Managers/UserManager.cs
new file mode 100644
index 0000000..e39236a
--- /dev/null
+++ b/module/upms/NPin.Framework.Upms.Domain/Managers/UserManager.cs
@@ -0,0 +1,274 @@
+using System.Text.RegularExpressions;
+using Mapster;
+using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using NPin.Framework.SqlSugarCore.Abstractions;
+using NPin.Framework.Upms.Domain.Entities;
+using NPin.Framework.Upms.Domain.Entities.ValueObjects;
+using NPin.Framework.Upms.Domain.Repositories;
+using NPin.Framework.Upms.Domain.Shared.Caches;
+using NPin.Framework.Upms.Domain.Shared.Consts;
+using NPin.Framework.Upms.Domain.Shared.Dtos;
+using NPin.Framework.Upms.Domain.Shared.Etos;
+using NPin.Framework.Upms.Domain.Shared.Options;
+using Volo.Abp.Authorization;
+using Volo.Abp.Caching;
+using Volo.Abp.Domain.Services;
+using Volo.Abp.EventBus.Local;
+using Volo.Abp.Uow;
+
+namespace NPin.Framework.Upms.Domain.Managers;
+
+///
+/// 用户管理器 领域服务
+///
+public partial class UserManager : DomainService
+{
+ [GeneratedRegex("^[a-zA-Z0-9]+$")]
+ private static partial Regex UsernameRegex();
+
+ private readonly IUserRepository _repository;
+ private readonly ISqlSugarRepository _repositoryRole;
+
+ private readonly ISqlSugarRepository _repositoryUserRole;
+ private readonly ISqlSugarRepository _repositoryUserPost;
+ private readonly ISqlSugarRepository _repositoryUserOrg;
+
+ private IDistributedCache _userCache;
+ private ILocalEventBus _localEventBus;
+
+ public UserManager(
+ IUserRepository repository,
+ ISqlSugarRepository repositoryUserRole,
+ ISqlSugarRepository repositoryUserPost,
+ ISqlSugarRepository repositoryUserOrg,
+ IDistributedCache userCache,
+ ILocalEventBus localEventBus, ISqlSugarRepository repositoryRole)
+ {
+ _repository = repository;
+ _repositoryUserRole = repositoryUserRole;
+ _repositoryUserPost = repositoryUserPost;
+ _repositoryUserOrg = repositoryUserOrg;
+ _userCache = userCache;
+ _localEventBus = localEventBus;
+ _repositoryRole = repositoryRole;
+ }
+
+ ///
+ /// 创建用户
+ ///
+ ///
+ public async Task CreateAsync(UserEntity entity)
+ {
+ await ValidateUser(entity);
+
+ var returnEntity = await _repository.InsertReturnEntityAsync(entity);
+ entity = returnEntity;
+
+ // 触发事件
+ await _localEventBus.PublishAsync(new UserCreatedEventArgs(entity.Id));
+ }
+
+ private async Task ValidateUser(UserEntity entity)
+ {
+ // TODO 不一定非要用户名,这里需要更自由的逻辑
+ if (entity.Username is UserConst.Admin or UserConst.TenantAdmin)
+ {
+ throw new UserFriendlyException("无效的用户名");
+ }
+
+ if (entity.Username.Length < 2)
+ {
+ throw new UserFriendlyException("用户名长度错误,需大于2个字符");
+ }
+
+ // 正则表达式,匹配只包含数字和字母的字符串
+ var isMatch = UsernameRegex().IsMatch(entity.Username);
+ if (!isMatch)
+ {
+ throw new UserFriendlyException("用户名不能包含除【字母】与【数字】的其他字符");
+ }
+
+ // 密码长度判断
+ // TODO(需要读取配置)
+ if (entity.EncryptPassword.Password.Length < 6)
+ {
+ throw new UserFriendlyException($"密码格式错误,长度需大于等于{6}位");
+ }
+
+ if (!string.IsNullOrEmpty(entity.PhoneNumber))
+ {
+ if (await _repository.IsAnyAsync(x => x.PhoneNumber == entity.PhoneNumber))
+ {
+ throw new UserFriendlyException("手机号重复");
+ }
+ }
+ }
+
+ ///
+ /// 设置默认角色
+ ///
+ ///
+ public async Task SetDefaultRoleAsync(Guid userId)
+ {
+ // 检查默认角色是否存在,不存在不处理
+ var role = await _repositoryRole.GetFirstAsync(x => x.Code == UserConst.DefaultRoleCode);
+ if (role is not null)
+ {
+ await SetRoleAsync([userId], [role.Id]);
+ }
+ }
+
+ ///
+ /// 给用户设置角色
+ ///
+ ///
+ ///
+ [UnitOfWork]
+ public async Task SetRoleAsync(List userIds, List roleIds)
+ {
+ // 删除用户之前的所有关系(物理)
+ await _repositoryUserRole.DeleteAsync(u => userIds.Contains(u.UserId));
+
+ var entities = (from userId in userIds
+ from roleId in roleIds
+ select new UserRoleEntity { UserId = userId, RoleId = roleId })
+ .ToList();
+
+ await _repositoryUserRole.InsertRangeAsync(entities);
+ }
+
+ ///
+ /// 设置用户岗位
+ ///
+ ///
+ ///
+ [UnitOfWork]
+ public async Task SetPostAsync(List userIds, List postIds)
+ {
+ // 删除用户之前的所有关系(物理)
+ await _repositoryUserPost.DeleteAsync(u => userIds.Contains(u.UserId));
+
+ var entities = (from userId in userIds
+ from postId in postIds
+ select new UserPostEntity { UserId = userId, PostId = postId })
+ .ToList();
+
+ await _repositoryUserPost.InsertRangeAsync(entities);
+ }
+
+ ///
+ /// 设置用户组织机构
+ ///
+ ///
+ ///
+ [UnitOfWork]
+ public async Task SetOrganizationAsync(List userIds, List orgIds)
+ {
+ // 删除用户之前的所有关系(物理)
+ await _repositoryUserOrg.DeleteAsync(u => userIds.Contains(u.UserId));
+
+ var entities = (from userId in userIds
+ from orgId in orgIds
+ select new UserOrganizationEntity { UserId = userId, OrganizationId = orgId })
+ .ToList();
+
+ await _repositoryUserOrg.InsertRangeAsync(entities);
+ }
+
+ ///
+ /// 从缓存中获取用户信息,若缓存不存在则查库
+ ///
+ ///
+ ///
+ /// 库中无此用户,403
+ public async Task GetInfoByCacheAsync(Guid userId)
+ {
+ // 1. 缓存获取
+ UserFullDto ret = null;
+ var tokenExpires = LazyServiceProvider.GetRequiredService>().Value.ExpiresMinuteTime;
+ var cached = await _userCache.GetOrAddAsync(new UserInfoCacheKey(userId),
+ async () =>
+ {
+ // 2. 库查询
+ var user = await _repository.GetAllInfoAsync(userId);
+ var dto = EntityMapToDto(user);
+ ret = dto ?? throw new AbpAuthorizationException();
+
+ return new UserInfoCacheItem(dto);
+ },
+ () => new DistributedCacheEntryOptions
+ {
+ AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(tokenExpires)
+ });
+
+ if (cached is not null)
+ {
+ ret = cached.Info;
+ }
+
+ return ret!;
+ }
+
+ private UserFullDto? EntityMapToDto(UserEntity? entity)
+ {
+ if (entity is null)
+ {
+ return null;
+ }
+ var ret = new UserFullDto();
+
+ // 密码过滤
+ entity.EncryptPassword = new EncryptPasswordValueObject();
+
+ // 超级管理员特殊处理
+ if (UserConst.Admin.Equals(entity.Username))
+ {
+ ret.User = _userCache.Adapt();
+ ret.RoleCodes.Add(UserConst.AdminRoleCode);
+ ret.PermissionCodes.Add(UserConst.AdminPermissionCode);
+ return ret;
+ }
+
+ // 角色
+ var roleDtoList = new List();
+ var roleCodes = new HashSet();
+ foreach (var role in entity.Roles)
+ {
+ roleDtoList.Add(role.Adapt());
+ roleCodes.Add(role.Code);
+ }
+
+ ret.Roles = roleDtoList;
+ ret.RoleCodes = roleCodes;
+
+ // 岗位
+ var postDtoList = new List();
+ var postCodes = new HashSet();
+ foreach (var post in entity.Posts)
+ {
+ postDtoList.Add(post.Adapt());
+ postCodes.Add(post.Code);
+ }
+
+ ret.Posts = postDtoList;
+ ret.PostCodes = postCodes;
+
+ // 组织结构
+ var orgDtoList = new List();
+ var orgCodes = new HashSet();
+ foreach (var org in entity.Organizations)
+ {
+ orgDtoList.Add(org.Adapt());
+ orgCodes.Add(org.Code);
+ }
+
+ ret.Organizations = orgDtoList;
+ ret.OrganizationCodes = orgCodes;
+
+ ret.User = entity.Adapt();
+ // TODO permissionCode
+ return ret;
+ }
+}
\ No newline at end of file
diff --git a/module/upms/NPin.Framework.Upms.Domain/Repositories/IUserRepository.cs b/module/upms/NPin.Framework.Upms.Domain/Repositories/IUserRepository.cs
index 5703863..2eafe2e 100644
--- a/module/upms/NPin.Framework.Upms.Domain/Repositories/IUserRepository.cs
+++ b/module/upms/NPin.Framework.Upms.Domain/Repositories/IUserRepository.cs
@@ -1,9 +1,22 @@
-using NPin.Framework.SqlSugarCore.Abstractions;
-using NPin.Framework.Upms.Domain.Entities;
-
-namespace NPin.Framework.Upms.Domain.Repositories;
-
-public interface IUserRepository: ISqlSugarRepository
-{
- // Task<>
+using NPin.Framework.SqlSugarCore.Abstractions;
+using NPin.Framework.Upms.Domain.Entities;
+
+namespace NPin.Framework.Upms.Domain.Repositories;
+
+public interface IUserRepository : ISqlSugarRepository
+{
+ ///
+ /// 获取用户所有信息,即所有的关联关系
+ ///
+ ///
+ ///
+ Task GetAllInfoAsync(Guid userId);
+
+ ///
+ /// 获取用户列表
+ /// 包含所有信息,即所有的关联关系
+ ///
+ ///
+ ///
+ Task> GetListUserAllInfoAsync(List userIds);
}
\ No newline at end of file
diff --git a/module/upms/NPin.Framework.Upms.SqlSugarCore/Repositories/UserRepository.cs b/module/upms/NPin.Framework.Upms.SqlSugarCore/Repositories/UserRepository.cs
new file mode 100644
index 0000000..58f485b
--- /dev/null
+++ b/module/upms/NPin.Framework.Upms.SqlSugarCore/Repositories/UserRepository.cs
@@ -0,0 +1,38 @@
+using NPin.Framework.SqlSugarCore.Abstractions;
+using NPin.Framework.SqlSugarCore.Repositories;
+using NPin.Framework.Upms.Domain.Entities;
+using NPin.Framework.Upms.Domain.Repositories;
+using Volo.Abp.DependencyInjection;
+
+namespace NPin.Framework.Upms.SqlSugarCore.Repositories;
+
+public class UserRepository : SqlSugarRepository, IUserRepository, ITransientDependency
+{
+ public UserRepository(ISugarDbContextProvider sugarDbContextProvider) : base(
+ sugarDbContextProvider)
+ {
+ }
+
+ public async Task GetAllInfoAsync(Guid userId)
+ {
+ var ret = await DbQueryable
+ .Includes(u => u.Metadata)
+ .Includes(u => u.Roles.Where(r => r.IsDeleted == false).ToList())
+ .Includes(u => u.Posts.Where(p => p.IsDeleted == false).ToList())
+ .Includes(u => u.Organizations.Where(o => o.IsDeleted == false).ToList())
+ .InSingleAsync(userId);
+ return ret;
+ }
+
+ public async Task> GetListUserAllInfoAsync(List userIds)
+ {
+ var ret = await DbQueryable
+ .Where(u => userIds.Contains(u.Id))
+ .Includes(u => u.Metadata)
+ .Includes(u => u.Roles.Where(r => r.IsDeleted == false).ToList())
+ .Includes(u => u.Posts.Where(p => p.IsDeleted == false).ToList())
+ .Includes(u => u.Organizations.Where(o => o.IsDeleted == false).ToList())
+ .ToListAsync();
+ return ret;
+ }
+}
\ No newline at end of file