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."