持久层重构为独立 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
+133 -180
View File
@@ -1,6 +1,9 @@
using System.Security.Claims;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MiGu.DB.Abstractions.Entities;
using MiGu.DB.Abstractions.Persistence;
using MiGu.Server.Persistence;
namespace MiGu.Server.Wms;
@@ -8,12 +11,20 @@ namespace MiGu.Server.Wms;
public sealed class WmsService
{
private readonly PlatformDbContext _db;
private readonly IUnitOfWork _uow;
private readonly IServiceProvider _services;
private readonly WmsReferenceValidator _refs;
private readonly JsonSerializerOptions _json = new(JsonSerializerDefaults.Web);
public WmsService(PlatformDbContext db, WmsReferenceValidator refs)
public WmsService(
PlatformDbContext db,
IUnitOfWork uow,
IServiceProvider services,
WmsReferenceValidator refs)
{
_db = db;
_uow = uow;
_services = services;
_refs = refs;
}
@@ -31,7 +42,10 @@ public sealed class WmsService
{
var query = _db.Storages.AsNoTracking().AsQueryable();
if (areaId.HasValue) query = query.Where(x => x.AreaId == areaId.Value);
if (!string.IsNullOrWhiteSpace(locationKind)) query = query.Where(x => x.LocationKind == locationKind);
if (!string.IsNullOrWhiteSpace(locationKind) &&
Enum.TryParse<LocationKind>(locationKind.Trim(), true, out var kind) &&
LocationKinds.All.Contains(kind))
query = query.Where(x => x.LocationKind == kind);
return FilterByKeyword(query.OrderBy(x => x.Code), q).ToListAsync();
}
@@ -44,7 +58,7 @@ public sealed class WmsService
public Task<List<Material>> Materials(string? q = null, string? lifecycle = null, bool? onlyUnbound = null, bool? onlyBound = null)
{
var query = _db.Materials.AsNoTracking().AsQueryable();
var life = string.IsNullOrWhiteSpace(lifecycle) ? MaterialLifecycles.Active : lifecycle.Trim();
var life = MaterialLifecycles.ParseOr(lifecycle);
query = query.Where(x => x.LifecycleStatus == life);
if (onlyUnbound == true || onlyBound == true)
{
@@ -58,7 +72,11 @@ public sealed class WmsService
public Task<List<ContainerLocation>> ContainerLocations(string? locationType = null, string? q = null)
{
var query = _db.ContainerLocations.AsNoTracking().OrderBy(x => x.ContainerId).AsQueryable();
if (!string.IsNullOrWhiteSpace(locationType)) query = query.Where(x => x.LocationType == locationType);
if (!string.IsNullOrWhiteSpace(locationType) && ContainerLocationTypes.IsDefined(locationType))
{
var lt = ContainerLocationTypes.ParseOr(locationType);
query = query.Where(x => x.LocationType == lt);
}
return FilterByKeyword(query, q).ToListAsync();
}
@@ -69,61 +87,71 @@ public sealed class WmsService
return FilterByKeyword(query.OrderBy(x => x.ContainerId), q).ToListAsync();
}
/// <summary>
/// 库存物料列表:筛选/排序下推到 SQL。
/// 库位:优先 ContainerLocation.StorageId;未回填时用 LocationId 与库位 Id 的存储字符串(Guid "D")匹配。
/// </summary>
public async Task<List<InventoryMaterialRow>> InventoryMaterials(Guid? areaId = null, Guid? storageId = null, string? q = null)
{
var binds = await _db.ContainerMaterials.AsNoTracking().ToListAsync();
var materials = await _db.Materials.AsNoTracking().ToDictionaryAsync(x => x.Id);
var locations = await _db.ContainerLocations.AsNoTracking().ToDictionaryAsync(x => x.ContainerId);
var storages = await _db.Storages.AsNoTracking().ToDictionaryAsync(x => x.Id);
var areas = await _db.WarehouseAreas.AsNoTracking().ToDictionaryAsync(x => x.Id);
var containers = await _db.Containers.AsNoTracking().ToDictionaryAsync(x => x.Id);
var query =
from b in _db.ContainerMaterials.AsNoTracking()
join mat in _db.Materials.AsNoTracking() on b.MaterialId equals mat.Id
from ctn in _db.Containers.AsNoTracking().Where(c => c.Id == b.ContainerId).DefaultIfEmpty()
from loc in _db.ContainerLocations.AsNoTracking().Where(l => l.ContainerId == b.ContainerId).DefaultIfEmpty()
from st in _db.Storages.AsNoTracking().Where(s =>
loc != null &&
loc.LocationType == ContainerLocationType.Storage &&
(loc.StorageId == s.Id ||
(loc.StorageId == null && loc.LocationId == EF.Property<string>(s, nameof(Storage.Id))))).DefaultIfEmpty()
from area in _db.WarehouseAreas.AsNoTracking().Where(a => st != null && a.Id == st.AreaId).DefaultIfEmpty()
select new { b, mat, ctn, loc, st, area };
var rows = new List<InventoryMaterialRow>();
foreach (var b in binds)
if (storageId.HasValue)
query = query.Where(x => x.st != null && x.st.Id == storageId.Value);
if (areaId.HasValue)
query = query.Where(x => x.area != null && x.area.Id == areaId.Value);
if (!string.IsNullOrWhiteSpace(q))
{
if (!materials.TryGetValue(b.MaterialId, out var mat)) continue;
locations.TryGetValue(b.ContainerId, out var loc);
Storage? storage = null;
WarehouseArea? area = null;
if (loc != null && loc.LocationType == ContainerLocationTypes.Storage && Guid.TryParse(loc.LocationId, out var sid))
storages.TryGetValue(sid, out storage);
if (storage != null) areas.TryGetValue(storage.AreaId, out area);
containers.TryGetValue(b.ContainerId, out var ctn);
if (storageId.HasValue && storage?.Id != storageId.Value) continue;
if (areaId.HasValue && area?.Id != areaId.Value) continue;
if (!string.IsNullOrWhiteSpace(q))
{
var s = q.Trim();
if (!(mat.Code.Contains(s) || mat.Name.Contains(s) || (ctn?.Code.Contains(s) ?? false) || (storage?.Code.Contains(s) ?? false)))
continue;
}
rows.Add(new InventoryMaterialRow(
mat.Id, mat.Code, mat.Name, mat.Barcode, mat.TypeCode,
b.ContainerId, ctn?.Code ?? "", ctn?.Name ?? "",
storage?.Id, storage?.Code ?? "", storage?.Name ?? "",
area?.Id, area?.Code ?? "", area?.Name ?? "",
loc?.LocationType ?? "", b.BoundAt));
var s = q.Trim();
query = query.Where(x =>
x.mat.Code.Contains(s) || x.mat.Name.Contains(s) ||
(x.ctn != null && (x.ctn.Code.Contains(s) || x.ctn.Name.Contains(s))) ||
(x.st != null && x.st.Code.Contains(s)));
}
return rows.OrderBy(x => x.MaterialCode).ToList();
return await query
.OrderBy(x => x.mat.Code)
.Select(x => new InventoryMaterialRow(
x.mat.Id, x.mat.Code, x.mat.Name, x.mat.Barcode, x.mat.TypeCode,
x.b.ContainerId, x.ctn != null ? x.ctn.Code : "", x.ctn != null ? x.ctn.Name : "",
x.st != null ? x.st.Id : null, x.st != null ? x.st.Code : "", x.st != null ? x.st.Name : "",
x.area != null ? x.area.Id : null, x.area != null ? x.area.Code : "", x.area != null ? x.area.Name : "",
x.loc != null ? x.loc.LocationType.ToString() : "", x.b.BoundAt))
.ToListAsync();
}
/// <summary>
/// 库存事件:条件与 OrderBy/Take(500) 均下推;非法 eventType 直接返回空,避免全表拉取后再过滤。
/// </summary>
public async Task<List<StockEvent>> StockEvents(string? eventType = null, Guid? materialId = null, Guid? containerId = null)
{
var query = _db.StockEvents.AsNoTracking().AsQueryable();
if (!string.IsNullOrWhiteSpace(eventType)) query = query.Where(x => x.EventType == eventType);
if (!string.IsNullOrWhiteSpace(eventType))
{
if (Enum.TryParse<StockEventType>(eventType.Trim(), true, out var et))
query = query.Where(x => x.EventType == et);
else
return [];
}
if (materialId.HasValue) query = query.Where(x => x.MaterialId == materialId.Value);
if (containerId.HasValue) query = query.Where(x => x.ContainerId == containerId.Value);
var rows = await query.ToListAsync();
return rows.OrderByDescending(x => x.OperatedAt).Take(500).ToList();
return await query.OrderByDescending(x => x.OperatedAt).Take(500).ToListAsync();
}
public async Task<Warehouse> SaveWarehouse(MasterDataRequest req, string actor)
{
Warehouse entity;
if (req.Id.HasValue) entity = await FindEditable(_db.Warehouses, req.Id.Value, req.Version);
if (req.Id.HasValue) entity = await FindEditable<Warehouse>(req.Id.Value, req.Version);
else
{
entity = new Warehouse();
@@ -137,7 +165,7 @@ public sealed class WmsService
entity.Enabled = req.Enabled;
entity.SortOrder = req.SortOrder;
ApplyCommon(entity, req, actor);
await _db.SaveChangesAsync();
await _uow.SaveChangesAsync();
return entity;
}
@@ -150,7 +178,7 @@ public sealed class WmsService
await _refs.EnsureWarehouseAsync(warehouseId);
WarehouseArea entity;
if (req.Id.HasValue) entity = await FindEditable(_db.WarehouseAreas, req.Id.Value, req.Version);
if (req.Id.HasValue) entity = await FindEditable<WarehouseArea>(req.Id.Value, req.Version);
else
{
entity = new WarehouseArea();
@@ -162,24 +190,24 @@ public sealed class WmsService
entity.Code = req.Code.Trim();
entity.Name = req.Name.Trim();
entity.Type = req.Type.TrimOr("Storage");
entity.LayoutMode = AreaLayoutModes.All.Contains(req.LayoutMode) ? req.LayoutMode : AreaLayoutModes.Flat;
entity.LayoutMode = AreaLayoutModes.ParseOr(req.LayoutMode);
entity.State = req.State.TrimOr("Default");
entity.Enabled = req.Enabled;
entity.SortOrder = req.SortOrder;
ApplyCommon(entity, req, actor);
await _db.SaveChangesAsync();
await _uow.SaveChangesAsync();
return entity;
}
public async Task<Storage> SaveStorage(StorageRequest req, string actor)
{
await _refs.EnsureAreaAsync(req.AreaId);
var kind = LocationKinds.All.Contains(req.LocationKind) ? req.LocationKind : LocationKinds.Station;
var kind = LocationKinds.ParseOr(req.LocationKind);
if (kind == LocationKinds.Grid)
await EnsureGridCoordUnique(req.AreaId, req.ColumnNo, req.LevelNo, req.DepthNo, req.Id);
Storage entity;
if (req.Id.HasValue) entity = await FindEditable(_db.Storages, req.Id.Value, req.Version);
if (req.Id.HasValue) entity = await FindEditable<Storage>(req.Id.Value, req.Version);
else
{
entity = new Storage();
@@ -199,7 +227,7 @@ public sealed class WmsService
entity.SiteCode = req.SiteCode.TrimOr(entity.SiteId);
entity.Barcode = req.Barcode.TrimOr("");
entity.Capacity = 1;
var status = StorageStatuses.Normalize(req.Status.TrimOr(StorageStatuses.Empty));
var status = StorageStatuses.Normalize(req.Status);
if (status == StorageStatuses.Disabled || req.Enabled == false)
entity.Status = req.Enabled ? status : StorageStatuses.Disabled;
else if (!string.IsNullOrWhiteSpace(req.Status) && StorageStatuses.All.Contains(status))
@@ -211,7 +239,7 @@ public sealed class WmsService
entity.AllowOutbound = req.AllowOutbound;
entity.Enabled = req.Enabled;
ApplyCommon(entity, req, actor);
await _db.SaveChangesAsync();
await _uow.SaveChangesAsync();
if (entity.Status != StorageStatuses.Disabled)
await SyncOccupancyStatus(storageId: entity.Id);
return entity;
@@ -262,28 +290,28 @@ public sealed class WmsService
_db.Storages.Add(entity);
created++;
}
await _db.SaveChangesAsync();
await _uow.SaveChangesAsync();
return created;
}
public async Task<Storage> SetStorageLock(Guid id, bool isLock, long? version, string actor)
{
var entity = await FindEditable(_db.Storages, id, version);
var entity = await FindEditable<Storage>(id, version);
entity.IsLock = isLock;
entity.UpdatedBy = actor;
await _db.SaveChangesAsync();
await _uow.SaveChangesAsync();
return entity;
}
public async Task<Storage> SetStorageEnabled(Guid id, bool enabled, long? version, string actor)
{
var entity = await FindEditable(_db.Storages, id, version);
var entity = await FindEditable<Storage>(id, version);
entity.Enabled = enabled;
entity.Status = enabled
? (entity.Status == StorageStatuses.Disabled ? StorageStatuses.Empty : entity.Status)
: StorageStatuses.Disabled;
entity.UpdatedBy = actor;
await _db.SaveChangesAsync();
await _uow.SaveChangesAsync();
if (enabled) await SyncOccupancyStatus(storageId: id);
return entity;
}
@@ -291,7 +319,7 @@ public sealed class WmsService
public async Task<Container> SaveContainer(ContainerRequest req, string actor)
{
Container entity;
if (req.Id.HasValue) entity = await FindEditable(_db.Containers, req.Id.Value, req.Version);
if (req.Id.HasValue) entity = await FindEditable<Container>(req.Id.Value, req.Version);
else
{
entity = new Container();
@@ -303,15 +331,14 @@ public sealed class WmsService
entity.Code = req.Code.Trim();
entity.Name = req.Name.Trim();
entity.ContainerType = req.Type.TrimOr("Box");
var status = req.Status.TrimOr(ContainerStatuses.EmptyMaterial);
entity.Status = ContainerStatuses.All.Contains(status) ? status : ContainerStatuses.EmptyMaterial;
entity.Status = ContainerStatuses.ParseOr(req.Status);
entity.Barcode = req.Barcode.TrimOr("");
entity.Length = req.Length;
entity.Width = req.Width;
entity.Height = req.Height;
entity.Enabled = req.Enabled;
ApplyCommon(entity, req, actor);
await _db.SaveChangesAsync();
await _uow.SaveChangesAsync();
await SyncOccupancyStatus(containerId: entity.Id);
return entity;
}
@@ -319,7 +346,7 @@ public sealed class WmsService
public async Task<MaterialType> SaveMaterialType(MaterialTypeRequest req, string actor)
{
MaterialType entity;
if (req.Id.HasValue) entity = await FindEditable(_db.MaterialTypes, req.Id.Value, req.Version);
if (req.Id.HasValue) entity = await FindEditable<MaterialType>(req.Id.Value, req.Version);
else
{
entity = new MaterialType();
@@ -335,7 +362,7 @@ public sealed class WmsService
entity.BarcodePrefix = req.BarcodePrefix.TrimOr("");
entity.Enabled = req.Enabled;
ApplyCommon(entity, req, actor);
await _db.SaveChangesAsync();
await _uow.SaveChangesAsync();
return entity;
}
@@ -344,7 +371,7 @@ public sealed class WmsService
if (!string.IsNullOrWhiteSpace(req.TypeCode))
await _refs.EnsureMaterialTypeCodeAsync(req.TypeCode);
Material entity;
if (req.Id.HasValue) entity = await FindEditable(_db.Materials, req.Id.Value, req.Version);
if (req.Id.HasValue) entity = await FindEditable<Material>(req.Id.Value, req.Version);
else
{
entity = new Material();
@@ -361,23 +388,22 @@ public sealed class WmsService
entity.Spec = req.Spec.TrimOr("");
entity.Unit = req.Unit.TrimOr("pcs");
entity.Category = req.Category.TrimOr("");
var life = req.LifecycleStatus.TrimOr(MaterialLifecycles.Active);
entity.LifecycleStatus = MaterialLifecycles.All.Contains(life) ? life : MaterialLifecycles.Active;
entity.LifecycleStatus = MaterialLifecycles.ParseOr(req.LifecycleStatus);
entity.Enabled = req.Enabled;
ApplyCommon(entity, req, actor);
await _db.SaveChangesAsync();
await _uow.SaveChangesAsync();
return entity;
}
public async Task<Material> ArchiveMaterial(Guid id, long? version, string actor)
{
var entity = await FindEditable(_db.Materials, id, version);
var entity = await FindEditable<Material>(id, version);
if (await _db.ContainerMaterials.AnyAsync(x => x.MaterialId == id))
throw new InvalidOperationException("物料仍在绑定中,不能归档");
entity.LifecycleStatus = MaterialLifecycles.Archived;
entity.UnboundAt ??= DateTimeOffset.UtcNow;
entity.UpdatedBy = actor;
await _db.SaveChangesAsync();
await _uow.SaveChangesAsync();
return entity;
}
@@ -396,13 +422,12 @@ public sealed class WmsService
entity.DeletedBy = actor;
entity.UpdatedBy = actor;
}
await _db.SaveChangesAsync();
await _uow.SaveChangesAsync();
}
public async Task DeleteEntity<T>(Guid id, long? version, string actor) where T : EntityBase
{
var set = _db.Set<T>();
var entity = await FindEditable(set, id, version);
var entity = await FindEditable<T>(id, version);
if (typeof(T) == typeof(WarehouseArea))
{
if (await _db.Storages.AnyAsync(x => x.AreaId == id))
@@ -423,19 +448,22 @@ public sealed class WmsService
entity.DeletedAt = DateTimeOffset.UtcNow;
entity.DeletedBy = actor;
entity.UpdatedBy = actor;
await _db.SaveChangesAsync();
await _uow.SaveChangesAsync();
}
public async Task<ContainerLocation> BindOrTransferLocation(ContainerLocationRequest req, string actor)
{
await _refs.EnsureContainerAsync(req.ContainerId);
var (code, name) = await _refs.ResolveLocationSnapshotAsync(req.LocationType, req.LocationId);
if (!ContainerLocationStatuses.All.Contains(req.Status))
if (!ContainerLocationStatuses.IsDefined(req.Status))
throw new InvalidOperationException("容器位置状态无效");
var locationType = ContainerLocationTypes.ParseOr(req.LocationType);
var locationStatus = ContainerLocationStatuses.ParseOr(req.Status);
Guid? fromStorageId = null;
Guid? toStorageId = null;
if (string.Equals(req.LocationType, ContainerLocationTypes.Storage, StringComparison.OrdinalIgnoreCase) &&
if (locationType == ContainerLocationTypes.Storage &&
Guid.TryParse(req.LocationId, out var targetStorageId))
{
toStorageId = targetStorageId;
@@ -455,7 +483,9 @@ public sealed class WmsService
var current = await _db.ContainerLocations.FirstOrDefaultAsync(x => x.ContainerId == req.ContainerId);
var before = current == null ? null : Snapshot(current);
if (before != null && before.LocationType == ContainerLocationTypes.Storage && Guid.TryParse(before.LocationId, out var fs))
if (before != null &&
ContainerLocationTypes.EqualsString(ContainerLocationTypes.Storage, before.LocationType) &&
Guid.TryParse(before.LocationId, out var fs))
fromStorageId = fs;
var now = DateTimeOffset.UtcNow;
@@ -471,11 +501,12 @@ public sealed class WmsService
EnsureUnlocked(current);
}
current.LocationType = req.LocationType;
current.LocationType = locationType;
current.LocationId = req.LocationId.Trim();
current.StorageId = toStorageId;
current.LocationCode = code;
current.LocationName = name;
current.Status = req.Status;
current.Status = locationStatus;
current.EnteredAt = req.EnteredAt ?? now;
ApplyCommon(current, req, actor);
@@ -486,7 +517,7 @@ public sealed class WmsService
EventType = before == null ? "Bind" : "Transfer",
FromLocationType = before?.LocationType ?? "",
FromLocationId = before?.LocationId ?? "",
ToLocationType = current.LocationType,
ToLocationType = current.LocationType.ToString(),
ToLocationId = current.LocationId,
BeforeJson = before == null ? "{}" : JsonSerializer.Serialize(before, _json),
AfterJson = JsonSerializer.Serialize(Snapshot(current), _json),
@@ -499,7 +530,7 @@ public sealed class WmsService
});
await AddContainerMoveEventAsync(req.ContainerId, fromStorageId, toStorageId, actor, req.Reason.TrimOr(""), now);
await _db.SaveChangesAsync();
await _uow.SaveChangesAsync();
await SyncOccupancyStatus(containerId: req.ContainerId, storageId: fromStorageId);
await SyncOccupancyStatus(storageId: toStorageId);
return current;
@@ -520,7 +551,7 @@ public sealed class WmsService
RelationId = current.Id,
ContainerId = current.ContainerId,
EventType = "Unbind",
FromLocationType = current.LocationType,
FromLocationType = current.LocationType.ToString(),
FromLocationId = current.LocationId,
BeforeJson = JsonSerializer.Serialize(before, _json),
AfterJson = "{}",
@@ -531,7 +562,7 @@ public sealed class WmsService
});
await AddContainerMoveEventAsync(containerId, fromStorageId, null, actor, reason, now);
_db.ContainerLocations.Remove(current);
await _db.SaveChangesAsync();
await _uow.SaveChangesAsync();
await SyncOccupancyStatus(containerId: containerId, storageId: fromStorageId);
}
@@ -578,7 +609,7 @@ public sealed class WmsService
material.UnboundAt = null;
await AddBindUnbindEventAsync(StockEventTypes.Bind, material, req.ContainerId, actor, req.Reason.TrimOr(""), now);
await _db.SaveChangesAsync();
await _uow.SaveChangesAsync();
await SyncOccupancyStatus(containerId: req.ContainerId);
return current;
}
@@ -620,7 +651,7 @@ public sealed class WmsService
}
_db.ContainerMaterials.Remove(current);
await _db.SaveChangesAsync();
await _uow.SaveChangesAsync();
await SyncOccupancyStatus(containerId: containerId);
}
@@ -666,7 +697,7 @@ public sealed class WmsService
}
}
await _db.SaveChangesAsync();
await _uow.SaveChangesAsync();
}
public async Task<Warehouse> EnsureDefaultWarehouseAsync(string actor = "system")
@@ -683,7 +714,7 @@ public sealed class WmsService
};
StampCreate(wh, actor);
_db.Warehouses.Add(wh);
await _db.SaveChangesAsync();
await _uow.SaveChangesAsync();
return wh;
}
@@ -692,44 +723,26 @@ public sealed class WmsService
var wh = await EnsureDefaultWarehouseAsync(actor);
var areas = await _db.WarehouseAreas.Where(x => x.WarehouseId == Guid.Empty).ToListAsync();
foreach (var a in areas)
{
a.WarehouseId = wh.Id;
if (string.IsNullOrWhiteSpace(a.LayoutMode)) a.LayoutMode = AreaLayoutModes.Flat;
}
var storages = await _db.Storages.ToListAsync();
foreach (var s in storages)
{
if (string.IsNullOrWhiteSpace(s.LocationKind))
s.LocationKind = LocationKinds.Station;
if (s.LevelNo <= 0) s.LevelNo = 1;
if (s.DepthNo <= 0) s.DepthNo = 1;
if (string.IsNullOrWhiteSpace(s.SiteCode)) s.SiteCode = s.SiteId;
s.Status = StorageStatuses.Normalize(s.Status) switch
{
StorageStatuses.Available or StorageStatuses.Idle => StorageStatuses.Empty,
StorageStatuses.Occupied => StorageStatuses.FullContainer,
var x => x
};
if (!StorageStatuses.All.Contains(s.Status))
s.Status = StorageStatuses.Empty;
if (!s.Enabled) s.Status = StorageStatuses.Disabled;
}
var containers = await _db.Containers.ToListAsync();
foreach (var c in containers)
{
c.Status = c.Status switch
{
"Idle" or "Empty" => ContainerStatuses.EmptyMaterial,
"Loaded" => ContainerStatuses.FullMaterial,
_ when ContainerStatuses.All.Contains(c.Status) => c.Status,
_ => ContainerStatuses.EmptyMaterial
};
if (!ContainerStatuses.All.Contains(c.Status))
c.Status = ContainerStatuses.EmptyMaterial;
}
var materials = await _db.Materials.Where(x => string.IsNullOrWhiteSpace(x.LifecycleStatus)).ToListAsync();
foreach (var m in materials)
m.LifecycleStatus = MaterialLifecycles.Active;
var binds = await _db.ContainerMaterials.ToListAsync();
foreach (var b in binds)
{
@@ -739,7 +752,7 @@ public sealed class WmsService
b.Status = ContainerMaterialStatuses.Bound;
}
await _db.SaveChangesAsync();
await _uow.SaveChangesAsync();
foreach (var s in storages.Where(x => x.Status != StorageStatuses.Disabled))
await SyncOccupancyStatus(storageId: s.Id);
@@ -773,13 +786,14 @@ public sealed class WmsService
if (exists) throw new InvalidOperationException("同库区网格坐标已存在");
}
private async Task AddBindUnbindEventAsync(string eventType, Material material, Guid containerId, string actor, string reason, DateTimeOffset now)
private async Task AddBindUnbindEventAsync(StockEventType eventType, Material material, Guid containerId, string actor, string reason, DateTimeOffset now)
{
var ctn = await _db.Containers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == containerId);
var loc = await _db.ContainerLocations.AsNoTracking().FirstOrDefaultAsync(x => x.ContainerId == containerId);
Storage? storage = null;
WarehouseArea? area = null;
if (loc is { LocationType: ContainerLocationTypes.Storage } && Guid.TryParse(loc.LocationId, out var sid))
if (loc is { LocationType: ContainerLocationType.Storage } &&
(loc.StorageId is { } sid || Guid.TryParse(loc.LocationId, out sid)))
{
storage = await _db.Storages.AsNoTracking().FirstOrDefaultAsync(x => x.Id == sid);
if (storage != null)
@@ -843,23 +857,21 @@ public sealed class WmsService
});
}
private async Task<T> FindEditable<T>(DbSet<T> set, Guid id, long? version) where T : EntityBase
{
var entity = await set.FirstOrDefaultAsync(x => x.Id == id) ?? throw new InvalidOperationException("数据不存在");
EnsureVersion(entity, version);
EnsureUnlocked(entity);
return entity;
}
private Task<T> FindEditable<T>(Guid id, long? version)
where T : class, IEntity<Guid>, ISoftDeletable, IVersioned, ILockable
=> _services.GetRequiredService<IEditableRepository<T>>().GetEditableAsync(id, version);
private static void EnsureVersion(EntityBase entity, long? version)
{
if (version.HasValue && entity.Version != version.Value)
throw new InvalidOperationException("数据已被其他用户修改,请刷新后重试");
throw new MiGu.DB.Abstractions.Exceptions.ConcurrencyConflictException(
entity.GetType().Name, entity.Id, version);
}
private static void EnsureUnlocked(EntityBase entity)
{
if (entity.IsLock) throw new InvalidOperationException("数据已锁定,不能修改");
if (entity.IsLock)
throw new MiGu.DB.Abstractions.Exceptions.EntityLockedException(entity.GetType().Name, entity.Id);
}
private static void StampCreate(EntityBase entity, string actor)
@@ -909,73 +921,14 @@ public sealed class WmsService
}
private static ContainerLocationSnapshot Snapshot(ContainerLocation x) => new(
x.Id, x.ContainerId, x.LocationType, x.LocationId, x.LocationCode, x.LocationName, x.Status, x.EnteredAt, x.Version);
x.Id, x.ContainerId, x.LocationType.ToString(), x.LocationId, x.LocationCode, x.LocationName,
x.Status.ToString(), x.EnteredAt, x.Version);
private static ContainerMaterialSnapshot Snapshot(ContainerMaterial x) => new(
x.Id, x.ContainerId, x.MaterialId, x.Quantity, x.BatchNo, x.SerialNo, x.Status, x.BoundAt, x.LoadedAt, x.UnloadedAt, x.Version);
x.Id, x.ContainerId, x.MaterialId, x.Quantity, x.BatchNo, x.SerialNo, x.Status.ToString(),
x.BoundAt, x.LoadedAt, x.UnloadedAt, x.Version);
}
public sealed record InventoryMaterialRow(
Guid MaterialId, string MaterialCode, string MaterialName, string MaterialBarcode, string TypeCode,
Guid ContainerId, string ContainerCode, string ContainerName,
Guid? StorageId, string StorageCode, string StorageName,
Guid? AreaId, string AreaCode, string AreaName,
string LocationType, DateTimeOffset BoundAt);
public sealed record ContainerLocationSnapshot(
Guid Id, Guid ContainerId, string LocationType, string LocationId, string LocationCode, string LocationName,
string Status, DateTimeOffset EnteredAt, long Version);
public sealed record ContainerMaterialSnapshot(
Guid Id, Guid ContainerId, Guid MaterialId, decimal Quantity, string BatchNo, string SerialNo, string Status,
DateTimeOffset BoundAt, DateTimeOffset LoadedAt, DateTimeOffset? UnloadedAt, long Version);
public abstract record CommonRequest(Guid? Id, long? Version, bool IsLock, string Remark, string Extend);
public sealed record MasterDataRequest(
Guid? Id, long? Version, string Code, string Name, string Type, string Status, bool Enabled, int SortOrder,
bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
public sealed record AreaRequest(
Guid? Id, long? Version, Guid? WarehouseId, string Code, string Name, string Type, string LayoutMode, string State,
bool Enabled, int SortOrder, bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
public sealed record StorageRequest(
Guid? Id, long? Version, Guid AreaId, string Code, string Name, string StorageType, string LocationKind,
int ColumnNo, int LevelNo, int DepthNo, string SiteId, string SiteCode, string Barcode, int Capacity,
string Status, string Usage, int Priority, string ZoneCode, bool AllowInbound, bool AllowOutbound, bool Enabled,
bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
public sealed record GenerateBinsRequest(int ColumnFrom, int ColumnTo, int LevelFrom, int LevelTo, int DepthFrom, int DepthTo, string? CodePattern);
public sealed record ContainerRequest(
Guid? Id, long? Version, Guid? AreaId, string Code, string Name, string Type, string Status, string Barcode,
double Length, double Width, double Height, bool Enabled, bool IsLock, string Remark, string Extend)
: CommonRequest(Id, Version, IsLock, Remark, Extend);
public sealed record MaterialTypeRequest(
Guid? Id, long? Version, string Code, string Name, string Spec, string Unit, string Category, string BarcodePrefix,
bool Enabled, bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
public sealed record MaterialRequest(
Guid? Id, long? Version, string Code, string Name, string TypeCode, string Barcode, string Spec, string Unit,
string Category, string LifecycleStatus, bool Enabled, bool IsLock, string Remark, string Extend)
: CommonRequest(Id, Version, IsLock, Remark, Extend);
public sealed record ContainerLocationRequest(
Guid? Id, long? Version, Guid ContainerId, string LocationType, string LocationId, string Status,
DateTimeOffset? EnteredAt, string Source, string Reason, bool IsLock, string Remark, string Extend)
: CommonRequest(Id, Version, IsLock, Remark, Extend);
public sealed record BindMaterialRequest(
Guid? Id, long? Version, Guid ContainerId, Guid MaterialId, string Source, string Reason,
bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
public sealed record ContainerMaterialRequest(
Guid? Id, long? Version, Guid ContainerId, Guid MaterialId, decimal Quantity, string BatchNo, string SerialNo,
string Status, DateTimeOffset? LoadedAt, DateTimeOffset? UnloadedAt, string Source, string Reason,
bool IsLock, string Remark, string Extend) : CommonRequest(Id, Version, IsLock, Remark, Extend);
public static class WmsStringExtensions
{
public static string TrimOr(this string? value, string fallback) =>