ExceptionHandlingMiddleware.cs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. 
  2. using Microsoft.Data.SqlClient;
  3. using System.Text.Json;
  4. using System.Text.Json.Serialization;
  5. namespace OASystem.API.Middlewares
  6. {
  7. /// <summary>
  8. /// 全局异常捕获中间件
  9. /// </summary>
  10. public class ExceptionHandlingMiddleware
  11. {
  12. private readonly RequestDelegate _next; // 用来处理上下文请求
  13. private readonly ILogger<ExceptionHandlingMiddleware> _logger;
  14. /// <summary>
  15. /// 初始化
  16. /// </summary>
  17. /// <param name="next"></param>
  18. /// <param name="logger"></param>
  19. public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
  20. {
  21. _next = next;
  22. _logger = logger;
  23. }
  24. /// <summary>
  25. /// 执行中间件
  26. /// </summary>
  27. /// <param name="httpContext"></param>
  28. /// <param name="db"></param>
  29. /// <returns></returns>
  30. public async Task InvokeAsync(HttpContext httpContext, SqlSugarClient db)
  31. {
  32. try
  33. {
  34. await _next(httpContext); //要么在中间件中处理,要么被传递到下一个中间件中去
  35. }
  36. catch (Exception ex)
  37. {
  38. await HandleExceptionAsync(httpContext, ex, db); // 捕获异常了 在HandleExceptionAsync中处理
  39. }
  40. }
  41. /// <summary>
  42. /// 自定义全局异常处理方法
  43. /// </summary>
  44. /// <param name="context">HTTP上下文</param>
  45. /// <param name="exception">捕获的异常</param>
  46. /// <param name="db">SqlSugar客户端</param>
  47. /// <returns></returns>
  48. private async Task HandleExceptionAsync(HttpContext context, Exception exception, SqlSugarClient db)
  49. {
  50. // ========== 1. 事务回滚(恢复并优化) ==========
  51. try
  52. {
  53. if (db?.Ado?.Transaction != null)
  54. {
  55. db.Ado.RollbackTran();
  56. _logger.LogWarning("Transaction rolled back due to exception: {ExceptionType}", exception.GetType().Name);
  57. }
  58. }
  59. catch (Exception rollbackEx)
  60. {
  61. _logger.LogError(rollbackEx, "Failed to rollback transaction");
  62. }
  63. // ========== 2. 响应已开始则仅记录日志 ==========
  64. if (context.Response.HasStarted)
  65. {
  66. _logger.LogError(exception, "Exception occurred after response started | RequestId: {RequestId}",
  67. context.TraceIdentifier); // 记录请求ID
  68. return;
  69. }
  70. // ========== 3. 构建标准化响应 ==========
  71. context.Response.ContentType = "application/json; charset=utf-8";
  72. var (statusCode, message, details, logLevel) = exception switch
  73. {
  74. // 业务异常:返回自定义信息,日志级别为Warning(非错误)
  75. BusinessException businessEx => (
  76. businessEx.Code,
  77. businessEx.Msg,
  78. businessEx.Data,
  79. LogLevel.Warning
  80. ),
  81. // 权限/资源异常:标准化提示,日志Warning
  82. UnauthorizedAccessException => (403, "无权访问此资源", null, LogLevel.Warning),
  83. KeyNotFoundException => (404, "请求的资源不存在", null, LogLevel.Warning),
  84. // 数据库超时:隐藏敏感信息,日志Error
  85. SqlException sqlEx when IsDbTimeoutException(sqlEx) => (
  86. 503,
  87. "数据库连接超时,请稍后重试。",
  88. null,
  89. LogLevel.Error
  90. ),
  91. SqlSugarException sugarEx when IsSugarTimeoutException(sugarEx) => (
  92. 503,
  93. "数据库连接超时,请稍后重试。",
  94. null,
  95. LogLevel.Error
  96. ),
  97. // 其他数据库异常:隐藏原始消息,日志Error
  98. SqlException => (500, "数据库服务异常", null, LogLevel.Error),
  99. SqlSugarException => (500, "数据库服务异常", null, LogLevel.Error),
  100. // 应用程序异常(业务逻辑错误):日志Warning
  101. ApplicationException appEx => (400, appEx.Message, null, LogLevel.Warning),
  102. // 兜底系统异常:隐藏原始消息,日志Critical
  103. _ => (500, "服务器内部错误", null, LogLevel.Critical)
  104. };
  105. // ========== 4. 设置HTTP状态码(关键优化) ==========
  106. context.Response.StatusCode = 200;
  107. // ========== 5. 分级记录日志(含上下文) ==========
  108. var logMessage = new Dictionary<string, string>
  109. {
  110. ["RequestId"] = context.TraceIdentifier,
  111. ["Path"] = context.Request.Path,
  112. ["Method"] = context.Request.Method,
  113. ["User"] = context.User?.Identity?.Name ?? "Anonymous",
  114. ["ExceptionType"] = exception.GetType().FullName
  115. };
  116. _logger.Log(logLevel, exception, "Global exception handled | {LogContext}",
  117. System.Text.Json.JsonSerializer.Serialize(logMessage));
  118. // ========== 6. 序列化响应(驼峰命名,隐藏空值) ==========
  119. var jsonOptions = new JsonSerializerOptions
  120. {
  121. PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
  122. DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
  123. };
  124. var result = System.Text.Json.JsonSerializer.Serialize(new JsonView
  125. {
  126. Code = statusCode,
  127. Msg = message,
  128. Data = details
  129. }, jsonOptions);
  130. await context.Response.WriteAsync(result);
  131. }
  132. // ========== 辅助方法:判断数据库超时异常(可扩展) ==========
  133. private bool IsDbTimeoutException(SqlException sqlEx)
  134. {
  135. // 显式声明long数组,覆盖SQL Server/MySQL常见超时错误码
  136. // 错误码说明:
  137. // -2/-4:SQL Server 连接超时
  138. // 1205:SQL Server 死锁
  139. // 4060:SQL Server 无法连接数据库
  140. // 2148732164:.NET 封装的数据库超时(对应0x80131904)
  141. long[] timeoutErrorCodes = new long[] { -2, -4, 1205, 4060, 2148732164 };
  142. // 转换sqlEx.Number为long后再判断(避免类型不匹配)
  143. return timeoutErrorCodes.Contains((long)sqlEx.Number);
  144. }
  145. // ========== 辅助方法:判断SqlSugar超时异常 ==========
  146. private bool IsSugarTimeoutException(SqlSugarException sugarEx)
  147. {
  148. var timeoutKeywords = new[] { "timeout", "超时", "deadlock", "死锁" };
  149. return timeoutKeywords.Any(k => sugarEx.Message.Contains(k, StringComparison.OrdinalIgnoreCase));
  150. }
  151. }
  152. }