覆盖包库回传、任务下发、CDM 任务同步与报警采集,并为包/任务 ID 与上传文件名加上路径安全校验。 Co-authored-by: Cursor <cursoragent@cursor.com>
567 lines
26 KiB
C#
567 lines
26 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);
|
|
await EnsureWmsStructureSchemaAsync(db);
|
|
await EnsureCdmTasksTableAsync(db);
|
|
await EnsureVehicleAlarmsTableAsync(db);
|
|
await MigrateWmsLegacyAsync(scope.ServiceProvider);
|
|
}
|
|
|
|
private static async Task MigrateWmsLegacyAsync(IServiceProvider sp)
|
|
{
|
|
try
|
|
{
|
|
var wms = sp.GetRequiredService<WmsService>();
|
|
await wms.MigrateLegacyAsync("system");
|
|
}
|
|
catch
|
|
{
|
|
// 首次建库或缺列时忽略,后续请求可再触发
|
|
}
|
|
}
|
|
|
|
/// <summary>补建仓库/物料类型/库存事件及结构扩展列(幂等,SQLite)。</summary>
|
|
private static async Task EnsureWmsStructureSchemaAsync(PlatformDbContext db)
|
|
{
|
|
if (!db.Database.IsSqlite()) return;
|
|
|
|
await db.Database.ExecuteSqlRawAsync("""
|
|
CREATE TABLE IF NOT EXISTS wms_warehouses (
|
|
Id TEXT NOT NULL CONSTRAINT PK_wms_warehouses PRIMARY KEY,
|
|
Code TEXT NOT NULL,
|
|
Name TEXT NOT NULL,
|
|
Type TEXT NOT NULL,
|
|
Enabled INTEGER NOT NULL,
|
|
SortOrder INTEGER NOT NULL,
|
|
CreatedAt TEXT NOT NULL,
|
|
UpdatedAt TEXT NOT NULL,
|
|
IsDeleted INTEGER NOT NULL,
|
|
DeletedAt TEXT,
|
|
DeletedBy TEXT NOT NULL DEFAULT '',
|
|
Version INTEGER NOT NULL,
|
|
IsLock INTEGER NOT NULL,
|
|
CreatedBy TEXT NOT NULL DEFAULT '',
|
|
UpdatedBy TEXT NOT NULL DEFAULT '',
|
|
Remark TEXT NOT NULL DEFAULT '',
|
|
Extend TEXT NOT NULL DEFAULT '{{}}'
|
|
);
|
|
""");
|
|
await db.Database.ExecuteSqlRawAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_wms_warehouses_Code ON wms_warehouses (Code);");
|
|
|
|
await db.Database.ExecuteSqlRawAsync("""
|
|
CREATE TABLE IF NOT EXISTS wms_material_types (
|
|
Id TEXT NOT NULL CONSTRAINT PK_wms_material_types PRIMARY KEY,
|
|
Code TEXT NOT NULL,
|
|
Name TEXT NOT NULL,
|
|
Spec TEXT NOT NULL DEFAULT '',
|
|
Unit TEXT NOT NULL DEFAULT 'pcs',
|
|
Category TEXT NOT NULL DEFAULT '',
|
|
BarcodePrefix TEXT NOT NULL DEFAULT '',
|
|
Enabled INTEGER NOT NULL,
|
|
CreatedAt TEXT NOT NULL,
|
|
UpdatedAt TEXT NOT NULL,
|
|
IsDeleted INTEGER NOT NULL,
|
|
DeletedAt TEXT,
|
|
DeletedBy TEXT NOT NULL DEFAULT '',
|
|
Version INTEGER NOT NULL,
|
|
IsLock INTEGER NOT NULL,
|
|
CreatedBy TEXT NOT NULL DEFAULT '',
|
|
UpdatedBy TEXT NOT NULL DEFAULT '',
|
|
Remark TEXT NOT NULL DEFAULT '',
|
|
Extend TEXT NOT NULL DEFAULT '{{}}'
|
|
);
|
|
""");
|
|
await db.Database.ExecuteSqlRawAsync("CREATE UNIQUE INDEX IF NOT EXISTS IX_wms_material_types_Code ON wms_material_types (Code);");
|
|
|
|
await db.Database.ExecuteSqlRawAsync("""
|
|
CREATE TABLE IF NOT EXISTS wms_stock_events (
|
|
Id TEXT NOT NULL CONSTRAINT PK_wms_stock_events PRIMARY KEY,
|
|
EventType TEXT NOT NULL,
|
|
MaterialId TEXT,
|
|
MaterialCode TEXT NOT NULL DEFAULT '',
|
|
MaterialName TEXT NOT NULL DEFAULT '',
|
|
MaterialBarcode TEXT NOT NULL DEFAULT '',
|
|
MaterialTypeCode TEXT NOT NULL DEFAULT '',
|
|
ContainerId TEXT,
|
|
ContainerCode TEXT NOT NULL DEFAULT '',
|
|
ContainerName TEXT NOT NULL DEFAULT '',
|
|
StorageId TEXT,
|
|
StorageCode TEXT NOT NULL DEFAULT '',
|
|
StorageName TEXT NOT NULL DEFAULT '',
|
|
AreaCode TEXT NOT NULL DEFAULT '',
|
|
FromStorageId TEXT,
|
|
FromStorageCode TEXT NOT NULL DEFAULT '',
|
|
FromStorageName TEXT NOT NULL DEFAULT '',
|
|
ToStorageId TEXT,
|
|
ToStorageCode TEXT NOT NULL DEFAULT '',
|
|
ToStorageName TEXT NOT NULL DEFAULT '',
|
|
RefType TEXT NOT NULL DEFAULT 'Manual',
|
|
RefId TEXT,
|
|
RefCode TEXT NOT NULL DEFAULT '',
|
|
Operator TEXT NOT NULL DEFAULT '',
|
|
OperatedAt TEXT NOT NULL,
|
|
Reason TEXT NOT NULL DEFAULT ''
|
|
);
|
|
""");
|
|
await db.Database.ExecuteSqlRawAsync("CREATE INDEX IF NOT EXISTS IX_wms_stock_events_OperatedAt ON wms_stock_events (OperatedAt);");
|
|
|
|
await EnsureSqliteColumnAsync(db, "wms_areas", "WarehouseId", "TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'");
|
|
await EnsureSqliteColumnAsync(db, "wms_areas", "LayoutMode", "TEXT NOT NULL DEFAULT 'Flat'");
|
|
await EnsureSqliteColumnAsync(db, "wms_areas", "State", "TEXT NOT NULL DEFAULT 'Default'");
|
|
|
|
await EnsureSqliteColumnAsync(db, "wms_storages", "LocationKind", "TEXT NOT NULL DEFAULT 'Station'");
|
|
await EnsureSqliteColumnAsync(db, "wms_storages", "ColumnNo", "INTEGER NOT NULL DEFAULT 0");
|
|
await EnsureSqliteColumnAsync(db, "wms_storages", "LevelNo", "INTEGER NOT NULL DEFAULT 1");
|
|
await EnsureSqliteColumnAsync(db, "wms_storages", "DepthNo", "INTEGER NOT NULL DEFAULT 1");
|
|
await EnsureSqliteColumnAsync(db, "wms_storages", "SiteCode", "TEXT NOT NULL DEFAULT ''");
|
|
await EnsureSqliteColumnAsync(db, "wms_storages", "Barcode", "TEXT NOT NULL DEFAULT ''");
|
|
|
|
await EnsureSqliteColumnAsync(db, "wms_containers", "AreaId", "TEXT");
|
|
await EnsureSqliteColumnAsync(db, "wms_containers", "Barcode", "TEXT NOT NULL DEFAULT ''");
|
|
await EnsureSqliteColumnAsync(db, "wms_containers", "Length", "REAL NOT NULL DEFAULT 0");
|
|
await EnsureSqliteColumnAsync(db, "wms_containers", "Width", "REAL NOT NULL DEFAULT 0");
|
|
await EnsureSqliteColumnAsync(db, "wms_containers", "Height", "REAL NOT NULL DEFAULT 0");
|
|
|
|
await EnsureSqliteColumnAsync(db, "wms_materials", "TypeCode", "TEXT NOT NULL DEFAULT ''");
|
|
await EnsureSqliteColumnAsync(db, "wms_materials", "Barcode", "TEXT NOT NULL DEFAULT ''");
|
|
await EnsureSqliteColumnAsync(db, "wms_materials", "LifecycleStatus", "TEXT NOT NULL DEFAULT 'Active'");
|
|
await EnsureSqliteColumnAsync(db, "wms_materials", "UnboundAt", "TEXT");
|
|
|
|
await EnsureSqliteColumnAsync(db, "wms_container_materials", "BoundAt", "TEXT NOT NULL DEFAULT ''");
|
|
}
|
|
|
|
/// <summary>为已存在的数据库补建 vehicle_alarms 表(车辆报警平台侧记录,幂等)。</summary>
|
|
private static async Task EnsureVehicleAlarmsTableAsync(PlatformDbContext db)
|
|
{
|
|
if (db.Database.IsSqlite())
|
|
{
|
|
await db.Database.ExecuteSqlRawAsync("""
|
|
CREATE TABLE IF NOT EXISTS vehicle_alarms (
|
|
id TEXT NOT NULL CONSTRAINT PK_vehicle_alarms PRIMARY KEY,
|
|
car_id INTEGER NOT NULL DEFAULT 0,
|
|
car_name TEXT NOT NULL DEFAULT '',
|
|
info TEXT NOT NULL DEFAULT '',
|
|
level INTEGER NOT NULL DEFAULT 0,
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
first_at TEXT NOT NULL,
|
|
last_at TEXT NOT NULL,
|
|
resolved_at TEXT,
|
|
duration_secs INTEGER,
|
|
acknowledged INTEGER NOT NULL DEFAULT 0,
|
|
acknowledged_at TEXT,
|
|
acknowledged_by TEXT
|
|
);
|
|
""");
|
|
await db.Database.ExecuteSqlRawAsync("CREATE INDEX IF NOT EXISTS IX_vehicle_alarms_car_status ON vehicle_alarms (car_id, status);");
|
|
await db.Database.ExecuteSqlRawAsync("CREATE INDEX IF NOT EXISTS IX_vehicle_alarms_status ON vehicle_alarms (status);");
|
|
await db.Database.ExecuteSqlRawAsync("CREATE INDEX IF NOT EXISTS IX_vehicle_alarms_first_at ON vehicle_alarms (first_at);");
|
|
return;
|
|
}
|
|
|
|
if (!await TableExistsAsync(db, "vehicle_alarms"))
|
|
{
|
|
var creator = db.GetService<Microsoft.EntityFrameworkCore.Storage.IRelationalDatabaseCreator>();
|
|
await creator.CreateTablesAsync();
|
|
}
|
|
}
|
|
|
|
/// <summary>为已存在的数据库补建 cdm_tasks 表(CDM 搬运任务平台侧快照,幂等)。</summary>
|
|
private static async Task EnsureCdmTasksTableAsync(PlatformDbContext db)
|
|
{
|
|
if (db.Database.IsSqlite())
|
|
{
|
|
await db.Database.ExecuteSqlRawAsync("""
|
|
CREATE TABLE IF NOT EXISTS cdm_tasks (
|
|
id TEXT NOT NULL CONSTRAINT PK_cdm_tasks PRIMARY KEY,
|
|
task_id TEXT,
|
|
mission_id INTEGER NOT NULL DEFAULT 0,
|
|
mission_name TEXT NOT NULL DEFAULT '',
|
|
mission_type TEXT NOT NULL DEFAULT '',
|
|
src_site_id INTEGER NOT NULL DEFAULT 0,
|
|
src_label TEXT NOT NULL DEFAULT '',
|
|
dst_site_id INTEGER NOT NULL DEFAULT 0,
|
|
dst_label TEXT NOT NULL DEFAULT '',
|
|
status TEXT NOT NULL DEFAULT '',
|
|
status_code TEXT NOT NULL DEFAULT '',
|
|
car_id INTEGER,
|
|
car_name TEXT,
|
|
priority INTEGER NOT NULL DEFAULT 0,
|
|
create_time TEXT,
|
|
start_time TEXT,
|
|
finish_time TEXT,
|
|
stuck_reason TEXT,
|
|
overdue INTEGER NOT NULL DEFAULT 0,
|
|
first_seen_at TEXT NOT NULL,
|
|
last_seen_at TEXT NOT NULL
|
|
);
|
|
""");
|
|
await db.Database.ExecuteSqlRawAsync("CREATE INDEX IF NOT EXISTS IX_cdm_tasks_status_code ON cdm_tasks (status_code);");
|
|
await db.Database.ExecuteSqlRawAsync("CREATE INDEX IF NOT EXISTS IX_cdm_tasks_create_time ON cdm_tasks (create_time);");
|
|
return;
|
|
}
|
|
|
|
if (!await TableExistsAsync(db, "cdm_tasks"))
|
|
{
|
|
var creator = db.GetService<Microsoft.EntityFrameworkCore.Storage.IRelationalDatabaseCreator>();
|
|
await creator.CreateTablesAsync();
|
|
}
|
|
}
|
|
|
|
/// <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
|
|
);
|
|
""");
|
|
|
|
// 库位占用唯一约束(幂等);若库内已有重复占用会创建失败,不阻断启动
|
|
try
|
|
{
|
|
await db.Database.ExecuteSqlRawAsync("""
|
|
CREATE UNIQUE INDEX IF NOT EXISTS IX_wms_container_locations_StorageLocationId
|
|
ON wms_container_locations (LocationId)
|
|
WHERE LocationType = 'Storage' AND IsDeleted = 0;
|
|
""");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[WMS] 无法创建库位占用唯一索引 IX_wms_container_locations_StorageLocationId(可能已有重复占用): {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private static async Task EnsureSqliteColumnAsync(PlatformDbContext db, string table, string column, string definition)
|
|
{
|
|
// 仅允许内部迁移调用方传入的标识符;拒绝注入用分隔符/空白。
|
|
static bool IsSafeIdent(string s) =>
|
|
!string.IsNullOrEmpty(s) && s.All(ch => char.IsAsciiLetterOrDigit(ch) || ch == '_');
|
|
if (!IsSafeIdent(table) || !IsSafeIdent(column)
|
|
|| definition.Contains(';') || definition.Contains("--")
|
|
|| !System.Text.RegularExpressions.Regex.IsMatch(definition, @"^[A-Za-z0-9_()'.,\s]+$"))
|
|
throw new ArgumentException("unsafe sqlite migration identifier");
|
|
|
|
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();
|
|
}
|
|
}
|