189 lines
5.3 KiB
C#
189 lines
5.3 KiB
C#
using MAX.Framework.Dtos;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.IO;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
|
|
namespace MAX.Framework.Commons
|
|
{
|
|
/// <summary>
|
|
/// 统一状态码中间件
|
|
/// 统一返回200 固定格式
|
|
/// </summary>
|
|
internal class UnifyStatusCodeMiddleware
|
|
{
|
|
|
|
private readonly RequestDelegate _next;
|
|
private readonly ILogger<UnifyStatusCodeMiddleware> _logger;
|
|
|
|
// 静态复用,全局唯一的内存池管理器
|
|
private static readonly RecyclableMemoryStreamManager _streamManager = new();
|
|
|
|
// 需要直接透传、不做缓冲的 Content-Type 前缀
|
|
private static readonly string[] _passthroughPrefixes =
|
|
{
|
|
"image/",
|
|
"audio/",
|
|
"video/",
|
|
"application/octet-stream",
|
|
"application/zip",
|
|
"application/pdf",
|
|
"application/x-msdownload",
|
|
"text/event-stream"
|
|
};
|
|
|
|
public UnifyStatusCodeMiddleware(RequestDelegate next, ILogger<UnifyStatusCodeMiddleware> logger)
|
|
{
|
|
_next = next;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task InvokeAsync(HttpContext context)
|
|
{
|
|
// 1. 只处理 /api 路径(按需调整或删除)
|
|
if (!context.Request.Path.StartsWithSegments("/api"))
|
|
{
|
|
await _next(context);
|
|
return;
|
|
}
|
|
|
|
// 2. 二进制 / 流式响应:不缓冲,直接透传
|
|
var accept = context.Request.Headers.Accept.ToString();
|
|
if (IsPassthroughByRequest(context) || IsPassthroughByAccept(accept))
|
|
{
|
|
await _next(context);
|
|
return;
|
|
}
|
|
|
|
var originalBody = context.Response.Body;
|
|
using var buffer = _streamManager.GetStream();
|
|
|
|
context.Response.Body = buffer;
|
|
|
|
try
|
|
{
|
|
await _next(context);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 兜底:Filter 没抓到的异常
|
|
_logger.LogError(ex, "未处理异常: {Path}", context.Request.Path);
|
|
|
|
// 清空已写入的内容,准备返回 500 统一格式
|
|
buffer.SetLength(0);
|
|
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
|
}
|
|
|
|
var statusCode = context.Response.StatusCode;
|
|
var contentType = context.Response.ContentType ?? string.Empty;
|
|
|
|
// 3. 响应体如果本身就是二进制/流式,透传不处理
|
|
if (IsPassthroughByResponseContentType(contentType))
|
|
{
|
|
buffer.Position = 0;
|
|
context.Response.Body = originalBody;
|
|
await buffer.CopyToAsync(originalBody);
|
|
return;
|
|
}
|
|
|
|
// 4. 2xx 且已有响应体:透传(已被 UnifyResultFilter 包装过,不再包一层)
|
|
if (statusCode >= 200 && statusCode < 300 && buffer.Length > 0)
|
|
{
|
|
buffer.Position = 0;
|
|
context.Response.Body = originalBody;
|
|
await buffer.CopyToAsync(originalBody);
|
|
return;
|
|
}
|
|
|
|
// 5. 非 2xx:统一改写为 200 + ApiResult
|
|
var (code, msg) = MapStatusCode(statusCode);
|
|
|
|
// 如果是 2xx 但响应体为空(少见,例如 204),也当作成功
|
|
if (statusCode >= 200 && statusCode < 300)
|
|
{
|
|
code = (int)ResultCode.Success;
|
|
msg = "success";
|
|
}
|
|
|
|
var result = statusCode >= 200 && statusCode < 300
|
|
? ApiResult.Ok()
|
|
: ApiResult.Fail(msg, code);
|
|
|
|
// 恢复原始流并写出
|
|
context.Response.Body = originalBody;
|
|
context.Response.StatusCode = StatusCodes.Status200OK;
|
|
context.Response.ContentType = "application/json; charset=utf-8";
|
|
context.Response.ContentLength = null;
|
|
|
|
// 清理可能存在的旧响应头
|
|
context.Response.Headers.Remove("WWW-Authenticate");
|
|
|
|
await context.Response.WriteAsJsonAsync(result, new JsonSerializerOptions
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
|
});
|
|
|
|
}
|
|
|
|
private static bool IsPassthroughByRequest(HttpContext context)
|
|
{
|
|
// 上传大文件等场景,请求体大,避免缓冲
|
|
var contentLength = context.Request.ContentLength;
|
|
if (contentLength.HasValue && contentLength.Value > 1_000_000) // >1MB
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Range 请求(断点续传)不缓冲
|
|
if (context.Request.Headers.ContainsKey("Range"))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool IsPassthroughByAccept(string accept)
|
|
{
|
|
if (string.IsNullOrEmpty(accept)) return false;
|
|
|
|
foreach (var prefix in _passthroughPrefixes)
|
|
{
|
|
if (accept.Contains(prefix, StringComparison.OrdinalIgnoreCase))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static bool IsPassthroughByResponseContentType(string contentType)
|
|
{
|
|
if (string.IsNullOrEmpty(contentType)) return false;
|
|
|
|
foreach (var prefix in _passthroughPrefixes)
|
|
{
|
|
if (contentType.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static (int code, string msg) MapStatusCode(int statusCode) => statusCode switch
|
|
{
|
|
400 => ((int)ResultCode.ValidationError, "请求参数错误"),
|
|
401 => ((int)ResultCode.Unauthorized, "未授权,请先登录"),
|
|
403 => ((int)ResultCode.Forbidden, "禁止访问"),
|
|
404 => ((int)ResultCode.NotFound, "资源不存在"),
|
|
405 => ((int)ResultCode.Fail, "请求方法不允许"),
|
|
429 => ((int)ResultCode.Fail, "请求过于频繁"),
|
|
500 => ((int)ResultCode.ServerError, "服务器内部错误"),
|
|
502 => ((int)ResultCode.ServerError, "网关错误"),
|
|
503 => ((int)ResultCode.ServerError, "服务暂不可用"),
|
|
504 => ((int)ResultCode.ServerError, "网关超时"),
|
|
_ => ((int)ResultCode.Fail, $"请求失败({statusCode})")
|
|
};
|
|
}
|
|
}
|