格式修改

This commit is contained in:
2026-08-05 10:35:07 +08:00
parent 81c9c92f5d
commit 496fc58a90
241 changed files with 5600 additions and 2088 deletions
@@ -10,9 +10,14 @@ import com.baiyue.firstleg.entity.InboundFeeItemEntity;
import com.baiyue.firstleg.entity.InboundPricingEntity;
import com.baiyue.firstleg.entity.InboundPricingIdempotencyEntity;
import com.baiyue.firstleg.repository.FbaPricingRepository;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.math.BigDecimal;
import java.time.Clock;
@@ -35,16 +40,19 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@DisplayName("FBA费用状态机")
@SpringJUnitConfig(classes = {FbaPricingService.class, FbaPricingServiceTest.TestConfig.class})
class FbaPricingServiceTest {
private static final long TENANT = 10001L;
private static final long OPERATOR = 20001L;
private static final long INBOUND = 30001L;
private static final LocalDateTime NOW = LocalDateTime.of(2026, 8, 3, 10, 0);
@MockBean private FbaPricingRepository repository;
@Resource private FbaPricingService service;
@Test
@DisplayName("包税KG按配置系数取体积重较大值并按规则向上舍入")
void calculatesTaxIncludedKgWithConfiguredFactorAndPrecision() {
FbaPricingRepository repository = readyRepository();
readyRepository();
InboundPricingEntity inbound = inbound("100.000", "1.0001");
CustomerChargeRuleEntity rule = rule("KG", "167.000000", 3, "CEILING");
CarrierFbaRateEntity rate = rate("KG", "2.345678", "10.00");
@@ -52,7 +60,7 @@ class FbaPricingServiceTest {
when(repository.matchRules(inbound, NOW)).thenReturn(List.of(rule));
when(repository.matchRates(inbound, "KG", NOW)).thenReturn(List.of(rate));
FeeSnapshotResponse response = service(repository).calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-1");
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-1");
assertThat(response.convertedQuantity()).isEqualByComparingTo("167.017");
assertThat(response.chargeableQuantity()).isEqualByComparingTo("167.017");
@@ -64,7 +72,7 @@ class FbaPricingServiceTest {
@Test
@DisplayName("不包税CBM按配置系数折算且精度边界向上取整")
void calculatesTaxExcludedCbmAtRoundingBoundary() {
FbaPricingRepository repository = readyRepository();
readyRepository();
InboundPricingEntity inbound = inbound("363.001", "0.5000");
CustomerChargeRuleEntity rule = rule("CBM", "363.000000", 4, "CEILING");
CarrierFbaRateEntity rate = rate("CBM", "100.000000", "0.00");
@@ -72,7 +80,7 @@ class FbaPricingServiceTest {
when(repository.matchRules(inbound, NOW)).thenReturn(List.of(rule));
when(repository.matchRates(inbound, "CBM", NOW)).thenReturn(List.of(rate));
FeeSnapshotResponse response = service(repository).calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-2");
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-2");
assertThat(response.convertedQuantity()).isEqualByComparingTo("1.0001");
assertThat(response.chargeableQuantity()).isEqualByComparingTo("1.0001");
@@ -82,12 +90,12 @@ class FbaPricingServiceTest {
@Test
@DisplayName("审核未通过的测量快照禁止计费")
void rejectsUnapprovedMeasurementSnapshot() {
FbaPricingRepository repository = readyRepository();
readyRepository();
InboundPricingEntity inbound = inbound("10", "1");
inbound.setMeasureReviewStatus("REJECTED");
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
assertThatThrownBy(() -> service(repository).calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-3"))
assertThatThrownBy(() -> service.calculate(TENANT, OPERATOR, INBOUND, NOW, "calc-3"))
.isInstanceOf(BizException.class)
.hasMessageContaining("审核通过");
verify(repository, never()).matchRules(any(), any());
@@ -96,25 +104,25 @@ class FbaPricingServiceTest {
@Test
@DisplayName("无客户规则和多客户规则都明确拒绝")
void rejectsMissingAndAmbiguousRules() {
FbaPricingRepository missing = readyRepository();
readyRepository();
InboundPricingEntity inbound = inbound("10", "1");
when(missing.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(missing.matchRules(inbound, NOW)).thenReturn(List.of());
assertThatThrownBy(() -> service(missing).calculate(TENANT, OPERATOR, INBOUND, NOW, "none"))
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(repository.matchRules(inbound, NOW)).thenReturn(List.of());
assertThatThrownBy(() -> service.calculate(TENANT, OPERATOR, INBOUND, NOW, "none"))
.isInstanceOf(BizException.class).hasMessageContaining("未匹配到有效客户计费规则");
FbaPricingRepository ambiguous = readyRepository();
when(ambiguous.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(ambiguous.matchRules(inbound, NOW)).thenReturn(List.of(
org.mockito.Mockito.reset(repository);
readyRepository();
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
when(repository.matchRules(inbound, NOW)).thenReturn(List.of(
rule("KG", "167", 3, "CEILING"), rule("KG", "168", 3, "CEILING")));
assertThatThrownBy(() -> service(ambiguous).calculate(TENANT, OPERATOR, INBOUND, NOW, "many"))
assertThatThrownBy(() -> service.calculate(TENANT, OPERATOR, INBOUND, NOW, "many"))
.isInstanceOf(BizException.class).hasMessageContaining("多条有效客户计费规则");
}
@Test
@DisplayName("同一幂等键重复试算直接返回原费用快照")
void returnsOriginalSnapshotForRepeatedCalculation() {
FbaPricingRepository repository = mock(FbaPricingRepository.class);
InboundPricingIdempotencyEntity completed = completed("FEE_CALCULATE", 501L);
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_CALCULATE", "same")).thenReturn(completed);
when(repository.findFee(TENANT, 501L)).thenReturn(savedFee("CALCULATED"));
@@ -122,7 +130,7 @@ class FbaPricingServiceTest {
InboundFeeItemEntity item = new InboundFeeItemEntity(); item.setUnitPrice(new BigDecimal("2.00"));
when(repository.findBaseItem(TENANT, 501L)).thenReturn(item);
FeeSnapshotResponse response = service(repository).calculate(TENANT, OPERATOR, INBOUND, NOW, "same");
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "same");
assertThat(response.feeId()).isEqualTo(501L);
verify(repository, never()).matchRules(any(), any());
@@ -132,7 +140,6 @@ class FbaPricingServiceTest {
@Test
@DisplayName("重复发送只返回首次结果且不会重复写Outbox")
void sendIsIdempotentAndDoesNotDuplicateOutbox() {
FbaPricingRepository repository = mock(FbaPricingRepository.class);
InboundPricingEntity inbound = inbound("10", "1");
InboundFeeEntity confirmed = savedFee("CONFIRMED");
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
@@ -148,7 +155,6 @@ class FbaPricingServiceTest {
doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(900L); return null; })
.when(repository).insertIdempotency(any());
FbaPricingService service = service(repository);
service.send(TENANT, OPERATOR, INBOUND, "send-once");
service.send(TENANT, OPERATOR, INBOUND, "send-once");
@@ -159,12 +165,11 @@ class FbaPricingServiceTest {
@Test
@DisplayName("同一幂等键用于不同试算时点会冲突")
void rejectsReusedIdempotencyKeyWithDifferentRequest() {
FbaPricingRepository repository = mock(FbaPricingRepository.class);
InboundPricingIdempotencyEntity completed = completed("FEE_CALCULATE", 501L);
completed.setRequestHash("different");
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_CALCULATE", "same")).thenReturn(completed);
assertThatThrownBy(() -> service(repository).calculate(TENANT, OPERATOR, INBOUND, NOW, "same"))
assertThatThrownBy(() -> service.calculate(TENANT, OPERATOR, INBOUND, NOW, "same"))
.isInstanceOf(BizException.class)
.extracting(exception -> ((BizException) exception).getResultCode())
.isEqualTo(ResultCode.CONFLICT);
@@ -173,7 +178,6 @@ class FbaPricingServiceTest {
@Test
@DisplayName("已发布费用可生成新版本且不回退已入库状态")
void adjustsPublishedFeeWithoutRollingBackInbound() {
FbaPricingRepository repository = mock(FbaPricingRepository.class);
InboundPricingEntity inbound = inbound("10", "1");
inbound.setProcessStatus("INBOUNDED");
inbound.setFeeStatus("SENT");
@@ -195,7 +199,7 @@ class FbaPricingServiceTest {
when(repository.completeIdempotency(any(), eq(502L), eq(NOW))).thenReturn(1);
when(repository.findFee(TENANT, 502L)).thenAnswer(invocation -> inserted.get());
FeeSnapshotResponse response = service(repository).adjustPublished(TENANT, OPERATOR, INBOUND,
FeeSnapshotResponse response = service.adjustPublished(TENANT, OPERATOR, INBOUND,
new com.baiyue.firstleg.dto.FbaPricingDtos.AdjustFeeRequest(
new BigDecimal("100.00"), new BigDecimal("20.00"), "客服修正"), "adjust-1");
@@ -208,8 +212,7 @@ class FbaPricingServiceTest {
verify(repository).insertOutbox(any());
}
private FbaPricingRepository readyRepository() {
FbaPricingRepository repository = mock(FbaPricingRepository.class);
private void readyRepository() {
when(repository.findIdempotency(anyLong(), anyLong(), any(), any())).thenReturn(null);
doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(900L); return null; })
.when(repository).insertIdempotency(any());
@@ -218,12 +221,14 @@ class FbaPricingServiceTest {
when(repository.nextFeeVersion(TENANT, INBOUND)).thenReturn(1);
when(repository.markInboundCalculated(eq(TENANT), eq(INBOUND), any(), any())).thenReturn(1);
when(repository.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1);
return repository;
}
private FbaPricingService service(FbaPricingRepository repository) {
return new FbaPricingService(repository,
Clock.fixed(Instant.parse("2026-08-03T02:00:00Z"), ZoneId.of("Asia/Shanghai")));
@Configuration
static class TestConfig {
@Bean
Clock clock() {
return Clock.fixed(Instant.parse("2026-08-03T02:00:00Z"), ZoneId.of("Asia/Shanghai"));
}
}
private InboundPricingEntity inbound(String weight, String volume) {