From 510c70cb8312979217e4779be34511703524e197 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 13 Dec 2024 17:56:57 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=91=BD=E4=BB=A4=E8=A1=8C?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E6=94=AF=E6=8C=81=E5=92=8C=E6=9C=AA=E5=A4=84?= =?UTF-8?q?=E7=90=86=E5=BC=82=E5=B8=B8=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Config/AppConfig.cs | 25 ++- src/Handlers/CmdArgsHandler.cs | 37 ++++ src/Helpers/CommandLineArgsHelper.cs | 51 +++++ .../ErrorHandlerMiddleware.cs | 12 +- .../IPAccessLimitMiddleware.cs | 0 .../JwtMiddleware.cs | 4 +- src/Program.cs | 56 ++++- src/SerilogConfig.cs | 24 ++- src/Services/DatabaseGateService.cs | 199 ++++++++---------- src/Services/LocalCacheManager.cs | 41 ++-- 10 files changed, 281 insertions(+), 168 deletions(-) create mode 100644 src/Handlers/CmdArgsHandler.cs create mode 100644 src/Helpers/CommandLineArgsHelper.cs rename src/{Middleware => Middlewares}/ErrorHandlerMiddleware.cs (78%) rename src/{Middleware => Middlewares}/IPAccessLimitMiddleware.cs (100%) rename src/{Middleware => Middlewares}/JwtMiddleware.cs (94%) diff --git a/src/Config/AppConfig.cs b/src/Config/AppConfig.cs index 4555ed4..890d0ac 100644 --- a/src/Config/AppConfig.cs +++ b/src/Config/AppConfig.cs @@ -69,7 +69,10 @@ namespace iFileProxy.Config } var databaseHelper = serviceProvider.GetRequiredService(); - databaseHelper.TestDbConfig(); + if (databaseHelper.TestDbConfig()) + { + _logger.Information("数据库配置验证成功."); + } } else { @@ -120,40 +123,40 @@ namespace iFileProxy.Config public partial class Common { [JsonPropertyName("Host")] - public string Host { get; set; } + public string Host { get; set; } = string.Empty; [JsonPropertyName("Port")] - public int Port { get; set; } = 3306; + public uint Port { get; set; } = 3306; [JsonPropertyName("User")] - public string User { get; set; } + public string User { get; set; } = string.Empty; [JsonPropertyName("Password")] - public string Password { get; set; } + public string Password { get; set; } = string.Empty; } public partial class DB { [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("Host")] - public string Host { get; set; } + public string? Host { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("Port")] - public long? Port { get; set; } = 3306; + public uint? Port { get; set; } = null; [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("User")] - public string User { get; set; } + public string? User { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("Password")] - public string Password { get; set; } + public string? Password { get; set; } [JsonPropertyName("DatabaseName")] - public string DatabaseName { get; set; } + public string? DatabaseName { get; set; } [JsonPropertyName("Description")] - public string Description { get; set; } + public string? Description { get; set; } } public class StreamProxyOptions diff --git a/src/Handlers/CmdArgsHandler.cs b/src/Handlers/CmdArgsHandler.cs new file mode 100644 index 0000000..c5465a3 --- /dev/null +++ b/src/Handlers/CmdArgsHandler.cs @@ -0,0 +1,37 @@ +using iFileProxy.Config; +using iFileProxy.Helpers; +using iFileProxy.Services; +namespace iFileProxy.Handlers +{ + public class CmdArgsHandler + { + public static void ParseArgs(string[] args) + { + if (args.Length == 0) + return; + Dictionary helpStrings = []; + helpStrings.Add("--init-db", "初始化数据库"); + helpStrings.Add("--dev-logger", "启用开发者logger"); + helpStrings.Add("--disable-startup-check", "禁用启动时程序配置自检"); + helpStrings.Add("--url=", "设置程序app.run() Url 如: http://0.0.0.0:1145"); + + if (args.Contains("-h") || args.Contains("--help")) + { + Console.WriteLine("命令行参数帮助: "); + foreach (var hs in helpStrings) + { + Console.WriteLine($"\t{hs.Key}\t{hs.Value}"); + } + Environment.Exit(0); + } + + CommandLineArgsHelper cmdlineHelper = new(args); + + if (cmdlineHelper.GetBooleanValue("--init-db")) + { + DatabaseGateService databaseGateService = new(AppConfig.GetCurrConfig()); + databaseGateService.TryInitialDB(); + } + } + } +} diff --git a/src/Helpers/CommandLineArgsHelper.cs b/src/Helpers/CommandLineArgsHelper.cs new file mode 100644 index 0000000..30efed3 --- /dev/null +++ b/src/Helpers/CommandLineArgsHelper.cs @@ -0,0 +1,51 @@ +using Newtonsoft.Json; +using Serilog; + +namespace iFileProxy.Helpers +{ + + public class CommandLineArgsHelper(string[] args) + { + private readonly Dictionary _parameters = ParseArgs(args); + + // 解析命令行参数 + private static Dictionary ParseArgs(string[] args) + { + var parameters = new Dictionary(); + + for (int i = 0; i < args.Length; i++) + { + var arg = args[i]; + + // 如果参数是键值对形式 + if (arg.Contains('=')) + { + var parts = arg.Split(['='], 2); + if (parts.Length == 2) + { + parameters[parts[0].TrimStart('-')] = parts[1]; + } + } + else + { + // 如果参数是没有值的标志性参数(比如 --dev-logger) + parameters[arg.TrimStart('-')] = "true"; + } + } + + return parameters; + } + + // 获取指定参数的值,若无值则返回null + public string? GetStringValue(string key) + { + return _parameters.ContainsKey(key) ? _parameters[key] : null; + } + + // 获取指定参数的布尔值,若参数值是 "true" 则返回true,否则返回false + public bool GetBooleanValue(string key) + { + return _parameters.ContainsKey(key) && _parameters[key].Equals("true", StringComparison.CurrentCultureIgnoreCase); + } + } +} diff --git a/src/Middleware/ErrorHandlerMiddleware.cs b/src/Middlewares/ErrorHandlerMiddleware.cs similarity index 78% rename from src/Middleware/ErrorHandlerMiddleware.cs rename to src/Middlewares/ErrorHandlerMiddleware.cs index 93843b0..246334e 100644 --- a/src/Middleware/ErrorHandlerMiddleware.cs +++ b/src/Middlewares/ErrorHandlerMiddleware.cs @@ -24,17 +24,13 @@ namespace iFileProxy.Middleware await _next(context); if (context.Response.HasStarted) { - _logger.Debug($"响应已经开始 错误处理中间件无法再修改其响应内容"); + _logger.Debug($"响应已经开始,无法再修改其响应内容"); return; } if (context.Response.StatusCode == 404) { - context.Response.OnStarting(async () => - { - context.Response.ContentType = "application/json"; - await context.Response.WriteAsync(JsonSerializer.Serialize(new CommonRsp { Retcode = 404, Message = "你正在请求的资源不存在!" })); - } - ); + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(JsonSerializer.Serialize(new CommonRsp { Retcode = 404, Message = "你正在请求的资源不存在!" })); } } catch (Exception ex) @@ -46,7 +42,7 @@ namespace iFileProxy.Middleware private static Task HandleExceptionAsync(HttpContext context, Exception exception) { var code = HttpStatusCode.InternalServerError; // 500 if unexpected - _logger.Fatal("崩溃数据: {exception}\n上下文信息: {context}", exception, JsonSerializer.Serialize(MasterHelper.ExtractDebugInfo(context))); + _logger.Error("崩溃数据: {exception}\n上下文信息: {context}", exception, JsonSerializer.Serialize(MasterHelper.ExtractDebugInfo(context))); switch (exception) { diff --git a/src/Middleware/IPAccessLimitMiddleware.cs b/src/Middlewares/IPAccessLimitMiddleware.cs similarity index 100% rename from src/Middleware/IPAccessLimitMiddleware.cs rename to src/Middlewares/IPAccessLimitMiddleware.cs diff --git a/src/Middleware/JwtMiddleware.cs b/src/Middlewares/JwtMiddleware.cs similarity index 94% rename from src/Middleware/JwtMiddleware.cs rename to src/Middlewares/JwtMiddleware.cs index 3966ee9..3289ec6 100644 --- a/src/Middleware/JwtMiddleware.cs +++ b/src/Middlewares/JwtMiddleware.cs @@ -14,9 +14,9 @@ namespace iFileProxy.Middleware public async Task InvokeAsync(HttpContext context) { - var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last(); + var token = context.Request.Headers.Authorization.FirstOrDefault()?.Split(" ").Last(); var fingerprint = context.Request.Headers["X-Device-Fingerprint"].FirstOrDefault(); - var userAgent = context.Request.Headers["User-Agent"].FirstOrDefault(); + var userAgent = context.Request.Headers.UserAgent.FirstOrDefault(); var ip = MasterHelper.GetClientIPAddr(context); if (token != null) diff --git a/src/Program.cs b/src/Program.cs index 0bb29cf..7278a7b 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,12 +1,13 @@ using iFileProxy.Config; using iFileProxy.Middleware; using iFileProxy.Helpers; +using iFileProxy.Handlers; using iFileProxy.Services; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using Serilog; using System.Text; -using System.Diagnostics; +using MySql.Data.MySqlClient; namespace iFileProxy { @@ -14,11 +15,16 @@ namespace iFileProxy { public static void Main(string[] args) { - SerilogConfig.CreateLogger(); - Serilog.ILogger logger = Log.Logger.ForContext(); + // 绑定异常捕获事件 + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + SerilogConfig.CreateLogger(args: args); + + CommandLineArgsHelper argsHelper = new (args); Console.Write(" "); // 补全日志第一行开头的空白 + CmdArgsHandler.ParseArgs(args); + var builder = WebApplication.CreateBuilder(args); // CORS配置 @@ -73,6 +79,10 @@ namespace iFileProxy // 初始化缓存管理服务 LocalCacheManager localCacheManager = new(app.Services); + if (!argsHelper.GetBooleanValue("disable-startup-check")) + // 初始化验证配置文件 + AppConfig.CheckAppConfig(app.Services); + // 1. 错误处理(放在请求管道的最前面) app.UseMiddleware(); @@ -118,9 +128,45 @@ namespace iFileProxy // 11. 启动应用 var dbGateService = app.Services.GetRequiredService(); - SerilogConfig.CreateLogger(dbGateService); + SerilogConfig.CreateLogger(dbGateService,args); - app.Run(); + if (!argsHelper.GetStringValue("url").IsNullOrEmpty()) + app.Run(argsHelper.GetStringValue("url")); + else + app.Run(); + } + + + private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) + { + var logger = Log.ForContext(); + Exception exc = (Exception)e.ExceptionObject; + + // 直接记录原始异常信息,不重新抛出 + if (exc is MySqlException mySqlEx) + { + if (mySqlEx.Message.Contains("is not allowed to connect to this MySQL server", StringComparison.CurrentCulture)) + { + logger.Fatal( "远程主机不允许你连接到该数据库, 请检查数据库服务端是否将本机IP加白! "); + } + else if (mySqlEx.Message.Contains("Unable to connect to any of the specified MySQL hosts")) + { + logger.Fatal("无法连接到远程数据库, 因为远程主机不可达, 请检查你的网络或数据库配置!!"); + } + } + else if (exc is IOException ioEx) + { + if (ioEx.Message.Contains("address already in use.")) + { + logger.Fatal("程序服务端口被占用!!!"); + } + } + + // 记录所有未处理的异常信息 + logger.Fatal(exc, "未处理的异常: {ExceptionMessage}", exc.Message); + + // 优雅退出应用程序 + Environment.Exit(1); } } } diff --git a/src/SerilogConfig.cs b/src/SerilogConfig.cs index 89f5e4a..d27995f 100644 --- a/src/SerilogConfig.cs +++ b/src/SerilogConfig.cs @@ -6,29 +6,34 @@ using iFileProxy.Sinks; using iFileProxy.Services; using ZstdSharp.Unsafe; + using iFileProxy.Helpers; public static class SerilogConfig { private static DatabaseGateService? _dbGateService; - public static void CreateLogger(DatabaseGateService? dbGateService = null) + public static void CreateLogger(DatabaseGateService? dbGateService = null, string[]? args = null) { _dbGateService = dbGateService; // 保存实例以供后续使用 var baseLogPath = Path.Combine(AppContext.BaseDirectory, "logs"); var appName = AppDomain.CurrentDomain.FriendlyName; - + // 确保日志目录存在 if (!Directory.Exists(baseLogPath)) { Directory.CreateDirectory(baseLogPath); } + var loggerConfiguration = new LoggerConfiguration(); - var loggerConfiguration = new LoggerConfiguration() -#if RELEASE - .MinimumLevel.Information() -#else - .MinimumLevel.Debug() -#endif + + if (new CommandLineArgsHelper(args).GetBooleanValue("dev-logger")) + { + loggerConfiguration.MinimumLevel.Debug(); + } + else + loggerConfiguration.MinimumLevel.Information(); + + loggerConfiguration .MinimumLevel.Override("Microsoft", LogEventLevel.Information) .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) .Enrich.FromLogContext() @@ -43,7 +48,7 @@ outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{SourceContext}] {ClientIp} {Message:lj} {contentType} {queryString}{NewLine}{Exception}", rollingInterval: RollingInterval.Day, fileSizeLimitBytes: 1073741824)) // 1GB - // 警告日志写入单独的文件 + // 警告日志写入单独的文件 .WriteTo.Logger(lc => lc .Filter.ByIncludingOnly(evt => evt.Level == LogEventLevel.Warning) .WriteTo.File( @@ -69,6 +74,7 @@ fileSizeLimitBytes: 1073741824)) .Enrich.WithProperty("node_ip", GetIpAddress()); + // 只有在提供了 DatabaseGateService 时才添加数据库 Sink if (_dbGateService != null) { diff --git a/src/Services/DatabaseGateService.cs b/src/Services/DatabaseGateService.cs index 1110dff..b8f2c1d 100644 --- a/src/Services/DatabaseGateService.cs +++ b/src/Services/DatabaseGateService.cs @@ -18,77 +18,13 @@ namespace iFileProxy.Services Dictionary _dbDictionary = []; - private const string CREATE_TASKS_TABLE_SQL = """ - CREATE TABLE IF NOT EXISTS `t_tasks_info` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增键', - `tid` varchar(48) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '任务id', - `file_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, - `client_ip` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户端IP', - `add_time` datetime NOT NULL COMMENT '任务在何时被添加', - `update_time` datetime NOT NULL COMMENT '任务状态更新时间', - `status` int(11) NULL DEFAULT NULL COMMENT '任务状态', - `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件url', - `size` int(11) NULL DEFAULT NULL COMMENT '文件大小', - `hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件hash', - PRIMARY KEY (`id`) USING BTREE - ) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; - """; - - private const string CREATE_USERS_TABLE_SQL = """ - CREATE TABLE IF NOT EXISTS `t_users` ( - `user_id` varchar(36) NOT NULL, - `username` varchar(50) NOT NULL, - `nickname` varchar(50) NOT NULL, - `email` varchar(100) NOT NULL, - `password_hash` varchar(255) NOT NULL, - `mask` int NOT NULL DEFAULT 0, - `state` int NOT NULL DEFAULT 0, - `create_time` datetime NOT NULL, - `last_login_time` datetime NULL, - `last_login_ip` varchar(45) NULL, - PRIMARY KEY (`user_id`), - UNIQUE KEY `username` (`username`), - UNIQUE KEY `email` (`email`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - """; - - private const string CREATE_USER_EVENTS_TABLE_SQL = """ - CREATE TABLE IF NOT EXISTS `t_user_events` ( - `event_id` varchar(36) NOT NULL, - `user_id` varchar(36) NOT NULL, - `event_type` int NOT NULL, - `event_time` datetime NOT NULL, - `event_ip` varchar(45) NULL, - `event_detail` text NULL, - PRIMARY KEY (`event_id`), - KEY `user_id` (`user_id`), - CONSTRAINT `fk_user_events_user` FOREIGN KEY (`user_id`) REFERENCES `t_users` (`user_id`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - """; - - private const string CREATE_SYSTEM_LOGS_TABLE_SQL = """ - CREATE TABLE IF NOT EXISTS `t_system_logs` ( - `log_id` varchar(36) NOT NULL, - `level` varchar(20) NOT NULL, - `message` text NOT NULL, - `exception` text, - `properties` json, - `timestamp` datetime NOT NULL, - PRIMARY KEY (`log_id`), - KEY `idx_timestamp` (`timestamp`), - KEY `idx_level` (`level`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - """; - - private const string CREATE_DOWNLOAD_HISTORY_TABLE_SQL = """ - CREATE TABLE IF NOT EXISTS `t_download_history` ( - `tid` varchar(48) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '任务UUID', - `time` datetime NULL DEFAULT NULL COMMENT '触发时间', - `client_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户端IP', - PRIMARY KEY (`tid`) USING BTREE, - CONSTRAINT `t_download_history_ibfk_1` FOREIGN KEY (`tid`) REFERENCES `t_tasks_info` (`tid`) ON DELETE CASCADE ON UPDATE CASCADE - ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; - """; + private Dictionary createDBSqls = new Dictionary { + { "任务信息",@"CREATE TABLE `t_tasks_info` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增键', `tid` varchar(48) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '任务id', `file_name` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '文件名', `client_ip` varchar(46) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户端IP', `add_time` datetime NOT NULL COMMENT '任务在何时被添加', `update_time` datetime NOT NULL COMMENT '任务状态更新时间', `status` int(1) NULL DEFAULT NULL COMMENT '任务状态', `url` varchar(8192) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件url', `size` bigint(32) NULL DEFAULT NULL COMMENT '文件大小', `hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件hash', `tag` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标记', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `tid`(`tid`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 139915 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;"}, + { "用户", @"CREATE TABLE `t_users` ( `user_id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '昵称', `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '电子邮箱', `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名', `password_hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码哈希值', `mask` int(11) NOT NULL DEFAULT 0 COMMENT '权限掩码', `state` int(11) NOT NULL DEFAULT 0 COMMENT '状态', `create_time` datetime NOT NULL COMMENT '账号创建时间', `last_login_time` datetime NULL DEFAULT NULL COMMENT '上次登录时间', `last_login_ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '上次登录IP', PRIMARY KEY (`user_id`) USING BTREE, UNIQUE INDEX `username`(`username`) USING BTREE, UNIQUE INDEX `email`(`email`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;"}, + { "用户事件", @"CREATE TABLE `t_user_events` ( `event_id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `user_id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `event_type` int(11) NOT NULL, `event_time` datetime NOT NULL, `event_ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `event_detail` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, PRIMARY KEY (`event_id`) USING BTREE, INDEX `user_id`(`user_id`) USING BTREE, CONSTRAINT `t_user_events_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `t_users` (`user_id`) ON DELETE RESTRICT ON UPDATE RESTRICT ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;"}, + { "系统日志", @"CREATE TABLE `t_system_logs` ( `log_id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `level` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `exception` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, `properties` json NULL, `timestamp` datetime NOT NULL, PRIMARY KEY (`log_id`) USING BTREE, INDEX `idx_timestamp`(`timestamp`) USING BTREE, INDEX `idx_level`(`level`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;" }, + {"下载历史",@"CREATE TABLE `t_download_history` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键', `tid` varchar(48) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '任务UUID', `time` datetime NULL DEFAULT NULL COMMENT '触发时间', `client_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户端IP', PRIMARY KEY (`id`) USING BTREE, INDEX `tid`(`tid`) USING BTREE, CONSTRAINT `t_download_history_ibfk_1` FOREIGN KEY (`tid`) REFERENCES `t_tasks_info` (`tid`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE = InnoDB AUTO_INCREMENT = 532 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;" } + }; public DatabaseGateService(AppConfig appConfig) { @@ -97,13 +33,13 @@ namespace iFileProxy.Services _appConfig = appConfig; try { + LoadDbDict(); _logger.Information("Done."); } catch (Exception e) { _logger.Fatal($"程序异常: {e.Message}"); } - LoadDbDict(); } /// @@ -154,7 +90,7 @@ namespace iFileProxy.Services try { - var conn = new MySqlConnection(builder.ConnectionString); + var conn = new MySqlConnection(builder.ConnectionString); conn.Open(); return conn; } @@ -178,7 +114,7 @@ namespace iFileProxy.Services catch (Exception) { _logger.Fatal($"=========== 数据库: {db.Key} 测试失败! ==========="); - return false; + throw; } } return true; @@ -206,10 +142,10 @@ namespace iFileProxy.Services /// /// /// - public string QueryTableData(string sql,string dbConfName) + public string QueryTableData(string sql, string dbConfName) { DataTable dataTable = new(); - using (MySqlDataAdapter adapter = new(new MySqlCommand(sql,GetAndOpenDBConn(dbConfName)))) + using (MySqlDataAdapter adapter = new(new MySqlCommand(sql, GetAndOpenDBConn(dbConfName)))) adapter.Fill(dataTable); return JsonConvert.SerializeObject(dataTable); } @@ -221,8 +157,8 @@ namespace iFileProxy.Services /// 配置文件中的Description字段 /// 影响的行数 public int Query(string sql, string dbConfName) - { - using MySqlCommand sqlCmd = new (sql, GetAndOpenDBConn(dbConfName)); + { + using MySqlCommand sqlCmd = new(sql, GetAndOpenDBConn(dbConfName)); int n = sqlCmd.ExecuteNonQuery(); _logger.Debug($"查询完成, 受影响的行数: {n}"); return n; @@ -242,7 +178,13 @@ namespace iFileProxy.Services return n; } - public List CheckCacheDependencies(string taskId,string ipAddr) + /// + /// 检查缓存依赖 + /// + /// 任务ID + /// 排除的IP地址 + /// 依赖任务信息列表 + public List CheckCacheDependencies(string taskId, string ipAddr) { string sql = $"SELECT * FROM t_tasks_info WHERE `status` = @status AND `tag` = @tag AND `client_ip` <> @ip_addr"; using MySqlConnection conn = GetAndOpenDBConn("iFileProxy_Db"); @@ -253,7 +195,7 @@ namespace iFileProxy.Services sqlCmd.Parameters.AddWithValue("@status", TaskState.Cached); sqlCmd.Parameters.AddWithValue("@ip_addr", ipAddr); - return JsonConvert.DeserializeObject>( QueryTableData(sqlCmd)); + return JsonConvert.DeserializeObject>(QueryTableData(sqlCmd)); } catch (Exception e) { @@ -271,7 +213,7 @@ namespace iFileProxy.Services { using MySqlCommand sqlCmd = new(sql, conn); sqlCmd.Parameters.AddWithValue("@ip_addr", ipAddr); - sqlCmd.Parameters.AddWithValue ("@status", status); + sqlCmd.Parameters.AddWithValue("@status", status); return QueryTableData(sqlCmd); } catch (Exception e) @@ -289,7 +231,7 @@ namespace iFileProxy.Services using MySqlConnection conn = GetAndOpenDBConn("iFileProxy_Db"); try { - using MySqlCommand sqlCmd = new (sql,conn); + using MySqlCommand sqlCmd = new(sql, conn); sqlCmd.Parameters.AddWithValue("@ip_addr", ipAddr); return QueryTableData(sqlCmd); } @@ -322,7 +264,8 @@ namespace iFileProxy.Services { string sql = $"SELECT * FROM t_tasks_info WHERE url = @url AND size = @size AND `status` = @status AND file_name = @fileName"; using MySqlConnection conn = GetAndOpenDBConn("iFileProxy_Db"); - try { + try + { MySqlCommand sqlCmd = new MySqlCommand(sql, conn); sqlCmd.Parameters.AddWithValue("@url", url); sqlCmd.Parameters.AddWithValue("@size", size); @@ -337,10 +280,12 @@ namespace iFileProxy.Services else return null; } - catch (Exception e){ + catch (Exception e) + { return null; } - finally { + finally + { conn.Close(); } @@ -380,21 +325,21 @@ namespace iFileProxy.Services } return true; } - public bool UpdateFieldsData(string fieldsName, string taskUUID,object val) + public bool UpdateFieldsData(string fieldsName, string taskUUID, object val) { string sql = $"UPDATE t_tasks_info set `{fieldsName}` = @Data WHERE `tid` = @tid"; using MySqlConnection conn = GetAndOpenDBConn("iFileProxy_Db"); try { using MySqlCommand sqlCmd = new(sql, conn); - sqlCmd.Parameters.AddWithValue("@Data",val); - sqlCmd.Parameters.AddWithValue("@tid",taskUUID); + sqlCmd.Parameters.AddWithValue("@Data", val); + sqlCmd.Parameters.AddWithValue("@tid", taskUUID); if (sqlCmd.ExecuteNonQuery() == 1) { return true; } else - return false; + return false; } catch (Exception) { @@ -412,15 +357,15 @@ namespace iFileProxy.Services { string sql = @"UPDATE t_tasks_info set `status` = @status , update_time = Now() WHERE `tid` = @tid"; MySqlConnection conn = connection ?? GetAndOpenDBConn("iFileProxy_Db"); - + try { - using MySqlCommand sqlCmd = new (sql, conn); + using MySqlCommand sqlCmd = new(sql, conn); sqlCmd.Parameters.AddWithValue("@status", taskInfo.Status); sqlCmd.Parameters.AddWithValue("@tid", taskInfo.TaskId); if (sqlCmd.ExecuteNonQuery() >= 1) - { - _logger.Information($"Task: {taskInfo.TaskId} Status Change to {taskInfo.Status}"); + { + _logger.Information($"Task: {taskInfo.TaskId} Status Change to {taskInfo.Status}"); return true; } else @@ -433,8 +378,8 @@ namespace iFileProxy.Services throw; } finally - { - conn.Close (); + { + conn.Close(); } } @@ -449,11 +394,11 @@ namespace iFileProxy.Services /// /// public int DeleteTaskInfoByIpAddr(string ipAddr) - { + { try { string sql = $"DELETE FROM `t_tasks_info` WHERE `client_ip` = '{ipAddr}'"; - return Query(sql,DbConfigName.iFileProxy); + return Query(sql, DbConfigName.iFileProxy); } catch (Exception) { @@ -466,13 +411,13 @@ namespace iFileProxy.Services using var conn = GetAndOpenDBConn("iFileProxy_Db"); try { - // 创建表 - using (var cmd = new MySqlCommand(CREATE_TASKS_TABLE_SQL, conn)) - cmd.ExecuteNonQuery(); - using (var cmd = new MySqlCommand(CREATE_USERS_TABLE_SQL, conn)) - cmd.ExecuteNonQuery(); - using (var cmd = new MySqlCommand(CREATE_USER_EVENTS_TABLE_SQL, conn)) - cmd.ExecuteNonQuery(); + foreach (var sql in createDBSqls) + { + _logger.Information($"尝试创建 {sql.Key} 数据表...."); + using MySqlCommand sqlcmd = new (sql.Value,conn); + sqlcmd.ExecuteNonQuery(); + _logger.Information("Done."); + } _logger.Information("数据库表结构初始化完成"); } @@ -482,6 +427,13 @@ namespace iFileProxy.Services throw; } } + /// + /// 执行查询并获取单个指定类型的对象 + /// + /// + /// + /// + /// public T ExecuteScalar(string sql, Dictionary parameters) { using var conn = GetAndOpenDBConn("iFileProxy_Db"); @@ -494,6 +446,13 @@ namespace iFileProxy.Services return (T)cmd.ExecuteScalar(); } + /// + /// 执行SQL查询 + /// + /// 预期返回的数据类型 + /// SQL语句 + /// 参数列表 + /// 所指定类型的数据 public List ExecuteQuery(string sql, Dictionary parameters) { using var conn = GetAndOpenDBConn("iFileProxy_Db"); @@ -505,6 +464,12 @@ namespace iFileProxy.Services return JsonConvert.DeserializeObject>(QueryTableData(cmd)); } + /// + /// 执行SQL查询 + /// + /// SQL语句 + /// 参数列表 + /// 受影响的行数 public int ExecuteNonQuery(string sql, Dictionary parameters) { using var conn = GetAndOpenDBConn("iFileProxy_Db"); @@ -630,7 +595,7 @@ namespace iFileProxy.Services """; var result = new Dictionary(); - + // 初始化所有状态的计数为0 foreach (TaskState state in Enum.GetValues(typeof(TaskState))) { @@ -641,7 +606,7 @@ namespace iFileProxy.Services using var conn = GetAndOpenDBConn("iFileProxy_Db"); using var cmd = new MySqlCommand(sql, conn); using var reader = cmd.ExecuteReader(); - + while (reader.Read()) { var status = (TaskState)reader.GetInt32("status"); @@ -686,7 +651,7 @@ namespace iFileProxy.Services { var sql = @"INSERT INTO t_users (user_id, username, password_hash, mask, state, create_time, last_login_time, last_login_ip, nickname, email) VALUES (@userId, @username, @passwordHash, @mask, @state, @createTime, @lastLoginTime, @lastLoginIp, @nickname, @email)"; - + var parameters = new Dictionary { { "@userId", user.UserId }, @@ -709,7 +674,7 @@ namespace iFileProxy.Services { var sql = @"INSERT INTO t_user_events (event_id, user_id, event_type, event_time, event_ip, event_detail) VALUES (@eventId, @userId, @eventType, @eventTime, @eventIp, @eventDetail)"; - + var parameters = new Dictionary { { "@eventId", userEvent.EventId }, @@ -747,7 +712,7 @@ namespace iFileProxy.Services last_login_ip = @lastLoginIp, nickname = @nickname WHERE user_id = @userId"; - + var parameters = new Dictionary { { "@userId", user.UserId }, @@ -852,9 +817,9 @@ namespace iFileProxy.Services } public async Task> GetPagedUserEventsAsync( - int page, - int pageSize, - string? userId = null, + int page, + int pageSize, + string? userId = null, string? keyword = null) { // 构建基础SQL @@ -863,7 +828,7 @@ namespace iFileProxy.Services FROM t_user_events e LEFT JOIN t_users u ON e.user_id = u.user_id WHERE 1=1"); - + var parameters = new Dictionary(); // 添加用户ID过滤 @@ -993,7 +958,7 @@ namespace iFileProxy.Services (log_id, level, message, exception, properties, timestamp) VALUES (@logId, @level, @message, @exception, @properties, @timestamp)"; - + var parameters = new Dictionary { { "@logId", log.LogId }, @@ -1009,8 +974,8 @@ namespace iFileProxy.Services } public async Task> GetPagedSystemLogsAsync( - int page, - int pageSize, + int page, + int pageSize, LogEventLevel? level = null, string? keyword = null, DateTime? startTime = null, @@ -1086,7 +1051,7 @@ namespace iFileProxy.Services { var sql = @"INSERT INTO t_download_history (tid, time, client_ip) VALUES (@taskId, @time, @clientIp)"; - + var parameters = new Dictionary { { "@taskId", taskId }, diff --git a/src/Services/LocalCacheManager.cs b/src/Services/LocalCacheManager.cs index c13ee0e..c039792 100644 --- a/src/Services/LocalCacheManager.cs +++ b/src/Services/LocalCacheManager.cs @@ -32,7 +32,8 @@ namespace iFileProxy.Services CACHE_LIFETIME = _appConfig.DownloadOptions.CacheLifetime; _dbGateService = serviceProvider.GetRequiredService(); // 开始定时清理任务 - _timer = new Timer((obj) => { + _timer = new Timer((obj) => + { lock (_lock) { CheckAndCleanCache(); @@ -58,7 +59,7 @@ namespace iFileProxy.Services { using var cmd = new MySqlCommand(sql, conn); using var adapter = new MySqlDataAdapter(cmd); - var dataTable = new DataTable(); + using var dataTable = new DataTable(); adapter.Fill(dataTable); taskInfos = JsonConvert.DeserializeObject>( JsonConvert.SerializeObject(dataTable) @@ -69,6 +70,16 @@ namespace iFileProxy.Services { foreach (TaskInfo taskInfo in taskInfos) { + var dependencies = _dbGateService.CheckCacheDependencies(taskInfo.TaskId, taskInfo.ClientIp); + // 获取依赖和已经过期的任务ID交集 + var intersect = dependencies.Select(x => x.TaskId).Intersect(taskInfos.Select(x => x.TaskId)).ToList(); + // 若依赖该缓存的任务也处于过期状态 则不进行缓存依赖检查 + if (intersect.IndexOf(taskInfo.TaskId) == -1) + if (dependencies != null && dependencies.Count > 0) + { + _logger.Warning($"TaskId: {taskInfo.TaskId} 所产生的缓存文件正被 {string.Join(",", dependencies.Select(x => x.TaskId))} 所依赖, 不会继续执行清理任务"); + continue; + } string cacheFileName = Path.Combine(_appConfig.DownloadOptions.SavePath, taskInfo.FileName); if (File.Exists(cacheFileName)) { @@ -85,22 +96,20 @@ namespace iFileProxy.Services } // 更新数据库状态 - using (var conn = _dbGateService.GetAndOpenDBConn(DbConfigName.iFileProxy)) + using var conn = _dbGateService.GetAndOpenDBConn(DbConfigName.iFileProxy); + // 更新标签 + using (var cmd = new MySqlCommand( + "UPDATE t_tasks_info SET tag = @tag WHERE tid = @tid", + conn)) { - // 更新标签 - using (var cmd = new MySqlCommand( - "UPDATE t_tasks_info SET tag = @tag WHERE tid = @tid", - conn)) - { - cmd.Parameters.AddWithValue("@tag", "CLEANED"); - cmd.Parameters.AddWithValue("@tid", taskInfo.TaskId); - cmd.ExecuteNonQuery(); - } - - // 更新状态 - taskInfo.Status = TaskState.Cleaned; - _dbGateService.UpdateTaskStatus(taskInfo); + cmd.Parameters.AddWithValue("@tag", "CLEANED"); + cmd.Parameters.AddWithValue("@tid", taskInfo.TaskId); + cmd.ExecuteNonQuery(); } + + // 更新状态 + taskInfo.Status = TaskState.Cleaned; + _dbGateService.UpdateTaskStatus(taskInfo); } } }