using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Data.Sqlite; using MiGu.Server.Wms; using MiGu.Server.SimpleFields; namespace MiGu.Server.Persistence; public static class PlatformPersistence { public static IServiceCollection AddPlatformPersistence(this IServiceCollection services, IConfiguration configuration) { services.AddDbContext((sp, options) => { var env = sp.GetRequiredService(); var provider = configuration["Database:Provider"] ?? "sqlite"; var connection = ResolveConnectionString(configuration, env, provider); switch (provider.Trim().ToLowerInvariant()) { case "sqlite": options.UseSqlite(connection); break; case "mysql": options.UseMySql(connection, ServerVersion.AutoDetect(connection)); break; case "postgres": case "postgresql": case "npgsql": options.UseNpgsql(connection); break; case "sqlserver": case "mssql": options.UseSqlServer(connection); break; default: throw new InvalidOperationException($"未知数据库 Provider: {provider}"); } }); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); return services; } public static async Task EnsurePlatformDatabaseAsync(this IServiceProvider services) { using var scope = services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); await db.Database.EnsureCreatedAsync(); // EnsureCreated 只在「库文件不存在」时建表;已有 platform.db 时新增实体不会自动补表。 await EnsureSimpleFieldsTableAsync(db); await EnsureUserDashboardShortcutsTableAsync(db); } /// 为已存在的数据库补建 simple_fields 表(幂等)。 private static async Task EnsureSimpleFieldsTableAsync(PlatformDbContext db) { if (db.Database.IsSqlite()) { await db.Database.ExecuteSqlRawAsync(""" CREATE TABLE IF NOT EXISTS simple_fields ( id TEXT NOT NULL CONSTRAINT PK_simple_fields PRIMARY KEY, car_type TEXT NOT NULL DEFAULT '', field_type TEXT NOT NULL, "key" TEXT NOT NULL, value TEXT NOT NULL DEFAULT '', data_type TEXT NOT NULL DEFAULT '', chinese TEXT, english TEXT, other TEXT NOT NULL DEFAULT '', is_default INTEGER NOT NULL, create_time TEXT NOT NULL, update_time TEXT NOT NULL ); """); // 须先删旧索引 (field_type, other, key):把 other 清空为「其他语言」后会与旧唯一约束冲突。 await db.Database.ExecuteSqlRawAsync("DROP INDEX IF EXISTS IX_simple_fields_field_type_other_key;"); await db.Database.ExecuteSqlRawAsync(""" UPDATE simple_fields SET car_type = other WHERE (car_type IS NULL OR car_type = '') AND other <> ''; """); await db.Database.ExecuteSqlRawAsync(""" UPDATE simple_fields SET other = '' WHERE other <> '' AND other = car_type; """); await db.Database.ExecuteSqlRawAsync(""" CREATE UNIQUE INDEX IF NOT EXISTS IX_simple_fields_car_type_field_type_key ON simple_fields (car_type, field_type, "key"); """); return; } // 非 SQLite:表不存在时尝试按当前模型创建(已有库不会走 EnsureCreated)。 if (!await TableExistsAsync(db, "simple_fields")) { var creator = db.GetService(); await creator.CreateTablesAsync(); } } /// 为已存在的数据库补建 user_dashboard_shortcuts 表(幂等)。 private static async Task EnsureUserDashboardShortcutsTableAsync(PlatformDbContext db) { if (db.Database.IsSqlite()) { await db.Database.ExecuteSqlRawAsync(""" CREATE TABLE IF NOT EXISTS user_dashboard_shortcuts ( user_id TEXT NOT NULL, scope TEXT NOT NULL, keys_json TEXT NOT NULL DEFAULT '[]', updated_at TEXT NOT NULL, CONSTRAINT PK_user_dashboard_shortcuts PRIMARY KEY (user_id, scope) ); """); return; } if (!await TableExistsAsync(db, "user_dashboard_shortcuts")) { var creator = db.GetService(); await creator.CreateTablesAsync(); } } /// /// 检查表是否存在 /// /// 数据库上下文 /// 表名 /// 表是否存在 private static async Task TableExistsAsync(PlatformDbContext db, string table) { var conn = db.Database.GetDbConnection(); if (conn.State != System.Data.ConnectionState.Open) await conn.OpenAsync(); try { await using var cmd = conn.CreateCommand(); if (db.Database.IsSqlServer()) { cmd.CommandText = "SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @t"; var p = cmd.CreateParameter(); p.ParameterName = "@t"; p.Value = table; cmd.Parameters.Add(p); } else if (db.Database.IsNpgsql()) { cmd.CommandText = "SELECT 1 FROM information_schema.tables WHERE table_name = @t"; var p = cmd.CreateParameter(); p.ParameterName = "@t"; p.Value = table; cmd.Parameters.Add(p); } else if (db.Database.IsMySql()) { cmd.CommandText = "SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = @t"; var p = cmd.CreateParameter(); p.ParameterName = "@t"; p.Value = table; cmd.Parameters.Add(p); } else { return false; } var result = await cmd.ExecuteScalarAsync(); return result != null; } finally { if (conn.State == System.Data.ConnectionState.Open) await conn.CloseAsync(); } } private static string ResolveConnectionString(IConfiguration configuration, IWebHostEnvironment env, string provider) { var key = provider.Trim().ToLowerInvariant() switch { "postgres" or "postgresql" or "npgsql" => "PostgreSQL", "mssql" => "SqlServer", _ => provider }; var configured = configuration.GetConnectionString(key) ?? configuration.GetConnectionString("Platform"); if (!string.IsNullOrWhiteSpace(configured)) { return IsSqlite(provider) ? NormalizeSqliteConnection(configured, env) : configured; } var dataDir = Path.Combine(env.ContentRootPath, "data"); Directory.CreateDirectory(dataDir); return $"Data Source={Path.Combine(dataDir, "platform.db")}"; } private static bool IsSqlite(string provider) => string.Equals(provider.Trim(), "sqlite", StringComparison.OrdinalIgnoreCase); private static string NormalizeSqliteConnection(string connection, IWebHostEnvironment env) { var builder = new SqliteConnectionStringBuilder(connection); if (string.IsNullOrWhiteSpace(builder.DataSource)) return connection; if (builder.DataSource is ":memory:") return connection; if (!Path.IsPathRooted(builder.DataSource)) { builder.DataSource = Path.Combine(env.ContentRootPath, builder.DataSource); } var dir = Path.GetDirectoryName(builder.DataSource); if (!string.IsNullOrWhiteSpace(dir)) Directory.CreateDirectory(dir); return builder.ToString(); } }