2026-05-11 14:35:34 +08:00
|
|
|
|
using Avalonia_Services.Core;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Avalonia_Services.Extensions
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Desktop (Avalonia-PC) 端点适配器。
|
|
|
|
|
|
/// 将统一端点转换为桌面端可用的路由处理器,支持过滤器和鉴权管道。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class DesktopEndpointAdapter
|
|
|
|
|
|
{
|
2026-05-18 11:35:13 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 统一端点集合。
|
|
|
|
|
|
/// </summary>
|
2026-05-11 14:35:34 +08:00
|
|
|
|
private readonly ServiceEndpointCollection _endpoints;
|
2026-05-18 11:35:13 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 鉴权服务。
|
|
|
|
|
|
/// </summary>
|
2026-05-11 14:35:34 +08:00
|
|
|
|
private readonly IAuthService _authService;
|
2026-05-18 11:35:13 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// DI 服务提供程序。
|
|
|
|
|
|
/// </summary>
|
2026-05-11 14:35:34 +08:00
|
|
|
|
private readonly IServiceProvider _serviceProvider;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 匹配后的路由结果(与原有 RouteDispatchResult 兼容)。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class RouteResult
|
|
|
|
|
|
{
|
2026-05-18 11:35:13 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取是否匹配到路由。
|
|
|
|
|
|
/// </summary>
|
2026-05-11 14:35:34 +08:00
|
|
|
|
public bool IsMatched { get; init; }
|
2026-05-18 11:35:13 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取 HTTP 状态码。
|
|
|
|
|
|
/// </summary>
|
2026-05-11 14:35:34 +08:00
|
|
|
|
public int StatusCode { get; init; } = 200;
|
2026-05-18 11:35:13 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取状态描述文本。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public string StatusMessage { get; init; } = "";
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取响应数据。
|
|
|
|
|
|
/// </summary>
|
2026-05-11 14:35:34 +08:00
|
|
|
|
public object? Data { get; init; }
|
2026-05-18 11:35:13 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取响应头字典。
|
|
|
|
|
|
/// </summary>
|
2026-05-11 14:35:34 +08:00
|
|
|
|
public Dictionary<string, string> ResponseHeaders { get; init; } = new();
|
|
|
|
|
|
|
2026-05-18 11:35:13 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 创建成功响应结果。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="data">响应数据。</param>
|
|
|
|
|
|
/// <param name="ctx">端点上下文。</param>
|
|
|
|
|
|
/// <returns>路由结果。</returns>
|
2026-05-11 14:35:34 +08:00
|
|
|
|
public static RouteResult Success(object? data, ServiceEndpointContext ctx)
|
|
|
|
|
|
{
|
|
|
|
|
|
return new RouteResult
|
|
|
|
|
|
{
|
|
|
|
|
|
IsMatched = true,
|
|
|
|
|
|
StatusCode = ctx.StatusCode,
|
|
|
|
|
|
StatusMessage = ctx.StatusMessage,
|
|
|
|
|
|
Data = data,
|
|
|
|
|
|
ResponseHeaders = new Dictionary<string, string>(ctx.ResponseHeaders, StringComparer.OrdinalIgnoreCase),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 11:35:13 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 创建 404 未找到响应。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <returns>表示未匹配的路由结果。</returns>
|
2026-05-11 14:35:34 +08:00
|
|
|
|
public static RouteResult NotFound() => new()
|
|
|
|
|
|
{
|
|
|
|
|
|
IsMatched = false,
|
|
|
|
|
|
StatusCode = 404,
|
|
|
|
|
|
StatusMessage = "Not Found",
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 11:35:13 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 初始化桌面端点适配器。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="endpoints">端点集合。</param>
|
|
|
|
|
|
/// <param name="authService">鉴权服务。</param>
|
|
|
|
|
|
/// <param name="serviceProvider">DI 服务提供程序。</param>
|
2026-05-11 14:35:34 +08:00
|
|
|
|
public DesktopEndpointAdapter(
|
|
|
|
|
|
ServiceEndpointCollection endpoints,
|
|
|
|
|
|
IAuthService authService,
|
|
|
|
|
|
IServiceProvider serviceProvider)
|
|
|
|
|
|
{
|
|
|
|
|
|
_endpoints = endpoints;
|
|
|
|
|
|
_authService = authService;
|
|
|
|
|
|
_serviceProvider = serviceProvider;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 处理来自前端(WebView2 Bridge)的请求。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="path">规范化路径,如 "api/wData"</param>
|
|
|
|
|
|
/// <param name="method">HTTP 方法</param>
|
|
|
|
|
|
/// <param name="body">请求体字符串</param>
|
|
|
|
|
|
/// <param name="headers">请求头字典</param>
|
|
|
|
|
|
/// <param name="query">查询参数字典</param>
|
|
|
|
|
|
public async Task<RouteResult> HandleRequestAsync(
|
|
|
|
|
|
string path,
|
|
|
|
|
|
string method,
|
|
|
|
|
|
string? body,
|
|
|
|
|
|
Dictionary<string, string>? headers = null,
|
|
|
|
|
|
Dictionary<string, string>? query = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 查找匹配的端点(忽略大小写 + 方法匹配)
|
|
|
|
|
|
var endpoint = _endpoints.Endpoints.FirstOrDefault(e =>
|
2026-05-15 17:35:07 +08:00
|
|
|
|
e.SupportsHost(EndpointHostTarget.Pc) &&
|
2026-05-11 14:35:34 +08:00
|
|
|
|
string.Equals(e.Pattern, path, StringComparison.OrdinalIgnoreCase) &&
|
|
|
|
|
|
string.Equals(e.HttpMethod, method, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
|
|
|
|
|
|
|
if (endpoint is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return RouteResult.NotFound();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建上下文
|
|
|
|
|
|
var ctx = new ServiceEndpointContext
|
|
|
|
|
|
{
|
|
|
|
|
|
Path = path,
|
|
|
|
|
|
Method = method,
|
|
|
|
|
|
Body = body,
|
|
|
|
|
|
Headers = headers ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
|
|
|
|
|
|
Query = query ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
|
|
|
|
|
|
Items = { ["ServiceProvider"] = _serviceProvider },
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
// 1. 鉴权检查
|
|
|
|
|
|
if (endpoint.RequireAuthorization)
|
|
|
|
|
|
{
|
|
|
|
|
|
var user = await _authService.AuthenticateAsync(ctx);
|
|
|
|
|
|
if (user is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
ctx.StatusCode = 401;
|
|
|
|
|
|
ctx.StatusMessage = "Unauthorized";
|
|
|
|
|
|
ctx.ResponseBody = new { success = false, error = "Unauthorized" };
|
|
|
|
|
|
return RouteResult.Success(ctx.ResponseBody, ctx);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 17:35:07 +08:00
|
|
|
|
if (endpoint.Roles.Count > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
var authorized = await _authService.AuthorizeAsync(user, $"roles:{string.Join(',', endpoint.Roles)}");
|
|
|
|
|
|
if (!authorized)
|
|
|
|
|
|
{
|
|
|
|
|
|
ctx.StatusCode = 403;
|
|
|
|
|
|
ctx.StatusMessage = "Forbidden";
|
|
|
|
|
|
ctx.ResponseBody = new { success = false, error = "Forbidden" };
|
|
|
|
|
|
return RouteResult.Success(ctx.ResponseBody, ctx);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (!string.IsNullOrEmpty(endpoint.Policy))
|
2026-05-11 14:35:34 +08:00
|
|
|
|
{
|
|
|
|
|
|
var authorized = await _authService.AuthorizeAsync(user, endpoint.Policy);
|
|
|
|
|
|
if (!authorized)
|
|
|
|
|
|
{
|
|
|
|
|
|
ctx.StatusCode = 403;
|
|
|
|
|
|
ctx.StatusMessage = "Forbidden";
|
|
|
|
|
|
ctx.ResponseBody = new { success = false, error = "Forbidden" };
|
|
|
|
|
|
return RouteResult.Success(ctx.ResponseBody, ctx);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ctx.Items["User"] = user;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 构建过滤管道:全局过滤器 → 端点过滤器 → 处理器
|
|
|
|
|
|
var pipeline = BuildPipeline(endpoint);
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 执行管道
|
|
|
|
|
|
await pipeline(ctx);
|
|
|
|
|
|
|
|
|
|
|
|
return RouteResult.Success(ctx.ResponseBody, ctx);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
ctx.StatusCode = 500;
|
|
|
|
|
|
ctx.StatusMessage = "Internal Server Error";
|
|
|
|
|
|
ctx.ResponseBody = new { success = false, error = ex.Message };
|
|
|
|
|
|
return RouteResult.Success(ctx.ResponseBody, ctx);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 构建过滤管道(全局过滤器 + 端点过滤器 → 端点处理器)。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private EndpointFilterDelegate BuildPipeline(ServiceEndpoint endpoint)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 最内层:端点处理器
|
|
|
|
|
|
EndpointFilterDelegate handler = async (ctx) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
ctx.ResponseBody = await endpoint.Handler(ctx);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 先包裹端点专属过滤器(后注册的先执行)
|
|
|
|
|
|
var filters = new List<IEndpointFilter>();
|
|
|
|
|
|
filters.AddRange(_endpoints.GlobalFilters);
|
|
|
|
|
|
filters.AddRange(endpoint.Filters);
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = filters.Count - 1; i >= 0; i--)
|
|
|
|
|
|
{
|
|
|
|
|
|
var filter = filters[i];
|
|
|
|
|
|
var next = handler;
|
|
|
|
|
|
handler = (ctx) => filter.InvokeAsync(ctx, next);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return handler;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Desktop 端的辅助扩展。不依赖 IServiceCollection(由宿主项目自行完成 DI 注册)。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static class DesktopServiceExtensions
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 快速构建 DesktopEndpointAdapter(用于非 DI 场景如 MainWindow)。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static DesktopEndpointAdapter CreateAdapter(
|
|
|
|
|
|
this ServiceEndpointCollection endpoints,
|
|
|
|
|
|
IServiceProvider serviceProvider)
|
|
|
|
|
|
{
|
|
|
|
|
|
var auth = (serviceProvider.GetService(typeof(IAuthService)) as IAuthService) ?? new AnonymousAuthService();
|
|
|
|
|
|
return new DesktopEndpointAdapter(endpoints, auth, serviceProvider);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|