You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

341 lines
15 KiB
C#

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// ============================================================================
// 【文件说明】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 StreamdbOptions.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;
}
}