完善 WMS 搬运任务与库位状态相关接口。
补齐运输规划与任务服务在状态/优先级字段上的读写与调度衔接。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -499,7 +499,14 @@ public sealed class WmsService
|
|||||||
});
|
});
|
||||||
|
|
||||||
await AddContainerMoveEventAsync(req.ContainerId, fromStorageId, toStorageId, actor, req.Reason.TrimOr(""), now);
|
await AddContainerMoveEventAsync(req.ContainerId, fromStorageId, toStorageId, actor, req.Reason.TrimOr(""), now);
|
||||||
|
try
|
||||||
|
{
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
catch (DbUpdateException ex) when (IsUniqueConstraintViolation(ex))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("目标库位已被其他容器占用,库位与容器为 1 对 1");
|
||||||
|
}
|
||||||
await SyncOccupancyStatus(containerId: req.ContainerId, storageId: fromStorageId);
|
await SyncOccupancyStatus(containerId: req.ContainerId, storageId: fromStorageId);
|
||||||
await SyncOccupancyStatus(storageId: toStorageId);
|
await SyncOccupancyStatus(storageId: toStorageId);
|
||||||
return current;
|
return current;
|
||||||
@@ -857,6 +864,20 @@ public sealed class WmsService
|
|||||||
throw new InvalidOperationException("数据已被其他用户修改,请刷新后重试");
|
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)
|
private static void EnsureUnlocked(EntityBase entity)
|
||||||
{
|
{
|
||||||
if (entity.IsLock) throw new InvalidOperationException("数据已锁定,不能修改");
|
if (entity.IsLock) throw new InvalidOperationException("数据已锁定,不能修改");
|
||||||
|
|||||||
@@ -69,8 +69,11 @@ public sealed class WmsTransportPlanner
|
|||||||
.Where(x => x.LocationType == ContainerLocationTypes.Storage).ToListAsync();
|
.Where(x => x.LocationType == ContainerLocationTypes.Storage).ToListAsync();
|
||||||
var materials = await _db.Materials.AsNoTracking().Where(x => x.Enabled).ToListAsync();
|
var materials = await _db.Materials.AsNoTracking().Where(x => x.Enabled).ToListAsync();
|
||||||
var containerMaterials = await _db.ContainerMaterials.AsNoTracking().ToListAsync();
|
var containerMaterials = await _db.ContainerMaterials.AsNoTracking().ToListAsync();
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
var activeReservations = await _db.WmsTransportReservations.AsNoTracking()
|
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()
|
var activeTasks = await _db.WmsTransportTasks.AsNoTracking()
|
||||||
.Where(x => WmsTransportTaskStatuses.Active.Contains(x.Status)).ToListAsync();
|
.Where(x => WmsTransportTaskStatuses.Active.Contains(x.Status)).ToListAsync();
|
||||||
|
|
||||||
|
|||||||
@@ -165,10 +165,27 @@ public sealed class WmsTransportTaskService
|
|||||||
|
|
||||||
public async Task<WmsTransportTask> DispatchAsync(Guid taskId, string actor)
|
public async Task<WmsTransportTask> DispatchAsync(Guid taskId, string actor)
|
||||||
{
|
{
|
||||||
var task = await FindTaskAsync(taskId);
|
// 原子抢占:仅一条 Reserved 且未在下发中的任务能进入 Dispatching,避免双发
|
||||||
if (task.Status != WmsTransportTaskStatuses.Reserved)
|
var now = DateTimeOffset.UtcNow;
|
||||||
throw new InvalidOperationException("只有 Reserved 状态的任务可以下发");
|
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 source = await _db.Storages.AsNoTracking().FirstAsync(x => x.Id == task.SourceStorageId);
|
||||||
var target = await _db.Storages.AsNoTracking().FirstAsync(x => x.Id == task.TargetStorageId);
|
var target = await _db.Storages.AsNoTracking().FirstAsync(x => x.Id == task.TargetStorageId);
|
||||||
var container = await _db.Containers.AsNoTracking().FirstAsync(x => x.Id == task.ContainerId);
|
var container = await _db.Containers.AsNoTracking().FirstAsync(x => x.Id == task.ContainerId);
|
||||||
@@ -194,14 +211,36 @@ public sealed class WmsTransportTaskService
|
|||||||
return task;
|
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)
|
private async Task EnsureNoActiveReservationAsync(Guid containerId, Guid targetStorageId)
|
||||||
{
|
{
|
||||||
|
await ExpireStaleReservationsAsync();
|
||||||
|
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
if (await _db.WmsTransportReservations.AnyAsync(x =>
|
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("容器已被其他任务预占");
|
throw new InvalidOperationException("容器已被其他任务预占");
|
||||||
|
|
||||||
if (await _db.WmsTransportReservations.AnyAsync(x =>
|
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("目标库位已被其他任务预占");
|
throw new InvalidOperationException("目标库位已被其他任务预占");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user