统一服务端点架构,支持多端接口与数据库切换
重构项目结构,引入 Avalonia-Common、Avalonia-EFCore、Avalonia-Services,实现 API 与桌面端统一端点注册、过滤器、鉴权和标准响应格式。支持多数据库自动迁移与配置,集成 Serilog 日志系统。移除旧路由与控制器,提升接口一致性与可维护性。
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user