diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java index 33e3cb2a8..ba8af670b 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java @@ -243,13 +243,16 @@ public class FbaPricingRepository { .set(InboundFeeEntity::getSentAt, now)); } - public int markInboundWaitingPallet(long tenantId, long inboundId) { - return inbounds.update(null, new LambdaUpdateWrapper() + public int markInboundFeeSent(long tenantId, long inboundId, String processStatus) { + LambdaUpdateWrapper update = new LambdaUpdateWrapper() .eq(InboundPricingEntity::getTenantId, tenantId).eq(InboundPricingEntity::getId, inboundId) - .eq(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING") + .eq(InboundPricingEntity::getProcessStatus, processStatus) .eq(InboundPricingEntity::getFeeStatus, "CONFIRMED") - .set(InboundPricingEntity::getProcessStatus, "WAIT_PALLET") - .set(InboundPricingEntity::getFeeStatus, "SENT")); + .set(InboundPricingEntity::getFeeStatus, "SENT"); + if ("FEE_CONFIRMING".equals(processStatus)) { + update.set(InboundPricingEntity::getProcessStatus, "WAIT_PALLET"); + } + return inbounds.update(null, update); } public InboundPricingIdempotencyEntity findIdempotency(long tenantId, long operatorId, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java index ab85557bf..f439ead5e 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java @@ -113,14 +113,16 @@ public class FbaPricingService { String hash = hash("SEND", inboundId, current.getId().toString()); InboundPricingIdempotencyEntity token = beginIdempotency( tenantId, operatorId, inboundId, "FEE_SEND", idempotencyKey, hash); + String nextProcessStatus = "INBOUNDED".equals(inbound.getProcessStatus()) + ? "INBOUNDED" : "WAIT_PALLET"; if ("COMPLETED".equals(token.getStatus())) { feeDetails.publishFreight(tenantId, inboundId, token.getResultResourceId(), operatorId, null, LocalDateTime.now(clock)); - return response(tenantId, token.getResultResourceId(), "WAIT_PALLET"); + return response(tenantId, token.getResultResourceId(), nextProcessStatus); } LocalDateTime now = LocalDateTime.now(clock); if (repository.sendFee(tenantId, current.getId(), operatorId, now) != 1 - || repository.markInboundWaitingPallet(tenantId, inboundId) != 1) { + || repository.markInboundFeeSent(tenantId, inboundId, inbound.getProcessStatus()) != 1) { throw conflict("费用未确认或入库单状态已变化,不能发送"); } feeDetails.publishFreight(tenantId, inboundId, current.getId(), operatorId, null, now); @@ -135,7 +137,7 @@ public class FbaPricingService { event.setCreatedAt(now); repository.insertOutbox(event); complete(token, current.getId(), now); - return response(tenantId, current.getId(), "WAIT_PALLET"); + return response(tenantId, current.getId(), nextProcessStatus); } public FeeSnapshotResponse latest(long tenantId, long inboundId) { diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingRepositoryRuleListTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingRepositoryRuleListTest.java index 454cfe888..be9fd8b35 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingRepositoryRuleListTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingRepositoryRuleListTest.java @@ -1,6 +1,7 @@ package com.baiyue.firstleg.repository; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.conditions.AbstractWrapper; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.MybatisConfiguration; @@ -150,6 +151,39 @@ class FbaPricingRepositoryRuleListTest { assertThat(params(wrapper.getValue()).values()).contains(10001L, 88L); } + @Test + void feeSendFromInboundedOnlyChangesFeeStatus() { + when(pricing.update(org.mockito.ArgumentMatchers.isNull(), org.mockito.ArgumentMatchers.any())) + .thenReturn(1); + + assertThat(repository.markInboundFeeSent(10001L, 88L, "INBOUNDED")).isEqualTo(1); + + @SuppressWarnings("unchecked") + ArgumentCaptor> wrapper = + ArgumentCaptor.forClass(LambdaUpdateWrapper.class); + verify(pricing).update(org.mockito.ArgumentMatchers.isNull(), wrapper.capture()); + assertThat(wrapper.getValue().getSqlSet()).contains("feeStatus").doesNotContain("processStatus"); + assertThat(wrapper.getValue().getCustomSqlSegment()).contains("tenantId", "id", "processStatus", "feeStatus"); + assertThat(params(wrapper.getValue()).values()).contains(10001L, 88L, "INBOUNDED", "CONFIRMED", "SENT"); + } + + @Test + void feeSendFromLegacyFlowAdvancesToWaitingPallet() { + when(pricing.update(org.mockito.ArgumentMatchers.isNull(), org.mockito.ArgumentMatchers.any())) + .thenReturn(1); + + assertThat(repository.markInboundFeeSent(10001L, 88L, "FEE_CONFIRMING")).isEqualTo(1); + + @SuppressWarnings("unchecked") + ArgumentCaptor> wrapper = + ArgumentCaptor.forClass(LambdaUpdateWrapper.class); + verify(pricing).update(org.mockito.ArgumentMatchers.isNull(), wrapper.capture()); + assertThat(wrapper.getValue().getSqlSet()).contains("feeStatus").contains("processStatus"); + assertThat(wrapper.getValue().getCustomSqlSegment()).contains("tenantId", "id", "processStatus", "feeStatus"); + assertThat(params(wrapper.getValue()).values()) + .contains(10001L, 88L, "FEE_CONFIRMING", "CONFIRMED", "SENT", "WAIT_PALLET"); + } + private Collection queryValues(Long tenantId, String ruleCode) { when(rules.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of()); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java index 9c002914a..6585f5a44 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java @@ -203,13 +203,15 @@ class FbaPricingServiceTest { @DisplayName("重复发送只返回首次结果且不会重复写Outbox") void sendIsIdempotentAndDoesNotDuplicateOutbox() { InboundPricingEntity inbound = inbound("10", "1"); + inbound.setProcessStatus("FEE_CONFIRMING"); + inbound.setFeeStatus("CONFIRMED"); InboundFeeEntity confirmed = savedFee("CONFIRMED"); when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound); when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(confirmed); when(repository.findIdempotency(TENANT, OPERATOR, "FEE_SEND", "send-once")) .thenReturn(null, completed("FEE_SEND", 501L)); when(repository.sendFee(TENANT, 501L, OPERATOR, NOW)).thenReturn(1); - when(repository.markInboundWaitingPallet(TENANT, INBOUND)).thenReturn(1); + when(repository.markInboundFeeSent(TENANT, INBOUND, "FEE_CONFIRMING")).thenReturn(1); when(repository.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1); when(repository.findFee(TENANT, 501L)).thenReturn(confirmed); InboundFeeItemEntity item = new InboundFeeItemEntity(); item.setUnitPrice(new BigDecimal("2.00")); @@ -217,14 +219,40 @@ class FbaPricingServiceTest { doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(900L); return null; }) .when(repository).insertIdempotency(any()); - service.send(TENANT, OPERATOR, INBOUND, "send-once"); - service.send(TENANT, OPERATOR, INBOUND, "send-once"); + FeeSnapshotResponse first = service.send(TENANT, OPERATOR, INBOUND, "send-once"); + FeeSnapshotResponse repeated = service.send(TENANT, OPERATOR, INBOUND, "send-once"); + assertThat(first.nextProcessStatus()).isEqualTo("WAIT_PALLET"); + assertThat(repeated.nextProcessStatus()).isEqualTo("WAIT_PALLET"); verify(repository, times(1)).insertOutbox(any()); verify(repository, times(1)).sendFee(TENANT, 501L, OPERATOR, NOW); verify(feeDetails, times(2)).publishFreight(TENANT, INBOUND, 501L, OPERATOR, null, NOW); } + @Test + @DisplayName("已入库费用发送只发布费用且不回退主流程状态") + void sendsFeeAfterInboundWithoutRollingBackProcessStatus() { + InboundPricingEntity inbound = inbound("10", "1"); + inbound.setProcessStatus("INBOUNDED"); + inbound.setFeeStatus("CONFIRMED"); + InboundFeeEntity confirmed = savedFee("CONFIRMED"); + when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound); + when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(confirmed); + when(repository.findIdempotency(TENANT, OPERATOR, "FEE_SEND", "send-inbounded")).thenReturn(null); + doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(900L); return null; }) + .when(repository).insertIdempotency(any()); + when(repository.sendFee(TENANT, 501L, OPERATOR, NOW)).thenReturn(1); + when(repository.markInboundFeeSent(TENANT, INBOUND, "INBOUNDED")).thenReturn(1); + when(repository.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1); + when(repository.findFee(TENANT, 501L)).thenReturn(confirmed); + + FeeSnapshotResponse response = service.send(TENANT, OPERATOR, INBOUND, "send-inbounded"); + + assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED"); + verify(repository).markInboundFeeSent(TENANT, INBOUND, "INBOUNDED"); + verify(repository).insertOutbox(any()); + } + @Test @DisplayName("同一幂等键用于不同试算时点会冲突") void rejectsReusedIdempotencyKeyWithDifferentRequest() {