using Microsoft.Data.SqlClient;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace OASystem.API.Middlewares
{
///
/// 全局异常捕获中间件
///
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next; // 用来处理上下文请求
private readonly ILogger _logger;
///
/// 初始化
///
///
///
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger)
{
_next = next;
_logger = logger;
}
///
/// 执行中间件
///
///
///
///
public async Task InvokeAsync(HttpContext httpContext, SqlSugarClient db)
{
try
{
await _next(httpContext); //要么在中间件中处理,要么被传递到下一个中间件中去
}
catch (Exception ex)
{
await HandleExceptionAsync(httpContext, ex, db); // 捕获异常了 在HandleExceptionAsync中处理
}
}
///
/// 自定义全局异常处理方法
///
/// HTTP上下文
/// 捕获的异常
/// SqlSugar客户端
///
private async Task HandleExceptionAsync(HttpContext context, Exception exception, SqlSugarClient db)
{
// ========== 1. 事务回滚(恢复并优化) ==========
try
{
if (db?.Ado?.Transaction != null)
{
db.Ado.RollbackTran();
_logger.LogWarning("Transaction rolled back due to exception: {ExceptionType}", exception.GetType().Name);
}
}
catch (Exception rollbackEx)
{
_logger.LogError(rollbackEx, "Failed to rollback transaction");
}
// ========== 2. 响应已开始则仅记录日志 ==========
if (context.Response.HasStarted)
{
_logger.LogError(exception, "Exception occurred after response started | RequestId: {RequestId}",
context.TraceIdentifier); // 记录请求ID
return;
}
// ========== 3. 构建标准化响应 ==========
context.Response.ContentType = "application/json; charset=utf-8";
var (statusCode, message, details, logLevel) = exception switch
{
// 业务异常:返回自定义信息,日志级别为Warning(非错误)
BusinessException businessEx => (
businessEx.Code,
businessEx.Msg,
businessEx.Data,
LogLevel.Warning
),
// 权限/资源异常:标准化提示,日志Warning
UnauthorizedAccessException => (403, "无权访问此资源", null, LogLevel.Warning),
KeyNotFoundException => (404, "请求的资源不存在", null, LogLevel.Warning),
// 数据库超时:隐藏敏感信息,日志Error
SqlException sqlEx when IsDbTimeoutException(sqlEx) => (
503,
"数据库连接超时,请稍后重试。",
null,
LogLevel.Error
),
SqlSugarException sugarEx when IsSugarTimeoutException(sugarEx) => (
503,
"数据库连接超时,请稍后重试。",
null,
LogLevel.Error
),
// 其他数据库异常:隐藏原始消息,日志Error
SqlException => (500, "数据库服务异常", null, LogLevel.Error),
SqlSugarException => (500, "数据库服务异常", null, LogLevel.Error),
// 应用程序异常(业务逻辑错误):日志Warning
ApplicationException appEx => (400, appEx.Message, null, LogLevel.Warning),
// 兜底系统异常:隐藏原始消息,日志Critical
_ => (500, "服务器内部错误", null, LogLevel.Critical)
};
// ========== 4. 设置HTTP状态码(关键优化) ==========
context.Response.StatusCode = 200;
// ========== 5. 分级记录日志(含上下文) ==========
var logMessage = new Dictionary
{
["RequestId"] = context.TraceIdentifier,
["Path"] = context.Request.Path,
["Method"] = context.Request.Method,
["User"] = context.User?.Identity?.Name ?? "Anonymous",
["ExceptionType"] = exception.GetType().FullName
};
_logger.Log(logLevel, exception, "Global exception handled | {LogContext}",
System.Text.Json.JsonSerializer.Serialize(logMessage));
// ========== 6. 序列化响应(驼峰命名,隐藏空值) ==========
var jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
var result = System.Text.Json.JsonSerializer.Serialize(new JsonView
{
Code = statusCode,
Msg = message,
Data = details
}, jsonOptions);
await context.Response.WriteAsync(result);
}
// ========== 辅助方法:判断数据库超时异常(可扩展) ==========
private bool IsDbTimeoutException(SqlException sqlEx)
{
// 显式声明long数组,覆盖SQL Server/MySQL常见超时错误码
// 错误码说明:
// -2/-4:SQL Server 连接超时
// 1205:SQL Server 死锁
// 4060:SQL Server 无法连接数据库
// 2148732164:.NET 封装的数据库超时(对应0x80131904)
long[] timeoutErrorCodes = new long[] { -2, -4, 1205, 4060, 2148732164 };
// 转换sqlEx.Number为long后再判断(避免类型不匹配)
return timeoutErrorCodes.Contains((long)sqlEx.Number);
}
// ========== 辅助方法:判断SqlSugar超时异常 ==========
private bool IsSugarTimeoutException(SqlSugarException sugarEx)
{
var timeoutKeywords = new[] { "timeout", "超时", "deadlock", "死锁" };
return timeoutKeywords.Any(k => sugarEx.Message.Contains(k, StringComparison.OrdinalIgnoreCase));
}
}
}