using BuildFlow.Infrastructure.Persistence; using BuildFlow.Platform.Infrastructure.Persistence; using File.Infrastructure.Persistence; using IM.Infrastructure.Mongo; using IM.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore.Diagnostics; using MongoDB.Entities; using Monitoring.Infrastructure.Persistence; using Scripting.Infrastructure.Persistence; using Workflow.Infrastructure.Persistence; using PlatformSeedData = BuildFlow.Platform.Infrastructure.Persistence.SeedData; using WorkflowSeedData = Workflow.Infrastructure.Persistence.SeedData; namespace BuildFlow.Host.Configuration; /// /// 启动期数据库初始化:按固定顺序完成「迁移 → IM MongoDB 建索引 → 种子数据」。 /// 容错策略:单个 DbContext / 单个步骤失败只记录警告并跳过,不影响其他库与主流程启动, /// 避免任一外部库抖动拖垮整个 Host。日志走 (可被 OTel 采集), /// 输出格式与 一致。 /// public static class DatabaseInitializer { public static async Task RunAsync(IServiceProvider rootProvider) { using var scope = rootProvider.CreateScope(); var sp = scope.ServiceProvider; var logger = sp.GetRequiredService().CreateLogger(nameof(DatabaseInitializer)); // 1. 各模块 DbContext 自动迁移(顺序:rag → platform → file → im → workflow) // workflow 传 suppressPendingChanges:true:该模块注册 DbContext 时未抑制 // PendingModelChangesWarning(有未提交的模型变更但不影响已有 migration),单独处理避免告警。 await MigrateAsync(sp, logger); await MigrateAsync(sp, logger); await MigrateAsync(sp, logger); await MigrateAsync(sp, logger); await MigrateAsync(sp, logger, suppressPendingChanges: true); // Scripting 与 Workflow 共用 workflow 库,迁移创建 funcs / func_execution_logs 表 await MigrateAsync(sp, logger, suppressPendingChanges: true); await MigrateAsync(sp, logger); // IM 列补丁:AddPinAndPinnedAtColumns 迁移因 ImDbContext Snapshot 与实体不匹配 // (PendingModelChangesWarning)被跳过,导致 b_conversation_participants 缺 is_pinned/pinned_at 列, // /conversations 查询 500。这里用幂等 raw SQL 兜底补列,迁移修复后可移除。 await SafeRunAsync(async () => { var imDb = sp.GetRequiredService(); await imDb.Database.ExecuteSqlRawAsync( "ALTER TABLE b_conversation_participants ADD COLUMN IF NOT EXISTS is_pinned boolean NOT NULL DEFAULT false"); await imDb.Database.ExecuteSqlRawAsync( "ALTER TABLE b_conversation_participants ADD COLUMN IF NOT EXISTS pinned_at timestamp NULL"); }, logger, "IM is_pinned/pinned_at 列补丁"); // 2. IM MongoDB 建索引(MongoDB.Entities:用 DB.Index 创建两个复合索引) // 两个复合索引支撑两种查询路径:按时间倒序拉取最新消息、按 ObjectId 游标分页上拉更早消息。 await SafeRunAsync( async () => { await DB.Default.Index() .Key(m => m.ConversationId, KeyType.Ascending) .Key(m => m.CreatedAt, KeyType.Descending) .CreateAsync(); await DB.Default.Index() .Key(m => m.ConversationId, KeyType.Ascending) .Key(m => m.ID, KeyType.Descending) .CreateAsync(); }, logger, "IM MongoDB 建索引"); // 3. 种子数据:平台(权限/角色/用户/菜单)+ workflow(组件注册) await SafeRunAsync( async () => { var platformDb = sp.GetRequiredService(); await PlatformSeedData.SeedAsync(platformDb); var wfDb = sp.GetRequiredService(); await WorkflowSeedData.SeedAsync(wfDb); // 把存量关系表(nodes/edges)迁移为 X6 JSON 单一事实源(幂等,已迁移的跳过) var (defs, snaps, insts) = await WorkflowGraphMigration.RunAsync(wfDb); if (defs > 0 || snaps > 0 || insts > 0) logger.LogInformation("Workflow graph migration: {Defs} definitions, {Snaps} snapshots, {Insts} instances backfilled", defs, snaps, insts); }, logger, "Seed"); } /// /// 迁移单个 DbContext,失败仅记录警告。 为 true 时, /// 用独立的 options(抑制 PendingModelChangesWarning)新建临时 DbContext 完成迁移,避免污染主注册。 /// private static async Task MigrateAsync( IServiceProvider sp, ILogger logger, bool suppressPendingChanges = false) where T : DbContext { try { if (suppressPendingChanges) { var db = sp.GetRequiredService(); var options = new DbContextOptionsBuilder() .UseNpgsql(db.Database.GetConnectionString()) .UseSnakeCaseNamingConvention() .ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning)) .Options; await using var tmp = (T)Activator.CreateInstance(typeof(T), options)!; await tmp.Database.MigrateAsync(); } else { await sp.GetRequiredService().Database.MigrateAsync(); } logger.LogInformation("[ok] {Name} migrated", typeof(T).Name); } catch (Exception ex) { logger.LogWarning(ex, "[warn] {Name} migrate 跳过: {Message}", typeof(T).Name, ex.Message); } } /// 包裹一个启动步骤,失败仅记录警告(含步骤名)不中断主流程。 private static async Task SafeRunAsync(Func action, ILogger logger, string stepName) { try { await action(); } catch (Exception ex) { logger.LogWarning(ex, "[warn] {Step} 跳过: {Message}", stepName, ex.Message); } } }