持久层重构为独立 MiGu.DB 工程并优化集成

- 新增 MiGu.DB 项目,迁移所有领域实体与枚举,统一模型约定
- 实现 Entity/Repository/UoW/Provider/Exception 等接口与实现
- 支持数据修补机制,完善 Sqlite 初始迁移与数据库管理
- Server 侧移除 EF Core 相关,依赖 MiGu.DB,PlatformPersistence 适配
- 业务服务注入 UoW/Repository,状态字段统一用 enum 及辅助类
- 统一异常处理,Controller 映射 HTTP 状态码
- 配置项与文档补充数据库启动、SchemaMode、迁移说明
- 新增 GlobalUsings.Db.cs、WmsStatusAliases.cs 简化类型引用
- 新增 HttpActorContextMiddleware 支持操作者上下文一致性
- 新增 MiGuDbContextModelSnapshot 追踪数据库结构
- 优化代码结构,解耦领域与持久层,提升扩展性与安全性
This commit is contained in:
2026-07-27 14:26:04 +08:00
parent bdcd88608c
commit 51c3fc1994
57 changed files with 6663 additions and 1396 deletions
View File
@@ -0,0 +1,9 @@
namespace MiGu.DB.Domains.Dashboard;
public sealed class UserDashboardShortcut
{
public string UserId { get; set; } = "";
public string Scope { get; set; } = "";
public string KeysJson { get; set; } = "[]";
public DateTimeOffset UpdatedAt { get; set; }
}
+105
View File
@@ -0,0 +1,105 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MiGu.DB.Abstractions.Runtime;
using MiGu.DB.Domains.Wms;
using MiGu.DB.Kernel.Context;
namespace MiGu.DB.Domains.Migrators;
// 启动期数据修补:Schema 初始化之后按 Order 执行,须幂等。
// 优先 LINQ / ExecuteUpdateAsync;枚举 converter 无法匹配旧字符串时允许定点 raw UPDATESqlite)。
/// <summary>将 simple_fields.other 回填到 car_type(替代原 PlatformPersistence raw UPDATE)。</summary>
public sealed class SimpleFieldsCarTypeBackfillMigrator : IDataMigrator
{
public int Order => 10;
public string Name => "SimpleFields.CarTypeBackfill";
public async Task MigrateAsync(DbContext db, CancellationToken ct = default)
{
if (db is not MiGuDbContext ctx) return;
await ctx.SimpleFields
.Where(x => (x.CarType == null || x.CarType == "") && x.Other != "")
.ExecuteUpdateAsync(s => s.SetProperty(x => x.CarType, x => x.Other), ct);
await ctx.SimpleFields
.Where(x => x.Other != "" && x.Other == x.CarType)
.ExecuteUpdateAsync(s => s.SetProperty(x => x.Other, ""), ct);
}
}
/// <summary>
/// 把库内旧状态字符串刷成规范枚举名(Available/Idle→EmptyOccupied→FullContainer)。
/// 仅 Sqlite 表名/列名;绕过枚举 HasConversionExecuteUpdate + 字符串比较会 InvalidCast)。
/// </summary>
public sealed class LegacyStatusNormalizationMigrator : IDataMigrator
{
public int Order => 15;
public string Name => "Wms.LegacyStatusNormalization";
public async Task MigrateAsync(DbContext db, CancellationToken ct = default)
{
if (db is not MiGuDbContext) return;
await db.Database.ExecuteSqlRawAsync(
"""
UPDATE wms_storages
SET Status = 'Empty'
WHERE Status IN ('Available', 'Idle')
""",
ct);
await db.Database.ExecuteSqlRawAsync(
"""
UPDATE wms_storages
SET Status = 'FullContainer'
WHERE Status = 'Occupied'
""",
ct);
await db.Database.ExecuteSqlRawAsync(
"""
UPDATE wms_areas
SET LayoutMode = 'Flat'
WHERE LayoutMode IS NULL OR LayoutMode = ''
""",
ct);
}
}
/// <summary>
/// LocationType=Storage 时回填 StorageId,供库存 join;写路径 BindOrTransferLocation 也会维护该字段。
/// </summary>
public sealed class ContainerLocationStorageIdBackfillMigrator : IDataMigrator
{
public int Order => 20;
public string Name => "Wms.ContainerLocation.StorageId";
public async Task MigrateAsync(DbContext db, CancellationToken ct = default)
{
if (db is not MiGuDbContext ctx) return;
var rows = await ctx.ContainerLocations
.Where(x => x.LocationType == ContainerLocationType.Storage && x.StorageId == null)
.ToListAsync(ct);
foreach (var row in rows)
{
if (Guid.TryParse(row.LocationId, out var sid))
row.StorageId = sid;
}
if (rows.Count > 0)
await ctx.SaveChangesAsync(ct);
}
}
public static class DataMigratorRegistration
{
public static IServiceCollection AddMiGuDataMigrators(this IServiceCollection services)
{
services.AddSingleton<IDataMigrator, SimpleFieldsCarTypeBackfillMigrator>();
services.AddSingleton<IDataMigrator, LegacyStatusNormalizationMigrator>();
services.AddSingleton<IDataMigrator, ContainerLocationStorageIdBackfillMigrator>();
return services;
}
}
@@ -0,0 +1,32 @@
using System.ComponentModel.DataAnnotations;
using MiGu.DB.Kernel.Entities;
namespace MiGu.DB.Domains.SimpleFields;
public sealed class SimpleField : Entity<Guid>
{
[MaxLength(64)] public string CarType { get; set; } = "";
[MaxLength(64)] public string FieldType { get; set; } = "";
[MaxLength(128)] public string Key { get; set; } = "";
public string Value { get; set; } = "";
[MaxLength(128)] public string DataType { get; set; } = "";
[MaxLength(256)] public string? Chinese { get; set; }
[MaxLength(256)] public string? English { get; set; }
[MaxLength(512)] public string Other { get; set; } = "";
public bool IsDefault { get; set; }
public DateTimeOffset CreateTime { get; set; }
public DateTimeOffset UpdateTime { get; set; }
}
public static class SimpleFieldDateTime
{
public const string Format = "yyyy-MM-dd HH:mm:ss";
public static DateTimeOffset Now => DateTimeOffset.UtcNow;
public static string ToStorage(DateTimeOffset value)
=> value.UtcDateTime.ToString(Format);
public static DateTimeOffset FromStorage(string value)
=> DateTimeOffset.Parse(value);
}
@@ -0,0 +1,104 @@
using System.ComponentModel.DataAnnotations;
using MiGu.DB.Kernel.Entities;
namespace MiGu.DB.Domains.Transport;
// 运输规则/任务/预占实体。状态字段为枚举;复数 *Statuses 辅助类供 Server 解析 DTO 字符串。
public sealed class WmsTransportRule : AggregateRoot
{
[MaxLength(64)] public string Code { get; set; } = "";
[MaxLength(128)] public string Name { get; set; } = "";
public WmsTransportTriggerType TriggerType { get; set; } = WmsTransportTriggerType.MaterialCall;
public bool Enabled { get; set; } = true;
public int Priority { get; set; }
public string SourceSelectorJson { get; set; } = "{}";
public string TargetSelectorJson { get; set; } = "{}";
public string TaskOptionsJson { get; set; } = "{}";
}
public sealed class WmsTransportTask : AggregateRoot
{
[MaxLength(64)] public string BusinessType { get; set; } = "";
public Guid? RuleId { get; set; }
public Guid SourceStorageId { get; set; }
public Guid TargetStorageId { get; set; }
public Guid ContainerId { get; set; }
public Guid? MaterialId { get; set; }
public decimal? Quantity { get; set; }
public WmsTransportTaskStatus Status { get; set; } = WmsTransportTaskStatus.Pending;
[MaxLength(64)] public string DispatchMissionId { get; set; } = "";
[MaxLength(64)] public string DeliveryId { get; set; } = "";
[MaxLength(32)] public string DispatchStatus { get; set; } = "";
[MaxLength(500)] public string Reason { get; set; } = "";
public string SnapshotJson { get; set; } = "{}";
[MaxLength(1000)] public string ErrorMessage { get; set; } = "";
public int TaskPriority { get; set; }
}
public sealed class WmsTransportReservation : AggregateRoot
{
public Guid TaskId { get; set; }
public Guid ContainerId { get; set; }
public Guid SourceStorageId { get; set; }
public Guid TargetStorageId { get; set; }
public WmsReservationStatus Status { get; set; } = WmsReservationStatus.Active;
public DateTimeOffset? ExpiresAt { get; set; }
}
public sealed class WmsTransportTaskHistory : Entity<Guid>
{
public Guid TaskId { get; set; }
[MaxLength(32)] public string FromStatus { get; set; } = "";
[MaxLength(32)] public string ToStatus { get; set; } = "";
[MaxLength(128)] public string Operator { get; set; } = "";
public DateTimeOffset OperatedAt { get; set; }
[MaxLength(500)] public string Reason { get; set; } = "";
[MaxLength(1000)] public string ErrorMessage { get; set; } = "";
public string SnapshotJson { get; set; } = "{}";
}
public static class WmsTransportTriggerTypes
{
// 复数辅助类:与枚举分离,避免与属性/类型同名冲突,并提供 ParseOr / All
public const WmsTransportTriggerType MaterialCall = WmsTransportTriggerType.MaterialCall;
public const WmsTransportTriggerType FinishedGoodsOffline = WmsTransportTriggerType.FinishedGoodsOffline;
public const WmsTransportTriggerType AutoTransfer = WmsTransportTriggerType.AutoTransfer;
public static readonly HashSet<WmsTransportTriggerType> All = new()
{ MaterialCall, FinishedGoodsOffline, AutoTransfer };
public static bool IsDefined(string? value) =>
Enum.TryParse<WmsTransportTriggerType>(value, true, out var e) && All.Contains(e);
public static WmsTransportTriggerType ParseOr(string? value, WmsTransportTriggerType fallback = WmsTransportTriggerType.MaterialCall) =>
Enum.TryParse<WmsTransportTriggerType>(value, true, out var e) && All.Contains(e) ? e : fallback;
}
public static class WmsTransportTaskStatuses
{
public const WmsTransportTaskStatus Pending = WmsTransportTaskStatus.Pending;
public const WmsTransportTaskStatus Reserved = WmsTransportTaskStatus.Reserved;
public const WmsTransportTaskStatus Dispatched = WmsTransportTaskStatus.Dispatched;
public const WmsTransportTaskStatus InTransit = WmsTransportTaskStatus.InTransit;
public const WmsTransportTaskStatus Completed = WmsTransportTaskStatus.Completed;
public const WmsTransportTaskStatus Failed = WmsTransportTaskStatus.Failed;
public const WmsTransportTaskStatus Cancelled = WmsTransportTaskStatus.Cancelled;
public static readonly HashSet<WmsTransportTaskStatus> Active = new()
{ Pending, Reserved, Dispatched, InTransit };
public static readonly HashSet<WmsTransportTaskStatus> All = new()
{ Pending, Reserved, Dispatched, InTransit, Completed, Failed, Cancelled };
}
public static class WmsReservationStatuses
{
public const WmsReservationStatus Active = WmsReservationStatus.Active;
public const WmsReservationStatus Released = WmsReservationStatus.Released;
public const WmsReservationStatus Expired = WmsReservationStatus.Expired;
}
public static class WmsDispatchStatuses
{
public const WmsDispatchStatus Dispatched = WmsDispatchStatus.Dispatched;
public const WmsDispatchStatus Failed = WmsDispatchStatus.Failed;
}
@@ -0,0 +1,35 @@
namespace MiGu.DB.Domains.Transport;
/// <summary>
/// 运输域状态/触发类型枚举。存储规则同 WmsHasConversion&lt;string&gt;,成员名 = 列值。
/// </summary>
public enum WmsTransportTriggerType
{
MaterialCall,
FinishedGoodsOffline,
AutoTransfer
}
public enum WmsTransportTaskStatus
{
Pending,
Reserved,
Dispatched,
InTransit,
Completed,
Failed,
Cancelled
}
public enum WmsReservationStatus
{
Active,
Released,
Expired
}
public enum WmsDispatchStatus
{
Dispatched,
Failed
}
@@ -0,0 +1,112 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using MiGu.DB.Domains.Dashboard;
using MiGu.DB.Domains.SimpleFields;
using MiGu.DB.Domains.Transport;
namespace MiGu.DB.Domains.Transport
{
public sealed class WmsTransportRuleConfiguration : IEntityTypeConfiguration<WmsTransportRule>
{
public void Configure(EntityTypeBuilder<WmsTransportRule> b)
{
b.ToTable("wms_transport_rules");
b.HasKey(x => x.Id);
b.HasIndex(x => x.Code).IsUnique();
b.HasIndex(x => new { x.TriggerType, x.Enabled, x.Priority });
b.Property(x => x.SourceSelectorJson).HasColumnType("text");
b.Property(x => x.TargetSelectorJson).HasColumnType("text");
b.Property(x => x.TaskOptionsJson).HasColumnType("text");
}
}
public sealed class WmsTransportTaskConfiguration : IEntityTypeConfiguration<WmsTransportTask>
{
public void Configure(EntityTypeBuilder<WmsTransportTask> b)
{
b.ToTable("wms_transport_tasks");
b.HasKey(x => x.Id);
b.HasIndex(x => x.Status);
b.HasIndex(x => x.ContainerId);
b.HasIndex(x => x.TargetStorageId);
b.Property(x => x.Quantity).HasPrecision(18, 4);
b.Property(x => x.SnapshotJson).HasColumnType("text");
}
}
public sealed class WmsTransportReservationConfiguration : IEntityTypeConfiguration<WmsTransportReservation>
{
public void Configure(EntityTypeBuilder<WmsTransportReservation> b)
{
b.ToTable("wms_transport_reservations");
b.HasKey(x => x.Id);
b.HasIndex(x => new { x.ContainerId, x.Status });
b.HasIndex(x => new { x.TargetStorageId, x.Status });
}
}
public sealed class WmsTransportTaskHistoryConfiguration : IEntityTypeConfiguration<WmsTransportTaskHistory>
{
public void Configure(EntityTypeBuilder<WmsTransportTaskHistory> b)
{
b.ToTable("wms_transport_task_history");
b.HasKey(x => x.Id);
b.Property(x => x.FromStatus).HasMaxLength(32);
b.Property(x => x.ToStatus).HasMaxLength(32);
b.Property(x => x.Operator).HasMaxLength(128);
b.Property(x => x.Reason).HasMaxLength(500);
b.Property(x => x.ErrorMessage).HasMaxLength(1000);
b.Property(x => x.SnapshotJson).HasColumnType("text");
b.HasIndex(x => x.TaskId);
b.HasIndex(x => x.OperatedAt);
}
}
}
namespace MiGu.DB.Domains.SimpleFields
{
public sealed class SimpleFieldConfiguration : IEntityTypeConfiguration<SimpleField>
{
public void Configure(EntityTypeBuilder<SimpleField> b)
{
b.ToTable("simple_fields");
b.HasKey(x => x.Id);
b.Property(x => x.Id).HasColumnName("id");
b.Property(x => x.CarType).HasColumnName("car_type").HasMaxLength(64);
b.Property(x => x.FieldType).HasColumnName("field_type").HasMaxLength(64);
b.Property(x => x.Key).HasColumnName("key").HasMaxLength(128);
b.Property(x => x.Value).HasColumnName("value");
b.Property(x => x.DataType).HasColumnName("data_type").HasMaxLength(128);
b.Property(x => x.Chinese).HasColumnName("chinese").HasMaxLength(256).IsRequired(false);
b.Property(x => x.English).HasColumnName("english").HasMaxLength(256).IsRequired(false);
b.Property(x => x.Other).HasColumnName("other").HasMaxLength(512);
b.Property(x => x.IsDefault).HasColumnName("is_default");
var dateTime = new ValueConverter<DateTimeOffset, string>(
v => SimpleFieldDateTime.ToStorage(v),
v => SimpleFieldDateTime.FromStorage(v));
b.Property(x => x.CreateTime).HasColumnName("create_time").HasConversion(dateTime).HasMaxLength(19);
b.Property(x => x.UpdateTime).HasColumnName("update_time").HasConversion(dateTime).HasMaxLength(19);
b.HasIndex(x => new { x.CarType, x.FieldType, x.Key }).IsUnique();
}
}
}
namespace MiGu.DB.Domains.Dashboard
{
public sealed class UserDashboardShortcutConfiguration : IEntityTypeConfiguration<UserDashboardShortcut>
{
public void Configure(EntityTypeBuilder<UserDashboardShortcut> b)
{
b.ToTable("user_dashboard_shortcuts");
b.HasKey(x => new { x.UserId, x.Scope });
b.Property(x => x.UserId).HasColumnName("user_id").HasMaxLength(64);
b.Property(x => x.Scope).HasColumnName("scope").HasMaxLength(32);
b.Property(x => x.KeysJson).HasColumnName("keys_json").HasColumnType("text");
var dateTime = new ValueConverter<DateTimeOffset, string>(
v => v.UtcDateTime.ToString("O"),
v => DateTimeOffset.Parse(v));
b.Property(x => x.UpdatedAt).HasColumnName("updated_at").HasConversion(dateTime).HasMaxLength(40);
}
}
}
+170
View File
@@ -0,0 +1,170 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using MiGu.DB.Domains.Dashboard;
using MiGu.DB.Domains.SimpleFields;
using MiGu.DB.Domains.Transport;
using MiGu.DB.Domains.Wms;
using MiGu.DB.Kernel.Entities;
namespace MiGu.DB.Domains.Wms;
public sealed class WarehouseConfiguration : IEntityTypeConfiguration<Warehouse>
{
public void Configure(EntityTypeBuilder<Warehouse> b)
{
b.ToTable("wms_warehouses");
b.HasKey(x => x.Id);
b.HasIndex(x => x.Code).IsUnique();
}
}
public sealed class WarehouseAreaConfiguration : IEntityTypeConfiguration<WarehouseArea>
{
public void Configure(EntityTypeBuilder<WarehouseArea> b)
{
b.ToTable("wms_areas");
b.HasKey(x => x.Id);
b.HasIndex(x => x.Code).IsUnique();
b.HasIndex(x => x.WarehouseId);
}
}
public sealed class StorageConfiguration : IEntityTypeConfiguration<Storage>
{
public void Configure(EntityTypeBuilder<Storage> b)
{
b.ToTable("wms_storages");
b.HasKey(x => x.Id);
b.HasIndex(x => x.Code).IsUnique();
b.HasIndex(x => x.AreaId);
b.HasIndex(x => x.Barcode);
}
}
public sealed class ContainerConfiguration : IEntityTypeConfiguration<Container>
{
public void Configure(EntityTypeBuilder<Container> b)
{
b.ToTable("wms_containers");
b.HasKey(x => x.Id);
b.HasIndex(x => x.Code).IsUnique();
}
}
public sealed class MaterialTypeConfiguration : IEntityTypeConfiguration<MaterialType>
{
public void Configure(EntityTypeBuilder<MaterialType> b)
{
b.ToTable("wms_material_types");
b.HasKey(x => x.Id);
b.HasIndex(x => x.Code).IsUnique();
}
}
public sealed class MaterialConfiguration : IEntityTypeConfiguration<Material>
{
public void Configure(EntityTypeBuilder<Material> b)
{
b.ToTable("wms_materials");
b.HasKey(x => x.Id);
b.HasIndex(x => x.Code).IsUnique();
b.HasIndex(x => x.Barcode);
b.HasIndex(x => new { x.LifecycleStatus, x.UpdatedAt });
b.HasIndex(x => x.TypeCode);
}
}
public sealed class ContainerLocationConfiguration : IEntityTypeConfiguration<ContainerLocation>
{
public void Configure(EntityTypeBuilder<ContainerLocation> b)
{
b.ToTable("wms_container_locations");
b.HasKey(x => x.Id);
b.HasIndex(x => x.ContainerId).IsUnique();
b.HasIndex(x => new { x.LocationType, x.LocationId });
b.HasIndex(x => x.StorageId);
}
}
public sealed class ContainerMaterialConfiguration : IEntityTypeConfiguration<ContainerMaterial>
{
public void Configure(EntityTypeBuilder<ContainerMaterial> b)
{
b.ToTable("wms_container_materials");
b.HasKey(x => x.Id);
b.HasIndex(x => x.MaterialId).IsUnique();
b.HasIndex(x => x.ContainerId);
b.Property(x => x.Quantity).HasPrecision(18, 4);
}
}
public sealed class StockEventConfiguration : IEntityTypeConfiguration<StockEvent>
{
public void Configure(EntityTypeBuilder<StockEvent> b)
{
b.ToTable("wms_stock_events");
b.HasKey(x => x.Id);
b.Property(x => x.EventType).HasMaxLength(32);
b.Property(x => x.MaterialCode).HasMaxLength(64);
b.Property(x => x.MaterialName).HasMaxLength(128);
b.Property(x => x.MaterialBarcode).HasMaxLength(128);
b.Property(x => x.MaterialTypeCode).HasMaxLength(64);
b.Property(x => x.ContainerCode).HasMaxLength(64);
b.Property(x => x.ContainerName).HasMaxLength(128);
b.Property(x => x.StorageCode).HasMaxLength(64);
b.Property(x => x.StorageName).HasMaxLength(128);
b.Property(x => x.AreaCode).HasMaxLength(64);
b.Property(x => x.FromStorageCode).HasMaxLength(64);
b.Property(x => x.FromStorageName).HasMaxLength(128);
b.Property(x => x.ToStorageCode).HasMaxLength(64);
b.Property(x => x.ToStorageName).HasMaxLength(128);
b.Property(x => x.RefType).HasMaxLength(64);
b.Property(x => x.RefCode).HasMaxLength(64);
b.Property(x => x.Operator).HasMaxLength(128);
b.Property(x => x.Reason).HasMaxLength(500);
b.HasIndex(x => x.OperatedAt);
b.HasIndex(x => x.EventType);
b.HasIndex(x => x.MaterialId);
b.HasIndex(x => x.ContainerId);
}
}
public abstract class HistoryConfigurationBase<T> : IEntityTypeConfiguration<T> where T : HistoryEntity
{
private readonly string _table;
protected HistoryConfigurationBase(string table) => _table = table;
public virtual void Configure(EntityTypeBuilder<T> b)
{
b.ToTable(_table);
b.HasKey(x => x.Id);
b.Property(x => x.EventType).HasMaxLength(64);
b.Property(x => x.BeforeJson).HasColumnType("text");
b.Property(x => x.AfterJson).HasColumnType("text");
b.Property(x => x.Operator).HasMaxLength(128);
b.Property(x => x.Source).HasMaxLength(32);
b.Property(x => x.Reason).HasMaxLength(500);
b.Property(x => x.Remark).HasMaxLength(1000);
b.Property(x => x.Extend).HasColumnType("text");
b.HasIndex(x => x.ContainerId);
b.HasIndex(x => x.OperatedAt);
b.HasIndex(x => x.EventType);
}
}
public sealed class ContainerLocationHistoryConfiguration : HistoryConfigurationBase<ContainerLocationHistory>
{
public ContainerLocationHistoryConfiguration() : base("wms_container_location_history") { }
}
public sealed class ContainerMaterialHistoryConfiguration : HistoryConfigurationBase<ContainerMaterialHistory>
{
public ContainerMaterialHistoryConfiguration() : base("wms_container_material_history") { }
public override void Configure(EntityTypeBuilder<ContainerMaterialHistory> b)
{
base.Configure(b);
b.Property(x => x.QuantityDelta).HasPrecision(18, 4);
}
}
+174
View File
@@ -0,0 +1,174 @@
using System.ComponentModel.DataAnnotations;
using MiGu.DB.Kernel.Entities;
namespace MiGu.DB.Domains.Wms;
// WMS 聚合实体。状态/类型字段为枚举(库内 TEXT,见全局 HasConversion);DTO 仍用字符串,由 *Statuses 辅助类解析。
public sealed class Warehouse : AggregateRoot
{
[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 : AggregateRoot
{
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";
public AreaLayoutMode LayoutMode { get; set; } = AreaLayoutMode.Flat;
[MaxLength(32)] public string State { get; set; } = "Default";
public bool Enabled { get; set; } = true;
public int SortOrder { get; set; }
}
public sealed class Storage : AggregateRoot
{
public Guid AreaId { get; set; }
[MaxLength(64)] public string Code { get; set; } = "";
[MaxLength(128)] public string Name { get; set; } = "";
[MaxLength(64)] public string StorageType { get; set; } = StorageTypeCodes.Storage;
// 属性名与枚举类型同名合法;默认值引用枚举成员 LocationKind.Station(勿再引入同名静态类)
public LocationKind LocationKind { get; set; } = LocationKind.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; } = "";
public int Capacity { get; set; }
public StorageStatus Status { get; set; } = StorageStatus.Empty;
[MaxLength(64)] public string Usage { get; set; } = "";
public int Priority { get; set; }
[MaxLength(64)] public string ZoneCode { get; set; } = "";
public bool AllowInbound { get; set; } = true;
public bool AllowOutbound { get; set; } = true;
public bool Enabled { get; set; } = true;
}
public sealed class Container : AggregateRoot
{
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";
public ContainerStatus Status { get; set; } = ContainerStatus.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 : AggregateRoot
{
[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;
}
public sealed class Material : AggregateRoot
{
[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; } = "";
public MaterialLifecycle LifecycleStatus { get; set; } = MaterialLifecycle.Active;
public DateTimeOffset? UnboundAt { get; set; }
public bool Enabled { get; set; } = true;
}
public sealed class ContainerLocation : AggregateRoot
{
public Guid ContainerId { get; set; }
public ContainerLocationType LocationType { get; set; } = ContainerLocationType.Storage;
[MaxLength(64)] public string LocationId { get; set; } = "";
/// <summary>LocationType=Storage 时的库位 Id;由 LocationId 回填,供库存 join 下推。</summary>
public Guid? StorageId { get; set; }
[MaxLength(64)] public string LocationCode { get; set; } = "";
[MaxLength(128)] public string LocationName { get; set; } = "";
public ContainerLocationStatus Status { get; set; } = ContainerLocationStatus.Active;
public DateTimeOffset EnteredAt { get; set; }
}
public sealed class ContainerMaterial : AggregateRoot
{
public Guid ContainerId { get; set; }
public Guid MaterialId { get; set; }
public decimal Quantity { get; set; } = 1;
[MaxLength(64)] public string BatchNo { get; set; } = "";
[MaxLength(64)] public string SerialNo { get; set; } = "";
public ContainerMaterialStatus Status { get; set; } = ContainerMaterialStatus.Bound;
public DateTimeOffset BoundAt { get; set; }
public DateTimeOffset LoadedAt { get; set; }
public DateTimeOffset? UnloadedAt { get; set; }
}
public sealed class StockEvent : Kernel.Entities.Entity<Guid>
{
public StockEventType 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 : HistoryEntity
{
[MaxLength(32)] public string FromLocationType { get; set; } = "";
[MaxLength(64)] public string FromLocationId { get; set; } = "";
[MaxLength(32)] public string ToLocationType { get; set; } = "";
[MaxLength(64)] public string ToLocationId { get; set; } = "";
}
public sealed class ContainerMaterialHistory : HistoryEntity
{
public Guid? MaterialId { get; set; }
public decimal QuantityDelta { get; set; }
}
public static class StorageTypeCodes
{
public const string Storage = "Storage";
public const string LineSide = "LineSide";
public const string OfflinePoint = "OfflinePoint";
public const string Buffer = "Buffer";
public const string FinishedGoods = "FinishedGoods";
}
public static class WmsDefaults
{
public const string DefaultWarehouseCode = "DEFAULT";
public const string DefaultWarehouseName = "默认仓库";
}
+66
View File
@@ -0,0 +1,66 @@
namespace MiGu.DB.Domains.Wms;
/// <summary>
/// WMS 状态/类型枚举。列以字符串存储(见约定 HasConversion&lt;string&gt;),成员名即库内合法值。
/// 旧别名(Available/Idle/Occupied 等)不进入枚举,由 LegacyStatusNormalizationMigrator 刷库。
/// </summary>
public enum AreaLayoutMode
{
Flat,
Grid
}
public enum LocationKind
{
Grid,
Station
}
public enum StorageStatus
{
Empty,
EmptyContainer,
FullContainer,
Disabled
}
public enum ContainerStatus
{
EmptyMaterial,
FullMaterial
}
public enum ContainerLocationType
{
Storage,
Car
}
public enum ContainerLocationStatus
{
Active,
Locked,
Exception
}
public enum ContainerMaterialStatus
{
Bound,
Loaded,
Unloaded,
Adjusted,
Frozen
}
public enum MaterialLifecycle
{
Active,
Archived
}
public enum StockEventType
{
Bind,
Unbind,
ContainerMove
}
+116
View File
@@ -0,0 +1,116 @@
namespace MiGu.DB.Domains.Wms;
/// <summary>
/// 枚举辅助类(复数命名):承接 API/DTO 的字符串入参,解析为实体上的枚举。
/// 与同名枚举分离,避免「属性 LocationKind 初始值引用实例属性」一类编译冲突。
/// Server 侧通过 global using 别名(如 StorageStatuses)引用本文件类型。
/// </summary>
public static class AreaLayoutModes
{
public static readonly HashSet<AreaLayoutMode> All = new() { AreaLayoutMode.Flat, AreaLayoutMode.Grid };
public static AreaLayoutMode ParseOr(string? value, AreaLayoutMode fallback = AreaLayoutMode.Flat) =>
Enum.TryParse<AreaLayoutMode>(value, true, out var e) && All.Contains(e) ? e : fallback;
}
public static class LocationKinds
{
public const LocationKind Grid = LocationKind.Grid;
public const LocationKind Station = LocationKind.Station;
public static readonly HashSet<LocationKind> All = new() { Grid, Station };
public static LocationKind ParseOr(string? value, LocationKind fallback = LocationKind.Station) =>
Enum.TryParse<LocationKind>(value, true, out var e) && All.Contains(e) ? e : fallback;
}
public static class StorageStatuses
{
public const StorageStatus Empty = StorageStatus.Empty;
public const StorageStatus EmptyContainer = StorageStatus.EmptyContainer;
public const StorageStatus FullContainer = StorageStatus.FullContainer;
public const StorageStatus Disabled = StorageStatus.Disabled;
public static readonly HashSet<StorageStatus> All = new()
{ Empty, EmptyContainer, FullContainer, Disabled };
/// <summary>
/// 写路径归一:兼容历史字符串 Available/Idle/Occupied。
/// 库内残留旧值须靠 DataMigrator 刷掉,不能依赖 converter 读侧归一(WHERE 会漏行)。
/// </summary>
public static StorageStatus Normalize(string? status) => status switch
{
"Available" or "Idle" => Empty,
"Occupied" => FullContainer,
_ when Enum.TryParse<StorageStatus>(status, true, out var e) && All.Contains(e) => e,
_ => Empty
};
}
public static class ContainerStatuses
{
public const ContainerStatus EmptyMaterial = ContainerStatus.EmptyMaterial;
public const ContainerStatus FullMaterial = ContainerStatus.FullMaterial;
public static readonly HashSet<ContainerStatus> All = new() { EmptyMaterial, FullMaterial };
public static ContainerStatus ParseOr(string? value, ContainerStatus fallback = ContainerStatus.EmptyMaterial) =>
Enum.TryParse<ContainerStatus>(value, true, out var e) && All.Contains(e) ? e : fallback;
}
public static class ContainerLocationTypes
{
public const ContainerLocationType Storage = ContainerLocationType.Storage;
public const ContainerLocationType Car = ContainerLocationType.Car;
public static readonly HashSet<ContainerLocationType> All = new() { Storage, Car };
public static bool IsDefined(string? value) =>
Enum.TryParse<ContainerLocationType>(value, true, out var e) && All.Contains(e);
public static ContainerLocationType ParseOr(string? value, ContainerLocationType fallback = ContainerLocationType.Storage) =>
Enum.TryParse<ContainerLocationType>(value, true, out var e) && All.Contains(e) ? e : fallback;
public static bool EqualsString(ContainerLocationType value, string? other) =>
Enum.TryParse<ContainerLocationType>(other, true, out var e) && e == value;
}
public static class ContainerLocationStatuses
{
public const ContainerLocationStatus Active = ContainerLocationStatus.Active;
public const ContainerLocationStatus Locked = ContainerLocationStatus.Locked;
public const ContainerLocationStatus Exception = ContainerLocationStatus.Exception;
public static readonly HashSet<ContainerLocationStatus> All = new() { Active, Locked, Exception };
public static bool IsDefined(string? value) =>
Enum.TryParse<ContainerLocationStatus>(value, true, out var e) && All.Contains(e);
public static ContainerLocationStatus ParseOr(string? value, ContainerLocationStatus fallback = ContainerLocationStatus.Active) =>
Enum.TryParse<ContainerLocationStatus>(value, true, out var e) && All.Contains(e) ? e : fallback;
}
public static class ContainerMaterialStatuses
{
public const ContainerMaterialStatus Bound = ContainerMaterialStatus.Bound;
public const ContainerMaterialStatus Loaded = ContainerMaterialStatus.Loaded;
public const ContainerMaterialStatus Unloaded = ContainerMaterialStatus.Unloaded;
public const ContainerMaterialStatus Adjusted = ContainerMaterialStatus.Adjusted;
public const ContainerMaterialStatus Frozen = ContainerMaterialStatus.Frozen;
public static readonly HashSet<ContainerMaterialStatus> ActiveBind = new()
{ Bound, Loaded, Adjusted, Frozen };
}
public static class MaterialLifecycles
{
public const MaterialLifecycle Active = MaterialLifecycle.Active;
public const MaterialLifecycle Archived = MaterialLifecycle.Archived;
public static readonly HashSet<MaterialLifecycle> All = new() { Active, Archived };
public static MaterialLifecycle ParseOr(string? value, MaterialLifecycle fallback = MaterialLifecycle.Active) =>
Enum.TryParse<MaterialLifecycle>(value, true, out var e) && All.Contains(e) ? e : fallback;
}
public static class StockEventTypes
{
public const StockEventType Bind = StockEventType.Bind;
public const StockEventType Unbind = StockEventType.Unbind;
public const StockEventType ContainerMove = StockEventType.ContainerMove;
}