using FluentValidation.Results; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Text.Json; using System.Text.Json.Serialization; using System.Collections.Generic; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Results; using FluentValidationException = FluentValidation.ValidationException; using SharedValidationException = TakeoutSaaS.Shared.Abstractions.Exceptions.ValidationException; namespace TakeoutSaaS.Shared.Web.Middleware; /// /// 全局异常处理中间件,将异常统一映射为 ApiResponse。 /// public sealed class ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger, IHostEnvironment environment) { private static readonly HashSet AllowedHttpErrorCodes = new() { ErrorCodes.BadRequest, ErrorCodes.Unauthorized, ErrorCodes.Forbidden, ErrorCodes.NotFound, ErrorCodes.Conflict, ErrorCodes.ValidationFailed }; private static readonly JsonSerializerOptions SerializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; /// /// 中间件入口,捕获并统一处理异常。 /// /// HTTP 上下文。 public async Task InvokeAsync(HttpContext context) { try { await next(context); } catch (Exception ex) { // 1. 记录异常 logger.LogError(ex, "未处理异常:{Message}", ex.Message); // 2. 返回统一错误响应 await HandleExceptionAsync(context, ex); } } private Task HandleExceptionAsync(HttpContext context, Exception exception) { // 1. 构建错误响应与状态码 var (statusCode, response) = BuildErrorResponse(exception); if (environment.IsDevelopment()) { // 2. 开发环境附加细节 response = response with { Message = exception.Message, Errors = new { response.Errors, detail = exception.ToString() } }; } // 3. 写入响应 context.Response.StatusCode = statusCode; context.Response.ContentType = "application/json"; return context.Response.WriteAsJsonAsync(response, SerializerOptions); } private static (int StatusCode, ApiResponse Response) BuildErrorResponse(Exception exception) { return exception switch { DbUpdateConcurrencyException => ( StatusCodes.Status409Conflict, ApiResponse.Error( ErrorCodes.Conflict, "数据已被他人修改,请刷新后重试", new Dictionary { ["RowVersion"] = ["数据已被他人修改,请刷新后重试"] })), UnauthorizedAccessException => ( StatusCodes.Status403Forbidden, ApiResponse.Error(ErrorCodes.Forbidden, "无权访问该资源")), SharedValidationException validationException => ( StatusCodes.Status422UnprocessableEntity, ApiResponse.Error(ErrorCodes.ValidationFailed, "请求参数验证失败", validationException.Errors)), FluentValidationException fluentValidationException => ( StatusCodes.Status422UnprocessableEntity, ApiResponse.Error( ErrorCodes.ValidationFailed, "请求参数验证失败", NormalizeValidationErrors(fluentValidationException.Errors))), BusinessException businessException => ( // 1. 仅当业务错误码在白名单且位于 400-499 时透传,否则回退 400 AllowedHttpErrorCodes.Contains(businessException.ErrorCode) && businessException.ErrorCode is >= 400 and < 500 ? businessException.ErrorCode : StatusCodes.Status400BadRequest, ApiResponse.Error(businessException.ErrorCode, businessException.Message)), _ => ( StatusCodes.Status500InternalServerError, ApiResponse.Error(ErrorCodes.InternalServerError, "服务器开小差啦,请稍后再试")) }; } private static IDictionary NormalizeValidationErrors(IEnumerable failures) { var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (var failure in failures) { var key = string.IsNullOrWhiteSpace(failure.PropertyName) ? "request" : failure.PropertyName; if (!result.TryGetValue(key, out var list)) { list = new List(); result[key] = list; } if (!string.IsNullOrWhiteSpace(failure.ErrorMessage)) { list.Add(failure.ErrorMessage); } } return result.ToDictionary(pair => pair.Key, pair => pair.Value.Distinct().ToArray(), StringComparer.OrdinalIgnoreCase); } }