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

重构项目结构,引入 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
+14 -31
View File
@@ -1,3 +1,6 @@
using Avalonia_Services.Core;
using Avalonia_Services.Endpoints;
using Avalonia_Services.Extensions;
using Avalonia_Services.Services;
using Microsoft.Extensions.DependencyInjection;
using System;
@@ -8,42 +11,22 @@ namespace Avalonia_PC.Views
{
public partial class MainWindow
{
// 路由表:key = 接口路径(忽略大小写),value = 处理方法
// 新增接口:在此方法中添加一行 _routes["api/xxx"] = ctx => ...
private Dictionary<string, Func<RouteRequestContext, Task<object?>>> _routes = [];
/// <summary>
/// 统一端点适配器(替代原来的 _routes 字典)。
/// 所有端点在 Avalonia-Services/AppEndpoints.cs 中统一定义。
/// </summary>
private DesktopEndpointAdapter _endpointAdapter = null!;
// 服务容器,通过构造函数注入,路由注册时按需解析服务
/// <summary>
/// 服务容器,通过构造函数注入。
/// </summary>
private IServiceProvider _services = null!;
private void RegisterRoutes()
{
var weather = _services.GetRequiredService<WeatherForecastService>();
// 新增服务示例:var myService = _services.GetRequiredService<MyService>();
_routes = new Dictionary<string, Func<RouteRequestContext, Task<object?>>>(StringComparer.OrdinalIgnoreCase)
{
["api/getUser"] = _ => GetUserFromDatabaseAsync(),
["api/processData"] = ctx => ProcessDataAsync(ExtractInput(ctx)),
["api/wData"] = _ => Task.FromResult<object?>(weather.GetWeatherForecasts()),
};
}
/// <summary>
/// 示例:模拟读取用户数据。
/// </summary>
private static async Task<object?> GetUserFromDatabaseAsync()
{
await Task.Delay(100);
return new { id = 1, name = "张三", email = "zhangsan@example.com" };
}
/// <summary>
/// 示例:模拟处理输入数据。
/// </summary>
private static async Task<object?> ProcessDataAsync(string? input)
{
await Task.Delay(200);
return $"Processed: {input?.ToUpperInvariant()}";
// 从 DI 获取已构建的端点集合
var endpointCollection = _services.GetRequiredService<ServiceEndpointCollection>();
_endpointAdapter = endpointCollection.CreateAdapter(_services);
}
}
}
+26 -100
View File
@@ -236,7 +236,7 @@ namespace Avalonia_PC.Views
}
/// <summary>
/// 统一请求处理:构建上下文、处理 OPTIONS、按前缀分发并封装标准响应
/// 统一请求处理:构建上下文、处理 OPTIONS、使用统一端点适配器分发
/// </summary>
private async Task<AppResponse> HandleAppRequestAsync(
string? id,
@@ -257,7 +257,6 @@ namespace Avalonia_PC.Views
try
{
var uri = new Uri(rawUrl ?? throw new InvalidOperationException("请求地址不能为空。"));
var requestContext = CreateRouteRequestContext(uri, body);
if (string.Equals(method, "OPTIONS", StringComparison.OrdinalIgnoreCase))
{
@@ -267,12 +266,25 @@ namespace Avalonia_PC.Views
return response;
}
var routeResult = await DispatchByPrefixAsync(requestContext);
// 使用统一端点适配器处理请求
var (normalizedPath, queryParams) = ParseRequestUri(uri);
var routeResult = await _endpointAdapter.HandleRequestAsync(
path: normalizedPath,
method: method ?? "GET",
body: body,
headers: headers,
query: queryParams);
if (routeResult.IsMatched)
{
response.StatusCode = routeResult.StatusCode;
response.StatusMessage = routeResult.StatusMessage;
response.Body = BuildSuccessResponseBody(routeResult.Data);
foreach (var kvp in routeResult.ResponseHeaders)
{
response.Headers[kvp.Key] = kvp.Value;
}
return response;
}
@@ -291,31 +303,9 @@ namespace Avalonia_PC.Views
}
/// <summary>
/// 按路由表匹配并调用对应处理器
/// 从 URI 解析规范化路径和查询参数(供统一端点适配器使用)
/// </summary>
private async Task<RouteDispatchResult> DispatchByPrefixAsync(RouteRequestContext requestContext)
{
if (_routes.TryGetValue(requestContext.NormalizedPath, out var handler))
{
var data = await handler(requestContext);
return RouteDispatchResult.Success(data);
}
return RouteDispatchResult.NotMatched();
}
/// <summary>
/// 统一构建成功响应体,保持前后端响应结构一致。
/// </summary>
private static string BuildSuccessResponseBody(object? data)
{
return JsonSerializer.Serialize(new { success = true, data });
}
/// <summary>
/// 从 URI 解析路径段、查询参数和 body,构建路由上下文。
/// </summary>
private static RouteRequestContext CreateRouteRequestContext(Uri uri, string? body)
private static (string normalizedPath, Dictionary<string, string> query) ParseRequestUri(Uri uri)
{
var host = uri.Host ?? string.Empty;
var absolutePath = uri.AbsolutePath ?? string.Empty;
@@ -329,13 +319,15 @@ namespace Avalonia_PC.Views
var normalizedPath = string.Join('/', pathSegments);
var query = ParseQueryParameters(uri.Query);
return new RouteRequestContext
{
NormalizedPath = normalizedPath,
PathSegments = pathSegments,
Query = query,
Body = body,
};
return (normalizedPath, query);
}
/// <summary>
/// 统一构建成功响应体,保持前后端响应结构一致。
/// </summary>
private static string BuildSuccessResponseBody(object? data)
{
return JsonSerializer.Serialize(new { success = true, data });
}
/// <summary>
@@ -367,33 +359,6 @@ namespace Avalonia_PC.Views
return query;
}
/// <summary>
/// 按 body -> query -> path 的优先级提取业务输入参数。
/// </summary>
private static string ExtractInput(RouteRequestContext requestContext)
{
if (!string.IsNullOrWhiteSpace(requestContext.Body))
{
using var jsonDocument = JsonDocument.Parse(requestContext.Body);
if (jsonDocument.RootElement.TryGetProperty("input", out var inputProperty))
{
return inputProperty.GetString() ?? string.Empty;
}
}
if (requestContext.Query.TryGetValue("input", out var inputFromQuery) &&
!string.IsNullOrWhiteSpace(inputFromQuery))
{
return inputFromQuery;
}
if (requestContext.PathSegments.Length > 2)
{
return string.Join('/', requestContext.PathSegments.Skip(2));
}
return string.Empty;
}
/// <summary>
/// 创建桥接响应的默认 JSON/CORS 头。
@@ -660,45 +625,6 @@ namespace Avalonia_PC.Views
public Dictionary<string, string> Headers { get; set; } = new();
}
private sealed class RouteRequestContext
{
public string NormalizedPath { get; init; } = string.Empty;
public string[] PathSegments { get; init; } = [];
public Dictionary<string, string> Query { get; init; } = new(StringComparer.OrdinalIgnoreCase);
public string? Body { get; init; }
}
private sealed class RouteDispatchResult
{
public bool IsMatched { get; init; }
public int StatusCode { get; init; } = 200;
public string StatusMessage { get; init; } = "OK";
public object? Data { get; init; }
public static RouteDispatchResult Success(object? data)
{
return new RouteDispatchResult
{
IsMatched = true,
Data = data,
};
}
public static RouteDispatchResult NotMatched()
{
return new RouteDispatchResult
{
IsMatched = false,
};
}
}
#endregion
}
}