From 67c7ef37e68bbc60056782b4439560ef4572b99a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A9=99=E5=AD=90?= <454313500@qq.com> Date: Sun, 1 Sep 2024 03:06:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=94=AF=E6=8C=81fur?= =?UTF-8?q?ion=E8=A7=84=E8=8C=83=E5=8C=96=E6=8E=A5=E5=8F=A3=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UnifyResult/ExceptionMetadata.cs | 46 +++ .../Fiters/FriendlyExceptionFilter.cs | 106 +++++++ .../Fiters/SucceededUnifyResultFilter.cs | 276 ++++++++++++++++++ .../UnifyResult/IUnifyResultProvider.cs | 58 ++++ .../UnifyResult/NonUnifyAttribute.cs | 23 ++ .../Providers/RESTfulResultProvider.cs | 136 +++++++++ .../UnifyResult/RESTfulResult.cs | 52 ++++ .../UnifyResult/UnifyResultExtensions.cs | 29 ++ .../UnifyResult/UnifyResultSettingsOptions.cs | 50 ++++ .../UnifyResult/ValidationMetadata.cs | 69 +++++ .../Extensions/HttpContextExtensions.cs | 10 + .../Services/TestService.cs | 11 + Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs | 10 + 13 files changed, 876 insertions(+) create mode 100644 Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/ExceptionMetadata.cs create mode 100644 Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/Fiters/FriendlyExceptionFilter.cs create mode 100644 Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/Fiters/SucceededUnifyResultFilter.cs create mode 100644 Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/IUnifyResultProvider.cs create mode 100644 Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/NonUnifyAttribute.cs create mode 100644 Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/Providers/RESTfulResultProvider.cs create mode 100644 Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/RESTfulResult.cs create mode 100644 Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/UnifyResultExtensions.cs create mode 100644 Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/UnifyResultSettingsOptions.cs create mode 100644 Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/ValidationMetadata.cs diff --git a/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/ExceptionMetadata.cs b/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/ExceptionMetadata.cs new file mode 100644 index 00000000..8277da2d --- /dev/null +++ b/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/ExceptionMetadata.cs @@ -0,0 +1,46 @@ +// MIT 许可证 +// +// 版权 © 2020-present 百小僧, 百签科技(广东)有限公司 和所有贡献者 +// +// 特此免费授予任何获得本软件副本和相关文档文件(下称“软件”)的人不受限制地处置该软件的权利, +// 包括不受限制地使用、复制、修改、合并、发布、分发、转授许可和/或出售该软件副本, +// 以及再授权被配发了本软件的人如上的权利,须在下列条件下: +// +// 上述版权声明和本许可声明应包含在该软件的所有副本或实质成分中。 +// +// 本软件是“如此”提供的,没有任何形式的明示或暗示的保证,包括但不限于对适销性、特定用途的适用性和不侵权的保证。 +// 在任何情况下,作者或版权持有人都不对任何索赔、损害或其他责任负责,无论这些追责来自合同、侵权或其它行为中, +// 还是产生于、源于或有关于本软件以及本软件的使用或其它处置。 + +namespace Yi.Framework.AspNetCore.UnifyResult; + +/// +/// 异常元数据 +/// +public sealed class ExceptionMetadata +{ + /// + /// 状态码 + /// + public int StatusCode { get; internal set; } + + /// + /// 错误码 + /// + public object ErrorCode { get; internal set; } + + /// + /// 错误码(没被复写过的 ErrorCode ) + /// + public object OriginErrorCode { get; internal set; } + + /// + /// 错误对象(信息) + /// + public object Errors { get; internal set; } + + /// + /// 额外数据 + /// + public object Data { get; internal set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/Fiters/FriendlyExceptionFilter.cs b/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/Fiters/FriendlyExceptionFilter.cs new file mode 100644 index 00000000..cf86d283 --- /dev/null +++ b/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/Fiters/FriendlyExceptionFilter.cs @@ -0,0 +1,106 @@ +// MIT 许可证 +// +// 版权 © 2020-present 百小僧, 百签科技(广东)有限公司 和所有贡献者 +// +// 特此免费授予任何获得本软件副本和相关文档文件(下称“软件”)的人不受限制地处置该软件的权利, +// 包括不受限制地使用、复制、修改、合并、发布、分发、转授许可和/或出售该软件副本, +// 以及再授权被配发了本软件的人如上的权利,须在下列条件下: +// +// 上述版权声明和本许可声明应包含在该软件的所有副本或实质成分中。 +// +// 本软件是“如此”提供的,没有任何形式的明示或暗示的保证,包括但不限于对适销性、特定用途的适用性和不侵权的保证。 +// 在任何情况下,作者或版权持有人都不对任何索赔、损害或其他责任负责,无论这些追责来自合同、侵权或其它行为中, +// 还是产生于、源于或有关于本软件以及本软件的使用或其它处置。 + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.DependencyInjection; +using Yi.Framework.Core.Extensions; + +namespace Yi.Framework.AspNetCore.UnifyResult.Fiters; + +/// +/// 友好异常拦截器 +/// +public sealed class FriendlyExceptionFilter : IAsyncExceptionFilter +{ + /// + /// 异常拦截 + /// + /// + /// + public async Task OnExceptionAsync(ExceptionContext context) + { + + // 排除 WebSocket 请求处理 + if (context.HttpContext.IsWebSocketRequest()) return; + + // 如果异常在其他地方被标记了处理,那么这里不再处理 + if (context.ExceptionHandled) return; + + // 解析异常信息 + var exceptionMetadata = GetExceptionMetadata(context); + + IUnifyResultProvider unifyResult = context.GetRequiredService(); + // 执行规范化异常处理 + context.Result = unifyResult.OnException(context, exceptionMetadata); + + // 创建日志记录器 + var logger = context.HttpContext.RequestServices.GetRequiredService>(); + + // 记录拦截日常 + logger.LogError(context.Exception, context.Exception.Message); + } + + /// + /// 获取异常元数据 + /// + /// + /// + public static ExceptionMetadata GetExceptionMetadata(ActionContext context) + { + object errorCode = default; + object originErrorCode = default; + object errors = default; + object data = default; + var statusCode = StatusCodes.Status500InternalServerError; + var isValidationException = false; // 判断是否是验证异常 + var isFriendlyException = false; + + // 判断是否是 ExceptionContext 或者 ActionExecutedContext + var exception = context is ExceptionContext exContext + ? exContext.Exception + : ( + context is ActionExecutedContext edContext + ? edContext.Exception + : default + ); + + // 判断是否是友好异常 + if (exception is UserFriendlyException friendlyException) + { + int statusCode2 = 500; + int.TryParse(friendlyException.Code, out statusCode2); + isFriendlyException = true; + errorCode = friendlyException.Code; + originErrorCode = friendlyException.Code; + statusCode = statusCode2==0?403:statusCode2; + isValidationException = false; + errors = friendlyException.Message; + data = friendlyException.Data; + } + + return new ExceptionMetadata + { + StatusCode = statusCode, + ErrorCode = errorCode, + OriginErrorCode = originErrorCode, + Errors = errors, + Data = data + }; + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/Fiters/SucceededUnifyResultFilter.cs b/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/Fiters/SucceededUnifyResultFilter.cs new file mode 100644 index 00000000..62288f25 --- /dev/null +++ b/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/Fiters/SucceededUnifyResultFilter.cs @@ -0,0 +1,276 @@ +using System.Collections; +using System.Reflection; +using System.Text.Encodings.Web; +using System.Text.Json; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.DependencyInjection; +using Yi.Framework.Core.Extensions; + +namespace Yi.Framework.AspNetCore.UnifyResult.Fiters; + +/// +/// 规范化结构(请求成功)过滤器 +/// +public class SucceededUnifyResultFilter : IAsyncActionFilter, IOrderedFilter +{ + /// + /// 过滤器排序 + /// + private const int FilterOrder = 8888; + + /// + /// 排序属性 + /// + public int Order => FilterOrder; + + /// + /// 处理规范化结果 + /// + /// + /// + /// + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + // 执行 Action 并获取结果 + var actionExecutedContext = await next(); + + // 排除 WebSocket 请求处理 + if (actionExecutedContext.HttpContext.IsWebSocketRequest()) return; + + // 处理已经含有状态码结果的 Result + if (actionExecutedContext.Result is IStatusCodeActionResult statusCodeResult && + statusCodeResult.StatusCode != null) + { + // 小于 200 或者 大于 299 都不是成功值,直接跳过 + if (statusCodeResult.StatusCode.Value < 200 || statusCodeResult.StatusCode.Value > 299) + { + // 处理规范化结果 + if (!CheckStatusCodeNonUnify(context.HttpContext, out var unifyRes)) + { + var httpContext = context.HttpContext; + var statusCode = statusCodeResult.StatusCode.Value; + + // 解决刷新 Token 时间和 Token 时间相近问题 + if (statusCodeResult.StatusCode.Value == StatusCodes.Status401Unauthorized + && httpContext.Response.Headers.ContainsKey("access-token") + && httpContext.Response.Headers.ContainsKey("x-access-token")) + { + httpContext.Response.StatusCode = statusCode = StatusCodes.Status403Forbidden; + } + + // 如果 Response 已经完成输出,则禁止写入 + if (httpContext.Response.HasStarted) return; + await unifyRes.OnResponseStatusCodes(httpContext, statusCode, + httpContext.RequestServices.GetService>()?.Value); + } + + return; + } + } + + // 如果出现异常,则不会进入该过滤器 + if (actionExecutedContext.Exception != null) return; + + // 获取控制器信息 + var actionDescriptor = context.ActionDescriptor as ControllerActionDescriptor; + + // 判断是否支持 MVC 规范化处理,检测配置而已 + // if (!UnifyContext.CheckSupportMvcController(context.HttpContext, actionDescriptor, out _)) return; + + // 判断是否跳过规范化处理,检测NonUnifyAttribute而已 + if (CheckSucceededNonUnify(actionDescriptor.MethodInfo)) + { + return; + } + IUnifyResultProvider unifyResult = context.GetRequiredService(); + + // 处理 BadRequestObjectResult 类型规范化处理 + if (actionExecutedContext.Result is BadRequestObjectResult badRequestObjectResult) + { + // 解析验证消息 + var validationMetadata = GetValidationMetadata(badRequestObjectResult.Value); + + var result = unifyResult.OnValidateFailed(context, validationMetadata); + if (result != null) actionExecutedContext.Result = result; + } + else + { + IActionResult result = default; + + // 检查是否是有效的结果(可进行规范化的结果) + if (CheckVaildResult(actionExecutedContext.Result, out var data)) + { + result = unifyResult.OnSucceeded(actionExecutedContext, data); + } + + // 如果是不能规范化的结果类型,则跳过 + if (result == null) return; + + actionExecutedContext.Result = result; + } + } + + /// + /// 获取验证错误信息 + /// + /// + /// + private static ValidationMetadata GetValidationMetadata(object errors) + { + ModelStateDictionary _modelState = null; + object validationResults = null; + (string message, string firstErrorMessage, string firstErrorProperty) = (default, default, default); + + // 判断是否是集合类型 + if (errors is IEnumerable && errors is not string) + { + // 如果是模型验证字典类型 + if (errors is ModelStateDictionary modelState) + { + _modelState = modelState; + // 将验证错误信息转换成字典并序列化成 Json + validationResults = modelState.Where(u => modelState[u.Key].ValidationState == ModelValidationState.Invalid) + .ToDictionary(u => u.Key, u => modelState[u.Key].Errors.Select(c => c.ErrorMessage).ToArray()); + } + // 如果是 ValidationProblemDetails 特殊类型 + else if (errors is ValidationProblemDetails validation) + { + validationResults = validation.Errors + .ToDictionary(u => u.Key, u => u.Value.ToArray()); + } + // 如果是字典类型 + else if (errors is Dictionary dicResults) + { + validationResults = dicResults; + } + + message = JsonSerializer.Serialize(validationResults, new JsonSerializerOptions + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + WriteIndented = true + }); + firstErrorMessage = (validationResults as Dictionary).First().Value[0]; + firstErrorProperty = (validationResults as Dictionary).First().Key; + } + // 其他类型 + else + { + validationResults = firstErrorMessage = message = errors?.ToString(); + } + + return new ValidationMetadata + { + ValidationResult = validationResults, + Message = message, + ModelState = _modelState, + FirstErrorProperty = firstErrorProperty, + FirstErrorMessage = firstErrorMessage + }; + } + + /// + /// 检查是否是有效的结果(可进行规范化的结果) + /// + /// + /// + /// + private bool CheckVaildResult(IActionResult result, out object data) + { + data = default; + + // 排除以下结果,跳过规范化处理 + var isDataResult = result switch + { + ViewResult => false, + PartialViewResult => false, + FileResult => false, + ChallengeResult => false, + SignInResult => false, + SignOutResult => false, + RedirectToPageResult => false, + RedirectToRouteResult => false, + RedirectResult => false, + RedirectToActionResult => false, + LocalRedirectResult => false, + ForbidResult => false, + ViewComponentResult => false, + PageResult => false, + NotFoundResult => false, + NotFoundObjectResult => false, + _ => true, + }; + + // 目前支持返回值 ActionResult + if (isDataResult) data = result switch + { + // 处理内容结果 + ContentResult content => content.Content, + // 处理对象结果 + ObjectResult obj => obj.Value, + // 处理 JSON 对象 + JsonResult json => json.Value, + _ => null, + }; + + return isDataResult; + } + + + /// + /// 检查短路状态码(>=400)是否进行规范化处理 + /// + /// + /// + /// 返回 true 跳过处理,否则进行规范化处理 + internal static bool CheckStatusCodeNonUnify(HttpContext context, out IUnifyResultProvider unifyResult) + { + // 获取终点路由特性 + var endpointFeature = context.Features.Get(); + if (endpointFeature == null) return (unifyResult = null) == null; + + // 判断是否跳过规范化处理 + var isSkip = context.GetEndpoint()?.Metadata?.GetMetadata()!= null + || endpointFeature?.Endpoint?.Metadata?.GetMetadata() != null + || context.Request.Headers["accept"].ToString().Contains("odata.metadata=", StringComparison.OrdinalIgnoreCase) + || context.Request.Headers["accept"].ToString().Contains("odata.streaming=", StringComparison.OrdinalIgnoreCase); + + if (isSkip == true) unifyResult = null; + else + { + unifyResult = context.RequestServices.GetRequiredService(); + } + + return unifyResult == null || isSkip; + } + + /// + /// 检查请求成功是否进行规范化处理 + /// + /// + /// + /// 返回 true 跳过处理,否则进行规范化处理 + private bool CheckSucceededNonUnify(MethodInfo method, bool isWebRequest = true) + { + // 判断是否跳过规范化处理 + var isSkip = method.CustomAttributes.Any(x => typeof(NonUnifyAttribute).IsAssignableFrom(x.AttributeType) || typeof(ProducesResponseTypeAttribute).IsAssignableFrom(x.AttributeType) || typeof(IApiResponseMetadataProvider).IsAssignableFrom(x.AttributeType)) + || method.ReflectedType.IsDefined(typeof(NonUnifyAttribute), true) + || method.DeclaringType.Assembly.GetName().Name.StartsWith("Microsoft.AspNetCore.OData"); + + if (!isWebRequest) + { + return isSkip; + } + return isSkip; + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/IUnifyResultProvider.cs b/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/IUnifyResultProvider.cs new file mode 100644 index 00000000..f8ac41eb --- /dev/null +++ b/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/IUnifyResultProvider.cs @@ -0,0 +1,58 @@ +// MIT 许可证 +// +// 版权 © 2020-present 百小僧, 百签科技(广东)有限公司 和所有贡献者 +// +// 特此免费授予任何获得本软件副本和相关文档文件(下称“软件”)的人不受限制地处置该软件的权利, +// 包括不受限制地使用、复制、修改、合并、发布、分发、转授许可和/或出售该软件副本, +// 以及再授权被配发了本软件的人如上的权利,须在下列条件下: +// +// 上述版权声明和本许可声明应包含在该软件的所有副本或实质成分中。 +// +// 本软件是“如此”提供的,没有任何形式的明示或暗示的保证,包括但不限于对适销性、特定用途的适用性和不侵权的保证。 +// 在任何情况下,作者或版权持有人都不对任何索赔、损害或其他责任负责,无论这些追责来自合同、侵权或其它行为中, +// 还是产生于、源于或有关于本软件以及本软件的使用或其它处置。 + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Yi.Framework.AspNetCore.UnifyResult; + +/// +/// 规范化结果提供器 +/// +public interface IUnifyResultProvider +{ + /// + /// 异常返回值 + /// + /// + /// + /// + IActionResult OnException(ExceptionContext context, ExceptionMetadata metadata); + + /// + /// 成功返回值 + /// + /// + /// + /// + IActionResult OnSucceeded(ActionExecutedContext context, object data); + + /// + /// 验证失败返回值 + /// + /// + /// + /// + IActionResult OnValidateFailed(ActionExecutingContext context, ValidationMetadata metadata); + + /// + /// 拦截返回状态码 + /// + /// + /// + /// + /// + Task OnResponseStatusCodes(HttpContext context, int statusCode, UnifyResultSettingsOptions unifyResultSettings = default); +} \ No newline at end of file diff --git a/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/NonUnifyAttribute.cs b/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/NonUnifyAttribute.cs new file mode 100644 index 00000000..fee1e9f8 --- /dev/null +++ b/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/NonUnifyAttribute.cs @@ -0,0 +1,23 @@ +// MIT 许可证 +// +// 版权 © 2020-present 百小僧, 百签科技(广东)有限公司 和所有贡献者 +// +// 特此免费授予任何获得本软件副本和相关文档文件(下称“软件”)的人不受限制地处置该软件的权利, +// 包括不受限制地使用、复制、修改、合并、发布、分发、转授许可和/或出售该软件副本, +// 以及再授权被配发了本软件的人如上的权利,须在下列条件下: +// +// 上述版权声明和本许可声明应包含在该软件的所有副本或实质成分中。 +// +// 本软件是“如此”提供的,没有任何形式的明示或暗示的保证,包括但不限于对适销性、特定用途的适用性和不侵权的保证。 +// 在任何情况下,作者或版权持有人都不对任何索赔、损害或其他责任负责,无论这些追责来自合同、侵权或其它行为中, +// 还是产生于、源于或有关于本软件以及本软件的使用或其它处置。 + +namespace Yi.Framework.AspNetCore.UnifyResult; + +/// +/// 禁止规范化处理 +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] +public sealed class NonUnifyAttribute : Attribute +{ +} \ No newline at end of file diff --git a/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/Providers/RESTfulResultProvider.cs b/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/Providers/RESTfulResultProvider.cs new file mode 100644 index 00000000..033b8a8c --- /dev/null +++ b/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/Providers/RESTfulResultProvider.cs @@ -0,0 +1,136 @@ +// MIT 许可证 +// +// 版权 © 2020-present 百小僧, 百签科技(广东)有限公司 和所有贡献者 +// +// 特此免费授予任何获得本软件副本和相关文档文件(下称“软件”)的人不受限制地处置该软件的权利, +// 包括不受限制地使用、复制、修改、合并、发布、分发、转授许可和/或出售该软件副本, +// 以及再授权被配发了本软件的人如上的权利,须在下列条件下: +// +// 上述版权声明和本许可声明应包含在该软件的所有副本或实质成分中。 +// +// 本软件是“如此”提供的,没有任何形式的明示或暗示的保证,包括但不限于对适销性、特定用途的适用性和不侵权的保证。 +// 在任何情况下,作者或版权持有人都不对任何索赔、损害或其他责任负责,无论这些追责来自合同、侵权或其它行为中, +// 还是产生于、源于或有关于本软件以及本软件的使用或其它处置。 + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Volo.Abp.DependencyInjection; + +namespace Yi.Framework.AspNetCore.UnifyResult.Providers; + +/// +/// RESTful 风格返回值 +/// +[Dependency(TryRegister = true)] +[ExposeServices(typeof(IUnifyResultProvider))] +public class RESTfulResultProvider : IUnifyResultProvider,ITransientDependency +{ + /// + /// 设置响应状态码 + /// + /// + /// + /// + public static void SetResponseStatusCodes(HttpContext context, int statusCode, UnifyResultSettingsOptions unifyResultSettings) + { + if (unifyResultSettings == null) return; + + // 篡改响应状态码 + if (unifyResultSettings.AdaptStatusCodes != null && unifyResultSettings.AdaptStatusCodes.Length > 0) + { + var adaptStatusCode = unifyResultSettings.AdaptStatusCodes.FirstOrDefault(u => u[0] == statusCode); + if (adaptStatusCode != null && adaptStatusCode.Length > 0 && adaptStatusCode[0] > 0) + { + context.Response.StatusCode = adaptStatusCode[1]; + return; + } + } + + // 如果为 null,则所有请求错误的状态码设置为 200 + if (unifyResultSettings.Return200StatusCodes == null) context.Response.StatusCode = 200; + // 否则只有里面的才设置为 200 + else if (unifyResultSettings.Return200StatusCodes.Contains(statusCode)) context.Response.StatusCode = 200; + else { } + } + + /// + /// 异常返回值 + /// + /// + /// + /// + public IActionResult OnException(ExceptionContext context, ExceptionMetadata metadata) + { + return new JsonResult(RESTfulResult(metadata.StatusCode, data: metadata.Data, errors: metadata.Errors)); + } + + /// + /// 成功返回值 + /// + /// + /// + /// + public IActionResult OnSucceeded(ActionExecutedContext context, object data) + { + return new JsonResult(RESTfulResult(StatusCodes.Status200OK, true, data)); + } + + /// + /// 验证失败/业务异常返回值 + /// + /// + /// + /// + public IActionResult OnValidateFailed(ActionExecutingContext context, ValidationMetadata metadata) + { + return new JsonResult(RESTfulResult(metadata.StatusCode ?? StatusCodes.Status400BadRequest, data: metadata.Data, errors: metadata.ValidationResult)); + } + + /// + /// 特定状态码返回值 + /// + /// + /// + /// + /// + public async Task OnResponseStatusCodes(HttpContext context, int statusCode, UnifyResultSettingsOptions unifyResultSettings) + { + // 设置响应状态码 + SetResponseStatusCodes(context, statusCode, unifyResultSettings); + + switch (statusCode) + { + // 处理 401 状态码 + case StatusCodes.Status401Unauthorized: + await context.Response.WriteAsJsonAsync(RESTfulResult(statusCode, errors: "401 Unauthorized")); + break; + // 处理 403 状态码 + case StatusCodes.Status403Forbidden: + await context.Response.WriteAsJsonAsync(RESTfulResult(statusCode, errors: "403 Forbidden")); + break; + + default: break; + } + } + + /// + /// 返回 RESTful 风格结果集 + /// + /// + /// + /// + /// + /// + public static RESTfulResult RESTfulResult(int statusCode, bool succeeded = default, object data = default, object errors = default) + { + return new RESTfulResult + { + StatusCode = statusCode, + Succeeded = succeeded, + Data = data, + Errors = errors, + Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + }; + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/RESTfulResult.cs b/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/RESTfulResult.cs new file mode 100644 index 00000000..9c0104b6 --- /dev/null +++ b/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/RESTfulResult.cs @@ -0,0 +1,52 @@ +// MIT 许可证 +// +// 版权 © 2020-present 百小僧, 百签科技(广东)有限公司 和所有贡献者 +// +// 特此免费授予任何获得本软件副本和相关文档文件(下称“软件”)的人不受限制地处置该软件的权利, +// 包括不受限制地使用、复制、修改、合并、发布、分发、转授许可和/或出售该软件副本, +// 以及再授权被配发了本软件的人如上的权利,须在下列条件下: +// +// 上述版权声明和本许可声明应包含在该软件的所有副本或实质成分中。 +// +// 本软件是“如此”提供的,没有任何形式的明示或暗示的保证,包括但不限于对适销性、特定用途的适用性和不侵权的保证。 +// 在任何情况下,作者或版权持有人都不对任何索赔、损害或其他责任负责,无论这些追责来自合同、侵权或其它行为中, +// 还是产生于、源于或有关于本软件以及本软件的使用或其它处置。 + +namespace Yi.Framework.AspNetCore.UnifyResult; + +/// +/// RESTful 风格结果集 +/// +/// +public class RESTfulResult +{ + /// + /// 状态码 + /// + public int? StatusCode { get; set; } + + /// + /// 数据 + /// + public T Data { get; set; } + + /// + /// 执行成功 + /// + public bool Succeeded { get; set; } + + /// + /// 错误信息 + /// + public object Errors { get; set; } + + /// + /// 附加数据 + /// + public object Extras { get; set; } + + /// + /// 时间戳 + /// + public long Timestamp { get; set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/UnifyResultExtensions.cs b/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/UnifyResultExtensions.cs new file mode 100644 index 00000000..20b8db9a --- /dev/null +++ b/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/UnifyResultExtensions.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Swashbuckle.AspNetCore.SwaggerGen; +using Volo.Abp.AspNetCore.Mvc.ExceptionHandling; +using Yi.Framework.AspNetCore.UnifyResult.Fiters; + +namespace Yi.Framework.AspNetCore.UnifyResult; + +/// +/// 规范化接口 +/// 由于太多人反应,想兼容一套类似furion的返回情况,200状态码包一层更符合国内习惯,既然如此,不如直接搬过来 +/// +public static class UnifyResultExtensions +{ + public static IServiceCollection AddFurionUnifyResultApi(this IServiceCollection services) + { + //成功规范接口 + services.AddTransient(); + //异常规范接口 + services.AddTransient(); + services.AddMvc(options => + { + options.Filters.AddService(99); + options.Filters.AddService(100); + options.Filters.RemoveAll(x => (x as ServiceFilterAttribute)?.ServiceType == typeof(AbpExceptionFilter)); + }); + return services; + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/UnifyResultSettingsOptions.cs b/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/UnifyResultSettingsOptions.cs new file mode 100644 index 00000000..d8c8a30b --- /dev/null +++ b/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/UnifyResultSettingsOptions.cs @@ -0,0 +1,50 @@ +// MIT 许可证 +// +// 版权 © 2020-present 百小僧, 百签科技(广东)有限公司 和所有贡献者 +// +// 特此免费授予任何获得本软件副本和相关文档文件(下称“软件”)的人不受限制地处置该软件的权利, +// 包括不受限制地使用、复制、修改、合并、发布、分发、转授许可和/或出售该软件副本, +// 以及再授权被配发了本软件的人如上的权利,须在下列条件下: +// +// 上述版权声明和本许可声明应包含在该软件的所有副本或实质成分中。 +// +// 本软件是“如此”提供的,没有任何形式的明示或暗示的保证,包括但不限于对适销性、特定用途的适用性和不侵权的保证。 +// 在任何情况下,作者或版权持有人都不对任何索赔、损害或其他责任负责,无论这些追责来自合同、侵权或其它行为中, +// 还是产生于、源于或有关于本软件以及本软件的使用或其它处置。 + +using Microsoft.Extensions.Configuration; + +namespace Yi.Framework.AspNetCore.UnifyResult; + +/// +/// 规范化配置选项 +/// +public sealed class UnifyResultSettingsOptions +{ + /// + /// 设置返回 200 状态码列表 + /// 默认:401,403,如果设置为 null,则标识所有状态码都返回 200 + /// + public int[] Return200StatusCodes { get; set; } + + /// + /// 适配(篡改)Http 状态码(只支持短路状态码,比如 401,403,500 等) + /// + public int[][] AdaptStatusCodes { get; set; } + + /// + /// 是否支持 MVC 控制台规范化处理 + /// + public bool? SupportMvcController { get; set; } + + /// + /// 选项后期配置 + /// + /// + /// + public void PostConfigure(UnifyResultSettingsOptions options, IConfiguration configuration) + { + options.Return200StatusCodes ??= new[] { 401, 403 }; + options.SupportMvcController ??= false; + } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/ValidationMetadata.cs b/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/ValidationMetadata.cs new file mode 100644 index 00000000..84536678 --- /dev/null +++ b/Yi.Abp.Net8/framework/Yi.Framework.AspNetCore/UnifyResult/ValidationMetadata.cs @@ -0,0 +1,69 @@ +// MIT 许可证 +// +// 版权 © 2020-present 百小僧, 百签科技(广东)有限公司 和所有贡献者 +// +// 特此免费授予任何获得本软件副本和相关文档文件(下称“软件”)的人不受限制地处置该软件的权利, +// 包括不受限制地使用、复制、修改、合并、发布、分发、转授许可和/或出售该软件副本, +// 以及再授权被配发了本软件的人如上的权利,须在下列条件下: +// +// 上述版权声明和本许可声明应包含在该软件的所有副本或实质成分中。 +// +// 本软件是“如此”提供的,没有任何形式的明示或暗示的保证,包括但不限于对适销性、特定用途的适用性和不侵权的保证。 +// 在任何情况下,作者或版权持有人都不对任何索赔、损害或其他责任负责,无论这些追责来自合同、侵权或其它行为中, +// 还是产生于、源于或有关于本软件以及本软件的使用或其它处置。 + +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Yi.Framework.AspNetCore.UnifyResult; + +/// +/// 验证信息元数据 +/// +public sealed class ValidationMetadata +{ + /// + /// 验证结果 + /// + /// 返回字典或字符串类型 + public object ValidationResult { get; internal set; } + + /// + /// 异常消息 + /// + public string Message { get; internal set; } + + /// + /// 验证状态 + /// + public ModelStateDictionary ModelState { get; internal set; } + + /// + /// 错误码 + /// + public object ErrorCode { get; internal set; } + + /// + /// 错误码(没被复写过的 ErrorCode ) + /// + public object OriginErrorCode { get; internal set; } + + /// + /// 状态码 + /// + public int? StatusCode { get; internal set; } + + /// + /// 首个错误属性 + /// + public string FirstErrorProperty { get; internal set; } + + /// + /// 首个错误消息 + /// + public string FirstErrorMessage { get; internal set; } + + /// + /// 额外数据 + /// + public object Data { get; internal set; } +} \ No newline at end of file diff --git a/Yi.Abp.Net8/framework/Yi.Framework.Core/Extensions/HttpContextExtensions.cs b/Yi.Abp.Net8/framework/Yi.Framework.Core/Extensions/HttpContextExtensions.cs index d84b475a..663003ec 100644 --- a/Yi.Abp.Net8/framework/Yi.Framework.Core/Extensions/HttpContextExtensions.cs +++ b/Yi.Abp.Net8/framework/Yi.Framework.Core/Extensions/HttpContextExtensions.cs @@ -96,5 +96,15 @@ namespace Yi.Framework.Core.Extensions { return context.User.Claims.Where(x => x.Type == permissionsName).Select(x => x.Value).ToArray(); } + + /// + /// 判断是否是 WebSocket 请求 + /// + /// + /// + public static bool IsWebSocketRequest(this HttpContext context) + { + return context.WebSockets.IsWebSocketRequest || context.Request.Path == "/ws"; + } } } diff --git a/Yi.Abp.Net8/src/Yi.Abp.Application/Services/TestService.cs b/Yi.Abp.Net8/src/Yi.Abp.Application/Services/TestService.cs index 1296b856..cde02589 100644 --- a/Yi.Abp.Net8/src/Yi.Abp.Application/Services/TestService.cs +++ b/Yi.Abp.Net8/src/Yi.Abp.Application/Services/TestService.cs @@ -39,6 +39,17 @@ namespace Yi.Abp.Application.Services return name ?? "HelloWord"; } + /// + /// 异常处理 + /// + /// + [HttpGet("error")] + public string GetError() + { + throw new UserFriendlyException("业务异常"); + throw new Exception("系统异常"); + } + /// /// SqlSugar /// diff --git a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs index 49fda747..3d6e17fc 100644 --- a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs +++ b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs @@ -3,6 +3,8 @@ using System.Text; using System.Threading.RateLimiting; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using Newtonsoft.Json.Converters; @@ -11,6 +13,7 @@ using Volo.Abp.AspNetCore.ExceptionHandling; using Volo.Abp.AspNetCore.MultiTenancy; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc.AntiForgery; +using Volo.Abp.AspNetCore.Mvc.ExceptionHandling; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.Auditing; using Volo.Abp.Autofac; @@ -25,6 +28,7 @@ using Yi.Framework.AspNetCore.Authentication.OAuth.Gitee; using Yi.Framework.AspNetCore.Authentication.OAuth.QQ; using Yi.Framework.AspNetCore.Microsoft.AspNetCore.Builder; using Yi.Framework.AspNetCore.Microsoft.Extensions.DependencyInjection; +using Yi.Framework.AspNetCore.UnifyResult; using Yi.Framework.Bbs.Application; using Yi.Framework.Bbs.Application.Extensions; using Yi.Framework.ChatHub.Application; @@ -68,6 +72,10 @@ namespace Yi.Abp.Web optios.AlwaysLogSelectors.Add(x => Task.FromResult(true)); }); + //采用furion格式的规范化api,默认不开启,使用abp优雅的方式 + //你没看错。。。 + //service.AddFurionUnifyResultApi(); + //配置错误处理显示详情 Configure(options => { options.SendExceptionsDetailsToClients = true; }); @@ -299,6 +307,8 @@ namespace Yi.Abp.Web app.UseDefaultFiles(); app.UseDirectoryBrowser("/api/app/wwwroot"); + + // app.Properties.Add("_AbpExceptionHandlingMiddleware_Added",false); //工作单元 app.UseUnitOfWork();