引入WMS仓储主数据与关系管理全流程能力
后端实现基于EF Core的库区/库位/容器/物料/关系/历史等模型、服务与RESTful接口,支持多数据库Provider。前端新增类型、API与聚合页面,支持主数据及容器位置/物料关系的增删改查、绑定/解绑、装料/卸料、历史追溯。完善权限、菜单与文档,平台具备完整WMS能力。
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
namespace MiGu.Server.Persistence;
|
||||
|
||||
public abstract class EntityBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
public DateTimeOffset UpdatedAt { get; set; }
|
||||
public bool IsDeleted { get; set; }
|
||||
public DateTimeOffset? DeletedAt { get; set; }
|
||||
public string DeletedBy { get; set; } = "";
|
||||
public long Version { get; set; }
|
||||
public bool IsLock { get; set; }
|
||||
public string CreatedBy { get; set; } = "";
|
||||
public string UpdatedBy { get; set; } = "";
|
||||
public string Remark { get; set; } = "";
|
||||
public string Extend { get; set; } = "{}";
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using MiGu.Server.Wms;
|
||||
|
||||
namespace MiGu.Server.Persistence;
|
||||
|
||||
public sealed class PlatformDbContext : DbContext
|
||||
{
|
||||
public PlatformDbContext(DbContextOptions<PlatformDbContext> options) : base(options) { }
|
||||
|
||||
public DbSet<WarehouseArea> WarehouseAreas => Set<WarehouseArea>();
|
||||
public DbSet<Storage> Storages => Set<Storage>();
|
||||
public DbSet<Container> Containers => Set<Container>();
|
||||
public DbSet<Material> Materials => Set<Material>();
|
||||
public DbSet<ContainerLocation> ContainerLocations => Set<ContainerLocation>();
|
||||
public DbSet<ContainerMaterial> ContainerMaterials => Set<ContainerMaterial>();
|
||||
public DbSet<ContainerLocationHistory> ContainerLocationHistories => Set<ContainerLocationHistory>();
|
||||
public DbSet<ContainerMaterialHistory> ContainerMaterialHistories => Set<ContainerMaterialHistory>();
|
||||
|
||||
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));
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
ConfigureEntityBase<WarehouseArea>(modelBuilder, "wms_areas");
|
||||
ConfigureEntityBase<Storage>(modelBuilder, "wms_storages");
|
||||
ConfigureEntityBase<Container>(modelBuilder, "wms_containers");
|
||||
ConfigureEntityBase<Material>(modelBuilder, "wms_materials");
|
||||
ConfigureEntityBase<ContainerLocation>(modelBuilder, "wms_container_locations");
|
||||
ConfigureEntityBase<ContainerMaterial>(modelBuilder, "wms_container_materials");
|
||||
|
||||
ConfigureHistory<ContainerLocationHistory>(modelBuilder, "wms_container_location_history");
|
||||
ConfigureHistory<ContainerMaterialHistory>(modelBuilder, "wms_container_material_history");
|
||||
|
||||
modelBuilder.Entity<WarehouseArea>().HasIndex(x => x.Code).IsUnique();
|
||||
modelBuilder.Entity<Storage>().HasIndex(x => x.Code).IsUnique();
|
||||
modelBuilder.Entity<Storage>().HasIndex(x => x.AreaId);
|
||||
modelBuilder.Entity<Container>().HasIndex(x => x.Code).IsUnique();
|
||||
modelBuilder.Entity<Material>().HasIndex(x => x.Code).IsUnique();
|
||||
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>().Property(x => x.Quantity).HasPrecision(18, 4);
|
||||
modelBuilder.Entity<ContainerMaterialHistory>().Property(x => x.QuantityDelta).HasPrecision(18, 4);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using MiGu.Server.Wms;
|
||||
|
||||
namespace MiGu.Server.Persistence;
|
||||
|
||||
public static class PlatformPersistence
|
||||
{
|
||||
public static IServiceCollection AddPlatformPersistence(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddDbContext<PlatformDbContext>((sp, options) =>
|
||||
{
|
||||
var env = sp.GetRequiredService<IWebHostEnvironment>();
|
||||
var provider = configuration["Database:Provider"] ?? "sqlite";
|
||||
var connection = ResolveConnectionString(configuration, env, provider);
|
||||
|
||||
switch (provider.Trim().ToLowerInvariant())
|
||||
{
|
||||
case "sqlite":
|
||||
options.UseSqlite(connection);
|
||||
break;
|
||||
case "mysql":
|
||||
options.UseMySql(connection, ServerVersion.AutoDetect(connection));
|
||||
break;
|
||||
case "postgres":
|
||||
case "postgresql":
|
||||
case "npgsql":
|
||||
options.UseNpgsql(connection);
|
||||
break;
|
||||
case "sqlserver":
|
||||
case "mssql":
|
||||
options.UseSqlServer(connection);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"未知数据库 Provider: {provider}");
|
||||
}
|
||||
});
|
||||
|
||||
services.AddScoped<WmsReferenceValidator>();
|
||||
services.AddScoped<WmsService>();
|
||||
return services;
|
||||
}
|
||||
|
||||
public static async Task EnsurePlatformDatabaseAsync(this IServiceProvider services)
|
||||
{
|
||||
using var scope = services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<PlatformDbContext>();
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
}
|
||||
|
||||
private static string ResolveConnectionString(IConfiguration configuration, IWebHostEnvironment env, string provider)
|
||||
{
|
||||
var key = provider.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"postgres" or "postgresql" or "npgsql" => "PostgreSQL",
|
||||
"mssql" => "SqlServer",
|
||||
_ => provider
|
||||
};
|
||||
var configured = configuration.GetConnectionString(key) ?? configuration.GetConnectionString("Platform");
|
||||
if (!string.IsNullOrWhiteSpace(configured))
|
||||
{
|
||||
return IsSqlite(provider) ? NormalizeSqliteConnection(configured, env) : configured;
|
||||
}
|
||||
|
||||
var dataDir = Path.Combine(env.ContentRootPath, "data");
|
||||
Directory.CreateDirectory(dataDir);
|
||||
return $"Data Source={Path.Combine(dataDir, "platform.db")}";
|
||||
}
|
||||
|
||||
private static bool IsSqlite(string provider) =>
|
||||
string.Equals(provider.Trim(), "sqlite", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static string NormalizeSqliteConnection(string connection, IWebHostEnvironment env)
|
||||
{
|
||||
var builder = new SqliteConnectionStringBuilder(connection);
|
||||
if (string.IsNullOrWhiteSpace(builder.DataSource)) return connection;
|
||||
if (builder.DataSource is ":memory:") return connection;
|
||||
if (!Path.IsPathRooted(builder.DataSource))
|
||||
{
|
||||
builder.DataSource = Path.Combine(env.ContentRootPath, builder.DataSource);
|
||||
}
|
||||
var dir = Path.GetDirectoryName(builder.DataSource);
|
||||
if (!string.IsNullOrWhiteSpace(dir)) Directory.CreateDirectory(dir);
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user