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

重构项目结构,引入 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
+24
View File
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>Avalonia_EFCore</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia-Common\Avalonia-Common.csproj" />
</ItemGroup>
</Project>
+93
View File
@@ -0,0 +1,93 @@
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Avalonia_EFCore.Database
{
/// <summary>
/// 应用数据库上下文基类 —— 自动根据 DatabaseConfiguration 选择数据库提供程序。
/// 所有业务 DbContext 继承此类即可获得多数据库支持。
/// </summary>
public abstract class AppDbContext(DatabaseConfiguration dbConfig) : DbContext
{
private readonly DatabaseConfiguration _dbConfig = dbConfig;
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (optionsBuilder.IsConfigured) return;
ConfigureProvider(optionsBuilder, _dbConfig);
if (_dbConfig.EnableDetailedLog)
{
optionsBuilder.LogTo(Console.WriteLine, Microsoft.Extensions.Logging.LogLevel.Information);
}
// 启用详细的 EF Core 错误信息
optionsBuilder.EnableDetailedErrors();
optionsBuilder.EnableSensitiveDataLogging(_dbConfig.EnableDetailedLog);
}
/// <summary>
/// 根据配置选择数据库提供程序。
/// 使用注册模式,由宿主项目注册具体的提供程序实现。
/// </summary>
public static void ConfigureProvider(DbContextOptionsBuilder optionsBuilder, DatabaseConfiguration config)
{
if (DatabaseProviderRegistry.TryGet(config.Provider, out var configurator))
{
configurator(optionsBuilder, config.ConnectionString, config.Timeout);
}
else
{
throw new NotSupportedException(
$"数据库提供程序 {config.Provider} 未注册。" +
$"请在宿主项目中安装对应的 EF Core NuGet 包并调用 DatabaseProviderRegistry.Register()。");
}
optionsBuilder.EnableDetailedErrors();
}
/// <summary>
/// 保存时自动设置时间戳。
/// </summary>
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
SetTimestamps();
return base.SaveChanges(acceptAllChangesOnSuccess);
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{
SetTimestamps();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
private void SetTimestamps()
{
var entries = ChangeTracker.Entries()
.Where(e => e.State == EntityState.Added || e.State == EntityState.Modified);
foreach (var entry in entries)
{
var entity = entry.Entity;
// 使用反射设置 CreatedAt / UpdatedAt(如果存在)
var createdAtProp = entity.GetType().GetProperty("CreatedAt");
var updatedAtProp = entity.GetType().GetProperty("UpdatedAt");
if (entry.State == EntityState.Added && createdAtProp != null)
{
createdAtProp.SetValue(entity, DateTime.UtcNow);
}
if (updatedAtProp != null)
{
updatedAtProp.SetValue(entity, DateTime.UtcNow);
}
}
}
}
}
@@ -0,0 +1,87 @@
namespace Avalonia_EFCore.Database
{
/// <summary>
/// 支持的数据库提供程序类型。
/// </summary>
public enum DatabaseProvider
{
/// <summary>SQLite(本地文件数据库,无需安装,跨平台)</summary>
SQLite,
/// <summary>MySQL / MariaDB</summary>
MySQL,
/// <summary>PostgreSQL</summary>
PostgreSQL,
/// <summary>SQL Server</summary>
SqlServer
}
/// <summary>
/// 数据库连接配置 —— 在 appsettings.json 中配置。
/// </summary>
public class DatabaseConfiguration
{
/// <summary>数据库提供程序</summary>
public DatabaseProvider Provider { get; set; } = DatabaseProvider.SQLite;
/// <summary>连接字符串</summary>
public string ConnectionString { get; set; } = "Data Source=app.db";
/// <summary>是否在启动时自动执行迁移</summary>
public bool AutoMigrate { get; set; } = true;
/// <summary>是否启用详细日志(会打印 SQL 语句)</summary>
public bool EnableDetailedLog { get; set; } = false;
/// <summary>连接超时(秒)</summary>
public int Timeout { get; set; } = 30;
// ---- 快捷构建方法 ----
/// <summary>SQLite 本地数据库</summary>
public static DatabaseConfiguration ForSQLite(string dataSource = "app.db")
{
return new DatabaseConfiguration
{
Provider = DatabaseProvider.SQLite,
ConnectionString = $"Data Source={dataSource}",
AutoMigrate = true,
};
}
/// <summary>MySQL 数据库</summary>
public static DatabaseConfiguration ForMySQL(string server, string database, string user, string password, uint port = 3306)
{
return new DatabaseConfiguration
{
Provider = DatabaseProvider.MySQL,
ConnectionString = $"Server={server};Port={port};Database={database};User={user};Password={password};",
};
}
/// <summary>PostgreSQL 数据库</summary>
public static DatabaseConfiguration ForPostgreSQL(string host, string database, string username, string password, int port = 5432)
{
return new DatabaseConfiguration
{
Provider = DatabaseProvider.PostgreSQL,
ConnectionString = $"Host={host};Port={port};Database={database};Username={username};Password={password};",
};
}
/// <summary>SQL Server 数据库</summary>
public static DatabaseConfiguration ForSqlServer(string server, string database, string? user = null, string? password = null)
{
var connStr = string.IsNullOrEmpty(user)
? $"Server={server};Database={database};Trusted_Connection=True;TrustServerCertificate=True;"
: $"Server={server};Database={database};User Id={user};Password={password};TrustServerCertificate=True;";
return new DatabaseConfiguration
{
Provider = DatabaseProvider.SqlServer,
ConnectionString = connStr,
};
}
}
}
@@ -0,0 +1,51 @@
using Microsoft.Extensions.DependencyInjection;
namespace Avalonia_EFCore.Database
{
/// <summary>
/// 数据库服务注册扩展 —— 在 Program.cs 中一行配置数据库。
/// </summary>
public static class DatabaseExtensions
{
/// <summary>
/// 注册数据库上下文及相关服务。
/// </summary>
/// <typeparam name="TContext">继承自 AppDbContext 的业务 DbContext</typeparam>
public static IServiceCollection AddAppDatabase<TContext>(
this IServiceCollection services,
DatabaseConfiguration config)
where TContext : AppDbContext
{
// 注册配置
services.AddSingleton(config);
// 注册 DbContext
services.AddDbContext<TContext>(options =>
{
AppDbContext.ConfigureProvider(options, config);
});
// 注册数据库管理器
services.AddScoped<DatabaseManager<TContext>>();
return services;
}
/// <summary>
/// 初始化数据库(在应用启动时调用一次)。
/// </summary>
public static IServiceProvider InitializeDatabase<TContext>(
this IServiceProvider serviceProvider,
Action<TContext, IServiceProvider?>? seeder = null)
where TContext : AppDbContext
{
using var scope = serviceProvider.CreateScope();
var dbManager = scope.ServiceProvider.GetRequiredService<DatabaseManager<TContext>>();
// 同步等待初始化(启动时阻塞)
dbManager.InitializeAsync(seeder).GetAwaiter().GetResult();
return serviceProvider;
}
}
}
+154
View File
@@ -0,0 +1,154 @@
using Avalonia_Common.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Avalonia_EFCore.Database
{
/// <summary>
/// 数据库管理器 —— 负责连接测试、自动迁移、种子数据、版本检查。
/// 在应用启动时调用,确保数据库结构与应用代码同步。
/// </summary>
public class DatabaseManager<TContext> where TContext : AppDbContext
{
private readonly TContext _context;
private readonly DatabaseConfiguration _config;
private readonly IServiceProvider? _serviceProvider;
public DatabaseManager(TContext context, DatabaseConfiguration config, IServiceProvider? serviceProvider = null)
{
_context = context;
_config = config;
_serviceProvider = serviceProvider;
}
/// <summary>
/// 初始化数据库:测试连接 → 自动迁移 → 种子数据。
/// </summary>
public async Task InitializeAsync(Action<TContext, IServiceProvider?>? seeder = null)
{
// 1. 测试数据库连接
var canConnect = await CanConnectAsync();
if (!canConnect)
{
throw new InvalidOperationException(
$"无法连接到数据库 [{_config.Provider}],请检查连接字符串和数据库服务状态。");
}
// 2. 自动迁移(如果启用)
if (_config.AutoMigrate)
{
await MigrateAsync();
}
// 3. 种子数据
if (seeder != null)
{
seeder(_context, _serviceProvider);
await _context.SaveChangesAsync();
}
}
/// <summary>
/// 测试数据库连接是否正常。
/// </summary>
public async Task<bool> CanConnectAsync()
{
try
{
return await _context.Database.CanConnectAsync();
}
catch
{
return false;
}
}
/// <summary>
/// 执行待处理的迁移。
/// 使用 EF Core 原生迁移机制,自动检测并应用 Schema 变更。
/// </summary>
public async Task MigrateAsync()
{
try
{
var pendingMigrations = await _context.Database.GetPendingMigrationsAsync();
if (pendingMigrations.Any())
{
AppLog.Information(
"检测到 {Count} 个待执行的数据库迁移: {Migrations}",
pendingMigrations.Count(),
string.Join(", ", pendingMigrations));
await _context.Database.MigrateAsync();
AppLog.Information("数据库迁移完成({Count} 个迁移已应用)", pendingMigrations.Count());
}
else
{
AppLog.Information("数据库已是最新版本,无需迁移");
}
}
catch (Exception ex)
{
AppLog.Error(ex, "数据库迁移失败");
throw;
}
}
/// <summary>
/// 获取数据库当前版本信息。
/// </summary>
public async Task<DatabaseVersionInfo> GetVersionInfoAsync()
{
var appliedMigrations = await _context.Database.GetAppliedMigrationsAsync();
var pendingMigrations = await _context.Database.GetPendingMigrationsAsync();
return new DatabaseVersionInfo
{
Provider = _config.Provider.ToString(),
AppliedMigrations = appliedMigrations.ToList(),
PendingMigrations = pendingMigrations.ToList(),
IsLatest = !pendingMigrations.Any(),
CanConnect = await CanConnectAsync(),
};
}
/// <summary>
/// 生成从指定迁移到最新版本的 SQL 脚本(用于生产环境审计)。
/// </summary>
public string GenerateMigrationScript(string? fromMigration = null)
{
var migrator = _context.GetService<IMigrator>();
return fromMigration is null
? migrator.GenerateScript()
: migrator.GenerateScript(fromMigration);
}
/// <summary>
/// 确保数据库已创建(不执行迁移,适用于简单场景)。
/// </summary>
public bool EnsureCreated()
{
return _context.Database.EnsureCreated();
}
}
/// <summary>
/// 数据库版本信息 DTO。
/// </summary>
public class DatabaseVersionInfo
{
public string Provider { get; set; } = string.Empty;
public List<string> AppliedMigrations { get; set; } = new();
public List<string> PendingMigrations { get; set; } = new();
public bool IsLatest { get; set; }
public bool CanConnect { get; set; }
}
}
@@ -0,0 +1,56 @@
using Microsoft.EntityFrameworkCore;
namespace Avalonia_EFCore.Database
{
/// <summary>
/// 数据库提供程序注册表 —— 统一注册所有支持的提供程序配置委托。
/// 具体使用哪个提供程序由各宿主项目决定:
/// Avalonia-API:从 appsettings.json 的 DatabaseConfiguration 节读取;
/// Avalonia-PC :固定使用 SQLite。
/// </summary>
public static class DatabaseProviderRegistry
{
/// <summary>
/// 提供程序配置委托:optionsBuilder, connectionString, timeout → void
/// </summary>
public delegate void ProviderConfigurator(DbContextOptionsBuilder optionsBuilder, string connectionString, int timeout);
private static readonly Dictionary<DatabaseProvider, ProviderConfigurator> _providers = new();
/// <summary>
/// 注册一个数据库提供程序。
/// </summary>
public static void Register(DatabaseProvider provider, ProviderConfigurator configurator)
{
_providers[provider] = configurator;
}
/// <summary>
/// 尝试获取注册的提供程序配置。
/// </summary>
public static bool TryGet(DatabaseProvider provider, out ProviderConfigurator configurator)
{
return _providers.TryGetValue(provider, out configurator!);
}
/// <summary>
/// 注册所有内置提供程序的默认配置(四个包均已内置在 Avalonia-EFCore 中)。
/// 注册完成后由调用方根据自身需求选择具体的 <see cref="DatabaseProvider"/>。
/// </summary>
public static void RegisterDefaults()
{
Register(DatabaseProvider.SQLite, (opts, cs, timeout) =>
opts.UseSqlite(cs, o => o.CommandTimeout(timeout)));
Register(DatabaseProvider.SqlServer, (opts, cs, timeout) =>
opts.UseSqlServer(cs, o => { o.CommandTimeout(timeout); o.EnableRetryOnFailure(3); }));
Register(DatabaseProvider.PostgreSQL, (opts, cs, timeout) =>
opts.UseNpgsql(cs, o => { o.CommandTimeout(timeout); o.EnableRetryOnFailure(3); }));
Register(DatabaseProvider.MySQL, (opts, cs, timeout) =>
opts.UseMySql(cs, ServerVersion.AutoDetect(cs), o => { o.CommandTimeout(timeout); o.EnableRetryOnFailure(3); }));
}
}
}