feat(ft546): add WCS operations console
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
package com.ruoyi.wcs.query;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class OperationsPage<T> {
|
||||
private final List<T> rows;
|
||||
private final long total;
|
||||
private final int offset;
|
||||
private final int limit;
|
||||
|
||||
public OperationsPage(List<T> rows, long total, int offset, int limit) {
|
||||
Objects.requireNonNull(rows, "rows");
|
||||
if (total < 0L || offset < 0 || limit <= 0) {
|
||||
throw new IllegalArgumentException("invalid operations page");
|
||||
}
|
||||
this.rows = Collections.unmodifiableList(new ArrayList<T>(rows));
|
||||
this.total = total;
|
||||
this.offset = offset;
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
public List<T> getRows() { return rows; }
|
||||
public long getTotal() { return total; }
|
||||
public int getOffset() { return offset; }
|
||||
public int getLimit() { return limit; }
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import java.util.Objects;
|
||||
|
||||
public final class OrderExecutionView {
|
||||
private final String orderId;
|
||||
private final String externalRequestId;
|
||||
private final String orderState;
|
||||
private final String workflowId;
|
||||
private final String stepId;
|
||||
private final String deviceId;
|
||||
@@ -17,7 +19,18 @@ public final class OrderExecutionView {
|
||||
String deviceId, String currentCommandKey,
|
||||
String commandState, String recoveryCaseId,
|
||||
Instant updatedAt) {
|
||||
this(orderId, null, null, workflowId, stepId, deviceId,
|
||||
currentCommandKey, commandState, recoveryCaseId, updatedAt);
|
||||
}
|
||||
|
||||
public OrderExecutionView(String orderId, String externalRequestId,
|
||||
String orderState, String workflowId,
|
||||
String stepId, String deviceId,
|
||||
String currentCommandKey, String commandState,
|
||||
String recoveryCaseId, Instant updatedAt) {
|
||||
this.orderId = requireText(orderId, "orderId");
|
||||
this.externalRequestId = optional(externalRequestId);
|
||||
this.orderState = optional(orderState);
|
||||
this.workflowId = optional(workflowId);
|
||||
this.stepId = optional(stepId);
|
||||
this.deviceId = optional(deviceId);
|
||||
@@ -28,6 +41,8 @@ public final class OrderExecutionView {
|
||||
}
|
||||
|
||||
public String getOrderId() { return orderId; }
|
||||
public String getExternalRequestId() { return externalRequestId; }
|
||||
public String getOrderState() { return orderState; }
|
||||
public String getWorkflowId() { return workflowId; }
|
||||
public String getStepId() { return stepId; }
|
||||
public String getDeviceId() { return deviceId; }
|
||||
|
||||
@@ -5,6 +5,10 @@ import java.util.Objects;
|
||||
|
||||
public final class ResourceOperationsView {
|
||||
private final String resourceKey;
|
||||
private final String resourceType;
|
||||
private final Integer capacity;
|
||||
private final Integer protectedCount;
|
||||
private final String holder;
|
||||
private final String reservationState;
|
||||
private final String orderId;
|
||||
private final String stepId;
|
||||
@@ -17,14 +21,44 @@ public final class ResourceOperationsView {
|
||||
String orderId, String stepId,
|
||||
String vehicleId, long fencingToken,
|
||||
String releaseEvidenceId, Instant updatedAt) {
|
||||
this(resourceKey, null, null, null, vehicleId, reservationState,
|
||||
orderId, stepId, vehicleId, fencingToken, releaseEvidenceId,
|
||||
updatedAt);
|
||||
}
|
||||
|
||||
public ResourceOperationsView(String resourceKey, String resourceType,
|
||||
int capacity, int protectedCount,
|
||||
String holder, String reservationState,
|
||||
String orderId, String stepId,
|
||||
String vehicleId, long fencingToken,
|
||||
String releaseEvidenceId, Instant updatedAt) {
|
||||
this(resourceKey, requireText(resourceType, "resourceType"),
|
||||
Integer.valueOf(capacity), Integer.valueOf(protectedCount),
|
||||
holder, reservationState, orderId, stepId, vehicleId,
|
||||
fencingToken, releaseEvidenceId, updatedAt);
|
||||
}
|
||||
|
||||
private ResourceOperationsView(String resourceKey, String resourceType,
|
||||
Integer capacity, Integer protectedCount,
|
||||
String holder, String reservationState,
|
||||
String orderId, String stepId,
|
||||
String vehicleId, long fencingToken,
|
||||
String releaseEvidenceId,
|
||||
Instant updatedAt) {
|
||||
this.resourceKey = requireText(resourceKey, "resourceKey");
|
||||
this.resourceType = optional(resourceType);
|
||||
validateCapacity(capacity, protectedCount);
|
||||
this.capacity = capacity;
|
||||
this.protectedCount = protectedCount;
|
||||
this.holder = optional(holder);
|
||||
this.reservationState = requireText(reservationState,
|
||||
"reservationState");
|
||||
this.orderId = requireText(orderId, "orderId");
|
||||
this.stepId = requireText(stepId, "stepId");
|
||||
this.vehicleId = requireText(vehicleId, "vehicleId");
|
||||
if (fencingToken <= 0L) {
|
||||
throw new IllegalArgumentException("fencingToken must be positive");
|
||||
this.orderId = optional(orderId);
|
||||
this.stepId = optional(stepId);
|
||||
this.vehicleId = optional(vehicleId);
|
||||
if (fencingToken < 0L) {
|
||||
throw new IllegalArgumentException(
|
||||
"fencingToken must not be negative");
|
||||
}
|
||||
this.fencingToken = fencingToken;
|
||||
this.releaseEvidenceId = optional(releaseEvidenceId);
|
||||
@@ -32,6 +66,10 @@ public final class ResourceOperationsView {
|
||||
}
|
||||
|
||||
public String getResourceKey() { return resourceKey; }
|
||||
public String getResourceType() { return resourceType; }
|
||||
public Integer getCapacity() { return capacity; }
|
||||
public Integer getProtectedCount() { return protectedCount; }
|
||||
public String getHolder() { return holder; }
|
||||
public String getReservationState() { return reservationState; }
|
||||
public String getOrderId() { return orderId; }
|
||||
public String getStepId() { return stepId; }
|
||||
@@ -40,6 +78,21 @@ public final class ResourceOperationsView {
|
||||
public String getReleaseEvidenceId() { return releaseEvidenceId; }
|
||||
public Instant getUpdatedAt() { return updatedAt; }
|
||||
|
||||
private static void validateCapacity(Integer capacity,
|
||||
Integer protectedCount) {
|
||||
if (capacity == null && protectedCount == null) {
|
||||
return;
|
||||
}
|
||||
if (capacity == null || protectedCount == null || capacity <= 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"capacity and protectedCount must be specified together");
|
||||
}
|
||||
if (protectedCount < 0 || protectedCount > capacity) {
|
||||
throw new IllegalArgumentException(
|
||||
"protectedCount must be between zero and capacity");
|
||||
}
|
||||
}
|
||||
|
||||
private static String optional(String value) {
|
||||
return value == null || value.trim().isEmpty() ? null : value.trim();
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package com.ruoyi.wcs.query;
|
||||
|
||||
import com.ruoyi.wcs.domain.shared.DomainRuleViolation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -21,6 +19,15 @@ public final class WcsOperationsQueryService {
|
||||
return requireResult(repository.findOrder(id), "order", id);
|
||||
}
|
||||
|
||||
public OperationsPage<OrderExecutionView> orders(String state, int offset,
|
||||
int limit) {
|
||||
validatePage(offset, limit);
|
||||
String filter = optional(state);
|
||||
return new OperationsPage<OrderExecutionView>(
|
||||
repository.findOrders(filter, offset, limit),
|
||||
repository.countOrders(filter), offset, limit);
|
||||
}
|
||||
|
||||
public OrderExecutionView command(String commandKey) {
|
||||
String key = requireText(commandKey, "commandKey");
|
||||
return requireResult(repository.findCommand(key), "command", key);
|
||||
@@ -31,11 +38,31 @@ public final class WcsOperationsQueryService {
|
||||
return requireResult(repository.findDevice(id), "device", id);
|
||||
}
|
||||
|
||||
public OperationsPage<DeviceOperationsView> devices(String state,
|
||||
int offset,
|
||||
int limit) {
|
||||
validatePage(offset, limit);
|
||||
String filter = optional(state);
|
||||
return new OperationsPage<DeviceOperationsView>(
|
||||
repository.findDevices(filter, offset, limit),
|
||||
repository.countDevices(filter), offset, limit);
|
||||
}
|
||||
|
||||
public ResourceOperationsView resource(String resourceKey) {
|
||||
String key = requireText(resourceKey, "resourceKey");
|
||||
return requireResult(repository.findResource(key), "resource", key);
|
||||
}
|
||||
|
||||
public OperationsPage<ResourceOperationsView> resources(String state,
|
||||
int offset,
|
||||
int limit) {
|
||||
validatePage(offset, limit);
|
||||
String filter = optional(state);
|
||||
return new OperationsPage<ResourceOperationsView>(
|
||||
repository.findResources(filter, offset, limit),
|
||||
repository.countResources(filter), offset, limit);
|
||||
}
|
||||
|
||||
public RecoveryCaseView recoveryCase(String caseId) {
|
||||
String id = requireText(caseId, "caseId");
|
||||
return requireResult(repository.findRecoveryCase(id),
|
||||
@@ -44,7 +71,21 @@ public final class WcsOperationsQueryService {
|
||||
|
||||
public List<RecoveryCaseView> recoveryCases(String state, int offset,
|
||||
int limit) {
|
||||
return recoveryCasePage(state, offset, limit).getRows();
|
||||
}
|
||||
|
||||
public OperationsPage<RecoveryCaseView> recoveryCasePage(
|
||||
String state, int offset, int limit) {
|
||||
String recoveryState = requireText(state, "state");
|
||||
validatePage(offset, limit);
|
||||
List<RecoveryCaseView> views = Objects.requireNonNull(
|
||||
repository.findRecoveryCases(recoveryState, offset, limit),
|
||||
"recovery case page");
|
||||
return new OperationsPage<RecoveryCaseView>(views,
|
||||
repository.countRecoveryCases(recoveryState), offset, limit);
|
||||
}
|
||||
|
||||
private static void validatePage(int offset, int limit) {
|
||||
if (offset < 0) {
|
||||
throw new IllegalArgumentException("offset must not be negative");
|
||||
}
|
||||
@@ -52,11 +93,6 @@ public final class WcsOperationsQueryService {
|
||||
throw new IllegalArgumentException(
|
||||
"limit must be between 1 and " + MAX_PAGE_SIZE);
|
||||
}
|
||||
List<RecoveryCaseView> views = Objects.requireNonNull(
|
||||
repository.findRecoveryCases(recoveryState, offset, limit),
|
||||
"recovery case page");
|
||||
return Collections.unmodifiableList(
|
||||
new ArrayList<RecoveryCaseView>(views));
|
||||
}
|
||||
|
||||
private static <T> T requireResult(T value, String type, String key) {
|
||||
@@ -72,4 +108,8 @@ public final class WcsOperationsQueryService {
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private static String optional(String value) {
|
||||
return value == null || value.trim().isEmpty() ? null : value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,32 @@ package com.ruoyi.wcs.query;
|
||||
import java.util.List;
|
||||
|
||||
public interface WcsOperationsReadRepository {
|
||||
List<OrderExecutionView> findOrders(String state, int offset, int limit);
|
||||
|
||||
long countOrders(String state);
|
||||
|
||||
OrderExecutionView findOrder(String orderId);
|
||||
|
||||
OrderExecutionView findCommand(String commandKey);
|
||||
|
||||
DeviceOperationsView findDevice(String deviceId);
|
||||
|
||||
List<DeviceOperationsView> findDevices(String state, int offset,
|
||||
int limit);
|
||||
|
||||
long countDevices(String state);
|
||||
|
||||
ResourceOperationsView findResource(String resourceKey);
|
||||
|
||||
List<ResourceOperationsView> findResources(String state, int offset,
|
||||
int limit);
|
||||
|
||||
long countResources(String state);
|
||||
|
||||
RecoveryCaseView findRecoveryCase(String caseId);
|
||||
|
||||
List<RecoveryCaseView> findRecoveryCases(String state, int offset,
|
||||
int limit);
|
||||
|
||||
long countRecoveryCases(String state);
|
||||
}
|
||||
|
||||
@@ -33,11 +33,29 @@ class WcsOperationsQueryServiceTest {
|
||||
new FakeReadRepository());
|
||||
|
||||
assertEquals("ALARM-1", queries.device("VEH-1").getActiveAlarmCode());
|
||||
assertEquals(77L, queries.resource("NODE:A").getFencingToken());
|
||||
ResourceOperationsView resource = queries.resource("NODE:A");
|
||||
assertEquals("NODE", resource.getResourceType());
|
||||
assertEquals(2, resource.getCapacity());
|
||||
assertEquals(1, resource.getProtectedCount());
|
||||
assertEquals("VEH-1", resource.getHolder());
|
||||
assertEquals(77L, resource.getFencingToken());
|
||||
assertEquals("TASK_NUMBER_MISMATCH",
|
||||
queries.recoveryCase("RC-1").getReason());
|
||||
}
|
||||
|
||||
@Test
|
||||
void representsAFreeResourceWithoutFabricatingReservationOwnership() {
|
||||
ResourceOperationsView resource = new ResourceOperationsView(
|
||||
"NODE:B", "NODE", 2, 0, null, "FREE", null, null,
|
||||
null, 0L, null, Instant.EPOCH);
|
||||
|
||||
assertEquals("FREE", resource.getReservationState());
|
||||
assertEquals(0, resource.getProtectedCount());
|
||||
assertEquals(null, resource.getHolder());
|
||||
assertEquals(null, resource.getOrderId());
|
||||
assertEquals(0L, resource.getFencingToken());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pagesRecoveryCasesByExplicitStateWithABoundedLimit() {
|
||||
WcsOperationsQueryService queries = new WcsOperationsQueryService(
|
||||
@@ -54,6 +72,31 @@ class WcsOperationsQueryServiceTest {
|
||||
() -> queries.recoveryCases("OPEN", 0, 501));
|
||||
}
|
||||
|
||||
@Test
|
||||
void pagesOperationalViewsWithStableTotalsAndOrderIdentifiers() {
|
||||
WcsOperationsQueryService queries = new WcsOperationsQueryService(
|
||||
new FakeReadRepository());
|
||||
|
||||
OperationsPage<OrderExecutionView> orders = queries.orders(
|
||||
"EXECUTING", 0, 20);
|
||||
OperationsPage<DeviceOperationsView> devices = queries.devices(
|
||||
"UNKNOWN", 0, 20);
|
||||
OperationsPage<ResourceOperationsView> resources = queries.resources(
|
||||
"OCCUPIED", 0, 20);
|
||||
OperationsPage<RecoveryCaseView> recovery = queries.recoveryCasePage(
|
||||
"OPEN", 0, 20);
|
||||
|
||||
assertEquals(2L, orders.getTotal());
|
||||
assertEquals("TASK-1", orders.getRows().get(0)
|
||||
.getExternalRequestId());
|
||||
assertEquals("EXECUTING", orders.getRows().get(0).getOrderState());
|
||||
assertEquals(1L, devices.getTotal());
|
||||
assertEquals(1L, resources.getTotal());
|
||||
assertEquals(2L, recovery.getTotal());
|
||||
assertThrows(UnsupportedOperationException.class,
|
||||
() -> orders.getRows().add(orders.getRows().get(0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingOrBlankOperationsLookupFailsExplicitly() {
|
||||
WcsOperationsQueryService queries = new WcsOperationsQueryService(
|
||||
@@ -71,8 +114,9 @@ class WcsOperationsQueryServiceTest {
|
||||
if (!"ORDER-1".equals(orderId)) {
|
||||
return null;
|
||||
}
|
||||
return new OrderExecutionView("ORDER-1", "WF-1", "STEP-1",
|
||||
"VEH-1", "CMD-1", "UNKNOWN", "RC-1", Instant.EPOCH);
|
||||
return new OrderExecutionView("ORDER-1", "TASK-1", "EXECUTING",
|
||||
"WF-1", "STEP-1", "VEH-1", "CMD-1", "UNKNOWN",
|
||||
"RC-1", Instant.EPOCH);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -89,9 +133,9 @@ class WcsOperationsQueryServiceTest {
|
||||
|
||||
@Override
|
||||
public ResourceOperationsView findResource(String resourceKey) {
|
||||
return new ResourceOperationsView("NODE:A", "OCCUPIED",
|
||||
"ORDER-1", "STEP-1", "VEH-1", 77L, null,
|
||||
Instant.EPOCH);
|
||||
return new ResourceOperationsView("NODE:A", "NODE", 2, 1,
|
||||
"VEH-1", "OCCUPIED", "ORDER-1", "STEP-1",
|
||||
"VEH-1", 77L, null, Instant.EPOCH);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -106,6 +150,48 @@ class WcsOperationsQueryServiceTest {
|
||||
return Arrays.asList(recovery("RC-1"), recovery("RC-2"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OrderExecutionView> findOrders(String state, int offset,
|
||||
int limit) {
|
||||
return Arrays.asList(findOrder("ORDER-1"),
|
||||
new OrderExecutionView("ORDER-2", "TASK-2", "PLANNED",
|
||||
"WF-2", null, null, null, null, null,
|
||||
Instant.EPOCH));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countOrders(String state) {
|
||||
return 2L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DeviceOperationsView> findDevices(String state,
|
||||
int offset, int limit) {
|
||||
return Collections.singletonList(findDevice("VEH-1"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countDevices(String state) {
|
||||
return 1L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ResourceOperationsView> findResources(String state,
|
||||
int offset,
|
||||
int limit) {
|
||||
return Collections.singletonList(findResource("NODE:A"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countResources(String state) {
|
||||
return 1L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countRecoveryCases(String state) {
|
||||
return 2L;
|
||||
}
|
||||
|
||||
private static RecoveryCaseView recovery(String caseId) {
|
||||
return new RecoveryCaseView(caseId, "VEH-1", "CMD-1", "OPEN",
|
||||
"TASK_NUMBER_MISMATCH", Instant.EPOCH, null, null);
|
||||
|
||||
Reference in New Issue
Block a user