|
|
// ============================================================================
|
|
|
// 【文件说明】Startup.cs - 门户插件启动类
|
|
|
// ============================================================================
|
|
|
// 这是 Admin.NET 插件架构的核心启动类,负责注册门户模块的所有服务。
|
|
|
//
|
|
|
// 【架构定位 - 什么是插件启动类?】
|
|
|
// 在 Admin.NET/Furion 框架中,插件是一个独立的 .dll 文件,可以被主应用动态加载。
|
|
|
// 每个插件都需要一个 Startup 类来告诉框架:
|
|
|
// 1. 我(插件)提供了哪些服务
|
|
|
// 2. 我需要哪些配置
|
|
|
// 3. 我的中间件应该在什么位置插入
|
|
|
//
|
|
|
// 【对比 Java Spring Boot】
|
|
|
// Java Spring Boot 的自动配置:
|
|
|
// @Configuration
|
|
|
// @EnableAutoConfiguration
|
|
|
// public class MyConfig { ... }
|
|
|
//
|
|
|
// 或者使用 spring.factories 文件声明自动配置类。
|
|
|
//
|
|
|
// Admin.NET 的方式:
|
|
|
// [AppStartup(100)]
|
|
|
// public class Startup : AppStartup { ... }
|
|
|
//
|
|
|
// 两者概念相同:都是在应用启动时执行配置代码。
|
|
|
// ============================================================================
|
|
|
|
|
|
using Admin.NET.Core;
|
|
|
using Microsoft.AspNetCore.Builder;
|
|
|
using Microsoft.AspNetCore.Hosting;
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
using Microsoft.Extensions.Options;
|
|
|
|
|
|
namespace Admin.NET.Plugin.HwPortal;
|
|
|
|
|
|
/// <summary>
|
|
|
/// 门户插件启动类。
|
|
|
/// <para>
|
|
|
/// 【C# 语法知识点 - AppStartup 特性】
|
|
|
/// [AppStartup(100)] 是 Furion 框架的特性,标记这是一个插件启动类。
|
|
|
///
|
|
|
/// 参数 100 是"启动顺序":
|
|
|
/// - 数值越小,越早执行
|
|
|
/// - 主应用通常是 0-99,插件可以是 100+
|
|
|
/// - 这样可以控制依赖关系:先启动核心服务,再启动插件服务
|
|
|
///
|
|
|
/// 对比 Java Spring Boot:
|
|
|
// Spring 用 @Order 注解或 Ordered 接口控制顺序:
|
|
|
// @Order(100)
|
|
|
// public class MyConfig { ... }
|
|
|
/// </para>
|
|
|
/// <para>
|
|
|
/// 【C# 语法知识点 - 继承 AppStartup】
|
|
|
/// public class Startup : AppStartup
|
|
|
///
|
|
|
/// AppStartup 是 Furion 提供的基类/接口,定义了插件必须实现的方法:
|
|
|
/// - ConfigureServices:注册服务到 DI 容器
|
|
|
/// - Configure:配置中间件管道
|
|
|
///
|
|
|
/// 这是"模板方法模式"的应用:框架定义流程,插件填充具体实现。
|
|
|
/// </para>
|
|
|
/// </summary>
|
|
|
[AppStartup(100)]
|
|
|
public class Startup : AppStartup
|
|
|
{
|
|
|
/// <summary>
|
|
|
/// 配置服务(注册到 DI 容器)。
|
|
|
/// <para>
|
|
|
/// 【C# 语法知识点 - IServiceCollection】
|
|
|
/// IServiceCollection 是 ASP.NET Core 的"服务注册表"(依赖注入容器)。
|
|
|
///
|
|
|
/// 你可以把它想象成一个"服务工厂":
|
|
|
/// - 在这里注册服务:"如果有人需要 IXxxService,就给他 XxxService 实例"
|
|
|
/// - 框架会在运行时自动解析依赖关系
|
|
|
///
|
|
|
/// 对比 Java Spring Boot:
|
|
|
/// Java 用 @Bean 注解或 @ComponentScan 自动扫描:
|
|
|
/// @Configuration
|
|
|
/// public class Config {
|
|
|
/// @Bean
|
|
|
/// public MyService myService() {
|
|
|
/// return new MyService();
|
|
|
/// }
|
|
|
/// }
|
|
|
///
|
|
|
/// C# 是显式注册(更可控),Java 是自动扫描(更便利),各有优劣。
|
|
|
/// </para>
|
|
|
/// <para>
|
|
|
/// 【C# 语法知识点 - 三种服务生命周期】
|
|
|
/// ASP.NET Core DI 容器支持三种服务生命周期:
|
|
|
///
|
|
|
/// 1. AddSingleton(单例):
|
|
|
/// - 整个应用只创建一个实例
|
|
|
/// - 适合:配置类、工具类、缓存
|
|
|
/// - 线程安全需要考虑
|
|
|
/// - 类比 Java 的 @Scope("singleton")
|
|
|
///
|
|
|
/// 2. AddScoped(作用域):
|
|
|
/// - 每个 HTTP 请求创建一个实例
|
|
|
/// - 适合:数据库上下文、业务服务
|
|
|
/// - 同一个请求内,多次获取返回同一实例
|
|
|
/// - 类比 Java 的 @Scope("request")
|
|
|
///
|
|
|
/// 3. AddTransient(瞬态):
|
|
|
/// - 每次获取都创建新实例
|
|
|
/// - 适合:轻量级服务、无状态服务
|
|
|
/// - 类比 Java 的 @Scope("prototype")
|
|
|
/// </para>
|
|
|
/// </summary>
|
|
|
/// <param name="services">服务注册表</param>
|
|
|
public void ConfigureServices(IServiceCollection services)
|
|
|
{
|
|
|
// 【配置选项注册】
|
|
|
// AddConfigurableOptions<T>() 是 Furion 的扩展方法,
|
|
|
// 把配置类注册到 DI 容器,并绑定到配置文件(如 appsettings.json)。
|
|
|
//
|
|
|
// 对比 Java:
|
|
|
// Java Spring Boot 用 @ConfigurationProperties + @EnableConfigurationProperties:
|
|
|
// @ConfigurationProperties(prefix = "hwportal.search")
|
|
|
// public class HwPortalSearchOptions { ... }
|
|
|
services.AddConfigurableOptions<HwPortalSearchOptions>();
|
|
|
|
|
|
// 【Singleton 生命周期】
|
|
|
// 整个应用只保留一个 HwPortalMapperRegistry 实例。
|
|
|
//
|
|
|
// 为什么用 Singleton?
|
|
|
// 1. HwPortalMapperRegistry 内部缓存了 XML Mapper 定义
|
|
|
// 2. 这些 XML 在应用运行期间不会变化
|
|
|
// 3. 多个请求共享同一个缓存,节省内存
|
|
|
//
|
|
|
// 对比 Java:
|
|
|
// Java 单例通常用 @Service(默认就是单例)
|
|
|
services.AddSingleton<HwPortalMapperRegistry>();
|
|
|
|
|
|
// 【Scoped 生命周期】
|
|
|
// 每个 HTTP 请求创建一个 HwPortalMyBatisExecutor 实例。
|
|
|
//
|
|
|
// 为什么用 Scoped?
|
|
|
// 1. 执行器依赖数据库客户端(ISqlSugarClient),通常是 Scoped
|
|
|
// 2. 同一个请求内多次执行 SQL,应该共用同一个连接/事务
|
|
|
// 3. 请求结束后自动释放资源
|
|
|
//
|
|
|
// 对比 Java:
|
|
|
// Java 通常用 @Repository 或 @Service(默认单例,但 @Autowired 注入的 SqlSession 是 request-scoped)
|
|
|
services.AddScoped<HwPortalMyBatisExecutor>();
|
|
|
|
|
|
// 搜索文本提取器是无状态对象,也注册为 Scoped。
|
|
|
services.AddScoped<PortalSearchDocConverter>();
|
|
|
|
|
|
// 【EF Core 数据库上下文注册】
|
|
|
// AddDbContext<T>() 注册 Entity Framework Core 的数据库上下文。
|
|
|
//
|
|
|
// 为什么用 Lambda 表达式?
|
|
|
// 参数是一个委托(函数),在第一次需要 DbContext 时才执行配置。
|
|
|
// 这叫"延迟初始化",避免应用启动时就建立数据库连接。
|
|
|
//
|
|
|
// 对比 Java:
|
|
|
// Java Spring Boot 自动配置 DataSource,不需要显式注册:
|
|
|
// spring.datasource.url=jdbc:mysql://...
|
|
|
services.AddDbContext<HwPortalSearchDbContext>((provider, options) =>
|
|
|
{
|
|
|
// 从 DI 容器获取配置选项
|
|
|
// GetRequiredService<T>():获取服务,如果不存在会抛异常
|
|
|
// IOptions<T> 是 ASP.NET Core 的配置包装接口
|
|
|
HwPortalSearchOptions searchOptions = provider.GetRequiredService<IOptions<HwPortalSearchOptions>>().Value;
|
|
|
DbConnectionOptions dbOptions = provider.GetRequiredService<IOptions<DbConnectionOptions>>().Value;
|
|
|
|
|
|
// 解析连接字符串(可能是独立配置,也可能是复用主库)
|
|
|
string connectionString = ResolveSearchConnectionString(searchOptions, dbOptions);
|
|
|
|
|
|
// 配置 MySQL 连接
|
|
|
// UseMySql():指定使用 MySQL 数据库提供程序
|
|
|
// ServerVersion.AutoDetect():自动检测 MySQL 服务器版本
|
|
|
options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), builder =>
|
|
|
{
|
|
|
// 启用失败重试:短暂网络故障时自动重试
|
|
|
builder.EnableRetryOnFailure();
|
|
|
});
|
|
|
});
|
|
|
|
|
|
// 【注册搜索相关服务】
|
|
|
// AddScoped<IInterface, TImplementation>() 是接口到实现的映射。
|
|
|
//
|
|
|
// 为什么面向接口编程?
|
|
|
// 1. 解耦:调用者依赖接口,不依赖具体实现
|
|
|
// 2. 可测试:单元测试时可以 Mock 接口
|
|
|
// 3. 可替换:可以随时替换实现类
|
|
|
//
|
|
|
// 对比 Java:
|
|
|
// Java Spring 自动根据类型注入,通常不需要显式注册:
|
|
|
// @Service
|
|
|
// public class HwSearchSchemaService implements IHwSearchSchemaService { ... }
|
|
|
services.AddScoped<IHwSearchSchemaService, HwSearchSchemaService>();
|
|
|
services.AddScoped<IHwSearchIndexService, HwSearchIndexService>();
|
|
|
services.AddScoped<HwSearchQueryService>();
|
|
|
services.AddScoped<LegacyHwSearchQueryService>();
|
|
|
services.AddScoped<IHwSearchRebuildService, HwSearchRebuildService>();
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 配置中间件管道。
|
|
|
/// <para>
|
|
|
/// 【C# 语法知识点 - IApplicationBuilder】
|
|
|
/// IApplicationBuilder 是 ASP.NET Core 的"中间件管道构建器"。
|
|
|
///
|
|
|
/// 什么是中间件?
|
|
|
/// 中间件是处理 HTTP 请求的"管道组件",每个中间件可以选择:
|
|
|
/// 1. 处理请求并返回响应(短路)
|
|
|
/// 2. 处理请求后,传递给下一个中间件
|
|
|
/// 3. 请求返回后,执行一些后置处理
|
|
|
///
|
|
|
/// 中间件执行顺序(洋葱模型):
|
|
|
/// 请求 → 中间件1 → 中间件2 → 控制器 → 中间件2 → 中间件1 → 响应
|
|
|
///
|
|
|
/// 对比 Java:
|
|
|
/// Java Spring Boot 用 Filter 或 HandlerInterceptor:
|
|
|
/// @Component
|
|
|
/// public class MyFilter implements Filter { ... }
|
|
|
/// </para>
|
|
|
/// <para>
|
|
|
/// 【当前实现说明】
|
|
|
/// 这个方法目前为空,因为门户插件不需要额外的中间件。
|
|
|
/// 如果未来需要添加自定义中间件(如请求日志、权限校验),在这里添加。
|
|
|
/// </para>
|
|
|
/// </summary>
|
|
|
/// <param name="app">应用构建器</param>
|
|
|
/// <param name="env">主机环境信息</param>
|
|
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
|
|
{
|
|
|
// 这里暂时不追加自定义中间件。
|
|
|
// 如果未来 hw-portal 需要自己的中间件,也是在这里接入。
|
|
|
//
|
|
|
// 示例:添加一个自定义中间件
|
|
|
// app.UseMiddleware<HwPortalMiddleware>();
|
|
|
//
|
|
|
// 示例:添加静态文件服务
|
|
|
// app.UseStaticFiles();
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
/// 解析搜索数据库的连接字符串。
|
|
|
/// <para>
|
|
|
/// 【业务逻辑说明】
|
|
|
/// 搜索功能可以:
|
|
|
/// 1. 使用独立的 MySQL 连接(推荐,隔离搜索负载)
|
|
|
/// 2. 复用主库连接(简单,但不适合高并发搜索场景)
|
|
|
///
|
|
|
/// 这个方法实现"自动回退"逻辑:
|
|
|
/// - 如果配置了独立连接,就用独立连接
|
|
|
/// - 如果没配置且允许回退,就复用主库
|
|
|
/// - 如果没配置且不允许回退,抛异常
|
|
|
/// </para>
|
|
|
/// <para>
|
|
|
/// 【C# 语法知识点 - private static 方法】
|
|
|
/// private:只能在当前类内部调用
|
|
|
/// static:不依赖实例状态,可以直接用 Startup.ResolveSearchConnectionString(...) 调用
|
|
|
///
|
|
|
/// 为什么用 static?
|
|
|
/// 1. 这个方法不访问任何实例成员(没有用到 this)
|
|
|
/// 2. 标记为 static 可以让编译器优化
|
|
|
/// 3. 语义清晰:这是一个纯工具方法
|
|
|
/// </para>
|
|
|
/// <para>
|
|
|
/// 【C# 语法知识点 - 空值检查】
|
|
|
/// string.IsNullOrWhiteSpace(str) 检查:
|
|
|
/// - null
|
|
|
/// - 空字符串 ""
|
|
|
/// - 纯空白字符串 " "
|
|
|
///
|
|
|
/// 这是 .NET 提供的最佳实践,比 str == null || str.Trim() == "" 更高效。
|
|
|
///
|
|
|
/// 对比 Java:
|
|
|
/// Java 需要:
|
|
|
/// if (str == null || str.trim().isEmpty()) { ... }
|
|
|
/// </para>
|
|
|
/// </summary>
|
|
|
/// <param name="searchOptions">搜索配置选项</param>
|
|
|
/// <param name="dbOptions">数据库连接配置</param>
|
|
|
/// <returns>解析后的连接字符串</returns>
|
|
|
/// <exception cref="InvalidOperationException">配置无效时抛出</exception>
|
|
|
private static string ResolveSearchConnectionString(HwPortalSearchOptions searchOptions, DbConnectionOptions dbOptions)
|
|
|
{
|
|
|
// 【第一优先级】如果配置了独立的搜索连接串,直接使用
|
|
|
if (!string.IsNullOrWhiteSpace(searchOptions.ConnectionString))
|
|
|
{
|
|
|
// .Trim() 去除首尾空格,避免配置错误
|
|
|
return searchOptions.ConnectionString.Trim();
|
|
|
}
|
|
|
|
|
|
// 【检查回退策略】
|
|
|
// 如果不允许回退到主库连接,直接抛异常
|
|
|
// 这是"防御性编程":明确失败比隐式行为更安全
|
|
|
if (!searchOptions.UseMainDbConnectionWhenEmpty)
|
|
|
{
|
|
|
throw new InvalidOperationException("HwPortalSearch.ConnectionString 未配置,且已关闭主库回退。");
|
|
|
}
|
|
|
|
|
|
// 【第二优先级】复用主库连接
|
|
|
// FirstOrDefault() 是 LINQ 方法,返回第一个元素或默认值(null)
|
|
|
//
|
|
|
// 对比 Java:
|
|
|
// Java Stream:dbOptions.getConnectionConfigs().stream().findFirst().orElse(null);
|
|
|
DbConnectionConfig mainConfig = dbOptions?.ConnectionConfigs?.FirstOrDefault();
|
|
|
|
|
|
// 检查主库连接是否存在且有效
|
|
|
if (mainConfig == null || string.IsNullOrWhiteSpace(mainConfig.ConnectionString))
|
|
|
{
|
|
|
throw new InvalidOperationException("未找到可复用的主库连接串,请在 Search.json 中配置 HwPortalSearch.ConnectionString。");
|
|
|
}
|
|
|
|
|
|
// 【数据库类型检查】
|
|
|
// 当前搜索功能只支持 MySQL,如果主库是 SQL Server/PostgreSQL 等,不能复用
|
|
|
// 这是业务约束,提前检查可以避免运行时的奇怪错误
|
|
|
if (mainConfig.DbType != SqlSugar.DbType.MySql && mainConfig.DbType != SqlSugar.DbType.MySqlConnector)
|
|
|
{
|
|
|
throw new InvalidOperationException("HwPortalSearch 默认只支持复用 MySQL 主库连接,请单独配置 HwPortalSearch.ConnectionString。");
|
|
|
}
|
|
|
|
|
|
// 【获取连接字符串】
|
|
|
string connectionString = mainConfig.ConnectionString;
|
|
|
|
|
|
// 【加密处理】
|
|
|
// 如果主库连接串是加密的(生产环境推荐),需要解密
|
|
|
// DbSettings?.EnableConnStringEncrypt 是"可空链式调用":
|
|
|
// - 如果 DbSettings 为 null,不会抛异常,整个表达式为 null
|
|
|
// - 这是 C# 的"空条件运算符"?. 语法
|
|
|
//
|
|
|
// 对比 Java:
|
|
|
// Java 需要:
|
|
|
// if (mainConfig.getDbSettings() != null && mainConfig.getDbSettings().getEnableConnStringEncrypt()) { ... }
|
|
|
if (mainConfig.DbSettings?.EnableConnStringEncrypt == true)
|
|
|
{
|
|
|
// 调用框架提供的解密工具
|
|
|
connectionString = CryptogramUtil.Decrypt(connectionString);
|
|
|
}
|
|
|
|
|
|
return connectionString;
|
|
|
}
|
|
|
}
|