// ============================================================================
// 【文件说明】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;
///
/// 门户插件启动类。
///
/// 【C# 语法知识点 - AppStartup 特性】
/// [AppStartup(100)] 是 Furion 框架的特性,标记这是一个插件启动类。
///
/// 参数 100 是"启动顺序":
/// - 数值越小,越早执行
/// - 主应用通常是 0-99,插件可以是 100+
/// - 这样可以控制依赖关系:先启动核心服务,再启动插件服务
///
/// 对比 Java Spring Boot:
// Spring 用 @Order 注解或 Ordered 接口控制顺序:
// @Order(100)
// public class MyConfig { ... }
///
///
/// 【C# 语法知识点 - 继承 AppStartup】
/// public class Startup : AppStartup
///
/// AppStartup 是 Furion 提供的基类/接口,定义了插件必须实现的方法:
/// - ConfigureServices:注册服务到 DI 容器
/// - Configure:配置中间件管道
///
/// 这是"模板方法模式"的应用:框架定义流程,插件填充具体实现。
///
///
[AppStartup(100)]
public class Startup : AppStartup
{
///
/// 配置服务(注册到 DI 容器)。
///
/// 【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 是自动扫描(更便利),各有优劣。
///
///
/// 【C# 语法知识点 - 三种服务生命周期】
/// ASP.NET Core DI 容器支持三种服务生命周期:
///
/// 1. AddSingleton(单例):
/// - 整个应用只创建一个实例
/// - 适合:配置类、工具类、缓存
/// - 线程安全需要考虑
/// - 类比 Java 的 @Scope("singleton")
///
/// 2. AddScoped(作用域):
/// - 每个 HTTP 请求创建一个实例
/// - 适合:数据库上下文、业务服务
/// - 同一个请求内,多次获取返回同一实例
/// - 类比 Java 的 @Scope("request")
///
/// 3. AddTransient(瞬态):
/// - 每次获取都创建新实例
/// - 适合:轻量级服务、无状态服务
/// - 类比 Java 的 @Scope("prototype")
///
///
/// 服务注册表
public void ConfigureServices(IServiceCollection services)
{
// 【配置选项注册】
// AddConfigurableOptions() 是 Furion 的扩展方法,
// 把配置类注册到 DI 容器,并绑定到配置文件(如 appsettings.json)。
//
// 对比 Java:
// Java Spring Boot 用 @ConfigurationProperties + @EnableConfigurationProperties:
// @ConfigurationProperties(prefix = "hwportal.search")
// public class HwPortalSearchOptions { ... }
services.AddConfigurableOptions();
// 【Singleton 生命周期】
// 整个应用只保留一个 HwPortalMapperRegistry 实例。
//
// 为什么用 Singleton?
// 1. HwPortalMapperRegistry 内部缓存了 XML Mapper 定义
// 2. 这些 XML 在应用运行期间不会变化
// 3. 多个请求共享同一个缓存,节省内存
//
// 对比 Java:
// Java 单例通常用 @Service(默认就是单例)
services.AddSingleton();
// 【Scoped 生命周期】
// 每个 HTTP 请求创建一个 HwPortalMyBatisExecutor 实例。
//
// 为什么用 Scoped?
// 1. 执行器依赖数据库客户端(ISqlSugarClient),通常是 Scoped
// 2. 同一个请求内多次执行 SQL,应该共用同一个连接/事务
// 3. 请求结束后自动释放资源
//
// 对比 Java:
// Java 通常用 @Repository 或 @Service(默认单例,但 @Autowired 注入的 SqlSession 是 request-scoped)
services.AddScoped();
// 搜索文本提取器是无状态对象,也注册为 Scoped。
services.AddScoped();
// 【EF Core 数据库上下文注册】
// AddDbContext() 注册 Entity Framework Core 的数据库上下文。
//
// 为什么用 Lambda 表达式?
// 参数是一个委托(函数),在第一次需要 DbContext 时才执行配置。
// 这叫"延迟初始化",避免应用启动时就建立数据库连接。
//
// 对比 Java:
// Java Spring Boot 自动配置 DataSource,不需要显式注册:
// spring.datasource.url=jdbc:mysql://...
services.AddDbContext((provider, options) =>
{
// 从 DI 容器获取配置选项
// GetRequiredService():获取服务,如果不存在会抛异常
// IOptions 是 ASP.NET Core 的配置包装接口
HwPortalSearchOptions searchOptions = provider.GetRequiredService>().Value;
DbConnectionOptions dbOptions = provider.GetRequiredService>().Value;
// 解析连接字符串(可能是独立配置,也可能是复用主库)
string connectionString = ResolveSearchConnectionString(searchOptions, dbOptions);
// 配置 MySQL 连接
// UseMySql():指定使用 MySQL 数据库提供程序
// ServerVersion.AutoDetect():自动检测 MySQL 服务器版本
options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), builder =>
{
// 启用失败重试:短暂网络故障时自动重试
builder.EnableRetryOnFailure();
});
});
// 【注册搜索相关服务】
// AddScoped() 是接口到实现的映射。
//
// 为什么面向接口编程?
// 1. 解耦:调用者依赖接口,不依赖具体实现
// 2. 可测试:单元测试时可以 Mock 接口
// 3. 可替换:可以随时替换实现类
//
// 对比 Java:
// Java Spring 自动根据类型注入,通常不需要显式注册:
// @Service
// public class HwSearchSchemaService implements IHwSearchSchemaService { ... }
services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped();
}
///
/// 配置中间件管道。
///
/// 【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 { ... }
///
///
/// 【当前实现说明】
/// 这个方法目前为空,因为门户插件不需要额外的中间件。
/// 如果未来需要添加自定义中间件(如请求日志、权限校验),在这里添加。
///
///
/// 应用构建器
/// 主机环境信息
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// 这里暂时不追加自定义中间件。
// 如果未来 hw-portal 需要自己的中间件,也是在这里接入。
//
// 示例:添加一个自定义中间件
// app.UseMiddleware();
//
// 示例:添加静态文件服务
// app.UseStaticFiles();
}
///
/// 解析搜索数据库的连接字符串。
///
/// 【业务逻辑说明】
/// 搜索功能可以:
/// 1. 使用独立的 MySQL 连接(推荐,隔离搜索负载)
/// 2. 复用主库连接(简单,但不适合高并发搜索场景)
///
/// 这个方法实现"自动回退"逻辑:
/// - 如果配置了独立连接,就用独立连接
/// - 如果没配置且允许回退,就复用主库
/// - 如果没配置且不允许回退,抛异常
///
///
/// 【C# 语法知识点 - private static 方法】
/// private:只能在当前类内部调用
/// static:不依赖实例状态,可以直接用 Startup.ResolveSearchConnectionString(...) 调用
///
/// 为什么用 static?
/// 1. 这个方法不访问任何实例成员(没有用到 this)
/// 2. 标记为 static 可以让编译器优化
/// 3. 语义清晰:这是一个纯工具方法
///
///
/// 【C# 语法知识点 - 空值检查】
/// string.IsNullOrWhiteSpace(str) 检查:
/// - null
/// - 空字符串 ""
/// - 纯空白字符串 " "
///
/// 这是 .NET 提供的最佳实践,比 str == null || str.Trim() == "" 更高效。
///
/// 对比 Java:
/// Java 需要:
/// if (str == null || str.trim().isEmpty()) { ... }
///
///
/// 搜索配置选项
/// 数据库连接配置
/// 解析后的连接字符串
/// 配置无效时抛出
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;
}
}