修改 Startup 加载 和Module 加载
This commit is contained in:
1 parent
c732ecb1d2
commit
c7fb447034
17 files changed
+462
-383
No files matched your search
@@ -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;
|
||||
/// </summary>
|
||||
public interface IStartup : IDependencyRegistrar
|
||||
{
|
||||
/// <summary>
|
||||
/// 优先级(优先级越高越先执行)
|
||||
/// </summary>
|
||||
int Priority { get; }
|
||||
/// <summary>
|
||||
/// 初始化
|
||||
/// </summary>
|
||||
void Initialize();
|
||||
|
||||
void Start(IServiceProvider serviceProvider);
|
||||
}
|
||||
@@ -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<ILazyServiceProvider, LazyServiceProvider>();
|
||||
}
|
||||
|
||||
public void Start(IServiceProvider serviceProvider)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 宿主(Program.cs)入口:模块两阶段装配 + 模块自己的配置文件。
|
||||
/// <code>
|
||||
/// var builder = WebApplication.CreateBuilder(args);
|
||||
/// builder.AddStoreHub(); // 阶段一:加载各模块配置文件 + 自动执行所有 IModule.AddModule
|
||||
///
|
||||
/// var app = builder.Build();
|
||||
/// // ... 宿主自己的中间件、认证、端点 ...
|
||||
/// app.UseStoreHubModules(); // 阶段二:管道装配(自动执行所有 IModule.UseModule)
|
||||
/// app.Run();
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public static class HostBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 在 <see cref="WebApplicationBuilder"/> 上注册应用框架服务:
|
||||
/// <list type="number">
|
||||
/// <item>发现所有 <see cref="IModule"/>;</item>
|
||||
/// <item>把每个模块声明的 <see cref="IModule.ConfigurationFile"/> 并入宿主配置树(宿主 appsettings.json 不需要放模块的配置);</item>
|
||||
/// <item>再执行 <c>builder.Services.AddStoreHub(builder.Configuration, builder.Environment)</c>。</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public static WebApplicationBuilder AddStoreHub(this WebApplicationBuilder builder)
|
||||
{
|
||||
IEnumerable<Assembly> assemblies = ServiceCollectionExtensions.GetApplicationAssemblies();
|
||||
//namespace MAX.Framework.DependencyInjection
|
||||
//{
|
||||
// /// <summary>
|
||||
// /// 宿主(Program.cs)入口:模块两阶段装配 + 模块自己的配置文件。
|
||||
// /// <code>
|
||||
// /// var builder = WebApplication.CreateBuilder(args);
|
||||
// /// builder.AddStoreHub(); // 阶段一:加载各模块配置文件 + 自动执行所有 IModule.AddModule
|
||||
// ///
|
||||
// /// var app = builder.Build();
|
||||
// /// // ... 宿主自己的中间件、认证、端点 ...
|
||||
// /// app.UseStoreHubModules(); // 阶段二:管道装配(自动执行所有 IModule.UseModule)
|
||||
// /// app.Run();
|
||||
// /// </code>
|
||||
// /// </summary>
|
||||
// public static class HostBuilderExtensions
|
||||
// {
|
||||
// /// <summary>
|
||||
// /// 在 <see cref="WebApplicationBuilder"/> 上注册应用框架服务:
|
||||
// /// <list type="number">
|
||||
// /// <item>发现所有 <see cref="IModule"/>;</item>
|
||||
// /// <item>把每个模块声明的 <see cref="IModule.ConfigurationFile"/> 并入宿主配置树(宿主 appsettings.json 不需要放模块的配置);</item>
|
||||
// /// <item>再执行 <c>builder.Services.AddStoreHub(builder.Configuration, builder.Environment)</c>。</item>
|
||||
// /// </list>
|
||||
// /// </summary>
|
||||
// public static WebApplicationBuilder AddStore(this WebApplicationBuilder builder)
|
||||
// {
|
||||
// IEnumerable<Assembly> assemblies = ServiceCollectionExtensions.GetApplicationAssemblies();
|
||||
|
||||
// 同一个类可能同时实现 IStartup 与 IModule,这里缓存实例,保证后面两个阶段拿到的是同一个对象
|
||||
Dictionary<Type, object> instances = [];
|
||||
ModuleRegistry modules = ServiceCollectionExtensions.DiscoverModules(assemblies, instances);
|
||||
// // 同一个类可能同时实现 IStartup 与 IModule,这里缓存实例,保证后面两个阶段拿到的是同一个对象
|
||||
// Dictionary<Type, object> 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;
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// 执行所有模块的管道阶段(<see cref="IModule.UseModule"/>):注册中间件、看板、Map 端点等。
|
||||
/// 必须在 <c>builder.Build()</c> 之后、<c>app.Run()</c> 之前调用;
|
||||
/// 建议放在 <c>UseAuthorization()</c> 之后、<c>MapControllers()</c> 之前。
|
||||
/// </summary>
|
||||
public static WebApplication UseStoreHubModules(this WebApplication app)
|
||||
{
|
||||
// 宿主没调用过 AddStoreHub() 时容器里没有注册表,直接返回即可
|
||||
ModuleRegistry? registry = app.Services.GetService<ModuleRegistry>();
|
||||
if (registry is null)
|
||||
{
|
||||
return app;
|
||||
}
|
||||
// /// <summary>
|
||||
// /// 执行所有模块的管道阶段(<see cref="IModule.UseModule"/>):注册中间件、看板、Map 端点等。
|
||||
// /// </summary>
|
||||
// public static WebApplication UseStoreHubModules(this WebApplication app)
|
||||
// {
|
||||
// // 宿主没调用过 AddStoreHub() 时容器里没有注册表,直接返回即可
|
||||
// ModuleRegistry? registry = app.Services.GetService<ModuleRegistry>();
|
||||
// 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;
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// 把模块自己的配置文件并进宿主配置:插在 appsettings*.json 之后、环境变量/命令行之前,
|
||||
/// 这样模块配置能覆盖宿主 appsettings.json,同时仍然允许用环境变量/命令行覆盖它。
|
||||
/// </summary>
|
||||
private static void AddModuleConfigurationFile(IConfigurationBuilder configuration, IModule module)
|
||||
{
|
||||
string? fileName = module.ConfigurationFile;
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
// /// <summary>
|
||||
// /// 把模块自己的配置文件并进宿主配置:插在 appsettings*.json 之后、环境变量/命令行之前,
|
||||
// /// 这样模块配置能覆盖宿主 appsettings.json,同时仍然允许用环境变量/命令行覆盖它。
|
||||
// /// </summary>
|
||||
// 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<IConfigurationSource> sources = configuration.Sources;
|
||||
int index = sources.Count;
|
||||
// IList<IConfigurationSource> 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();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -1,20 +0,0 @@
|
||||
using MAX.Framework.Interfaces;
|
||||
|
||||
namespace MAX.Framework.DependencyInjection
|
||||
{
|
||||
/// <summary>
|
||||
/// 模块注册表:保存 <c>AddStoreHub()</c> 阶段发现(并按优先级排序)的模块实例。
|
||||
/// 它被注册进容器,好让 <c>app.UseStoreHubModules()</c> 在 <c>builder.Build()</c> 之后拿到
|
||||
/// 「同一批实例」继续执行管道阶段,而不是重新反射创建一遍。
|
||||
/// </summary>
|
||||
internal sealed class ModuleRegistry
|
||||
{
|
||||
public ModuleRegistry(IReadOnlyList<IModule> modules)
|
||||
{
|
||||
Modules = modules;
|
||||
}
|
||||
|
||||
/// <summary>按优先级降序排列的模块实例</summary>
|
||||
public IReadOnlyList<IModule> Modules { get; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// 兼容入口:只有服务集合,没有配置与环境(模块拿到的 <see cref="ModuleContext.Configuration"/> 是空配置),
|
||||
/// 也拿不到 IConfigurationBuilder,所以模块自己的配置文件不会加载。新代码请用 <c>builder.AddStoreHub()</c>。
|
||||
/// 添加框架注册
|
||||
/// </summary>
|
||||
public static IServiceCollection AddStoreHub(this IServiceCollection services)
|
||||
{
|
||||
return services.AddStoreHub(null, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 带配置的注册入口:<c>builder.Services.AddStoreHub(builder.Configuration)</c>
|
||||
/// </summary>
|
||||
public static IServiceCollection AddStoreHub(this IServiceCollection services, IConfiguration? configuration)
|
||||
{
|
||||
return services.AddStoreHub(configuration, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册应用服务:
|
||||
/// <list type="number">
|
||||
/// <item>自动注册所有实现 <see cref="ISingletonDependency"/> / <see cref="IScopedDependency"/> / <see cref="ITransientDependency"/> 的类型;</item>
|
||||
/// <item>发现所有 <see cref="IStartup"/>,按 <see cref="IStartup.Priority"/> 降序执行 Initialize + Register;</item>
|
||||
/// <item>发现所有 <see cref="IModule"/>,按 <see cref="IModule.Priority"/> 降序执行 <see cref="IModule.AddModule"/>,
|
||||
/// 并把模块实例登记进容器,留给 <c>app.UseStoreHubModules()</c> 执行管道阶段。</item>
|
||||
/// </list>
|
||||
/// 这个入口不加载模块自己的配置文件,需要模块配置请用 <c>builder.AddStoreHub()</c>。
|
||||
/// </summary>
|
||||
public static IServiceCollection AddStoreHub(this IServiceCollection services, IConfiguration? configuration, IHostEnvironment? environment)
|
||||
public static IServiceCollection AddStore(this IHostApplicationBuilder host)
|
||||
{
|
||||
IEnumerable<Assembly> assemblies = GetApplicationAssemblies();
|
||||
|
||||
// 同一个类可能同时实现 IStartup 与 IModule,这里缓存实例,保证两个阶段拿到的是同一个对象
|
||||
Dictionary<Type, object> instances = [];
|
||||
ModuleRegistry modules = DiscoverModules(assemblies, instances);
|
||||
|
||||
return services.AddStoreHubCore(assemblies, modules, instances, configuration, environment);
|
||||
return AddStoreCore(host, assemblies, instances);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 注册应用服务的公共实现:宿主入口(<c>builder.AddStoreHub()</c>)与 <see cref="IServiceCollection"/> 入口共用。
|
||||
/// </summary>
|
||||
internal static IServiceCollection AddStoreHubCore(
|
||||
this IServiceCollection services,
|
||||
internal static IServiceCollection AddStoreCore(
|
||||
IHostApplicationBuilder host,
|
||||
IEnumerable<Assembly> assemblies,
|
||||
ModuleRegistry modules,
|
||||
IDictionary<Type, object> instances,
|
||||
IConfiguration? configuration,
|
||||
IHostEnvironment? environment)
|
||||
IDictionary<Type, object> 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<GlobalExceptionFilter>();
|
||||
options.Filters.Add<UnifyResultFilter>();
|
||||
@@ -113,7 +83,7 @@ namespace MAX.Framework.DependencyInjection
|
||||
};
|
||||
|
||||
});
|
||||
return services;
|
||||
return host.Services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -149,39 +119,33 @@ namespace MAX.Framework.DependencyInjection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<IStartup> Startups = [];
|
||||
/// <summary>
|
||||
/// 发现所有 <see cref="IStartup"/> 实现,按 <see cref="IStartup.Priority"/> 降序执行
|
||||
/// </summary>
|
||||
private static void RunStartups(IServiceCollection services, IEnumerable<Assembly> assemblies, IDictionary<Type, object> instances)
|
||||
{
|
||||
IServiceRegistrar registrar = new ServiceRegistrarProxy(services);
|
||||
|
||||
foreach (IStartup startup in CreateInstances<IStartup>(assemblies, instances).OrderByDescending(s => s.Priority))
|
||||
Startups = CreateInstances<IStartup>(assemblies, instances).OrderByDescending(s => s.Priority);
|
||||
foreach (IStartup startup in Startups)
|
||||
{
|
||||
startup.Initialize();
|
||||
startup.Register(registrar);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发现所有 <see cref="IModule"/> 实现,只创建实例,不执行任何阶段
|
||||
/// </summary>
|
||||
internal static ModuleRegistry DiscoverModules(IEnumerable<Assembly> assemblies, IDictionary<Type, object> instances)
|
||||
{
|
||||
return new ModuleRegistry([.. CreateInstances<IModule>(assemblies, instances).OrderByDescending(m => m.Priority)]);
|
||||
}
|
||||
|
||||
private static IEnumerable<IModule> Modules = [];
|
||||
/// <summary>
|
||||
/// 执行所有模块的服务注册阶段(<see cref="IModule.AddModule"/>)
|
||||
/// </summary>
|
||||
private static void RunModules(ModuleRegistry modules, ModuleContext context)
|
||||
private static void RunModules(IHostApplicationBuilder host, IEnumerable<Assembly> assemblies, IDictionary<Type, object> instances)
|
||||
{
|
||||
foreach (IModule module in modules.Modules)
|
||||
Modules = CreateInstances<IModule>(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<Assembly> 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,9 @@ namespace MAX.Framework.DependencyInjection
|
||||
/// 本应用的类型查找器:在 AppTypeFinder 默认前缀(MAX. / Nexus.)之外,
|
||||
/// 增加 StoreHub、KJD 前缀,用于发现宿主和各业务模块的程序集。
|
||||
/// </summary>
|
||||
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<string> AvailablePrefix()
|
||||
{
|
||||
|
||||
@@ -1,47 +1,22 @@
|
||||
using MAX.Core.Interfaces;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace MAX.Framework.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 业务模块:把「服务注册」和「管道装配」两件事都交给模块自己,
|
||||
/// 由宿主的两行代码统一驱动(模块不需要出现在 Program.cs 里):
|
||||
/// <list type="number">
|
||||
/// <item><c>builder.AddStoreHub()</c> → 自动发现所有模块并执行 <see cref="AddModule"/>(服务注册阶段,<c>Build()</c> 之前);</item>
|
||||
/// <item><c>app.UseStoreHubModules()</c> → 自动执行 <see cref="UseModule"/>(管道阶段,<c>Build()</c> 之后、<c>Run()</c> 之前)。</item>
|
||||
/// </list>
|
||||
/// 模块实例由反射创建,因此实现类必须有无参构造函数。
|
||||
/// </summary>
|
||||
public interface IModule
|
||||
public interface IModule : IPriority
|
||||
{
|
||||
/// <summary>
|
||||
/// 模块执行顺序(数值越大越先执行),默认 <see cref="MAX.Core.Interfaces.IntPriority.Normal"/>。
|
||||
/// 两个阶段都按这个顺序执行。
|
||||
/// 添加Module
|
||||
/// </summary>
|
||||
int Priority => MAX.Core.Interfaces.IntPriority.Normal;
|
||||
void AddModule(IHostApplicationBuilder host);
|
||||
|
||||
/// <summary>
|
||||
/// 模块自己的配置文件(相对宿主输出目录 <see cref="AppContext.BaseDirectory"/>),
|
||||
/// 例如 "appsettings.CloudProduct.json";默认 null 表示该模块没有独立配置文件。
|
||||
/// 该文件由 <c>builder.AddStoreHub()</c> 自动并入宿主配置树(插在 appsettings*.json 之后、环境变量/命令行之前),
|
||||
/// 因此模块内的服务照常注入 <c>IConfiguration</c> 就能读到自己的配置节,模块之间互不干扰。
|
||||
/// 注意:只有 <c>builder.AddStoreHub()</c> 入口能加载模块配置文件,
|
||||
/// 直接用 <c>builder.Services.AddStoreHub()</c> 时拿不到 IConfigurationBuilder,模块配置不会生效。
|
||||
/// 管道装配阶段
|
||||
/// </summary>
|
||||
string? ConfigurationFile => null;
|
||||
|
||||
/// <summary>
|
||||
/// 服务注册阶段(<c>builder.Build()</c> 之前):注册本模块的服务、选项、后台任务类型。
|
||||
/// 需要读配置时用 <see cref="ModuleContext.Configuration"/>,不要再去碰 <c>builder</c>。
|
||||
/// </summary>
|
||||
void AddModule(ModuleContext context);
|
||||
|
||||
/// <summary>
|
||||
/// 管道装配阶段(<c>builder.Build()</c> 之后、<c>app.Run()</c> 之前):
|
||||
/// 注册中间件(如 Hangfire 看板)、Map 端点、读取 <see cref="WebApplication.Services"/> 解析服务、注册启动任务等。
|
||||
/// 不需要管道装配的模块可以不实现(默认空实现)。
|
||||
/// </summary>
|
||||
void UseModule(WebApplication app)
|
||||
{
|
||||
}
|
||||
void UseModule(IApplicationBuilder app);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using MAX.Core.Interfaces;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
|
||||
namespace MAX.Framework.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 模块基类:给出 <see cref="IModule.Priority"/> 与 <see cref="IModule.UseModule"/> 的默认实现,
|
||||
/// 模块只需重写自己真正关心的阶段(只有服务注册时只写 <see cref="AddModule"/>)。
|
||||
/// </summary>
|
||||
public abstract class ModuleBase : IModule
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public virtual int Priority => IntPriority.Normal;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string? ConfigurationFile => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract void AddModule(ModuleContext context);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void UseModule(WebApplication app)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
using MAX.Core.DependencyInjection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace MAX.Framework.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 模块上下文:模块在「服务注册阶段」能拿到的全部宿主信息。
|
||||
/// 之所以包成一个对象,是为了以后要再补宿主的其他东西(日志、配置节、HostApplicationBuilder 等)时不用改接口签名。
|
||||
/// </summary>
|
||||
public sealed class ModuleContext
|
||||
{
|
||||
public ModuleContext(
|
||||
IServiceCollection services,
|
||||
IServiceRegistrar registrar,
|
||||
IConfiguration configuration,
|
||||
IHostEnvironment? environment = null)
|
||||
{
|
||||
Services = services;
|
||||
Registrar = registrar;
|
||||
Configuration = configuration;
|
||||
Environment = environment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 服务集合(Microsoft.Extensions.DependencyInjection),等价于宿主里的 <c>builder.Services</c>
|
||||
/// </summary>
|
||||
public IServiceCollection Services { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 容器无关的注册器,等价于 <see cref="IStartup.Register"/> 里拿到的那个,便于以后换容器
|
||||
/// </summary>
|
||||
public IServiceRegistrar Registrar { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 应用配置,等价于宿主的 <c>builder.Configuration</c>(永不为 null;走旧的无参入口时是空配置)
|
||||
/// </summary>
|
||||
public IConfiguration Configuration { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 宿主环境,等价于宿主的 <c>builder.Environment</c>;走旧的无参入口时为 null
|
||||
/// </summary>
|
||||
public IHostEnvironment? Environment { get; }
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,11 @@ namespace MAX.Framework
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void Start(IServiceProvider serviceProvider)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// <item><c>app.UseStoreHubModules()</c> 执行 <see cref="UseModule"/>(挂看板中间件 + 注册定时任务,此时才有 app)。</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public class CloudProductModule : ModuleBase
|
||||
public class CloudProductModule : IModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 本模块自己的配置文件(源码里是 Models/CloudProduct/appsettings.json,
|
||||
@@ -27,23 +29,31 @@ namespace Store.Modules.CloudProduct
|
||||
/// </summary>
|
||||
public const string ConfigurationFileName = "appsettings.CloudProduct.json";
|
||||
|
||||
public override int Priority => IntPriority.Normal;
|
||||
public int Priority => IntPriority.Normal;
|
||||
|
||||
public override string? ConfigurationFile => ConfigurationFileName;
|
||||
|
||||
/// <summary>
|
||||
/// 服务注册阶段(builder.Build() 之前):这里只能拿到容器和配置,碰不到 app
|
||||
/// </summary>
|
||||
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<GoodsIndexJob>();
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 管道装配阶段(builder.Build() 之后、app.Run() 之前):这里可以挂中间件、解析容器里的服务
|
||||
/// </summary>
|
||||
public override void UseModule(WebApplication app)
|
||||
public void UseModule(IApplicationBuilder app)
|
||||
{
|
||||
try
|
||||
{
|
||||
var Configuration = app.ApplicationServices.GetService<IConfiguration>()!;
|
||||
// Hangfire 后台任务看板(默认地址 /hangfire)
|
||||
app.UseHangfireDashboard("/hangfire", new DashboardOptions
|
||||
{
|
||||
Authorization = new[]
|
||||
{
|
||||
new HangfireCustomBasicAuthenticationFilter(){
|
||||
User = app.Configuration.GetValue<string>("Hangfire:Dashboard:User") ?? "admin",
|
||||
Pass = app.Configuration.GetValue<string>("Hangfire:Dashboard:Pass") ?? "1234560"
|
||||
User = Configuration.GetValue<string>("Hangfire:Dashboard:User") ?? "admin",
|
||||
Pass = Configuration.GetValue<string>("Hangfire:Dashboard:Pass") ?? "1234560"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 注册定时任务:把未同步(IndexStatus=0)的商品写入 OpenSearch 分词索引
|
||||
var cron = app.Configuration.GetValue<string>("Indexing:Cron") ?? "*/5 * * * *";
|
||||
var cron = Configuration.GetValue<string>("Indexing:Cron") ?? "*/5 * * * *";
|
||||
|
||||
// 用容器里的 IRecurringJobManager,而不是静态 RecurringJob:
|
||||
// AddHangfire 只把 JobStorage 注册进容器(没有设置 JobStorage.Current),静态 API 在 Build() 之后调用会直接抛异常。
|
||||
app.Services.GetRequiredService<IRecurringJobManager>().AddOrUpdate<GoodsIndexJob>(
|
||||
app.ApplicationServices.GetRequiredService<IRecurringJobManager>().AddOrUpdate<GoodsIndexJob>(
|
||||
"goods-index-pending",
|
||||
job => job.ProcessPendingAsync(),
|
||||
cron);
|
||||
|
||||
@@ -246,6 +246,65 @@ namespace Store.Modules.CloudProduct.Controllers
|
||||
return await _searchService.GetStatusAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查搜索索引是否存在。
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<bool> IndexExists()
|
||||
{
|
||||
return await _searchService.IndexExistsAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查索引映射(分析器)是否符合当前服务要求。
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
public async Task<bool> IndexMappingHealthy()
|
||||
{
|
||||
return await _searchService.IsIndexMappingHealthyAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建搜索索引(含 IK 分词映射,已存在且映射正常时跳过)。
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<bool> CreateIndex()
|
||||
{
|
||||
return await _searchService.CreateIndexAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除搜索索引。
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task DeleteIndex()
|
||||
{
|
||||
await _searchService.DeleteIndexAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动刷新搜索索引,使最新写入立即可搜索。
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task RefreshIndex()
|
||||
{
|
||||
await _searchService.RefreshIndexAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动新增或覆盖一条搜索文档。
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task AddSearchDocument([FromBody] GoodsSearchDocument document)
|
||||
{
|
||||
if (document == null || document.Id <= 0)
|
||||
{
|
||||
throw new AppException("请提供有效的搜索文档", ResultCode.ValidationError);
|
||||
}
|
||||
|
||||
await _searchService.AddDocumentAsync(document);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定商品详情:解压缩原始数据并返回 JSON。
|
||||
/// </summary>
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Store.Modules.CloudProduct.Dtos
|
||||
namespace Store.Modules.CloudProduct.Dtos
|
||||
{
|
||||
/// <summary>
|
||||
/// OpenSearch 运行状态
|
||||
@@ -24,18 +20,88 @@ namespace Store.Modules.CloudProduct.Dtos
|
||||
/// </summary>
|
||||
public bool Connected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// OpenSearch 服务及目标索引是否健康
|
||||
/// </summary>
|
||||
public bool Healthy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 集群名称
|
||||
/// </summary>
|
||||
public string? ClusterName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 集群状态(green、yellow 或 red)
|
||||
/// </summary>
|
||||
public string? ClusterStatus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 集群节点总数
|
||||
/// </summary>
|
||||
public int NumberOfNodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 数据节点数量
|
||||
/// </summary>
|
||||
public int NumberOfDataNodes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 活跃主分片数量
|
||||
/// </summary>
|
||||
public int ActivePrimaryShards { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 活跃分片总数
|
||||
/// </summary>
|
||||
public int ActiveShards { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 正在迁移的分片数量
|
||||
/// </summary>
|
||||
public int RelocatingShards { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 正在初始化的分片数量
|
||||
/// </summary>
|
||||
public int InitializingShards { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 未分配的分片数量
|
||||
/// </summary>
|
||||
public int UnassignedShards { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 索引是否存在
|
||||
/// </summary>
|
||||
public bool IndexExists { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文档总数(索引不存在时为 -1)
|
||||
/// 索引映射是否符合当前服务要求
|
||||
/// </summary>
|
||||
public bool MappingHealthy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标题字段的索引分析器
|
||||
/// </summary>
|
||||
public string? IndexAnalyzer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标题字段的搜索分析器
|
||||
/// </summary>
|
||||
public string? SearchAnalyzer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文档总数(索引不存在或读取失败时为 -1)
|
||||
/// </summary>
|
||||
public long DocumentCount { get; set; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// 说明信息
|
||||
/// 状态检测耗时(毫秒)
|
||||
/// </summary>
|
||||
public long ElapsedMilliseconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态说明或错误信息
|
||||
/// </summary>
|
||||
public string? Message { get; set; }
|
||||
}
|
||||
|
||||
@@ -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<long> ids);
|
||||
Task DeleteIndexAsync();
|
||||
Task<OpenSearchStatusDto> GetStatusAsync();
|
||||
Task<bool> IndexExistsAsync();
|
||||
Task<bool> IsIndexMappingHealthyAsync();
|
||||
Task RefreshIndexAsync();
|
||||
Task<long> RebuildIndexAsync(IEnumerable<GoodsEntity> goodsList);
|
||||
Task<(long Total, List<GoodsSearchDocument> 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<OpenSearchService> _logger;
|
||||
|
||||
public OpenSearchService(IConfiguration configuration)
|
||||
public OpenSearchService(IConfiguration configuration,ILogger<OpenSearchService> logger)
|
||||
{
|
||||
var config = configuration.GetSection("Search");
|
||||
_nodeUrl = config.GetValue<string>("Url") ?? "";
|
||||
_indexName = config.GetValue<string>("IndexName") ?? "";
|
||||
_client = CreateClient();
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private IOpenSearchClient CreateClient()
|
||||
@@ -100,34 +107,38 @@ namespace Store.Modules.CloudProduct.Services
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验索引映射是否健康:title 字段必须使用 IK 分析器
|
||||
/// </summary>
|
||||
private async Task<bool> IsIndexMappingHealthyAsync()
|
||||
public async Task<bool> 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<CreateIndexResponse> TryCreateIndexAsync(bool useIk)
|
||||
@@ -173,6 +184,23 @@ namespace Store.Modules.CloudProduct.Services
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ============ 3.1 批量写入商品搜索文档(存在则覆盖) ============
|
||||
/// </summary>
|
||||
@@ -198,8 +226,7 @@ namespace Store.Modules.CloudProduct.Services
|
||||
}
|
||||
|
||||
// 刷新索引,保证数据立即可搜索(仅用于演示,生产环境不建议频繁 Refresh)
|
||||
await _client.Indices.RefreshAsync(_indexName, d => d
|
||||
.RequestConfiguration(r => r.RequestTimeout(AdminTimeout)));
|
||||
await RefreshIndexAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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
|
||||
/// </summary>
|
||||
public async Task<OpenSearchStatusDto> 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<GoodsSearchDocument>(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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// CloudProduct 模块的启动器(<see cref="IStartup"/>):由 AddStoreHub() 自动发现,
|
||||
/// 按 <see cref="Priority"/> 降序执行 <see cref="Initialize"/> + <see cref="Register"/>。
|
||||
/// 需要配置或管道装配(app)的部分不在这里,见 <see cref="CloudProductModule"/>。
|
||||
/// </summary>
|
||||
|
||||
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<OpenSearchService>();
|
||||
await openSearchService.GetStatusAsync();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
+8
-15
@@ -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<Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions>(options =>
|
||||
{
|
||||
// 兼容迁移的同步控制器:允许同步读写请求/响应 IO(旧 asmx 行为)
|
||||
options.AllowSynchronousIO = true;
|
||||
});
|
||||
//builder.Services.Configure<Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions>(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();
|
||||
|
||||
@@ -49,6 +49,11 @@ namespace Store.Framework
|
||||
return KJDFsqlFactory(s);
|
||||
});
|
||||
}
|
||||
|
||||
public void Start(IServiceProvider serviceProvider)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user