diff --git a/Configurations/SimApiJobOptions.cs b/Configurations/SimApiJobOptions.cs new file mode 100644 index 0000000..ccfef12 --- /dev/null +++ b/Configurations/SimApiJobOptions.cs @@ -0,0 +1,22 @@ +namespace SimApi.Configurations; + +public class SimApiJobOptions +{ + /// + /// WebUi地址,设置为null表示不启用 + /// + public string? DashboardUrl { get; set; } = "/jobs"; + + public string DashboardAuthUser { get; set; } = "admin"; + public string DashboardAuthPass { get; set; } = "Admin@123!"; + public string? RedisConfiguration { get; set; } + + public int? Database { get; set; } = null; + public SimApiJobServerConfig[] Servers { get; set; } = [new()]; +} + +public class SimApiJobServerConfig() +{ + public string[] Queues { get; set; } = ["default"]; + public int WorkerNum { get; set; } = 50; +} \ No newline at end of file diff --git a/Configurations/SimApiOptions.cs b/Configurations/SimApiOptions.cs index 3609ce1..fe23e9e 100644 --- a/Configurations/SimApiOptions.cs +++ b/Configurations/SimApiOptions.cs @@ -5,6 +5,13 @@ namespace SimApi.Configurations; public class SimApiOptions { + public string? RedisConfiguration { get; set; } + + /// + /// 是否启用后台任务系统 *基于Hangfire + /// + public bool EnableJob { get; set; } = false; + /// /// 启用全部Cors,对于开发前后分离的时候很有用。 /// default: true @@ -36,7 +43,7 @@ public class SimApiOptions /// default: true /// public bool EnableSimApiException { get; set; } = true; - + /// /// 启用返回结果拦截 /// @@ -71,6 +78,10 @@ public class SimApiOptions /// public bool EnableSynapse { get; set; } + /// + /// 配置Job + /// + public SimApiJobOptions SimApiJobOptions { get; set; } = new(); /// /// Swagger文档相关配置,需要启用 EnableSimApiDoc @@ -103,4 +114,9 @@ public class SimApiOptions { options?.Invoke(SimApiStorageOptions); } + + public void ConfigureSimApiJob(Action? options = null) + { + options?.Invoke(SimApiJobOptions); + } } \ No newline at end of file diff --git a/Helpers/SimApiCache.cs b/Helpers/SimApiCache.cs new file mode 100644 index 0000000..4d4926f --- /dev/null +++ b/Helpers/SimApiCache.cs @@ -0,0 +1,32 @@ +using System.Text.Json; +using Microsoft.Extensions.Caching.Distributed; + +namespace SimApi.Helpers; + +public class SimApiCache(IDistributedCache cache) +{ + private const string Prefix = "SimApi:Cache"; + + public void Set(string key, object value, DistributedCacheEntryOptions? options = null) + { + if (options is not null) + { + cache.SetString(Prefix + key, SimApiUtil.Json(value), options); + } + else + { + cache.SetString(Prefix + key, SimApiUtil.Json(value)); + } + } + + public string? Get(string key) + { + return cache.GetString(Prefix + key); + } + + public T? Get(string key) + { + var data = cache.GetString(Prefix + key); + return data == null ? default : JsonSerializer.Deserialize(data); + } +} \ No newline at end of file diff --git a/Helpers/SimApiJobWebAuth.cs b/Helpers/SimApiJobWebAuth.cs new file mode 100644 index 0000000..4b70d6c --- /dev/null +++ b/Helpers/SimApiJobWebAuth.cs @@ -0,0 +1,33 @@ +using System; +using System.Linq; +using System.Text; +using Hangfire.Dashboard; +using Microsoft.AspNetCore.Http; + +namespace SimApi.Helpers; + +public class SimApiJobWebAuth(string user, string pass) : IDashboardAuthorizationFilter +{ + public bool Authorize(DashboardContext context) + { + var httpContext = context.GetHttpContext(); + var authHeader = httpContext.Request.Headers["Authorization"].FirstOrDefault(); + if (authHeader != null && authHeader.StartsWith("Basic ")) + { + var encodedUsernamePassword = authHeader.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries)[1]?.Trim(); + var decodedUsernamePassword = Encoding.UTF8.GetString(Convert.FromBase64String(encodedUsernamePassword)); + var username = decodedUsernamePassword.Split(':', 2)[0]; + var password = decodedUsernamePassword.Split(':', 2)[1]; + + if (username == user && password == pass) + { + return true; + } + } + + httpContext.Response.StatusCode = 401; + httpContext.Response.Headers.WWWAuthenticate = "Basic realm=\"SimApiBasicAuth\""; + httpContext.Response.WriteAsync("").Wait(); + return false; + } +} \ No newline at end of file diff --git a/SimApi.csproj b/SimApi.csproj index 8e004ac..ca15b53 100644 --- a/SimApi.csproj +++ b/SimApi.csproj @@ -23,10 +23,14 @@ - - - - + + + + + + + + diff --git a/SimApiExtensions.cs b/SimApiExtensions.cs index 8bbc8db..2c0b8fa 100644 --- a/SimApiExtensions.cs +++ b/SimApiExtensions.cs @@ -3,6 +3,9 @@ using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text.Json; +using Hangfire; +using Hangfire.Console; +using Hangfire.Redis.StackExchange; using SimApi.Helpers; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; @@ -29,6 +32,12 @@ public static class SimApiExtensions { var simApiOptions = new SimApiOptions(); options?.Invoke(simApiOptions); + if (simApiOptions.RedisConfiguration != null) + { + builder.AddStackExchangeRedisCache(x => x.Configuration = simApiOptions.RedisConfiguration); + builder.AddSingleton(); + } + if (simApiOptions.EnableLogger) { builder.AddLogging(logger => @@ -49,6 +58,30 @@ public static class SimApiExtensions builder.AddSingleton(); } + if (simApiOptions.EnableJob) + { + builder.AddHangfire(x => + { + var redisOption = new RedisStorageOptions(); + if (simApiOptions.SimApiJobOptions.Database.HasValue) + { + redisOption.Db = simApiOptions.SimApiJobOptions.Database.Value; + } + + x.UseRedisStorage(simApiOptions.SimApiJobOptions.RedisConfiguration ?? + simApiOptions.RedisConfiguration, redisOption); + x.UseConsole(); + }); + foreach (var server in simApiOptions.SimApiJobOptions.Servers) + { + builder.AddHangfireServer(hfs => + { + hfs.Queues = server.Queues; + hfs.WorkerCount = server.WorkerNum; + }); + } + } + if (simApiOptions.EnableCors) { builder.AddCors(cors => cors.AddPolicy("any", @@ -228,6 +261,11 @@ public static class SimApiExtensions logger.LogInformation("当前时区: {LocalId}", TimeZoneInfo.Local.Id); + if (options.RedisConfiguration != null) + { + logger.LogInformation("开始配置 RedisCache ..."); + } + //请求一下检测存储错误 if (options.EnableSimApiStorage) { @@ -235,11 +273,6 @@ public static class SimApiExtensions builder.Services.GetService(); } - if (options.EnableSimApiResponseFilter) - { - logger.LogInformation("开始配置SimApiResponseFilter..."); - } - if (options.EnableCoceSdk) { logger.LogInformation("开始配置CoceAppSdk...\nApi入口: {ApiUrl}\nAuth入口:{AuthUrl}n\nAppId: {AppId}", @@ -247,17 +280,17 @@ public static class SimApiExtensions options.CoceSdkOptions.AppId); } - if (options.EnableLowerUrl) - { - logger.LogInformation("开始配置使用URL小写..."); - } - if (options.EnableSynapse) { var synapse = builder.Services.GetRequiredService(); synapse.Init(); } + if (options.EnableJob) + { + logger.LogInformation("开始配置 SimApiJob ..."); + } + return builder; } @@ -269,10 +302,8 @@ public static class SimApiExtensions public static WebApplication UseSimApi(this WebApplication builder) { var options = builder.Services.GetRequiredService(); - var logger = builder.Services.GetRequiredService>(); - - logger.LogInformation("当前时区: {LocalId}", TimeZoneInfo.Local.Id); + UseSimApi((IHost)builder); if (options.EnableForwardHeaders) { logger.LogInformation("开始配置ForwardedHeaders..."); @@ -285,6 +316,11 @@ public static class SimApiExtensions builder.UseCors("any"); } + if (options.EnableSimApiResponseFilter) + { + logger.LogInformation("开始配置SimApiResponseFilter..."); + } + if (options.EnableSimApiAuth) { logger.LogInformation("开始配置SimApiAuth..."); @@ -333,22 +369,22 @@ public static class SimApiExtensions builder.UseMiddleware(); } - //请求一下检测存储错误 - if (options.EnableSimApiStorage) - { - logger.LogInformation("开始配置SimApiStorage..."); - builder.Services.GetService(); - } - if (options.EnableLowerUrl) { logger.LogInformation("开始配置使用URL小写..."); } - if (options.EnableSynapse) + if (options is { EnableJob: true, SimApiJobOptions.DashboardUrl: not null }) { - var synapse = builder.Services.GetRequiredService(); - synapse.Init(); + logger.LogInformation("开始配置 SimApiJob Web控制台..."); + builder.UseHangfireDashboard(options.SimApiJobOptions.DashboardUrl, new DashboardOptions + { + Authorization = + [ + new SimApiJobWebAuth(options.SimApiJobOptions.DashboardAuthUser, + options.SimApiJobOptions.DashboardAuthPass) + ] + }); } return builder; diff --git a/Synapse/Synapse.cs b/Synapse/Synapse.cs index 6073dc5..9211ee1 100644 --- a/Synapse/Synapse.cs +++ b/Synapse/Synapse.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using MQTTnet; -using MQTTnet.Client; using MQTTnet.Formatter; using SimApi.Attributes; using SimApi.Communications; @@ -22,7 +21,7 @@ public partial class Synapse(SimApiOptions simApiOptions, ILogger logge { private SimApiSynapseOptions Options { get; } = simApiOptions.SimApiSynapseOptions; - private MqttFactory MqttFactory { get; } = new(); + private MqttClientFactory MqttFactory { get; } = new(); public IMqttClient? Client { get; set; } private List EventRegistry { get; set; } = new();