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