统一服务端点架构,支持多端接口与数据库切换

重构项目结构,引入 Avalonia-Common、Avalonia-EFCore、Avalonia-Services,实现 API 与桌面端统一端点注册、过滤器、鉴权和标准响应格式。支持多数据库自动迁移与配置,集成 Serilog 日志系统。移除旧路由与控制器,提升接口一致性与可维护性。
This commit is contained in:
2026-05-11 14:35:34 +08:00
parent 99631df085
commit 271e9714ff
38 changed files with 2073 additions and 182 deletions
+64
View File
@@ -0,0 +1,64 @@
using System;
using System.Linq;
namespace Avalonia_Services.Core
{
/// <summary>
/// 端点列表打印工具 —— 在应用启动时输出所有已注册的拦截接口。
/// 类似 Swagger 的接口清单效果。
/// </summary>
public static class EndpointPrinter
{
/// <summary>
/// 打印所有已注册端点到控制台。
/// </summary>
public static void PrintEndpoints(ServiceEndpointCollection collection, string? title = null)
{
title ??= "API Endpoints";
var maxMethodLen = collection.Endpoints.Count > 0
? collection.Endpoints.Max(e => e.HttpMethod.Length)
: 4;
var maxPathLen = collection.Endpoints.Count > 0
? collection.Endpoints.Max(e => e.Pattern.Length)
: 8;
var totalWidth = maxMethodLen + maxPathLen + 5;
var separator = new string('─', Math.Max(totalWidth, 50));
Console.WriteLine();
Console.WriteLine($"╔═ {title} ═{new string('═', Math.Max(0, totalWidth - title.Length - 3))}╗");
Console.WriteLine($"║ {"Method".PadRight(maxMethodLen)} │ {"Path".PadRight(maxPathLen)} │ Auth ║");
Console.WriteLine($"╟{separator}╢");
foreach (var ep in collection.Endpoints.OrderBy(e => e.Pattern))
{
var auth = ep.RequireAuthorization ? (ep.Policy ?? "✓") : "—";
var methodColor = ep.HttpMethod switch
{
"GET" => ConsoleColor.Green,
"POST" => ConsoleColor.Blue,
"PUT" => ConsoleColor.Yellow,
"DELETE" => ConsoleColor.Red,
_ => ConsoleColor.Gray,
};
var savedColor = Console.ForegroundColor;
Console.Write("║ ");
Console.ForegroundColor = methodColor;
Console.Write(ep.HttpMethod.PadRight(maxMethodLen));
Console.ForegroundColor = savedColor;
Console.Write(" │ ");
Console.Write(ep.Pattern.PadRight(maxPathLen));
Console.Write(" │ ");
Console.Write(auth.PadRight(4));
Console.WriteLine(" ║");
}
Console.WriteLine($"╚{separator}╝");
Console.WriteLine($" Total: {collection.Endpoints.Count} endpoint(s)");
Console.WriteLine();
}
}
}
@@ -0,0 +1,93 @@
using Avalonia_Common.Core;
using System;
using System.Threading.Tasks;
namespace Avalonia_Services.Core
{
/// <summary>
/// 全局异常拦截过滤器 —— 自动包裹所有端点处理器,无需在每个方法中写 try-catch。
/// 所有未捕获异常会被转为统一的 ApiResponse 错误格式。
/// </summary>
public sealed class GlobalExceptionFilter : IEndpointFilter
{
private readonly bool _includeDetails;
/// <summary>
/// </summary>
/// <param name="includeDetails">是否在响应中包含异常详情(开发环境建议 true,生产环境 false</param>
public GlobalExceptionFilter(bool includeDetails = false)
{
_includeDetails = includeDetails;
}
public async Task InvokeAsync(ServiceEndpointContext context, EndpointFilterDelegate next)
{
try
{
await next(context);
}
catch (OperationCanceledException)
{
// 取消操作不视为错误
context.StatusCode = 499;
context.StatusMessage = "Client Closed Request";
context.ResponseBody = ApiResponse<object>.Fail(499, "请求已取消");
}
catch (UnauthorizedAccessException ex)
{
context.StatusCode = 401;
context.StatusMessage = "Unauthorized";
context.ResponseBody = ApiResponse<object>.Unauthorized(
_includeDetails ? ex.Message : "未授权访问");
}
catch (InvalidOperationException ex) when (ex.Message.Contains("not found", StringComparison.OrdinalIgnoreCase))
{
context.StatusCode = 404;
context.StatusMessage = "Not Found";
context.ResponseBody = ApiResponse<object>.NotFound(
_includeDetails ? ex.Message : "资源不存在");
}
catch (ArgumentException ex)
{
context.StatusCode = 400;
context.StatusMessage = "Bad Request";
context.ResponseBody = ApiResponse<object>.BadRequest(
_includeDetails ? ex.Message : "参数错误");
}
catch (Exception ex)
{
// 记录完整日志(无论是否返回详情)
LogException(context, ex);
context.StatusCode = 500;
context.StatusMessage = "Internal Server Error";
context.ResponseBody = ApiResponse<object>.ServerError(
_includeDetails ? ex.Message : "服务器内部错误,请联系管理员");
// 可选:在开发环境附加堆栈信息
if (_includeDetails)
{
// 通过 Items 传递额外调试信息
context.Items["ExceptionDetail"] = ex.ToString();
}
}
}
private static void LogException(ServiceEndpointContext context, Exception ex)
{
try
{
// 使用 Serilog(如果已配置)
Serilog.Log.Error(ex,
"全局异常拦截 | {Method} {Path} | {ExceptionType}: {Message}",
context.Method, context.Path, ex.GetType().Name, ex.Message);
}
catch
{
// Serilog 不可用时回退到 Console
Console.Error.WriteLine(
$"[ERROR] {context.Method} {context.Path} | {ex.GetType().Name}: {ex.Message}");
}
}
}
}
+38
View File
@@ -0,0 +1,38 @@
using System.Security.Claims;
namespace Avalonia_Services.Core
{
/// <summary>
/// 鉴权服务抽象 —— 各宿主按自己的方式实现(JWT / Cookie / Token 等)。
/// </summary>
public interface IAuthService
{
/// <summary>
/// 验证请求并返回用户主体;返回 null 表示未授权。
/// </summary>
Task<ClaimsPrincipal?> AuthenticateAsync(ServiceEndpointContext context);
/// <summary>
/// 检查当前用户是否有指定权限。
/// </summary>
Task<bool> AuthorizeAsync(ClaimsPrincipal user, string policy);
}
/// <summary>
/// 无需鉴权的默认实现(开发/公开 API 场景)。
/// </summary>
public sealed class AnonymousAuthService : IAuthService
{
public Task<ClaimsPrincipal?> AuthenticateAsync(ServiceEndpointContext context)
{
// 匿名用户,始终通过
var identity = new ClaimsIdentity("anonymous");
return Task.FromResult<ClaimsPrincipal?>(new ClaimsPrincipal(identity));
}
public Task<bool> AuthorizeAsync(ClaimsPrincipal user, string policy)
{
return Task.FromResult(true);
}
}
}
+40
View File
@@ -0,0 +1,40 @@
using System.Threading.Tasks;
namespace Avalonia_Services.Core
{
/// <summary>
/// 端点过滤器抽象 —— 在请求处理前后执行逻辑。
/// 类似于 ASP.NET Core 的 IEndpointFilter,但可在任何宿主中使用。
/// </summary>
public interface IEndpointFilter
{
/// <summary>
/// 过滤器执行方法。
/// 调用 next(ctx) 继续管道;不调用则短路。
/// </summary>
Task InvokeAsync(ServiceEndpointContext context, EndpointFilterDelegate next);
}
/// <summary>
/// 过滤器管道中的下一个委托。
/// </summary>
public delegate Task EndpointFilterDelegate(ServiceEndpointContext context);
/// <summary>
/// 用于包装匿名过滤器的简单实现。
/// </summary>
internal sealed class AnonymousEndpointFilter : IEndpointFilter
{
private readonly Func<ServiceEndpointContext, EndpointFilterDelegate, Task> _filter;
public AnonymousEndpointFilter(Func<ServiceEndpointContext, EndpointFilterDelegate, Task> filter)
{
_filter = filter;
}
public Task InvokeAsync(ServiceEndpointContext context, EndpointFilterDelegate next)
{
return _filter(context, next);
}
}
}
@@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Avalonia_Services.Core
{
/// <summary>
/// 单个端点定义。
/// </summary>
public class ServiceEndpoint
{
/// <summary>路由路径,如 "api/wData"</summary>
public string Pattern { get; init; } = string.Empty;
/// <summary>HTTP 方法(GET/POST/PUT/DELETE</summary>
public string HttpMethod { get; init; } = "GET";
/// <summary>端点名称(用于 OpenAPI / 日志)</summary>
public string? Name { get; set; }
/// <summary>端点处理器</summary>
public Func<ServiceEndpointContext, Task<object?>> Handler { get; init; } = _ => Task.FromResult<object?>(null);
/// <summary>该端点专属的过滤器(按顺序执行)</summary>
public List<IEndpointFilter> Filters { get; init; } = new();
/// <summary>是否需要鉴权</summary>
public bool RequireAuthorization { get; set; }
/// <summary>鉴权策略名</summary>
public string? Policy { get; set; }
/// <summary>
/// 设置端点名称(Fluent API)。
/// </summary>
public ServiceEndpoint WithName(string name)
{
Name = name;
return this;
}
}
/// <summary>
/// 端点集合 —— 所有端点的注册中心。在 Avalonia-Services 中统一配置。
/// </summary>
public class ServiceEndpointCollection
{
/// <summary>所有已注册的端点</summary>
public List<ServiceEndpoint> Endpoints { get; } = new();
/// <summary>作用于所有端点的全局过滤器</summary>
public List<IEndpointFilter> GlobalFilters { get; } = new();
/// <summary>
/// 注册一个端点。
/// </summary>
public ServiceEndpoint MapGet(string pattern, Func<ServiceEndpointContext, Task<object?>> handler)
{
return AddEndpoint(pattern, "GET", handler);
}
/// <summary>
/// 注册一个 POST 端点。
/// </summary>
public ServiceEndpoint MapPost(string pattern, Func<ServiceEndpointContext, Task<object?>> handler)
{
return AddEndpoint(pattern, "POST", handler);
}
/// <summary>
/// 注册一个 PUT 端点。
/// </summary>
public ServiceEndpoint MapPut(string pattern, Func<ServiceEndpointContext, Task<object?>> handler)
{
return AddEndpoint(pattern, "PUT", handler);
}
/// <summary>
/// 注册一个 DELETE 端点。
/// </summary>
public ServiceEndpoint MapDelete(string pattern, Func<ServiceEndpointContext, Task<object?>> handler)
{
return AddEndpoint(pattern, "DELETE", handler);
}
/// <summary>
/// 添加全局过滤器(作用于所有端点)。
/// </summary>
public ServiceEndpointCollection AddGlobalFilter(IEndpointFilter filter)
{
GlobalFilters.Add(filter);
return this;
}
/// <summary>
/// 通过匿名函数添加全局过滤器。
/// </summary>
public ServiceEndpointCollection AddGlobalFilter(Func<ServiceEndpointContext, EndpointFilterDelegate, Task> filter)
{
GlobalFilters.Add(new AnonymousEndpointFilter(filter));
return this;
}
private ServiceEndpoint AddEndpoint(string pattern, string method, Func<ServiceEndpointContext, Task<object?>> handler)
{
var endpoint = new ServiceEndpoint
{
Pattern = pattern,
HttpMethod = method,
Handler = handler,
};
Endpoints.Add(endpoint);
return endpoint;
}
}
/// <summary>
/// 构建器 —— 提供 Fluent API 来配置所有端点。
/// </summary>
public class ServiceEndpointBuilder
{
/// <summary>
/// 端点集合
/// </summary>
public ServiceEndpointCollection Endpoints { get; } = new();
/// <summary>
/// 鉴权服务(默认匿名)
/// </summary>
public IAuthService AuthService { get; set; } = new AnonymousAuthService();
/// <summary>
/// 配置端点(在此方法中调用 endpoints.MapGet 等)。
/// </summary>
public ServiceEndpointBuilder ConfigureEndpoints(Action<ServiceEndpointCollection> configure)
{
configure(Endpoints);
return this;
}
/// <summary>
/// 设置鉴权服务。
/// </summary>
public ServiceEndpointBuilder UseAuthService(IAuthService authService)
{
AuthService = authService;
return this;
}
/// <summary>
/// 构建最终的端点集合。
/// </summary>
public ServiceEndpointCollection Build()
{
return Endpoints;
}
}
}
@@ -0,0 +1,79 @@
using System.Collections.Generic;
namespace Avalonia_Services.Core
{
/// <summary>
/// 抽象的请求上下文,屏蔽不同宿主(ASP.NET Core / Desktop WebView)的差异。
/// </summary>
public class ServiceEndpointContext
{
/// <summary>
/// 请求路径,例如 "api/wData"
/// </summary>
public string Path { get; init; } = string.Empty;
/// <summary>
/// HTTP 方法(GET, POST, PUT, DELETE 等)
/// </summary>
public string Method { get; init; } = "GET";
/// <summary>
/// 请求头
/// </summary>
public Dictionary<string, string> Headers { get; init; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// 请求体(原始字符串)
/// </summary>
public string? Body { get; set; }
/// <summary>
/// 查询参数
/// </summary>
public Dictionary<string, string> Query { get; init; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// 响应状态码
/// </summary>
public int StatusCode { get; set; } = 200;
/// <summary>
/// 响应状态描述
/// </summary>
public string StatusMessage { get; set; } = "OK";
/// <summary>
/// 响应头
/// </summary>
public Dictionary<string, string> ResponseHeaders { get; set; } = new(StringComparer.OrdinalIgnoreCase)
{
["Content-Type"] = "application/json; charset=utf-8"
};
/// <summary>
/// 响应体
/// </summary>
public object? ResponseBody { get; set; }
/// <summary>
/// 存储在请求生命周期中的任意数据(由中间件/过滤器使用)
/// </summary>
public Dictionary<string, object?> Items { get; init; } = new();
/// <summary>
/// 获取请求头值
/// </summary>
public string? GetHeader(string key)
{
return Headers.TryGetValue(key, out var value) ? value : null;
}
/// <summary>
/// 设置响应头
/// </summary>
public void SetResponseHeader(string key, string value)
{
ResponseHeaders[key] = value;
}
}
}