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, "写入文件日志失败");
}
}
}
}