diff --git a/Core/MAX.Core/Interfaces/IStartup.cs b/Core/MAX.Core/Interfaces/IStartup.cs
index 0767a3f..e7e3882 100644
--- a/Core/MAX.Core/Interfaces/IStartup.cs
+++ b/Core/MAX.Core/Interfaces/IStartup.cs
@@ -1,4 +1,7 @@
using MAX.Core.DependencyInjection;
+using System;
+using System.ComponentModel;
+using System.Threading.Tasks;
namespace MAX.Core.Interfaces;
@@ -7,12 +10,10 @@ namespace MAX.Core.Interfaces;
///
public interface IStartup : IDependencyRegistrar
{
- ///
- /// 优先级(优先级越高越先执行)
- ///
- int Priority { get; }
///
/// 初始化
///
void Initialize();
+
+ void Start(IServiceProvider serviceProvider);
}
diff --git a/Core/MAX.Core/Startup.cs b/Core/MAX.Core/Startup.cs
index 2f5cb1d..c510de9 100644
--- a/Core/MAX.Core/Startup.cs
+++ b/Core/MAX.Core/Startup.cs
@@ -25,6 +25,7 @@
using MAX.Core.DependencyInjection;
using MAX.Core.Interfaces;
+using System;
namespace MAX.Core;
@@ -42,4 +43,9 @@ public class Startup : IStartup
// 示例:恢复 LazyServiceProvider 后,可在这里注册为单例
// registrar.RegisterSingleton();
}
+
+ public void Start(IServiceProvider serviceProvider)
+ {
+
+ }
}
diff --git a/Core/MAX.Framework/DependencyInjection/HostBuilderExtensions.cs b/Core/MAX.Framework/DependencyInjection/HostBuilderExtensions.cs
index 70f2faf..69609a5 100644
--- a/Core/MAX.Framework/DependencyInjection/HostBuilderExtensions.cs
+++ b/Core/MAX.Framework/DependencyInjection/HostBuilderExtensions.cs
@@ -1,125 +1,123 @@
-using MAX.Framework.Interfaces;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Configuration.Json;
-using Microsoft.Extensions.DependencyInjection;
-using System.Reflection;
+//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)入口:模块两阶段装配 + 模块自己的配置文件。
- ///
- /// var builder = WebApplication.CreateBuilder(args);
- /// builder.AddStoreHub(); // 阶段一:加载各模块配置文件 + 自动执行所有 IModule.AddModule
- ///
- /// var app = builder.Build();
- /// // ... 宿主自己的中间件、认证、端点 ...
- /// app.UseStoreHubModules(); // 阶段二:管道装配(自动执行所有 IModule.UseModule)
- /// app.Run();
- ///
- ///
- public static class HostBuilderExtensions
- {
- ///
- /// 在 上注册应用框架服务:
- ///
- /// - 发现所有 ;
- /// - 把每个模块声明的 并入宿主配置树(宿主 appsettings.json 不需要放模块的配置);
- /// - 再执行 builder.Services.AddStoreHub(builder.Configuration, builder.Environment)。
- ///
- ///
- public static WebApplicationBuilder AddStoreHub(this WebApplicationBuilder builder)
- {
- IEnumerable assemblies = ServiceCollectionExtensions.GetApplicationAssemblies();
+//namespace MAX.Framework.DependencyInjection
+//{
+// ///
+// /// 宿主(Program.cs)入口:模块两阶段装配 + 模块自己的配置文件。
+// ///
+// /// var builder = WebApplication.CreateBuilder(args);
+// /// builder.AddStoreHub(); // 阶段一:加载各模块配置文件 + 自动执行所有 IModule.AddModule
+// ///
+// /// var app = builder.Build();
+// /// // ... 宿主自己的中间件、认证、端点 ...
+// /// app.UseStoreHubModules(); // 阶段二:管道装配(自动执行所有 IModule.UseModule)
+// /// app.Run();
+// ///
+// ///
+// public static class HostBuilderExtensions
+// {
+// ///
+// /// 在 上注册应用框架服务:
+// ///
+// /// - 发现所有 ;
+// /// - 把每个模块声明的 并入宿主配置树(宿主 appsettings.json 不需要放模块的配置);
+// /// - 再执行 builder.Services.AddStoreHub(builder.Configuration, builder.Environment)。
+// ///
+// ///
+// public static WebApplicationBuilder AddStore(this WebApplicationBuilder builder)
+// {
+// IEnumerable assemblies = ServiceCollectionExtensions.GetApplicationAssemblies();
- // 同一个类可能同时实现 IStartup 与 IModule,这里缓存实例,保证后面两个阶段拿到的是同一个对象
- Dictionary instances = [];
- ModuleRegistry modules = ServiceCollectionExtensions.DiscoverModules(assemblies, instances);
+// // 同一个类可能同时实现 IStartup 与 IModule,这里缓存实例,保证后面两个阶段拿到的是同一个对象
+// Dictionary instances = [];
+// ModuleRegistry modules = ServiceCollectionExtensions.DiscoverModules(assemblies, instances);
- // 必须在跑 AddModule 之前并入,模块注册时才读得到自己的配置节
- foreach (IModule module in modules.Modules)
- {
- AddModuleConfigurationFile(builder.Configuration, module);
- }
+// // 必须在跑 AddModule 之前并入,模块注册时才读得到自己的配置节
+// foreach (IModule module in modules.Modules)
+// {
+// AddModuleConfigurationFile(builder.Configuration, module);
+// }
- builder.Services.AddStoreHubCore(assemblies, modules, instances, builder.Configuration, builder.Environment);
- return builder;
- }
+// builder.Services.AddStoreHubCore(assemblies, modules, instances, builder.Configuration, builder.Environment);
+// return builder;
+// }
- ///
- /// 执行所有模块的管道阶段():注册中间件、看板、Map 端点等。
- /// 必须在 builder.Build() 之后、app.Run() 之前调用;
- /// 建议放在 UseAuthorization() 之后、MapControllers() 之前。
- ///
- public static WebApplication UseStoreHubModules(this WebApplication app)
- {
- // 宿主没调用过 AddStoreHub() 时容器里没有注册表,直接返回即可
- ModuleRegistry? registry = app.Services.GetService();
- if (registry is null)
- {
- return app;
- }
+// ///
+// /// 执行所有模块的管道阶段():注册中间件、看板、Map 端点等。
+// ///
+// public static WebApplication UseStoreHubModules(this WebApplication app)
+// {
+// // 宿主没调用过 AddStoreHub() 时容器里没有注册表,直接返回即可
+// ModuleRegistry? registry = app.Services.GetService();
+// if (registry is null)
+// {
+// return app;
+// }
- foreach (IModule module in registry.Modules)
- {
- try
- {
- module.UseModule(app);
- }
- catch (Exception ex)
- {
- throw new InvalidOperationException($"模块 {module.GetType().FullName} 的 UseModule 执行失败。", ex);
- }
- }
+// foreach (IModule module in registry.Modules)
+// {
+// try
+// {
+// module.UseModule(app);
+// }
+// catch (Exception ex)
+// {
+// throw new InvalidOperationException($"模块 {module.GetType().FullName} 的 UseModule 执行失败。", ex);
+// }
+// }
- return app;
- }
+// return app;
+// }
- ///
- /// 把模块自己的配置文件并进宿主配置:插在 appsettings*.json 之后、环境变量/命令行之前,
- /// 这样模块配置能覆盖宿主 appsettings.json,同时仍然允许用环境变量/命令行覆盖它。
- ///
- private static void AddModuleConfigurationFile(IConfigurationBuilder configuration, IModule module)
- {
- string? fileName = module.ConfigurationFile;
- if (string.IsNullOrWhiteSpace(fileName))
- {
- return;
- }
+// ///
+// /// 把模块自己的配置文件并进宿主配置:插在 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);
+// string path = Path.IsPathRooted(fileName)
+// ? fileName
+// : Path.Combine(AppContext.BaseDirectory, fileName);
- JsonConfigurationSource source = new()
- {
- Path = path,
- Optional = true, // 文件不存在时不报错(模块没配就是没有)
- ReloadOnChange = true // 允许改配置不重启(与 appsettings.json 行为一致)
- };
- source.ResolveFileProvider();
+// JsonConfigurationSource source = new()
+// {
+// Path = path,
+// Optional = true, // 文件不存在时不报错(模块没配就是没有)
+// ReloadOnChange = true // 允许改配置不重启(与 appsettings.json 行为一致)
+// };
+// source.ResolveFileProvider();
- IList sources = configuration.Sources;
- int index = sources.Count;
+// 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;
- }
- }
+// // 找到最后一个 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);
+// sources.Insert(index, source);
- if (configuration is IConfigurationRoot root)
- {
- root.Reload();
- }
- }
- }
-}
+// if (configuration is IConfigurationRoot root)
+// {
+// root.Reload();
+// }
+// }
+// }
+//}
diff --git a/Core/MAX.Framework/DependencyInjection/ModuleRegistry.cs b/Core/MAX.Framework/DependencyInjection/ModuleRegistry.cs
deleted file mode 100644
index 25aef10..0000000
--- a/Core/MAX.Framework/DependencyInjection/ModuleRegistry.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using MAX.Framework.Interfaces;
-
-namespace MAX.Framework.DependencyInjection
-{
- ///
- /// 模块注册表:保存 AddStoreHub() 阶段发现(并按优先级排序)的模块实例。
- /// 它被注册进容器,好让 app.UseStoreHubModules() 在 builder.Build() 之后拿到
- /// 「同一批实例」继续执行管道阶段,而不是重新反射创建一遍。
- ///
- internal sealed class ModuleRegistry
- {
- public ModuleRegistry(IReadOnlyList modules)
- {
- Modules = modules;
- }
-
- /// 按优先级降序排列的模块实例
- public IReadOnlyList Modules { get; }
- }
-}
diff --git a/Core/MAX.Framework/DependencyInjection/ServiceCollectionExtensions.cs b/Core/MAX.Framework/DependencyInjection/ServiceCollectionExtensions.cs
index cb16315..a5aa210 100644
--- a/Core/MAX.Framework/DependencyInjection/ServiceCollectionExtensions.cs
+++ b/Core/MAX.Framework/DependencyInjection/ServiceCollectionExtensions.cs
@@ -3,10 +3,13 @@ using MAX.Core.Interfaces;
using MAX.Framework.Commons;
using MAX.Framework.Dtos;
using MAX.Framework.Interfaces;
+using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
+using Microsoft.Win32;
+using System.Configuration;
using System.Diagnostics;
using System.Reflection;
@@ -25,70 +28,37 @@ namespace MAX.Framework.DependencyInjection
];
///
- /// 兼容入口:只有服务集合,没有配置与环境(模块拿到的 是空配置),
- /// 也拿不到 IConfigurationBuilder,所以模块自己的配置文件不会加载。新代码请用 builder.AddStoreHub()。
+ /// 添加框架注册
///
- public static IServiceCollection AddStoreHub(this IServiceCollection services)
- {
- return services.AddStoreHub(null, null);
- }
-
- ///
- /// 带配置的注册入口:builder.Services.AddStoreHub(builder.Configuration)
- ///
- public static IServiceCollection AddStoreHub(this IServiceCollection services, IConfiguration? configuration)
- {
- return services.AddStoreHub(configuration, null);
- }
-
- ///
- /// 注册应用服务:
- ///
- /// - 自动注册所有实现 / / 的类型;
- /// - 发现所有 ,按 降序执行 Initialize + Register;
- /// - 发现所有 ,按 降序执行 ,
- /// 并把模块实例登记进容器,留给 app.UseStoreHubModules() 执行管道阶段。
- ///
- /// 这个入口不加载模块自己的配置文件,需要模块配置请用 builder.AddStoreHub()。
- ///
- public static IServiceCollection AddStoreHub(this IServiceCollection services, IConfiguration? configuration, IHostEnvironment? environment)
+ public static IServiceCollection AddStore(this IHostApplicationBuilder host)
{
IEnumerable assemblies = GetApplicationAssemblies();
-
// 同一个类可能同时实现 IStartup 与 IModule,这里缓存实例,保证两个阶段拿到的是同一个对象
Dictionary instances = [];
- ModuleRegistry modules = DiscoverModules(assemblies, instances);
-
- return services.AddStoreHubCore(assemblies, modules, instances, configuration, environment);
+ return AddStoreCore(host, assemblies, instances);
}
+
+
+
+
///
/// 注册应用服务的公共实现:宿主入口(builder.AddStoreHub())与 入口共用。
///
- internal static IServiceCollection AddStoreHubCore(
- this IServiceCollection services,
+ internal static IServiceCollection AddStoreCore(
+ IHostApplicationBuilder host,
IEnumerable assemblies,
- ModuleRegistry modules,
- IDictionary instances,
- IConfiguration? configuration,
- IHostEnvironment? environment)
+ IDictionary instances)
{
- RegisterDependencyMarkers(services, assemblies);
- RunStartups(services, assemblies, instances);
+ RegisterDependencyMarkers(host.Services, assemblies);
- ModuleContext context = new(
- services,
- new ServiceRegistrarProxy(services),
- configuration ?? new ConfigurationBuilder().Build(),
- environment);
+ RunModules(host, assemblies, instances);
- RunModules(modules, context);
-
- // 注册表必须在 Build() 之前进容器,UseStoreHubModules() 才拿得到
- services.AddSingleton(modules);
+ RunStartups(host.Services, assemblies, instances);
- services.AddControllers(options =>
+
+ host.Services.AddControllers(options =>
{
options.Filters.Add();
options.Filters.Add();
@@ -113,7 +83,7 @@ namespace MAX.Framework.DependencyInjection
};
});
- return services;
+ return host.Services;
}
///
@@ -149,39 +119,33 @@ namespace MAX.Framework.DependencyInjection
}
}
}
-
+ private static IEnumerable Startups = [];
///
/// 发现所有 实现,按 降序执行
///
private static void RunStartups(IServiceCollection services, IEnumerable assemblies, IDictionary instances)
{
IServiceRegistrar registrar = new ServiceRegistrarProxy(services);
-
- foreach (IStartup startup in CreateInstances(assemblies, instances).OrderByDescending(s => s.Priority))
+ Startups = CreateInstances(assemblies, instances).OrderByDescending(s => s.Priority);
+ foreach (IStartup startup in Startups)
{
startup.Initialize();
startup.Register(registrar);
}
}
- ///
- /// 发现所有 实现,只创建实例,不执行任何阶段
- ///
- internal static ModuleRegistry DiscoverModules(IEnumerable assemblies, IDictionary instances)
- {
- return new ModuleRegistry([.. CreateInstances(assemblies, instances).OrderByDescending(m => m.Priority)]);
- }
-
+ private static IEnumerable Modules = [];
///
/// 执行所有模块的服务注册阶段()
///
- private static void RunModules(ModuleRegistry modules, ModuleContext context)
+ private static void RunModules(IHostApplicationBuilder host, IEnumerable assemblies, IDictionary instances)
{
- foreach (IModule module in modules.Modules)
+ Modules = CreateInstances(assemblies, instances).OrderByDescending(s => s.Priority);
+ foreach (IModule module in Modules)
{
try
{
- module.AddModule(context);
+ module.AddModule(host);
}
catch (Exception ex)
{
@@ -234,7 +198,7 @@ namespace MAX.Framework.DependencyInjection
internal static IEnumerable GetApplicationAssemblies()
{
// 使用 AppTypeFinder:扫描应用输出目录 + 已加载程序集,
- return StoreHubAppTypeFinder.Current.GetAssemblies();
+ return StoreAppTypeFinder.Current.GetAssemblies();
}
private static bool IsSystemInterface(Type type)
@@ -244,5 +208,20 @@ namespace MAX.Framework.DependencyInjection
|| type.Namespace.StartsWith("Microsoft", StringComparison.Ordinal);
}
+
+ public static IApplicationBuilder UseStore(this IApplicationBuilder app)
+ {
+ foreach (IStartup startup in Startups)
+ {
+ startup.Start((app as IHost).Services);
+ }
+
+ foreach (IModule module in Modules)
+ {
+ module.UseModule(app);
+ }
+ return app;
+ }
+
}
}
diff --git a/Core/MAX.Framework/DependencyInjection/StoreHubAppTypeFinder.cs b/Core/MAX.Framework/DependencyInjection/StoreHubAppTypeFinder.cs
index f4f5085..a552937 100644
--- a/Core/MAX.Framework/DependencyInjection/StoreHubAppTypeFinder.cs
+++ b/Core/MAX.Framework/DependencyInjection/StoreHubAppTypeFinder.cs
@@ -6,9 +6,9 @@ namespace MAX.Framework.DependencyInjection
/// 本应用的类型查找器:在 AppTypeFinder 默认前缀(MAX. / Nexus.)之外,
/// 增加 StoreHub、KJD 前缀,用于发现宿主和各业务模块的程序集。
///
- internal sealed class StoreHubAppTypeFinder : AppTypeFinder
+ internal sealed class StoreAppTypeFinder : AppTypeFinder
{
- public static StoreHubAppTypeFinder Current { get; } = new();
+ public static StoreAppTypeFinder Current { get; } = new();
protected override List AvailablePrefix()
{
diff --git a/Core/MAX.Framework/Interfaces/IModule.cs b/Core/MAX.Framework/Interfaces/IModule.cs
index 0d02414..8f59880 100644
--- a/Core/MAX.Framework/Interfaces/IModule.cs
+++ b/Core/MAX.Framework/Interfaces/IModule.cs
@@ -1,47 +1,22 @@
+using MAX.Core.Interfaces;
using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.Hosting;
namespace MAX.Framework.Interfaces
{
///
- /// 业务模块:把「服务注册」和「管道装配」两件事都交给模块自己,
- /// 由宿主的两行代码统一驱动(模块不需要出现在 Program.cs 里):
- ///
- /// - builder.AddStoreHub() → 自动发现所有模块并执行 (服务注册阶段,Build() 之前);
- /// - app.UseStoreHubModules() → 自动执行 (管道阶段,Build() 之后、Run() 之前)。
- ///
/// 模块实例由反射创建,因此实现类必须有无参构造函数。
///
- public interface IModule
+ public interface IModule : IPriority
{
///
- /// 模块执行顺序(数值越大越先执行),默认 。
- /// 两个阶段都按这个顺序执行。
+ /// 添加Module
///
- int Priority => MAX.Core.Interfaces.IntPriority.Normal;
+ void AddModule(IHostApplicationBuilder host);
///
- /// 模块自己的配置文件(相对宿主输出目录 ),
- /// 例如 "appsettings.CloudProduct.json";默认 null 表示该模块没有独立配置文件。
- /// 该文件由 builder.AddStoreHub() 自动并入宿主配置树(插在 appsettings*.json 之后、环境变量/命令行之前),
- /// 因此模块内的服务照常注入 IConfiguration 就能读到自己的配置节,模块之间互不干扰。
- /// 注意:只有 builder.AddStoreHub() 入口能加载模块配置文件,
- /// 直接用 builder.Services.AddStoreHub() 时拿不到 IConfigurationBuilder,模块配置不会生效。
+ /// 管道装配阶段
///
- string? ConfigurationFile => null;
-
- ///
- /// 服务注册阶段(builder.Build() 之前):注册本模块的服务、选项、后台任务类型。
- /// 需要读配置时用 ,不要再去碰 builder。
- ///
- void AddModule(ModuleContext context);
-
- ///
- /// 管道装配阶段(builder.Build() 之后、app.Run() 之前):
- /// 注册中间件(如 Hangfire 看板)、Map 端点、读取 解析服务、注册启动任务等。
- /// 不需要管道装配的模块可以不实现(默认空实现)。
- ///
- void UseModule(WebApplication app)
- {
- }
+ void UseModule(IApplicationBuilder app);
}
}
diff --git a/Core/MAX.Framework/Interfaces/ModuleBase.cs b/Core/MAX.Framework/Interfaces/ModuleBase.cs
deleted file mode 100644
index af0a9e1..0000000
--- a/Core/MAX.Framework/Interfaces/ModuleBase.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using MAX.Core.Interfaces;
-using Microsoft.AspNetCore.Builder;
-
-namespace MAX.Framework.Interfaces
-{
- ///
- /// 模块基类:给出 与 的默认实现,
- /// 模块只需重写自己真正关心的阶段(只有服务注册时只写 )。
- ///
- public abstract class ModuleBase : IModule
- {
- ///
- public virtual int Priority => IntPriority.Normal;
-
- ///
- public virtual string? ConfigurationFile => null;
-
- ///
- public abstract void AddModule(ModuleContext context);
-
- ///
- public virtual void UseModule(WebApplication app)
- {
- }
- }
-}
diff --git a/Core/MAX.Framework/Interfaces/ModuleContext.cs b/Core/MAX.Framework/Interfaces/ModuleContext.cs
deleted file mode 100644
index da6bfa3..0000000
--- a/Core/MAX.Framework/Interfaces/ModuleContext.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using MAX.Core.DependencyInjection;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
-
-namespace MAX.Framework.Interfaces
-{
- ///
- /// 模块上下文:模块在「服务注册阶段」能拿到的全部宿主信息。
- /// 之所以包成一个对象,是为了以后要再补宿主的其他东西(日志、配置节、HostApplicationBuilder 等)时不用改接口签名。
- ///
- public sealed class ModuleContext
- {
- public ModuleContext(
- IServiceCollection services,
- IServiceRegistrar registrar,
- IConfiguration configuration,
- IHostEnvironment? environment = null)
- {
- Services = services;
- Registrar = registrar;
- Configuration = configuration;
- Environment = environment;
- }
-
- ///
- /// 服务集合(Microsoft.Extensions.DependencyInjection),等价于宿主里的 builder.Services
- ///
- public IServiceCollection Services { get; }
-
- ///
- /// 容器无关的注册器,等价于 里拿到的那个,便于以后换容器
- ///
- public IServiceRegistrar Registrar { get; }
-
- ///
- /// 应用配置,等价于宿主的 builder.Configuration(永不为 null;走旧的无参入口时是空配置)
- ///
- public IConfiguration Configuration { get; }
-
- ///
- /// 宿主环境,等价于宿主的 builder.Environment;走旧的无参入口时为 null
- ///
- public IHostEnvironment? Environment { get; }
- }
-}
diff --git a/Core/MAX.Framework/Startup.cs b/Core/MAX.Framework/Startup.cs
index 86f2677..6431681 100644
--- a/Core/MAX.Framework/Startup.cs
+++ b/Core/MAX.Framework/Startup.cs
@@ -19,6 +19,11 @@ namespace MAX.Framework
{
}
+
+ public void Start(IServiceProvider serviceProvider)
+ {
+
+ }
}
}
diff --git a/Models/CloudProduct/CloudProductModule.cs b/Models/CloudProduct/CloudProductModule.cs
index e0d7260..f71a3c3 100644
--- a/Models/CloudProduct/CloudProductModule.cs
+++ b/Models/CloudProduct/CloudProductModule.cs
@@ -6,8 +6,10 @@ using MAX.Framework.Interfaces;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
using MySql.Data.MySqlClient;
using Store.Modules.CloudProduct.Services;
+using Microsoft.Extensions.Configuration;
namespace Store.Modules.CloudProduct
{
@@ -19,7 +21,7 @@ namespace Store.Modules.CloudProduct
/// - app.UseStoreHubModules() 执行 (挂看板中间件 + 注册定时任务,此时才有 app)。
///
///
- public class CloudProductModule : ModuleBase
+ public class CloudProductModule : IModule
{
///
/// 本模块自己的配置文件(源码里是 Models/CloudProduct/appsettings.json,
@@ -27,23 +29,31 @@ namespace Store.Modules.CloudProduct
///
public const string ConfigurationFileName = "appsettings.CloudProduct.json";
- public override int Priority => IntPriority.Normal;
+ public int Priority => IntPriority.Normal;
- public override string? ConfigurationFile => ConfigurationFileName;
///
/// 服务注册阶段(builder.Build() 之前):这里只能拿到容器和配置,碰不到 app
///
- public override void AddModule(ModuleContext context)
+ public void AddModule(IHostApplicationBuilder host)
{
- IServiceCollection services = context.Services;
- IConfiguration configuration = context.Configuration;
+ IServiceCollection services = host.Services;
+ IConfigurationManager configuration = host.Configuration;
- // 本模块的配置节(Search / RawData / Indexing / Delete)都在模块自己的配置文件里,
- // 关键项缺失直接报错,避免 OpenSearch 地址为空、原始数据写到意外目录之类的隐性故障
+ if (!string.IsNullOrWhiteSpace(ConfigurationFileName))
+ {
+ string path = Path.IsPathRooted(ConfigurationFileName)? ConfigurationFileName: Path.Combine(AppContext.BaseDirectory, ConfigurationFileName);
+ configuration.AddJsonFile(path, optional: false, reloadOnChange: true);
+ }
+
+
+
+
+ //// 本模块的配置节(Search / RawData / Indexing / Delete)都在模块自己的配置文件里,
+ //// 关键项缺失直接报错,避免 OpenSearch 地址为空、原始数据写到意外目录之类的隐性故障
RequireConfiguration(configuration, "Search:Url");
- RequireConfiguration(configuration, "Search:IndexName");
- RequireConfiguration(configuration, "RawData:Path");
+ //RequireConfiguration(configuration, "Search:IndexName");
+ //RequireConfiguration(configuration, "RawData:Path");
// ===== Hangfire 后台定时任务(商品分词同步) =====
var hangfireConnection = configuration.GetConnectionString("DefaultConnection")
@@ -62,33 +72,35 @@ namespace Store.Modules.CloudProduct
.UseStorage(new MySqlStorage(connectionStringBuilder.ConnectionString, new MySqlStorageOptions() { TablesPrefix = "Hangfire_" })));
services.AddHangfireServer();
services.AddTransient();
+
}
///
/// 管道装配阶段(builder.Build() 之后、app.Run() 之前):这里可以挂中间件、解析容器里的服务
///
- public override void UseModule(WebApplication app)
+ public void UseModule(IApplicationBuilder app)
{
try
{
+ var Configuration = app.ApplicationServices.GetService()!;
// Hangfire 后台任务看板(默认地址 /hangfire)
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new[]
{
new HangfireCustomBasicAuthenticationFilter(){
- User = app.Configuration.GetValue("Hangfire:Dashboard:User") ?? "admin",
- Pass = app.Configuration.GetValue("Hangfire:Dashboard:Pass") ?? "1234560"
+ User = Configuration.GetValue("Hangfire:Dashboard:User") ?? "admin",
+ Pass = Configuration.GetValue("Hangfire:Dashboard:Pass") ?? "1234560"
}
}
});
// 注册定时任务:把未同步(IndexStatus=0)的商品写入 OpenSearch 分词索引
- var cron = app.Configuration.GetValue("Indexing:Cron") ?? "*/5 * * * *";
+ var cron = Configuration.GetValue("Indexing:Cron") ?? "*/5 * * * *";
// 用容器里的 IRecurringJobManager,而不是静态 RecurringJob:
// AddHangfire 只把 JobStorage 注册进容器(没有设置 JobStorage.Current),静态 API 在 Build() 之后调用会直接抛异常。
- app.Services.GetRequiredService().AddOrUpdate(
+ app.ApplicationServices.GetRequiredService().AddOrUpdate(
"goods-index-pending",
job => job.ProcessPendingAsync(),
cron);
diff --git a/Models/CloudProduct/Controllers/GoodsController.cs b/Models/CloudProduct/Controllers/GoodsController.cs
index 1e88bb6..456babe 100644
--- a/Models/CloudProduct/Controllers/GoodsController.cs
+++ b/Models/CloudProduct/Controllers/GoodsController.cs
@@ -246,6 +246,65 @@ namespace Store.Modules.CloudProduct.Controllers
return await _searchService.GetStatusAsync();
}
+ ///
+ /// 检查搜索索引是否存在。
+ ///
+ [HttpGet]
+ public async Task IndexExists()
+ {
+ return await _searchService.IndexExistsAsync();
+ }
+
+ ///
+ /// 检查索引映射(分析器)是否符合当前服务要求。
+ ///
+ [HttpGet]
+ public async Task IndexMappingHealthy()
+ {
+ return await _searchService.IsIndexMappingHealthyAsync();
+ }
+
+ ///
+ /// 创建搜索索引(含 IK 分词映射,已存在且映射正常时跳过)。
+ ///
+ [HttpPost]
+ public async Task CreateIndex()
+ {
+ return await _searchService.CreateIndexAsync();
+ }
+
+ ///
+ /// 删除搜索索引。
+ ///
+ [HttpPost]
+ public async Task DeleteIndex()
+ {
+ await _searchService.DeleteIndexAsync();
+ }
+
+ ///
+ /// 手动刷新搜索索引,使最新写入立即可搜索。
+ ///
+ [HttpPost]
+ public async Task RefreshIndex()
+ {
+ await _searchService.RefreshIndexAsync();
+ }
+
+ ///
+ /// 手动新增或覆盖一条搜索文档。
+ ///
+ [HttpPost]
+ public async Task AddSearchDocument([FromBody] GoodsSearchDocument document)
+ {
+ if (document == null || document.Id <= 0)
+ {
+ throw new AppException("请提供有效的搜索文档", ResultCode.ValidationError);
+ }
+
+ await _searchService.AddDocumentAsync(document);
+ }
+
///
/// 读取指定商品详情:解压缩原始数据并返回 JSON。
///
diff --git a/Models/CloudProduct/Dtos/OpenSearchStatusDto.cs b/Models/CloudProduct/Dtos/OpenSearchStatusDto.cs
index ea61c2c..b714bf8 100644
--- a/Models/CloudProduct/Dtos/OpenSearchStatusDto.cs
+++ b/Models/CloudProduct/Dtos/OpenSearchStatusDto.cs
@@ -1,8 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace Store.Modules.CloudProduct.Dtos
+namespace Store.Modules.CloudProduct.Dtos
{
///
/// OpenSearch 运行状态
@@ -24,18 +20,88 @@ namespace Store.Modules.CloudProduct.Dtos
///
public bool Connected { get; set; }
+ ///
+ /// OpenSearch 服务及目标索引是否健康
+ ///
+ public bool Healthy { get; set; }
+
+ ///
+ /// 集群名称
+ ///
+ public string? ClusterName { get; set; }
+
+ ///
+ /// 集群状态(green、yellow 或 red)
+ ///
+ public string? ClusterStatus { get; set; }
+
+ ///
+ /// 集群节点总数
+ ///
+ public int NumberOfNodes { get; set; }
+
+ ///
+ /// 数据节点数量
+ ///
+ public int NumberOfDataNodes { get; set; }
+
+ ///
+ /// 活跃主分片数量
+ ///
+ public int ActivePrimaryShards { get; set; }
+
+ ///
+ /// 活跃分片总数
+ ///
+ public int ActiveShards { get; set; }
+
+ ///
+ /// 正在迁移的分片数量
+ ///
+ public int RelocatingShards { get; set; }
+
+ ///
+ /// 正在初始化的分片数量
+ ///
+ public int InitializingShards { get; set; }
+
+ ///
+ /// 未分配的分片数量
+ ///
+ public int UnassignedShards { get; set; }
+
///
/// 索引是否存在
///
public bool IndexExists { get; set; }
///
- /// 文档总数(索引不存在时为 -1)
+ /// 索引映射是否符合当前服务要求
+ ///
+ public bool MappingHealthy { get; set; }
+
+ ///
+ /// 标题字段的索引分析器
+ ///
+ public string? IndexAnalyzer { get; set; }
+
+ ///
+ /// 标题字段的搜索分析器
+ ///
+ public string? SearchAnalyzer { get; set; }
+
+ ///
+ /// 文档总数(索引不存在或读取失败时为 -1)
///
public long DocumentCount { get; set; } = -1;
///
- /// 说明信息
+ /// 状态检测耗时(毫秒)
+ ///
+ public long ElapsedMilliseconds { get; set; }
+
+ ///
+ /// 状态说明或错误信息
///
public string? Message { get; set; }
}
diff --git a/Models/CloudProduct/Services/OpenSearchService.cs b/Models/CloudProduct/Services/OpenSearchService.cs
index 9c8c7f9..ead187c 100644
--- a/Models/CloudProduct/Services/OpenSearchService.cs
+++ b/Models/CloudProduct/Services/OpenSearchService.cs
@@ -1,6 +1,8 @@
+using System.Diagnostics;
using MAX.Core.DependencyInjection;
using MAX.Framework.Commons;
using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
using OpenSearch.Client;
using OpenSearch.Net;
using Store.Modules.CloudProduct.Dtos;
@@ -19,6 +21,9 @@ namespace Store.Modules.CloudProduct.Services
Task DeleteDocumentsAsync(IEnumerable ids);
Task DeleteIndexAsync();
Task GetStatusAsync();
+ Task IndexExistsAsync();
+ Task IsIndexMappingHealthyAsync();
+ Task RefreshIndexAsync();
Task RebuildIndexAsync(IEnumerable goodsList);
Task<(long Total, List Documents)> SearchByTitleAsync(GoodsSearchRequest request, GoodsSearchSortBy sortBy = GoodsSearchSortBy.Relevance);
}
@@ -33,13 +38,15 @@ namespace Store.Modules.CloudProduct.Services
private static readonly TimeSpan QueryTimeout = TimeSpan.FromSeconds(5);
// 批量写入/建索引/删除索引等管理类操作超时(数据量大时需要更长,可挪到配置文件)
private static readonly TimeSpan AdminTimeout = TimeSpan.FromSeconds(30);
+ private readonly ILogger _logger;
- public OpenSearchService(IConfiguration configuration)
+ public OpenSearchService(IConfiguration configuration,ILogger logger)
{
var config = configuration.GetSection("Search");
_nodeUrl = config.GetValue("Url") ?? "";
_indexName = config.GetValue("IndexName") ?? "";
_client = CreateClient();
+ _logger = logger;
}
private IOpenSearchClient CreateClient()
@@ -100,34 +107,38 @@ namespace Store.Modules.CloudProduct.Services
}
}
- ///
- /// 校验索引映射是否健康:title 字段必须使用 IK 分析器
- ///
- private async Task IsIndexMappingHealthyAsync()
+ public async Task IsIndexMappingHealthyAsync()
{
- try
- {
- var getResponse = await _client.Indices.GetAsync(_indexName, d => d
- .RequestConfiguration(r => r.RequestTimeout(QueryTimeout)));
- if (!getResponse.IsValid || !getResponse.Indices.TryGetValue(_indexName, out var indexState))
- {
- return false;
- }
+ var mappingStatus = await GetIndexMappingStatusAsync();
+ return mappingStatus.Healthy;
+ }
- var mapping = indexState.Mappings;
- if (mapping?.Properties == null
- || !mapping.Properties.TryGetValue(new PropertyName("title"), out var property)
- || property is not ITextProperty textProperty)
- {
- return false;
- }
-
- return textProperty.Analyzer == "ik_max_word" && textProperty.SearchAnalyzer == "ik_smart";
- }
- catch
+ private async Task<(bool Healthy, string? IndexAnalyzer, string? SearchAnalyzer)> GetIndexMappingStatusAsync()
+ {
+ var getResponse = await _client.Indices.GetAsync(_indexName, d => d
+ .RequestConfiguration(r => r.RequestTimeout(QueryTimeout)));
+ if (!getResponse.IsValid)
{
- return false;
+ throw new AppException("读取索引映射失败: " + getResponse.DebugInformation);
}
+ if (!getResponse.Indices.TryGetValue(_indexName, out var indexState))
+ {
+ return (false, null, null);
+ }
+
+ var mapping = indexState.Mappings;
+ if (mapping?.Properties == null
+ || !mapping.Properties.TryGetValue(new PropertyName("title"), out var property)
+ || property is not ITextProperty textProperty)
+ {
+ return (false, null, null);
+ }
+
+ var indexAnalyzer = textProperty.Analyzer;
+ var searchAnalyzer = textProperty.SearchAnalyzer;
+ var usesIk = indexAnalyzer == "ik_max_word" && searchAnalyzer == "ik_smart";
+ var usesStandard = indexAnalyzer == "standard" && searchAnalyzer == "standard";
+ return (usesIk || usesStandard, indexAnalyzer, searchAnalyzer);
}
private async Task TryCreateIndexAsync(bool useIk)
@@ -173,6 +184,23 @@ namespace Store.Modules.CloudProduct.Services
}
}
+ public async Task IndexExistsAsync()
+ {
+ var response = await _client.Indices.ExistsAsync(_indexName, d => d
+ .RequestConfiguration(r => r.RequestTimeout(QueryTimeout)));
+ return response.Exists;
+ }
+
+ public async Task RefreshIndexAsync()
+ {
+ var response = await _client.Indices.RefreshAsync(_indexName, d => d
+ .RequestConfiguration(r => r.RequestTimeout(AdminTimeout)));
+ if (!response.IsValid)
+ {
+ throw new AppException("刷新索引失败: " + response.DebugInformation);
+ }
+ }
+
///
/// ============ 3.1 批量写入商品搜索文档(存在则覆盖) ============
///
@@ -198,8 +226,7 @@ namespace Store.Modules.CloudProduct.Services
}
// 刷新索引,保证数据立即可搜索(仅用于演示,生产环境不建议频繁 Refresh)
- await _client.Indices.RefreshAsync(_indexName, d => d
- .RequestConfiguration(r => r.RequestTimeout(AdminTimeout)));
+ await RefreshIndexAsync();
}
///
@@ -260,8 +287,7 @@ namespace Store.Modules.CloudProduct.Services
throw new AppException("删除 OpenSearch 文档失败: " + response.DebugInformation);
}
- await _client.Indices.RefreshAsync(_indexName, d => d
- .RequestConfiguration(r => r.RequestTimeout(AdminTimeout)));
+ await RefreshIndexAsync();
}
///
@@ -279,8 +305,7 @@ namespace Store.Modules.CloudProduct.Services
if (response.IsValid)
{
- await _client.Indices.RefreshAsync(_indexName, d => d
- .RequestConfiguration(r => r.RequestTimeout(QueryTimeout)));
+ await RefreshIndexAsync();
}
else
{
@@ -414,6 +439,7 @@ namespace Store.Modules.CloudProduct.Services
///
public async Task GetStatusAsync()
{
+ var stopwatch = Stopwatch.StartNew();
var status = new OpenSearchStatusDto
{
NodeUrl = _nodeUrl,
@@ -428,28 +454,65 @@ namespace Store.Modules.CloudProduct.Services
if (!ping.IsValid)
{
status.Message = ping.DebugInformation;
+ _logger.LogError(ping.DebugInformation);
+ return status;
+ }
+ _logger.LogInformation("OpenSerach 已连接");
+ var clusterHealth = await _client.Cluster.HealthAsync(Indices.All, h => h
+ .RequestConfiguration(r => r.RequestTimeout(QueryTimeout)));
+ if (!clusterHealth.IsValid)
+ {
+ status.Message = clusterHealth.DebugInformation;
return status;
}
- var exists = await _client.Indices.ExistsAsync(_indexName, d => d
- .RequestConfiguration(r => r.RequestTimeout(QueryTimeout)));
- status.IndexExists = exists.Exists;
+ status.ClusterName = clusterHealth.ClusterName;
+ status.ClusterStatus = clusterHealth.Status.ToString().ToLowerInvariant();
+ status.NumberOfNodes = clusterHealth.NumberOfNodes;
+ status.NumberOfDataNodes = clusterHealth.NumberOfDataNodes;
+ status.ActivePrimaryShards = clusterHealth.ActivePrimaryShards;
+ status.ActiveShards = clusterHealth.ActiveShards;
+ status.RelocatingShards = clusterHealth.RelocatingShards;
+ status.InitializingShards = clusterHealth.InitializingShards;
+ status.UnassignedShards = clusterHealth.UnassignedShards;
+ status.IndexExists = await IndexExistsAsync();
- if (exists.Exists)
+ if (status.IndexExists)
{
+ var mappingStatus = await GetIndexMappingStatusAsync();
+ status.MappingHealthy = mappingStatus.Healthy;
+ status.IndexAnalyzer = mappingStatus.IndexAnalyzer;
+ status.SearchAnalyzer = mappingStatus.SearchAnalyzer;
+
var count = await _client.CountAsync(c => c
.Index(_indexName)
.RequestConfiguration(r => r.RequestTimeout(QueryTimeout)));
status.DocumentCount = count.IsValid ? count.Count : -1;
if (!count.IsValid)
+ {
status.Message = count.DebugInformation;
+ }
}
+ else
+ {
+ status.Message = $"索引 [{_indexName}] 不存在";
+ }
+
+ status.Healthy = status.IndexExists
+ && status.MappingHealthy
+ && status.ClusterStatus != "red"
+ && status.DocumentCount >= 0;
}
catch (Exception ex)
{
- status.Connected = false;
+ status.Healthy = false;
status.Message = ex.Message;
}
+ finally
+ {
+ stopwatch.Stop();
+ status.ElapsedMilliseconds = stopwatch.ElapsedMilliseconds;
+ }
return status;
}
diff --git a/Models/CloudProduct/Startup.cs b/Models/CloudProduct/Startup.cs
index 3eb8ccf..383d4dd 100644
--- a/Models/CloudProduct/Startup.cs
+++ b/Models/CloudProduct/Startup.cs
@@ -1,13 +1,14 @@
using MAX.Core.DependencyInjection;
using MAX.Core.Interfaces;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Store.Modules.CloudProduct.Services;
+using System.ComponentModel;
namespace Store.Modules.CloudProduct
{
- ///
- /// CloudProduct 模块的启动器():由 AddStoreHub() 自动发现,
- /// 按 降序执行 + 。
- /// 需要配置或管道装配(app)的部分不在这里,见 。
- ///
+
public class Startup : IStartup
{
public int Priority => IntPriority.Normal;
@@ -19,5 +20,13 @@ namespace Store.Modules.CloudProduct
public void Register(IServiceRegistrar registrar)
{
}
+
+ public async void Start(IServiceProvider serviceProvider)
+ {
+ var openSearchService = serviceProvider.GetRequiredService();
+ await openSearchService.GetStatusAsync();
+ }
+
+
}
}
diff --git a/Store.API/Program.cs b/Store.API/Program.cs
index b716fe9..30b61a1 100644
--- a/Store.API/Program.cs
+++ b/Store.API/Program.cs
@@ -10,17 +10,17 @@ public partial class Program
// IOC 注册:自动注册 ISingletonDependency / IScopedDependency / ITransientDependency,并执行所有 IStartup / IModule.AddModule
// 模块服务注册阶段(等价于 builder.Services.AddStoreHub(builder.Configuration, builder.Environment),额外把配置/环境交给模块)
- builder.AddStoreHub();
+ builder.AddStore();
builder.Services.AddFreeSqlDB();
// JWT 验证:注册 JwtBearer 认证 + 令牌服务 + OpenAPI Bearer 安全方案(配置节 Jwt)
builder.Services.AddStoreHubJwt(builder.Configuration);
// Add services to the container.
- builder.Services.Configure(options =>
- {
- // 兼容迁移的同步控制器:允许同步读写请求/响应 IO(旧 asmx 行为)
- options.AllowSynchronousIO = true;
- });
+ //builder.Services.Configure(options =>
+ //{
+ // // 兼容迁移的同步控制器:允许同步读写请求/响应 IO(旧 asmx 行为)
+ // options.AllowSynchronousIO = true;
+ //});
builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddApiVersioning(options =>
@@ -34,13 +34,9 @@ public partial class Program
options.SubstituteApiVersionInUrl = true;
}).AddOpenApi();
- #region DB
-
-
- #endregion
-
- var app = builder.Build();
+ WebApplication app = builder.Build();
+ app.UseStore();
// 初始化 KJD 迁移的静态提供者(MysqlUtils 连接提供 / 业务配置读取)
//StoreHub.KJDWebService.Commons.AdoProvider.SetServiceProvider(app.Services);
//StoreHub.KJDWebService.Commons.KJDConfig.SetConfiguration(app.Configuration);
@@ -63,9 +59,6 @@ public partial class Program
app.UseAuthorization();
- // 模块管道装配阶段:执行所有 IModule.UseModule(Hangfire 看板、定时任务等),必须在 Run() 之前
- app.UseStoreHubModules();
-
app.MapControllers();
app.Run();
diff --git a/Store.Framework/Startup.cs b/Store.Framework/Startup.cs
index 489c502..34e101f 100644
--- a/Store.Framework/Startup.cs
+++ b/Store.Framework/Startup.cs
@@ -49,6 +49,11 @@ namespace Store.Framework
return KJDFsqlFactory(s);
});
}
+
+ public void Start(IServiceProvider serviceProvider)
+ {
+
+ }
}
}