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