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:
2026-07-24 10:19:36 +08:00
parent ce66da772e
commit bdcd88608c
12 changed files with 1794 additions and 288 deletions
+106 -17
View File
@@ -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);
+44 -1
View File
@@ -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>
+137 -8
View File
@@ -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
View File
@@ -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))
File diff suppressed because it is too large Load Diff