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,299 @@
package com.ruoyi.project.ft546.web;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.wcs.application.task.CreateTransportOrderCommand;
import com.ruoyi.wcs.application.task.TransportOrderResult;
import com.ruoyi.wcs.domain.recovery.RecoveryEvidence;
import com.ruoyi.wcs.port.in.RecoveryUseCase;
import com.ruoyi.wcs.port.in.TransportOrderUseCase;
import com.ruoyi.wcs.query.DeviceOperationsView;
import com.ruoyi.wcs.query.OrderExecutionView;
import com.ruoyi.wcs.query.RecoveryCaseView;
import com.ruoyi.wcs.query.ResourceOperationsView;
import com.ruoyi.wcs.query.WcsOperationsQueryService;
import com.ruoyi.wcs.query.WcsOperationsReadRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.lang.reflect.Method;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
class WcsOperationsControllerTest {
private RecordingOrderUseCase orders;
private RecordingRecoveryUseCase recovery;
private MockMvc mockMvc;
@BeforeEach
void setUp() {
orders = new RecordingOrderUseCase();
recovery = new RecordingRecoveryUseCase();
WcsOperationsQueryService queries = new WcsOperationsQueryService(
new OperationsReadRepository());
mockMvc = MockMvcBuilders.standaloneSetup(
new WcsOrderController(queries, orders),
new WcsDeviceController(queries),
new WcsTrafficController(queries),
new WcsRecoveryController(queries, recovery,
() -> Instant.parse("2026-08-11T08:00:00Z")))
.build();
}
@AfterEach
void clearSecurityContext() {
SecurityContextHolder.clearContext();
}
@Test
void returnsPagedOrdersWithCanonicalAndExternalIdentifiers()
throws Exception {
mockMvc.perform(get("/wcs/orders")
.param("state", "EXECUTING")
.param("pageNum", "1")
.param("pageSize", "20"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.total").value(1))
.andExpect(jsonPath("$.rows[0].orderId").value("ORDER-1"))
.andExpect(jsonPath("$.rows[0].externalRequestId")
.value("TASK-1"))
.andExpect(jsonPath("$.rows[0].orderState")
.value("EXECUTING"));
}
@Test
void returnsResourceCapacityProtectionAndHolderEvidence()
throws Exception {
mockMvc.perform(get("/wcs/traffic/resources")
.param("state", "OCCUPIED")
.param("pageNum", "1")
.param("pageSize", "20"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.rows[0].resourceType").value("NODE"))
.andExpect(jsonPath("$.rows[0].capacity").value(2))
.andExpect(jsonPath("$.rows[0].protectedCount").value(1))
.andExpect(jsonPath("$.rows[0].holder").value("VEH-1"))
.andExpect(jsonPath("$.rows[0].reservationState")
.value("OCCUPIED"));
}
@Test
void orderControlsUseTheCorrectCoreIdentifiers() throws Exception {
mockMvc.perform(post("/wcs/orders/ORDER-1/suspend")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"reason\":\"现场暂停\"}"))
.andExpect(status().isOk());
mockMvc.perform(post("/wcs/orders/ORDER-1/resume"))
.andExpect(status().isOk());
mockMvc.perform(post("/wcs/orders/ORDER-1/cancel"))
.andExpect(status().isOk());
assertEquals("ORDER-1", orders.suspendedOrderId);
assertEquals("现场暂停", orders.suspendReason);
assertEquals("ORDER-1", orders.resumedOrderId);
assertEquals("TASK-1", orders.cancelledExternalRequestId);
}
@Test
void confirmsRecoveryWithAuthenticatedOperatorAndCompleteEvidence()
throws Exception {
authenticate("operator-a");
mockMvc.perform(post("/wcs/recovery/cases/RC-1/confirm")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"reason\":\"现场复核\","
+ "\"currentSite\":\"070501\","
+ "\"loaded\":false,"
+ "\"taskNumber\":\"30001\","
+ "\"deviceMode\":\"AUTO\"}"))
.andExpect(status().isOk());
assertEquals("RC-1", recovery.confirmedCaseId);
assertEquals("operator-a", recovery.operatorId);
assertEquals("070501", recovery.evidence.getObservedSite());
assertEquals("EMPTY", recovery.evidence.getLoadState());
assertEquals("30001", recovery.evidence.getTaskNumber());
assertEquals("AUTO", recovery.evidence.getDeviceMode());
assertEquals("现场复核", recovery.evidence.getOperatorReason());
assertEquals(Instant.parse("2026-08-11T08:00:00Z"),
recovery.evidence.getEvidenceTime());
}
@Test
void declaresRecoveryConfirmationPermission() {
Method confirm = null;
for (Method method : WcsRecoveryController.class.getMethods()) {
if ("confirm".equals(method.getName())) {
confirm = method;
break;
}
}
assertNotNull(confirm);
PreAuthorize permission = confirm.getAnnotation(PreAuthorize.class);
assertNotNull(permission);
assertEquals("@ss.hasPermi('wcs:recovery:confirm')",
permission.value());
}
private static void authenticate(String username) {
SysUser user = new SysUser();
user.setUserName(username);
LoginUser loginUser = new LoginUser(user,
Collections.singleton("wcs:recovery:confirm"));
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(loginUser, null,
Collections.emptyList()));
}
private static final class RecordingOrderUseCase
implements TransportOrderUseCase {
private String cancelledExternalRequestId;
private String suspendedOrderId;
private String suspendReason;
private String resumedOrderId;
@Override
public TransportOrderResult create(CreateTransportOrderCommand command) {
return null;
}
@Override
public TransportOrderResult cancel(String externalRequestId) {
cancelledExternalRequestId = externalRequestId;
return null;
}
@Override
public TransportOrderResult suspend(String orderId, String reason) {
suspendedOrderId = orderId;
suspendReason = reason;
return null;
}
@Override
public TransportOrderResult resume(String orderId) {
resumedOrderId = orderId;
return null;
}
}
private static final class RecordingRecoveryUseCase
implements RecoveryUseCase {
private String confirmedCaseId;
private String operatorId;
private RecoveryEvidence evidence;
@Override
public void confirmFacts(String caseId, String operator,
RecoveryEvidence recoveryEvidence) {
confirmedCaseId = caseId;
operatorId = operator;
evidence = recoveryEvidence;
}
@Override
public void abort(String caseId, String operator, String reason) {
}
}
private static final class OperationsReadRepository
implements WcsOperationsReadRepository {
@Override
public OrderExecutionView findOrder(String orderId) {
return "ORDER-1".equals(orderId)
? new OrderExecutionView("ORDER-1", "TASK-1",
"EXECUTING", "WF-1", "STEP-1", "VEH-1", "CMD-1",
"SENT", null, Instant.EPOCH) : null;
}
@Override
public OrderExecutionView findCommand(String commandKey) {
return findOrder("ORDER-1");
}
@Override
public DeviceOperationsView findDevice(String deviceId) {
return new DeviceOperationsView("VEH-1", "SHUTTLE", "CMD-1",
"SENT", "070501", true, null, null, Instant.EPOCH);
}
@Override
public ResourceOperationsView findResource(String resourceKey) {
return new ResourceOperationsView("NODE:070501", "NODE", 2, 1,
"VEH-1", "OCCUPIED", "ORDER-1", "STEP-1",
"VEH-1", 11L, null, Instant.EPOCH);
}
@Override
public RecoveryCaseView findRecoveryCase(String caseId) {
return recovery(caseId);
}
@Override
public List<OrderExecutionView> findOrders(String state, int offset,
int limit) {
return Collections.singletonList(findOrder("ORDER-1"));
}
@Override
public long countOrders(String state) {
return 1L;
}
@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:070501"));
}
@Override
public long countResources(String state) {
return 1L;
}
@Override
public List<RecoveryCaseView> findRecoveryCases(String state,
int offset,
int limit) {
return Arrays.asList(recovery("RC-1"), recovery("RC-2"));
}
@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);
}
}
}