391 lines
14 KiB
C#
391 lines
14 KiB
C#
using System.Linq;
|
||
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.Dictionary.Enums;
|
||
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;
|
||
|
||
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)
|
||
{
|
||
if (!_options.Enabled)
|
||
{
|
||
logger.LogInformation("AppSeed 未启用,跳过业务数据初始化");
|
||
return;
|
||
}
|
||
|
||
using var scope = serviceProvider.CreateScope();
|
||
var appDbContext = scope.ServiceProvider.GetRequiredService<TakeoutAppDbContext>();
|
||
var dictionaryDbContext = scope.ServiceProvider.GetRequiredService<DictionaryDbContext>();
|
||
|
||
var defaultTenantId = await EnsureDefaultTenantAsync(appDbContext, cancellationToken);
|
||
await EnsureDictionarySeedsAsync(dictionaryDbContext, defaultTenantId, cancellationToken);
|
||
|
||
logger.LogInformation("AppSeed 完成业务数据初始化");
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||
|
||
/// <summary>
|
||
/// 确保默认租户存在。
|
||
/// </summary>
|
||
private async Task<long?> EnsureDefaultTenantAsync(TakeoutAppDbContext dbContext, CancellationToken cancellationToken)
|
||
{
|
||
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
|
||
.IgnoreQueryFilters()
|
||
.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 (!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>
|
||
/// 确保基础字典存在。
|
||
/// </summary>
|
||
private async Task EnsureDictionarySeedsAsync(DictionaryDbContext dbContext, 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();
|
||
|
||
var group = await dbContext.DictionaryGroups
|
||
.IgnoreQueryFilters()
|
||
.FirstOrDefaultAsync(x => x.TenantId == tenantId && 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 (!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, defaultTenantId, cancellationToken);
|
||
await dbContext.SaveChangesAsync(cancellationToken);
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 确保系统参数以独立表形式可重复种子。
|
||
/// </summary>
|
||
private async Task EnsureSystemParametersAsync(DictionaryDbContext dbContext, 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;
|
||
var existingParameters = await dbContext.SystemParameters
|
||
.IgnoreQueryFilters()
|
||
.Where(x => x.TenantId == tenantId)
|
||
.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 (!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)
|
||
{
|
||
var materializedItems = seedItems
|
||
.Where(item => !string.IsNullOrWhiteSpace(item.Key) && !string.IsNullOrWhiteSpace(item.Value))
|
||
.ToList();
|
||
|
||
if (materializedItems.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var existingItems = await dbContext.DictionaryItems
|
||
.IgnoreQueryFilters()
|
||
.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 (!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);
|
||
}
|
||
}
|
||
}
|
||
}
|