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)); } } }