Browse Source

feat(UserAppService): 确保更新用户状态与DTO一致, 填充角色信息

🐛 fix(UserManager): 修复删除用户时的并发冲突问题, 增加重试机制

 feat(ProjectNameApplicationTestModule): 添加默认的双因素令牌提供者配置

 feat(Migrate.ps1): 动态查找所有可用的DbContext, 提升灵活性
pull/1149/head
feijie 1 year ago
parent
commit
4dea327cb7
  1. 73
      aspnet-core/templates/aio/content/migrations/Migrate.ps1
  2. 23
      aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/Users/UserAppService.cs
  3. 77
      aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/UserManager.cs
  4. 2
      aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/ProjectNameApplicationTestModule.cs

73
aspnet-core/templates/aio/content/migrations/Migrate.ps1

@ -9,15 +9,76 @@ $env:FROM_MIGRATION = "true"
# 定义项目路径 # 定义项目路径
$projectPath = Resolve-Path (Join-Path $PSScriptRoot "..") $projectPath = Resolve-Path (Join-Path $PSScriptRoot "..")
# 定义可用的DbContext # 定义函数来动态查找所有可用的DbContext
$dbContexts = @{ function Get-AvailableDbContexts {
"1" = @{ $migrationsPath = Join-Path $projectPath "migrations"
Name = "PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DatabaseManagementName" $dbContexts = @{}
Context = "SingleMigrationsDbContext" $counter = 1
Factory = "SingleMigrationsDbContextFactory"
# 查找所有包含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选择菜单 # 显示DbContext选择菜单
function Show-DbContextMenu { function Show-DbContextMenu {
$host.UI.RawUI.BackgroundColor = "Black" $host.UI.RawUI.BackgroundColor = "Black"

23
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.Position,
input.IsActive); 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对象 // 返回DTO对象
return await MapToUserDtoAsync(user); return await MapToUserDtoAsync(user);
} }
@ -121,7 +132,7 @@ namespace PackageName.CompanyName.ProjectName.Users
foreach (var user in users) foreach (var user in users)
{ {
var userDto = ObjectMapper.Map<User, UserItemDto>(user); var userDto = ObjectMapper.Map<User, UserItemDto>(user);
// 填充角色信息 // 填充角色信息
if (user.IdentityUser != null) if (user.IdentityUser != null)
{ {
@ -129,7 +140,7 @@ namespace PackageName.CompanyName.ProjectName.Users
userDto.RoleNames = string.Join("、", roles); userDto.RoleNames = string.Join("、", roles);
userDto.IsActive = user.IdentityUser.LockoutEnd == null || user.IdentityUser.LockoutEnd < DateTimeOffset.Now; userDto.IsActive = user.IdentityUser.LockoutEnd == null || user.IdentityUser.LockoutEnd < DateTimeOffset.Now;
} }
userDtos.Add(userDto); userDtos.Add(userDto);
} }
@ -182,17 +193,19 @@ namespace PackageName.CompanyName.ProjectName.Users
/// <summary> /// <summary>
/// 将User实体映射为UserDto,并填充权限信息 /// 将User实体映射为UserDto,并填充权限信息
/// </summary> /// </summary>
private Task<UserDto> MapToUserDtoAsync(User user) private async Task<UserDto> MapToUserDtoAsync(User user)
{ {
var userDto = ObjectMapper.Map<User, UserDto>(user); var userDto = ObjectMapper.Map<User, UserDto>(user);
// 设置用户状态 // 设置用户状态和角色信息
if (user.IdentityUser != null) 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; userDto.IsActive = user.IdentityUser.LockoutEnd == null || user.IdentityUser.LockoutEnd < DateTimeOffset.Now;
} }
return Task.FromResult(userDto); return userDto;
} }
} }
} }

77
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))); 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( var user = new User(
@ -221,29 +232,61 @@ namespace PackageName.CompanyName.ProjectName.Users
/// <returns>操作任务</returns> /// <returns>操作任务</returns>
public async Task DeleteAsync(Guid id) public async Task DeleteAsync(Guid id)
{ {
// 获取用户 // 最大重试次数
var user = await _userRepository.GetAsync(id); const int maxRetries = 3;
if (user == null) int retryCount = 0;
{
throw new UserFriendlyException("用户不存在");
}
// 删除Identity用户 while (true)
var identityUser = await _identityUserManager.FindByIdAsync(user.IdentityUserId.ToString());
if (identityUser != null)
{ {
var result = await _identityUserManager.DeleteAsync(identityUser); try
if (!result.Succeeded)
{ {
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}次重试...");
}
}
} }
/// <summary> /// <summary>

2
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.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false; options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false; options.Password.RequireUppercase = false;
// 添加默认的双因素令牌提供者配置
options.Tokens.ProviderMap.Add("Default", new TokenProviderDescriptor(typeof(EmailTokenProvider<IdentityUser>)));
}); });
Configure<AbpClaimsPrincipalFactoryOptions>(options => Configure<AbpClaimsPrincipalFactoryOptions>(options =>
{ {

Loading…
Cancel
Save