using Hangfire;
using Hangfire.MySql;
using HangfireBasicAuthenticationFilter;
using MAX.Core.Interfaces;
using MAX.Framework.Interfaces;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MySql.Data.MySqlClient;
using Store.Modules.CloudProduct.Services;
namespace Store.Modules.CloudProduct
{
///
/// CloudProduct 模块的模块化入口():Hangfire 后台任务(商品分词同步)+ Hangfire 看板。
/// 与 (只做 IStartup 的 Initialize/Register)分开,是因为模块注册需要配置和 app:
///
/// - builder.AddStoreHub() 执行 (注册 Hangfire 服务,此时只能拿到容器和配置);
/// - app.UseStoreHubModules() 执行 (挂看板中间件 + 注册定时任务,此时才有 app)。
///
///
public class CloudProductModule : ModuleBase
{
///
/// 本模块自己的配置文件(源码里是 Models/CloudProduct/appsettings.json,
/// 构建时复制到宿主输出目录并改名成这个名字,避免与宿主的 appsettings.json 冲突)
///
public const string ConfigurationFileName = "appsettings.CloudProduct.json";
public override int Priority => IntPriority.Normal;
public override string? ConfigurationFile => ConfigurationFileName;
///
/// 服务注册阶段(builder.Build() 之前):这里只能拿到容器和配置,碰不到 app
///
public override void AddModule(ModuleContext context)
{
IServiceCollection services = context.Services;
IConfiguration configuration = context.Configuration;
// 本模块的配置节(Search / RawData / Indexing / Delete)都在模块自己的配置文件里,
// 关键项缺失直接报错,避免 OpenSearch 地址为空、原始数据写到意外目录之类的隐性故障
RequireConfiguration(configuration, "Search:Url");
RequireConfiguration(configuration, "Search:IndexName");
RequireConfiguration(configuration, "RawData:Path");
// ===== Hangfire 后台定时任务(商品分词同步) =====
var hangfireConnection = configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("缺少配置 ConnectionStrings:DefaultConnection,CloudProduct 模块的 Hangfire 无法启动。");
// AllowUserVariables=true:Hangfire.MySqlStorage 的建表/查询语句依赖用户变量
var connectionStringBuilder = new MySqlConnectionStringBuilder(hangfireConnection)
{
AllowUserVariables = true
};
services.AddHangfire(config => config
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseStorage(new MySqlStorage(connectionStringBuilder.ConnectionString, new MySqlStorageOptions() { TablesPrefix = "Hangfire_" })));
services.AddHangfireServer();
services.AddTransient();
}
///
/// 管道装配阶段(builder.Build() 之后、app.Run() 之前):这里可以挂中间件、解析容器里的服务
///
public override void UseModule(WebApplication app)
{
try
{
// Hangfire 后台任务看板(默认地址 /hangfire)
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new[]
{
new HangfireCustomBasicAuthenticationFilter(){
User = app.Configuration.GetValue("Hangfire:Dashboard:User") ?? "admin",
Pass = app.Configuration.GetValue("Hangfire:Dashboard:Pass") ?? "1234560"
}
}
});
// 注册定时任务:把未同步(IndexStatus=0)的商品写入 OpenSearch 分词索引
var cron = app.Configuration.GetValue("Indexing:Cron") ?? "*/5 * * * *";
// 用容器里的 IRecurringJobManager,而不是静态 RecurringJob:
// AddHangfire 只把 JobStorage 注册进容器(没有设置 JobStorage.Current),静态 API 在 Build() 之后调用会直接抛异常。
app.Services.GetRequiredService().AddOrUpdate(
"goods-index-pending",
job => job.ProcessPendingAsync(),
cron);
}
catch (Exception ex)
{
// 数据库不可用/表未初始化时避免阻断应用启动(看板与定时任务都会连库),恢复后重启即可重新装配
Console.WriteLine("[Hangfire] 看板/定时任务装配失败: " + ex.Message);
}
}
///
/// 校验模块配置项存在(值来自模块自己的 )
///
private static void RequireConfiguration(IConfiguration configuration, string key)
{
if (string.IsNullOrWhiteSpace(configuration.GetValue(key)))
{
throw new InvalidOperationException($"缺少配置 {key},请检查模块配置文件 {ConfigurationFileName}。");
}
}
}
}