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

重构项目结构,引入 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
+9 -1
View File
@@ -8,13 +8,19 @@
</PropertyGroup>
<ItemGroup>
<Folder Include="Models\" />
<AvaloniaResource Include="Assets\**" />
<Content Include="www\**\*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<AvaloniaXaml Remove="Models\**" />
<Compile Remove="Models\**" />
<EmbeddedResource Remove="Models\**" />
<None Remove="Models\**" />
</ItemGroup>
<ItemGroup>
<None Include=".github\copilot-instructions.md" />
</ItemGroup>
@@ -35,5 +41,7 @@
<ItemGroup>
<ProjectReference Include="..\Avalonia-Services\Avalonia-Services.csproj" />
<ProjectReference Include="..\Avalonia-Common\Avalonia-Common.csproj" />
<ProjectReference Include="..\Avalonia-EFCore\Avalonia-EFCore.csproj" />
</ItemGroup>
</Project>
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup>
<ActiveDebugProfile>Avalonia-PC</ActiveDebugProfile>
</PropertyGroup>
</Project>
+2
View File
@@ -1,5 +1,7 @@
<Solution>
<Project Path="../Avalonia-API/Avalonia-API.csproj" Id="e33aba9a-a56b-4f6b-8eaa-3acbed65ebad" />
<Project Path="../Avalonia-Common/Avalonia-Common.csproj" Id="caed4118-2161-4382-90b8-35fb4efe3b5f" />
<Project Path="../Avalonia-EFCore/Avalonia-EFCore.csproj" Id="64557501-62a7-4863-b2bf-1570b8c6fecb" />
<Project Path="../Avalonia-Services/Avalonia-Services.csproj" Id="b8757cf9-5422-4c67-acae-3c967c95f866" />
<Project Path="../avalonia-web-react/avalonia-web-react.esproj">
<Build />
+35 -3
View File
@@ -1,7 +1,13 @@
using Avalonia;
using Avalonia_Common.Infrastructure;
using Avalonia_EFCore.Database;
using Avalonia_PC.Views;
using Avalonia_Services.Core;
using Avalonia_Services.Database;
using Avalonia_Services.Endpoints;
using Avalonia_Services.Services;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using System;
namespace Avalonia_PC
@@ -10,14 +16,23 @@ namespace Avalonia_PC
{
public static IServiceProvider Services { get; private set; } = null!;
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args)
{
// 初始化日志系统
AppLog.Initialize(LoggingConfiguration.CreateDefaultLogger(logDir: "logs"));
AppLog.Information("Avalonia-PC 正在启动...");
ConfigureServices();
// 初始化数据库(自动迁移 + 种子数据)
Services.InitializeDatabase<AppDataContext>();
// 启动时打印所有拦截的接口
var endpoints = Services.GetRequiredService<ServiceEndpointCollection>();
EndpointPrinter.PrintEndpoints(endpoints, "Avalonia-PC 拦截接口列表");
#if DEBUG
// 开启 WebView2 远程调试,启动后在 Edge 中访问 edge://inspect 调试网页
Environment.SetEnvironmentVariable(
@@ -30,7 +45,24 @@ namespace Avalonia_PC
private static void ConfigureServices()
{
var services = new ServiceCollection();
// ---- 数据库 ----
// 注册默认数据库提供程序(SQLite / MySQL / PostgreSQL / SqlServer
DatabaseProviderRegistry.RegisterDefaults();
// 桌面端固定使用 SQLite 本地数据库
services.AddAppDatabase<AppDataContext>(DatabaseConfiguration.ForSQLite("app.db"));
// ---- 业务服务 ----
services.AddSingleton<WeatherForecastService>();
// ---- 统一端点 ----
var endpointBuilder = new ServiceEndpointBuilder();
AppEndpoints.Configure(endpointBuilder);
var endpoints = endpointBuilder.Build();
services.AddSingleton(endpoints);
// 注册 Window
services.AddTransient<MainWindow>(sp => new MainWindow(sp));
Services = services.BuildServiceProvider();
@@ -0,0 +1,11 @@
{
"profiles": {
"Avalonia-PC": {
"commandName": "Project"
},
"WSL": {
"commandName": "WSL2",
"distributionName": ""
}
}
}
+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
}
}