This commit is contained in:
2026-10-30 18:46:44 +08:00
parent edd4b1cd38
commit 5825739b92
@@ -0,0 +1,81 @@
package com.baiyue.lastleg.exceptionorder.service;
import com.baiyue.lastleg.exceptionorder.dto.ExceptionOrderRequests;
import com.baiyue.lastleg.exceptionorder.entity.ExceptionOrder;
import com.baiyue.lastleg.exceptionorder.entity.ExceptionOrderCommand;
import com.baiyue.lastleg.exceptionorder.repository.ExceptionWorkflowRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.ArgumentCaptor;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
/** 人工完结只关闭异常,不调用正常实收、库存或运输完成路径。 */
class ExceptionForceCloseTest {
private final ExceptionWorkflowRepository repository = mock(ExceptionWorkflowRepository.class);
private final ExceptionOrderService service = new ExceptionOrderService();
/** 仅接入工单仓储边界,其他业务依赖不装配以防误调用。 */
ExceptionForceCloseTest() {
ReflectionTestUtils.setField(service, "workflow", repository);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
}
/** 构造已有关联退回及历史实收的工单,历史事实必须保留。 */
private ExceptionOrder order(String type, String status) {
var order = new ExceptionOrder();
order.setId(1L); order.setCustomerId("C1"); order.setExceptionNo("EX1"); order.setVersion(3L);
order.setStatus(status); order.setExceptionType(type); order.setReturnDirection("OVERSEAS_WAREHOUSE");
order.setInventoryAdjustmentId(99L); order.setEvidenceFileIdsJson("[123]"); order.setDeliveryNo("DL1");
return order;
}
/** 各类型未结束异常无需先开始处理或上传文件,且只有工单及幂等记录写入。 */
@ParameterizedTest
@CsvSource({"RETURN,PENDING", "RETURN,PROCESSING", "REJECT,PENDING", "REJECT,PROCESSING", "DAMAGED,PENDING", "DAMAGED,PROCESSING", "OTHER,PENDING", "OTHER,PROCESSING"})
void closesEveryActiveTypeWithoutChangingPhysicalFacts(String type, String status) {
var order = order(type, status); when(repository.lock("C1", "EX1")).thenReturn(order);
assertThat(ReflectionTestUtils.<java.util.List<String>>invokeMethod(service, "allowedActions", order)).contains("FORCE_CLOSE");
clearInvocations(repository);
var result = service.forceClose("C1", "OP", "EX1", new ExceptionOrderRequests.ForceClose(3, "key", " 客户协商后人工结束 "));
assertThat(result.status()).isEqualTo("COMPLETED"); assertThat(result.version()).isEqualTo(4);
assertThat(result.restoredPackages()).isZero(); assertThat(order.getCompletedAt()).isNotNull();
assertThat(order.getHandlingAction()).isEqualTo("其他"); assertThat(order.getHandlingResult()).isEqualTo("客户协商后人工结束");
assertThat(order.getHandlerId()).isEqualTo("OP"); assertThat(order.getInventoryAdjustmentId()).isEqualTo(99L);
assertThat(order.getEvidenceFileIdsJson()).isEqualTo("[123]");
verify(repository).updateException(order);
var saved = ArgumentCaptor.forClass(ExceptionOrderCommand.class); verify(repository).insertCommand(saved.capture());
assertThat(saved.getValue().getAction()).isEqualTo("FORCE_CLOSE");
verify(repository).lock("C1", "EX1"); verify(repository).closeCommand("C1", "key"); verifyNoMoreInteractions(repository);
}
/** 空白原因直接拒绝,不读取或修改任何工单。 */
@Test void requiresNonBlankReason() {
assertThatThrownBy(() -> service.forceClose("C1", "OP", "EX1", new ExceptionOrderRequests.ForceClose(3, "key", " "))).hasMessageContaining("填写完结原因");
verifyNoInteractions(repository);
}
/** 陈旧版本、已结束及其他客户的异常不能被重新完结。 */
@Test void rejectsStaleTerminalAndForeignOrders() {
var order = order("RETURN", "PROCESSING"); when(repository.lock("C1", "EX1")).thenReturn(order);
assertThatThrownBy(() -> service.forceClose("C1", "OP", "EX1", new ExceptionOrderRequests.ForceClose(2, "key", "原因"))).hasMessageContaining("版本");
for (String status : java.util.List.of("COMPLETED", "CANCELLED")) {
order.setStatus(status);
assertThat(ReflectionTestUtils.<java.util.List<String>>invokeMethod(service, "allowedActions", order)).doesNotContain("FORCE_CLOSE");
assertThatThrownBy(() -> service.forceClose("C1", "OP", "EX1", new ExceptionOrderRequests.ForceClose(3, "key", "原因"))).hasMessageContaining("状态");
}
assertThatThrownBy(() -> service.forceClose("C2", "OP", "EX1", new ExceptionOrderRequests.ForceClose(3, "key", "原因"))).hasMessageContaining("不存在");
verify(repository, never()).updateException(any()); verify(repository, never()).insertCommand(any());
}
/** 相同请求恢复原结果,不同原因或其他工单复用同键必须冲突。 */
@Test void replaysOnceAndRejectsChangedIntent() {
var order = order("RETURN", "PROCESSING"); when(repository.lock("C1", "EX1")).thenReturn(order);
var request = new ExceptionOrderRequests.ForceClose(3, "key", "原因");
service.forceClose("C1", "OP", "EX1", request);
var saved = ArgumentCaptor.forClass(ExceptionOrderCommand.class); verify(repository).insertCommand(saved.capture());
when(repository.closeCommand("C1", "key")).thenReturn(saved.getValue());
assertThat(service.forceClose("C1", "OP", "EX1", request).replayed()).isTrue();
assertThatThrownBy(() -> service.forceClose("C1", "OP", "EX1", new ExceptionOrderRequests.ForceClose(3, "key", "其他原因"))).hasMessageContaining("幂等");
var other = order("OTHER", "PENDING"); other.setId(2L); when(repository.lock("C1", "EX2")).thenReturn(other);
assertThatThrownBy(() -> service.forceClose("C1", "OP", "EX2", request)).hasMessageContaining("其他异常单");
verify(repository, times(1)).updateException(any()); verify(repository, times(1)).insertCommand(any());
}
}