diff --git a/MiGu.Server/Wms/WmsService.cs b/MiGu.Server/Wms/WmsService.cs index 065eb4d..9160a54 100644 --- a/MiGu.Server/Wms/WmsService.cs +++ b/MiGu.Server/Wms/WmsService.cs @@ -499,7 +499,14 @@ public sealed class WmsService }); await AddContainerMoveEventAsync(req.ContainerId, fromStorageId, toStorageId, actor, req.Reason.TrimOr(""), now); - await _db.SaveChangesAsync(); + try + { + await _db.SaveChangesAsync(); + } + catch (DbUpdateException ex) when (IsUniqueConstraintViolation(ex)) + { + throw new InvalidOperationException("目标库位已被其他容器占用,库位与容器为 1 对 1"); + } await SyncOccupancyStatus(containerId: req.ContainerId, storageId: fromStorageId); await SyncOccupancyStatus(storageId: toStorageId); return current; @@ -857,6 +864,20 @@ public sealed class WmsService throw new InvalidOperationException("数据已被其他用户修改,请刷新后重试"); } + private static bool IsUniqueConstraintViolation(DbUpdateException ex) + { + for (Exception? e = ex; e != null; e = e.InnerException) + { + var msg = e.Message; + if (msg.Contains("UNIQUE constraint failed", StringComparison.OrdinalIgnoreCase) + || msg.Contains("unique index", StringComparison.OrdinalIgnoreCase) + || msg.Contains("duplicate key", StringComparison.OrdinalIgnoreCase)) + return true; + } + + return false; + } + private static void EnsureUnlocked(EntityBase entity) { if (entity.IsLock) throw new InvalidOperationException("数据已锁定,不能修改"); diff --git a/MiGu.Server/Wms/WmsTransportPlanner.cs b/MiGu.Server/Wms/WmsTransportPlanner.cs index 1dee59b..e01e3bd 100644 --- a/MiGu.Server/Wms/WmsTransportPlanner.cs +++ b/MiGu.Server/Wms/WmsTransportPlanner.cs @@ -69,8 +69,11 @@ public sealed class WmsTransportPlanner .Where(x => x.LocationType == ContainerLocationTypes.Storage).ToListAsync(); var materials = await _db.Materials.AsNoTracking().Where(x => x.Enabled).ToListAsync(); var containerMaterials = await _db.ContainerMaterials.AsNoTracking().ToListAsync(); + var now = DateTimeOffset.UtcNow; var activeReservations = await _db.WmsTransportReservations.AsNoTracking() - .Where(x => x.Status == WmsReservationStatuses.Active).ToListAsync(); + .Where(x => x.Status == WmsReservationStatuses.Active + && (x.ExpiresAt == null || x.ExpiresAt > now)) + .ToListAsync(); var activeTasks = await _db.WmsTransportTasks.AsNoTracking() .Where(x => WmsTransportTaskStatuses.Active.Contains(x.Status)).ToListAsync(); diff --git a/MiGu.Server/Wms/WmsTransportTaskService.cs b/MiGu.Server/Wms/WmsTransportTaskService.cs index d4ff880..64bfb34 100644 --- a/MiGu.Server/Wms/WmsTransportTaskService.cs +++ b/MiGu.Server/Wms/WmsTransportTaskService.cs @@ -165,10 +165,27 @@ public sealed class WmsTransportTaskService public async Task DispatchAsync(Guid taskId, string actor) { - var task = await FindTaskAsync(taskId); - if (task.Status != WmsTransportTaskStatuses.Reserved) - throw new InvalidOperationException("只有 Reserved 状态的任务可以下发"); + // 原子抢占:仅一条 Reserved 且未在下发中的任务能进入 Dispatching,避免双发 + var now = DateTimeOffset.UtcNow; + var claimed = await _db.WmsTransportTasks + .Where(x => x.Id == taskId + && x.Status == WmsTransportTaskStatuses.Reserved + && x.DispatchStatus != "Dispatching" + && x.DispatchStatus != "Dispatched") + .ExecuteUpdateAsync(s => s + .SetProperty(x => x.DispatchStatus, "Dispatching") + .SetProperty(x => x.UpdatedAt, now) + .SetProperty(x => x.UpdatedBy, actor)); + if (claimed == 0) + { + var existing = await FindTaskAsync(taskId); + if (existing.Status != WmsTransportTaskStatuses.Reserved) + throw new InvalidOperationException("只有 Reserved 状态的任务可以下发"); + throw new InvalidOperationException("任务正在下发中,请勿重复操作"); + } + + var task = await FindTaskAsync(taskId); var source = await _db.Storages.AsNoTracking().FirstAsync(x => x.Id == task.SourceStorageId); var target = await _db.Storages.AsNoTracking().FirstAsync(x => x.Id == task.TargetStorageId); var container = await _db.Containers.AsNoTracking().FirstAsync(x => x.Id == task.ContainerId); @@ -194,14 +211,36 @@ public sealed class WmsTransportTaskService return task; } + private async Task ExpireStaleReservationsAsync() + { + var now = DateTimeOffset.UtcNow; + var rows = await _db.WmsTransportReservations + .Where(x => x.Status == WmsReservationStatuses.Active + && x.ExpiresAt != null + && x.ExpiresAt < now) + .ToListAsync(); + if (rows.Count == 0) return; + + foreach (var row in rows) + row.Status = WmsReservationStatuses.Expired; + await _db.SaveChangesAsync(); + } + private async Task EnsureNoActiveReservationAsync(Guid containerId, Guid targetStorageId) { + await ExpireStaleReservationsAsync(); + + var now = DateTimeOffset.UtcNow; if (await _db.WmsTransportReservations.AnyAsync(x => - x.Status == WmsReservationStatuses.Active && x.ContainerId == containerId)) + x.Status == WmsReservationStatuses.Active + && (x.ExpiresAt == null || x.ExpiresAt > now) + && x.ContainerId == containerId)) throw new InvalidOperationException("容器已被其他任务预占"); if (await _db.WmsTransportReservations.AnyAsync(x => - x.Status == WmsReservationStatuses.Active && x.TargetStorageId == targetStorageId)) + x.Status == WmsReservationStatuses.Active + && (x.ExpiresAt == null || x.ExpiresAt > now) + && x.TargetStorageId == targetStorageId)) throw new InvalidOperationException("目标库位已被其他任务预占"); }