修复已入库费用发送状态回退

This commit is contained in:
2026-08-08 09:53:39 +08:00
parent 2779bf9ff7
commit 0c37d40cf1
4 changed files with 78 additions and 11 deletions
@@ -243,13 +243,16 @@ public class FbaPricingRepository {
.set(InboundFeeEntity::getSentAt, now));
}
public int markInboundWaitingPallet(long tenantId, long inboundId) {
return inbounds.update(null, new LambdaUpdateWrapper<InboundPricingEntity>()
public int markInboundFeeSent(long tenantId, long inboundId, String processStatus) {
LambdaUpdateWrapper<InboundPricingEntity> update = new LambdaUpdateWrapper<InboundPricingEntity>()
.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,
@@ -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) {
@@ -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<LambdaUpdateWrapper<InboundPricingEntity>> 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<LambdaUpdateWrapper<InboundPricingEntity>> 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<Object> queryValues(Long tenantId, String ruleCode) {
when(rules.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of());
@@ -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() {