覆盖包库回传、任务下发、CDM 任务同步与报警采集,并为包/任务 ID 与上传文件名加上路径安全校验。 Co-authored-by: Cursor <cursoragent@cursor.com>
317 lines
17 KiB
C#
317 lines
17 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
using MiGu.Server.Dashboard;
|
|
using MiGu.Server.Wms;
|
|
using MiGu.Server.SimpleFields;
|
|
|
|
namespace MiGu.Server.Persistence;
|
|
|
|
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>();
|
|
public DbSet<WmsTransportTask> WmsTransportTasks => Set<WmsTransportTask>();
|
|
public DbSet<WmsTransportReservation> WmsTransportReservations => Set<WmsTransportReservation>();
|
|
public DbSet<WmsTransportTaskHistory> WmsTransportTaskHistories => Set<WmsTransportTaskHistory>();
|
|
public DbSet<SimpleField> SimpleFields => Set<SimpleField>();
|
|
public DbSet<UserDashboardShortcut> UserDashboardShortcuts => Set<UserDashboardShortcut>();
|
|
public DbSet<MiGu.Server.Fleet.CdmTaskRecord> CdmTasks => Set<MiGu.Server.Fleet.CdmTaskRecord>();
|
|
public DbSet<MiGu.Server.Fleet.VehicleAlarmRecord> VehicleAlarms => Set<MiGu.Server.Fleet.VehicleAlarmRecord>();
|
|
|
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
{
|
|
var guid = new ValueConverter<Guid, string>(
|
|
v => v.ToString("D"),
|
|
v => Guid.Parse(v));
|
|
var nullableGuid = new ValueConverter<Guid?, string?>(
|
|
v => v.HasValue ? v.Value.ToString("D") : null,
|
|
v => string.IsNullOrWhiteSpace(v) ? null : Guid.Parse(v));
|
|
var dateTimeOffset = new ValueConverter<DateTimeOffset, string>(
|
|
v => v.UtcDateTime.ToString("O"),
|
|
v => DateTimeOffset.Parse(v));
|
|
var nullableDateTimeOffset = new ValueConverter<DateTimeOffset?, string?>(
|
|
v => v.HasValue ? v.Value.UtcDateTime.ToString("O") : null,
|
|
v => string.IsNullOrWhiteSpace(v) ? null : DateTimeOffset.Parse(v));
|
|
|
|
foreach (var entity in modelBuilder.Model.GetEntityTypes())
|
|
{
|
|
foreach (var p in entity.ClrType.GetProperties().Where(p => p.PropertyType == typeof(Guid)))
|
|
modelBuilder.Entity(entity.ClrType).Property(p.Name).HasConversion(guid).HasMaxLength(36);
|
|
foreach (var p in entity.ClrType.GetProperties().Where(p => p.PropertyType == typeof(Guid?)))
|
|
modelBuilder.Entity(entity.ClrType).Property(p.Name).HasConversion(nullableGuid).HasMaxLength(36);
|
|
foreach (var p in entity.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset)))
|
|
modelBuilder.Entity(entity.ClrType).Property(p.Name).HasConversion(dateTimeOffset).HasMaxLength(40);
|
|
foreach (var p in entity.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset?)))
|
|
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");
|
|
ConfigureEntityBase<WmsTransportRule>(modelBuilder, "wms_transport_rules");
|
|
ConfigureEntityBase<WmsTransportTask>(modelBuilder, "wms_transport_tasks");
|
|
ConfigureEntityBase<WmsTransportReservation>(modelBuilder, "wms_transport_reservations");
|
|
|
|
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();
|
|
// 库位占用 1:1:同一 Storage LocationId 同时只能有一条未删除记录(Car 等其它类型不限)
|
|
modelBuilder.Entity<ContainerLocation>()
|
|
.HasIndex(x => x.LocationId)
|
|
.IsUnique()
|
|
.HasFilter("LocationType = 'Storage' AND IsDeleted = 0")
|
|
.HasDatabaseName("IX_wms_container_locations_StorageLocationId");
|
|
modelBuilder.Entity<ContainerLocation>().HasIndex(x => new { x.LocationType, x.LocationId });
|
|
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 });
|
|
modelBuilder.Entity<WmsTransportTask>().HasIndex(x => x.Status);
|
|
modelBuilder.Entity<WmsTransportTask>().HasIndex(x => x.ContainerId);
|
|
modelBuilder.Entity<WmsTransportTask>().HasIndex(x => x.TargetStorageId);
|
|
modelBuilder.Entity<WmsTransportReservation>().HasIndex(x => new { x.ContainerId, x.Status });
|
|
modelBuilder.Entity<WmsTransportReservation>().HasIndex(x => new { x.TargetStorageId, x.Status });
|
|
modelBuilder.Entity<WmsTransportTask>().Property(x => x.Quantity).HasPrecision(18, 4);
|
|
|
|
modelBuilder.Entity<ContainerMaterial>().Property(x => x.Quantity).HasPrecision(18, 4);
|
|
modelBuilder.Entity<ContainerMaterialHistory>().Property(x => x.QuantityDelta).HasPrecision(18, 4);
|
|
|
|
ConfigureSimpleField(modelBuilder);
|
|
ConfigureUserDashboardShortcut(modelBuilder);
|
|
ConfigureCdmTask(modelBuilder);
|
|
ConfigureVehicleAlarm(modelBuilder);
|
|
}
|
|
|
|
private static void ConfigureVehicleAlarm(ModelBuilder modelBuilder)
|
|
{
|
|
var e = modelBuilder.Entity<MiGu.Server.Fleet.VehicleAlarmRecord>();
|
|
e.ToTable("vehicle_alarms");
|
|
e.HasKey(x => x.Id);
|
|
e.Property(x => x.Id).HasColumnName("id").HasMaxLength(36);
|
|
e.Property(x => x.CarId).HasColumnName("car_id");
|
|
e.Property(x => x.CarName).HasColumnName("car_name").HasMaxLength(128);
|
|
e.Property(x => x.Info).HasColumnName("info").HasColumnType("text");
|
|
e.Property(x => x.Level).HasColumnName("level");
|
|
e.Property(x => x.Status).HasColumnName("status").HasMaxLength(16);
|
|
e.Property(x => x.FirstAt).HasColumnName("first_at");
|
|
e.Property(x => x.LastAt).HasColumnName("last_at");
|
|
e.Property(x => x.ResolvedAt).HasColumnName("resolved_at").IsRequired(false);
|
|
e.Property(x => x.DurationSecs).HasColumnName("duration_secs").IsRequired(false);
|
|
e.Property(x => x.Acknowledged).HasColumnName("acknowledged");
|
|
e.Property(x => x.AcknowledgedAt).HasColumnName("acknowledged_at").IsRequired(false);
|
|
e.Property(x => x.AcknowledgedBy).HasColumnName("acknowledged_by").HasMaxLength(128).IsRequired(false);
|
|
e.HasIndex(x => new { x.CarId, x.Status });
|
|
e.HasIndex(x => x.Status);
|
|
e.HasIndex(x => x.FirstAt);
|
|
}
|
|
|
|
private static void ConfigureCdmTask(ModelBuilder modelBuilder)
|
|
{
|
|
var e = modelBuilder.Entity<MiGu.Server.Fleet.CdmTaskRecord>();
|
|
e.ToTable("cdm_tasks");
|
|
e.HasKey(x => x.Id);
|
|
e.Property(x => x.Id).HasColumnName("id").HasMaxLength(64);
|
|
e.Property(x => x.TaskId).HasColumnName("task_id").HasMaxLength(128).IsRequired(false);
|
|
e.Property(x => x.MissionId).HasColumnName("mission_id");
|
|
e.Property(x => x.MissionName).HasColumnName("mission_name").HasMaxLength(128);
|
|
e.Property(x => x.MissionTypeName).HasColumnName("mission_type").HasMaxLength(128);
|
|
e.Property(x => x.SrcSiteId).HasColumnName("src_site_id");
|
|
e.Property(x => x.SrcLabel).HasColumnName("src_label").HasMaxLength(256);
|
|
e.Property(x => x.DstSiteId).HasColumnName("dst_site_id");
|
|
e.Property(x => x.DstLabel).HasColumnName("dst_label").HasMaxLength(256);
|
|
e.Property(x => x.Status).HasColumnName("status").HasMaxLength(32);
|
|
e.Property(x => x.StatusCode).HasColumnName("status_code").HasMaxLength(32);
|
|
e.Property(x => x.CarId).HasColumnName("car_id").IsRequired(false);
|
|
e.Property(x => x.CarName).HasColumnName("car_name").HasMaxLength(128).IsRequired(false);
|
|
e.Property(x => x.Priority).HasColumnName("priority");
|
|
e.Property(x => x.CreateTime).HasColumnName("create_time").HasMaxLength(40).IsRequired(false);
|
|
e.Property(x => x.StartTime).HasColumnName("start_time").HasMaxLength(40).IsRequired(false);
|
|
e.Property(x => x.FinishTime).HasColumnName("finish_time").HasMaxLength(40).IsRequired(false);
|
|
e.Property(x => x.StuckReason).HasColumnName("stuck_reason").HasMaxLength(512).IsRequired(false);
|
|
e.Property(x => x.Overdue).HasColumnName("overdue");
|
|
e.Property(x => x.FirstSeenAt).HasColumnName("first_seen_at");
|
|
e.Property(x => x.LastSeenAt).HasColumnName("last_seen_at");
|
|
e.HasIndex(x => x.StatusCode);
|
|
e.HasIndex(x => x.CreateTime);
|
|
}
|
|
|
|
private static void ConfigureUserDashboardShortcut(ModelBuilder modelBuilder)
|
|
{
|
|
var e = modelBuilder.Entity<UserDashboardShortcut>();
|
|
e.ToTable("user_dashboard_shortcuts");
|
|
e.HasKey(x => new { x.UserId, x.Scope });
|
|
e.Property(x => x.UserId).HasColumnName("user_id").HasMaxLength(64);
|
|
e.Property(x => x.Scope).HasColumnName("scope").HasMaxLength(32);
|
|
e.Property(x => x.KeysJson).HasColumnName("keys_json").HasColumnType("text");
|
|
var dateTime = new ValueConverter<DateTimeOffset, string>(
|
|
v => v.UtcDateTime.ToString("O"),
|
|
v => DateTimeOffset.Parse(v));
|
|
e.Property(x => x.UpdatedAt).HasColumnName("updated_at").HasConversion(dateTime).HasMaxLength(40);
|
|
}
|
|
|
|
private static void ConfigureSimpleField(ModelBuilder modelBuilder)
|
|
{
|
|
var e = modelBuilder.Entity<SimpleField>();
|
|
e.ToTable("simple_fields");
|
|
e.HasKey(x => x.Id);
|
|
e.Property(x => x.Id).HasColumnName("id");
|
|
e.Property(x => x.CarType).HasColumnName("car_type").HasMaxLength(64);
|
|
e.Property(x => x.FieldType).HasColumnName("field_type").HasMaxLength(64);
|
|
e.Property(x => x.Key).HasColumnName("key").HasMaxLength(128);
|
|
e.Property(x => x.Value).HasColumnName("value");
|
|
e.Property(x => x.DataType).HasColumnName("data_type").HasMaxLength(128);
|
|
e.Property(x => x.Chinese).HasColumnName("chinese").HasMaxLength(256).IsRequired(false);
|
|
e.Property(x => x.English).HasColumnName("english").HasMaxLength(256).IsRequired(false);
|
|
e.Property(x => x.Other).HasColumnName("other").HasMaxLength(512);
|
|
e.Property(x => x.IsDefault).HasColumnName("is_default");
|
|
var dateTime = new ValueConverter<DateTimeOffset, string>(
|
|
v => SimpleFieldDateTime.ToStorage(v),
|
|
v => SimpleFieldDateTime.FromStorage(v));
|
|
e.Property(x => x.CreateTime).HasColumnName("create_time").HasConversion(dateTime).HasMaxLength(19);
|
|
e.Property(x => x.UpdateTime).HasColumnName("update_time").HasConversion(dateTime).HasMaxLength(19);
|
|
e.HasIndex(x => new { x.CarType, x.FieldType, x.Key }).IsUnique();
|
|
}
|
|
|
|
public override int SaveChanges(bool acceptAllChangesOnSuccess)
|
|
{
|
|
StampEntities();
|
|
return base.SaveChanges(acceptAllChangesOnSuccess);
|
|
}
|
|
|
|
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
|
|
{
|
|
StampEntities();
|
|
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
|
|
}
|
|
|
|
private void StampEntities()
|
|
{
|
|
var now = DateTimeOffset.UtcNow;
|
|
foreach (var e in ChangeTracker.Entries<EntityBase>())
|
|
{
|
|
if (e.State == EntityState.Added)
|
|
{
|
|
if (e.Entity.Id == Guid.Empty) e.Entity.Id = Guid.NewGuid();
|
|
e.Entity.CreatedAt = now;
|
|
e.Entity.UpdatedAt = now;
|
|
e.Entity.Version = Math.Max(1, e.Entity.Version);
|
|
if (string.IsNullOrWhiteSpace(e.Entity.Extend)) e.Entity.Extend = "{}";
|
|
}
|
|
else if (e.State == EntityState.Modified)
|
|
{
|
|
e.Entity.UpdatedAt = now;
|
|
e.Entity.Version += 1;
|
|
if (string.IsNullOrWhiteSpace(e.Entity.Extend)) e.Entity.Extend = "{}";
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void ConfigureEntityBase<T>(ModelBuilder modelBuilder, string table) where T : EntityBase
|
|
{
|
|
var e = modelBuilder.Entity<T>();
|
|
e.ToTable(table);
|
|
e.HasKey(x => x.Id);
|
|
e.Property(x => x.CreatedBy).HasMaxLength(128);
|
|
e.Property(x => x.UpdatedBy).HasMaxLength(128);
|
|
e.Property(x => x.DeletedBy).HasMaxLength(128);
|
|
e.Property(x => x.Remark).HasMaxLength(1000);
|
|
e.Property(x => x.Extend).HasColumnType("text");
|
|
e.HasQueryFilter(x => !x.IsDeleted);
|
|
}
|
|
|
|
private static void ConfigureHistory<T>(ModelBuilder modelBuilder, string table) where T : WarehouseHistoryBase
|
|
{
|
|
var e = modelBuilder.Entity<T>();
|
|
e.ToTable(table);
|
|
e.HasKey(x => x.Id);
|
|
e.Property(x => x.EventType).HasMaxLength(64);
|
|
e.Property(x => x.BeforeJson).HasColumnType("text");
|
|
e.Property(x => x.AfterJson).HasColumnType("text");
|
|
e.Property(x => x.Operator).HasMaxLength(128);
|
|
e.Property(x => x.Source).HasMaxLength(32);
|
|
e.Property(x => x.Reason).HasMaxLength(500);
|
|
e.Property(x => x.Remark).HasMaxLength(1000);
|
|
e.Property(x => x.Extend).HasColumnType("text");
|
|
e.HasIndex(x => x.ContainerId);
|
|
e.HasIndex(x => x.OperatedAt);
|
|
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>();
|
|
e.ToTable("wms_transport_task_history");
|
|
e.HasKey(x => x.Id);
|
|
e.Property(x => x.FromStatus).HasMaxLength(32);
|
|
e.Property(x => x.ToStatus).HasMaxLength(32);
|
|
e.Property(x => x.Operator).HasMaxLength(128);
|
|
e.Property(x => x.Reason).HasMaxLength(500);
|
|
e.Property(x => x.ErrorMessage).HasMaxLength(1000);
|
|
e.Property(x => x.SnapshotJson).HasColumnType("text");
|
|
e.HasIndex(x => x.TaskId);
|
|
e.HasIndex(x => x.OperatedAt);
|
|
}
|
|
}
|