WMS主数据结构升级,支持仓库/物料类型等新实体
后端:重构WmsController/WmsService,新增仓库、库区、货位、物料类型等实体及其增删改查接口,支持批量生成网格货位、货位/容器锁定与启用、物料归档与批量清理。数据库模型新增Warehouse、MaterialType、StockEvent等表,调整索引和字段,支持SQLite自动补表/补列及数据迁移,接口兼容旧语义。 前端:wms.ts/types/wms.ts全面适配新结构,升级API路由和类型定义。WarehouseManagementView.vue重构,支持仓库、物料类型管理、货位网格可视化、库存事件展示等。WmsEntityDialog.vue支持新字段编辑及联动选择。新增AreaFacadePanel.vue实现库区网格立面可视化。wmsOptions.ts新增相关常量及UI优化。 兼容性:后端自动迁移旧数据,接口兼容旧绑定语义,提升灵活性、可维护性和可视化能力。
This commit is contained in:
@@ -22,14 +22,34 @@ public sealed class WmsController : ControllerBase
|
||||
_transportTasks = transportTasks;
|
||||
}
|
||||
|
||||
[HttpGet("warehouses")]
|
||||
public Task<List<Warehouse>> Warehouses([FromQuery] string? q) => _service.Warehouses(q);
|
||||
|
||||
[HttpPost("warehouses")]
|
||||
public async Task<IActionResult> SaveWarehouse([FromBody] MasterDataRequest req) =>
|
||||
Ok(await _service.SaveWarehouse(req, User.ActorName()));
|
||||
|
||||
[HttpPut("warehouses/{id:guid}")]
|
||||
public async Task<IActionResult> UpdateWarehouse(Guid id, [FromBody] MasterDataRequest req) =>
|
||||
Ok(await _service.SaveWarehouse(req with { Id = id }, User.ActorName()));
|
||||
|
||||
[HttpDelete("warehouses/{id:guid}")]
|
||||
public async Task<IActionResult> DeleteWarehouse(Guid id, [FromQuery] long? version)
|
||||
{
|
||||
await _service.DeleteEntity<Warehouse>(id, version, User.ActorName());
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("areas")]
|
||||
public Task<List<WarehouseArea>> Areas([FromQuery] string? q) => _service.Areas(q);
|
||||
public Task<List<WarehouseArea>> Areas([FromQuery] string? q, [FromQuery] Guid? warehouseId) =>
|
||||
_service.Areas(q, warehouseId);
|
||||
|
||||
[HttpPost("areas")]
|
||||
public async Task<IActionResult> SaveArea([FromBody] MasterDataRequest req) => Ok(await _service.SaveArea(req, User.ActorName()));
|
||||
public async Task<IActionResult> SaveArea([FromBody] AreaRequest req) =>
|
||||
Ok(await _service.SaveArea(req, User.ActorName()));
|
||||
|
||||
[HttpPut("areas/{id:guid}")]
|
||||
public async Task<IActionResult> UpdateArea(Guid id, [FromBody] MasterDataRequest req) =>
|
||||
public async Task<IActionResult> UpdateArea(Guid id, [FromBody] AreaRequest req) =>
|
||||
Ok(await _service.SaveArea(req with { Id = id }, User.ActorName()));
|
||||
|
||||
[HttpDelete("areas/{id:guid}")]
|
||||
@@ -40,10 +60,12 @@ public sealed class WmsController : ControllerBase
|
||||
}
|
||||
|
||||
[HttpGet("storages")]
|
||||
public Task<List<Storage>> Storages([FromQuery] string? q) => _service.Storages(q);
|
||||
public Task<List<Storage>> Storages([FromQuery] string? q, [FromQuery] Guid? areaId, [FromQuery] string? locationKind) =>
|
||||
_service.Storages(q, areaId, locationKind);
|
||||
|
||||
[HttpPost("storages")]
|
||||
public async Task<IActionResult> SaveStorage([FromBody] StorageRequest req) => Ok(await _service.SaveStorage(req, User.ActorName()));
|
||||
public async Task<IActionResult> SaveStorage([FromBody] StorageRequest req) =>
|
||||
Ok(await _service.SaveStorage(req, User.ActorName()));
|
||||
|
||||
[HttpPut("storages/{id:guid}")]
|
||||
public async Task<IActionResult> UpdateStorage(Guid id, [FromBody] StorageRequest req) =>
|
||||
@@ -56,14 +78,35 @@ public sealed class WmsController : ControllerBase
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("areas/{id:guid}/generate-bins")]
|
||||
public async Task<IActionResult> GenerateBins(Guid id, [FromBody] GenerateBinsRequest req) =>
|
||||
Ok(new { created = await _service.GenerateBins(id, req, User.ActorName()) });
|
||||
|
||||
[HttpPost("storages/{id:guid}/lock")]
|
||||
public async Task<IActionResult> LockStorage(Guid id, [FromQuery] long? version) =>
|
||||
Ok(await _service.SetStorageLock(id, true, version, User.ActorName()));
|
||||
|
||||
[HttpPost("storages/{id:guid}/unlock")]
|
||||
public async Task<IActionResult> UnlockStorage(Guid id, [FromQuery] long? version) =>
|
||||
Ok(await _service.SetStorageLock(id, false, version, User.ActorName()));
|
||||
|
||||
[HttpPost("storages/{id:guid}/enable")]
|
||||
public async Task<IActionResult> EnableStorage(Guid id, [FromQuery] long? version) =>
|
||||
Ok(await _service.SetStorageEnabled(id, true, version, User.ActorName()));
|
||||
|
||||
[HttpPost("storages/{id:guid}/disable")]
|
||||
public async Task<IActionResult> DisableStorage(Guid id, [FromQuery] long? version) =>
|
||||
Ok(await _service.SetStorageEnabled(id, false, version, User.ActorName()));
|
||||
|
||||
[HttpGet("containers")]
|
||||
public Task<List<Container>> Containers([FromQuery] string? q) => _service.Containers(q);
|
||||
|
||||
[HttpPost("containers")]
|
||||
public async Task<IActionResult> SaveContainer([FromBody] MasterDataRequest req) => Ok(await _service.SaveContainer(req, User.ActorName()));
|
||||
public async Task<IActionResult> SaveContainer([FromBody] ContainerRequest req) =>
|
||||
Ok(await _service.SaveContainer(req, User.ActorName()));
|
||||
|
||||
[HttpPut("containers/{id:guid}")]
|
||||
public async Task<IActionResult> UpdateContainer(Guid id, [FromBody] MasterDataRequest req) =>
|
||||
public async Task<IActionResult> UpdateContainer(Guid id, [FromBody] ContainerRequest req) =>
|
||||
Ok(await _service.SaveContainer(req with { Id = id }, User.ActorName()));
|
||||
|
||||
[HttpDelete("containers/{id:guid}")]
|
||||
@@ -73,11 +116,35 @@ public sealed class WmsController : ControllerBase
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("material-types")]
|
||||
public Task<List<MaterialType>> MaterialTypes([FromQuery] string? q) => _service.MaterialTypes(q);
|
||||
|
||||
[HttpPost("material-types")]
|
||||
public async Task<IActionResult> SaveMaterialType([FromBody] MaterialTypeRequest req) =>
|
||||
Ok(await _service.SaveMaterialType(req, User.ActorName()));
|
||||
|
||||
[HttpPut("material-types/{id:guid}")]
|
||||
public async Task<IActionResult> UpdateMaterialType(Guid id, [FromBody] MaterialTypeRequest req) =>
|
||||
Ok(await _service.SaveMaterialType(req with { Id = id }, User.ActorName()));
|
||||
|
||||
[HttpDelete("material-types/{id:guid}")]
|
||||
public async Task<IActionResult> DeleteMaterialType(Guid id, [FromQuery] long? version)
|
||||
{
|
||||
await _service.DeleteEntity<MaterialType>(id, version, User.ActorName());
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("materials")]
|
||||
public Task<List<Material>> Materials([FromQuery] string? q) => _service.Materials(q);
|
||||
public Task<List<Material>> Materials(
|
||||
[FromQuery] string? q,
|
||||
[FromQuery] string? lifecycle,
|
||||
[FromQuery] bool? onlyUnbound,
|
||||
[FromQuery] bool? onlyBound) =>
|
||||
_service.Materials(q, lifecycle, onlyUnbound, onlyBound);
|
||||
|
||||
[HttpPost("materials")]
|
||||
public async Task<IActionResult> SaveMaterial([FromBody] MaterialRequest req) => Ok(await _service.SaveMaterial(req, User.ActorName()));
|
||||
public async Task<IActionResult> SaveMaterial([FromBody] MaterialRequest req) =>
|
||||
Ok(await _service.SaveMaterial(req, User.ActorName()));
|
||||
|
||||
[HttpPut("materials/{id:guid}")]
|
||||
public async Task<IActionResult> UpdateMaterial(Guid id, [FromBody] MaterialRequest req) =>
|
||||
@@ -90,6 +157,17 @@ public sealed class WmsController : ControllerBase
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("materials/{id:guid}/archive")]
|
||||
public async Task<IActionResult> ArchiveMaterial(Guid id, [FromQuery] long? version) =>
|
||||
Ok(await _service.ArchiveMaterial(id, version, User.ActorName()));
|
||||
|
||||
[HttpPost("materials/purge-archived")]
|
||||
public async Task<IActionResult> PurgeArchivedMaterials([FromQuery] int take = 200)
|
||||
{
|
||||
await _service.PurgeArchivedMaterials(User.ActorName(), take);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("container-locations")]
|
||||
public Task<List<ContainerLocation>> ContainerLocations([FromQuery] string? locationType, [FromQuery] string? q) =>
|
||||
_service.ContainerLocations(locationType, q);
|
||||
@@ -110,23 +188,34 @@ public sealed class WmsController : ControllerBase
|
||||
}
|
||||
|
||||
[HttpGet("container-materials")]
|
||||
public Task<List<ContainerMaterial>> ContainerMaterials([FromQuery] string? q) => _service.ContainerMaterials(q);
|
||||
public Task<List<ContainerMaterial>> ContainerMaterials([FromQuery] string? q, [FromQuery] Guid? containerId) =>
|
||||
_service.ContainerMaterials(q, containerId);
|
||||
|
||||
[HttpPost("container-materials")]
|
||||
public async Task<IActionResult> SaveContainerMaterial([FromBody] ContainerMaterialRequest req) =>
|
||||
Ok(await _service.SaveContainerMaterial(req, User.ActorName()));
|
||||
public async Task<IActionResult> SaveContainerMaterial([FromBody] BindMaterialRequest req) =>
|
||||
Ok(await _service.BindMaterial(req, User.ActorName()));
|
||||
|
||||
[HttpPut("container-materials/{id:guid}")]
|
||||
public async Task<IActionResult> UpdateContainerMaterial(Guid id, [FromBody] ContainerMaterialRequest req) =>
|
||||
Ok(await _service.SaveContainerMaterial(req with { Id = id }, User.ActorName()));
|
||||
[HttpPost("container-materials/bind")]
|
||||
public async Task<IActionResult> BindMaterial([FromBody] BindMaterialRequest req) =>
|
||||
Ok(await _service.BindMaterial(req, User.ActorName()));
|
||||
|
||||
[HttpDelete("container-materials/{id:guid}")]
|
||||
public async Task<IActionResult> UnloadContainerMaterial(Guid id, [FromQuery] string? reason)
|
||||
public async Task<IActionResult> UnbindMaterial(Guid id, [FromQuery] string? reason, [FromQuery] bool archive = false)
|
||||
{
|
||||
await _service.UnloadMaterial(id, User.ActorName(), reason ?? "");
|
||||
await _service.UnbindMaterial(id, User.ActorName(), reason ?? "", archive);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("inventory/materials")]
|
||||
public Task<List<InventoryMaterialRow>> InventoryMaterials(
|
||||
[FromQuery] Guid? areaId, [FromQuery] Guid? storageId, [FromQuery] string? q) =>
|
||||
_service.InventoryMaterials(areaId, storageId, q);
|
||||
|
||||
[HttpGet("stock-events")]
|
||||
public Task<List<StockEvent>> StockEvents(
|
||||
[FromQuery] string? eventType, [FromQuery] Guid? materialId, [FromQuery] Guid? containerId) =>
|
||||
_service.StockEvents(eventType, materialId, containerId);
|
||||
|
||||
[HttpGet("container-location-history")]
|
||||
public Task<List<ContainerLocationHistory>> ContainerLocationHistory([FromQuery] Guid? containerId) =>
|
||||
_service.LocationHistory(containerId);
|
||||
|
||||
@@ -10,12 +10,15 @@ public sealed class PlatformDbContext : DbContext
|
||||
{
|
||||
public PlatformDbContext(DbContextOptions<PlatformDbContext> options) : base(options) { }
|
||||
|
||||
public DbSet<Warehouse> Warehouses => Set<Warehouse>();
|
||||
public DbSet<WarehouseArea> WarehouseAreas => Set<WarehouseArea>();
|
||||
public DbSet<Storage> Storages => Set<Storage>();
|
||||
public DbSet<Container> Containers => Set<Container>();
|
||||
public DbSet<MaterialType> MaterialTypes => Set<MaterialType>();
|
||||
public DbSet<Material> Materials => Set<Material>();
|
||||
public DbSet<ContainerLocation> ContainerLocations => Set<ContainerLocation>();
|
||||
public DbSet<ContainerMaterial> ContainerMaterials => Set<ContainerMaterial>();
|
||||
public DbSet<StockEvent> StockEvents => Set<StockEvent>();
|
||||
public DbSet<ContainerLocationHistory> ContainerLocationHistories => Set<ContainerLocationHistory>();
|
||||
public DbSet<ContainerMaterialHistory> ContainerMaterialHistories => Set<ContainerMaterialHistory>();
|
||||
public DbSet<WmsTransportRule> WmsTransportRules => Set<WmsTransportRule>();
|
||||
@@ -52,9 +55,11 @@ public sealed class PlatformDbContext : DbContext
|
||||
modelBuilder.Entity(entity.ClrType).Property(p.Name).HasConversion(nullableDateTimeOffset).HasMaxLength(40);
|
||||
}
|
||||
|
||||
ConfigureEntityBase<Warehouse>(modelBuilder, "wms_warehouses");
|
||||
ConfigureEntityBase<WarehouseArea>(modelBuilder, "wms_areas");
|
||||
ConfigureEntityBase<Storage>(modelBuilder, "wms_storages");
|
||||
ConfigureEntityBase<Container>(modelBuilder, "wms_containers");
|
||||
ConfigureEntityBase<MaterialType>(modelBuilder, "wms_material_types");
|
||||
ConfigureEntityBase<Material>(modelBuilder, "wms_materials");
|
||||
ConfigureEntityBase<ContainerLocation>(modelBuilder, "wms_container_locations");
|
||||
ConfigureEntityBase<ContainerMaterial>(modelBuilder, "wms_container_materials");
|
||||
@@ -64,16 +69,25 @@ public sealed class PlatformDbContext : DbContext
|
||||
|
||||
ConfigureHistory<ContainerLocationHistory>(modelBuilder, "wms_container_location_history");
|
||||
ConfigureHistory<ContainerMaterialHistory>(modelBuilder, "wms_container_material_history");
|
||||
ConfigureStockEvent(modelBuilder);
|
||||
ConfigureTransportTaskHistory(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<Warehouse>().HasIndex(x => x.Code).IsUnique();
|
||||
modelBuilder.Entity<WarehouseArea>().HasIndex(x => x.Code).IsUnique();
|
||||
modelBuilder.Entity<WarehouseArea>().HasIndex(x => x.WarehouseId);
|
||||
modelBuilder.Entity<Storage>().HasIndex(x => x.Code).IsUnique();
|
||||
modelBuilder.Entity<Storage>().HasIndex(x => x.AreaId);
|
||||
modelBuilder.Entity<Storage>().HasIndex(x => x.Barcode);
|
||||
modelBuilder.Entity<Container>().HasIndex(x => x.Code).IsUnique();
|
||||
modelBuilder.Entity<MaterialType>().HasIndex(x => x.Code).IsUnique();
|
||||
modelBuilder.Entity<Material>().HasIndex(x => x.Code).IsUnique();
|
||||
modelBuilder.Entity<Material>().HasIndex(x => x.Barcode);
|
||||
modelBuilder.Entity<Material>().HasIndex(x => new { x.LifecycleStatus, x.UpdatedAt });
|
||||
modelBuilder.Entity<Material>().HasIndex(x => x.TypeCode);
|
||||
modelBuilder.Entity<ContainerLocation>().HasIndex(x => x.ContainerId).IsUnique();
|
||||
modelBuilder.Entity<ContainerLocation>().HasIndex(x => new { x.LocationType, x.LocationId });
|
||||
modelBuilder.Entity<ContainerMaterial>().HasIndex(x => new { x.ContainerId, x.MaterialId, x.BatchNo, x.SerialNo }).IsUnique();
|
||||
modelBuilder.Entity<ContainerMaterial>().HasIndex(x => x.MaterialId).IsUnique();
|
||||
modelBuilder.Entity<ContainerMaterial>().HasIndex(x => x.ContainerId);
|
||||
|
||||
modelBuilder.Entity<WmsTransportRule>().HasIndex(x => x.Code).IsUnique();
|
||||
modelBuilder.Entity<WmsTransportRule>().HasIndex(x => new { x.TriggerType, x.Enabled, x.Priority });
|
||||
@@ -193,6 +207,35 @@ public sealed class PlatformDbContext : DbContext
|
||||
e.HasIndex(x => x.EventType);
|
||||
}
|
||||
|
||||
private static void ConfigureStockEvent(ModelBuilder modelBuilder)
|
||||
{
|
||||
var e = modelBuilder.Entity<StockEvent>();
|
||||
e.ToTable("wms_stock_events");
|
||||
e.HasKey(x => x.Id);
|
||||
e.Property(x => x.EventType).HasMaxLength(32);
|
||||
e.Property(x => x.MaterialCode).HasMaxLength(64);
|
||||
e.Property(x => x.MaterialName).HasMaxLength(128);
|
||||
e.Property(x => x.MaterialBarcode).HasMaxLength(128);
|
||||
e.Property(x => x.MaterialTypeCode).HasMaxLength(64);
|
||||
e.Property(x => x.ContainerCode).HasMaxLength(64);
|
||||
e.Property(x => x.ContainerName).HasMaxLength(128);
|
||||
e.Property(x => x.StorageCode).HasMaxLength(64);
|
||||
e.Property(x => x.StorageName).HasMaxLength(128);
|
||||
e.Property(x => x.AreaCode).HasMaxLength(64);
|
||||
e.Property(x => x.FromStorageCode).HasMaxLength(64);
|
||||
e.Property(x => x.FromStorageName).HasMaxLength(128);
|
||||
e.Property(x => x.ToStorageCode).HasMaxLength(64);
|
||||
e.Property(x => x.ToStorageName).HasMaxLength(128);
|
||||
e.Property(x => x.RefType).HasMaxLength(64);
|
||||
e.Property(x => x.RefCode).HasMaxLength(64);
|
||||
e.Property(x => x.Operator).HasMaxLength(128);
|
||||
e.Property(x => x.Reason).HasMaxLength(500);
|
||||
e.HasIndex(x => x.OperatedAt);
|
||||
e.HasIndex(x => x.EventType);
|
||||
e.HasIndex(x => x.MaterialId);
|
||||
e.HasIndex(x => x.ContainerId);
|
||||
}
|
||||
|
||||
private static void ConfigureTransportTaskHistory(ModelBuilder modelBuilder)
|
||||
{
|
||||
var e = modelBuilder.Entity<WmsTransportTaskHistory>();
|
||||
|
||||
@@ -58,6 +58,131 @@ public static class PlatformPersistence
|
||||
await EnsureSimpleFieldsTableAsync(db);
|
||||
await EnsureUserDashboardShortcutsTableAsync(db);
|
||||
await EnsureWmsTransportSchemaAsync(db);
|
||||
await EnsureWmsStructureSchemaAsync(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>为已存在的数据库补建 simple_fields 表(幂等)。</summary>
|
||||
|
||||
@@ -19,11 +19,23 @@ public abstract class WarehouseHistoryBase
|
||||
public string Extend { get; set; } = "{}";
|
||||
}
|
||||
|
||||
public sealed class WarehouseArea : EntityBase
|
||||
public sealed class Warehouse : EntityBase
|
||||
{
|
||||
[MaxLength(64)] public string Code { get; set; } = "";
|
||||
[MaxLength(128)] public string Name { get; set; } = "";
|
||||
[MaxLength(64)] public string Type { get; set; } = "Default";
|
||||
public bool Enabled { get; set; } = true;
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WarehouseArea : EntityBase
|
||||
{
|
||||
public Guid WarehouseId { get; set; }
|
||||
[MaxLength(64)] public string Code { get; set; } = "";
|
||||
[MaxLength(128)] public string Name { get; set; } = "";
|
||||
[MaxLength(64)] public string Type { get; set; } = "Storage";
|
||||
[MaxLength(32)] public string LayoutMode { get; set; } = AreaLayoutModes.Flat;
|
||||
[MaxLength(32)] public string State { get; set; } = "Default";
|
||||
public bool Enabled { get; set; } = true;
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
@@ -34,9 +46,16 @@ public sealed class Storage : EntityBase
|
||||
[MaxLength(64)] public string Code { get; set; } = "";
|
||||
[MaxLength(128)] public string Name { get; set; } = "";
|
||||
[MaxLength(64)] public string StorageType { get; set; } = "Storage";
|
||||
[MaxLength(32)] public string LocationKind { get; set; } = LocationKinds.Station;
|
||||
public int ColumnNo { get; set; }
|
||||
public int LevelNo { get; set; } = 1;
|
||||
public int DepthNo { get; set; } = 1;
|
||||
[MaxLength(64)] public string SiteId { get; set; } = "";
|
||||
[MaxLength(64)] public string SiteCode { get; set; } = "";
|
||||
[MaxLength(128)] public string Barcode { get; set; } = "";
|
||||
/// <summary>已废弃:一货位一容器,保留列兼容旧库。</summary>
|
||||
public int Capacity { get; set; }
|
||||
[MaxLength(32)] public string Status { get; set; } = StorageStatuses.Available;
|
||||
[MaxLength(32)] public string Status { get; set; } = StorageStatuses.Empty;
|
||||
[MaxLength(64)] public string Usage { get; set; } = "";
|
||||
public int Priority { get; set; }
|
||||
[MaxLength(64)] public string ZoneCode { get; set; } = "";
|
||||
@@ -47,10 +66,26 @@ public sealed class Storage : EntityBase
|
||||
|
||||
public sealed class Container : EntityBase
|
||||
{
|
||||
public Guid? AreaId { get; set; }
|
||||
[MaxLength(64)] public string Code { get; set; } = "";
|
||||
[MaxLength(128)] public string Name { get; set; } = "";
|
||||
[MaxLength(64)] public string ContainerType { get; set; } = "Box";
|
||||
[MaxLength(64)] public string Status { get; set; } = "Idle";
|
||||
[MaxLength(64)] public string Status { get; set; } = ContainerStatuses.EmptyMaterial;
|
||||
[MaxLength(128)] public string Barcode { get; set; } = "";
|
||||
public double Length { get; set; }
|
||||
public double Width { get; set; }
|
||||
public double Height { get; set; }
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
|
||||
public sealed class MaterialType : EntityBase
|
||||
{
|
||||
[MaxLength(64)] public string Code { get; set; } = "";
|
||||
[MaxLength(128)] public string Name { get; set; } = "";
|
||||
[MaxLength(128)] public string Spec { get; set; } = "";
|
||||
[MaxLength(32)] public string Unit { get; set; } = "pcs";
|
||||
[MaxLength(64)] public string Category { get; set; } = "";
|
||||
[MaxLength(64)] public string BarcodePrefix { get; set; } = "";
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
|
||||
@@ -58,9 +93,13 @@ public sealed class Material : EntityBase
|
||||
{
|
||||
[MaxLength(64)] public string Code { get; set; } = "";
|
||||
[MaxLength(128)] public string Name { get; set; } = "";
|
||||
[MaxLength(64)] public string TypeCode { get; set; } = "";
|
||||
[MaxLength(128)] public string Barcode { get; set; } = "";
|
||||
[MaxLength(128)] public string Spec { get; set; } = "";
|
||||
[MaxLength(32)] public string Unit { get; set; } = "pcs";
|
||||
[MaxLength(64)] public string Category { get; set; } = "";
|
||||
[MaxLength(32)] public string LifecycleStatus { get; set; } = MaterialLifecycles.Active;
|
||||
public DateTimeOffset? UnboundAt { get; set; }
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
|
||||
@@ -75,18 +114,50 @@ public sealed class ContainerLocation : EntityBase
|
||||
public DateTimeOffset EnteredAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>容器-物料绑定(无数量语义;Quantity 列仅兼容旧库,固定为 1)。</summary>
|
||||
public sealed class ContainerMaterial : EntityBase
|
||||
{
|
||||
public Guid ContainerId { get; set; }
|
||||
public Guid MaterialId { get; set; }
|
||||
public decimal Quantity { get; set; }
|
||||
public decimal Quantity { get; set; } = 1;
|
||||
[MaxLength(64)] public string BatchNo { get; set; } = "";
|
||||
[MaxLength(64)] public string SerialNo { get; set; } = "";
|
||||
[MaxLength(32)] public string Status { get; set; } = ContainerMaterialStatuses.Loaded;
|
||||
[MaxLength(32)] public string Status { get; set; } = ContainerMaterialStatuses.Bound;
|
||||
public DateTimeOffset BoundAt { get; set; }
|
||||
public DateTimeOffset LoadedAt { get; set; }
|
||||
public DateTimeOffset? UnloadedAt { get; set; }
|
||||
}
|
||||
|
||||
public sealed class StockEvent
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(32)] public string EventType { get; set; } = "";
|
||||
public Guid? MaterialId { get; set; }
|
||||
[MaxLength(64)] public string MaterialCode { get; set; } = "";
|
||||
[MaxLength(128)] public string MaterialName { get; set; } = "";
|
||||
[MaxLength(128)] public string MaterialBarcode { get; set; } = "";
|
||||
[MaxLength(64)] public string MaterialTypeCode { get; set; } = "";
|
||||
public Guid? ContainerId { get; set; }
|
||||
[MaxLength(64)] public string ContainerCode { get; set; } = "";
|
||||
[MaxLength(128)] public string ContainerName { get; set; } = "";
|
||||
public Guid? StorageId { get; set; }
|
||||
[MaxLength(64)] public string StorageCode { get; set; } = "";
|
||||
[MaxLength(128)] public string StorageName { get; set; } = "";
|
||||
[MaxLength(64)] public string AreaCode { get; set; } = "";
|
||||
public Guid? FromStorageId { get; set; }
|
||||
[MaxLength(64)] public string FromStorageCode { get; set; } = "";
|
||||
[MaxLength(128)] public string FromStorageName { get; set; } = "";
|
||||
public Guid? ToStorageId { get; set; }
|
||||
[MaxLength(64)] public string ToStorageCode { get; set; } = "";
|
||||
[MaxLength(128)] public string ToStorageName { get; set; } = "";
|
||||
[MaxLength(64)] public string RefType { get; set; } = "Manual";
|
||||
public Guid? RefId { get; set; }
|
||||
[MaxLength(64)] public string RefCode { get; set; } = "";
|
||||
[MaxLength(128)] public string Operator { get; set; } = "";
|
||||
public DateTimeOffset OperatedAt { get; set; }
|
||||
[MaxLength(500)] public string Reason { get; set; } = "";
|
||||
}
|
||||
|
||||
public sealed class ContainerLocationHistory : WarehouseHistoryBase
|
||||
{
|
||||
[MaxLength(32)] public string FromLocationType { get; set; } = "";
|
||||
@@ -101,6 +172,20 @@ public sealed class ContainerMaterialHistory : WarehouseHistoryBase
|
||||
public decimal QuantityDelta { get; set; }
|
||||
}
|
||||
|
||||
public static class AreaLayoutModes
|
||||
{
|
||||
public const string Flat = "Flat";
|
||||
public const string Grid = "Grid";
|
||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase) { Flat, Grid };
|
||||
}
|
||||
|
||||
public static class LocationKinds
|
||||
{
|
||||
public const string Grid = "Grid";
|
||||
public const string Station = "Station";
|
||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase) { Grid, Station };
|
||||
}
|
||||
|
||||
public static class ContainerLocationTypes
|
||||
{
|
||||
public const string Storage = "Storage";
|
||||
@@ -118,20 +203,43 @@ public static class ContainerLocationStatuses
|
||||
|
||||
public static class ContainerMaterialStatuses
|
||||
{
|
||||
public const string Bound = "Bound";
|
||||
// 兼容旧数据
|
||||
public const string Loaded = "Loaded";
|
||||
public const string Unloaded = "Unloaded";
|
||||
public const string Adjusted = "Adjusted";
|
||||
public const string Frozen = "Frozen";
|
||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase) { Loaded, Unloaded, Adjusted, Frozen };
|
||||
public static readonly HashSet<string> ActiveBind = new(StringComparer.OrdinalIgnoreCase) { Bound, Loaded, Adjusted, Frozen };
|
||||
}
|
||||
|
||||
public static class ContainerStatuses
|
||||
{
|
||||
public const string EmptyMaterial = "EmptyMaterial";
|
||||
public const string FullMaterial = "FullMaterial";
|
||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase) { EmptyMaterial, FullMaterial };
|
||||
}
|
||||
|
||||
public static class StorageStatuses
|
||||
{
|
||||
public const string Empty = "Empty";
|
||||
public const string EmptyContainer = "EmptyContainer";
|
||||
public const string FullContainer = "FullContainer";
|
||||
public const string Disabled = "Disabled";
|
||||
// 兼容旧值(读时映射,写时用新值)
|
||||
public const string Available = "Available";
|
||||
public const string Idle = "Idle";
|
||||
public const string Occupied = "Occupied";
|
||||
public const string Disabled = "Disabled";
|
||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase) { Available, Idle, Occupied, Disabled };
|
||||
|
||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase)
|
||||
{ Empty, EmptyContainer, FullContainer, Disabled };
|
||||
|
||||
public static string Normalize(string? status) => status switch
|
||||
{
|
||||
Available or Idle => Empty,
|
||||
Occupied => FullContainer,
|
||||
_ when string.IsNullOrWhiteSpace(status) => Empty,
|
||||
_ => status
|
||||
};
|
||||
}
|
||||
|
||||
public static class StorageTypes
|
||||
@@ -143,6 +251,20 @@ public static class StorageTypes
|
||||
public const string FinishedGoods = "FinishedGoods";
|
||||
}
|
||||
|
||||
public static class MaterialLifecycles
|
||||
{
|
||||
public const string Active = "Active";
|
||||
public const string Archived = "Archived";
|
||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase) { Active, Archived };
|
||||
}
|
||||
|
||||
public static class StockEventTypes
|
||||
{
|
||||
public const string Bind = "Bind";
|
||||
public const string Unbind = "Unbind";
|
||||
public const string ContainerMove = "ContainerMove";
|
||||
}
|
||||
|
||||
public static class WmsTransportTriggerTypes
|
||||
{
|
||||
public const string MaterialCall = "MaterialCall";
|
||||
@@ -194,6 +316,7 @@ public sealed class WmsTransportTask : EntityBase
|
||||
public Guid TargetStorageId { get; set; }
|
||||
public Guid ContainerId { get; set; }
|
||||
public Guid? MaterialId { get; set; }
|
||||
/// <summary>已废弃:单物料实体模型不再使用数量。</summary>
|
||||
public decimal? Quantity { get; set; }
|
||||
[MaxLength(32)] public string Status { get; set; } = WmsTransportTaskStatuses.Pending;
|
||||
[MaxLength(64)] public string DispatchMissionId { get; set; } = "";
|
||||
@@ -227,3 +350,9 @@ public sealed class WmsTransportTaskHistory
|
||||
[MaxLength(1000)] public string ErrorMessage { get; set; } = "";
|
||||
public string SnapshotJson { get; set; } = "{}";
|
||||
}
|
||||
|
||||
public static class WmsDefaults
|
||||
{
|
||||
public const string DefaultWarehouseCode = "DEFAULT";
|
||||
public const string DefaultWarehouseName = "默认仓库";
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@ public sealed class WmsReferenceValidator
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task EnsureWarehouseAsync(Guid id)
|
||||
{
|
||||
if (!await _db.Warehouses.AnyAsync(x => x.Id == id))
|
||||
throw new InvalidOperationException("仓库不存在");
|
||||
}
|
||||
|
||||
public async Task EnsureAreaAsync(Guid id)
|
||||
{
|
||||
if (!await _db.WarehouseAreas.AnyAsync(x => x.Id == id))
|
||||
@@ -36,6 +42,12 @@ public sealed class WmsReferenceValidator
|
||||
throw new InvalidOperationException("物料不存在");
|
||||
}
|
||||
|
||||
public async Task EnsureMaterialTypeCodeAsync(string typeCode)
|
||||
{
|
||||
if (!await _db.MaterialTypes.AnyAsync(x => x.Code == typeCode))
|
||||
throw new InvalidOperationException("物料类型不存在");
|
||||
}
|
||||
|
||||
public async Task<(string Code, string Name)> ResolveLocationSnapshotAsync(string locationType, string locationId)
|
||||
{
|
||||
if (!ContainerLocationTypes.All.Contains(locationType))
|
||||
|
||||
+621
-166
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user