|
|
@@ -1,5 +1,7 @@
|
|
|
|
|
|
using Microsoft.Data.SqlClient;
|
|
|
+using System.Text.Json;
|
|
|
+using System.Text.Json.Serialization;
|
|
|
|
|
|
namespace OASystem.API.Middlewares
|
|
|
{
|
|
|
@@ -43,86 +45,121 @@ namespace OASystem.API.Middlewares
|
|
|
/// <summary>
|
|
|
/// 自定义全局异常处理方法
|
|
|
/// </summary>
|
|
|
- /// <param name="context"></param>
|
|
|
- /// <param name="exception"></param>
|
|
|
- /// <param name="db"></param>
|
|
|
+ /// <param name="context">HTTP上下文</param>
|
|
|
+ /// <param name="exception">捕获的异常</param>
|
|
|
+ /// <param name="db">SqlSugar客户端</param>
|
|
|
/// <returns></returns>
|
|
|
private async Task HandleExceptionAsync(HttpContext context, Exception exception, SqlSugarClient db)
|
|
|
{
|
|
|
- //if (db.Ado != null && db.Ado.Transaction != null)
|
|
|
- //{
|
|
|
- // try
|
|
|
- // {
|
|
|
- // db.Ado.RollbackTran();
|
|
|
- // }
|
|
|
- // catch (Exception ex)
|
|
|
- // {
|
|
|
- // _logger.LogError("Error rolling back transaction: {ErrorMessage}", ex.Message);
|
|
|
- // }
|
|
|
- //}
|
|
|
-
|
|
|
- // 检查响应是否已开始
|
|
|
- if (!context.Response.HasStarted)
|
|
|
+ // ========== 1. 事务回滚(恢复并优化) ==========
|
|
|
+ try
|
|
|
{
|
|
|
- context.Response.ContentType = "application/json"; // 设置响应内容类型为 JSON
|
|
|
- var response = context.Response;
|
|
|
- var errorResponse = new JsonView
|
|
|
+ if (db?.Ado?.Transaction != null)
|
|
|
{
|
|
|
- Code = StatusCodes.Status500InternalServerError,
|
|
|
- Data = ""
|
|
|
- };
|
|
|
-
|
|
|
- // 根据异常类型设置不同的响应状态码和消息
|
|
|
- switch (exception)
|
|
|
- {
|
|
|
- case BusinessException businessEx: // 新增:处理 BusinessException
|
|
|
- response.StatusCode = StatusCodes.Status400BadRequest;
|
|
|
- errorResponse.Msg = businessEx.Message;
|
|
|
- errorResponse.Code = StatusCodes.Status400BadRequest; // 设置正确的 HTTP 状态码
|
|
|
- break;
|
|
|
- case SqlException sqlEx when sqlEx.Number == -2:
|
|
|
- response.StatusCode = StatusCodes.Status503ServiceUnavailable;
|
|
|
- errorResponse.Msg = "数据库连接超时,请稍后重试。";
|
|
|
- break;
|
|
|
- case SqlException sqlEx when sqlEx.Number == -4:
|
|
|
- response.StatusCode = StatusCodes.Status503ServiceUnavailable;
|
|
|
- errorResponse.Msg = "数据库连接池耗尽,请稍后重试。";
|
|
|
- break;
|
|
|
- case ApplicationException ex when ex.Message.Contains("Invalid token"):
|
|
|
- response.StatusCode = StatusCodes.Status403Forbidden;
|
|
|
- errorResponse.Msg = ex.Message;
|
|
|
- break;
|
|
|
- case ApplicationException ex:
|
|
|
- response.StatusCode = StatusCodes.Status400BadRequest;
|
|
|
- errorResponse.Msg = ex.Message;
|
|
|
- break;
|
|
|
- case KeyNotFoundException ex:
|
|
|
- response.StatusCode = StatusCodes.Status404NotFound;
|
|
|
- errorResponse.Msg = ex.Message;
|
|
|
- break;
|
|
|
- case SqlSugarException ex:
|
|
|
- response.StatusCode = ex.Message.Contains("timeout")
|
|
|
- ? StatusCodes.Status503ServiceUnavailable
|
|
|
- : StatusCodes.Status500InternalServerError;
|
|
|
- errorResponse.Msg = ex.Message.Contains("timeout")
|
|
|
- ? "数据库连接超时,请稍后重试。"
|
|
|
- : ex.Message;
|
|
|
- break;
|
|
|
- default:
|
|
|
- response.StatusCode = StatusCodes.Status400BadRequest;
|
|
|
- errorResponse.Msg = exception.Message; // 不直接暴露异常详细信息
|
|
|
- break;
|
|
|
+ db.Ado.RollbackTran();
|
|
|
+ _logger.LogWarning("Transaction rolled back due to exception: {ExceptionType}", exception.GetType().Name);
|
|
|
}
|
|
|
-
|
|
|
- _logger.LogError(exception, "An exception occurred."); // 记录异常详细信息
|
|
|
- var result = JsonConvert.SerializeObject(errorResponse);
|
|
|
- await context.Response.WriteAsync(result); // 将错误响应写入客户端
|
|
|
}
|
|
|
- else
|
|
|
+ catch (Exception rollbackEx)
|
|
|
{
|
|
|
- // 如果响应已开始,记录日志但不做进一步处理
|
|
|
- _logger.LogError(exception, "An exception occurred after response started.");
|
|
|
+ _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<string, string>
|
|
|
+ {
|
|
|
+ ["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));
|
|
|
}
|
|
|
+
|
|
|
}
|
|
|
}
|