From c5f741e6a42a9528cb9fcdb27bcb32447dbd0e76 Mon Sep 17 00:00:00 2001
From: luoqian <2769838458@qq.com>
Date: Fri, 15 May 2026 15:26:46 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B0=86=E6=95=B0=E6=8D=AE=E5=BA=93?=
=?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=92=8C=E8=BF=81=E7=A7=BB=E9=9B=86=E4=B8=AD?=
=?UTF-8?q?=E5=88=B0=20EFCore=20=E9=A1=B9=E7=9B=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 将 AppDataContext、实体模型、Migrations 从 Avalonia-Services 移动到 Avalonia-EFCore
- 更新 API、PC、Services 中的数据库上下文和实体引用命名空间
- 在实体上显式绑定表名、字段名和数据库注释
- 更新 InitialCreate、Designer、Snapshot,使用新的表名、字段名和注释
- 新增 AppDataContextFactory,支持 dotnet ef 设计时创建 DbContext
- 新增本地 dotnet-ef 工具清单
- 新增一键生成迁移脚本 add-migration.ps1 / .cmd / .bat
- 启动时自动检测并执行未应用迁移
- 从 appsettings.json 读取数据库配置
---
.config/dotnet-tools.json | 13 ++
.gitignore | 4 +
Avalonia-API/Avalonia-API.csproj | 4 +
.../Configuration/ServicesConfiguration.cs | 11 +-
Avalonia-API/Program.cs | 3 +-
Avalonia-API/appsettings.json | 1 +
Avalonia-EFCore/Avalonia-EFCore.csproj | 4 +
.../Database/AppDataContext.cs | 9 +-
.../Database/AppDataContextFactory.cs | 13 ++
Avalonia-EFCore/Database/AppDbContext.cs | 6 +
.../Database/DatabaseConfiguration.cs | 6 +
Avalonia-EFCore/Database/DatabaseManager.cs | 56 +++++++--
.../20260514000100_InitialCreate.Designer.cs | 94 +++++++++++++++
.../20260514000100_InitialCreate.cs | 65 ++++++++++
...5_AutoMigration_20260515152037.Designer.cs | 113 ++++++++++++++++++
...0515072045_AutoMigration_20260515152037.cs | 30 +++++
.../Migrations/AppDataContextModelSnapshot.cs | 110 +++++++++++++++++
.../Models/UserEntity.cs | 21 +++-
.../Models/WeatherForecast.cs | 2 +-
.../Models/WeatherForecastEntity.cs | 20 +++-
Avalonia-PC/Program.cs | 1 -
Avalonia-Services/Endpoints/AppEndpoints.cs | 4 +-
.../Services/WeatherForecastService.cs | 2 +-
scripts/add-migration.bat | 2 +
scripts/add-migration.cmd | 2 +
scripts/add-migration.ps1 | 65 ++++++++++
26 files changed, 631 insertions(+), 30 deletions(-)
create mode 100644 .config/dotnet-tools.json
rename {Avalonia-Services => Avalonia-EFCore}/Database/AppDataContext.cs (84%)
create mode 100644 Avalonia-EFCore/Database/AppDataContextFactory.cs
create mode 100644 Avalonia-EFCore/Migrations/20260514000100_InitialCreate.Designer.cs
create mode 100644 Avalonia-EFCore/Migrations/20260514000100_InitialCreate.cs
create mode 100644 Avalonia-EFCore/Migrations/20260515072045_AutoMigration_20260515152037.Designer.cs
create mode 100644 Avalonia-EFCore/Migrations/20260515072045_AutoMigration_20260515152037.cs
create mode 100644 Avalonia-EFCore/Migrations/AppDataContextModelSnapshot.cs
rename {Avalonia-Services => Avalonia-EFCore}/Models/UserEntity.cs (51%)
rename {Avalonia-Services => Avalonia-EFCore}/Models/WeatherForecast.cs (87%)
rename {Avalonia-Services => Avalonia-EFCore}/Models/WeatherForecastEntity.cs (51%)
create mode 100644 scripts/add-migration.bat
create mode 100644 scripts/add-migration.cmd
create mode 100644 scripts/add-migration.ps1
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
new file mode 100644
index 0000000..9f80faf
--- /dev/null
+++ b/.config/dotnet-tools.json
@@ -0,0 +1,13 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "dotnet-ef": {
+ "version": "10.0.0",
+ "commands": [
+ "dotnet-ef"
+ ],
+ "rollForward": false
+ }
+ }
+}
diff --git a/.gitignore b/.gitignore
index aa66bfa..47ef72d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,7 @@
/Avalonia-EFCore/obj
/Avalonia-Common/bin
/Avalonia-Common/obj
+/Avalonia-API/logs
+/Avalonia-API/avalonia-api.db
+/Avalonia-API/avalonia-api.db-shm
+/Avalonia-API/avalonia-api.db-wal
diff --git a/Avalonia-API/Avalonia-API.csproj b/Avalonia-API/Avalonia-API.csproj
index 56c79b6..11e285e 100644
--- a/Avalonia-API/Avalonia-API.csproj
+++ b/Avalonia-API/Avalonia-API.csproj
@@ -9,6 +9,10 @@
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/Avalonia-API/Configuration/ServicesConfiguration.cs b/Avalonia-API/Configuration/ServicesConfiguration.cs
index 6d3a6a0..cced9cf 100644
--- a/Avalonia-API/Configuration/ServicesConfiguration.cs
+++ b/Avalonia-API/Configuration/ServicesConfiguration.cs
@@ -1,8 +1,8 @@
using Avalonia_EFCore.Database;
using Avalonia_Services.Core;
-using Avalonia_Services.Database;
using Avalonia_Services.Endpoints;
using Avalonia_Services.Services;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Avalonia_API.Configuration
@@ -13,15 +13,20 @@ namespace Avalonia_API.Configuration
/// 注册统一端点及其依赖的服务(含数据库)。
/// 所有业务端点定义在 Avalonia-Services/Endpoints/AppEndpoints.cs。
///
- public static IServiceCollection AddUnifiedApiServices(this IServiceCollection services)
+ public static IServiceCollection AddUnifiedApiServices(this IServiceCollection services, IConfiguration configuration)
{
// ---- 数据库 ----
// 从 appsettings.json 读取 DatabaseConfiguration 节
// 注册默认数据库提供程序(SQLite / MySQL / PostgreSQL / SqlServer)
DatabaseProviderRegistry.RegisterDefaults();
+ var databaseConfig = configuration
+ .GetSection(nameof(DatabaseConfiguration))
+ .Get()
+ ?? DatabaseConfiguration.ForSQLite("app.db");
+
// 注册 AppDataContext(共享数据上下文)
- services.AddAppDatabase(DatabaseConfiguration.ForSQLite("app.db"));
+ services.AddAppDatabase(databaseConfig);
// ---- 业务服务 ----
services.AddScoped();
diff --git a/Avalonia-API/Program.cs b/Avalonia-API/Program.cs
index e6c539a..554dcb7 100644
--- a/Avalonia-API/Program.cs
+++ b/Avalonia-API/Program.cs
@@ -3,7 +3,6 @@ using Avalonia_API.Extensions;
using Avalonia_Common.Infrastructure;
using Avalonia_EFCore.Database;
using Avalonia_Services.Core;
-using Avalonia_Services.Database;
using Serilog;
// 初始化日志系统
@@ -22,7 +21,7 @@ try
builder.Services.AddOpenApi();
// 注册统一端点及业务服务(入口在 Avalonia-Services/Endpoints/AppEndpoints.cs)
- builder.Services.AddUnifiedApiServices();
+ builder.Services.AddUnifiedApiServices(builder.Configuration);
var app = builder.Build();
diff --git a/Avalonia-API/appsettings.json b/Avalonia-API/appsettings.json
index 3b39ebc..e0c98cf 100644
--- a/Avalonia-API/appsettings.json
+++ b/Avalonia-API/appsettings.json
@@ -10,6 +10,7 @@
"Provider": "SQLite",
"ConnectionString": "Data Source=avalonia-api.db",
"AutoMigrate": true,
+ "RecreateDatabase": false,
"EnableDetailedLog": false,
"Timeout": 30
}
diff --git a/Avalonia-EFCore/Avalonia-EFCore.csproj b/Avalonia-EFCore/Avalonia-EFCore.csproj
index 7c05d0f..6640c83 100644
--- a/Avalonia-EFCore/Avalonia-EFCore.csproj
+++ b/Avalonia-EFCore/Avalonia-EFCore.csproj
@@ -14,6 +14,10 @@
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/Avalonia-Services/Database/AppDataContext.cs b/Avalonia-EFCore/Database/AppDataContext.cs
similarity index 84%
rename from Avalonia-Services/Database/AppDataContext.cs
rename to Avalonia-EFCore/Database/AppDataContext.cs
index 150d91b..a01a194 100644
--- a/Avalonia-Services/Database/AppDataContext.cs
+++ b/Avalonia-EFCore/Database/AppDataContext.cs
@@ -1,8 +1,7 @@
-using Avalonia_EFCore.Database;
-using Avalonia_Services.Models;
+using Avalonia_EFCore.Models;
using Microsoft.EntityFrameworkCore;
-namespace Avalonia_Services.Database
+namespace Avalonia_EFCore.Database
{
///
/// 应用数据库上下文 —— 继承自 Avalonia-EFCore 的 AppDbContext。
@@ -23,13 +22,13 @@ namespace Avalonia_Services.Database
modelBuilder.Entity(entity =>
{
- entity.HasKey(e => e.Id);
+ entity.HasKey(e => e.Id).HasName("pk-weather-forecast");
entity.Property(e => e.Summary).HasMaxLength(200);
});
modelBuilder.Entity(entity =>
{
- entity.HasKey(e => e.Id);
+ entity.HasKey(e => e.Id).HasName("pk-user");
entity.Property(e => e.Email).HasMaxLength(200);
});
}
diff --git a/Avalonia-EFCore/Database/AppDataContextFactory.cs b/Avalonia-EFCore/Database/AppDataContextFactory.cs
new file mode 100644
index 0000000..1a332c4
--- /dev/null
+++ b/Avalonia-EFCore/Database/AppDataContextFactory.cs
@@ -0,0 +1,13 @@
+using Microsoft.EntityFrameworkCore.Design;
+
+namespace Avalonia_EFCore.Database
+{
+ public class AppDataContextFactory : IDesignTimeDbContextFactory
+ {
+ public AppDataContext CreateDbContext(string[] args)
+ {
+ DatabaseProviderRegistry.RegisterDefaults();
+ return new AppDataContext(DatabaseConfiguration.ForSQLite("avalonia-api.db"));
+ }
+ }
+}
diff --git a/Avalonia-EFCore/Database/AppDbContext.cs b/Avalonia-EFCore/Database/AppDbContext.cs
index c81ff66..9a0686e 100644
--- a/Avalonia-EFCore/Database/AppDbContext.cs
+++ b/Avalonia-EFCore/Database/AppDbContext.cs
@@ -48,6 +48,12 @@ namespace Avalonia_EFCore.Database
}
optionsBuilder.EnableDetailedErrors();
+ optionsBuilder.EnableSensitiveDataLogging(config.EnableDetailedLog);
+
+ if (config.EnableDetailedLog)
+ {
+ optionsBuilder.LogTo(Console.WriteLine, Microsoft.Extensions.Logging.LogLevel.Information);
+ }
}
///
diff --git a/Avalonia-EFCore/Database/DatabaseConfiguration.cs b/Avalonia-EFCore/Database/DatabaseConfiguration.cs
index e54b448..0f36c53 100644
--- a/Avalonia-EFCore/Database/DatabaseConfiguration.cs
+++ b/Avalonia-EFCore/Database/DatabaseConfiguration.cs
@@ -32,6 +32,12 @@ namespace Avalonia_EFCore.Database
/// 是否在启动时自动执行迁移
public bool AutoMigrate { get; set; } = true;
+ ///
+ /// 是否在迁移前删除并重建当前连接指向的数据库。
+ /// 仅用于切换数据库类型或本地开发重建库;生产环境默认必须保持 false。
+ ///
+ public bool RecreateDatabase { get; set; } = false;
+
/// 是否启用详细日志(会打印 SQL 语句)
public bool EnableDetailedLog { get; set; } = false;
diff --git a/Avalonia-EFCore/Database/DatabaseManager.cs b/Avalonia-EFCore/Database/DatabaseManager.cs
index 8ce4170..de17e18 100644
--- a/Avalonia-EFCore/Database/DatabaseManager.cs
+++ b/Avalonia-EFCore/Database/DatabaseManager.cs
@@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
@@ -32,21 +33,37 @@ namespace Avalonia_EFCore.Database
///
public async Task InitializeAsync(Action? seeder = null)
{
- // 1. 测试数据库连接
- var canConnect = await CanConnectAsync();
- if (!canConnect)
- {
- throw new InvalidOperationException(
- $"无法连接到数据库 [{_config.Provider}],请检查连接字符串和数据库服务状态。");
- }
+ AppLog.Information(
+ "正在初始化数据库 Provider={Provider}, AppVersion={AppVersion}",
+ _config.Provider,
+ GetApplicationVersion());
- // 2. 自动迁移(如果启用)
+ // 1. 自动迁移(如果启用)。MigrateAsync 会按迁移历史顺序执行全部待处理迁移,
+ // 支持用户从较旧软件版本直接升级到当前版本。
if (_config.AutoMigrate)
{
+ if (_config.RecreateDatabase)
+ {
+ AppLog.Warning(
+ "RecreateDatabase=true,将删除并重建当前连接指向的数据库。Provider={Provider}",
+ _config.Provider);
+
+ await _context.Database.EnsureDeletedAsync();
+ }
+
await MigrateAsync();
}
+ else
+ {
+ var canConnect = await CanConnectAsync();
+ if (!canConnect)
+ {
+ throw new InvalidOperationException(
+ $"无法连接到数据库 [{_config.Provider}],请检查连接字符串和数据库服务状态。");
+ }
+ }
- // 3. 种子数据
+ // 2. 种子数据
if (seeder != null)
{
seeder(_context, _serviceProvider);
@@ -77,12 +94,21 @@ namespace Avalonia_EFCore.Database
{
try
{
+ var appliedMigrations = (await _context.Database.GetAppliedMigrationsAsync()).ToList();
var pendingMigrations = await _context.Database.GetPendingMigrationsAsync();
if (pendingMigrations.Any())
{
+ if (appliedMigrations.Count == 0)
+ {
+ AppLog.Information(
+ "未检测到已应用迁移,将按当前 Provider={Provider} 从 0 构建完整表结构",
+ _config.Provider);
+ }
+
AppLog.Information(
- "检测到 {Count} 个待执行的数据库迁移: {Migrations}",
+ "当前已应用 {AppliedCount} 个迁移,检测到 {PendingCount} 个待执行迁移: {Migrations}",
+ appliedMigrations.Count,
pendingMigrations.Count(),
string.Join(", ", pendingMigrations));
@@ -102,6 +128,16 @@ namespace Avalonia_EFCore.Database
}
}
+ private static string GetApplicationVersion()
+ {
+ var assembly = Assembly.GetEntryAssembly() ?? typeof(TContext).Assembly;
+ return assembly
+ .GetCustomAttribute()
+ ?.InformationalVersion
+ ?? assembly.GetName().Version?.ToString()
+ ?? "unknown";
+ }
+
///
/// 获取数据库当前版本信息。
///
diff --git a/Avalonia-EFCore/Migrations/20260514000100_InitialCreate.Designer.cs b/Avalonia-EFCore/Migrations/20260514000100_InitialCreate.Designer.cs
new file mode 100644
index 0000000..82082bf
--- /dev/null
+++ b/Avalonia-EFCore/Migrations/20260514000100_InitialCreate.Designer.cs
@@ -0,0 +1,94 @@
+using System;
+using Avalonia_EFCore.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Avalonia_EFCore.Migrations
+{
+ [DbContext(typeof(AppDataContext))]
+ [Migration("20260514000100_InitialCreate")]
+ partial class InitialCreate
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.0");
+
+ modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
+ {
+ b.Property("Id")
+ .HasComment("用户主键")
+ .HasColumnName("id")
+ .ValueGeneratedOnAdd();
+
+ b.Property("CreatedAt")
+ .HasComment("创建时间")
+ .HasColumnName("created-at");
+
+ b.Property("Email")
+ .HasComment("用户邮箱")
+ .HasColumnName("email")
+ .HasMaxLength(200);
+
+ b.Property("Name")
+ .HasComment("用户名称")
+ .HasColumnName("name")
+ .HasMaxLength(100);
+
+ b.Property("UpdatedAt")
+ .HasComment("更新时间")
+ .HasColumnName("updated-at");
+
+ b.HasKey("Id")
+ .HasName("pk-user");
+
+ b.ToTable("user", t =>
+ {
+ t.HasComment("用户实体,演示数据库 CRUD 操作");
+ });
+ });
+
+ modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
+ {
+ b.Property("Id")
+ .HasComment("天气预报主键")
+ .HasColumnName("id")
+ .ValueGeneratedOnAdd();
+
+ b.Property("CreatedAt")
+ .HasComment("创建时间")
+ .HasColumnName("created-at");
+
+ b.Property("Date")
+ .HasComment("预报日期")
+ .HasColumnName("date");
+
+ b.Property("Summary")
+ .HasComment("天气摘要")
+ .HasColumnName("summary")
+ .HasMaxLength(200);
+
+ b.Property("TemperatureC")
+ .HasComment("摄氏温度")
+ .HasColumnName("temperature-c");
+
+ b.Property("UpdatedAt")
+ .HasComment("更新时间")
+ .HasColumnName("updated-at");
+
+ b.HasKey("Id")
+ .HasName("pk-weather-forecast");
+
+ b.ToTable("weather-forecast", t =>
+ {
+ t.HasComment("天气预报数据实体");
+ });
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Avalonia-EFCore/Migrations/20260514000100_InitialCreate.cs b/Avalonia-EFCore/Migrations/20260514000100_InitialCreate.cs
new file mode 100644
index 0000000..0915dc1
--- /dev/null
+++ b/Avalonia-EFCore/Migrations/20260514000100_InitialCreate.cs
@@ -0,0 +1,65 @@
+using System;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace Avalonia_EFCore.Migrations
+{
+ ///
+ /// 初始数据库基线。后续软件版本只追加新的 Migration,不修改已发布 Migration。
+ ///
+ public partial class InitialCreate : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "user",
+ columns: table => new
+ {
+ Id = table.Column(name: "id", nullable: false, comment: "用户主键")
+ .Annotation("SqlServer:Identity", "1, 1")
+ .Annotation("Sqlite:Autoincrement", true)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
+ .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
+ Name = table.Column(name: "name", maxLength: 100, nullable: true, comment: "用户名称"),
+ Email = table.Column(name: "email", maxLength: 200, nullable: true, comment: "用户邮箱"),
+ CreatedAt = table.Column(name: "created-at", nullable: false, comment: "创建时间"),
+ UpdatedAt = table.Column(name: "updated-at", nullable: false, comment: "更新时间")
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk-user", x => x.Id);
+ },
+ comment: "用户实体,演示数据库 CRUD 操作");
+
+ migrationBuilder.CreateTable(
+ name: "weather-forecast",
+ columns: table => new
+ {
+ Id = table.Column(name: "id", nullable: false, comment: "天气预报主键")
+ .Annotation("SqlServer:Identity", "1, 1")
+ .Annotation("Sqlite:Autoincrement", true)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
+ .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
+ Date = table.Column(name: "date", nullable: false, comment: "预报日期"),
+ TemperatureC = table.Column(name: "temperature-c", nullable: false, comment: "摄氏温度"),
+ Summary = table.Column(name: "summary", maxLength: 200, nullable: true, comment: "天气摘要"),
+ CreatedAt = table.Column(name: "created-at", nullable: false, comment: "创建时间"),
+ UpdatedAt = table.Column(name: "updated-at", nullable: false, comment: "更新时间")
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("pk-weather-forecast", x => x.Id);
+ },
+ comment: "天气预报数据实体");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(name: "weather-forecast");
+ migrationBuilder.DropTable(name: "user");
+ }
+ }
+}
diff --git a/Avalonia-EFCore/Migrations/20260515072045_AutoMigration_20260515152037.Designer.cs b/Avalonia-EFCore/Migrations/20260515072045_AutoMigration_20260515152037.Designer.cs
new file mode 100644
index 0000000..fa1adb9
--- /dev/null
+++ b/Avalonia-EFCore/Migrations/20260515072045_AutoMigration_20260515152037.Designer.cs
@@ -0,0 +1,113 @@
+//
+using System;
+using Avalonia_EFCore.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Avalonia_EFCore.Migrations
+{
+ [DbContext(typeof(AppDataContext))]
+ [Migration("20260515072045_AutoMigration_20260515152037")]
+ partial class AutoMigration_20260515152037
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
+
+ modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("id")
+ .HasComment("用户主键");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("created-at")
+ .HasComment("创建时间");
+
+ b.Property("Email")
+ .HasMaxLength(200)
+ .HasColumnType("TEXT")
+ .HasColumnName("email")
+ .HasComment("用户邮箱");
+
+ b.Property("Name")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT")
+ .HasColumnName("name")
+ .HasComment("用户名称");
+
+ b.Property("PhoneNumber")
+ .HasMaxLength(50)
+ .HasColumnType("TEXT")
+ .HasColumnName("phone-number")
+ .HasComment("电话号码");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("updated-at")
+ .HasComment("更新时间");
+
+ b.HasKey("Id")
+ .HasName("pk-user");
+
+ b.ToTable("user", t =>
+ {
+ t.HasComment("用户实体,演示数据库 CRUD 操作");
+ });
+ });
+
+ modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("id")
+ .HasComment("天气预报主键");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("created-at")
+ .HasComment("创建时间");
+
+ b.Property("Date")
+ .HasColumnType("TEXT")
+ .HasColumnName("date")
+ .HasComment("预报日期");
+
+ b.Property("Summary")
+ .HasMaxLength(200)
+ .HasColumnType("TEXT")
+ .HasColumnName("summary")
+ .HasComment("天气摘要");
+
+ b.Property("TemperatureC")
+ .HasColumnType("INTEGER")
+ .HasColumnName("temperature-c")
+ .HasComment("摄氏温度");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("updated-at")
+ .HasComment("更新时间");
+
+ b.HasKey("Id")
+ .HasName("pk-weather-forecast");
+
+ b.ToTable("weather-forecast", t =>
+ {
+ t.HasComment("天气预报数据实体");
+ });
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Avalonia-EFCore/Migrations/20260515072045_AutoMigration_20260515152037.cs b/Avalonia-EFCore/Migrations/20260515072045_AutoMigration_20260515152037.cs
new file mode 100644
index 0000000..e211d68
--- /dev/null
+++ b/Avalonia-EFCore/Migrations/20260515072045_AutoMigration_20260515152037.cs
@@ -0,0 +1,30 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Avalonia_EFCore.Migrations
+{
+ ///
+ public partial class AutoMigration_20260515152037 : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "phone-number",
+ table: "user",
+ type: "TEXT",
+ maxLength: 50,
+ nullable: true,
+ comment: "电话号码");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "phone-number",
+ table: "user");
+ }
+ }
+}
diff --git a/Avalonia-EFCore/Migrations/AppDataContextModelSnapshot.cs b/Avalonia-EFCore/Migrations/AppDataContextModelSnapshot.cs
new file mode 100644
index 0000000..13ded97
--- /dev/null
+++ b/Avalonia-EFCore/Migrations/AppDataContextModelSnapshot.cs
@@ -0,0 +1,110 @@
+//
+using System;
+using Avalonia_EFCore.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Avalonia_EFCore.Migrations
+{
+ [DbContext(typeof(AppDataContext))]
+ partial class AppDataContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
+
+ modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("id")
+ .HasComment("用户主键");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("created-at")
+ .HasComment("创建时间");
+
+ b.Property("Email")
+ .HasMaxLength(200)
+ .HasColumnType("TEXT")
+ .HasColumnName("email")
+ .HasComment("用户邮箱");
+
+ b.Property("Name")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT")
+ .HasColumnName("name")
+ .HasComment("用户名称");
+
+ b.Property("PhoneNumber")
+ .HasMaxLength(50)
+ .HasColumnType("TEXT")
+ .HasColumnName("phone-number")
+ .HasComment("电话号码");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("updated-at")
+ .HasComment("更新时间");
+
+ b.HasKey("Id")
+ .HasName("pk-user");
+
+ b.ToTable("user", t =>
+ {
+ t.HasComment("用户实体,演示数据库 CRUD 操作");
+ });
+ });
+
+ modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("id")
+ .HasComment("天气预报主键");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("created-at")
+ .HasComment("创建时间");
+
+ b.Property("Date")
+ .HasColumnType("TEXT")
+ .HasColumnName("date")
+ .HasComment("预报日期");
+
+ b.Property("Summary")
+ .HasMaxLength(200)
+ .HasColumnType("TEXT")
+ .HasColumnName("summary")
+ .HasComment("天气摘要");
+
+ b.Property("TemperatureC")
+ .HasColumnType("INTEGER")
+ .HasColumnName("temperature-c")
+ .HasComment("摄氏温度");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("updated-at")
+ .HasComment("更新时间");
+
+ b.HasKey("Id")
+ .HasName("pk-weather-forecast");
+
+ b.ToTable("weather-forecast", t =>
+ {
+ t.HasComment("天气预报数据实体");
+ });
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Avalonia-Services/Models/UserEntity.cs b/Avalonia-EFCore/Models/UserEntity.cs
similarity index 51%
rename from Avalonia-Services/Models/UserEntity.cs
rename to Avalonia-EFCore/Models/UserEntity.cs
index aa94ea6..284511d 100644
--- a/Avalonia-Services/Models/UserEntity.cs
+++ b/Avalonia-EFCore/Models/UserEntity.cs
@@ -1,26 +1,43 @@
+using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
-namespace Avalonia_Services.Models
+namespace Avalonia_EFCore.Models
{
///
/// 用户实体 —— 演示数据库 CRUD 操作。
///
- [Table("Users")]
+ [Comment("用户实体,演示数据库 CRUD 操作")]
+ [Table("user")]
public class UserEntity
{
[Key]
+ [Comment("用户主键")]
+ [Column("id")]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
+ [Comment("用户名称")]
+ [Column("name")]
[MaxLength(100)]
public string? Name { get; set; }
+ [Comment("用户邮箱")]
+ [Column("email")]
[MaxLength(200)]
public string? Email { get; set; }
+ [Comment("电话号码")]
+ [Column("phone-number")]
+ [MaxLength(50)]
+ public string? PhoneNumber { get; set; }
+
+ [Comment("创建时间")]
+ [Column("created-at")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+ [Comment("更新时间")]
+ [Column("updated-at")]
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
}
diff --git a/Avalonia-Services/Models/WeatherForecast.cs b/Avalonia-EFCore/Models/WeatherForecast.cs
similarity index 87%
rename from Avalonia-Services/Models/WeatherForecast.cs
rename to Avalonia-EFCore/Models/WeatherForecast.cs
index c708921..85a17e7 100644
--- a/Avalonia-Services/Models/WeatherForecast.cs
+++ b/Avalonia-EFCore/Models/WeatherForecast.cs
@@ -1,4 +1,4 @@
-namespace Avalonia_Services.Models
+namespace Avalonia_EFCore.Models
{
public class WeatherForecast
{
diff --git a/Avalonia-Services/Models/WeatherForecastEntity.cs b/Avalonia-EFCore/Models/WeatherForecastEntity.cs
similarity index 51%
rename from Avalonia-Services/Models/WeatherForecastEntity.cs
rename to Avalonia-EFCore/Models/WeatherForecastEntity.cs
index e2b1460..0ff5e26 100644
--- a/Avalonia-Services/Models/WeatherForecastEntity.cs
+++ b/Avalonia-EFCore/Models/WeatherForecastEntity.cs
@@ -1,27 +1,41 @@
+using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
-namespace Avalonia_Services.Models
+namespace Avalonia_EFCore.Models
{
///
- /// 天气预报数据实体 —— 对应数据库中的 WeatherForecasts 表。
+ /// 天气预报数据实体。
///
- [Table("WeatherForecasts")]
+ [Comment("天气预报数据实体")]
+ [Table("weather-forecast")]
public class WeatherForecastEntity
{
[Key]
+ [Comment("天气预报主键")]
+ [Column("id")]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
+ [Comment("预报日期")]
+ [Column("date")]
public DateOnly Date { get; set; }
+ [Comment("摄氏温度")]
+ [Column("temperature-c")]
public int TemperatureC { get; set; }
+ [Comment("天气摘要")]
+ [Column("summary")]
[MaxLength(200)]
public string? Summary { get; set; }
+ [Comment("创建时间")]
+ [Column("created-at")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
+ [Comment("更新时间")]
+ [Column("updated-at")]
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
}
diff --git a/Avalonia-PC/Program.cs b/Avalonia-PC/Program.cs
index c4e3444..9b098ab 100644
--- a/Avalonia-PC/Program.cs
+++ b/Avalonia-PC/Program.cs
@@ -3,7 +3,6 @@ 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;
diff --git a/Avalonia-Services/Endpoints/AppEndpoints.cs b/Avalonia-Services/Endpoints/AppEndpoints.cs
index 8115c8b..fb45604 100644
--- a/Avalonia-Services/Endpoints/AppEndpoints.cs
+++ b/Avalonia-Services/Endpoints/AppEndpoints.cs
@@ -1,7 +1,7 @@
using Avalonia_Common.Core;
+using Avalonia_EFCore.Database;
+using Avalonia_EFCore.Models;
using Avalonia_Services.Core;
-using Avalonia_Services.Database;
-using Avalonia_Services.Models;
using Avalonia_Services.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
diff --git a/Avalonia-Services/Services/WeatherForecastService.cs b/Avalonia-Services/Services/WeatherForecastService.cs
index 9e65602..94a1ece 100644
--- a/Avalonia-Services/Services/WeatherForecastService.cs
+++ b/Avalonia-Services/Services/WeatherForecastService.cs
@@ -1,4 +1,4 @@
-using Avalonia_Services.Models;
+using Avalonia_EFCore.Models;
namespace Avalonia_Services.Services
{
diff --git a/scripts/add-migration.bat b/scripts/add-migration.bat
new file mode 100644
index 0000000..c5ce612
--- /dev/null
+++ b/scripts/add-migration.bat
@@ -0,0 +1,2 @@
+@echo off
+powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0add-migration.ps1" %*
diff --git a/scripts/add-migration.cmd b/scripts/add-migration.cmd
new file mode 100644
index 0000000..c5ce612
--- /dev/null
+++ b/scripts/add-migration.cmd
@@ -0,0 +1,2 @@
+@echo off
+powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0add-migration.ps1" %*
diff --git a/scripts/add-migration.ps1 b/scripts/add-migration.ps1
new file mode 100644
index 0000000..9fce894
--- /dev/null
+++ b/scripts/add-migration.ps1
@@ -0,0 +1,65 @@
+param(
+ [string]$Name,
+ [string]$Context = "AppDataContext",
+ [string]$Project = "Avalonia-EFCore/Avalonia-EFCore.csproj",
+ [string]$StartupProject = "Avalonia-API/Avalonia-API.csproj",
+ [string]$OutputDir = "Migrations"
+)
+
+$ErrorActionPreference = "Stop"
+
+$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
+Set-Location $repoRoot
+
+if ([string]::IsNullOrWhiteSpace($Name)) {
+ $Name = "AutoMigration_{0}" -f (Get-Date -Format "yyyyMMddHHmmss")
+}
+
+Write-Host "Restoring local dotnet tools..."
+dotnet tool restore
+if ($LASTEXITCODE -ne 0) {
+ throw "dotnet tool restore failed."
+}
+
+Write-Host "Generating migration '$Name'..."
+dotnet tool run dotnet-ef migrations add $Name `
+ --project $Project `
+ --startup-project $StartupProject `
+ --context $Context `
+ --output-dir $OutputDir
+if ($LASTEXITCODE -ne 0) {
+ throw "dotnet ef migrations add failed."
+}
+
+$migrationDir = Join-Path (Split-Path $Project -Parent) $OutputDir
+$migrationFile = Get-ChildItem $migrationDir -Filter "*_$Name.cs" |
+ Where-Object { $_.Name -notlike "*.Designer.cs" } |
+ Sort-Object LastWriteTime -Descending |
+ Select-Object -First 1
+
+if ($null -eq $migrationFile) {
+ throw "Migration file was not found for '$Name'."
+}
+
+$content = Get-Content $migrationFile.FullName -Raw
+$upMatch = [regex]::Match($content, "protected override void Up\(MigrationBuilder migrationBuilder\)\s*\{(?.*?)\n\s*\}", "Singleline")
+$downMatch = [regex]::Match($content, "protected override void Down\(MigrationBuilder migrationBuilder\)\s*\{(?.*?)\n\s*\}", "Singleline")
+
+$upBody = if ($upMatch.Success) { $upMatch.Groups["body"].Value.Trim() } else { "" }
+$downBody = if ($downMatch.Success) { $downMatch.Groups["body"].Value.Trim() } else { "" }
+
+if ([string]::IsNullOrWhiteSpace($upBody) -and [string]::IsNullOrWhiteSpace($downBody)) {
+ Write-Host "No model changes were detected. Removing empty migration '$Name'..."
+ dotnet tool run dotnet-ef migrations remove --force `
+ --project $Project `
+ --startup-project $StartupProject `
+ --context $Context
+ if ($LASTEXITCODE -ne 0) {
+ throw "dotnet ef migrations remove failed."
+ }
+ exit 0
+}
+
+Write-Host "Migration generated:"
+Write-Host " $($migrationFile.FullName)"
+Write-Host "Review the migration, then start the app. Startup will automatically apply pending migrations."