feat(ft546): add WCS operations console

This commit is contained in:
zgw
2026-08-11 19:25:29 +08:00
parent c80acf0d99
commit ceba465675
20 changed files with 1313 additions and 18 deletions

View File

@@ -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; }
}

View File

@@ -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; }

View File

@@ -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();
}

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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);