|
|
@@ -3,6 +3,11 @@ 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
|
|
|
{
|
|
|
@@ -14,25 +19,43 @@ namespace OASystem.API.Middlewares
|
|
|
private readonly RequestDelegate _next;
|
|
|
private readonly HttpClient _httpClient;
|
|
|
private readonly IConfiguration _config;
|
|
|
- private readonly SqlSugarClient _sqlSugar;
|
|
|
+ private readonly ILogger<RecordAPIOperationMiddleware> _logger;
|
|
|
+ private readonly IServiceProvider _serviceProvider;
|
|
|
|
|
|
/// <summary>
|
|
|
- /// 初始化DeleteUserId
|
|
|
+ /// 初始化
|
|
|
/// </summary>
|
|
|
- /// <param name="next"></param>
|
|
|
- /// <param name="config"></param>
|
|
|
- /// <param name="httpClient"></param>
|
|
|
- /// <param name="sqlSugar"></param>
|
|
|
- public RecordAPIOperationMiddleware(RequestDelegate next, IConfiguration config, HttpClient httpClient, SqlSugarClient sqlSugar)
|
|
|
+ public RecordAPIOperationMiddleware(
|
|
|
+ RequestDelegate next,
|
|
|
+ IConfiguration config,
|
|
|
+ IHttpClientFactory httpClientFactory,
|
|
|
+ ILogger<RecordAPIOperationMiddleware> logger,
|
|
|
+ IServiceProvider serviceProvider)
|
|
|
{
|
|
|
_next = next;
|
|
|
- _httpClient = httpClient;
|
|
|
_config = config;
|
|
|
- _sqlSugar = sqlSugar;
|
|
|
+ _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();
|
|
|
|
|
|
@@ -47,80 +70,95 @@ namespace OASystem.API.Middlewares
|
|
|
var startTime = DateTime.UtcNow;
|
|
|
int portType = 1, userId = 0, id = 0, status = 0;
|
|
|
string updatePreData = string.Empty, updateBefData = string.Empty;
|
|
|
- bool param5Bool = false, param6Bool = false;
|
|
|
-
|
|
|
- try
|
|
|
+
|
|
|
+ // 获取用户ID和其他参数
|
|
|
+ using (var scope = _serviceProvider.CreateScope())
|
|
|
{
|
|
|
- userId = await ReadToken(context);
|
|
|
+ var sqlSugar = scope.ServiceProvider.GetRequiredService<SqlSugarClient>();
|
|
|
|
|
|
- if (!string.IsNullOrEmpty(requestBodyText))
|
|
|
+ try
|
|
|
{
|
|
|
- object param1 = string.Empty, param2 = string.Empty, param3 = string.Empty, param4 = string.Empty,
|
|
|
- param5 = string.Empty, param6 = string.Empty, param7 = string.Empty, param8 = string.Empty;
|
|
|
- var requestBodyJson = JsonConvert.DeserializeObject<Dictionary<string, object>>(requestBodyText);
|
|
|
- bool exists1 = requestBodyJson.TryGetValue("portType", out param1);
|
|
|
- bool exists2 = requestBodyJson.TryGetValue("userId", out param2);
|
|
|
- bool exists3 = requestBodyJson.TryGetValue("currUserId", out param3);
|
|
|
- bool exists4 = requestBodyJson.TryGetValue("createUserId", out param4);
|
|
|
- bool exists5 = requestBodyJson.TryGetValue("id", out param5);
|
|
|
- bool exists6 = requestBodyJson.TryGetValue("status", out param6);
|
|
|
- bool exists7 = requestBodyJson.TryGetValue("operationUserId", out param7);
|
|
|
- bool exists8 = requestBodyJson.TryGetValue("deleteUserId", out param8);
|
|
|
-
|
|
|
- if (exists1) int.TryParse(param1.ToString(), out portType);
|
|
|
-
|
|
|
- //用户Id处理
|
|
|
- if (userId < 1)
|
|
|
+ userId = await ReadToken(context);
|
|
|
+
|
|
|
+ if (!string.IsNullOrEmpty(requestBodyText))
|
|
|
{
|
|
|
- if (apiLogAttribute.OperationEnum == OperationEnum.Login)
|
|
|
+ var requestBodyJson = JsonConvert.DeserializeObject<Dictionary<string, object>>(requestBodyText);
|
|
|
+
|
|
|
+ if (requestBodyJson != null)
|
|
|
{
|
|
|
- var number = requestBodyJson?["number"].ToString();
|
|
|
- if (!string.IsNullOrEmpty(number))
|
|
|
+ // 提取参数
|
|
|
+ if (requestBodyJson.TryGetValue("portType", out var param1Obj) && param1Obj != null)
|
|
|
{
|
|
|
- var info = await _sqlSugar.Queryable<Sys_Users>().Where(x => x.IsDel == 0 && x.Number.Equals(number)).FirstAsync();
|
|
|
+ int.TryParse(param1Obj.ToString(), out portType);
|
|
|
+ }
|
|
|
|
|
|
- userId = info?.Id ?? 0;
|
|
|
+ if (requestBodyJson.TryGetValue("id", out var param5Obj) && param5Obj != null)
|
|
|
+ {
|
|
|
+ int.TryParse(param5Obj.ToString(), out id);
|
|
|
}
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- if (exists2) int.TryParse(param2.ToString(), out userId);
|
|
|
- else if (exists3) int.TryParse(param3.ToString(), out userId);
|
|
|
- else if (exists4) int.TryParse(param4.ToString(), out userId);
|
|
|
- else if (exists7) int.TryParse(param7.ToString(), out userId);
|
|
|
- else if (exists8) int.TryParse(param8.ToString(), out userId);
|
|
|
- }
|
|
|
- }
|
|
|
- //编辑前数据查询;
|
|
|
- if (exists5) param5Bool = int.TryParse(param5.ToString(), out id);
|
|
|
- if (exists6) param6Bool = int.TryParse(param6.ToString(), out status);
|
|
|
|
|
|
- if (param6Bool)
|
|
|
- {
|
|
|
- // 2 修改
|
|
|
- if (status == 1) apiLogAttribute.OperationEnum = OperationEnum.Add;
|
|
|
- else if (status == 2)
|
|
|
- {
|
|
|
- apiLogAttribute.OperationEnum = OperationEnum.Edit;
|
|
|
- updatePreData = await TableInfoToJson(apiLogAttribute.TableName, id);
|
|
|
- }
|
|
|
- }
|
|
|
- else if (param5Bool)
|
|
|
- {
|
|
|
- if (apiLogAttribute.OperationEnum != OperationEnum.Del)
|
|
|
- {
|
|
|
- // id > 0 修改
|
|
|
- if (id < 1) apiLogAttribute.OperationEnum = OperationEnum.Add;
|
|
|
- else if (id > 0)
|
|
|
+ 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<Sys_Users>()
|
|
|
+ .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(apiLogAttribute.TableName, id);
|
|
|
+ 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, "解析请求参数时发生错误");
|
|
|
+ }
|
|
|
}
|
|
|
- catch (JsonException) { }
|
|
|
|
|
|
// 保存原始响应体流
|
|
|
var originalResponseBody = context.Response.Body;
|
|
|
@@ -141,95 +179,92 @@ namespace OASystem.API.Middlewares
|
|
|
// 将响应体内容写回原始响应体流
|
|
|
await responseMemoryStream.CopyToAsync(originalResponseBody);
|
|
|
|
|
|
- //修改后数据查询
|
|
|
- if (param6Bool)
|
|
|
- {
|
|
|
- // 2 修改
|
|
|
- if (status == 2) updateBefData = await TableInfoToJson(apiLogAttribute.TableName, id);
|
|
|
- }
|
|
|
- else if (param5Bool)
|
|
|
+ // 修改后数据查询
|
|
|
+ if (status == 2 && id > 0)
|
|
|
{
|
|
|
-
|
|
|
- if (apiLogAttribute.OperationEnum != OperationEnum.Del)
|
|
|
+ using (var scope = _serviceProvider.CreateScope())
|
|
|
{
|
|
|
- // id > 0 修改
|
|
|
- if (id > 0) updateBefData = await TableInfoToJson(apiLogAttribute.TableName, id);
|
|
|
+ var sqlSugar = scope.ServiceProvider.GetRequiredService<SqlSugarClient>();
|
|
|
+ updateBefData = await TableInfoToJson(sqlSugar, apiLogAttribute.TableName, id);
|
|
|
}
|
|
|
}
|
|
|
- string remoteIp = string.Empty,
|
|
|
- location = string.Empty;
|
|
|
-
|
|
|
- // 检查请求头中的X-Forwarded-For,以获取真实的客户端IP地址
|
|
|
- if (context.Request.Headers.ContainsKey("X-Forwarded-For"))
|
|
|
- {
|
|
|
- remoteIp = context.Request.Headers["X-Forwarded-For"].ToString().Split(',', StringSplitOptions.RemoveEmptyEntries)[0];
|
|
|
- //_logger.LogInformation($"IP:{remoteIp}");
|
|
|
- }
|
|
|
- else
|
|
|
+ else if (apiLogAttribute.OperationEnum == OperationEnum.Edit && id > 0)
|
|
|
{
|
|
|
- remoteIp = context.Connection.RemoteIpAddress?.ToString();
|
|
|
+ using (var scope = _serviceProvider.CreateScope())
|
|
|
+ {
|
|
|
+ var sqlSugar = scope.ServiceProvider.GetRequiredService<SqlSugarClient>();
|
|
|
+ updateBefData = await TableInfoToJson(sqlSugar, apiLogAttribute.TableName, id);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- (remoteIp, location) = await GetIpInfo(remoteIp);
|
|
|
- //remoteIp = await GetExternalIp();
|
|
|
- //location = await GetIpLocation(remoteIp);
|
|
|
-
|
|
|
- string deviceType = string.Empty, browser = string.Empty, os = string.Empty;
|
|
|
- var userAgent = context.Request.Headers["User-Agent"].FirstOrDefault();
|
|
|
- if (!string.IsNullOrEmpty(userAgent))
|
|
|
- {
|
|
|
- // 解析User-Agent头
|
|
|
- var parser = Parser.GetDefault();
|
|
|
- var client = parser.Parse(userAgent);
|
|
|
-
|
|
|
- // 提取浏览器信息
|
|
|
- browser = client.UA.Family; // 浏览器名称
|
|
|
- var browserVersion = client.UA.Major + "." + client.UA.Minor + "." + client.UA.Patch; // 浏览器版本
|
|
|
- browser += $"({browserVersion})";
|
|
|
-
|
|
|
- // 提取操作系统信息
|
|
|
- os = client.OS.Family; // 操作系统名称
|
|
|
-
|
|
|
- var osVersion = string.Empty; // 操作系统版本
|
|
|
- if (!string.IsNullOrEmpty(client.OS.Major)) osVersion += client.OS.Major;
|
|
|
- if (!string.IsNullOrEmpty(client.OS.Minor)) osVersion += "." + client.OS.Minor;
|
|
|
- if (!string.IsNullOrEmpty(client.OS.Patch)) osVersion += "." + client.OS.Patch;
|
|
|
-
|
|
|
- if (!string.IsNullOrEmpty(osVersion)) os += $"({osVersion})";
|
|
|
-
|
|
|
- // 提取设备信息
|
|
|
- deviceType = client.Device.Family; // 设备类型,如 'mobile', 'tablet', 'desktop' 等
|
|
|
-
|
|
|
- }
|
|
|
+ // 获取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,
|
|
|
+ RequestUrl = context.Request.Path + context.Request.QueryString,
|
|
|
RemoteIp = remoteIp,
|
|
|
Location = location,
|
|
|
- RequestParam = !string.IsNullOrEmpty(requestBodyText) ? JsonConvert.SerializeObject(requestBodyText) : null,
|
|
|
- ReturnResult = !string.IsNullOrEmpty(responseBodyText) ? JsonConvert.SerializeObject(responseBodyText) : null,
|
|
|
+ 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 = updatePreData,
|
|
|
- UpdateBefData = updateBefData,
|
|
|
+ UpdatePreData = truncatedPreData,
|
|
|
+ UpdateBefData = truncatedBefData,
|
|
|
Browser = browser,
|
|
|
Os = os,
|
|
|
DeviceType = deviceType,
|
|
|
+ CreateTime = DateTime.Now
|
|
|
};
|
|
|
|
|
|
- // 存储到数据库
|
|
|
- await _sqlSugar.Insertable(logInfo).ExecuteCommandAsync();
|
|
|
+ // 异步记录日志,不阻塞响应
|
|
|
+ _ = Task.Run(async () =>
|
|
|
+ {
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // 在异步任务中创建新的作用域
|
|
|
+ using var logScope = _serviceProvider.CreateScope();
|
|
|
+ var logSqlSugar = logScope.ServiceProvider.GetRequiredService<SqlSugarClient>();
|
|
|
+
|
|
|
+ // 设置较短的超时时间
|
|
|
+ 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
|
|
|
{
|
|
|
@@ -237,119 +272,386 @@ namespace OASystem.API.Middlewares
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private async Task<string> TableInfoToJson(string tableName, int id)
|
|
|
+ /// <summary>
|
|
|
+ /// 从请求参数中解析用户ID
|
|
|
+ /// </summary>
|
|
|
+ private int ParseUserIdFromParams(Dictionary<string, object> requestBodyJson)
|
|
|
{
|
|
|
- if (_sqlSugar.DbMaintenance.IsAnyTable(tableName))
|
|
|
+ var userIdKeys = new[] { "userId", "currUserId", "createUserId", "operationUserId", "deleteUserId" };
|
|
|
+
|
|
|
+ foreach (var key in userIdKeys)
|
|
|
{
|
|
|
- string jsonLabel = string.Empty;
|
|
|
- if (tableName.Equals("Crm_NewClientData"))
|
|
|
+ if (requestBodyJson.TryGetValue(key, out var value) && value != null)
|
|
|
{
|
|
|
- var info = await _sqlSugar.Queryable<Crm_NewClientData>().FirstAsync(x => x.Id == id);
|
|
|
- if (info != null)
|
|
|
+ if (int.TryParse(value.ToString(), out var parsedUserId) && parsedUserId > 0)
|
|
|
{
|
|
|
- EncryptionProcessor.DecryptProperties(info);
|
|
|
- if (info != null) jsonLabel = JsonConvert.SerializeObject(info);
|
|
|
+ return parsedUserId;
|
|
|
}
|
|
|
}
|
|
|
- else
|
|
|
+ }
|
|
|
+
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 获取表数据JSON
|
|
|
+ /// </summary>
|
|
|
+ private async Task<string> TableInfoToJson(SqlSugarClient sqlSugar, string tableName, int id)
|
|
|
+ {
|
|
|
+ if (sqlSugar.DbMaintenance.IsAnyTable(tableName))
|
|
|
+ {
|
|
|
+ try
|
|
|
{
|
|
|
- var sql = string.Format(" Select * From {0} Where Id={1}", tableName, id);
|
|
|
+ string jsonLabel = string.Empty;
|
|
|
+ if (tableName.Equals("Crm_NewClientData"))
|
|
|
+ {
|
|
|
+ var info = await sqlSugar.Queryable<Crm_NewClientData>()
|
|
|
+ .Where(x => x.Id == id)
|
|
|
+ .FirstAsync();
|
|
|
|
|
|
- var info = await _sqlSugar.SqlQueryable<dynamic>(sql).FirstAsync();
|
|
|
- if (info != null) jsonLabel = JsonConvert.SerializeObject(info);
|
|
|
+ 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<dynamic>(sql).FirstAsync();
|
|
|
+ if (info != null)
|
|
|
+ jsonLabel = JsonConvert.SerializeObject(info);
|
|
|
+ }
|
|
|
+ return jsonLabel;
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
+ {
|
|
|
+ _logger.LogError(ex, $"获取表数据失败: {tableName}, Id: {id}");
|
|
|
+ return string.Empty;
|
|
|
}
|
|
|
- return jsonLabel;
|
|
|
}
|
|
|
-
|
|
|
return string.Empty;
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// 是否是静态文件请求
|
|
|
+ /// </summary>
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 读取请求体
|
|
|
+ /// </summary>
|
|
|
private async Task<string> ReadRequestBody(HttpRequest request)
|
|
|
{
|
|
|
- request.EnableBuffering();
|
|
|
-
|
|
|
- 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;
|
|
|
+ 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;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// 读取响应体
|
|
|
+ /// </summary>
|
|
|
private async Task<string> ReadResponseBody(Stream stream)
|
|
|
{
|
|
|
- using var reader = new StreamReader(
|
|
|
- stream,
|
|
|
- Encoding.UTF8,
|
|
|
- detectEncodingFromByteOrderMarks: false,
|
|
|
- bufferSize: 8192,
|
|
|
- leaveOpen: true
|
|
|
- );
|
|
|
-
|
|
|
- var body = await reader.ReadToEndAsync();
|
|
|
- stream.Position = 0;
|
|
|
- return body;
|
|
|
+ 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;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// 读取Token
|
|
|
+ /// </summary>
|
|
|
private async Task<int> ReadToken(HttpContext context)
|
|
|
{
|
|
|
- var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
|
|
|
|
|
|
- // 检查Authorization头是否存在且以"Bearer "开头
|
|
|
- if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
|
|
|
+ if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
|
|
|
+ {
|
|
|
+ var authInfo = JwtHelper.SerializeJwt(authHeader);
|
|
|
+ if (authInfo != null)
|
|
|
+ return authInfo.UserId;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (Exception ex)
|
|
|
{
|
|
|
- var authInfo = JwtHelper.SerializeJwt(authHeader);
|
|
|
- if (authInfo != null) return authInfo.UserId;
|
|
|
+ _logger.LogError(ex, "读取Token失败");
|
|
|
}
|
|
|
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
- /// 获取IP信息
|
|
|
+ /// 获取客户端IP
|
|
|
/// </summary>
|
|
|
- /// <param name="ip">ipv4 or ipv6</param>
|
|
|
- /// <returns></returns>
|
|
|
- private async Task<(string ip, string local)> GetIpInfo(string ip)
|
|
|
+ private string GetClientIp(HttpContext context)
|
|
|
{
|
|
|
- string local = string.Empty;
|
|
|
-
|
|
|
- //if (IPAddress.TryParse(ip,out _))
|
|
|
- //{
|
|
|
- // var response = await _httpClient.GetAsync($"https://api.vore.top/api/IPdata?ip={ip}");
|
|
|
-
|
|
|
- // response.EnsureSuccessStatusCode();
|
|
|
- // var json = await response.Content.ReadAsStringAsync();
|
|
|
- // var ipInfo = Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(json);
|
|
|
- // if (ipInfo.code == 200)
|
|
|
- // {
|
|
|
- // ip = ipInfo.ipinfo.text;
|
|
|
- // local = $"{ipInfo.adcode.o}";
|
|
|
- // }
|
|
|
- //}
|
|
|
-
|
|
|
- return (ip, local);
|
|
|
+ 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";
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- private async Task<string> GetExternalIp()
|
|
|
+ /// <summary>
|
|
|
+ /// 安全获取IP位置
|
|
|
+ /// </summary>
|
|
|
+ private async Task<string> GetIpLocationSafe(string ip)
|
|
|
{
|
|
|
- var response = await _httpClient.GetAsync("https://ifconfig.me");
|
|
|
- response.EnsureSuccessStatusCode();
|
|
|
- return await response.Content.ReadAsStringAsync();
|
|
|
+ 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 "未知";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 检查是否是内网IP
|
|
|
+ /// </summary>
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 获取设备信息
|
|
|
+ /// </summary>
|
|
|
+ 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<string>();
|
|
|
+ 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<string>();
|
|
|
+ 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 async Task<string> GetIpLocation(string ip)
|
|
|
+ /// <summary>
|
|
|
+ /// 截断字符串
|
|
|
+ /// </summary>
|
|
|
+ private string TruncateString(string input, int maxLength)
|
|
|
{
|
|
|
- var response = await _httpClient.GetAsync($"https://ipinfo.io/{ip}/json?&language=zh-CN");
|
|
|
- response.EnsureSuccessStatusCode();
|
|
|
- var json = await response.Content.ReadAsStringAsync();
|
|
|
- var ipInfo = Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(json);
|
|
|
- return $"{ipInfo.country}, {ipInfo.city}";
|
|
|
+ if (string.IsNullOrEmpty(input) || input.Length <= maxLength)
|
|
|
+ return input;
|
|
|
+
|
|
|
+ return input.Substring(0, maxLength) + "...[已截断]";
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// 将日志写入文件
|
|
|
+ /// </summary>
|
|
|
+ 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, "写入文件日志失败");
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
-}
|
|
|
+}
|