2026-06-11 18:01:07 +08:00
|
|
|
|
using Hangfire;
|
|
|
|
|
|
using Hangfire.Dashboard;
|
|
|
|
|
|
using Hangfire.MemoryStorage;
|
|
|
|
|
|
using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ;
|
2026-06-25 18:01:37 +08:00
|
|
|
|
using QYZH.InteractiveMagazine.Infrastructure.Redis;
|
|
|
|
|
|
using QYZH.InteractiveMagazine.Infrastructure.SDK;
|
2026-06-11 18:01:07 +08:00
|
|
|
|
using QYZH.InteractiveMagazine.Models.Settings;
|
|
|
|
|
|
using QYZH.InteractiveMagazine.WorkService.Consumers;
|
|
|
|
|
|
using QYZH.InteractiveMagazine.WorkService.Jobs;
|
|
|
|
|
|
using Serilog;
|
|
|
|
|
|
using SqlSugar;
|
|
|
|
|
|
using SqlSugar.IOC;
|
|
|
|
|
|
using System.Linq.Expressions;
|
|
|
|
|
|
using System.Reflection;
|
|
|
|
|
|
using Yitter.IdGenerator;
|
|
|
|
|
|
|
|
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
|
|
|
|
|
|
|
|
// 加载配置
|
|
|
|
|
|
builder.Configuration
|
|
|
|
|
|
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
|
|
|
|
|
|
.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true);
|
|
|
|
|
|
|
|
|
|
|
|
// 配置Serilog
|
|
|
|
|
|
Log.Logger = new LoggerConfiguration()
|
|
|
|
|
|
.ReadFrom.Configuration(builder.Configuration)
|
|
|
|
|
|
.Enrich.FromLogContext()
|
|
|
|
|
|
.CreateLogger();
|
|
|
|
|
|
builder.Services.AddSerilog();
|
|
|
|
|
|
|
2026-07-10 10:44:00 +08:00
|
|
|
|
var snowflakeSettings = builder.Configuration.GetSection("SnowflakeSettings").Get<SnowflakeSettings>() ?? new SnowflakeSettings { WorkerId = 2 };
|
|
|
|
|
|
YitIdHelper.SetIdGenerator(new IdGeneratorOptions { WorkerId = snowflakeSettings.WorkerId });
|
2026-06-11 18:01:07 +08:00
|
|
|
|
|
|
|
|
|
|
// 初始化MySQL(SqlSugar)
|
|
|
|
|
|
builder.Services.AddSqlSugar(new IocConfig
|
|
|
|
|
|
{
|
|
|
|
|
|
ConfigId = 0,
|
|
|
|
|
|
DbType = IocDbType.MySql,
|
|
|
|
|
|
ConnectionString = builder.Configuration.GetConnectionString("DefaultConnection"),
|
|
|
|
|
|
IsAutoCloseConnection = true
|
|
|
|
|
|
});
|
|
|
|
|
|
SugarIocServices.ConfigurationSugar(db =>
|
|
|
|
|
|
{
|
|
|
|
|
|
db.Aop.OnLogExecuting = (sql, pars) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Log.Information("[SQL] {Sql}", UtilMethods.GetSqlString((DbType)IocDbType.MySql, sql, pars));
|
|
|
|
|
|
};
|
|
|
|
|
|
db.Aop.OnError = ex =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Log.Error(ex, "[SQL Error] {Message}", ex.Message);
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-25 18:01:37 +08:00
|
|
|
|
// 显式注册 ISqlSugarClient,供后台服务中通过 DI 解析
|
|
|
|
|
|
builder.Services.AddScoped<ISqlSugarClient>(_ => DbScoped.SugarScope);
|
|
|
|
|
|
|
2026-06-11 18:01:07 +08:00
|
|
|
|
// 配置Hangfire(内存存储,后续可切换Redis)
|
|
|
|
|
|
builder.Services.AddHangfire(config => config
|
|
|
|
|
|
.UseMemoryStorage()
|
|
|
|
|
|
.UseSerializerSettings(new Newtonsoft.Json.JsonSerializerSettings
|
|
|
|
|
|
{
|
|
|
|
|
|
TypeNameHandling = Newtonsoft.Json.TypeNameHandling.All
|
|
|
|
|
|
}));
|
|
|
|
|
|
builder.Services.AddHangfireServer();
|
|
|
|
|
|
|
2026-06-25 18:01:37 +08:00
|
|
|
|
// 配置Redis
|
|
|
|
|
|
builder.Services.AddCSRedisCacheExtension(builder.Configuration.GetSection("RedisSettings"));
|
|
|
|
|
|
|
|
|
|
|
|
// 配置HttpClient
|
|
|
|
|
|
builder.Services.AddHttpClient();
|
|
|
|
|
|
|
|
|
|
|
|
// 配置OSS/SDK服务
|
|
|
|
|
|
builder.Services.AddSDKService(builder.Configuration);
|
|
|
|
|
|
|
2026-06-11 18:01:07 +08:00
|
|
|
|
// 配置RabbitMQ
|
|
|
|
|
|
builder.Services.AddRabbitMQ(builder.Configuration);
|
|
|
|
|
|
|
|
|
|
|
|
// 注册队列消费者(新增消费者只需实现 IQueueConsumer 并在此注册)
|
|
|
|
|
|
builder.Services.AddScoped<IQueueConsumer, JournalTaskReceiveConsumer>();
|
2026-07-02 09:46:34 +08:00
|
|
|
|
builder.Services.AddScoped<IQueueConsumer, UserJournalQrCodeGenerateConsumer>();
|
2026-06-11 18:01:07 +08:00
|
|
|
|
|
|
|
|
|
|
// 注册消费者后台服务
|
|
|
|
|
|
builder.Services.AddHostedService<RabbitMQHostedService>();
|
2026-07-10 10:44:00 +08:00
|
|
|
|
builder.Services.AddHostedService<MessageOutboxDispatchService>();
|
2026-06-11 18:01:07 +08:00
|
|
|
|
|
|
|
|
|
|
// 从配置文件读取定时任务列表
|
|
|
|
|
|
var jobSettings = builder.Configuration.GetSection("HangfireJobs").Get<HangfireJobSettings>();
|
|
|
|
|
|
|
|
|
|
|
|
var app = builder.Build();
|
|
|
|
|
|
|
|
|
|
|
|
// 配置Hangfire Dashboard(仅本机访问)
|
|
|
|
|
|
app.UseHangfireDashboard("/hangfire", new DashboardOptions
|
|
|
|
|
|
{
|
|
|
|
|
|
Authorization = new[] { new LocalRequestsOnlyAuthorizationFilter() }
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 根据配置动态注册定时任务
|
|
|
|
|
|
if (jobSettings?.Jobs != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var job in jobSettings.Jobs)
|
|
|
|
|
|
{
|
|
|
|
|
|
var jobType = Type.GetType(job.JobType);
|
|
|
|
|
|
if (jobType == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Log.Warning("定时任务 [{Name}] 类型未找到: {JobType},跳过注册", job.Name, job.JobType);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!job.Enabled)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 配置为关闭的任务,从 Hangfire 中移除
|
|
|
|
|
|
RecurringJob.RemoveIfExists(job.Name);
|
|
|
|
|
|
Log.Information("定时任务 [{Name}] 已禁用,已移除", job.Name);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构造表达式:job => job.MethodName()
|
|
|
|
|
|
var method = jobType.GetMethod(job.MethodName);
|
|
|
|
|
|
if (method == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Log.Warning("定时任务 [{Name}] 方法未找到: {MethodName},跳过注册", job.Name, job.MethodName);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var param = Expression.Parameter(jobType, "job");
|
|
|
|
|
|
var call = Expression.Call(param, method);
|
2026-07-09 18:01:37 +08:00
|
|
|
|
if (method.ReturnType == typeof(void))
|
|
|
|
|
|
{
|
|
|
|
|
|
var lambda = Expression.Lambda(typeof(Action<>).MakeGenericType(jobType), call, param);
|
|
|
|
|
|
InvokeRecurringJobAddOrUpdate(jobType, typeof(Action<>), job.Name, lambda, job.Cron);
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (method.ReturnType == typeof(Task))
|
|
|
|
|
|
{
|
|
|
|
|
|
var lambda = Expression.Lambda(typeof(Func<,>).MakeGenericType(jobType, typeof(Task)), call, param);
|
|
|
|
|
|
InvokeRecurringJobAddOrUpdate(jobType, typeof(Func<,>), job.Name, lambda, job.Cron);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
Log.Warning("定时任务 [{Name}] 方法返回类型不支持 {ReturnType},跳过注册", job.Name, method.ReturnType);
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-06-11 18:01:07 +08:00
|
|
|
|
|
|
|
|
|
|
Log.Information("定时任务 [{Name}] 已注册,Cron: {Cron}", job.Name, job.Cron);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Log.Information("WorkService 已启动,Hangfire Dashboard: /hangfire");
|
|
|
|
|
|
|
|
|
|
|
|
app.Run();
|
2026-07-09 18:01:37 +08:00
|
|
|
|
|
|
|
|
|
|
static void InvokeRecurringJobAddOrUpdate(Type jobType, Type delegateGenericTypeDefinition, string jobName, LambdaExpression lambda, string cron)
|
|
|
|
|
|
{
|
|
|
|
|
|
var method = typeof(RecurringJob).GetMethods()
|
|
|
|
|
|
.Where(m => m.Name == nameof(RecurringJob.AddOrUpdate) && m.IsGenericMethodDefinition)
|
|
|
|
|
|
.First(m =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var parameters = m.GetParameters();
|
|
|
|
|
|
if (parameters.Length != 3
|
|
|
|
|
|
|| parameters[0].ParameterType != typeof(string)
|
|
|
|
|
|
|| parameters[2].ParameterType != typeof(string)
|
|
|
|
|
|
|| !parameters[1].ParameterType.IsGenericType
|
|
|
|
|
|
|| parameters[1].ParameterType.GetGenericTypeDefinition() != typeof(Expression<>))
|
|
|
|
|
|
{
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var expressionArgument = parameters[1].ParameterType.GetGenericArguments()[0];
|
|
|
|
|
|
return expressionArgument.IsGenericType
|
|
|
|
|
|
&& expressionArgument.GetGenericTypeDefinition() == delegateGenericTypeDefinition;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
method.MakeGenericMethod(jobType).Invoke(null, [jobName, lambda, cron]);
|
|
|
|
|
|
}
|