Files
QYZH.InteractiveMagazine/QYZH.InteractiveMagazine.WorkService/Program.cs
glz 766be485d0 feat: 新增操作日志功能并优化定时任务配置
1.  新增OperationLogAttribute与OperationLogActionFilter,实现自动化操作日志记录
2.  新增OperationLogRecordInput输入模型,重构IOperationLogService日志接口
3.  为所有业务控制器接口添加操作日志注解
4.  调整期刊AI批改任务的执行周期与方法适配异步调用
5.  优化Hangfire定时任务注册逻辑,支持异步任务
6.  补充完善操作日志类型与目标类型枚举
2026-07-09 18:01:37 +08:00

173 lines
6.0 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Hangfire;
using Hangfire.Dashboard;
using Hangfire.MemoryStorage;
using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ;
using QYZH.InteractiveMagazine.Infrastructure.Redis;
using QYZH.InteractiveMagazine.Infrastructure.SDK;
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();
// 初始化雪花ID生成器
YitIdHelper.SetIdGenerator(new IdGeneratorOptions { WorkerId = 2 });
// 初始化MySQLSqlSugar
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);
};
});
// 显式注册 ISqlSugarClient供后台服务中通过 DI 解析
builder.Services.AddScoped<ISqlSugarClient>(_ => DbScoped.SugarScope);
// 配置Hangfire内存存储后续可切换Redis
builder.Services.AddHangfire(config => config
.UseMemoryStorage()
.UseSerializerSettings(new Newtonsoft.Json.JsonSerializerSettings
{
TypeNameHandling = Newtonsoft.Json.TypeNameHandling.All
}));
builder.Services.AddHangfireServer();
// 配置Redis
builder.Services.AddCSRedisCacheExtension(builder.Configuration.GetSection("RedisSettings"));
// 配置HttpClient
builder.Services.AddHttpClient();
// 配置OSS/SDK服务
builder.Services.AddSDKService(builder.Configuration);
// 配置RabbitMQ
builder.Services.AddRabbitMQ(builder.Configuration);
// 注册队列消费者(新增消费者只需实现 IQueueConsumer 并在此注册)
builder.Services.AddScoped<IQueueConsumer, JournalTaskReceiveConsumer>();
builder.Services.AddScoped<IQueueConsumer, UserJournalQrCodeGenerateConsumer>();
// 注册消费者后台服务
builder.Services.AddHostedService<RabbitMQHostedService>();
// 从配置文件读取定时任务列表
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);
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;
}
Log.Information("定时任务 [{Name}] 已注册Cron: {Cron}", job.Name, job.Cron);
}
}
Log.Information("WorkService 已启动Hangfire Dashboard: /hangfire");
app.Run();
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]);
}