From c732ecb1d2fd38d7f3d6eeba239047a601e5e910 Mon Sep 17 00:00:00 2001 From: chfei Date: Fri, 18 Sep 2026 19:22:49 +0800 Subject: [PATCH] =?UTF-8?q?gx=20=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HostBuilderExtensions.cs | 74 +++++++++++++++++-- .../ServiceCollectionExtensions.cs | 45 ++++++++--- Core/MAX.Framework/Interfaces/IModule.cs | 10 +++ Core/MAX.Framework/Interfaces/ModuleBase.cs | 3 + Models/CloudProduct/CloudProductModule.cs | 25 +++++++ .../Store.Modules.CloudProduct.csproj | 8 +- Models/CloudProduct/appsettings.json | 17 +++++ 7 files changed, 166 insertions(+), 16 deletions(-) create mode 100644 Models/CloudProduct/appsettings.json diff --git a/Core/MAX.Framework/DependencyInjection/HostBuilderExtensions.cs b/Core/MAX.Framework/DependencyInjection/HostBuilderExtensions.cs index a24ef14..70f2faf 100644 --- a/Core/MAX.Framework/DependencyInjection/HostBuilderExtensions.cs +++ b/Core/MAX.Framework/DependencyInjection/HostBuilderExtensions.cs @@ -1,14 +1,17 @@ using MAX.Framework.Interfaces; using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Json; using Microsoft.Extensions.DependencyInjection; +using System.Reflection; namespace MAX.Framework.DependencyInjection { /// - /// 宿主(Program.cs)入口:模块两阶段装配。 + /// 宿主(Program.cs)入口:模块两阶段装配 + 模块自己的配置文件。 /// /// var builder = WebApplication.CreateBuilder(args); - /// builder.AddStoreHub(); // 阶段一:服务注册(自动执行所有 IModule.AddModule) + /// builder.AddStoreHub(); // 阶段一:加载各模块配置文件 + 自动执行所有 IModule.AddModule /// /// var app = builder.Build(); /// // ... 宿主自己的中间件、认证、端点 ... @@ -20,12 +23,27 @@ namespace MAX.Framework.DependencyInjection { /// /// 在 上注册应用框架服务: - /// 等价于 builder.Services.AddStoreHub(builder.Configuration, builder.Environment), - /// 区别是会把配置与环境一起包装成 交给各模块。 + /// + /// 发现所有 ; + /// 把每个模块声明的 并入宿主配置树(宿主 appsettings.json 不需要放模块的配置); + /// 再执行 builder.Services.AddStoreHub(builder.Configuration, builder.Environment)。 + /// /// public static WebApplicationBuilder AddStoreHub(this WebApplicationBuilder builder) { - builder.Services.AddStoreHub(builder.Configuration, builder.Environment); + IEnumerable assemblies = ServiceCollectionExtensions.GetApplicationAssemblies(); + + // 同一个类可能同时实现 IStartup 与 IModule,这里缓存实例,保证后面两个阶段拿到的是同一个对象 + Dictionary instances = []; + ModuleRegistry modules = ServiceCollectionExtensions.DiscoverModules(assemblies, instances); + + // 必须在跑 AddModule 之前并入,模块注册时才读得到自己的配置节 + foreach (IModule module in modules.Modules) + { + AddModuleConfigurationFile(builder.Configuration, module); + } + + builder.Services.AddStoreHubCore(assemblies, modules, instances, builder.Configuration, builder.Environment); return builder; } @@ -57,5 +75,51 @@ namespace MAX.Framework.DependencyInjection return app; } + + /// + /// 把模块自己的配置文件并进宿主配置:插在 appsettings*.json 之后、环境变量/命令行之前, + /// 这样模块配置能覆盖宿主 appsettings.json,同时仍然允许用环境变量/命令行覆盖它。 + /// + private static void AddModuleConfigurationFile(IConfigurationBuilder configuration, IModule module) + { + string? fileName = module.ConfigurationFile; + if (string.IsNullOrWhiteSpace(fileName)) + { + return; + } + + string path = Path.IsPathRooted(fileName) + ? fileName + : Path.Combine(AppContext.BaseDirectory, fileName); + + JsonConfigurationSource source = new() + { + Path = path, + Optional = true, // 文件不存在时不报错(模块没配就是没有) + ReloadOnChange = true // 允许改配置不重启(与 appsettings.json 行为一致) + }; + source.ResolveFileProvider(); + + IList sources = configuration.Sources; + int index = sources.Count; + + // 找到最后一个 appsettings*.json,插到它后面 + for (int i = sources.Count - 1; i >= 0; i--) + { + if (sources[i] is FileConfigurationSource file + && Path.GetFileName(file.Path).StartsWith("appsettings", StringComparison.OrdinalIgnoreCase)) + { + index = i + 1; + break; + } + } + + sources.Insert(index, source); + + if (configuration is IConfigurationRoot root) + { + root.Reload(); + } + } } } diff --git a/Core/MAX.Framework/DependencyInjection/ServiceCollectionExtensions.cs b/Core/MAX.Framework/DependencyInjection/ServiceCollectionExtensions.cs index c8be9ad..cb16315 100644 --- a/Core/MAX.Framework/DependencyInjection/ServiceCollectionExtensions.cs +++ b/Core/MAX.Framework/DependencyInjection/ServiceCollectionExtensions.cs @@ -25,8 +25,8 @@ namespace MAX.Framework.DependencyInjection ]; /// - /// 兼容入口:只有服务集合,没有配置与环境(模块拿到的 是空配置)。 - /// 新代码请用 builder.AddStoreHub()。 + /// 兼容入口:只有服务集合,没有配置与环境(模块拿到的 是空配置), + /// 也拿不到 IConfigurationBuilder,所以模块自己的配置文件不会加载。新代码请用 builder.AddStoreHub()。 /// public static IServiceCollection AddStoreHub(this IServiceCollection services) { @@ -49,6 +49,7 @@ namespace MAX.Framework.DependencyInjection /// 发现所有 ,按 降序执行 , /// 并把模块实例登记进容器,留给 app.UseStoreHubModules() 执行管道阶段。 /// + /// 这个入口不加载模块自己的配置文件,需要模块配置请用 builder.AddStoreHub()。 /// public static IServiceCollection AddStoreHub(this IServiceCollection services, IConfiguration? configuration, IHostEnvironment? environment) { @@ -56,7 +57,22 @@ namespace MAX.Framework.DependencyInjection // 同一个类可能同时实现 IStartup 与 IModule,这里缓存实例,保证两个阶段拿到的是同一个对象 Dictionary instances = []; + ModuleRegistry modules = DiscoverModules(assemblies, instances); + return services.AddStoreHubCore(assemblies, modules, instances, configuration, environment); + } + + /// + /// 注册应用服务的公共实现:宿主入口(builder.AddStoreHub())与 入口共用。 + /// + internal static IServiceCollection AddStoreHubCore( + this IServiceCollection services, + IEnumerable assemblies, + ModuleRegistry modules, + IDictionary instances, + IConfiguration? configuration, + IHostEnvironment? environment) + { RegisterDependencyMarkers(services, assemblies); RunStartups(services, assemblies, instances); @@ -66,8 +82,10 @@ namespace MAX.Framework.DependencyInjection configuration ?? new ConfigurationBuilder().Build(), environment); + RunModules(modules, context); + // 注册表必须在 Build() 之前进容器,UseStoreHubModules() 才拿得到 - services.AddSingleton(RunModules(assemblies, instances, context)); + services.AddSingleton(modules); services.AddControllers(options => @@ -147,13 +165,19 @@ namespace MAX.Framework.DependencyInjection } /// - /// 发现所有 实现,按 降序执行服务注册阶段 + /// 发现所有 实现,只创建实例,不执行任何阶段 /// - private static ModuleRegistry RunModules(IEnumerable assemblies, IDictionary instances, ModuleContext context) + internal static ModuleRegistry DiscoverModules(IEnumerable assemblies, IDictionary instances) { - List modules = [.. CreateInstances(assemblies, instances).OrderByDescending(m => m.Priority)]; + return new ModuleRegistry([.. CreateInstances(assemblies, instances).OrderByDescending(m => m.Priority)]); + } - foreach (IModule module in modules) + /// + /// 执行所有模块的服务注册阶段() + /// + private static void RunModules(ModuleRegistry modules, ModuleContext context) + { + foreach (IModule module in modules.Modules) { try { @@ -165,8 +189,6 @@ namespace MAX.Framework.DependencyInjection throw new InvalidOperationException($"模块 {module.GetType().FullName} 的 AddModule 执行失败。", ex); } } - - return new ModuleRegistry(modules); } /// @@ -206,7 +228,10 @@ namespace MAX.Framework.DependencyInjection } } - private static IEnumerable GetApplicationAssemblies() + /// + /// 应用程序集(模块、Startup 都从这里发现) + /// + internal static IEnumerable GetApplicationAssemblies() { // 使用 AppTypeFinder:扫描应用输出目录 + 已加载程序集, return StoreHubAppTypeFinder.Current.GetAssemblies(); diff --git a/Core/MAX.Framework/Interfaces/IModule.cs b/Core/MAX.Framework/Interfaces/IModule.cs index a14214a..0d02414 100644 --- a/Core/MAX.Framework/Interfaces/IModule.cs +++ b/Core/MAX.Framework/Interfaces/IModule.cs @@ -19,6 +19,16 @@ namespace MAX.Framework.Interfaces /// int Priority => MAX.Core.Interfaces.IntPriority.Normal; + /// + /// 模块自己的配置文件(相对宿主输出目录 ), + /// 例如 "appsettings.CloudProduct.json";默认 null 表示该模块没有独立配置文件。 + /// 该文件由 builder.AddStoreHub() 自动并入宿主配置树(插在 appsettings*.json 之后、环境变量/命令行之前), + /// 因此模块内的服务照常注入 IConfiguration 就能读到自己的配置节,模块之间互不干扰。 + /// 注意:只有 builder.AddStoreHub() 入口能加载模块配置文件, + /// 直接用 builder.Services.AddStoreHub() 时拿不到 IConfigurationBuilder,模块配置不会生效。 + /// + string? ConfigurationFile => null; + /// /// 服务注册阶段(builder.Build() 之前):注册本模块的服务、选项、后台任务类型。 /// 需要读配置时用 ,不要再去碰 builder。 diff --git a/Core/MAX.Framework/Interfaces/ModuleBase.cs b/Core/MAX.Framework/Interfaces/ModuleBase.cs index 88a5e4b..af0a9e1 100644 --- a/Core/MAX.Framework/Interfaces/ModuleBase.cs +++ b/Core/MAX.Framework/Interfaces/ModuleBase.cs @@ -12,6 +12,9 @@ namespace MAX.Framework.Interfaces /// public virtual int Priority => IntPriority.Normal; + /// + public virtual string? ConfigurationFile => null; + /// public abstract void AddModule(ModuleContext context); diff --git a/Models/CloudProduct/CloudProductModule.cs b/Models/CloudProduct/CloudProductModule.cs index 0963d8d..e0d7260 100644 --- a/Models/CloudProduct/CloudProductModule.cs +++ b/Models/CloudProduct/CloudProductModule.cs @@ -21,8 +21,16 @@ namespace Store.Modules.CloudProduct /// public class CloudProductModule : ModuleBase { + /// + /// 本模块自己的配置文件(源码里是 Models/CloudProduct/appsettings.json, + /// 构建时复制到宿主输出目录并改名成这个名字,避免与宿主的 appsettings.json 冲突) + /// + public const string ConfigurationFileName = "appsettings.CloudProduct.json"; + public override int Priority => IntPriority.Normal; + public override string? ConfigurationFile => ConfigurationFileName; + /// /// 服务注册阶段(builder.Build() 之前):这里只能拿到容器和配置,碰不到 app /// @@ -31,6 +39,12 @@ namespace Store.Modules.CloudProduct IServiceCollection services = context.Services; IConfiguration configuration = context.Configuration; + // 本模块的配置节(Search / RawData / Indexing / Delete)都在模块自己的配置文件里, + // 关键项缺失直接报错,避免 OpenSearch 地址为空、原始数据写到意外目录之类的隐性故障 + RequireConfiguration(configuration, "Search:Url"); + RequireConfiguration(configuration, "Search:IndexName"); + RequireConfiguration(configuration, "RawData:Path"); + // ===== Hangfire 后台定时任务(商品分词同步) ===== var hangfireConnection = configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("缺少配置 ConnectionStrings:DefaultConnection,CloudProduct 模块的 Hangfire 无法启动。"); @@ -85,5 +99,16 @@ namespace Store.Modules.CloudProduct Console.WriteLine("[Hangfire] 看板/定时任务装配失败: " + ex.Message); } } + + /// + /// 校验模块配置项存在(值来自模块自己的 ) + /// + private static void RequireConfiguration(IConfiguration configuration, string key) + { + if (string.IsNullOrWhiteSpace(configuration.GetValue(key))) + { + throw new InvalidOperationException($"缺少配置 {key},请检查模块配置文件 {ConfigurationFileName}。"); + } + } } } diff --git a/Models/CloudProduct/Store.Modules.CloudProduct.csproj b/Models/CloudProduct/Store.Modules.CloudProduct.csproj index 4e98693..9e875dc 100644 --- a/Models/CloudProduct/Store.Modules.CloudProduct.csproj +++ b/Models/CloudProduct/Store.Modules.CloudProduct.csproj @@ -1,4 +1,4 @@ - + net10.0 @@ -11,6 +11,12 @@ + + + + diff --git a/Models/CloudProduct/appsettings.json b/Models/CloudProduct/appsettings.json new file mode 100644 index 0000000..733f8f3 --- /dev/null +++ b/Models/CloudProduct/appsettings.json @@ -0,0 +1,17 @@ +{ + "Search": { + "Url": "http://192.168.0.200:9200", + "IndexName": "goods_search" + }, + "RawData": { + "Path": "E:\\GoodsRawData" + }, + "Indexing": { + "Mode": "Background", + "Cron": "*/5 * * * *", + "BatchSize": 20000 + }, + "Delete": { + "SoftDelete": true + } +}