diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantsController.cs index 3ab1c0a..7ce7cb0 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/TenantsController.cs @@ -127,7 +127,7 @@ public sealed class TenantsController(IMediator mediator) : BaseApiController await mediator.Send(updatedCommand, cancellationToken); // 3. 返回成功 - return ApiResponse.Ok(null, "更新成功"); + return ApiResponse.Success("更新成功"); } /// @@ -154,7 +154,7 @@ public sealed class TenantsController(IMediator mediator) : BaseApiController await mediator.Send(updatedCommand, cancellationToken); // 3. 返回成功 - return ApiResponse.Ok(null, "更新成功"); + return ApiResponse.Success("更新成功"); } /// @@ -236,7 +236,7 @@ public sealed class TenantsController(IMediator mediator) : BaseApiController await mediator.Send(updatedCommand, cancellationToken); // 3. 返回成功 - return ApiResponse.Ok(null, command.Approve ? "审核通过" : "审核驳回"); + return ApiResponse.Success(command.Approve ? "审核通过" : "审核驳回"); } /// @@ -333,7 +333,7 @@ public sealed class TenantsController(IMediator mediator) : BaseApiController await mediator.Send(command, cancellationToken); // 3. 返回成功 - return ApiResponse.Ok(null, "已释放审核"); + return ApiResponse.Success("已释放审核"); } /// diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantManuallyCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantManuallyCommandHandler.cs index a7a0c10..2ce065d 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantManuallyCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantManuallyCommandHandler.cs @@ -24,14 +24,16 @@ namespace TakeoutSaaS.Application.App.Tenants.Handlers; /// /// /// 该操作涉及跨数据库(App + Identity)写入,采用 Saga 补偿模式保证数据一致性: -/// 1. 先在 Identity 库创建管理员账号 +/// 1. 先在 Identity 库创建管理员账号、默认角色(tenant-admin)并分配角色 /// 2. 再在 App 库创建租户、订阅、认证资料 -/// 3. 如果步骤 2 失败,回滚步骤 1(删除管理员账号) +/// 3. 如果步骤 2 失败,回滚步骤 1(删除用户角色关系、角色、管理员账号) /// public sealed class CreateTenantManuallyCommandHandler( ITenantRepository tenantRepository, ITenantPackageRepository tenantPackageRepository, IIdentityUserRepository identityUserRepository, + IRoleRepository roleRepository, + IUserRoleRepository userRoleRepository, IPasswordHasher 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, "创建租户成功但查询详情失败"); }