using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Npgsql; using TakeoutSaaS.Infrastructure.Identity.Options; using TakeoutSaaS.Shared.Abstractions.Tenancy; using DomainIdentityUser = TakeoutSaaS.Domain.Identity.Entities.IdentityUser; using DomainPermission = TakeoutSaaS.Domain.Identity.Entities.Permission; using DomainRole = TakeoutSaaS.Domain.Identity.Entities.Role; using DomainRolePermission = TakeoutSaaS.Domain.Identity.Entities.RolePermission; using DomainRoleTemplate = TakeoutSaaS.Domain.Identity.Entities.RoleTemplate; using DomainRoleTemplatePermission = TakeoutSaaS.Domain.Identity.Entities.RoleTemplatePermission; using DomainUserRole = TakeoutSaaS.Domain.Identity.Entities.UserRole; namespace TakeoutSaaS.Infrastructure.Identity.Persistence; /// /// 后台账号初始化种子任务 /// public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger logger) : IHostedService { /// /// 执行后台账号与权限种子。 /// /// 取消标记。 /// 异步任务。 public async Task StartAsync(CancellationToken cancellationToken) { // 1. 创建作用域并解析依赖 using var scope = serviceProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); var options = scope.ServiceProvider.GetRequiredService>().Value; var passwordHasher = scope.ServiceProvider.GetRequiredService>(); var tenantContextAccessor = scope.ServiceProvider.GetRequiredService(); // 2. 校验功能开关 if (!options.Enabled) { logger.LogInformation("AdminSeed 已禁用,跳过后台账号初始化"); return; } // 3. 确保数据库已迁移 await context.Database.MigrateAsync(cancellationToken); // 4. 校验账号配置 if (options.Users is null or { Count: 0 }) { logger.LogInformation("AdminSeed 未配置账号,跳过后台账号初始化"); return; } // 5. 写入角色模板 await SeedRoleTemplatesAsync(context, options.RoleTemplates, cancellationToken); // 6. 逐个账号处理 foreach (var userOptions in options.Users) { // 6.1 进入租户作用域 using var tenantScope = EnterTenantScope(tenantContextAccessor, userOptions.TenantId); // 6.2 查询账号并收集配置 var user = await context.IdentityUsers.FirstOrDefaultAsync(x => x.Account == userOptions.Account, cancellationToken); var roles = NormalizeValues(userOptions.Roles); var permissions = NormalizeValues(userOptions.Permissions); if (user == null) { // 6.3 创建新账号 user = new DomainIdentityUser { Id = 0, Account = userOptions.Account, DisplayName = userOptions.DisplayName, TenantId = userOptions.TenantId, MerchantId = userOptions.MerchantId, Avatar = null }; user.PasswordHash = passwordHasher.HashPassword(user, userOptions.Password); context.IdentityUsers.Add(user); logger.LogInformation("已创建后台账号 {Account}", user.Account); } else { // 6.4 更新既有账号 user.DisplayName = userOptions.DisplayName; user.TenantId = userOptions.TenantId; user.MerchantId = userOptions.MerchantId; user.PasswordHash = passwordHasher.HashPassword(user, userOptions.Password); logger.LogInformation("已更新后台账号 {Account}", user.Account); } // 6.5 确保角色存在 var existingRoles = await context.Roles .Where(r => r.TenantId == userOptions.TenantId && roles.Contains(r.Code)) .ToListAsync(cancellationToken); var existingRoleCodes = existingRoles.Select(r => r.Code).ToHashSet(StringComparer.OrdinalIgnoreCase); foreach (var code in roles) { if (existingRoleCodes.Contains(code)) { continue; } context.Roles.Add(new DomainRole { TenantId = userOptions.TenantId, Code = code, Name = code, Description = $"Seed role {code}" }); } // 6.6 确保权限存在 var existingPermissions = await context.Permissions .Where(p => p.TenantId == userOptions.TenantId && permissions.Contains(p.Code)) .ToListAsync(cancellationToken); var existingPermissionCodes = existingPermissions.Select(p => p.Code).ToHashSet(StringComparer.OrdinalIgnoreCase); foreach (var code in permissions) { if (existingPermissionCodes.Contains(code)) { continue; } context.Permissions.Add(new DomainPermission { TenantId = userOptions.TenantId, Code = code, Name = code, Description = $"Seed permission {code}" }); } // 6.7 保存基础角色/权限 await context.SaveChangesAsync(cancellationToken); // 6.8 重新加载角色/权限以获取 Id var roleEntities = await context.Roles .Where(r => r.TenantId == userOptions.TenantId && roles.Contains(r.Code)) .ToListAsync(cancellationToken); var permissionEntities = await context.Permissions .Where(p => p.TenantId == userOptions.TenantId && permissions.Contains(p.Code)) .ToListAsync(cancellationToken); // 6.9 重置用户角色 var existingUserRoles = await context.UserRoles .Where(ur => ur.TenantId == userOptions.TenantId && ur.UserId == user.Id) .ToListAsync(cancellationToken); context.UserRoles.RemoveRange(existingUserRoles); await context.SaveChangesAsync(cancellationToken); var roleIds = roleEntities.Select(r => r.Id).Distinct().ToArray(); foreach (var roleId in roleIds) { try { var alreadyExists = await context.UserRoles.AnyAsync( ur => ur.TenantId == userOptions.TenantId && ur.UserId == user.Id && ur.RoleId == roleId, cancellationToken); if (alreadyExists) { continue; } await context.UserRoles.AddAsync(new DomainUserRole { TenantId = userOptions.TenantId, UserId = user.Id, RoleId = roleId }, cancellationToken); await context.SaveChangesAsync(cancellationToken); } catch (DbUpdateException ex) when (ex.InnerException is PostgresException pg && pg.SqlState == PostgresErrorCodes.UniqueViolation) { context.ChangeTracker.Clear(); } } // 为种子角色绑定种子权限 if (permissions.Length > 0 && roleIds.Length > 0) { var permissionIds = permissionEntities.Select(p => p.Id).Distinct().ToArray(); var existingRolePermissions = await context.RolePermissions .Where(rp => rp.TenantId == userOptions.TenantId && roleIds.Contains(rp.RoleId)) .ToListAsync(cancellationToken); context.RolePermissions.RemoveRange(existingRolePermissions); await context.SaveChangesAsync(cancellationToken); var distinctRoleIds = roleIds.Distinct().ToArray(); var distinctPermissionIds = permissionIds.Distinct().ToArray(); foreach (var roleId in distinctRoleIds) { foreach (var permissionId in distinctPermissionIds) { try { var exists = await context.RolePermissions.AnyAsync( rp => rp.TenantId == userOptions.TenantId && rp.RoleId == roleId && rp.PermissionId == permissionId, cancellationToken); if (exists) { continue; } // 6.10 绑定角色与权限 await context.RolePermissions.AddAsync(new DomainRolePermission { TenantId = userOptions.TenantId, RoleId = roleId, PermissionId = permissionId }, cancellationToken); await context.SaveChangesAsync(cancellationToken); } catch (DbUpdateException ex) when (ex.InnerException is PostgresException pg && pg.SqlState == PostgresErrorCodes.UniqueViolation) { context.ChangeTracker.Clear(); } } } } } // 7. 最终保存 await context.SaveChangesAsync(cancellationToken); } /// /// 停止生命周期时的清理(此处无需处理)。 /// /// 取消标记。 /// 已完成任务。 public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; private static async Task SeedRoleTemplatesAsync( IdentityDbContext context, IList templates, CancellationToken cancellationToken) { // 1. 空集合直接返回 if (templates is null || templates.Count == 0) { return; } // 2. 逐个处理模板 foreach (var templateOptions in templates) { // 2.1 校验必填字段 if (string.IsNullOrWhiteSpace(templateOptions.TemplateCode) || string.IsNullOrWhiteSpace(templateOptions.Name)) { continue; } // 2.2 查询现有模板 var code = templateOptions.TemplateCode.Trim(); var existing = await context.RoleTemplates.FirstOrDefaultAsync(x => x.TemplateCode == code, cancellationToken); if (existing == null) { // 2.3 新增模板 existing = new DomainRoleTemplate { TemplateCode = code, Name = templateOptions.Name.Trim(), Description = templateOptions.Description, IsActive = templateOptions.IsActive }; await context.RoleTemplates.AddAsync(existing, cancellationToken); await context.SaveChangesAsync(cancellationToken); } else { // 2.4 更新模板 existing.Name = templateOptions.Name.Trim(); existing.Description = templateOptions.Description; existing.IsActive = templateOptions.IsActive; context.RoleTemplates.Update(existing); await context.SaveChangesAsync(cancellationToken); } // 2.5 重置模板权限 var permissionCodes = NormalizeValues(templateOptions.Permissions); var existingPermissions = await context.RoleTemplatePermissions .Where(x => x.RoleTemplateId == existing.Id) .ToListAsync(cancellationToken); // 2.6 清空旧权限并保存 context.RoleTemplatePermissions.RemoveRange(existingPermissions); await context.SaveChangesAsync(cancellationToken); // 2.7 去重后的权限编码 var distinctPermissionCodes = permissionCodes.Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); foreach (var permissionCode in distinctPermissionCodes) { try { var alreadyExists = await context.RoleTemplatePermissions.AnyAsync( x => x.RoleTemplateId == existing.Id && x.PermissionCode == permissionCode, cancellationToken); if (alreadyExists) { continue; } await context.RoleTemplatePermissions.AddAsync(new DomainRoleTemplatePermission { RoleTemplateId = existing.Id, PermissionCode = permissionCode }, cancellationToken); await context.SaveChangesAsync(cancellationToken); } catch (DbUpdateException ex) when (ex.InnerException is PostgresException pg && pg.SqlState == PostgresErrorCodes.UniqueViolation) { context.ChangeTracker.Clear(); } } } } private static string[] NormalizeValues(string[]? values) => values == null ? [] : [.. values .Where(v => !string.IsNullOrWhiteSpace(v)) .Select(v => v.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase)]; private static IDisposable EnterTenantScope(ITenantContextAccessor accessor, long tenantId) { var previous = accessor.Current; accessor.Current = new TenantContext(tenantId, null, "admin-seed"); return new Scope(() => accessor.Current = previous); } private sealed class Scope(Action disposeAction) : IDisposable { private readonly Action _disposeAction = disposeAction; public void Dispose() => _disposeAction(); } }