diff --git a/ruoyi-admin/src/main/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImpl.java b/ruoyi-admin/src/main/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImpl.java index 8033931..7ff3613 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImpl.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImpl.java @@ -1,6 +1,8 @@ package com.ruoyi.out.outStockout.service.impl; import cn.hutool.core.util.StrUtil; +import com.ruoyi.algorithm.shadow.OutboundShadowObservation; +import com.ruoyi.algorithm.shadow.ShadowObservationGateway; import com.ruoyi.api.service.IToWcsService; import com.ruoyi.bas.basJobId.service.IBasJobIdService; import com.ruoyi.common.annotation.DataScope; @@ -14,6 +16,7 @@ import com.ruoyi.out.outStockout.service.IOutStockoutService; import com.ruoyi.out.outStockoutDetail.domain.OutStockoutDetail; import com.ruoyi.out.outStockoutDetail.mapper.OutStockoutDetailMapper; import com.ruoyi.out.outStockoutDetail.service.IOutStockoutDetailService; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; @@ -32,6 +35,7 @@ import java.util.List; * @date 2025-05-20 */ @Service +@Slf4j public class OutStockoutServiceImpl implements IOutStockoutService { @Autowired @@ -45,6 +49,8 @@ public class OutStockoutServiceImpl implements IOutStockoutService private IOutStockoutDetailService outStockoutDetailService; @Autowired private IToWcsService toWcsService; + @Autowired + private ShadowObservationGateway shadowObservationGateway; /** * 查询出库主单 @@ -182,7 +188,10 @@ public class OutStockoutServiceImpl implements IOutStockoutService map.put("v_stockout_detail_id", detail.getStockoutDetailId()); map.put("v_result", null); map.put("v_resultmsg", null); + OutboundShadowObservation shadowObservation = + planProjectOutboundShadow(detail.getStockoutDetailId()); outStockoutMapper.autoAllot(map); + compareOutboundShadow(shadowObservation, map.get("v_result")); if (!"10".equals(map.get("v_result"))){ stringBuilder.append(",").append(detail.getStockoutDetailId()+map.get("v_resultmsg")); } @@ -209,7 +218,10 @@ public class OutStockoutServiceImpl implements IOutStockoutService for (String stock : stockList) { map.put("v_stockout_detail_id", stockoutDetailId); map.put("v_stock_id", stock); + OutboundShadowObservation shadowObservation = + planExactStockOutboundShadow(stockoutDetailId, stock); outStockoutMapper.manualAllot(map); + compareOutboundShadow(shadowObservation, map.get("v_result")); //toWcsService.pushOutMydoJob(); if (!"10".equals(map.get("v_result"))){ stringBuilder.append(",").append(stockoutDetailId+map.get("v_resultString")); @@ -221,6 +233,35 @@ public class OutStockoutServiceImpl implements IOutStockoutService return AjaxResult.success(); } + private OutboundShadowObservation planProjectOutboundShadow(String stockoutDetailId) { + try { + return shadowObservationGateway.planProjectOutbound(stockoutDetailId); + } catch (RuntimeException exception) { + log.warn("Project outbound shadow observation failed open for detail {}: {}", + stockoutDetailId, exception.getMessage()); + return OutboundShadowObservation.empty(); + } + } + + private OutboundShadowObservation planExactStockOutboundShadow(String stockoutDetailId, String stockId) { + try { + return shadowObservationGateway.planExactStockOutbound(stockoutDetailId, stockId); + } catch (RuntimeException exception) { + log.warn("Exact-stock shadow observation failed open for detail {} stock {}: {}", + stockoutDetailId, stockId, exception.getMessage()); + return OutboundShadowObservation.empty(); + } + } + + private void compareOutboundShadow(OutboundShadowObservation observation, String legacyResultCode) { + try { + shadowObservationGateway.compareOutbound(observation, legacyResultCode); + } catch (RuntimeException exception) { + log.warn("Outbound shadow comparison failed open for request {}: {}", + observation == null ? null : observation.requestId(), exception.getMessage()); + } + } + @Override public void updateOutStockouts(OutStockout mainOut) { outStockoutMapper.updateOutStockouts(mainOut); diff --git a/ruoyi-admin/src/test/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImplShadowTest.java b/ruoyi-admin/src/test/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImplShadowTest.java new file mode 100644 index 0000000..6507910 --- /dev/null +++ b/ruoyi-admin/src/test/java/com/ruoyi/out/outStockout/service/impl/OutStockoutServiceImplShadowTest.java @@ -0,0 +1,127 @@ +package com.ruoyi.out.outStockout.service.impl; + +import com.ruoyi.algorithm.shadow.OutboundShadowObservation; +import com.ruoyi.algorithm.shadow.ShadowObservationGateway; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.out.outStockout.domain.OutStockout; +import com.ruoyi.out.outStockout.mapper.OutStockoutMapper; +import com.ruoyi.out.outStockoutDetail.domain.OutStockoutDetail; +import com.ruoyi.out.outStockoutDetail.service.IOutStockoutDetailService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InOrder; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class OutStockoutServiceImplShadowTest { + @Mock + private OutStockoutMapper outStockoutMapper; + @Mock + private IOutStockoutDetailService outStockoutDetailService; + @Mock + private ShadowObservationGateway shadowObservationGateway; + @InjectMocks + private OutStockoutServiceImpl service; + + private OutStockoutDetail pendingDetail; + + @BeforeEach + void setUp() { + pendingDetail = new OutStockoutDetail(); + pendingDetail.setStockoutDetailId("DETAIL-1"); + pendingDetail.setQuantity(BigDecimal.TEN); + pendingDetail.setAllotQuantity(BigDecimal.ZERO); + } + + @Test + void missingShadowProjectStillRunsAutoAllotAndPreservesLegacyWarning() { + prepareAutoAllot(); + OutboundShadowObservation missingProject = OutboundShadowObservation.unavailable( + "W01", "DETAIL-1", "SKIPPED_MISSING_PROJECT", "missing project"); + when(shadowObservationGateway.planProjectOutbound("DETAIL-1")).thenReturn(missingProject); + doAnswer(invocation -> { + @SuppressWarnings("unchecked") + HashMap parameters = invocation.getArgument(0); + parameters.put("v_result", "20"); + parameters.put("v_resultmsg", "LEGACY-WARNING"); + return null; + }).when(outStockoutMapper).autoAllot(any()); + + AjaxResult result = service.autoAllot("OUT-1"); + + assertThat(result.get(AjaxResult.MSG_TAG)).isEqualTo(",DETAIL-1LEGACY-WARNING"); + InOrder order = inOrder(shadowObservationGateway, outStockoutMapper); + order.verify(shadowObservationGateway).planProjectOutbound("DETAIL-1"); + order.verify(outStockoutMapper).autoAllot(any()); + order.verify(shadowObservationGateway).compareOutbound(missingProject, "20"); + verify(outStockoutMapper, times(1)).autoAllot(any()); + } + + @Test + void shadowExceptionStillRunsAutoAllotAndPreservesLegacySuccess() { + prepareAutoAllot(); + when(shadowObservationGateway.planProjectOutbound("DETAIL-1")) + .thenThrow(new IllegalStateException("shadow unavailable")); + doAnswer(invocation -> { + @SuppressWarnings("unchecked") + HashMap parameters = invocation.getArgument(0); + parameters.put("v_result", "10"); + parameters.put("v_resultmsg", "ok"); + return null; + }).when(outStockoutMapper).autoAllot(any()); + + AjaxResult result = service.autoAllot("OUT-1"); + + assertThat(result.isSuccess()).isTrue(); + verify(shadowObservationGateway).planProjectOutbound("DETAIL-1"); + verify(outStockoutMapper, times(1)).autoAllot(any()); + } + + @Test + void missingShadowStockStillRunsManualAllotOncePerSelectedStock() { + OutboundShadowObservation missingStock = OutboundShadowObservation.unavailable( + "W01", "DETAIL-1", "SKIPPED_MISSING_STOCK", "missing stock"); + when(shadowObservationGateway.planExactStockOutbound("DETAIL-1", "STOCK-1")) + .thenReturn(missingStock); + when(shadowObservationGateway.planExactStockOutbound("DETAIL-1", "STOCK-2")) + .thenReturn(missingStock); + doAnswer(invocation -> { + @SuppressWarnings("unchecked") + HashMap parameters = invocation.getArgument(0); + parameters.put("v_result", "10"); + parameters.put("v_resultString", "ok"); + return null; + }).when(outStockoutMapper).manualAllot(any()); + + AjaxResult result = service.manualAllot(new String[]{"STOCK-1", "STOCK-2"}, "DETAIL-1"); + + assertThat(result.isSuccess()).isTrue(); + verify(outStockoutMapper, times(2)).manualAllot(any()); + verify(shadowObservationGateway).planExactStockOutbound("DETAIL-1", "STOCK-1"); + verify(shadowObservationGateway).planExactStockOutbound("DETAIL-1", "STOCK-2"); + verify(shadowObservationGateway, times(2)).compareOutbound(missingStock, "10"); + } + + private void prepareAutoAllot() { + OutStockout bill = new OutStockout(); + bill.setStatus("2"); + when(outStockoutMapper.selectOutStockoutByStockoutId("OUT-1")).thenReturn(bill); + when(outStockoutDetailService.selectOutStockoutDetailList(any())) + .thenReturn(List.of(pendingDetail)); + } +}