refactor: 重构多租户功能

main
NoahLan 6 months ago
parent d069ff4727
commit caa9d30940

@ -48,58 +48,4 @@ public class DbConnOptions
/// 开启Saas多租户 /// 开启Saas多租户
/// </summary> /// </summary>
public bool EnabledSaasMultiTenancy { get; set; } = false; public bool EnabledSaasMultiTenancy { get; set; } = false;
/// <summary>
/// 默认租户库连接,如果不填,那就是默认库的地址
/// </summary>
public string? MasterSaasMultiTenancyUrl { get; set; }
/// <summary>
/// Saas租户连接
/// </summary>
public List<SaasMultiTenancyOptions>? 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
{
/// <summary>
/// 租户名称标识
/// </summary>
public string Name { get; set; }
/// <summary>
/// 连接Url
/// </summary>
public string Url { get; set; }
} }

@ -1,6 +1,6 @@
namespace NPin.Framework.SqlSugarCore.Abstractions; namespace NPin.Framework.SqlSugarCore.Abstractions;
[AttributeUsage(AttributeTargets.Class)] [AttributeUsage(AttributeTargets.Class)]
public class MasterTenantAttribute : Attribute public class DefaultTenantTableAttribute : Attribute
{ {
} }

@ -22,6 +22,10 @@ public class SqlSugarDbContext : ISqlSugarDbContext
public ISqlSugarClient SqlSugarClient { get; private set; } public ISqlSugarClient SqlSugarClient { get; private set; }
public DbConnOptions Options => LazyServiceProvider.LazyGetRequiredService<IOptions<DbConnOptions>>().Value; public DbConnOptions Options => LazyServiceProvider.LazyGetRequiredService<IOptions<DbConnOptions>>().Value;
private AbpDbConnectionOptions _connectionOptions =>
LazyServiceProvider.LazyGetRequiredService<IOptions<AbpDbConnectionOptions>>().Value;
public ICurrentUser CurrentUser => LazyServiceProvider.GetRequiredService<ICurrentUser>(); public ICurrentUser CurrentUser => LazyServiceProvider.GetRequiredService<ICurrentUser>();
public ICurrentTenant CurrentTenant => LazyServiceProvider.LazyGetRequiredService<ICurrentTenant>(); public ICurrentTenant CurrentTenant => LazyServiceProvider.LazyGetRequiredService<ICurrentTenant>();
public IDataFilter DataFilter => LazyServiceProvider.LazyGetRequiredService<IDataFilter>(); public IDataFilter DataFilter => LazyServiceProvider.LazyGetRequiredService<IDataFilter>();
@ -66,6 +70,15 @@ public class SqlSugarDbContext : ISqlSugarDbContext
/// <returns></returns> /// <returns></returns>
protected virtual string GetCurrentConnectionString() protected virtual string GetCurrentConnectionString()
{ {
var defaultUrl = Options.Url ??
_connectionOptions.GetConnectionStringOrNull(ConnectionStrings.DefaultConnectionStringName);
// 如果未开启多租户返回db url 或者 默认连接字符串
if (!Options.EnabledSaasMultiTenancy)
{
return defaultUrl;
}
// 开启多租户
var connectionStringResolver = LazyServiceProvider.LazyGetRequiredService<IConnectionStringResolver>(); var connectionStringResolver = LazyServiceProvider.LazyGetRequiredService<IConnectionStringResolver>();
var connectionString = connectionStringResolver.ResolveAsync().Result; var connectionString = connectionStringResolver.ResolveAsync().Result;
@ -73,15 +86,7 @@ public class SqlSugarDbContext : ISqlSugarDbContext
if (string.IsNullOrWhiteSpace(connectionString)) if (string.IsNullOrWhiteSpace(connectionString))
{ {
Volo.Abp.Check.NotNull(Options.Url, "租户默认库Default未找到"); Volo.Abp.Check.NotNull(Options.Url, "租户默认库Default未找到");
connectionString = Options.Url; connectionString = defaultUrl;
}
//如果当前租户是主库,单独使用主要库
if (CurrentTenant.Name == DbConnOptions.MasterTenantName)
{
var conStrOrNull = Options.GetMasterSaasMultiTenancy();
Volo.Abp.Check.NotNull(conStrOrNull, "租户主库Master未找到");
connectionString = conStrOrNull.Url;
} }
return connectionString!; return connectionString!;
@ -254,6 +259,10 @@ public class SqlSugarDbContext : ISqlSugarDbContext
column.IsNullable = true; column.IsNullable = true;
} }
if (property.Name == "ConcurrencyStamp")
{
column.IsIgnore = true;
}
if (property.PropertyType == typeof(ExtraPropertyDictionary)) if (property.PropertyType == typeof(ExtraPropertyDictionary))
{ {
column.IsIgnore = true; column.IsIgnore = true;

@ -7,11 +7,10 @@ using NPin.Framework.TenantManagement.Application.Contracts;
using NPin.Framework.TenantManagement.Application.Contracts.Dtos; using NPin.Framework.TenantManagement.Application.Contracts.Dtos;
using NPin.Framework.TenantManagement.Domain; using NPin.Framework.TenantManagement.Domain;
using SqlSugar; using SqlSugar;
using Volo.Abp;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
using Volo.Abp.Data; using Volo.Abp.Data;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
using Volo.Abp.Modularity; using Volo.Abp.Uow;
namespace NPin.Framework.TenantManagement.Application; namespace NPin.Framework.TenantManagement.Application;
@ -114,6 +113,7 @@ public class TenantService : NPinCrudAppService<TenantAggregateRoot, TenantGetOu
[HttpPost("tenant/init/{id}")] [HttpPost("tenant/init/{id}")]
public async Task InitAsync([FromRoute] Guid id) public async Task InitAsync([FromRoute] Guid id)
{ {
await CurrentUnitOfWork.SaveChangesAsync();
using (CurrentTenant.Change(id)) using (CurrentTenant.Change(id))
{ {
// 初始化 租户数据库/表 结构 // 初始化 租户数据库/表 结构
@ -130,10 +130,16 @@ public class TenantService : NPinCrudAppService<TenantAggregateRoot, TenantGetOu
private async Task CodeFirst(IServiceProvider service) private async Task CodeFirst(IServiceProvider service)
{ {
var moduleContainer = service.GetRequiredService<IModuleContainer>(); var moduleContainer = service.GetRequiredService<IModuleContainer>();
var db = await _repository.GetDbContextAsync();
// 没有数据库,不能创建工作单元,先创建库再关闭
ISqlSugarClient db = null;
using (var uow = UnitOfWorkManager.Begin(requiresNew: true, isTransactional: false))
{
db = await _repository.GetDbContextAsync();
// 尝试创建数据库 // 尝试创建数据库
db.DbMaintenance.CreateDatabase(); db.DbMaintenance.CreateDatabase();
await uow.CompleteAsync();
}
List<Type> types = new List<Type>(); List<Type> types = new List<Type>();
foreach (var module in moduleContainer.Modules) foreach (var module in moduleContainer.Modules)
@ -141,6 +147,7 @@ public class TenantService : NPinCrudAppService<TenantAggregateRoot, TenantGetOu
types.AddRange(module.Assembly.GetTypes() types.AddRange(module.Assembly.GetTypes()
.Where(x => x.GetCustomAttribute<IgnoreCodeFirstAttribute>() == null) .Where(x => x.GetCustomAttribute<IgnoreCodeFirstAttribute>() == null)
.Where(x => x.GetCustomAttribute<SugarTable>() != null) .Where(x => x.GetCustomAttribute<SugarTable>() != null)
.Where(x => x.GetCustomAttribute<DefaultTenantTableAttribute>() is null)
.Where(x => x.GetCustomAttribute<SplitTableAttribute>() is null)); .Where(x => x.GetCustomAttribute<SplitTableAttribute>() is null));
} }

@ -1,6 +1,5 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Volo.Abp;
using Volo.Abp.Data; using Volo.Abp.Data;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
@ -40,11 +39,22 @@ public class NPinMultiTenantConnectionStringResolver : DefaultConnectionStringRe
} }
var tenantDefaultConnectionString = tenant.ConnectionStrings?.Default; 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; var connString = tenant.ConnectionStrings?.FirstOrDefault().Value;
if (!connString.IsNullOrWhiteSpace()) if (!connString.IsNullOrWhiteSpace())
{ {
return connString; // Found for the tenant
return connString!;
} }
// 库隔离 // 库隔离

@ -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<AbpMultiTenancyResource> StringLocalizer { get; }
public NPinTenantConfigurationProvider(
ITenantResolver tenantResolver,
ITenantStore tenantStore,
ITenantResolveResultAccessor tenantResolveResultAccessor,
IStringLocalizer<AbpMultiTenancyResource> stringLocalizer)
{
TenantResolver = tenantResolver;
TenantStore = tenantStore;
TenantResolveResultAccessor = tenantResolveResultAccessor;
StringLocalizer = stringLocalizer;
}
public async Task<TenantConfiguration?> 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<TenantConfiguration?> FindTenantAsync(string tenantIdOrName)
{
if (Guid.TryParse(tenantIdOrName, out var parsedTenantId))
{
return await TenantStore.FindAsync(parsedTenantId);
}
else
{
return await TenantStore.FindAsync(tenantIdOrName);
}
}
}

@ -16,25 +16,35 @@ public class SqlSugarTenantStore: ITenantStore
protected ICurrentTenant CurrentTenant { get; } protected ICurrentTenant CurrentTenant { get; }
protected IDistributedCache<TenantCacheItem> Cache { get; } protected IDistributedCache<TenantCacheItem> Cache { get; }
public SqlSugarTenantStore(ISqlSugarTenantRepository tenantRepository, ICurrentTenant currentTenant, IDistributedCache<TenantCacheItem> cache) public SqlSugarTenantStore(ISqlSugarTenantRepository tenantRepository, ICurrentTenant currentTenant,
IDistributedCache<TenantCacheItem> cache)
{ {
TenantRepository = tenantRepository; TenantRepository = tenantRepository;
CurrentTenant = currentTenant; CurrentTenant = currentTenant;
Cache = cache; Cache = cache;
} }
public TenantConfiguration? Find(string name)
{
throw new NotImplementedException("请使用异步方法");
}
public Task<TenantConfiguration?> FindAsync(string name) public TenantConfiguration? Find(Guid id)
{ {
throw new NotImplementedException(); throw new NotImplementedException("请使用异步方法");
} }
public Task<TenantConfiguration?> FindAsync(Guid id) public async Task<TenantConfiguration?> FindAsync(string name)
{ {
throw new NotImplementedException(); return (await GetCacheItemAsync(null, name)).Value;
} }
protected virtual async Task<TenantCacheItem> GetCacheItemAsync(Guid? id, string name) public async Task<TenantConfiguration?> FindAsync(Guid id)
{
return (await GetCacheItemAsync(id, null)).Value;
}
protected virtual async Task<TenantCacheItem> GetCacheItemAsync(Guid? id, string? name)
{ {
var cacheKey = CalculateCacheKey(id, name); var cacheKey = CalculateCacheKey(id, name);
var cacheItem = await Cache.GetAsync(cacheKey, considerUow: true); var cacheItem = await Cache.GetAsync(cacheKey, considerUow: true);
@ -84,26 +94,12 @@ public class SqlSugarTenantStore: ITenantStore
return tenantConfiguration; return tenantConfiguration;
} }
private ConnectionStrings? MapToString(string tenantConnectionString) private ConnectionStrings MapToString(string tenantConnectionString)
{ {
var connectionStrings = new ConnectionStrings
//tenantConnectionString = tenantConnectionString.TrimEnd(';'); {
//var strSpiteds = tenantConnectionString.Split(";"); [ConnectionStrings.DefaultConnectionStringName] = tenantConnectionString
//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; return connectionStrings;
} }
@ -111,14 +107,4 @@ public class SqlSugarTenantStore: ITenantStore
{ {
return TenantCacheItem.CalculateCacheKey(id, name); return TenantCacheItem.CalculateCacheKey(id, name);
} }
public TenantConfiguration? Find(string name)
{
throw new NotImplementedException("请使用异步方法");
}
public TenantConfiguration? Find(Guid id)
{
throw new NotImplementedException("请使用异步方法");
}
} }

@ -10,7 +10,7 @@ using Check = Volo.Abp.Check;
namespace NPin.Framework.TenantManagement.Domain; namespace NPin.Framework.TenantManagement.Domain;
[SugarTable("SysTenant", "租户表")] [SugarTable("SysTenant", "租户表")]
[MasterTenant] [DefaultTenantTable]
public class TenantAggregateRoot : FullAuditedAggregateRoot<Guid>, IHasEntityVersion public class TenantAggregateRoot : FullAuditedAggregateRoot<Guid>, IHasEntityVersion
{ {
[SugarColumn(IsPrimaryKey = true)] public override Guid Id { get; protected set; } [SugarColumn(IsPrimaryKey = true)] public override Guid Id { get; protected set; }

@ -1,17 +1,12 @@
using NPin.Framework.SqlSugarCore.Abstractions; using Volo.Abp.Data;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
namespace NPin.Framework.TenantManagement.Domain; namespace NPin.Framework.TenantManagement.Domain;
public static class TenantManagementExtensions public static class TenantManagementExtensions
{ {
public static IDisposable ChangeMaster(this ICurrentTenant currentTenant)
{
return currentTenant.Change(null, DbConnOptions.MasterTenantName);
}
public static IDisposable ChangeDefault(this ICurrentTenant currentTenant) public static IDisposable ChangeDefault(this ICurrentTenant currentTenant)
{ {
return currentTenant.Change(null, DbConnOptions.DefaultTenantName); return currentTenant.Change(null, ConnectionStrings.DefaultConnectionStringName);
} }
} }

@ -0,0 +1,34 @@
using NPin.Framework.Upms.Domain.Shared.Dtos;
namespace NPin.Framework.Upms.Domain.Shared.Caches;
public class UserInfoCacheItem
{
/// <summary>
/// 用户完整信息
/// </summary>
public UserFullDto Info { get; set; }
public UserInfoCacheItem(UserFullDto info)
{
Info = info;
}
}
public class UserInfoCacheKey
{
/// <summary>
/// 用户ID
/// </summary>
public Guid UserId { get; set; }
public UserInfoCacheKey(Guid userId)
{
UserId = userId;
}
public override string ToString()
{
return $"User:{UserId}";
}
}

@ -11,12 +11,13 @@ public class UserFullDto
public UserDto User { get; set; } = new(); public UserDto User { get; set; } = new();
// Relations // Relations
public HashSet<RoleDto> Roles { get; set; } = []; public List<RoleDto> Roles { get; set; } = [];
public HashSet<PostDto> Posts { get; set; } = []; public List<PostDto> Posts { get; set; } = [];
public HashSet<OrganizationDto> Organizations { get; set; } = []; public List<OrganizationDto> Organizations { get; set; } = [];
public HashSet<string> PostCodes { get; set; } = [];
public HashSet<string> RoleCodes { get; set; } = []; public HashSet<string> RoleCodes { get; set; } = [];
public HashSet<string> PostCodes { get; set; } = [];
public HashSet<string> OrganizationCodes { get; set; } = [];
public HashSet<string> PermissionCodes { get; set; } = []; public HashSet<string> PermissionCodes { get; set; } = [];
} }

@ -6,4 +6,9 @@
public class UserCreatedEventArgs public class UserCreatedEventArgs
{ {
public Guid UserId { get; set; } public Guid UserId { get; set; }
public UserCreatedEventArgs(Guid userId)
{
UserId = userId;
}
} }

@ -23,13 +23,13 @@ public class RefreshTokenMiddleware : IMiddleware, ITransientDependency
{ {
var userId = Guid.Parse(authResult.Principal.FindFirst(AbpClaimTypes.UserId).Value); var userId = Guid.Parse(authResult.Principal.FindFirst(AbpClaimTypes.UserId).Value);
// TODO // TODO
// var accessToken = var accessToken = "";
// var refreshToken = // var refreshToken =
context.Response.Headers["access_token"] = ""; context.Response.Headers["access_token"] = "";
context.Response.Headers["refresh_token"] = ""; context.Response.Headers["refresh_token"] = "";
// 请求头替换 // 请求头替换
context.Request.Headers["Authorization"] = $"Bearer {""}"; context.Request.Headers["Authorization"] = $"Bearer {accessToken}";
} }
} }
await next(context); await next(context);

