using OASystem.Domain.AesEncryption; using OASystem.Domain.Attributes; using OASystem.Domain.Entities.Customer; using UAParser; using static OASystem.API.OAMethodLib.JWTHelper; using System.Text; using Microsoft.Extensions.Logging; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace OASystem.API.Middlewares { /// /// 指定API操作记录信息 /// public class RecordAPIOperationMiddleware { private readonly RequestDelegate _next; private readonly HttpClient _httpClient; private readonly IConfiguration _config; private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; /// /// 初始化 /// public RecordAPIOperationMiddleware( RequestDelegate next, IConfiguration config, IHttpClientFactory httpClientFactory, ILogger logger, IServiceProvider serviceProvider) { _next = next; _config = config; _logger = logger; _serviceProvider = serviceProvider; _httpClient = httpClientFactory.CreateClient(); _httpClient.Timeout = TimeSpan.FromSeconds(5); } public async Task InvokeAsync(HttpContext context) { // 跳过 OPTIONS 请求(CORS 预检请求) if (context.Request.Method.Equals(HttpMethods.Options, StringComparison.OrdinalIgnoreCase)) { await _next(context); return; } // 跳过静态文件请求 if (IsStaticFileRequest(context.Request.Path)) { await _next(context); return; } // 启用请求体流的缓冲,允许多次读取 context.Request.EnableBuffering(); // 读取请求体内容 var requestBodyText = await ReadRequestBody(context.Request); // 检查控制器方法是否使用了自定义属性 var endpoint = context.GetEndpoint(); var apiLogAttribute = endpoint?.Metadata?.GetMetadata(); if (apiLogAttribute != null) { var startTime = DateTime.UtcNow; int portType = 1, userId = 0, id = 0, status = 0; string updatePreData = string.Empty, updateBefData = string.Empty; // 获取用户ID和其他参数 using (var scope = _serviceProvider.CreateScope()) { var sqlSugar = scope.ServiceProvider.GetRequiredService(); try { userId = await ReadToken(context); if (!string.IsNullOrEmpty(requestBodyText)) { var requestBodyJson = JsonConvert.DeserializeObject>(requestBodyText); if (requestBodyJson != null) { // 提取参数 if (requestBodyJson.TryGetValue("portType", out var param1Obj) && param1Obj != null) { int.TryParse(param1Obj.ToString(), out portType); } if (requestBodyJson.TryGetValue("id", out var param5Obj) && param5Obj != null) { int.TryParse(param5Obj.ToString(), out id); } if (requestBodyJson.TryGetValue("status", out var param6Obj) && param6Obj != null) { int.TryParse(param6Obj.ToString(), out status); } // 用户Id处理 if (userId < 1) { if (apiLogAttribute.OperationEnum == OperationEnum.Login) { var number = requestBodyJson.TryGetValue("number", out var numberObj) ? numberObj?.ToString() : null; if (!string.IsNullOrEmpty(number)) { var info = await sqlSugar.Queryable() .Where(x => x.IsDel == 0 && x.Number.Equals(number)) .FirstAsync(); userId = info?.Id ?? 0; } } else { userId = ParseUserIdFromParams(requestBodyJson); } } // 根据status判断操作类型 if (status > 0) { if (status == 1) apiLogAttribute.OperationEnum = OperationEnum.Add; else if (status == 2) { apiLogAttribute.OperationEnum = OperationEnum.Edit; if (id > 0) { updatePreData = await TableInfoToJson(sqlSugar, apiLogAttribute.TableName, id); } } } // 根据id判断操作类型 else if (id > 0 && apiLogAttribute.OperationEnum != OperationEnum.Del) { apiLogAttribute.OperationEnum = OperationEnum.Edit; updatePreData = await TableInfoToJson(sqlSugar, apiLogAttribute.TableName, id); } else if (apiLogAttribute.OperationEnum != OperationEnum.Del && id < 1) { apiLogAttribute.OperationEnum = OperationEnum.Add; } } } } catch (JsonException) { _logger.LogDebug("JSON解析失败,可能请求体不是JSON格式"); } catch (Exception ex) { _logger.LogError(ex, "解析请求参数时发生错误"); } } // 保存原始响应体流 var originalResponseBody = context.Response.Body; // 创建一个新的内存流来捕获响应体 using var responseMemoryStream = new MemoryStream(); context.Response.Body = responseMemoryStream; // 调用下一个中间件 await _next(context); // 重置响应体流的位置 responseMemoryStream.Position = 0; // 读取响应体内容 var responseBodyText = await ReadResponseBody(responseMemoryStream); // 将响应体内容写回原始响应体流 await responseMemoryStream.CopyToAsync(originalResponseBody); // 修改后数据查询 if (status == 2 && id > 0) { using (var scope = _serviceProvider.CreateScope()) { var sqlSugar = scope.ServiceProvider.GetRequiredService(); updateBefData = await TableInfoToJson(sqlSugar, apiLogAttribute.TableName, id); } } else if (apiLogAttribute.OperationEnum == OperationEnum.Edit && id > 0) { using (var scope = _serviceProvider.CreateScope()) { var sqlSugar = scope.ServiceProvider.GetRequiredService(); updateBefData = await TableInfoToJson(sqlSugar, apiLogAttribute.TableName, id); } } // 获取IP信息和设备信息 string remoteIp = GetClientIp(context); string location = await GetIpLocationSafe(remoteIp); var (deviceType, browser, os) = GetDeviceInfo(context); // 记录请求结束时间 var endTime = DateTime.UtcNow; // 计算耗时 var duration = (long)(endTime - startTime).TotalMilliseconds; // 截断过长的文本 var truncatedRequestBody = TruncateString(requestBodyText, 4000); var truncatedResponseBody = TruncateString(responseBodyText, 4000); var truncatedPreData = TruncateString(updatePreData, 4000); var truncatedBefData = TruncateString(updateBefData, 4000); var logInfo = new Crm_TableOperationRecord() { TableName = apiLogAttribute.TableName, PortType = portType, OperationItem = apiLogAttribute.OperationEnum, DataId = id, RequestUrl = context.Request.Path + context.Request.QueryString, RemoteIp = remoteIp, Location = location, RequestParam = !string.IsNullOrEmpty(truncatedRequestBody) ? JsonConvert.SerializeObject(truncatedRequestBody) : null, ReturnResult = !string.IsNullOrEmpty(truncatedResponseBody) ? JsonConvert.SerializeObject(truncatedResponseBody) : null, Elapsed = duration, Status = context.Response.StatusCode.ToString(), CreateUserId = userId, UpdatePreData = truncatedPreData, UpdateBefData = truncatedBefData, Browser = browser, Os = os, DeviceType = deviceType, CreateTime = DateTime.Now }; // 异步记录日志,不阻塞响应 _ = Task.Run(async () => { try { // 在异步任务中创建新的作用域 using var logScope = _serviceProvider.CreateScope(); var logSqlSugar = logScope.ServiceProvider.GetRequiredService(); // 设置较短的超时时间 logSqlSugar.Ado.CommandTimeOut = 3; // 存储到数据库 await logSqlSugar.Insertable(logInfo).ExecuteCommandAsync(); } catch (Exception ex) { _logger.LogError(ex, "异步记录操作日志失败"); // 降级:写入文件日志 try { await WriteLogToFile(apiLogAttribute, requestBodyText, responseBodyText, userId, ex.Message); } catch (Exception fileEx) { _logger.LogError(fileEx, "写入文件日志失败"); } } }); } else { await _next(context); } } /// /// 从请求参数中解析用户ID /// private int ParseUserIdFromParams(Dictionary requestBodyJson) { var userIdKeys = new[] { "userId", "currUserId", "createUserId", "operationUserId", "deleteUserId" }; foreach (var key in userIdKeys) { if (requestBodyJson.TryGetValue(key, out var value) && value != null) { if (int.TryParse(value.ToString(), out var parsedUserId) && parsedUserId > 0) { return parsedUserId; } } } return 0; } /// /// 获取表数据JSON /// private async Task TableInfoToJson(SqlSugarClient sqlSugar, string tableName, int id) { if (sqlSugar.DbMaintenance.IsAnyTable(tableName)) { try { string jsonLabel = string.Empty; if (tableName.Equals("Crm_NewClientData")) { var info = await sqlSugar.Queryable() .Where(x => x.Id == id) .FirstAsync(); if (info != null) { EncryptionProcessor.DecryptProperties(info); if (info != null) jsonLabel = JsonConvert.SerializeObject(info); } } else { var sql = $"SELECT * FROM {tableName} WHERE Id = {id}"; var info = await sqlSugar.SqlQueryable(sql).FirstAsync(); if (info != null) jsonLabel = JsonConvert.SerializeObject(info); } return jsonLabel; } catch (Exception ex) { _logger.LogError(ex, $"获取表数据失败: {tableName}, Id: {id}"); return string.Empty; } } return string.Empty; } /// /// 是否是静态文件请求 /// private bool IsStaticFileRequest(PathString path) { try { var staticExtensions = new[] { ".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".ico", ".svg", ".woff", ".woff2", ".ttf", ".eot", ".mp4", ".mp3", ".avi", ".mov", ".wmv", ".flv", ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".zip", ".rar", ".7z", ".tar", ".gz" }; var pathString = path.Value?.ToLower() ?? string.Empty; return staticExtensions.Any(ext => pathString.EndsWith(ext.ToLower(), StringComparison.OrdinalIgnoreCase)); } catch { return false; } } /// /// 读取请求体 /// private async Task ReadRequestBody(HttpRequest request) { try { request.Body.Position = 0; using var reader = new StreamReader( request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 8192, leaveOpen: true ); var body = await reader.ReadToEndAsync(); request.Body.Position = 0; return body; } catch (Exception ex) { _logger.LogError(ex, "读取请求体失败"); return string.Empty; } } /// /// 读取响应体 /// private async Task ReadResponseBody(Stream stream) { try { using var reader = new StreamReader( stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 8192, leaveOpen: true ); var body = await reader.ReadToEndAsync(); stream.Position = 0; return body; } catch (Exception ex) { _logger.LogError(ex, "读取响应体失败"); return string.Empty; } } /// /// 读取Token /// private async Task ReadToken(HttpContext context) { try { var authHeader = context.Request.Headers["Authorization"].FirstOrDefault(); if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) { var authInfo = JwtHelper.SerializeJwt(authHeader); if (authInfo != null) return authInfo.UserId; } } catch (Exception ex) { _logger.LogError(ex, "读取Token失败"); } return 0; } /// /// 获取客户端IP /// private string GetClientIp(HttpContext context) { try { // 优先从X-Forwarded-For获取 if (context.Request.Headers.ContainsKey("X-Forwarded-For")) { var xForwardedFor = context.Request.Headers["X-Forwarded-For"].ToString(); if (!string.IsNullOrEmpty(xForwardedFor)) { var ips = xForwardedFor.Split(',', StringSplitOptions.RemoveEmptyEntries); if (ips.Length > 0) return ips[0].Trim(); } } // 其次从X-Real-IP获取 if (context.Request.Headers.ContainsKey("X-Real-IP")) { var xRealIp = context.Request.Headers["X-Real-IP"].ToString(); if (!string.IsNullOrEmpty(xRealIp)) return xRealIp.Trim(); } // 最后从RemoteIpAddress获取 var remoteIp = context.Connection.RemoteIpAddress?.ToString(); if (!string.IsNullOrEmpty(remoteIp)) { // 处理IPv6映射的IPv4地址 if (remoteIp.StartsWith("::ffff:")) return remoteIp.Substring(7); return remoteIp; } return "Unknown"; } catch { return "Unknown"; } } /// /// 安全获取IP位置 /// private async Task GetIpLocationSafe(string ip) { if (string.IsNullOrWhiteSpace(ip) || ip == "Unknown" || ip == "::1" || ip == "127.0.0.1") return "本地"; try { // IPv6地址 if (ip.Contains(":")) return "IPv6"; // 检查是否是内网地址 if (IsPrivateIp(ip)) return "内网"; // 可以在这里调用IP查询API // 但为了性能,暂时返回未知 return "未知"; } catch (Exception ex) { _logger.LogWarning(ex, $"获取IP位置失败: {ip}"); return "未知"; } } /// /// 检查是否是内网IP /// private bool IsPrivateIp(string ip) { try { if (string.IsNullOrWhiteSpace(ip)) return false; // 常见的私有IP地址段 if (ip.StartsWith("10.") || ip.StartsWith("192.168.") || ip.StartsWith("172.16.") || ip.StartsWith("172.17.") || ip.StartsWith("172.18.") || ip.StartsWith("172.19.") || ip.StartsWith("172.20.") || ip.StartsWith("172.21.") || ip.StartsWith("172.22.") || ip.StartsWith("172.23.") || ip.StartsWith("172.24.") || ip.StartsWith("172.25.") || ip.StartsWith("172.26.") || ip.StartsWith("172.27.") || ip.StartsWith("172.28.") || ip.StartsWith("172.29.") || ip.StartsWith("172.30.") || ip.StartsWith("172.31.")) { return true; } return false; } catch { return false; } } /// /// 获取设备信息 /// private (string deviceType, string browser, string os) GetDeviceInfo(HttpContext context) { try { var userAgent = context.Request.Headers["User-Agent"].FirstOrDefault(); if (string.IsNullOrEmpty(userAgent)) return ("Unknown", "Unknown", "Unknown"); var parser = Parser.GetDefault(); var client = parser.Parse(userAgent); // 提取浏览器信息 var browser = client.UA.Family; var browserVersion = new List(); if (!string.IsNullOrEmpty(client.UA.Major)) browserVersion.Add(client.UA.Major); if (!string.IsNullOrEmpty(client.UA.Minor)) browserVersion.Add(client.UA.Minor); if (!string.IsNullOrEmpty(client.UA.Patch)) browserVersion.Add(client.UA.Patch); if (browserVersion.Any()) browser += " " + string.Join(".", browserVersion); // 提取操作系统信息 var os = client.OS.Family; var osVersion = new List(); if (!string.IsNullOrEmpty(client.OS.Major)) osVersion.Add(client.OS.Major); if (!string.IsNullOrEmpty(client.OS.Minor)) osVersion.Add(client.OS.Minor); if (!string.IsNullOrEmpty(client.OS.Patch)) osVersion.Add(client.OS.Patch); if (!string.IsNullOrEmpty(client.OS.PatchMinor)) osVersion.Add(client.OS.PatchMinor); if (osVersion.Any()) os += " " + string.Join(".", osVersion); // 提取设备信息 var deviceType = client.Device.Family; if (string.Equals(deviceType, "Other", StringComparison.OrdinalIgnoreCase)) { // 根据userAgent判断设备类型 var ua = userAgent.ToLower(); if (ua.Contains("mobile") || ua.Contains("android") || ua.Contains("iphone") || ua.Contains("ipad")) deviceType = "Mobile"; else deviceType = "Desktop"; } return (deviceType, browser, os); } catch (Exception ex) { _logger.LogDebug(ex, "解析设备信息失败"); return ("Unknown", "Unknown", "Unknown"); } } /// /// 截断字符串 /// private string TruncateString(string input, int maxLength) { if (string.IsNullOrEmpty(input) || input.Length <= maxLength) return input; return input.Substring(0, maxLength) + "...[已截断]"; } /// /// 将日志写入文件 /// private async Task WriteLogToFile(ApiLogAttribute apiLogAttribute, string requestBody, string responseBody, int userId, string error) { try { var logDir = Path.Combine(Directory.GetCurrentDirectory(), "Logs", "OperationLogs"); Directory.CreateDirectory(logDir); var logFile = Path.Combine(logDir, $"operation_fallback_{DateTime.Now:yyyyMMdd}.log"); var logEntry = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} | " + $"Table: {apiLogAttribute.TableName} | " + $"Operation: {apiLogAttribute.OperationEnum} | " + $"User: {userId} | " + $"Request: {TruncateString(requestBody, 500)} | " + $"Response: {TruncateString(responseBody, 500)} | " + $"Error: {error}" + Environment.NewLine; await File.AppendAllTextAsync(logFile, logEntry); } catch (Exception ex) { _logger.LogError(ex, "写入文件日志失败"); } } } }