using AgileConfig.Client; using Autofac; using Autofac.Extensions.DependencyInjection; using FreeSql; using Hangfire; using Hangfire.SqlServer; using LiveForum.Code.Extend; using LiveForum.Code.JsonConverterExtend; using LiveForum.Code.JwtInfrastructure; using LiveForum.Code.MiddlewareExtend; using LiveForum.Code.Redis.Contract; using LiveForum.Code.SensitiveWord.Interfaces; using LiveForum.Model; using LiveForum.Model.Dto.Others; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; using Serilog; using SKIT.FlurlHttpClient.Wechat.Api; using StackExchange.Redis; using System.Reflection; var builder = WebApplication.CreateBuilder(args); #region AgileConfig配置中心 // 配置AgileConfig,从appsettings.json读取AgileConfig节点配置 // 注意:AgileConfig会作为配置源添加到Configuration中,优先级高于appsettings.json builder.Host.UseAgileConfig(e => { // 从appsettings.json读取AgileConfig配置 var agileConfig = builder.Configuration.GetSection("AgileConfig"); e.AppId = agileConfig["appId"] ?? "LiveForum"; e.Secret = agileConfig["secret"] ?? ""; e.Nodes = agileConfig["nodes"] ?? agileConfig["url"] ?? ""; e.Name = agileConfig["appId"] ?? "LiveForum"; e.ENV = agileConfig["env"] ?? "DEV"; e.HttpTimeout = 100; // HTTP超时时间(秒) e.Tag = agileConfig["env"] ?? "DEV"; // 标签,用于区分环境 }); #endregion #region 日志 // Add services to the container. builder.Host.UseSerilog((context, services, configuration) => configuration .ReadFrom.Configuration(context.Configuration) .ReadFrom.Services(services) .Enrich.FromLogContext()); builder.Services.AddSingleton(typeof(ILogger), serviceProvider => { var loggerFactory = serviceProvider.GetRequiredService(); return loggerFactory.CreateLogger(); }); #endregion #region autofac // Add services to the container. builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory()); // 统一在 Autofac 中注册数据访问相关对象 builder.Host.ConfigureContainer(autofac => { var connStr = builder.Configuration.GetConnectionString("LiveForumConnection") ?? "Data Source=app.db;Cache=Shared"; var fsql = new FreeSqlBuilder() .UseConnectionString(DataType.SqlServer, connStr) #if DEBUG .UseAutoSyncStructure(true) .UseMonitorCommand(cmd => Console.WriteLine($"SQL: {cmd.CommandText}")) #endif .Build(); // 注册单一实例 autofac.RegisterInstance(fsql).As().SingleInstance(); //// 注册 FreeSql 仓储 - Autofac 版本 //autofac.RegisterGeneric(typeof(FreeSql.BaseRepository<>)) // .As(typeof(FreeSql.IBaseRepository<>)) // .InstancePerLifetimeScope(); // 批量注册服务层,并启用属性注入 var serviceAssembly = Assembly.Load("LiveForum.Service"); // 替换为你的服务实现所在程序集 autofac.RegisterAssemblyTypes(serviceAssembly) .Where(t => t.IsClass && !t.IsAbstract) // 只注册具体类 .Where(t => t.Name != "SensitiveWordService") // 排除敏感词服务,使用 Program.cs 中的 Singleton 注册 .Where(t => t.Name != "WechatApiClientManager") // 排除微信客户端管理器,使用 Program.cs 中的 Singleton 注册 .AsImplementedInterfaces() // 自动关联该类实现的所有接口 .PropertiesAutowired() //启动属性注入 .InstancePerLifetimeScope(); // Web 请求作用域内单例 // Redis 相关注册 var redisConnectionString = builder.Configuration["Redis:Configuration"] ?? "localhost:6379"; // 默认值,可从配置读取,比如 appsettings.json 中的 "Redis": "localhost:6379" // 注册 ConnectionMultiplexer(单例) autofac.Register(c => { // 创建并返回单例的 ConnectionMultiplexer return ConnectionMultiplexer.Connect(redisConnectionString); }) .As() .SingleInstance(); // 关键:注册为单例! // 注册 IDatabase(每个请求一个实例) autofac.Register(c => { var redis = c.Resolve(); return redis.GetDatabase(); // 默认 db0 }) .As() .InstancePerLifetimeScope(); // 注册 RedisService(统一注册一次,避免重复) autofac.RegisterType() .As() .InstancePerLifetimeScope(); // 注册事件总线 autofac.RegisterType() .As() .InstancePerLifetimeScope(); // 注册消息事件处理器 autofac.RegisterType() .As() .InstancePerLifetimeScope(); // 注册消息发布器 autofac.RegisterType() .As() .InstancePerLifetimeScope(); // 注册权限服务 autofac.RegisterType() .As() .InstancePerLifetimeScope(); }); #endregion // 先看入门文档注入 IFreeSql 注入freesql 仓储类 builder.Services.AddFreeRepository(); #region Hangfire配置 // 配置Hangfire使用SQL Server存储 var hangfireConnectionString = builder.Configuration.GetConnectionString("LiveForumConnection") ?? "Data Source=app.db;Cache=Shared"; builder.Services.AddHangfire(config => { config.UseSqlServerStorage(hangfireConnectionString, new SqlServerStorageOptions { SchemaName = "HangFire", // 表架构名称 QueuePollInterval = TimeSpan.FromSeconds(15), // 队列轮询间隔 JobExpirationCheckInterval = TimeSpan.FromHours(1), // 任务过期检查间隔 CountersAggregateInterval = TimeSpan.FromMinutes(5), // 计数器聚合间隔 PrepareSchemaIfNecessary = true, // 自动创建表结构 DashboardJobListLimit = 50000, // Dashboard显示的任务数量限制 TransactionTimeout = TimeSpan.FromMinutes(1), // 事务超时时间 CommandTimeout = TimeSpan.FromMinutes(60) // 数据库命令超时时间(支持长时间运行的任务,设置为60分钟) }); // 使用默认的序列化设置 config.UseSimpleAssemblyNameTypeSerializer(); config.UseRecommendedSerializerSettings(); }); // 添加Hangfire服务器(执行任务) builder.Services.AddHangfireServer(options => { options.WorkerCount = 1; // 工作线程数 options.ServerName = "LiveForum-Server"; // 服务器名称 options.Queues = new[] { "default" }; // 队列名称 options.ServerTimeout = TimeSpan.FromMinutes(60); // 服务器超时时间(支持长时间运行的任务,设置为60分钟) options.HeartbeatInterval = TimeSpan.FromSeconds(30); // 心跳间隔(服务器每30秒发送一次心跳,表示任务仍在运行) options.SchedulePollingInterval = TimeSpan.FromSeconds(15); // 调度轮询间隔 }); #endregion #region 注入jwt builder.AddJwtConfig((token, tokenMd5, serviceProvider) => { //在数据库中验证token有效性 var userToken = serviceProvider.GetRequiredService>(); var userAccessToken = userToken.Select.NoTracking().Where(it => it.AccessToken == token).First(it => it.AccessToken); if (userAccessToken == null || string.IsNullOrEmpty(userAccessToken)) { return null; } return userAccessToken ?? ""; }); #endregion builder.Services.AddControllers(); builder.Services.AddMemoryCache(); builder.Services.AddHttpClient(); builder.Services.AddHttpContextAccessor(); //添加httpContext注入访问 // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); #region swagger builder.Services.AddSwaggerGen(c => { var securityScheme = new OpenApiSecurityScheme { Name = "JWT 身份验证(Authentication)", Description = "请输入登录后获取JWT的**token**", In = ParameterLocation.Header, Type = SecuritySchemeType.Http, Scheme = "bearer", //必须小写 BearerFormat = "JWT", Reference = new OpenApiReference { Id = "Bearer",//JwtBearerDefaults.AuthenticationScheme Type = ReferenceType.SecurityScheme } }; c.AddSecurityDefinition(securityScheme.Reference.Id, securityScheme); c.AddSecurityRequirement(new OpenApiSecurityRequirement { {securityScheme, Array.Empty()} }); string description = ""; //.versionDescribe var filePath = Path.GetFullPath(".versionDescribe"); if (File.Exists(filePath)) { description = File.ReadAllText(filePath); //string[] lines = File.ReadAllLines(filePath); //foreach (string line in lines) //{ // if (line.Contains("##")) // { // description += $"**{line}**"; // 使用Markdown的加粗 // } // else // { // description += line + "
"; // } //} //description = $"{description}"; } c.SwaggerDoc("v1", new OpenApiInfo { Title = "论坛社区", Version = "0.1.7", Description = description }); foreach (var assemblies in AppDomain.CurrentDomain.GetAssemblies()) { // 添加 XML 注释文件路径 var xmlFile = $"{assemblies.GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); if (File.Exists(xmlPath)) { c.IncludeXmlComments(xmlPath); } } //c.ParameterFilter(); //c.RequestBodyFilter(); }); #endregion builder.Services.AddControllers() .AddNewtonsoftJson(options => { // 配置 Newtonsoft.Json 选项 options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; // 忽略循环引用 options.SerializerSettings.ContractResolver = new CustomContractResolver(); //options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();// 首字母小写(驼峰样式) options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";// 时间格式化 options.SerializerSettings.Converters.Add(new NullToEmptyStringConverter()); #if !DEBUG options.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.None; #endif //options.SerializerSettings.Converters.Add() // 其他配置... }); #region 微信小程序配置 // 绑定微信小程序配置(使用 Configure 支持热更新) builder.Services.Configure(builder.Configuration.GetSection("Wechat")); // 注册微信客户端管理器(单例,负责管理 WechatApiClient 的生命周期) builder.Services.AddSingleton(); // 注册微信小程序服务(Scoped,使用接口,方便替换实现) builder.Services.AddScoped(); var wechatConfig = builder.Configuration.GetSection("Wechat").Get() ?? new WechatConfig(); var appIdDisplay = string.IsNullOrEmpty(wechatConfig.AppId) ? "未配置" : wechatConfig.AppId.Length > 8 ? $"{wechatConfig.AppId.Substring(0, 8)}***" : wechatConfig.AppId; Console.WriteLine($"[Program] 微信小程序配置已加载 - AppId: {appIdDisplay}"); Console.WriteLine($"[Program] 提示:微信配置支持热更新,配置变更后将自动重新加载客户端"); #endregion #region 注册消息事件后台消费者 // 注册点赞消息消费者 builder.Services.AddHostedService(); // 注册评论回复消息消费者 builder.Services.AddHostedService(); // 注册自定义消息消费者 builder.Services.AddHostedService(); Console.WriteLine("[Program] 消息事件后台消费者已注册:LikeConsumer, CommentConsumer, CustomConsumer"); #endregion #region 注册点赞批量同步后台服务 // 注册点赞批量同步服务 builder.Services.AddHostedService(); Console.WriteLine("[Program] 点赞批量同步后台服务已注册:LikeBatchSyncService"); #endregion #region 注册浏览批量同步后台服务 // 注册浏览批量同步服务 builder.Services.AddHostedService(); Console.WriteLine("[Program] 浏览批量同步后台服务已注册:ViewBatchSyncService"); #endregion #region 注册送花批量同步后台服务 // 注册送花批量同步服务 builder.Services.AddHostedService(); Console.WriteLine("[Program] 送花批量同步后台服务已注册:FlowerBatchSyncService"); #endregion #region 腾讯云COS配置 // 绑定腾讯云COS配置(使用 Bind 方法支持自动映射和热更新) builder.Services.Configure(builder.Configuration.GetSection("TENCENT_COS")); // 可选:后处理配置,设置默认值 builder.Services.PostConfigure(options => { // 设置默认值(如果配置中没有提供) if (options.MaxSize <= 0) { options.MaxSize = 100; // 默认100MB } if (options.DurationSecond <= 0) { options.DurationSecond = 600; // 默认600秒 } if (string.IsNullOrEmpty(options.Prefixes)) { options.Prefixes = "file"; // 默认前缀 } }); var cosConfig = builder.Configuration.GetSection("TENCENT_COS").Get() ?? new TencentCosConfig(); Console.WriteLine($"[Program] 腾讯云COS配置已加载 - Region: {cosConfig.Region}, Bucket: {cosConfig.BucketName}"); Console.WriteLine($"[Program] 提示:COS 配置支持热更新,服务将通过 IOptionsSnapshot 获取最新配置"); #endregion #region 敏感词检测服务 // 绑定敏感词配置(使用 Configure 支持热更新) builder.Services.Configure(builder.Configuration.GetSection("SensitiveWord")); // 注册敏感词配置为单例(注意:这里使用 IOptions 而不是 IOptionsSnapshot,因为服务本身是单例) // 敏感词服务需要稳定的配置引用,配置变更通过 ReloadAllAsync 方法处理 builder.Services.AddSingleton(sp => { var options = sp.GetRequiredService>(); return options.Value; }); // 注册敏感词服务(单例,只初始化一次) builder.Services.AddSingleton(sp => { var repository = sp.GetRequiredService>(); var redisService = sp.GetRequiredService(); var config = sp.GetRequiredService(); var appRootPath = builder.Environment.ContentRootPath; // 应用根目录 Console.WriteLine("[Program] ========================================"); Console.WriteLine("[Program] 正在初始化敏感词服务(单例,只执行一次)..."); Console.WriteLine($"[Program] 应用根目录: {appRootPath}"); var service = new LiveForum.Service.SensitiveWord.SensitiveWordService(repository, redisService, config, appRootPath); Console.WriteLine("[Program] 敏感词服务初始化完成!"); Console.WriteLine("[Program] ========================================"); return service; }); #endregion #region 应用配置 // 绑定应用配置(使用 Configure 支持热更新) builder.Services.Configure(builder.Configuration.GetSection("AppSettings")); // 注册 IOptionsSnapshot 用于 Scoped 服务(每次请求获取最新配置) // 这样在 LoginService 等服务中注入时,每次请求都会读取最新配置 // 注意:由于 LoginService 等服务的生命周期,我们需要使用 IOptionsSnapshot var appSettings = builder.Configuration.GetSection("AppSettings").Get() ?? new LiveForum.Model.Dto.Others.AppSettings(); Console.WriteLine($"[Program] 应用配置已加载 - 默认头像: {(string.IsNullOrEmpty(appSettings.UserDefaultIcon) ? "未配置" : "已配置")}, 默认昵称前缀: {appSettings.UserDefaultName}"); Console.WriteLine($"[Program] 提示:AppSettings 配置支持热更新,服务将通过 IOptionsSnapshot 获取最新配置"); #endregion #region AppConfig 缓存管理器 // 注册 AppConfig 缓存管理器(Scoped,每个请求一个实例,实例字段缓存) builder.Services.AddScoped(sp => { var configuration = sp.GetRequiredService(); return new LiveForum.Code.SystemCache.AppConfigManage.AppConfigCacheManager(configuration); }); #endregion var app = builder.Build(); // 预热敏感词服务(触发词库加载) using (var scope = app.Services.CreateScope()) { var sensitiveWordService = scope.ServiceProvider.GetRequiredService(); Console.WriteLine("[Program] 敏感词服务已预热"); } #region AgileConfig 配置变更监听 // 监听 AgileConfig 配置变更事件 var configClient = app.Services.GetService(); if (configClient != null) { Console.WriteLine("[Program] 正在注册 AgileConfig 配置变更监听器..."); // event Action ConfigChanged; 只有一个参数 configClient.ConfigChanged += (args) => { try { var action = args?.Action ?? "Unknown"; var key = args?.Key ?? "(全局重新加载)"; Console.WriteLine($"[AgileConfig] 检测到配置变更: {action} - Key: {key}"); // 如果 Key 为空或是 reload 操作,说明是全局配置重新加载 // 需要检查所有配置是否变更 var isGlobalReload = string.IsNullOrEmpty(args?.Key) || args.Action == "reload"; // 检查敏感词配置是否变更 if (isGlobalReload || args.Key?.StartsWith("SensitiveWord") == true) { Console.WriteLine("[AgileConfig] 检查敏感词配置是否变更..."); Task.Run(async () => { try { using var scope = app.Services.CreateScope(); var sensitiveWordService = scope.ServiceProvider.GetRequiredService(); // 获取最新的配置 var newConfig = app.Configuration.GetSection("SensitiveWord").Get(); // 调用重新加载(内部会对比配置,只有真正变化才重新加载) await sensitiveWordService.ReloadAllAsync(newConfig); } catch (Exception ex) { Console.WriteLine($"[AgileConfig] 敏感词服务重新加载失败: {ex.Message}"); } }).Wait(); } // 检查应用配置是否变更 if (isGlobalReload || args.Key?.StartsWith("AppSettings") == true) { Console.WriteLine("[AgileConfig] 应用配置已变更"); var newAppSettings = app.Configuration.GetSection("AppSettings").Get(); if (newAppSettings != null) { Console.WriteLine($"[AgileConfig] 新的默认头像: {(string.IsNullOrEmpty(newAppSettings.UserDefaultIcon) ? "未配置" : "已配置")}, 默认昵称前缀: {newAppSettings.UserDefaultName}"); Console.WriteLine($"[AgileConfig] 提示:新注册用户将使用新配置"); } } // 检查 AppConfig 配置是否变更 if (isGlobalReload || args.Key?.StartsWith("AppConfig") == true) { Console.WriteLine("[AgileConfig] AppConfig 配置已变更"); Console.WriteLine("[AgileConfig] 提示:由于 AppConfigCacheManager 为 Scoped 服务,下次请求将自动使用新配置"); } // 检查腾讯云COS配置是否变更 if (isGlobalReload || args.Key?.StartsWith("TENCENT_COS") == true) { Console.WriteLine("[AgileConfig] 腾讯云COS配置已变更"); var newCosConfig = app.Configuration.GetSection("TENCENT_COS").Get(); if (newCosConfig != null) { Console.WriteLine($"[AgileConfig] 新的存储桶: {newCosConfig.BucketName}, 最大上传: {newCosConfig.MaxSize}MB"); Console.WriteLine($"[AgileConfig] 提示:下次文件上传将使用新配置(通过 IOptionsSnapshot 自动获取)"); } } // 检查微信小程序配置是否变更 if (isGlobalReload || args.Key?.StartsWith("Wechat") == true) { Console.WriteLine("[AgileConfig] 微信小程序配置已变更,正在重新加载客户端..."); Task.Run(() => { try { using var scope = app.Services.CreateScope(); var manager = scope.ServiceProvider.GetRequiredService(); var success = manager.Reload(); if (success) { var newAppId = manager.GetCurrentAppId(); var appIdDisplay = string.IsNullOrEmpty(newAppId) ? "未配置" : newAppId.Length > 8 ? $"{newAppId.Substring(0, 8)}***" : newAppId; Console.WriteLine($"[AgileConfig] 微信 API 客户端重新加载成功 - 新 AppId: {appIdDisplay}"); } else { Console.WriteLine($"[AgileConfig] 微信 API 客户端重新加载失败,请检查配置是否正确"); } } catch (Exception ex) { Console.WriteLine($"[AgileConfig] 微信 API 客户端重新加载失败: {ex.Message}"); } }).Wait(); } // 检查 JWT 配置是否变更 if (isGlobalReload || args.Key?.StartsWith("JwtTokenConfig") == true) { Console.WriteLine("[AgileConfig] JWT 配置已变更"); var newJwtConfig = app.Configuration.GetSection("JwtTokenConfig").Get(); if (newJwtConfig != null) { var secretDisplay = string.IsNullOrEmpty(newJwtConfig.Secret) ? "未配置" : newJwtConfig.Secret.Length > 8 ? $"{newJwtConfig.Secret.Substring(0, 8)}***" : newJwtConfig.Secret; Console.WriteLine($"[AgileConfig] 新的 JWT 配置 - Issuer: {newJwtConfig.Issuer}, Audience: {newJwtConfig.Audience}, Secret: {secretDisplay}, Expiration: {newJwtConfig.AccessTokenExpiration}分钟"); Console.WriteLine($"[AgileConfig] 提示:JWT 配置支持热更新,新生成的 token 将使用新配置(通过 IOptionsMonitor 自动获取)"); Console.WriteLine($"[AgileConfig] 注意:已签发的 token 仍使用旧配置验证,如需完全切换,建议重启应用"); } } } catch (Exception ex) { Console.WriteLine($"[AgileConfig] 处理配置变更失败: {ex.Message}"); } }; Console.WriteLine("[AgileConfig] 配置变更监听器注册完成!"); } else { Console.WriteLine("[AgileConfig] 未检测到 AgileConfig 客户端,配置变更监听功能未启用"); } #endregion //引入中间件 app.UseMiddlewareAll(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment() || true) { app.UseSwagger(); app.UseSwaggerUI(c => { c.EnableDeepLinking(); c.DefaultModelsExpandDepth(3); c.DefaultModelExpandDepth(3); c.EnableFilter("true"); //c.RoutePrefix = string.Empty; c.SwaggerEndpoint("/swagger/v1/swagger.json", "API V1"); // 使用自定义CSS //c.InjectStylesheet("/custom.css"); }); } // 配置静态文件目录,允许访问uploads目录 app.UseStaticFiles(new StaticFileOptions { FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider( Path.Combine(app.Environment.ContentRootPath, "wwwroot")), RequestPath = "", //OnPrepareResponse = ctx => //{ // // 设置静态文件缓存 // ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=31536000"); //} }); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); // 响应缓存中间件(需要在路由解析后执行,可以获取Endpoint信息) app.UseResponseCacheMiddleware(); app.MapControllers(); #region Hangfire Dashboard // 配置Hangfire Dashboard(任务管理UI) app.UseHangfireDashboard("/hangfire", new DashboardOptions { DashboardTitle = "定时任务管理" // 不添加认证(按需求) }); #endregion // app.UseAppRequest("论坛社区"); #region 注册定时任务 // 注册定时任务:每月1号凌晨3点执行主播送花数量重置任务 // Hangfire会自动从DI容器(Autofac)中解析IScheduledJobService服务 //RecurringJob.AddOrUpdate( // "streamer-flower-reset", // 任务ID(唯一标识) // job => job.ResetStreamerFlowerCountAsync(), // "0 3 1 * *", // Cron表达式:每月1号凌晨3点 // new RecurringJobOptions // { // TimeZone = TimeZoneInfo.Local // 使用本地时区 // }); //Console.WriteLine("[Program] Hangfire定时任务已注册:streamer-flower-reset(每月1号凌晨3点执行)"); #endregion app.Run();