@ -1,12 +1,173 @@
using Volo.Abp.Domain.Services; 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; namespace NPin.Framework.Upms.Domain.Managers;
public interface IAccountManager public interface IAccountManager
{ {
/// <summary>
/// 创建 RefreshToken
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
string CreateRefreshToken(Guid userId);
/// <summary>
/// 获取 AccessToken
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
Task<string> CreateAccessTokenAsync(Guid userId);
/// <summary>
/// 重置密码
/// </summary>
/// <param name="userId"></param>
/// <param name="password"></param>
/// <returns></returns>
Task<bool> RestPasswordAsync(Guid userId, string password);
/// <summary>
/// 修改密码
/// </summary>
/// <param name="userId"></param>
/// <param name="newPassword"></param>
/// <param name="oldPassword"></param>
/// <returns></returns>
Task UpdatePasswordAsync(Guid userId, string newPassword, string oldPassword);
} }
public class AccountManager : DomainService, IAccountManager public class AccountManager : DomainService, IAccountManager
{ {
// private readonly 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> jwtOptions,
IOptions<RefreshJwtOptions> refreshJwtOptions,
UserManager userManager,
IHttpContextAccessor httpContextAccessor)
{
_repository = repository;
_localEventBus = localEventBus;
_jwtOptions = jwtOptions.Value;
_refreshJwtOptions = refreshJwtOptions.Value;
_userManager = userManager;
_httpContextAccessor = httpContextAccessor;
}
/// <summary>
/// 根据用户Id获取AccessToken
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public async Task<string> 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<LoginEventArgs>();
loginEto.Username = userInfo.User.Username;
loginEto.UserId = userInfo.User.Id;
// 异步
_ = _localEventBus.PublishAsync(loginEto);
}
return CreateAccessToken(UserInfoToClaims(userInfo));
}
private string CreateAccessToken(List<KeyValuePair<string, string>> 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<KeyValuePair<string, string>> UserInfoToClaims(UserFullDto dto)
{
var claims = new List<KeyValuePair<string, string>>();
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<KeyValuePair<string, string>> claims, string key, string value)
{
claims.Add(new KeyValuePair<string, string>(key, value));
}
/// <summary>
/// 重置密码(仅执行最终重置,无前序判断)
/// </summary>
/// <param name="userId"></param>
/// <param name="password"></param>
/// <returns></returns>
public async Task<bool> 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();
}
} }

