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();
}
}