From 81303b6164460368336bb57a7aaf78d3091fce22 Mon Sep 17 00:00:00 2001 From: ck_yeun9 Date: Sat, 14 Feb 2026 20:34:33 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E6=96=B0=E5=A2=9ERedis/=E9=82=AE=E4=BB=B6/?= =?UTF-8?q?=E5=9B=BE=E5=BA=8A=E5=81=A5=E5=BA=B7=E6=A3=80=E6=9F=A5=E5=8F=8A?= =?UTF-8?q?=E5=AE=9A=E6=97=B6=E4=BD=9C=E4=B8=9A=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次提交集成了Redis服务,增加了健康检查与基本操作工具类,并实现了Redis、邮件、图床服务的健康检查Quartz作业。扩展了Quartz作业配置,支持多服务定时检测。优化了依赖注入与配置管理,增强了日志输出,提升系统稳定性与可维护性。 --- .../Extensions/AutofacConfigExtensions.cs | 1 + .../Extensions/ServiceExtensions.cs | 62 ++++++-- .../appsettings.Application.json | 4 +- .../appsettings.Database.json | 7 +- .../EOM.TSHotelManagement.Common.csproj | 1 + .../Helper/JWTHelper.cs | 4 +- .../Helper/LskyHelper.cs | 61 +++++++- .../Helper/MailHelper.cs | 40 +++++- .../Helper/RedisHelper.cs | 132 ++++++++++++++++++ .../Helper/UrlHelper.cs | 78 +++++++++++ .../ImageHostingServiceCheckJob.cs | 49 +++++++ .../Job/ReservationExpirationCheckJob.cs | 24 +++- .../Mail/MailServiceCheckJob.cs | 51 +++++++ .../Redis/RedisServiceCheckJob.cs | 49 +++++++ .../Config/RedisConfig.cs | 8 ++ .../Factory/IJwtConfigFactory.cs | 8 -- .../Factory/ILskyConfigFactory.cs | 8 -- .../Factory/IMailConfigFactory.cs | 8 -- .../Factory/JwtConfigFactory.cs | 2 +- .../Factory/LskyConfigFactory.cs | 2 +- .../Factory/MailConfigFactory.cs | 2 +- .../Factory/RedisConfigFactory.cs | 28 ++++ 22 files changed, 575 insertions(+), 54 deletions(-) create mode 100644 EOM.TSHotelManagement.Common/Helper/RedisHelper.cs create mode 100644 EOM.TSHotelManagement.Common/Helper/UrlHelper.cs create mode 100644 EOM.TSHotelManagement.Common/QuartzWorkspace/ImageHosting/ImageHostingServiceCheckJob.cs create mode 100644 EOM.TSHotelManagement.Common/QuartzWorkspace/Mail/MailServiceCheckJob.cs create mode 100644 EOM.TSHotelManagement.Common/QuartzWorkspace/Redis/RedisServiceCheckJob.cs create mode 100644 EOM.TSHotelManagement.Infrastructure/Config/RedisConfig.cs delete mode 100644 EOM.TSHotelManagement.Infrastructure/Factory/IJwtConfigFactory.cs delete mode 100644 EOM.TSHotelManagement.Infrastructure/Factory/ILskyConfigFactory.cs delete mode 100644 EOM.TSHotelManagement.Infrastructure/Factory/IMailConfigFactory.cs create mode 100644 EOM.TSHotelManagement.Infrastructure/Factory/RedisConfigFactory.cs diff --git a/EOM.TSHotelManagement.API/Extensions/AutofacConfigExtensions.cs b/EOM.TSHotelManagement.API/Extensions/AutofacConfigExtensions.cs index b746dbb..8ab4bc1 100644 --- a/EOM.TSHotelManagement.API/Extensions/AutofacConfigExtensions.cs +++ b/EOM.TSHotelManagement.API/Extensions/AutofacConfigExtensions.cs @@ -28,6 +28,7 @@ namespace EOM.TSHotelManagement.WebApi builder.RegisterType() .InstancePerDependency(); + builder.RegisterType().AsSelf().InstancePerLifetimeScope(); builder.RegisterType().AsSelf().InstancePerLifetimeScope(); builder.RegisterType().AsSelf().InstancePerLifetimeScope(); builder.RegisterType().AsSelf().InstancePerLifetimeScope(); diff --git a/EOM.TSHotelManagement.API/Extensions/ServiceExtensions.cs b/EOM.TSHotelManagement.API/Extensions/ServiceExtensions.cs index 6e2b649..1cbff9f 100644 --- a/EOM.TSHotelManagement.API/Extensions/ServiceExtensions.cs +++ b/EOM.TSHotelManagement.API/Extensions/ServiceExtensions.cs @@ -43,27 +43,64 @@ namespace EOM.TSHotelManagement.WebApi public static void ConfigureQuartz(this IServiceCollection services, IConfiguration configuration) { - var defaultDatabaseName = configuration.GetValue("DefaultDatabase") ?? SystemConstant.MariaDB.Code; services.AddQuartz(q => { var jobs = configuration.GetSection(SystemConstant.JobKeys.Code).Get() ?? Array.Empty(); foreach (var job in jobs) { - q.AddJob(opts => opts.WithIdentity(job) - .StoreDurably() - .WithDescription($"{job} 定时作业")); + var reservationJobKey = $"{job}-Reservation"; + var mailJobKey = $"{job}-Mail"; + var imageHostingJobKey = $"{job}-ImageHosting"; + var redisJobKey = $"{job}-Redis"; + + q.AddJob(opts => opts + .WithIdentity(reservationJobKey) + .StoreDurably() + .WithDescription($"{reservationJobKey} 定时作业")); - // 创建触发器(每天凌晨1点执行) q.AddTrigger(opts => opts - .ForJob(job) - .WithIdentity($"{job}-Trigger") - //.WithCronSchedule("1-2 * * * * ? ")); // Debug Use Only + .ForJob(reservationJobKey) + .WithIdentity($"{reservationJobKey}-Trigger") + //.WithCronSchedule("* * * * * ? ")); // Debug Use Only 每秒执行一次 .WithCronSchedule("0 0 1 * * ?")); // 每天1:00 AM执行 + + q.AddJob(opts => opts + .WithIdentity(mailJobKey) + .StoreDurably() + .WithDescription($"{mailJobKey} 定时作业")); + + q.AddTrigger(opts => opts + .ForJob(mailJobKey) + .WithIdentity($"{mailJobKey}-Trigger") + //.WithCronSchedule("* * * * * ? ")); // Debug Use Only 每秒执行一次 + .WithCronSchedule("0 */5 * * * ?")); // 每5分钟执行一次 + + q.AddJob(opts => opts + .WithIdentity(imageHostingJobKey) + .StoreDurably() + .WithDescription($"{imageHostingJobKey} 定时作业")); + + q.AddTrigger(opts => opts + .ForJob(imageHostingJobKey) + .WithIdentity($"{imageHostingJobKey}-Trigger") + //.WithCronSchedule("* * * * * ? ")); // Debug Use Only 每秒执行一次 + .WithCronSchedule("0 */5 * * * ?")); // 每5分钟执行一次 + + q.AddJob(opts => opts + .WithIdentity(redisJobKey) + .StoreDurably() + .WithDescription($"{redisJobKey} 定时作业")); + + q.AddTrigger(opts => opts + .ForJob(redisJobKey) + .WithIdentity($"{redisJobKey}-Trigger") + //.WithCronSchedule("* * * * * ? ")); // Debug Use Only 每秒执行一次 + .WithCronSchedule("0 */5 * * * ?")); // 每5分钟执行一次 } }); - services.AddQuartzHostedService(q => + services.AddQuartzHostedService(q => { q.WaitForJobsToComplete = true; q.AwaitApplicationStarted = true; @@ -86,9 +123,10 @@ namespace EOM.TSHotelManagement.WebApi public static void RegisterSingletonServices(this IServiceCollection services, IConfiguration configuration) { services.AddScoped(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.Configure(configuration.GetSection("CsrfToken")); services.AddSingleton(); diff --git a/EOM.TSHotelManagement.API/appsettings.Application.json b/EOM.TSHotelManagement.API/appsettings.Application.json index 1f0c6fa..8fd1da2 100644 --- a/EOM.TSHotelManagement.API/appsettings.Application.json +++ b/EOM.TSHotelManagement.API/appsettings.Application.json @@ -12,7 +12,9 @@ ], "AllowedHosts": "*", "JobKeys": [ - "ReservationExpirationCheckJob" + "ReservationExpirationCheckJob", + "MailServiceCheckJob", + "RedisServiceCheckJob" ], "ExpirationSettings": { "NotifyDaysBefore": 3, diff --git a/EOM.TSHotelManagement.API/appsettings.Database.json b/EOM.TSHotelManagement.API/appsettings.Database.json index 265b373..0c020cb 100644 --- a/EOM.TSHotelManagement.API/appsettings.Database.json +++ b/EOM.TSHotelManagement.API/appsettings.Database.json @@ -7,5 +7,10 @@ "SqlServerConnectStr": "Server=my_sqlserver_host;Database=tshoteldb;User Id=my_sqlserver_user;Password=my_sqlserver_password;", "OracleConnectStr": "User Id=my_oracle_user;Password=my_oracle_password;Data Source=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=my_oracle_host)(PORT=1521)))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=my_oracle_service_name)));" }, - "InitializeDatabase": true + "Redis": { + "Enabled": false, + "ConnectionString": "host:port,password=your_redis_password", //host:port,password=your_redis_password + "DefaultDatabase": 0 + }, + "InitializeDatabase": false } \ No newline at end of file diff --git a/EOM.TSHotelManagement.Common/EOM.TSHotelManagement.Common.csproj b/EOM.TSHotelManagement.Common/EOM.TSHotelManagement.Common.csproj index af3b4bc..6dd72b3 100644 --- a/EOM.TSHotelManagement.Common/EOM.TSHotelManagement.Common.csproj +++ b/EOM.TSHotelManagement.Common/EOM.TSHotelManagement.Common.csproj @@ -29,6 +29,7 @@ + diff --git a/EOM.TSHotelManagement.Common/Helper/JWTHelper.cs b/EOM.TSHotelManagement.Common/Helper/JWTHelper.cs index 6c95ca2..5714c4d 100644 --- a/EOM.TSHotelManagement.Common/Helper/JWTHelper.cs +++ b/EOM.TSHotelManagement.Common/Helper/JWTHelper.cs @@ -10,9 +10,9 @@ namespace EOM.TSHotelManagement.Common { public class JWTHelper { - private readonly IJwtConfigFactory _jwtConfigFactory; + private readonly JwtConfigFactory _jwtConfigFactory; - public JWTHelper(IJwtConfigFactory jwtConfigFactory) + public JWTHelper(JwtConfigFactory jwtConfigFactory) { _jwtConfigFactory = jwtConfigFactory; } diff --git a/EOM.TSHotelManagement.Common/Helper/LskyHelper.cs b/EOM.TSHotelManagement.Common/Helper/LskyHelper.cs index 6d50d9a..f5663d5 100644 --- a/EOM.TSHotelManagement.Common/Helper/LskyHelper.cs +++ b/EOM.TSHotelManagement.Common/Helper/LskyHelper.cs @@ -11,15 +11,72 @@ namespace EOM.TSHotelManagement.Common { public class LskyHelper { - private readonly ILskyConfigFactory lskyConfigFactory; + private readonly LskyConfigFactory lskyConfigFactory; private readonly ILogger logger; - public LskyHelper(ILskyConfigFactory lskyConfigFactory, ILogger logger) + public LskyHelper(LskyConfigFactory lskyConfigFactory, ILogger logger) { this.lskyConfigFactory = lskyConfigFactory; this.logger = logger; } + public async Task CheckServiceStatusAsync() + { + var lskyConfig = lskyConfigFactory.GetLskyConfig(); + var address = lskyConfig.BaseAddress; + var enabled = lskyConfig.Enabled; + + try + { + if (!enabled) + { + logger.LogWarning("Lsky图床服务未启用,跳过状态检查。"); + return false; + } + + var domain = UrlHelper.ExtractDomainFromBaseAddress(lskyConfig.BaseAddress); + + if (string.IsNullOrWhiteSpace(domain)) + { + logger.LogError("无法从BaseAddress中提取有效域名: {BaseAddress}", lskyConfig.BaseAddress); + return false; + } + + logger.LogInformation("使用域名进行健康检查: {Domain}", domain); + + using var httpClient = new HttpClient(); + httpClient.Timeout = TimeSpan.FromSeconds(10); + + var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, domain)); + + if (response.IsSuccessStatusCode) + { + logger.LogInformation("Lsky图床服务状态检查成功,状态码: {StatusCode}", response.StatusCode); + return true; + } + else + { + logger.LogWarning("Lsky图床服务返回异常状态码: {StatusCode}", response.StatusCode); + return false; + } + } + catch (HttpRequestException httpEx) + { + logger.LogError(httpEx, "HTTP请求异常,Lsky图床服务可能不可用"); + return false; + } + catch (TaskCanceledException timeoutEx) when (timeoutEx.CancellationToken == CancellationToken.None) + { + logger.LogError(timeoutEx, "Lsky图床服务连接超时"); + return false; + } + catch (Exception ex) + { + logger.LogError(ex, "Lsky图床服务状态检查发生未知异常"); + return false; + } + } + public async Task GetEnabledState() { logger.LogError("Checking if Lsky image storage is enabled."); diff --git a/EOM.TSHotelManagement.Common/Helper/MailHelper.cs b/EOM.TSHotelManagement.Common/Helper/MailHelper.cs index cd76c38..6bece4d 100644 --- a/EOM.TSHotelManagement.Common/Helper/MailHelper.cs +++ b/EOM.TSHotelManagement.Common/Helper/MailHelper.cs @@ -1,22 +1,25 @@ using EOM.TSHotelManagement.Infrastructure; using MailKit.Net.Smtp; using MailKit.Security; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using MimeKit; +using Quartz.Logging; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Mime; +using System.Net.Sockets; namespace EOM.TSHotelManagement.Common { public class MailHelper { - private readonly IMailConfigFactory mailConfigFactory; + private readonly MailConfigFactory mailConfigFactory; private readonly ILogger logger; - public MailHelper(IMailConfigFactory mailConfigFactory, ILogger logger) + public MailHelper(MailConfigFactory mailConfigFactory, ILogger logger) { this.mailConfigFactory = mailConfigFactory; this.logger = logger; @@ -105,6 +108,39 @@ namespace EOM.TSHotelManagement.Common } } + + public async Task CheckServiceStatusAsync() + { + var mailConfig = mailConfigFactory.GetMailConfig(); + var host = mailConfig.Host; + var port = mailConfig.Port; + var enabled = mailConfig.Enabled; + + try + { + if (!enabled) + { + logger.LogWarning("邮件服务未启用, 跳过检查"); + return false; + } + + if (port == 0) + { + logger.LogError("邮件服务配置信息缺失"); + return false; + } + + using var client = new TcpClient(); + await client.ConnectAsync(host, port); + return client.Connected; + } + catch (Exception ex) + { + logger.LogError(ex, "邮件服务连接异常"); + return false; + } + } + #region Private Methods private bool IsMailConfigValid() diff --git a/EOM.TSHotelManagement.Common/Helper/RedisHelper.cs b/EOM.TSHotelManagement.Common/Helper/RedisHelper.cs new file mode 100644 index 0000000..ea47f3a --- /dev/null +++ b/EOM.TSHotelManagement.Common/Helper/RedisHelper.cs @@ -0,0 +1,132 @@ +using EOM.TSHotelManagement.Infrastructure; +using Microsoft.Extensions.Logging; +using StackExchange.Redis; +using System; + +namespace EOM.TSHotelManagement.Common +{ + public class RedisHelper + { + private IConnectionMultiplexer _connection; + private readonly ILogger logger; + private readonly RedisConfigFactory configFactory; + + public RedisHelper(RedisConfigFactory configFactory, ILogger logger) + { + this.configFactory = configFactory; + this.logger = logger; + Initialize(); + } + + public void Initialize() + { + try + { + var redisConfig = configFactory.GetRedisConfig(); + + if (!redisConfig.Enable) + { + logger.LogInformation("Redis功能未启用,跳过初始化"); + return; + } + + if (string.IsNullOrWhiteSpace(redisConfig?.ConnectionString)) + throw new ArgumentException("Redis连接字符串不能为空"); + + var options = ConfigurationOptions.Parse(redisConfig.ConnectionString); + options.AbortOnConnectFail = false; + options.ConnectTimeout = 5000; + options.ReconnectRetryPolicy = new ExponentialRetry(3000); + + _connection = ConnectionMultiplexer.Connect(options); + _connection.GetDatabase().Ping(); + } + catch (Exception ex) + { + logger.LogCritical(ex, "Redis初始化失败"); + throw; + } + } + + public IDatabase GetDatabase() + { + if (_connection == null) + throw new System.Exception("RedisHelper not initialized. Call Initialize first."); + + return _connection.GetDatabase(); + } + + public async Task CheckServiceStatusAsync() + { + try + { + var db = GetDatabase(); + var ping = await db.PingAsync(); + logger.LogInformation($"Redis响应时间:{ping.TotalMilliseconds} ms"); + return true; + } + catch (Exception ex) + { + logger.LogError(ex, "Redis服务检查失败"); + return false; + } + } + + public async Task SetAsync(string key, string value, TimeSpan? expiry = null) + { + try + { + var db = GetDatabase(); + return await db.StringSetAsync(key, value, (Expiration)expiry); + } + catch (Exception ex) + { + logger.LogError(ex, $"Redis设置值失败,键:{key}!"); + return false; + } + } + + public async Task GetAsync(string key) + { + try + { + var db = GetDatabase(); + return await db.StringGetAsync(key); + } + catch (Exception ex) + { + logger.LogError(ex, $"Redis获取值失败,键:{key}!"); + return null; + } + } + + public async Task DeleteAsync(string key) + { + try + { + var db = GetDatabase(); + return await db.KeyDeleteAsync(key); + } + catch (Exception ex) + { + logger.LogError(ex, $"Redis删除键失败,键:{key}!"); + return false; + } + } + + public async Task KeyExistsAsync(string key) + { + try + { + var db = GetDatabase(); + return await db.KeyExistsAsync(key); + } + catch (Exception ex) + { + logger.LogError(ex, $"Redis键存在检查失败,键:{key}!"); + return false; + } + } + + } +} diff --git a/EOM.TSHotelManagement.Common/Helper/UrlHelper.cs b/EOM.TSHotelManagement.Common/Helper/UrlHelper.cs new file mode 100644 index 0000000..06b244f --- /dev/null +++ b/EOM.TSHotelManagement.Common/Helper/UrlHelper.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace EOM.TSHotelManagement.Common +{ + public static class UrlHelper + { + /// + /// 从 BaseAddress 中提取域名(不包含 API 路径) + /// + /// 完整的 BaseAddress + /// 提取的域名 + public static string ExtractDomainFromBaseAddress(string baseAddress) + { + if (string.IsNullOrWhiteSpace(baseAddress)) + { + return string.Empty; + } + + try + { + // 确保地址是有效的 URI + if (!Uri.TryCreate(baseAddress, UriKind.Absolute, out Uri uri)) + { + // 如果不是完整 URI,尝试添加协议 + if (!baseAddress.StartsWith("http://") && !baseAddress.StartsWith("https://")) + { + var httpsAddress = "https://" + baseAddress; + if (Uri.TryCreate(httpsAddress, UriKind.Absolute, out uri)) + { + return $"{uri.Scheme}://{uri.Host}"; + } + } + return baseAddress; // 返回原始地址作为后备方案 + } + + // 提取协议和主机名 + return $"{uri.Scheme}://{uri.Host}"; + } + catch (Exception) + { + return baseAddress; // 如果解析失败,返回原始地址 + } + } + + /// + /// 获取干净的域名(不包含协议) + /// + public static string ExtractCleanDomain(string baseAddress) + { + if (string.IsNullOrWhiteSpace(baseAddress)) + { + return string.Empty; + } + + try + { + if (Uri.TryCreate(baseAddress, UriKind.Absolute, out Uri uri)) + { + return uri.Host; + } + + // 尝试处理不完整的地址 + var cleanAddress = baseAddress + .Replace("https://", "") + .Replace("http://", "") + .Split('/')[0]; // 取第一个部分(域名) + + return cleanAddress; + } + catch (Exception) + { + return baseAddress; + } + } + } +} diff --git a/EOM.TSHotelManagement.Common/QuartzWorkspace/ImageHosting/ImageHostingServiceCheckJob.cs b/EOM.TSHotelManagement.Common/QuartzWorkspace/ImageHosting/ImageHostingServiceCheckJob.cs new file mode 100644 index 0000000..7ce390c --- /dev/null +++ b/EOM.TSHotelManagement.Common/QuartzWorkspace/ImageHosting/ImageHostingServiceCheckJob.cs @@ -0,0 +1,49 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Quartz; +using System; +using System.Collections.Generic; +using System.Text; + +namespace EOM.TSHotelManagement.Common +{ + [DisallowConcurrentExecution] + public class ImageHostingServiceCheckJob : IJob + { + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + private readonly IConfiguration _configuration; + + public ImageHostingServiceCheckJob( + ILogger logger, + IServiceProvider serviceProvider, + IConfiguration configuration) + { + _logger = logger; + _serviceProvider = serviceProvider; + _configuration = configuration; + } + + public async Task Execute(IJobExecutionContext context) + { + _logger.LogInformation("开始检测图床服务状态..."); + + using var scope = _serviceProvider.CreateScope(); + var imageHostingService = scope.ServiceProvider.GetRequiredService(); + + bool isHealthy = false; + try + { + isHealthy = await imageHostingService.CheckServiceStatusAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "图床服务检测异常"); + } + + _logger.LogInformation($"图床服务检测完成,状态:{(isHealthy ? "健康" : "故障")}"); + } + + } +} diff --git a/EOM.TSHotelManagement.Common/QuartzWorkspace/Job/ReservationExpirationCheckJob.cs b/EOM.TSHotelManagement.Common/QuartzWorkspace/Job/ReservationExpirationCheckJob.cs index 4b7076d..6431906 100644 --- a/EOM.TSHotelManagement.Common/QuartzWorkspace/Job/ReservationExpirationCheckJob.cs +++ b/EOM.TSHotelManagement.Common/QuartzWorkspace/Job/ReservationExpirationCheckJob.cs @@ -38,6 +38,13 @@ namespace EOM.TSHotelManagement.Common var db = scope.ServiceProvider.GetRequiredService(); var emailService = scope.ServiceProvider.GetRequiredService(); + var isHealthForMailService = await emailService.CheckServiceStatusAsync(); + + if (!isHealthForMailService) + { + _logger.LogWarning("邮件服务不可用,跳过发送过期提醒邮件的步骤。"); + } + // 获取配置的提前提醒天数 var notifyDays = _configuration.GetValue("ExpirationSettings:NotifyDaysBefore", 3); var currentDate = DateOnly.FromDateTime(DateTime.Now.Date); @@ -73,7 +80,7 @@ namespace EOM.TSHotelManagement.Common var helper = new EnumHelper(); var channelDescription = helper.GetDescriptionByName(item.ReservationChannel); - + var mailTemplate = EmailTemplate.SendReservationExpirationNotificationTemplate( item.ReservationRoomNumber, channelDescription ?? "我店", @@ -83,13 +90,16 @@ namespace EOM.TSHotelManagement.Common daysLeft ); - emailService.SendMail( - null, // To-do: Add recipient email address - mailTemplate.Subject, - mailTemplate.Body - ); + if (isHealthForMailService) + { + emailService.SendMail( + null, // To-do: Add recipient email address + mailTemplate.Subject, + mailTemplate.Body + ); + } - if (shouldDeleteReservations.Any() && shouldReleaseRooms.Any()) + if (shouldDeleteReservations.Any() && shouldReleaseRooms.Any()) { db.Updateable() .SetColumns(r => new Room { RoomStateId = (int)RoomState.Vacant }) diff --git a/EOM.TSHotelManagement.Common/QuartzWorkspace/Mail/MailServiceCheckJob.cs b/EOM.TSHotelManagement.Common/QuartzWorkspace/Mail/MailServiceCheckJob.cs new file mode 100644 index 0000000..91e1090 --- /dev/null +++ b/EOM.TSHotelManagement.Common/QuartzWorkspace/Mail/MailServiceCheckJob.cs @@ -0,0 +1,51 @@ +using EOM.TSHotelManagement.Domain; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Quartz; +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Text; + +namespace EOM.TSHotelManagement.Common +{ + [DisallowConcurrentExecution] + public class MailServiceCheckJob : IJob + { + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + private readonly IConfiguration _configuration; + + public MailServiceCheckJob( + ILogger logger, + IServiceProvider serviceProvider, + IConfiguration configuration) + { + _logger = logger; + _serviceProvider = serviceProvider; + _configuration = configuration; + } + + public async Task Execute(IJobExecutionContext context) + { + _logger.LogInformation("开始检测邮件服务状态..."); + + using var scope = _serviceProvider.CreateScope(); + var emailService = scope.ServiceProvider.GetRequiredService(); + + bool isHealthy = false; + try + { + isHealthy = await emailService.CheckServiceStatusAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "邮件服务检测异常"); + } + + _logger.LogInformation($"邮件服务检测完成,状态:{(isHealthy ? "健康" : "故障")}"); + } + + } +} diff --git a/EOM.TSHotelManagement.Common/QuartzWorkspace/Redis/RedisServiceCheckJob.cs b/EOM.TSHotelManagement.Common/QuartzWorkspace/Redis/RedisServiceCheckJob.cs new file mode 100644 index 0000000..1257fca --- /dev/null +++ b/EOM.TSHotelManagement.Common/QuartzWorkspace/Redis/RedisServiceCheckJob.cs @@ -0,0 +1,49 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Quartz; +using System; +using System.Collections.Generic; +using System.Text; + +namespace EOM.TSHotelManagement.Common +{ + [DisallowConcurrentExecution] + public class RedisServiceCheckJob : IJob + { + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + private readonly IConfiguration _configuration; + + public RedisServiceCheckJob( + ILogger logger, + IServiceProvider serviceProvider, + IConfiguration configuration) + { + _logger = logger; + _serviceProvider = serviceProvider; + _configuration = configuration; + } + + public async Task Execute(IJobExecutionContext context) + { + _logger.LogInformation("开始检测Redis服务状态..."); + + using var scope = _serviceProvider.CreateScope(); + var redisHelper = scope.ServiceProvider.GetRequiredService(); + + bool isHealthy = false; + try + { + isHealthy = await redisHelper.CheckServiceStatusAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Redis服务检测异常"); + } + + _logger.LogInformation($"Redis服务检测完成,状态:{(isHealthy ? "健康" : "故障")}"); + } + + } +} diff --git a/EOM.TSHotelManagement.Infrastructure/Config/RedisConfig.cs b/EOM.TSHotelManagement.Infrastructure/Config/RedisConfig.cs new file mode 100644 index 0000000..c4e27a1 --- /dev/null +++ b/EOM.TSHotelManagement.Infrastructure/Config/RedisConfig.cs @@ -0,0 +1,8 @@ +namespace EOM.TSHotelManagement.Infrastructure +{ + public class RedisConfig + { + public string ConnectionString { get; set; } + public bool Enable { get; set; } + } +} diff --git a/EOM.TSHotelManagement.Infrastructure/Factory/IJwtConfigFactory.cs b/EOM.TSHotelManagement.Infrastructure/Factory/IJwtConfigFactory.cs deleted file mode 100644 index 3f8a5e4..0000000 --- a/EOM.TSHotelManagement.Infrastructure/Factory/IJwtConfigFactory.cs +++ /dev/null @@ -1,8 +0,0 @@ - -namespace EOM.TSHotelManagement.Infrastructure -{ - public interface IJwtConfigFactory - { - JwtConfig GetJwtConfig(); - } -} diff --git a/EOM.TSHotelManagement.Infrastructure/Factory/ILskyConfigFactory.cs b/EOM.TSHotelManagement.Infrastructure/Factory/ILskyConfigFactory.cs deleted file mode 100644 index ad3b7ee..0000000 --- a/EOM.TSHotelManagement.Infrastructure/Factory/ILskyConfigFactory.cs +++ /dev/null @@ -1,8 +0,0 @@ - -namespace EOM.TSHotelManagement.Infrastructure -{ - public interface ILskyConfigFactory - { - LskyConfig GetLskyConfig(); - } -} diff --git a/EOM.TSHotelManagement.Infrastructure/Factory/IMailConfigFactory.cs b/EOM.TSHotelManagement.Infrastructure/Factory/IMailConfigFactory.cs deleted file mode 100644 index 0a441ff..0000000 --- a/EOM.TSHotelManagement.Infrastructure/Factory/IMailConfigFactory.cs +++ /dev/null @@ -1,8 +0,0 @@ - -namespace EOM.TSHotelManagement.Infrastructure -{ - public interface IMailConfigFactory - { - MailConfig GetMailConfig(); - } -} diff --git a/EOM.TSHotelManagement.Infrastructure/Factory/JwtConfigFactory.cs b/EOM.TSHotelManagement.Infrastructure/Factory/JwtConfigFactory.cs index 451ed40..216268a 100644 --- a/EOM.TSHotelManagement.Infrastructure/Factory/JwtConfigFactory.cs +++ b/EOM.TSHotelManagement.Infrastructure/Factory/JwtConfigFactory.cs @@ -2,7 +2,7 @@ namespace EOM.TSHotelManagement.Infrastructure { - public class JwtConfigFactory : IJwtConfigFactory + public class JwtConfigFactory { private readonly IConfiguration _configuration; diff --git a/EOM.TSHotelManagement.Infrastructure/Factory/LskyConfigFactory.cs b/EOM.TSHotelManagement.Infrastructure/Factory/LskyConfigFactory.cs index 08fb4c6..f36a3d5 100644 --- a/EOM.TSHotelManagement.Infrastructure/Factory/LskyConfigFactory.cs +++ b/EOM.TSHotelManagement.Infrastructure/Factory/LskyConfigFactory.cs @@ -2,7 +2,7 @@ namespace EOM.TSHotelManagement.Infrastructure { - public class LskyConfigFactory : ILskyConfigFactory + public class LskyConfigFactory { private readonly IConfiguration _configuration; diff --git a/EOM.TSHotelManagement.Infrastructure/Factory/MailConfigFactory.cs b/EOM.TSHotelManagement.Infrastructure/Factory/MailConfigFactory.cs index 56a0c94..63a8bef 100644 --- a/EOM.TSHotelManagement.Infrastructure/Factory/MailConfigFactory.cs +++ b/EOM.TSHotelManagement.Infrastructure/Factory/MailConfigFactory.cs @@ -2,7 +2,7 @@ namespace EOM.TSHotelManagement.Infrastructure { - public class MailConfigFactory : IMailConfigFactory + public class MailConfigFactory { private readonly IConfiguration _configuration; diff --git a/EOM.TSHotelManagement.Infrastructure/Factory/RedisConfigFactory.cs b/EOM.TSHotelManagement.Infrastructure/Factory/RedisConfigFactory.cs new file mode 100644 index 0000000..415087e --- /dev/null +++ b/EOM.TSHotelManagement.Infrastructure/Factory/RedisConfigFactory.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Text; + +namespace EOM.TSHotelManagement.Infrastructure +{ + public class RedisConfigFactory + { + private readonly IConfiguration _configuration; + + public RedisConfigFactory(IConfiguration configuration) + { + _configuration = configuration; + } + + public RedisConfig GetRedisConfig() + { + var redisSection = _configuration.GetSection("Redis"); + var redisConfig = new RedisConfig + { + ConnectionString = redisSection.GetValue("ConnectionString"), + Enable = redisSection.GetValue("Enable") + }; + return redisConfig; + } + } +} -- Gitee From 855f49166f3249c165e3eb18cf039d26fcbc1461 Mon Sep 17 00:00:00 2001 From: ck_yeun9 Date: Sat, 14 Feb 2026 21:02:17 +0800 Subject: [PATCH 2/3] =?UTF-8?q?Redis=E5=8D=95=E4=BE=8B=E5=8C=96=E4=B8=8EHt?= =?UTF-8?q?tpClient=E5=B7=A5=E5=8E=82=E6=B3=A8=E5=85=A5=E5=8F=8A=E5=81=A5?= =?UTF-8?q?=E5=BA=B7=E6=A3=80=E6=9F=A5=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RedisHelper 改为全局单例,增加线程安全锁,优化初始化流程 - LskyHelper 注入 IHttpClientFactory,统一 HttpClient 管理 - 新增命名 HttpClient,提升连接复用与超时控制 - MailHelper 健康检查增强,增加 SMTP 220 响应校验 - 新增 Microsoft.Extensions.Http 依赖 - 优化 RedisHelper SetAsync 过期参数处理 - 统一代码风格,微调部分文件结构 --- .../Extensions/AutofacConfigExtensions.cs | 4 +- .../Extensions/ServiceExtensions.cs | 6 ++- .../EOM.TSHotelManagement.Common.csproj | 1 + .../Helper/LskyHelper.cs | 14 +++-- .../Helper/MailHelper.cs | 11 +++- .../Helper/RedisHelper.cs | 54 +++++++++++-------- 6 files changed, 61 insertions(+), 29 deletions(-) diff --git a/EOM.TSHotelManagement.API/Extensions/AutofacConfigExtensions.cs b/EOM.TSHotelManagement.API/Extensions/AutofacConfigExtensions.cs index 8ab4bc1..6c75f14 100644 --- a/EOM.TSHotelManagement.API/Extensions/AutofacConfigExtensions.cs +++ b/EOM.TSHotelManagement.API/Extensions/AutofacConfigExtensions.cs @@ -28,7 +28,7 @@ namespace EOM.TSHotelManagement.WebApi builder.RegisterType() .InstancePerDependency(); - builder.RegisterType().AsSelf().InstancePerLifetimeScope(); + builder.RegisterType().AsSelf().SingleInstance(); builder.RegisterType().AsSelf().InstancePerLifetimeScope(); builder.RegisterType().AsSelf().InstancePerLifetimeScope(); builder.RegisterType().AsSelf().InstancePerLifetimeScope(); @@ -56,4 +56,4 @@ namespace EOM.TSHotelManagement.WebApi #endregion } } -} \ No newline at end of file +} diff --git a/EOM.TSHotelManagement.API/Extensions/ServiceExtensions.cs b/EOM.TSHotelManagement.API/Extensions/ServiceExtensions.cs index 1cbff9f..2e9b9da 100644 --- a/EOM.TSHotelManagement.API/Extensions/ServiceExtensions.cs +++ b/EOM.TSHotelManagement.API/Extensions/ServiceExtensions.cs @@ -123,6 +123,10 @@ namespace EOM.TSHotelManagement.WebApi public static void RegisterSingletonServices(this IServiceCollection services, IConfiguration configuration) { services.AddScoped(); + services.AddHttpClient("HeartBeatCheckClient", client => + { + client.Timeout = TimeSpan.FromSeconds(30); + }); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -263,4 +267,4 @@ namespace EOM.TSHotelManagement.WebApi }); } } -} \ No newline at end of file +} diff --git a/EOM.TSHotelManagement.Common/EOM.TSHotelManagement.Common.csproj b/EOM.TSHotelManagement.Common/EOM.TSHotelManagement.Common.csproj index 6dd72b3..a0d5fec 100644 --- a/EOM.TSHotelManagement.Common/EOM.TSHotelManagement.Common.csproj +++ b/EOM.TSHotelManagement.Common/EOM.TSHotelManagement.Common.csproj @@ -24,6 +24,7 @@ + diff --git a/EOM.TSHotelManagement.Common/Helper/LskyHelper.cs b/EOM.TSHotelManagement.Common/Helper/LskyHelper.cs index f5663d5..7fab1b3 100644 --- a/EOM.TSHotelManagement.Common/Helper/LskyHelper.cs +++ b/EOM.TSHotelManagement.Common/Helper/LskyHelper.cs @@ -11,12 +11,18 @@ namespace EOM.TSHotelManagement.Common { public class LskyHelper { + private const string LskyHttpClientName = "HeartBeatCheckClient"; private readonly LskyConfigFactory lskyConfigFactory; + private readonly IHttpClientFactory httpClientFactory; private readonly ILogger logger; - public LskyHelper(LskyConfigFactory lskyConfigFactory, ILogger logger) + public LskyHelper( + LskyConfigFactory lskyConfigFactory, + IHttpClientFactory httpClientFactory, + ILogger logger) { this.lskyConfigFactory = lskyConfigFactory; + this.httpClientFactory = httpClientFactory; this.logger = logger; } @@ -44,7 +50,7 @@ namespace EOM.TSHotelManagement.Common logger.LogInformation("使用域名进行健康检查: {Domain}", domain); - using var httpClient = new HttpClient(); + var httpClient = httpClientFactory.CreateClient(LskyHttpClientName); httpClient.Timeout = TimeSpan.FromSeconds(10); var response = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, domain)); @@ -86,7 +92,7 @@ namespace EOM.TSHotelManagement.Common public async Task GetImageStorageTokenAsync() { var lskConfig = lskyConfigFactory.GetLskyConfig(); - using var httpClient = new HttpClient(); + var httpClient = httpClientFactory.CreateClient(LskyHttpClientName); var tokenRequest = new { email = lskConfig.Email, @@ -122,7 +128,7 @@ namespace EOM.TSHotelManagement.Common if (string.IsNullOrEmpty(lskConfig?.BaseAddress)) throw new InvalidOperationException(LocalizationHelper.GetLocalizedString("The base URL for the Lsky service is not configured.", "兰空图床基础地址未配置")); - using var httpClient = new HttpClient(); + var httpClient = httpClientFactory.CreateClient(LskyHttpClientName); using var content = new MultipartFormDataContent(); var fileContent = new StreamContent(fileStream); diff --git a/EOM.TSHotelManagement.Common/Helper/MailHelper.cs b/EOM.TSHotelManagement.Common/Helper/MailHelper.cs index 6bece4d..f3d3633 100644 --- a/EOM.TSHotelManagement.Common/Helper/MailHelper.cs +++ b/EOM.TSHotelManagement.Common/Helper/MailHelper.cs @@ -132,7 +132,16 @@ namespace EOM.TSHotelManagement.Common using var client = new TcpClient(); await client.ConnectAsync(host, port); - return client.Connected; + using var stream = client.GetStream(); + using var reader = new StreamReader(stream); + string response = await reader.ReadLineAsync(); + if (response?.StartsWith("220") == true) + { + var writer = new StreamWriter(stream); + await writer.WriteLineAsync("QUIT"); + return true; + } + return false; } catch (Exception ex) { diff --git a/EOM.TSHotelManagement.Common/Helper/RedisHelper.cs b/EOM.TSHotelManagement.Common/Helper/RedisHelper.cs index ea47f3a..98deaa6 100644 --- a/EOM.TSHotelManagement.Common/Helper/RedisHelper.cs +++ b/EOM.TSHotelManagement.Common/Helper/RedisHelper.cs @@ -7,6 +7,7 @@ namespace EOM.TSHotelManagement.Common { public class RedisHelper { + private readonly object _lock = new object(); private IConnectionMultiplexer _connection; private readonly ILogger logger; private readonly RedisConfigFactory configFactory; @@ -20,31 +21,35 @@ namespace EOM.TSHotelManagement.Common public void Initialize() { - try + lock (_lock) { - var redisConfig = configFactory.GetRedisConfig(); - - if (!redisConfig.Enable) + if (_connection != null) return; + try { - logger.LogInformation("Redis功能未启用,跳过初始化"); - return; - } + var redisConfig = configFactory.GetRedisConfig(); - if (string.IsNullOrWhiteSpace(redisConfig?.ConnectionString)) - throw new ArgumentException("Redis连接字符串不能为空"); + if (!redisConfig.Enable) + { + logger.LogInformation("Redis功能未启用,跳过初始化"); + return; + } - var options = ConfigurationOptions.Parse(redisConfig.ConnectionString); - options.AbortOnConnectFail = false; - options.ConnectTimeout = 5000; - options.ReconnectRetryPolicy = new ExponentialRetry(3000); + if (string.IsNullOrWhiteSpace(redisConfig?.ConnectionString)) + throw new ArgumentException("Redis连接字符串不能为空"); - _connection = ConnectionMultiplexer.Connect(options); - _connection.GetDatabase().Ping(); - } - catch (Exception ex) - { - logger.LogCritical(ex, "Redis初始化失败"); - throw; + var options = ConfigurationOptions.Parse(redisConfig.ConnectionString); + options.AbortOnConnectFail = false; + options.ConnectTimeout = 5000; + options.ReconnectRetryPolicy = new ExponentialRetry(3000); + + _connection = ConnectionMultiplexer.Connect(options); + _connection.GetDatabase().Ping(); + } + catch (Exception ex) + { + logger.LogCritical(ex, "Redis初始化失败"); + throw; + } } } @@ -77,7 +82,14 @@ namespace EOM.TSHotelManagement.Common try { var db = GetDatabase(); - return await db.StringSetAsync(key, value, (Expiration)expiry); + if (expiry.HasValue) + { + return await db.StringSetAsync(key, value, new StackExchange.Redis.Expiration(expiry.Value)); + } + else + { + return await db.StringSetAsync(key, value); + } } catch (Exception ex) { -- Gitee From 42ac6c9b95436d6f6ba8bce1f02c86fd56c20f4a Mon Sep 17 00:00:00 2001 From: ck_yeun9 Date: Sat, 14 Feb 2026 21:22:00 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E9=87=8A=E6=94=BE=E4=B8=8E=E5=9C=B0=E5=9D=80=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 MailHelper.cs 中使用 using 声明 StreamWriter,确保资源正确释放,提高代码健壮性。 在 UrlHelper.cs 中改进 baseAddress 的解析,使用 StringSplitOptions.RemoveEmptyEntries 和 FirstOrDefault(),提升域名提取的准确性。 --- EOM.TSHotelManagement.Common/Helper/MailHelper.cs | 2 +- EOM.TSHotelManagement.Common/Helper/UrlHelper.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/EOM.TSHotelManagement.Common/Helper/MailHelper.cs b/EOM.TSHotelManagement.Common/Helper/MailHelper.cs index f3d3633..ca0aa73 100644 --- a/EOM.TSHotelManagement.Common/Helper/MailHelper.cs +++ b/EOM.TSHotelManagement.Common/Helper/MailHelper.cs @@ -137,7 +137,7 @@ namespace EOM.TSHotelManagement.Common string response = await reader.ReadLineAsync(); if (response?.StartsWith("220") == true) { - var writer = new StreamWriter(stream); + using var writer = new StreamWriter(stream); await writer.WriteLineAsync("QUIT"); return true; } diff --git a/EOM.TSHotelManagement.Common/Helper/UrlHelper.cs b/EOM.TSHotelManagement.Common/Helper/UrlHelper.cs index 06b244f..214723c 100644 --- a/EOM.TSHotelManagement.Common/Helper/UrlHelper.cs +++ b/EOM.TSHotelManagement.Common/Helper/UrlHelper.cs @@ -65,7 +65,7 @@ namespace EOM.TSHotelManagement.Common var cleanAddress = baseAddress .Replace("https://", "") .Replace("http://", "") - .Split('/')[0]; // 取第一个部分(域名) + .Split('/', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); // 取第一个部分(域名) return cleanAddress; } -- Gitee