Files
Migu2.0/MiGu.Server/Persistence/PlatformPersistence.cs
T
ArtoriasWu 15405ba114 新增 WMS 搬运规则与任务管理
支持搬运规则维护、候选预览、任务生成、预占、下发、取消和完成,并完善仓储管理前端交互。
2026-06-25 11:02:22 +08:00

340 lines
14 KiB
C#

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<PlatformDbContext>((sp, options) =>
{
var env = sp.GetRequiredService<IWebHostEnvironment>();
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<WmsReferenceValidator>();
services.AddScoped<WmsService>();
services.AddScoped<WmsTransportRuleService>();
services.AddScoped<WmsTransportPlanner>();
services.AddScoped<WmsTransportTaskService>();
services.AddScoped<IWmsDispatchAdapter, NoopWmsDispatchAdapter>();
services.AddScoped<SimpleFieldService>();
services.AddScoped<MiGu.Server.Dashboard.DashboardShortcutService>();
return services;
}
public static async Task EnsurePlatformDatabaseAsync(this IServiceProvider services)
{
using var scope = services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<PlatformDbContext>();
await db.Database.EnsureCreatedAsync();
// EnsureCreated 只在「库文件不存在」时建表;已有 platform.db 时新增实体不会自动补表。
await EnsureSimpleFieldsTableAsync(db);
await EnsureUserDashboardShortcutsTableAsync(db);
await EnsureWmsTransportSchemaAsync(db);
}
/// <summary>为已存在的数据库补建 simple_fields 表(幂等)。</summary>
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<Microsoft.EntityFrameworkCore.Storage.IRelationalDatabaseCreator>();
await creator.CreateTablesAsync();
}
}
/// <summary>为已存在的数据库补建 WMS 搬运规则/任务相关表及库位扩展列(幂等)。</summary>
private static async Task EnsureWmsTransportSchemaAsync(PlatformDbContext db)
{
if (!db.Database.IsSqlite()) return;
await EnsureSqliteColumnAsync(db, "wms_storages", "Status", "TEXT NOT NULL DEFAULT 'Available'");
await EnsureSqliteColumnAsync(db, "wms_storages", "Usage", "TEXT NOT NULL DEFAULT ''");
await EnsureSqliteColumnAsync(db, "wms_storages", "Priority", "INTEGER NOT NULL DEFAULT 0");
await EnsureSqliteColumnAsync(db, "wms_storages", "ZoneCode", "TEXT NOT NULL DEFAULT ''");
await EnsureSqliteColumnAsync(db, "wms_storages", "AllowInbound", "INTEGER NOT NULL DEFAULT 1");
await EnsureSqliteColumnAsync(db, "wms_storages", "AllowOutbound", "INTEGER NOT NULL DEFAULT 1");
await db.Database.ExecuteSqlRawAsync("""
CREATE TABLE IF NOT EXISTS wms_transport_rules (
Id TEXT NOT NULL CONSTRAINT PK_wms_transport_rules PRIMARY KEY,
CreatedAt TEXT NOT NULL,
UpdatedAt TEXT NOT NULL,
IsDeleted INTEGER NOT NULL,
DeletedAt TEXT,
DeletedBy TEXT,
Version INTEGER NOT NULL,
IsLock INTEGER NOT NULL,
CreatedBy TEXT NOT NULL,
UpdatedBy TEXT NOT NULL,
Remark TEXT NOT NULL,
Extend TEXT NOT NULL,
Code TEXT NOT NULL,
Name TEXT NOT NULL,
TriggerType TEXT NOT NULL,
Enabled INTEGER NOT NULL,
Priority INTEGER NOT NULL,
SourceSelectorJson TEXT NOT NULL,
TargetSelectorJson TEXT NOT NULL,
TaskOptionsJson TEXT NOT NULL
);
""");
await db.Database.ExecuteSqlRawAsync("""
CREATE UNIQUE INDEX IF NOT EXISTS IX_wms_transport_rules_Code ON wms_transport_rules (Code);
""");
await db.Database.ExecuteSqlRawAsync("""
CREATE TABLE IF NOT EXISTS wms_transport_tasks (
Id TEXT NOT NULL CONSTRAINT PK_wms_transport_tasks PRIMARY KEY,
CreatedAt TEXT NOT NULL,
UpdatedAt TEXT NOT NULL,
IsDeleted INTEGER NOT NULL,
DeletedAt TEXT,
DeletedBy TEXT,
Version INTEGER NOT NULL,
IsLock INTEGER NOT NULL,
CreatedBy TEXT NOT NULL,
UpdatedBy TEXT NOT NULL,
Remark TEXT NOT NULL,
Extend TEXT NOT NULL,
BusinessType TEXT NOT NULL,
RuleId TEXT,
SourceStorageId TEXT NOT NULL,
TargetStorageId TEXT NOT NULL,
ContainerId TEXT NOT NULL,
MaterialId TEXT,
Quantity REAL,
Status TEXT NOT NULL,
DispatchMissionId TEXT NOT NULL DEFAULT '',
DeliveryId TEXT NOT NULL DEFAULT '',
DispatchStatus TEXT NOT NULL DEFAULT '',
Reason TEXT NOT NULL DEFAULT '',
SnapshotJson TEXT NOT NULL,
ErrorMessage TEXT NOT NULL DEFAULT '',
TaskPriority INTEGER NOT NULL DEFAULT 0
);
""");
await db.Database.ExecuteSqlRawAsync("""
CREATE TABLE IF NOT EXISTS wms_transport_reservations (
Id TEXT NOT NULL CONSTRAINT PK_wms_transport_reservations PRIMARY KEY,
CreatedAt TEXT NOT NULL,
UpdatedAt TEXT NOT NULL,
IsDeleted INTEGER NOT NULL,
DeletedAt TEXT,
DeletedBy TEXT,
Version INTEGER NOT NULL,
IsLock INTEGER NOT NULL,
CreatedBy TEXT NOT NULL,
UpdatedBy TEXT NOT NULL,
Remark TEXT NOT NULL,
Extend TEXT NOT NULL,
TaskId TEXT NOT NULL,
ContainerId TEXT NOT NULL,
SourceStorageId TEXT NOT NULL,
TargetStorageId TEXT NOT NULL,
Status TEXT NOT NULL,
ExpiresAt TEXT
);
""");
await db.Database.ExecuteSqlRawAsync("""
CREATE TABLE IF NOT EXISTS wms_transport_task_history (
Id TEXT NOT NULL CONSTRAINT PK_wms_transport_task_history PRIMARY KEY,
TaskId TEXT NOT NULL,
FromStatus TEXT NOT NULL,
ToStatus TEXT NOT NULL,
Operator TEXT NOT NULL,
OperatedAt TEXT NOT NULL,
Reason TEXT NOT NULL,
ErrorMessage TEXT NOT NULL,
SnapshotJson TEXT NOT NULL
);
""");
}
private static async Task EnsureSqliteColumnAsync(PlatformDbContext db, string table, string column, string definition)
{
var conn = db.Database.GetDbConnection();
if (conn.State != System.Data.ConnectionState.Open)
await conn.OpenAsync();
await using var cmd = conn.CreateCommand();
cmd.CommandText = $"PRAGMA table_info({table});";
await using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
var name = reader.GetString(1);
if (string.Equals(name, column, StringComparison.OrdinalIgnoreCase))
return;
}
await db.Database.ExecuteSqlRawAsync($"ALTER TABLE {table} ADD COLUMN {column} {definition};");
}
/// <summary>为已存在的数据库补建 user_dashboard_shortcuts 表(幂等)。</summary>
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<Microsoft.EntityFrameworkCore.Storage.IRelationalDatabaseCreator>();
await creator.CreateTablesAsync();
}
}
/// <summary>
/// 检查表是否存在
/// </summary>
/// <param name="db">数据库上下文</param>
/// <param name="table">表名</param>
/// <returns>表是否存在</returns>
private static async Task<bool> 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();
}
}