using System.Diagnostics; using Admin.NET.Plugin.AiDOP.ChatBI; using Admin.NET.Plugin.AiDOP.Entity; using Admin.NET.Plugin.AiDOP.Entity.S0.Manufacturing; using Admin.NET.Plugin.AiDOP.Entity.S0.Quality; using Admin.NET.Plugin.AiDOP.Entity.S0.Sales; using Admin.NET.Plugin.AiDOP.Entity.S0.Supply; using Admin.NET.Plugin.AiDOP.Entity.S8; using Admin.NET.Plugin.AiDOP.Entity.S8.OrderFlow; using Admin.NET.Plugin.AiDOP.Order; using Admin.NET.Plugin.AiDOP.DataPlatform.Executors; using Admin.NET.Plugin.AiDOP.DataPlatform.HotWatch; using Admin.NET.Plugin.AiDOP.Entity.DataPlatform; using Admin.NET.Plugin.AiDOP.Infrastructure; using Admin.NET.Plugin.AiDOP.MaterialWarehouse; using Admin.NET.Plugin.AiDOP.WorkOrder; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using SqlSugar; namespace Admin.NET.Plugin.AiDOP; /// /// AiDOP 插件启动配置(按需扩展 DI) /// [AppStartup(900)] public class Startup : AppStartup { public void ConfigureServices(IServiceCollection services) { services.AddHttpClient(); services.AddHttpClient("AidopChatBI.DeepSeek"); services.AddScoped(); services.AddScoped(); services.AddConfigurableOptions(); services.AddConfigurableOptions(); services.AddConfigurableOptions(); services.AddConfigurableOptions(); services.AddConfigurableOptions(); services.AddTransient(); services.AddTransient(); // WP9:IPickBillCreator 唯一实现为 Facade(避免 Local/Mes 多实现竞注) services.AddTransient(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.Configure(options => { options.Filters.AddService(); options.Filters.AddService(); }); // MDP 出站事件驱动:入队 Pulse → 立即推送;Period Job 仅兜底 services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); services.AddSingleton(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); // S8-DATASET-FOUNDATION-HARDENING-2:S8 取数底座的接口映射显式注册 // (与上方 IModuleRebuildLock 同一先例:本项目不依赖 ITransient 自动按接口注册)。 services.AddTransient(); // S8-STANDARD-DATASET-HARD-CUTOVER-1:IS8LegacySqlDataProvider 及其实现已物理删除。 // S8-RULE01-PURCHASE-DELIVERY-A:接入第一个真实业务数据集 PURCHASE_DELIVERY。 // Catalog 有定义 + Registry 有实现,Enable Gate 才判 READY: // 缺定义 → 规则连保存都过不了(dataset_not_defined); // 缺实现 → 可存草稿但不可启用(dataset_provider_not_registered)。 // 两个接口都必须显式注册(同上,不依赖 ITransient 自动按接口注册)。 services.AddTransient(); services.AddTransient(); // S8-RULE-GOVERNANCE-BATCH1:规则定义目录。业务语义从此只存在于代码里, // ado_s8_watch_rule 上的同名列降级为 provisioning 维护的只读投影。 // // 注册为 **Singleton**(数据集目录是 Transient):目录构造期会做完整性校验, // 而 Transient 的失败形态是「首次解析该服务的那个请求 500」,启动日志干净但目录其实是坏的。 // Singleton + Configure 里强制解析一次 = 定义写错就起不来,见下方 Configure。 services.AddSingleton(); services.AddSingleton(); // S8-RULE-GOVERNANCE-BATCH2:代码定义 → 每租户运行策略的供给服务。 // ITransient 在本仓不做按接口自动注册,且它没有接口,故显式登记具体类型。 services.AddTransient(); // S8-ACTION-PERMISSION-1:异常动作授权矩阵的首次供给(同 S8RuleProvisioningService,无接口需显式登记)。 services.AddTransient(); } /// /// 开发环境下为 Demo 表执行 CodeFirst(表已存在则跳过变更,行为取决于 SqlSugar 配置)。 /// public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // S8-RULE-GOVERNANCE-BATCH1:强制解析一次规则目录,把「定义写错」变成启动失败。 // 刻意**不 try/catch**:重复 rule_code、rule_type 不在词表、TIMEOUT 却没给判定语义, // 都是发布事故。吞掉它只会让服务带着一个坏目录起来,然后在某条规则跑到那一 tick 时 // 才在后台日志里报错——那时页面上一切正常,没人会看见。 { var ruleCatalog = app.ApplicationServices .GetRequiredService(); Trace.TraceInformation($"Ai-DOP S8RuleCatalog: {ruleCatalog.Definitions.Count} rule definition(s) loaded"); } try { AidopMenuLinkSync.EnsureLinked(app.ApplicationServices); } catch (Exception ex) { Trace.TraceWarning("Ai-DOP AidopMenuLinkSync: " + ex); } // S8-RULE-GOVERNANCE-BATCH2:启动期对账 —— 确保每个启用租户都拥有全部代码定义规则的运行策略行。 // // 失败策略沿用本方法内既有的 sync 惯例(AidopMenuLinkSync 等):**记录后放行,不阻断启动**。 // 理由是这段 Configure 与 S0–S7 共用同一个宿主,供给失败不该把整个平台拖下线; // 且新 provision 的规则一律 disabled,缺行的后果是"该租户暂时看不到这条规则", // 不是数据错误。异常对象完整落日志,不做静默吞掉 —— 「以为同步成功」比同步失败更危险。 // // 与之对照:规则目录自身构造失败(重复定义 / 定义写错)**不在此列**, // 它在上方 GetRequiredService 处直接抛出、阻断启动,因为那是代码包本身有问题。 try { using var provisioningScope = app.ApplicationServices.CreateScope(); var provisioning = provisioningScope.ServiceProvider .GetRequiredService(); var summary = provisioning.SyncAsync().GetAwaiter().GetResult(); Trace.TraceInformation( $"Ai-DOP S8RuleProvisioning: tenants={summary.TenantCount} definitions={summary.DefinitionCount} " + $"created={summary.CreatedCount} refreshed={summary.RefreshedCount} unchanged={summary.UnchangedCount} " + $"orphaned={summary.OrphanedCount} duplicate={summary.DuplicateCount}"); } catch (Exception ex) { Trace.TraceError("Ai-DOP S8RuleProvisioning FAILED(本次启动未完成规则运行策略对账): " + ex); } // S8-ACTION-PERMISSION-1:动作授权矩阵首次供给。 // 失败策略与上方规则供给一致:记录后放行,不阻断启动(本宿主与 S0–S7 共用)。 // 后果是"该租户的异常动作暂时全部关闭"——fail closed,不是数据错误,且日志可见。 try { using var actionScope = app.ApplicationServices.CreateScope(); var actionProvisioning = actionScope.ServiceProvider .GetRequiredService(); var actionSummary = actionProvisioning.SyncAsync().GetAwaiter().GetResult(); Trace.TraceInformation( $"Ai-DOP S8ActionRoleProvisioning: tenants={actionSummary.TenantCount} actions={actionSummary.ActionCount} " + $"created={actionSummary.CreatedCount} skippedTenants={actionSummary.SkippedTenantCount} " + $"unconfigured={actionSummary.UnconfiguredCount}"); } catch (Exception ex) { Trace.TraceError("Ai-DOP S8ActionRoleProvisioning FAILED(本次启动未完成动作授权供给): " + ex); } try { using var scopeS4 = app.ApplicationServices.CreateScope(); var dbS4 = scopeS4.ServiceProvider.GetRequiredService(); dbS4.CodeFirst.InitTables( typeof(AdoSmartOpsLayoutItem), typeof(AdoSmartOpsHomeModule), typeof(AdoSmartOpsKpiMaster), typeof(AdoSmartOpsDashboardPageConfig), typeof(AdoSmartOpsDashboardWidget), typeof(AdoSmartOpsImprovementPlan), typeof(AdoMdpHotWatch), typeof(Admin.NET.Plugin.AiDOP.Entity.SmartOps.AdoS1DashboardRebuildJob), typeof(Admin.NET.Plugin.AiDOP.Entity.SmartOps.AdoModuleDashboardRebuildJob), typeof(Admin.NET.Plugin.AiDOP.Entity.SmartOps.AdoMdpRebuildLock), typeof(AdoTenantMenuSuppression)); AidopTenantMigration.MigrateOldTenantId(dbS4); AidopKpiMasterSeed.EnsureSeed(dbS4); AidopKpiMasterSeed.EnsureSeed(dbS4, 1300000000888); var kpiTenantIds = dbS4.Queryable() .Select(x => x.TenantId) .Distinct() .ToList(); foreach (var tenantId in kpiTenantIds.Where(x => x > 0)) AidopKpiMasterSeed.EnsureSeed(dbS4, tenantId!.Value); AidopImprovementApprovalSeed.EnsureSeed(dbS4); Admin.NET.Plugin.AiDOP.Infrastructure.IqcInspBillFlowSeed.EnsureSeed(dbS4); Admin.NET.Plugin.AiDOP.Infrastructure.IpqcInspectionFlowSeed.EnsureSeed(dbS4); Admin.NET.Plugin.AiDOP.Infrastructure.S6ProcessInspectionFlowSeed.EnsureSeed(dbS4); Admin.NET.Plugin.AiDOP.Infrastructure.FqcInspBillFlowSeed.EnsureSeed(dbS4); } catch (Exception ex) { Trace.TraceWarning("Ai-DOP CodeFirst/S4 seed: " + ex); } if (!env.IsDevelopment()) return; // 客户/物料/优先级等;远端若已是遗留表(已有主键),CodeFirst 会报 Multiple primary key,不可让启动失败。 try { using var scope = app.ApplicationServices.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); db.CodeFirst.InitTables( typeof(AdoS0CustMaster), typeof(AdoS0ItemMaster), typeof(AdoS0ItemSubstituteDetail), typeof(AdoS0PriorityCode), typeof(AdoS0ProductStructureOp), typeof(AdoS0StdOpMaster), typeof(AdoS0LineMaster), typeof(AdoS0MfgRoutingOpDetail), typeof(AdoS0MfgPersonSkillAssignment), typeof(AdoS0MfgWorkOrderControl), typeof(AdoS0MfgPersonSkill), typeof(AdoS0MfgLinePost), typeof(AdoS0MfgWorkCenter), typeof(AdoS0MfgLineMaterial), typeof(AdoS0ProcessFlowCard), typeof(AdoS0OrderScheduleCycle), typeof(AdoS0MfgElementParam), typeof(AdoS0MfgMaterialProcessElement), typeof(AdoS0MfgPreprocessElement), typeof(AdoS0MfgPreprocessElementParam), typeof(AdoS0ImageType), typeof(AdoS0QualitySegmentImage), typeof(AdoS0QmsQualityBaseType), typeof(AdoS0QmsFqcInspectionSpec), typeof(AdoS0QmsFqcInspectionSpecEntry), typeof(AdoS0QmsOqcInspectionSpec), typeof(AdoS0QmsOqcInspectionSpecEntry), typeof(AdoS0CategoryLeadTime), typeof(AdoS0MaterialPlanCycle), typeof(AdoS0SrmPurchase), typeof(AdoS8Exception), typeof(AdoS8ExceptionTimeline), typeof(AdoS8DecisionRecord), typeof(AdoS8Evidence), // S8-STANDARD-DATASET-HARD-CUTOVER-1:AdoS8AlertRule 已物理删除 // (运行时权威一直是 AdoS8WatchRule;该表零 Runtime Consumer)。 typeof(AdoS8SceneConfig), typeof(AdoS8NotificationLayer), typeof(AdoS8WatchRule), typeof(AdoS8RolePermissionConfig), // S8-ACTION-PERMISSION-1:异常动作 ↔ 角色授权矩阵。 typeof(AdoS8ExceptionActionRole), // S8-HANDLER-POOL-1:规则 ↔ 处理账号池。 typeof(AdoS8RuleHandlerUser), typeof(AdoS8NotificationLog), typeof(AdoS8DashboardCellConfig), typeof(AdoS8ExceptionType), typeof(AdoS8DetectionLog), typeof(AdoS8RuleDetectionState), typeof(AdoS8Dimension), typeof(AdoS8DimensionNode), typeof(AdoS8ConfigDraft), typeof(AdoS8MonitorObject), typeof(AdoS8MonitorMetric), typeof(AdoS8OrderFlowOrder), typeof(AdoS8OrderFlowStage), typeof(AdoS8OrderFlowSnapshot), typeof(AdoS8OrderFlowSubstep), typeof(AdoS8OrderFlowSubstepUnit), typeof(AdoS8OrderFlowProcurementPivot), typeof(ContractReview), typeof(ContractReviewFlow), typeof(ProductDesign), typeof(ProductDesignBom), typeof(ProductDesignRouting) ); AidopDimensionSeed.EnsureSeed(db); AidopMonitorDictionarySeed.EnsureSeed(db); } catch (Exception ex) { Trace.TraceWarning("Ai-DOP Development CodeFirst (S0/S8/Order demo tables): " + ex); } } }