feat(ft546): add WCS operations console
This commit is contained in:
@@ -6,8 +6,13 @@ import com.ruoyi.wcs.application.device.DeviceExecutionService;
|
||||
import com.ruoyi.wcs.application.device.SnapshotCollector;
|
||||
import com.ruoyi.wcs.application.dispatch.DispatchCoordinator;
|
||||
import com.ruoyi.wcs.application.recovery.RecoveryGate;
|
||||
import com.ruoyi.wcs.application.recovery.RecoveryApplicationService;
|
||||
import com.ruoyi.wcs.domain.shared.IdGenerator;
|
||||
import com.ruoyi.wcs.domain.shared.WcsClock;
|
||||
import com.ruoyi.wcs.port.out.DispatchSnapshotProvider;
|
||||
import com.ruoyi.wcs.port.out.RecoveryCaseRepository;
|
||||
import com.ruoyi.wcs.query.WcsOperationsQueryService;
|
||||
import com.ruoyi.wcs.query.WcsOperationsReadRepository;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -37,4 +42,18 @@ public class Ft546AdapterConfiguration {
|
||||
WcsRuntimeCoordinator coordinator) {
|
||||
return new Ft546WcsScheduler(coordinator);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WcsOperationsQueryService wcsOperationsQueryService(
|
||||
WcsOperationsReadRepository repository) {
|
||||
return new WcsOperationsQueryService(repository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RecoveryApplicationService recoveryApplicationService(
|
||||
RecoveryCaseRepository repository, IdGenerator idGenerator,
|
||||
WcsClock clock) {
|
||||
return new RecoveryApplicationService(repository, idGenerator,
|
||||
clock::now);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.ruoyi.project.ft546.web;
|
||||
|
||||
import com.ruoyi.common.constant.HttpStatus;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.wcs.query.OperationsPage;
|
||||
import com.ruoyi.wcs.query.WcsOperationsQueryService;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/wcs/devices")
|
||||
@ConditionalOnProperty(prefix = "ft546.wcs", name = "runtime-mode",
|
||||
havingValue = "STANDARD")
|
||||
public class WcsDeviceController extends BaseController {
|
||||
private final WcsOperationsQueryService queries;
|
||||
|
||||
public WcsDeviceController(WcsOperationsQueryService queries) {
|
||||
this.queries = Objects.requireNonNull(queries, "queries");
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('wcs:device:list')")
|
||||
@GetMapping
|
||||
public TableDataInfo list(@RequestParam(required = false) String state,
|
||||
@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "20") int pageSize) {
|
||||
OperationsPage<?> page = queries.devices(state,
|
||||
offset(pageNum, pageSize), pageSize);
|
||||
TableDataInfo result = new TableDataInfo(page.getRows(), page.getTotal());
|
||||
result.setCode(HttpStatus.SUCCESS);
|
||||
result.setMsg("查询成功");
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int offset(int pageNum, int pageSize) {
|
||||
if (pageNum <= 0 || pageSize <= 0) {
|
||||
throw new IllegalArgumentException("invalid page request");
|
||||
}
|
||||
return Math.multiplyExact(pageNum - 1, pageSize);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.ruoyi.project.ft546.web;
|
||||
|
||||
import com.ruoyi.common.constant.HttpStatus;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.wcs.application.task.TransportOrderResult;
|
||||
import com.ruoyi.wcs.port.in.TransportOrderUseCase;
|
||||
import com.ruoyi.wcs.query.OperationsPage;
|
||||
import com.ruoyi.wcs.query.OrderExecutionView;
|
||||
import com.ruoyi.wcs.query.WcsOperationsQueryService;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/wcs/orders")
|
||||
@ConditionalOnProperty(prefix = "ft546.wcs", name = "runtime-mode",
|
||||
havingValue = "STANDARD")
|
||||
public class WcsOrderController extends BaseController {
|
||||
private final WcsOperationsQueryService queries;
|
||||
private final TransportOrderUseCase orders;
|
||||
|
||||
public WcsOrderController(WcsOperationsQueryService queries,
|
||||
TransportOrderUseCase orders) {
|
||||
this.queries = Objects.requireNonNull(queries, "queries");
|
||||
this.orders = Objects.requireNonNull(orders, "orders");
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('wcs:order:list')")
|
||||
@GetMapping
|
||||
public TableDataInfo list(
|
||||
@RequestParam(required = false) String state,
|
||||
@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "20") int pageSize) {
|
||||
return table(queries.orders(state, offset(pageNum, pageSize), pageSize));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('wcs:order:control')")
|
||||
@PostMapping("/{id}/suspend")
|
||||
public AjaxResult suspend(@PathVariable("id") String orderId,
|
||||
@RequestBody OrderControlRequest request) {
|
||||
TransportOrderResult result = orders.suspend(orderId,
|
||||
requireText(request == null ? null : request.getReason(),
|
||||
"reason"));
|
||||
return success(result);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('wcs:order:control')")
|
||||
@PostMapping("/{id}/resume")
|
||||
public AjaxResult resume(@PathVariable("id") String orderId) {
|
||||
return success(orders.resume(orderId));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('wcs:order:control')")
|
||||
@PostMapping("/{id}/cancel")
|
||||
public AjaxResult cancel(@PathVariable("id") String orderId) {
|
||||
OrderExecutionView view = queries.order(orderId);
|
||||
return success(orders.cancel(requireText(view.getExternalRequestId(),
|
||||
"externalRequestId")));
|
||||
}
|
||||
|
||||
private static TableDataInfo table(OperationsPage<?> page) {
|
||||
TableDataInfo result = new TableDataInfo(page.getRows(), page.getTotal());
|
||||
result.setCode(HttpStatus.SUCCESS);
|
||||
result.setMsg("查询成功");
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int offset(int pageNum, int pageSize) {
|
||||
if (pageNum <= 0 || pageSize <= 0) {
|
||||
throw new IllegalArgumentException("invalid page request");
|
||||
}
|
||||
return Math.multiplyExact(pageNum - 1, pageSize);
|
||||
}
|
||||
|
||||
private static String requireText(String value, String field) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException(field + " must not be blank");
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
public static final class OrderControlRequest {
|
||||
private String reason;
|
||||
public String getReason() { return reason; }
|
||||
public void setReason(String reason) { this.reason = reason; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.ruoyi.project.ft546.web;
|
||||
|
||||
import com.ruoyi.common.constant.HttpStatus;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.wcs.domain.recovery.RecoveryEvidence;
|
||||
import com.ruoyi.wcs.domain.shared.WcsClock;
|
||||
import com.ruoyi.wcs.port.in.RecoveryUseCase;
|
||||
import com.ruoyi.wcs.query.OperationsPage;
|
||||
import com.ruoyi.wcs.query.WcsOperationsQueryService;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/wcs/recovery/cases")
|
||||
@ConditionalOnProperty(prefix = "ft546.wcs", name = "runtime-mode",
|
||||
havingValue = "STANDARD")
|
||||
public class WcsRecoveryController extends BaseController {
|
||||
private final WcsOperationsQueryService queries;
|
||||
private final RecoveryUseCase recovery;
|
||||
private final WcsClock clock;
|
||||
|
||||
public WcsRecoveryController(WcsOperationsQueryService queries,
|
||||
RecoveryUseCase recovery, WcsClock clock) {
|
||||
this.queries = Objects.requireNonNull(queries, "queries");
|
||||
this.recovery = Objects.requireNonNull(recovery, "recovery");
|
||||
this.clock = Objects.requireNonNull(clock, "clock");
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('wcs:recovery:list')")
|
||||
@GetMapping
|
||||
public TableDataInfo list(
|
||||
@RequestParam(defaultValue = "OPEN") String state,
|
||||
@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "20") int pageSize) {
|
||||
OperationsPage<?> page = queries.recoveryCasePage(state,
|
||||
offset(pageNum, pageSize), pageSize);
|
||||
TableDataInfo result = new TableDataInfo(page.getRows(), page.getTotal());
|
||||
result.setCode(HttpStatus.SUCCESS);
|
||||
result.setMsg("查询成功");
|
||||
return result;
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('wcs:recovery:confirm')")
|
||||
@PostMapping("/{id}/confirm")
|
||||
public AjaxResult confirm(@PathVariable("id") String caseId,
|
||||
@RequestBody RecoveryConfirmRequest request) {
|
||||
if (request == null || request.getLoaded() == null) {
|
||||
throw new IllegalArgumentException("loaded must be specified");
|
||||
}
|
||||
RecoveryEvidence evidence = new RecoveryEvidence(
|
||||
request.getCurrentSite(),
|
||||
request.getLoaded() ? "LOADED" : "EMPTY",
|
||||
request.getTaskNumber(), request.getDeviceMode(), clock.now(),
|
||||
"OPERATOR_UI", request.getReason());
|
||||
recovery.confirmFacts(caseId, getUsername(), evidence);
|
||||
return success();
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('wcs:recovery:confirm')")
|
||||
@PostMapping("/{id}/abort")
|
||||
public AjaxResult abort(@PathVariable("id") String caseId,
|
||||
@RequestBody RecoveryAbortRequest request) {
|
||||
recovery.abort(caseId, getUsername(),
|
||||
requireText(request == null ? null : request.getReason(),
|
||||
"reason"));
|
||||
return success();
|
||||
}
|
||||
|
||||
private static int offset(int pageNum, int pageSize) {
|
||||
if (pageNum <= 0 || pageSize <= 0) {
|
||||
throw new IllegalArgumentException("invalid page request");
|
||||
}
|
||||
return Math.multiplyExact(pageNum - 1, pageSize);
|
||||
}
|
||||
|
||||
private static String requireText(String value, String field) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException(field + " must not be blank");
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
public static final class RecoveryConfirmRequest {
|
||||
private String reason;
|
||||
private String currentSite;
|
||||
private Boolean loaded;
|
||||
private String taskNumber;
|
||||
private String deviceMode;
|
||||
|
||||
public String getReason() { return reason; }
|
||||
public void setReason(String reason) { this.reason = reason; }
|
||||
public String getCurrentSite() { return currentSite; }
|
||||
public void setCurrentSite(String currentSite) {
|
||||
this.currentSite = currentSite;
|
||||
}
|
||||
public Boolean getLoaded() { return loaded; }
|
||||
public void setLoaded(Boolean loaded) { this.loaded = loaded; }
|
||||
public String getTaskNumber() { return taskNumber; }
|
||||
public void setTaskNumber(String taskNumber) {
|
||||
this.taskNumber = taskNumber;
|
||||
}
|
||||
public String getDeviceMode() { return deviceMode; }
|
||||
public void setDeviceMode(String deviceMode) {
|
||||
this.deviceMode = deviceMode;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class RecoveryAbortRequest {
|
||||
private String reason;
|
||||
public String getReason() { return reason; }
|
||||
public void setReason(String reason) { this.reason = reason; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.ruoyi.project.ft546.web;
|
||||
|
||||
import com.ruoyi.common.constant.HttpStatus;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.wcs.query.OperationsPage;
|
||||
import com.ruoyi.wcs.query.WcsOperationsQueryService;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/wcs/traffic/resources")
|
||||
@ConditionalOnProperty(prefix = "ft546.wcs", name = "runtime-mode",
|
||||
havingValue = "STANDARD")
|
||||
public class WcsTrafficController extends BaseController {
|
||||
private final WcsOperationsQueryService queries;
|
||||
|
||||
public WcsTrafficController(WcsOperationsQueryService queries) {
|
||||
this.queries = Objects.requireNonNull(queries, "queries");
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('wcs:traffic:list')")
|
||||
@GetMapping
|
||||
public TableDataInfo list(@RequestParam(required = false) String state,
|
||||
@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "20") int pageSize) {
|
||||
OperationsPage<?> page = queries.resources(state,
|
||||
offset(pageNum, pageSize), pageSize);
|
||||
TableDataInfo result = new TableDataInfo(page.getRows(), page.getTotal());
|
||||
result.setCode(HttpStatus.SUCCESS);
|
||||
result.setMsg("查询成功");
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int offset(int pageNum, int pageSize) {
|
||||
if (pageNum <= 0 || pageSize <= 0) {
|
||||
throw new IllegalArgumentException("invalid page request");
|
||||
}
|
||||
return Math.multiplyExact(pageNum - 1, pageSize);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
5
ruoyi-ui/src/api/wcs/device.js
Normal file
5
ruoyi-ui/src/api/wcs/device.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function listDevices(query) {
|
||||
return request({ url: '/wcs/devices', method: 'get', params: query })
|
||||
}
|
||||
21
ruoyi-ui/src/api/wcs/order.js
Normal file
21
ruoyi-ui/src/api/wcs/order.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function listOrders(query) {
|
||||
return request({ url: '/wcs/orders', method: 'get', params: query })
|
||||
}
|
||||
|
||||
export function suspendOrder(orderId, reason) {
|
||||
return request({
|
||||
url: `/wcs/orders/${orderId}/suspend`,
|
||||
method: 'post',
|
||||
data: { reason }
|
||||
})
|
||||
}
|
||||
|
||||
export function resumeOrder(orderId) {
|
||||
return request({ url: `/wcs/orders/${orderId}/resume`, method: 'post' })
|
||||
}
|
||||
|
||||
export function cancelOrder(orderId) {
|
||||
return request({ url: `/wcs/orders/${orderId}/cancel`, method: 'post' })
|
||||
}
|
||||
21
ruoyi-ui/src/api/wcs/recovery.js
Normal file
21
ruoyi-ui/src/api/wcs/recovery.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function listRecoveryCases(query) {
|
||||
return request({ url: '/wcs/recovery/cases', method: 'get', params: query })
|
||||
}
|
||||
|
||||
export function confirmRecovery(caseId, data) {
|
||||
return request({
|
||||
url: `/wcs/recovery/cases/${caseId}/confirm`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function abortRecovery(caseId, reason) {
|
||||
return request({
|
||||
url: `/wcs/recovery/cases/${caseId}/abort`,
|
||||
method: 'post',
|
||||
data: { reason }
|
||||
})
|
||||
}
|
||||
9
ruoyi-ui/src/api/wcs/traffic.js
Normal file
9
ruoyi-ui/src/api/wcs/traffic.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function listResources(query) {
|
||||
return request({
|
||||
url: '/wcs/traffic/resources',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
61
ruoyi-ui/src/views/wcs/device/index.vue
Normal file
61
ruoyi-ui/src/views/wcs/device/index.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="app-container wcs-page">
|
||||
<div class="wcs-page__bar"><h2>WCS 设备</h2><el-switch v-model="autoRefresh" active-text="自动刷新" /></div>
|
||||
<el-form ref="queryForm" :model="queryParams" size="small" :inline="true">
|
||||
<el-form-item label="命令状态" prop="state"><el-select v-model="queryParams.state" clearable placeholder="全部状态"><el-option v-for="state in commandStates" :key="state" :label="state" :value="state" /></el-select></el-form-item>
|
||||
<el-form-item><el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">查询</el-button><el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button></el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="loading" :data="rows" stripe @row-click="openDetail">
|
||||
<el-table-column label="设备 ID" prop="deviceId" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column label="类型" prop="deviceType" width="120" />
|
||||
<el-table-column label="连接" prop="connected" width="90" align="center"><template slot-scope="scope"><el-tag size="mini" :type="scope.row.connected ? 'success' : 'danger'">{{ scope.row.connected ? '在线' : '离线' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="当前位置" prop="currentSite" min-width="130" />
|
||||
<el-table-column label="命令状态" prop="commandState" width="130" align="center" />
|
||||
<el-table-column label="当前命令" prop="currentCommandKey" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="活动告警" prop="activeAlarmCode" min-width="140"><template slot-scope="scope"><span :class="{ 'alarm-text': scope.row.activeAlarmCode }">{{ scope.row.activeAlarmCode || '-' }}</span></template></el-table-column>
|
||||
<el-table-column label="更新时间" prop="updatedAt" width="180"><template slot-scope="scope">{{ parseTime(scope.row.updatedAt) }}</template></el-table-column>
|
||||
</el-table>
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
||||
<el-drawer title="设备详情" :visible.sync="detailOpen" size="420px"><dl v-if="current" class="wcs-detail"><template v-for="item in details"><dt :key="item.key + '-label'">{{ item.label }}</dt><dd :key="item.key">{{ current[item.key] === null || current[item.key] === undefined ? '-' : current[item.key] }}</dd></template></dl></el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listDevices } from '@/api/wcs/device'
|
||||
|
||||
export default {
|
||||
name: 'WcsDevice',
|
||||
data() {
|
||||
return {
|
||||
loading: false, autoRefresh: true, timer: null, rows: [], total: 0, current: null, detailOpen: false,
|
||||
queryParams: { pageNum: 1, pageSize: 20, state: null },
|
||||
commandStates: ['CREATED', 'SENT', 'ACKNOWLEDGED', 'RUNNING', 'COMPLETED', 'FAILED', 'UNKNOWN'],
|
||||
details: [
|
||||
{ key: 'deviceId', label: '设备 ID' }, { key: 'deviceType', label: '类型' },
|
||||
{ key: 'connected', label: '连接' }, { key: 'currentSite', label: '当前位置' },
|
||||
{ key: 'currentCommandKey', label: '当前命令' }, { key: 'commandState', label: '命令状态' },
|
||||
{ key: 'activeAlarmCode', label: '活动告警' }, { key: 'recoveryCaseId', label: '恢复案例' },
|
||||
{ key: 'updatedAt', label: '更新时间' }
|
||||
]
|
||||
}
|
||||
},
|
||||
created() { this.getList() },
|
||||
mounted() { this.timer = window.setInterval(() => { if (this.autoRefresh) this.getList(true) }, 5000) },
|
||||
beforeDestroy() { window.clearInterval(this.timer) },
|
||||
methods: {
|
||||
getList(silent) { if (!silent) this.loading = true; return listDevices(this.queryParams).then(r => { this.rows = r.rows || []; this.total = r.total || 0 }).finally(() => { this.loading = false }) },
|
||||
handleQuery() { this.queryParams.pageNum = 1; this.getList() },
|
||||
resetQuery() { this.resetForm('queryForm'); this.handleQuery() },
|
||||
openDetail(row) { this.current = row; this.detailOpen = true }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wcs-page__bar { display:flex; align-items:center; justify-content:space-between; min-height:58px; margin-bottom:16px; padding-bottom:12px; border-bottom:1px solid #dcdfe6; }
|
||||
.wcs-page__bar h2 { margin:0; color:#17202a; font-size:20px; font-weight:600; letter-spacing:0; }
|
||||
.alarm-text { color:#c0362c; font-weight:600; }
|
||||
.wcs-detail { display:grid; grid-template-columns:110px minmax(0, 1fr); gap:0; margin:0 24px; border-top:1px solid #ebeef5; }
|
||||
.wcs-detail dt,.wcs-detail dd { margin:0; padding:12px 8px; border-bottom:1px solid #ebeef5; overflow-wrap:anywhere; }
|
||||
.wcs-detail dt { color:#667085; background:#f7f8fa; }.wcs-detail dd { color:#17202a; }
|
||||
</style>
|
||||
141
ruoyi-ui/src/views/wcs/order/index.vue
Normal file
141
ruoyi-ui/src/views/wcs/order/index.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div class="app-container wcs-page">
|
||||
<div class="wcs-page__bar">
|
||||
<div>
|
||||
<h2>WCS 订单</h2>
|
||||
<span class="wcs-page__stamp">{{ refreshStamp || '-' }}</span>
|
||||
</div>
|
||||
<el-switch v-model="autoRefresh" active-text="自动刷新" />
|
||||
</div>
|
||||
|
||||
<el-form ref="queryForm" :model="queryParams" size="small" :inline="true">
|
||||
<el-form-item label="订单状态" prop="state">
|
||||
<el-select v-model="queryParams.state" clearable placeholder="全部状态">
|
||||
<el-option v-for="state in orderStates" :key="state" :label="state" :value="state" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">查询</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="loading" :data="rows" stripe @row-click="openDetail">
|
||||
<el-table-column label="WMS 任务号" prop="externalRequestId" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="订单 ID" prop="orderId" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="订单状态" prop="orderState" width="140" align="center">
|
||||
<template slot-scope="scope"><el-tag size="mini" :type="stateType(scope.row.orderState)">{{ scope.row.orderState || '-' }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="步骤" prop="stepId" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column label="设备" prop="deviceId" width="110" />
|
||||
<el-table-column label="命令状态" prop="commandState" width="130" align="center" />
|
||||
<el-table-column label="更新时间" prop="updatedAt" width="180">
|
||||
<template slot-scope="scope">{{ parseTime(scope.row.updatedAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="230" fixed="right" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button v-if="canSuspend(scope.row)" type="text" icon="el-icon-video-pause" v-hasPermi="['wcs:order:control']" @click.stop="openSuspend(scope.row)">暂停</el-button>
|
||||
<el-button v-if="scope.row.orderState === 'SUSPENDED'" type="text" icon="el-icon-video-play" v-hasPermi="['wcs:order:control']" @click.stop="resume(scope.row)">恢复</el-button>
|
||||
<el-button v-if="canCancel(scope.row)" type="text" class="danger-action" icon="el-icon-circle-close" v-hasPermi="['wcs:order:control']" @click.stop="cancel(scope.row)">取消</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
||||
|
||||
<el-dialog title="暂停订单" :visible.sync="suspendOpen" width="460px" append-to-body>
|
||||
<el-form ref="suspendForm" :model="suspendForm" :rules="suspendRules" label-width="80px">
|
||||
<el-form-item label="原因" prop="reason"><el-input v-model="suspendForm.reason" type="textarea" :rows="3" maxlength="200" show-word-limit /></el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer"><el-button @click="suspendOpen = false">取消</el-button><el-button type="primary" @click="submitSuspend">确认暂停</el-button></div>
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer title="订单详情" :visible.sync="detailOpen" size="420px">
|
||||
<dl v-if="current" class="wcs-detail">
|
||||
<template v-for="item in orderDetails"><dt :key="item.key + '-label'">{{ item.label }}</dt><dd :key="item.key">{{ current[item.key] || '-' }}</dd></template>
|
||||
</dl>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listOrders, suspendOrder, resumeOrder, cancelOrder } from '@/api/wcs/order'
|
||||
|
||||
export default {
|
||||
name: 'WcsOrder',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
autoRefresh: true,
|
||||
timer: null,
|
||||
refreshStamp: '',
|
||||
rows: [],
|
||||
total: 0,
|
||||
current: null,
|
||||
detailOpen: false,
|
||||
suspendOpen: false,
|
||||
suspendForm: { orderId: null, reason: '' },
|
||||
suspendRules: { reason: [{ required: true, message: '请输入暂停原因', trigger: 'blur' }] },
|
||||
queryParams: { pageNum: 1, pageSize: 20, state: null },
|
||||
orderStates: ['ACCEPTED', 'PLANNED', 'EXECUTING', 'SUSPENDED', 'CANCEL_REQUESTED', 'COMPLETED', 'FAILED', 'CANCELLED'],
|
||||
orderDetails: [
|
||||
{ key: 'externalRequestId', label: 'WMS 任务号' }, { key: 'orderId', label: '订单 ID' },
|
||||
{ key: 'orderState', label: '订单状态' }, { key: 'workflowId', label: '工作流' },
|
||||
{ key: 'stepId', label: '当前步骤' }, { key: 'deviceId', label: '设备' },
|
||||
{ key: 'currentCommandKey', label: '命令键' }, { key: 'commandState', label: '命令状态' },
|
||||
{ key: 'recoveryCaseId', label: '恢复案例' }, { key: 'updatedAt', label: '更新时间' }
|
||||
]
|
||||
}
|
||||
},
|
||||
created() { this.getList() },
|
||||
mounted() { this.timer = window.setInterval(() => { if (this.autoRefresh) this.getList(true) }, 5000) },
|
||||
beforeDestroy() { window.clearInterval(this.timer) },
|
||||
methods: {
|
||||
getList(silent) {
|
||||
if (!silent) this.loading = true
|
||||
return listOrders(this.queryParams).then(response => {
|
||||
this.rows = response.rows || []
|
||||
this.total = response.total || 0
|
||||
this.refreshStamp = this.parseTime(new Date())
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
handleQuery() { this.queryParams.pageNum = 1; this.getList() },
|
||||
resetQuery() { this.resetForm('queryForm'); this.handleQuery() },
|
||||
openDetail(row) { this.current = row; this.detailOpen = true },
|
||||
stateType(state) {
|
||||
if (state === 'COMPLETED') return 'success'
|
||||
if (state === 'FAILED' || state === 'CANCELLED') return 'danger'
|
||||
if (state === 'SUSPENDED' || state === 'CANCEL_REQUESTED') return 'warning'
|
||||
return ''
|
||||
},
|
||||
canSuspend(row) { return ['ACCEPTED', 'PLANNED', 'EXECUTING'].includes(row.orderState) },
|
||||
canCancel(row) { return ['ACCEPTED', 'PLANNED', 'CANCEL_REQUESTED'].includes(row.orderState) },
|
||||
openSuspend(row) { this.suspendForm = { orderId: row.orderId, reason: '' }; this.suspendOpen = true },
|
||||
submitSuspend() {
|
||||
this.$refs.suspendForm.validate(valid => {
|
||||
if (!valid) return
|
||||
suspendOrder(this.suspendForm.orderId, this.suspendForm.reason).then(() => {
|
||||
this.suspendOpen = false; this.$modal.msgSuccess('订单已暂停'); this.getList()
|
||||
})
|
||||
})
|
||||
},
|
||||
resume(row) { resumeOrder(row.orderId).then(() => { this.$modal.msgSuccess('订单已恢复'); this.getList() }) },
|
||||
cancel(row) {
|
||||
this.$modal.confirm(`确认取消 WMS 任务 ${row.externalRequestId || row.orderId}?`).then(() => cancelOrder(row.orderId)).then(() => {
|
||||
this.$modal.msgSuccess('订单已取消'); this.getList()
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wcs-page__bar { display:flex; align-items:center; justify-content:space-between; min-height:58px; margin-bottom:16px; padding-bottom:12px; border-bottom:1px solid #dcdfe6; }
|
||||
.wcs-page__bar h2 { margin:0 0 5px; color:#17202a; font-size:20px; font-weight:600; letter-spacing:0; }
|
||||
.wcs-page__stamp { color:#7b8794; font-size:12px; }
|
||||
.danger-action { color:#d14343; }
|
||||
.wcs-detail { display:grid; grid-template-columns:110px minmax(0, 1fr); gap:0; margin:0 24px; border-top:1px solid #ebeef5; }
|
||||
.wcs-detail dt,.wcs-detail dd { margin:0; padding:12px 8px; border-bottom:1px solid #ebeef5; overflow-wrap:anywhere; }
|
||||
.wcs-detail dt { color:#667085; background:#f7f8fa; }
|
||||
.wcs-detail dd { color:#17202a; }
|
||||
</style>
|
||||
102
ruoyi-ui/src/views/wcs/recovery/index.vue
Normal file
102
ruoyi-ui/src/views/wcs/recovery/index.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="app-container wcs-page">
|
||||
<div class="wcs-page__bar"><h2>恢复处置</h2><el-switch v-model="autoRefresh" active-text="自动刷新" /></div>
|
||||
<el-form ref="queryForm" :model="queryParams" size="small" :inline="true">
|
||||
<el-form-item label="案例状态" prop="state"><el-select v-model="queryParams.state" placeholder="案例状态"><el-option v-for="state in recoveryStates" :key="state" :label="state" :value="state" /></el-select></el-form-item>
|
||||
<el-form-item><el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">查询</el-button><el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button></el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="loading" :data="rows" stripe @row-click="openDetail">
|
||||
<el-table-column label="案例 ID" prop="caseId" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="设备" prop="deviceId" width="110" />
|
||||
<el-table-column label="命令键" prop="commandKey" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="状态" prop="state" width="120" align="center"><template slot-scope="scope"><el-tag size="mini" :type="recoveryType(scope.row.state)">{{ scope.row.state }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="原因" prop="reason" min-width="210" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" prop="createdAt" width="180"><template slot-scope="scope">{{ parseTime(scope.row.createdAt) }}</template></el-table-column>
|
||||
<el-table-column label="操作" width="170" fixed="right" align="center"><template slot-scope="scope"><template v-if="scope.row.state === 'OPEN'"><el-button type="text" icon="el-icon-circle-check" v-hasPermi="['wcs:recovery:confirm']" @click.stop="openConfirm(scope.row)">确认事实</el-button><el-button type="text" class="danger-action" icon="el-icon-circle-close" v-hasPermi="['wcs:recovery:confirm']" @click.stop="openAbort(scope.row)">中止</el-button></template></template></el-table-column>
|
||||
</el-table>
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
||||
|
||||
<el-dialog title="确认现场事实" :visible.sync="confirmOpen" width="520px" append-to-body>
|
||||
<el-form ref="confirmForm" :model="confirmForm" :rules="confirmRules" label-width="90px">
|
||||
<el-form-item label="当前位置" prop="currentSite"><el-input v-model="confirmForm.currentSite" /></el-form-item>
|
||||
<el-form-item label="任务号" prop="taskNumber"><el-input v-model="confirmForm.taskNumber" /></el-form-item>
|
||||
<el-form-item label="设备模式" prop="deviceMode"><el-select v-model="confirmForm.deviceMode"><el-option label="AUTO" value="AUTO" /><el-option label="MANUAL" value="MANUAL" /></el-select></el-form-item>
|
||||
<el-form-item label="载货状态" prop="loaded"><el-switch v-model="confirmForm.loaded" active-text="有货" inactive-text="空载" /></el-form-item>
|
||||
<el-form-item label="确认原因" prop="reason"><el-input v-model="confirmForm.reason" type="textarea" :rows="3" maxlength="200" show-word-limit /></el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer"><el-button @click="confirmOpen = false">取消</el-button><el-button type="primary" @click="submitConfirm">提交事实</el-button></div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog title="中止恢复案例" :visible.sync="abortOpen" width="460px" append-to-body>
|
||||
<el-form ref="abortForm" :model="abortForm" :rules="abortRules" label-width="80px"><el-form-item label="原因" prop="reason"><el-input v-model="abortForm.reason" type="textarea" :rows="3" maxlength="200" show-word-limit /></el-form-item></el-form>
|
||||
<div slot="footer"><el-button @click="abortOpen = false">取消</el-button><el-button type="danger" @click="submitAbort">确认中止</el-button></div>
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer title="恢复案例详情" :visible.sync="detailOpen" size="420px"><dl v-if="current" class="wcs-detail"><template v-for="item in details"><dt :key="item.key + '-label'">{{ item.label }}</dt><dd :key="item.key">{{ current[item.key] || '-' }}</dd></template></dl></el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listRecoveryCases, confirmRecovery, abortRecovery } from '@/api/wcs/recovery'
|
||||
|
||||
export default {
|
||||
name: 'WcsRecovery',
|
||||
data() {
|
||||
return {
|
||||
loading: false, autoRefresh: true, timer: null, rows: [], total: 0, current: null, detailOpen: false,
|
||||
confirmOpen: false, abortOpen: false,
|
||||
queryParams: { pageNum: 1, pageSize: 20, state: 'OPEN' },
|
||||
recoveryStates: ['OPEN', 'CONFIRMED', 'ABORTED'],
|
||||
confirmForm: { caseId: null, currentSite: '', taskNumber: '', deviceMode: 'AUTO', loaded: false, reason: '' },
|
||||
abortForm: { caseId: null, reason: '' },
|
||||
confirmRules: {
|
||||
currentSite: [{ required: true, message: '请输入当前位置', trigger: 'blur' }],
|
||||
taskNumber: [{ required: true, message: '请输入任务号', trigger: 'blur' }],
|
||||
deviceMode: [{ required: true, message: '请选择设备模式', trigger: 'change' }],
|
||||
reason: [{ required: true, message: '请输入确认原因', trigger: 'blur' }]
|
||||
},
|
||||
abortRules: { reason: [{ required: true, message: '请输入中止原因', trigger: 'blur' }] },
|
||||
details: [
|
||||
{ key: 'caseId', label: '案例 ID' }, { key: 'deviceId', label: '设备' },
|
||||
{ key: 'commandKey', label: '命令键' }, { key: 'state', label: '状态' },
|
||||
{ key: 'reason', label: '原因' }, { key: 'createdAt', label: '创建时间' },
|
||||
{ key: 'resolvedBy', label: '处置人' }, { key: 'resolvedAt', label: '处置时间' }
|
||||
]
|
||||
}
|
||||
},
|
||||
created() { this.getList() },
|
||||
mounted() { this.timer = window.setInterval(() => { if (this.autoRefresh) this.getList(true) }, 5000) },
|
||||
beforeDestroy() { window.clearInterval(this.timer) },
|
||||
methods: {
|
||||
getList(silent) { if (!silent) this.loading = true; return listRecoveryCases(this.queryParams).then(r => { this.rows = r.rows || []; this.total = r.total || 0 }).finally(() => { this.loading = false }) },
|
||||
handleQuery() { this.queryParams.pageNum = 1; this.getList() },
|
||||
resetQuery() { this.resetForm('queryForm'); this.queryParams.state = 'OPEN'; this.handleQuery() },
|
||||
openDetail(row) { this.current = row; this.detailOpen = true },
|
||||
recoveryType(state) { if (state === 'CONFIRMED') return 'success'; if (state === 'ABORTED') return 'danger'; return 'warning' },
|
||||
openConfirm(row) { this.confirmForm = { caseId: row.caseId, currentSite: '', taskNumber: '', deviceMode: 'AUTO', loaded: false, reason: '' }; this.confirmOpen = true },
|
||||
submitConfirm() {
|
||||
this.$refs.confirmForm.validate(valid => {
|
||||
if (!valid) return
|
||||
const data = { ...this.confirmForm }; delete data.caseId
|
||||
confirmRecovery(this.confirmForm.caseId, data).then(() => { this.confirmOpen = false; this.$modal.msgSuccess('现场事实已确认'); this.getList() })
|
||||
})
|
||||
},
|
||||
openAbort(row) { this.abortForm = { caseId: row.caseId, reason: '' }; this.abortOpen = true },
|
||||
submitAbort() {
|
||||
this.$refs.abortForm.validate(valid => {
|
||||
if (!valid) return
|
||||
abortRecovery(this.abortForm.caseId, this.abortForm.reason).then(() => { this.abortOpen = false; this.$modal.msgSuccess('恢复案例已中止'); this.getList() })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wcs-page__bar { display:flex; align-items:center; justify-content:space-between; min-height:58px; margin-bottom:16px; padding-bottom:12px; border-bottom:1px solid #dcdfe6; }
|
||||
.wcs-page__bar h2 { margin:0; color:#17202a; font-size:20px; font-weight:600; letter-spacing:0; }
|
||||
.danger-action { color:#d14343; }
|
||||
.wcs-detail { display:grid; grid-template-columns:110px minmax(0, 1fr); gap:0; margin:0 24px; border-top:1px solid #ebeef5; }
|
||||
.wcs-detail dt,.wcs-detail dd { margin:0; padding:12px 8px; border-bottom:1px solid #ebeef5; overflow-wrap:anywhere; }
|
||||
.wcs-detail dt { color:#667085; background:#f7f8fa; }.wcs-detail dd { color:#17202a; }
|
||||
</style>
|
||||
63
ruoyi-ui/src/views/wcs/traffic/index.vue
Normal file
63
ruoyi-ui/src/views/wcs/traffic/index.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="app-container wcs-page">
|
||||
<div class="wcs-page__bar"><h2>交通资源</h2><el-switch v-model="autoRefresh" active-text="自动刷新" /></div>
|
||||
<el-form ref="queryForm" :model="queryParams" size="small" :inline="true">
|
||||
<el-form-item label="占用状态" prop="state"><el-select v-model="queryParams.state" clearable placeholder="全部状态"><el-option v-for="state in resourceStates" :key="state" :label="state" :value="state" /></el-select></el-form-item>
|
||||
<el-form-item><el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">查询</el-button><el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button></el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="loading" :data="rows" stripe @row-click="openDetail">
|
||||
<el-table-column label="资源键" prop="resourceKey" min-width="210" show-overflow-tooltip />
|
||||
<el-table-column label="类型" prop="resourceType" width="100" />
|
||||
<el-table-column label="容量" prop="capacity" width="80" align="right" />
|
||||
<el-table-column label="保护数" prop="protectedCount" width="90" align="right" />
|
||||
<el-table-column label="持有者" prop="holder" width="110" show-overflow-tooltip />
|
||||
<el-table-column label="状态" prop="reservationState" width="130" align="center"><template slot-scope="scope"><el-tag size="mini" :type="resourceType(scope.row.reservationState)">{{ scope.row.reservationState }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="释放证据" prop="releaseEvidenceId" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="更新时间" prop="updatedAt" width="180"><template slot-scope="scope">{{ parseTime(scope.row.updatedAt) }}</template></el-table-column>
|
||||
</el-table>
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
||||
<el-drawer title="资源详情" :visible.sync="detailOpen" size="420px"><dl v-if="current" class="wcs-detail"><template v-for="item in details"><dt :key="item.key + '-label'">{{ item.label }}</dt><dd :key="item.key">{{ detailValue(current[item.key]) }}</dd></template></dl></el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listResources } from '@/api/wcs/traffic'
|
||||
|
||||
export default {
|
||||
name: 'WcsTraffic',
|
||||
data() {
|
||||
return {
|
||||
loading: false, autoRefresh: true, timer: null, rows: [], total: 0, current: null, detailOpen: false,
|
||||
queryParams: { pageNum: 1, pageSize: 20, state: null },
|
||||
resourceStates: ['FREE', 'RESERVED', 'OCCUPIED', 'RELEASING', 'UNKNOWN'],
|
||||
details: [
|
||||
{ key: 'resourceKey', label: '资源键' }, { key: 'resourceType', label: '类型' },
|
||||
{ key: 'capacity', label: '容量' }, { key: 'protectedCount', label: '保护数' },
|
||||
{ key: 'holder', label: '持有者' }, { key: 'reservationState', label: '状态' },
|
||||
{ key: 'orderId', label: '订单' }, { key: 'stepId', label: '步骤' },
|
||||
{ key: 'vehicleId', label: '车辆' }, { key: 'fencingToken', label: '围栏令牌' },
|
||||
{ key: 'releaseEvidenceId', label: '释放证据' }, { key: 'updatedAt', label: '更新时间' }
|
||||
]
|
||||
}
|
||||
},
|
||||
created() { this.getList() },
|
||||
mounted() { this.timer = window.setInterval(() => { if (this.autoRefresh) this.getList(true) }, 5000) },
|
||||
beforeDestroy() { window.clearInterval(this.timer) },
|
||||
methods: {
|
||||
getList(silent) { if (!silent) this.loading = true; return listResources(this.queryParams).then(r => { this.rows = r.rows || []; this.total = r.total || 0 }).finally(() => { this.loading = false }) },
|
||||
handleQuery() { this.queryParams.pageNum = 1; this.getList() },
|
||||
resetQuery() { this.resetForm('queryForm'); this.handleQuery() },
|
||||
openDetail(row) { this.current = row; this.detailOpen = true },
|
||||
detailValue(value) { return value === null || value === undefined || value === '' ? '-' : value },
|
||||
resourceType(state) { if (state === 'FREE') return 'success'; if (state === 'UNKNOWN') return 'danger'; if (state === 'RESERVED' || state === 'RELEASING') return 'warning'; return '' }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wcs-page__bar { display:flex; align-items:center; justify-content:space-between; min-height:58px; margin-bottom:16px; padding-bottom:12px; border-bottom:1px solid #dcdfe6; }
|
||||
.wcs-page__bar h2 { margin:0; color:#17202a; font-size:20px; font-weight:600; letter-spacing:0; }
|
||||
.wcs-detail { display:grid; grid-template-columns:110px minmax(0, 1fr); gap:0; margin:0 24px; border-top:1px solid #ebeef5; }
|
||||
.wcs-detail dt,.wcs-detail dd { margin:0; padding:12px 8px; border-bottom:1px solid #ebeef5; overflow-wrap:anywhere; }
|
||||
.wcs-detail dt { color:#667085; background:#f7f8fa; }.wcs-detail dd { color:#17202a; }
|
||||
</style>
|
||||
@@ -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