using Microsoft.EntityFrameworkCore; using Tiku.Application.Security; using Tiku.Domain.Common; using Tiku.Domain.Platform; using Tiku.Infrastructure.Persistence; namespace Tiku.Infrastructure.Bootstrap; public sealed class BuiltinStarterOfferingSeeder(TikuDbContext dbContext) { public const string OfferingCode = "starter"; public const int OfferingVersion = 1; private static readonly string[] FeatureCodes = [ SaasFeatureCatalog.CoreBackoffice, SaasFeatureCatalog.SiteContent ]; public async Task SeedAsync(CancellationToken cancellationToken = default) { var availableFeatureCodes = await dbContext.SaasFeatures.AsNoTracking() .Where(feature => FeatureCodes.Contains(feature.Code) && feature.Status == SaasFeatureStatus.Active) .Select(feature => feature.Code) .ToArrayAsync(cancellationToken); if (availableFeatureCodes.Length != FeatureCodes.Length) { throw new InvalidOperationException( "The built-in feature catalog must be seeded before the starter offering."); } var now = DateTimeOffset.UtcNow; var offering = await dbContext.SaasOfferings .SingleOrDefaultAsync(item => item.Code == OfferingCode, cancellationToken); if (offering is null) { offering = new SaasOffering { Code = OfferingCode, Name = "建站基础版", Type = SaasOfferingType.BasePlan, Status = SaasOfferingStatus.Active, Description = "包含租户后台与站点内容管理的免费基础版本。", SortOrder = 0 }; dbContext.SaasOfferings.Add(offering); } else { offering.Name = "建站基础版"; offering.Type = SaasOfferingType.BasePlan; offering.Status = SaasOfferingStatus.Active; offering.Description = "包含租户后台与站点内容管理的免费基础版本。"; offering.SortOrder = 0; offering.UpdatedAt = now; } var version = await dbContext.SaasOfferingVersions .SingleOrDefaultAsync( item => item.OfferingId == offering.Id && item.Version == OfferingVersion, cancellationToken); if (version is null) { version = new SaasOfferingVersion { OfferingId = offering.Id, Version = OfferingVersion, Status = SaasOfferingVersionStatus.Draft, BillingCycle = PlatformBillingCycle.Yearly, OriginalAmountCents = 0, AmountCents = 0, Currency = "CNY", EffectiveAt = now, Metadata = JsonDefaults.Object() }; dbContext.SaasOfferingVersions.Add(version); } else if (version.Status == SaasOfferingVersionStatus.Draft) { version.BillingCycle = PlatformBillingCycle.Yearly; version.OriginalAmountCents = 0; version.AmountCents = 0; version.Currency = "CNY"; version.EffectiveAt ??= now; version.RetiredAt = null; version.UpdatedAt = now; } else if (version.Status == SaasOfferingVersionStatus.Retired) { throw new InvalidOperationException("The built-in starter offering version 1 is retired and cannot be repaired automatically."); } var existingFeatureCodes = await dbContext.SaasOfferingVersionFeatures .Where(binding => binding.OfferingVersionId == version.Id) .Select(binding => binding.FeatureCode) .ToHashSetAsync(StringComparer.Ordinal, cancellationToken); var missingFeatureCodes = FeatureCodes .Where(code => !existingFeatureCodes.Contains(code)) .ToArray(); if (version.Status == SaasOfferingVersionStatus.Published && missingFeatureCodes.Length > 0) { throw new InvalidOperationException( "The published built-in starter offering is missing required features and cannot be repaired in place."); } dbContext.SaasOfferingVersionFeatures.AddRange(missingFeatureCodes.Select(code => new SaasOfferingVersionFeature { OfferingVersionId = version.Id, FeatureCode = code })); await dbContext.SaveChangesAsync(cancellationToken); if (version.Status == SaasOfferingVersionStatus.Draft) { version.Status = SaasOfferingVersionStatus.Published; version.PublishedAt = now; version.EffectiveAt ??= now; await dbContext.SaveChangesAsync(cancellationToken); } } }