@ -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;
/// <summary>
/// 用户管理器 领域服务
/// </summary>
public partial class UserManager : DomainService
{
[GeneratedRegex("^[a-zA-Z0-9]+$")]
private static partial Regex UsernameRegex();
private readonly IUserRepository _repository;
private readonly ISqlSugarRepository<RoleEntity> _repositoryRole;
private readonly ISqlSugarRepository<UserRoleEntity> _repositoryUserRole;
private readonly ISqlSugarRepository<UserPostEntity> _repositoryUserPost;
private readonly ISqlSugarRepository<UserOrganizationEntity> _repositoryUserOrg;
private IDistributedCache<UserInfoCacheItem, UserInfoCacheKey> _userCache;
private ILocalEventBus _localEventBus;
public UserManager(
IUserRepository repository,
ISqlSugarRepository<UserRoleEntity> repositoryUserRole,
ISqlSugarRepository<UserPostEntity> repositoryUserPost,
ISqlSugarRepository<UserOrganizationEntity> repositoryUserOrg,
IDistributedCache<UserInfoCacheItem, UserInfoCacheKey> userCache,
ILocalEventBus localEventBus, ISqlSugarRepository<RoleEntity> repositoryRole)
{
_repository = repository;
_repositoryUserRole = repositoryUserRole;
_repositoryUserPost = repositoryUserPost;
_repositoryUserOrg = repositoryUserOrg;
_userCache = userCache;
_localEventBus = localEventBus;
_repositoryRole = repositoryRole;
}
/// <summary>
/// 创建用户
/// </summary>
/// <param name="entity"></param>
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("手机号重复");
}
}
}
/// <summary>
/// 设置默认角色
/// </summary>
/// <param name="userId"></param>
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]);
}
}
/// <summary>
/// 给用户设置角色
/// </summary>
/// <param name="userIds"></param>
/// <param name="roleIds"></param>
[UnitOfWork]
public async Task SetRoleAsync(List<Guid> userIds, List<Guid> 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);
}
/// <summary>
/// 设置用户岗位
/// </summary>
/// <param name="userIds"></param>
/// <param name="postIds"></param>
[UnitOfWork]
public async Task SetPostAsync(List<Guid> userIds, List<Guid> 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);
}
/// <summary>
/// 设置用户组织机构
/// </summary>
/// <param name="userIds"></param>
/// <param name="orgIds"></param>
[UnitOfWork]
public async Task SetOrganizationAsync(List<Guid> userIds, List<Guid> 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);
}
/// <summary>
/// 从缓存中获取用户信息,若缓存不存在则查库
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
/// <exception cref="AbpAuthorizationException">库中无此用户403</exception>
public async Task<UserFullDto> GetInfoByCacheAsync(Guid userId)
{
// 1. 缓存获取
UserFullDto ret = null;
var tokenExpires = LazyServiceProvider.GetRequiredService<IOptions<JwtOptions>>().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<UserDto>();
ret.RoleCodes.Add(UserConst.AdminRoleCode);
ret.PermissionCodes.Add(UserConst.AdminPermissionCode);
return ret;
}
// 角色
var roleDtoList = new List<RoleDto>();
var roleCodes = new HashSet<string>();
foreach (var role in entity.Roles)
{
roleDtoList.Add(role.Adapt<RoleDto>());
roleCodes.Add(role.Code);
}
ret.Roles = roleDtoList;
ret.RoleCodes = roleCodes;
// 岗位
var postDtoList = new List<PostDto>();
var postCodes = new HashSet<string>();
foreach (var post in entity.Posts)
{
postDtoList.Add(post.Adapt<PostDto>());
postCodes.Add(post.Code);
}
ret.Posts = postDtoList;
ret.PostCodes = postCodes;
// 组织结构
var orgDtoList = new List<OrganizationDto>();
var orgCodes = new HashSet<string>();
foreach (var org in entity.Organizations)
{
orgDtoList.Add(org.Adapt<OrganizationDto>());
orgCodes.Add(org.Code);
}
ret.Organizations = orgDtoList;
ret.OrganizationCodes = orgCodes;
ret.User = entity.Adapt<UserDto>();
// TODO permissionCode
return ret;
}
}

@ -5,5 +5,18 @@ namespace NPin.Framework.Upms.Domain.Repositories;
public interface IUserRepository : ISqlSugarRepository<UserEntity> public interface IUserRepository : ISqlSugarRepository<UserEntity>
{ {
// Task<> /// <summary>
/// 获取用户所有信息,即所有的关联关系
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
Task<UserEntity> GetAllInfoAsync(Guid userId);
/// <summary>
/// 获取用户列表
/// 包含所有信息,即所有的关联关系
/// </summary>
/// <param name="userIds"></param>
/// <returns></returns>
Task<List<UserEntity>> GetListUserAllInfoAsync(List<Guid> userIds);
} }

@ -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<UserEntity>, IUserRepository, ITransientDependency
{
public UserRepository(ISugarDbContextProvider<ISqlSugarDbContext> sugarDbContextProvider) : base(
sugarDbContextProvider)
{
}
public async Task<UserEntity> 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<List<UserEntity>> GetListUserAllInfoAsync(List<Guid> 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;
}
}
Loading…
Cancel
Save