fix: 创建租户时自动创建默认角色并分配给管理员
- 在 CreateTenantManuallyCommandHandler 中添加创建 tenant-admin 角色逻辑 - 自动将默认角色分配给租户管理员用户 - 更新 Saga 补偿逻辑,失败时回滚角色和用户角色关系 - 修复 TenantsController 中 4 处 CS8625 可空引用类型警告 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -127,7 +127,7 @@ public sealed class TenantsController(IMediator mediator) : BaseApiController
|
||||
await mediator.Send(updatedCommand, cancellationToken);
|
||||
|
||||
// 3. 返回成功
|
||||
return ApiResponse<object>.Ok(null, "更新成功");
|
||||
return ApiResponse.Success("更新成功");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -154,7 +154,7 @@ public sealed class TenantsController(IMediator mediator) : BaseApiController
|
||||
await mediator.Send(updatedCommand, cancellationToken);
|
||||
|
||||
// 3. 返回成功
|
||||
return ApiResponse<object>.Ok(null, "更新成功");
|
||||
return ApiResponse.Success("更新成功");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -236,7 +236,7 @@ public sealed class TenantsController(IMediator mediator) : BaseApiController
|
||||
await mediator.Send(updatedCommand, cancellationToken);
|
||||
|
||||
// 3. 返回成功
|
||||
return ApiResponse<object>.Ok(null, command.Approve ? "审核通过" : "审核驳回");
|
||||
return ApiResponse.Success(command.Approve ? "审核通过" : "审核驳回");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -333,7 +333,7 @@ public sealed class TenantsController(IMediator mediator) : BaseApiController
|
||||
await mediator.Send(command, cancellationToken);
|
||||
|
||||
// 3. 返回成功
|
||||
return ApiResponse<object>.Ok(null, "已释放审核");
|
||||
return ApiResponse.Success("已释放审核");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -24,14 +24,16 @@ namespace TakeoutSaaS.Application.App.Tenants.Handlers;
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该操作涉及跨数据库(App + Identity)写入,采用 Saga 补偿模式保证数据一致性:
|
||||
/// 1. 先在 Identity 库创建管理员账号
|
||||
/// 1. 先在 Identity 库创建管理员账号、默认角色(tenant-admin)并分配角色
|
||||
/// 2. 再在 App 库创建租户、订阅、认证资料
|
||||
/// 3. 如果步骤 2 失败,回滚步骤 1(删除管理员账号)
|
||||
/// 3. 如果步骤 2 失败,回滚步骤 1(删除用户角色关系、角色、管理员账号)
|
||||
/// </remarks>
|
||||
public sealed class CreateTenantManuallyCommandHandler(
|
||||
ITenantRepository tenantRepository,
|
||||
ITenantPackageRepository tenantPackageRepository,
|
||||
IIdentityUserRepository identityUserRepository,
|
||||
IRoleRepository roleRepository,
|
||||
IUserRoleRepository userRoleRepository,
|
||||
IPasswordHasher<IdentityUser> passwordHasher,
|
||||
ICurrentUserAccessor currentUserAccessor,
|
||||
IIdGenerator idGenerator,
|
||||
@@ -107,13 +109,32 @@ public sealed class CreateTenantManuallyCommandHandler(
|
||||
await identityUserRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("租户 {TenantId} 管理员账号 {AdminUserId} 创建成功", tenantId, adminUser.Id);
|
||||
|
||||
// 10. 创建租户默认角色(tenant-admin)
|
||||
var defaultRole = new Role
|
||||
{
|
||||
Id = idGenerator.NextId(),
|
||||
Portal = PortalType.Tenant,
|
||||
TenantId = tenantId,
|
||||
Code = "tenant-admin",
|
||||
Name = "租户管理员",
|
||||
Description = "租户默认管理员角色,拥有租户端全部权限"
|
||||
};
|
||||
await roleRepository.AddAsync(defaultRole, cancellationToken);
|
||||
await roleRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("租户 {TenantId} 默认角色 {RoleId} 创建成功", tenantId, defaultRole.Id);
|
||||
|
||||
// 11. 为管理员用户分配默认角色
|
||||
await userRoleRepository.ReplaceUserRolesAsync(PortalType.Tenant, tenantId, adminUser.Id, [defaultRole.Id], cancellationToken);
|
||||
await userRoleRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("租户 {TenantId} 管理员 {AdminUserId} 已分配角色 {RoleId}", tenantId, adminUser.Id, defaultRole.Id);
|
||||
|
||||
try
|
||||
{
|
||||
// 10. 根据是否跳过审核确定租户状态和认证状态
|
||||
// 12. 根据是否跳过审核确定租户状态和认证状态
|
||||
var tenantStatus = request.IsSkipApproval ? TenantStatus.Active : TenantStatus.PendingReview;
|
||||
var verificationStatus = request.IsSkipApproval ? TenantVerificationStatus.Approved : TenantVerificationStatus.Pending;
|
||||
|
||||
// 11. 创建租户实体
|
||||
// 13. 创建租户实体
|
||||
var tenant = new Tenant
|
||||
{
|
||||
Id = tenantId,
|
||||
@@ -138,7 +159,7 @@ public sealed class CreateTenantManuallyCommandHandler(
|
||||
PrimaryOwnerUserId = adminUser.Id
|
||||
};
|
||||
|
||||
// 12. 创建订阅实体
|
||||
// 14. 创建订阅实体
|
||||
var subscription = new TenantSubscription
|
||||
{
|
||||
Id = idGenerator.NextId(),
|
||||
@@ -152,7 +173,7 @@ public sealed class CreateTenantManuallyCommandHandler(
|
||||
Notes = request.SubscriptionNotes?.Trim()
|
||||
};
|
||||
|
||||
// 13. 创建认证资料实体
|
||||
// 15. 创建认证资料实体
|
||||
var verification = new TenantVerificationProfile
|
||||
{
|
||||
Id = idGenerator.NextId(),
|
||||
@@ -172,10 +193,10 @@ public sealed class CreateTenantManuallyCommandHandler(
|
||||
ReviewedBy = request.IsSkipApproval ? currentUserAccessor.UserId : null
|
||||
};
|
||||
|
||||
// 14. 根据套餐配额创建配额使用记录
|
||||
// 16. 根据套餐配额创建配额使用记录
|
||||
var quotaUsages = CreateQuotaUsagesFromPackage(tenantId, package);
|
||||
|
||||
// 15. 创建账单记录和支付记录(可选)
|
||||
// 17. 创建账单记录和支付记录(可选)
|
||||
TenantBillingStatement? billing = null;
|
||||
TenantPayment? payment = null;
|
||||
if (request.CreateBilling)
|
||||
@@ -193,46 +214,58 @@ public sealed class CreateTenantManuallyCommandHandler(
|
||||
request.BillingStatus,
|
||||
request.BillingNotes);
|
||||
|
||||
// 16. 如果账单状态为已支付,创建支付记录
|
||||
// 18. 如果账单状态为已支付,创建支付记录
|
||||
if (request.BillingStatus == TenantBillingStatus.Paid && billing.AmountDue > 0)
|
||||
{
|
||||
payment = CreatePaymentFromBilling(tenantId, billing);
|
||||
}
|
||||
}
|
||||
|
||||
// 17. 【Saga 步骤 2】在 App 库创建租户、订阅、认证资料、配额使用记录、账单、支付记录(使用事务)
|
||||
// 19. 【Saga 步骤 2】在 App 库创建租户、订阅、认证资料、配额使用记录、账单、支付记录(使用事务)
|
||||
await tenantRepository.CreateTenantWithRelatedDataAsync(tenant, subscription, verification, quotaUsages, billing, payment, cancellationToken);
|
||||
logger.LogInformation("租户 {TenantId} 及相关数据创建成功,跳过审核:{IsSkipApproval}", tenantId, request.IsSkipApproval);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 18. 【Saga 补偿】App 库操作失败,回滚 Identity 库的管理员账号
|
||||
// 18. 【Saga 补偿】App 库操作失败,回滚 Identity 库的管理员账号、角色和用户角色关系
|
||||
// 记录完整异常信息(包括内部异常)
|
||||
var fullErrorMessage = ex.InnerException?.Message ?? ex.Message;
|
||||
logger.LogError(ex, "租户 {TenantId} 创建失败,错误详情:{ErrorDetail},开始回滚管理员账号 {AdminUserId}",
|
||||
tenantId, fullErrorMessage, adminUser.Id);
|
||||
logger.LogError(ex, "租户 {TenantId} 创建失败,错误详情:{ErrorDetail},开始回滚 Identity 数据",
|
||||
tenantId, fullErrorMessage);
|
||||
|
||||
try
|
||||
{
|
||||
// 19. 回滚用户角色关系(清空用户的角色)
|
||||
await userRoleRepository.ReplaceUserRolesAsync(PortalType.Tenant, tenantId, adminUser.Id, [], cancellationToken);
|
||||
await userRoleRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("用户角色关系回滚成功,用户 {AdminUserId}", adminUser.Id);
|
||||
|
||||
// 20. 回滚默认角色
|
||||
await roleRepository.DeleteAsync(PortalType.Tenant, tenantId, defaultRole.Id, cancellationToken);
|
||||
await roleRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("默认角色 {RoleId} 回滚成功", defaultRole.Id);
|
||||
|
||||
// 21. 回滚管理员账号
|
||||
await identityUserRepository.RemoveAsync(adminUser, cancellationToken);
|
||||
await identityUserRepository.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("管理员账号 {AdminUserId} 回滚成功", adminUser.Id);
|
||||
}
|
||||
catch (Exception rollbackEx)
|
||||
{
|
||||
// 19. 补偿失败,记录严重错误(需要人工介入)
|
||||
// 22. 补偿失败,记录严重错误(需要人工介入)
|
||||
logger.LogCritical(
|
||||
rollbackEx,
|
||||
"严重:租户 {TenantId} 创建失败且管理员账号 {AdminUserId} 回滚失败,需要人工介入清理",
|
||||
"严重:租户 {TenantId} 创建失败且 Identity 数据回滚失败,需要人工介入清理(用户 {AdminUserId},角色 {RoleId})",
|
||||
tenantId,
|
||||
adminUser.Id);
|
||||
adminUser.Id,
|
||||
defaultRole.Id);
|
||||
}
|
||||
|
||||
// 20. 重新抛出业务异常(包含详细错误信息)
|
||||
// 23. 重新抛出业务异常(包含详细错误信息)
|
||||
throw new BusinessException(ErrorCodes.InternalServerError, $"创建租户失败:{fullErrorMessage}");
|
||||
}
|
||||
|
||||
// 21. 查询并返回租户详情
|
||||
// 24. 查询并返回租户详情
|
||||
var detail = await mediator.Send(new GetTenantDetailQuery { TenantId = tenantId }, cancellationToken);
|
||||
return detail ?? throw new BusinessException(ErrorCodes.InternalServerError, "创建租户成功但查询详情失败");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user