持久层重构为独立 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:
+133
-180
@@ -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) =>
|
||||
|
||||
Reference in New Issue
Block a user