Files
TakeoutSaaS.TenantApi/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/AppDataSeeder.cs
MSuMshk 2c086d1149
All checks were successful
Build and Deploy TenantApi / build-and-deploy (push) Successful in 41s
fix(infra): auto-apply app and dictionary migrations on startup
2026-02-18 20:41:22 +08:00

501 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using TakeoutSaaS.Domain.Dictionary.Entities;
using TakeoutSaaS.Domain.SystemParameters.Entities;
using TakeoutSaaS.Domain.Tenants.Entities;
using TakeoutSaaS.Domain.Tenants.Enums;
using TakeoutSaaS.Infrastructure.App.Options;
using TakeoutSaaS.Infrastructure.Dictionary.Persistence;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Infrastructure.App.Persistence;
/// <summary>
/// 业务数据种子,确保默认租户与基础字典可重复执行。
/// </summary>
/// <remarks>
/// 初始化种子服务。
/// </remarks>
public sealed class AppDataSeeder(
IServiceProvider serviceProvider,
ILogger<AppDataSeeder> logger,
IOptions<AppSeedOptions> options) : IHostedService
{
private readonly AppSeedOptions _options = options.Value;
/// <inheritdoc />
public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = serviceProvider.CreateScope();
var appDbContext = scope.ServiceProvider.GetRequiredService<TakeoutAppDbContext>();
var dictionaryDbContext = scope.ServiceProvider.GetRequiredService<DictionaryDbContext>();
// 1. 启动时优先确保 App/Dictionary 数据库迁移到最新版本。
await appDbContext.Database.MigrateAsync(cancellationToken);
await dictionaryDbContext.Database.MigrateAsync(cancellationToken);
if (!_options.Enabled)
{
logger.LogInformation("AppSeed 未启用,仅执行数据库迁移,跳过业务数据初始化");
return;
}
var tenantContextAccessor = scope.ServiceProvider.GetRequiredService<ITenantContextAccessor>();
// 2. 执行业务数据种子。
await EnsureSystemTenantAsync(appDbContext, cancellationToken);
var defaultTenantId = await EnsureDefaultTenantAsync(appDbContext, cancellationToken);
await EnsureDictionarySeedsAsync(dictionaryDbContext, tenantContextAccessor, defaultTenantId, cancellationToken);
logger.LogInformation("AppSeed 完成业务数据初始化");
}
/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
/// <summary>
/// 确保默认租户存在。
/// </summary>
private async Task<long?> EnsureDefaultTenantAsync(TakeoutAppDbContext dbContext, CancellationToken cancellationToken)
{
using var _ = dbContext.DisableSoftDeleteFilter();
var tenantOptions = _options.DefaultTenant;
if (tenantOptions == null || string.IsNullOrWhiteSpace(tenantOptions.Code) || string.IsNullOrWhiteSpace(tenantOptions.Name))
{
logger.LogInformation("AppSeed 未配置默认租户,跳过租户种子");
return null;
}
var code = tenantOptions.Code.Trim();
var existingTenant = await dbContext.Tenants
.FirstOrDefaultAsync(x => x.Code == code, cancellationToken);
if (existingTenant == null)
{
var tenant = new Tenant
{
Id = tenantOptions.TenantId,
Code = code,
Name = tenantOptions.Name.Trim(),
ShortName = tenantOptions.ShortName?.Trim(),
ContactName = tenantOptions.ContactName?.Trim(),
ContactPhone = tenantOptions.ContactPhone?.Trim(),
Status = TenantStatus.Active
};
await dbContext.Tenants.AddAsync(tenant, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
logger.LogInformation("AppSeed 已创建默认租户 {TenantCode}", code);
return tenant.Id;
}
var updated = false;
if (existingTenant.DeletedAt.HasValue)
{
existingTenant.DeletedAt = null;
existingTenant.DeletedBy = null;
updated = true;
}
if (!string.Equals(existingTenant.Name, tenantOptions.Name, StringComparison.Ordinal))
{
existingTenant.Name = tenantOptions.Name.Trim();
updated = true;
}
if (!string.Equals(existingTenant.ShortName, tenantOptions.ShortName, StringComparison.Ordinal))
{
existingTenant.ShortName = tenantOptions.ShortName?.Trim();
updated = true;
}
if (!string.Equals(existingTenant.ContactName, tenantOptions.ContactName, StringComparison.Ordinal))
{
existingTenant.ContactName = tenantOptions.ContactName?.Trim();
updated = true;
}
if (!string.Equals(existingTenant.ContactPhone, tenantOptions.ContactPhone, StringComparison.Ordinal))
{
existingTenant.ContactPhone = tenantOptions.ContactPhone?.Trim();
updated = true;
}
if (existingTenant.Status != TenantStatus.Active)
{
existingTenant.Status = TenantStatus.Active;
updated = true;
}
if (updated)
{
dbContext.Tenants.Update(existingTenant);
await dbContext.SaveChangesAsync(cancellationToken);
logger.LogInformation("AppSeed 已更新默认租户 {TenantCode}", code);
}
else
{
logger.LogInformation("AppSeed 默认租户 {TenantCode} 已存在且无需更新", code);
}
return existingTenant.Id;
}
/// <summary>
/// 确保系统租户存在TenantId=0用于系统级数据归属
/// </summary>
private async Task EnsureSystemTenantAsync(TakeoutAppDbContext dbContext, CancellationToken cancellationToken)
{
using var _ = dbContext.DisableSoftDeleteFilter();
var existingTenant = await dbContext.Tenants
.FirstOrDefaultAsync(x => x.Id == 0, cancellationToken);
if (existingTenant != null)
{
// 1. (空行后) 若历史数据仍为 PLATFORM则自动修正为 SYSTEM
var updated = false;
if (existingTenant.DeletedAt.HasValue)
{
existingTenant.DeletedAt = null;
existingTenant.DeletedBy = null;
updated = true;
}
if (!string.Equals(existingTenant.Code, "SYSTEM", StringComparison.Ordinal))
{
existingTenant.Code = "SYSTEM";
updated = true;
}
if (!string.Equals(existingTenant.Name, "System", StringComparison.Ordinal))
{
existingTenant.Name = "System";
updated = true;
}
if (existingTenant.Status != TenantStatus.Active)
{
existingTenant.Status = TenantStatus.Active;
updated = true;
}
if (updated)
{
dbContext.Tenants.Update(existingTenant);
await dbContext.SaveChangesAsync(cancellationToken);
logger.LogInformation("AppSeed 已更新系统租户 SYSTEM");
}
return;
}
var tenant = new Tenant
{
Id = 0,
Code = "SYSTEM",
Name = "System",
Status = TenantStatus.Active
};
await dbContext.Tenants.AddAsync(tenant, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
logger.LogInformation("AppSeed 已创建系统租户 SYSTEM");
}
/// <summary>
/// 确保基础字典存在。
/// </summary>
private async Task EnsureDictionarySeedsAsync(
DictionaryDbContext dbContext,
ITenantContextAccessor tenantContextAccessor,
long? defaultTenantId,
CancellationToken cancellationToken)
{
var dictionaryGroups = _options.DictionaryGroups ?? new List<DictionarySeedGroupOptions>();
var hasDictionaryGroups = dictionaryGroups.Count > 0;
if (!hasDictionaryGroups)
{
logger.LogInformation("AppSeed 未配置基础字典,跳过字典种子");
}
if (hasDictionaryGroups)
{
foreach (var groupOptions in dictionaryGroups)
{
if (string.IsNullOrWhiteSpace(groupOptions.Code) || string.IsNullOrWhiteSpace(groupOptions.Name))
{
logger.LogWarning("AppSeed 跳过字典分组Code 或 Name 为空");
continue;
}
var tenantId = groupOptions.TenantId ?? defaultTenantId ?? 0;
var code = groupOptions.Code.Trim();
using var tenantScope = tenantContextAccessor.EnterTenantScope(tenantId, "app-seed");
using var disableSoftDeleteScope = dbContext.DisableSoftDeleteFilter();
var group = await dbContext.DictionaryGroups
.FirstOrDefaultAsync(x => x.Code == code, cancellationToken);
if (group == null)
{
group = new DictionaryGroup
{
Id = 0,
TenantId = tenantId,
Code = code,
Name = groupOptions.Name.Trim(),
Scope = groupOptions.Scope,
Description = groupOptions.Description?.Trim(),
IsEnabled = groupOptions.IsEnabled
};
await dbContext.DictionaryGroups.AddAsync(group, cancellationToken);
logger.LogInformation("AppSeed 创建字典分组 {GroupCode} (Tenant: {TenantId})", code, tenantId);
}
else
{
var groupUpdated = false;
if (group.DeletedAt.HasValue)
{
group.DeletedAt = null;
group.DeletedBy = null;
groupUpdated = true;
}
if (!string.Equals(group.Name, groupOptions.Name, StringComparison.Ordinal))
{
group.Name = groupOptions.Name.Trim();
groupUpdated = true;
}
if (!string.Equals(group.Description, groupOptions.Description, StringComparison.Ordinal))
{
group.Description = groupOptions.Description?.Trim();
groupUpdated = true;
}
if (group.Scope != groupOptions.Scope)
{
group.Scope = groupOptions.Scope;
groupUpdated = true;
}
if (group.IsEnabled != groupOptions.IsEnabled)
{
group.IsEnabled = groupOptions.IsEnabled;
groupUpdated = true;
}
if (groupUpdated)
{
dbContext.DictionaryGroups.Update(group);
}
}
await UpsertDictionaryItemsAsync(dbContext, group, groupOptions.Items, tenantId, cancellationToken);
}
}
await EnsureSystemParametersAsync(dbContext, tenantContextAccessor, defaultTenantId, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
}
/// <summary>
/// 确保系统参数以独立表形式可重复种子。
/// </summary>
private async Task EnsureSystemParametersAsync(
DictionaryDbContext dbContext,
ITenantContextAccessor tenantContextAccessor,
long? defaultTenantId,
CancellationToken cancellationToken)
{
var systemParameters = _options.SystemParameters ?? new List<SystemParameterSeedOptions>();
if (systemParameters.Count == 0)
{
logger.LogInformation("AppSeed 未配置系统参数,跳过系统参数种子");
return;
}
var grouped = systemParameters
.Where(x => !string.IsNullOrWhiteSpace(x.Key) && !string.IsNullOrWhiteSpace(x.Value))
.GroupBy(x => x.TenantId ?? defaultTenantId ?? 0);
if (!grouped.Any())
{
logger.LogInformation("AppSeed 系统参数配置为空,跳过系统参数种子");
return;
}
foreach (var group in grouped)
{
var tenantId = group.Key;
using var tenantScope = tenantContextAccessor.EnterTenantScope(tenantId, "app-seed");
using var disableSoftDeleteScope = dbContext.DisableSoftDeleteFilter();
var existingParameters = await dbContext.SystemParameters
.ToListAsync(cancellationToken);
foreach (var seed in group)
{
var key = seed.Key.Trim();
var existing = existingParameters.FirstOrDefault(x => x.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
if (existing == null)
{
var parameter = new SystemParameter
{
Id = 0,
TenantId = tenantId,
Key = key,
Value = seed.Value.Trim(),
Description = seed.Description?.Trim(),
SortOrder = seed.SortOrder,
IsEnabled = seed.IsEnabled
};
await dbContext.SystemParameters.AddAsync(parameter, cancellationToken);
continue;
}
var updated = false;
if (existing.DeletedAt.HasValue)
{
existing.DeletedAt = null;
existing.DeletedBy = null;
updated = true;
}
if (!string.Equals(existing.Value, seed.Value, StringComparison.Ordinal))
{
existing.Value = seed.Value.Trim();
updated = true;
}
if (!string.Equals(existing.Description, seed.Description, StringComparison.Ordinal))
{
existing.Description = seed.Description?.Trim();
updated = true;
}
if (existing.SortOrder != seed.SortOrder)
{
existing.SortOrder = seed.SortOrder;
updated = true;
}
if (existing.IsEnabled != seed.IsEnabled)
{
existing.IsEnabled = seed.IsEnabled;
updated = true;
}
if (updated)
{
dbContext.SystemParameters.Update(existing);
}
}
}
}
/// <summary>
/// 合并字典项。
/// </summary>
private static async Task UpsertDictionaryItemsAsync(
DictionaryDbContext dbContext,
DictionaryGroup group,
IEnumerable<DictionarySeedItemOptions> seedItems,
long tenantId,
CancellationToken cancellationToken)
{
// 确保分组已持久化以获取正确的主键,避免 FK 约束报错。
if (!dbContext.Entry(group).IsKeySet || group.Id == 0)
{
await dbContext.SaveChangesAsync(cancellationToken);
}
var materializedItems = seedItems
.Where(item => !string.IsNullOrWhiteSpace(item.Key) && !string.IsNullOrWhiteSpace(item.Value))
.ToList();
if (materializedItems.Count == 0)
{
return;
}
var existingItems = await dbContext.DictionaryItems
.Where(x => x.GroupId == group.Id)
.ToListAsync(cancellationToken);
foreach (var seed in materializedItems)
{
var key = seed.Key.Trim();
var existing = existingItems.FirstOrDefault(x => x.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
if (existing == null)
{
var newItem = new DictionaryItem
{
Id = 0,
TenantId = tenantId,
GroupId = group.Id,
Key = key,
Value = seed.Value.Trim(),
Description = seed.Description?.Trim(),
SortOrder = seed.SortOrder,
IsEnabled = seed.IsEnabled
};
await dbContext.DictionaryItems.AddAsync(newItem, cancellationToken);
continue;
}
var updated = false;
if (existing.DeletedAt.HasValue)
{
existing.DeletedAt = null;
existing.DeletedBy = null;
updated = true;
}
if (!string.Equals(existing.Value, seed.Value, StringComparison.Ordinal))
{
existing.Value = seed.Value.Trim();
updated = true;
}
if (!string.Equals(existing.Description, seed.Description, StringComparison.Ordinal))
{
existing.Description = seed.Description?.Trim();
updated = true;
}
if (existing.SortOrder != seed.SortOrder)
{
existing.SortOrder = seed.SortOrder;
updated = true;
}
if (existing.IsEnabled != seed.IsEnabled)
{
existing.IsEnabled = seed.IsEnabled;
updated = true;
}
if (updated)
{
dbContext.DictionaryItems.Update(existing);
}
}
}
}